Cloudflare Agents tracing with Future AGI
Trace Cloudflare Agents SDK HTTP turns in Future AGI using Workers tracing and an OpenTelemetry Collector.
Trace a Cloudflare Agents SDK agent in Future AGI using Workers tracing, wrapAISDK, and an OpenTelemetry Collector you operate. The Worker sends traces through a Cloudflare destination to the collector; the collector maps and filters them before forwarding them to Future AGI. Future AGI keys stay in the collector, not the Worker.
This example covers awaited HTTP generateText turns using the pinned packages below. Streaming, WebSocket, RPC, scheduled and background turns, human tool approvals and sub-agents are outside its scope. Do not add @traceai/vercel: use Cloudflare’s wrapAISDK for AI SDK tracing on Workers.
Warning
Exporting traces to an OTLP destination requires a Workers Paid subscription. Check Cloudflare observability pricing before enabling export. Run the collector on a public HTTPS endpoint protected by the bearer token shown below.
1. Install
npm install --save-exact agents@0.28.1 ai@7.0.142 @ai-sdk/openai@4.0.95 zod@4.6.7
npm install --save-dev --save-exact wrangler@4.151.1
wrapAISDK ships in agents (agents/observability/ai). This recipe uses the pinned AI SDK v7 and OpenAI provider above.
2. Trace the AI SDK inside your Agent
Wrap the AI SDK namespace and call the wrapped generateText. Give the turn an agent identity, so the spans carry gen_ai.agent.name, gen_ai.agent.id and gen_ai.conversation.id.
// A Cloudflare Agents SDK agent (npm `agents`, class `Agent`) whose turns are traced by Workers tracing.
//
// Nothing in this file talks to Future AGI. The Worker only creates spans; Cloudflare exports them to the
// OTLP destination named in wrangler.jsonc, and your collector (otelcol.yaml) forwards them to Future AGI.
//
// Agent (agent_start, agent_initialization, ... spans) emitted by `agents` itself
// wrapAISDK(ai) (invoke_agent, chat, execute_tool, tool_approval) emitted by agents/observability/ai
//
// A plain Agent does not trace model or tool calls on its own: without wrapAISDK you get the lifecycle spans
// only, with no model, token or tool spans.
import { tracing } from "cloudflare:workers";
import { Agent, routeAgentRequest } from "agents";
import { wrapAISDK } from "agents/observability/ai";
import { createOpenAI } from "@ai-sdk/openai";
import * as ai from "ai";
import { z } from "zod";
type Env = {
WeatherAgent: DurableObjectNamespace;
OPENAI_API_KEY: string; // secret: `wrangler secret put OPENAI_API_KEY`
OPENAI_BASE_URL?: string; // optional, any OpenAI-compatible endpoint
MODEL?: string;
// Cloudflare's payload switches, both off unless set to "true". Off: no message text, tool arguments or
// tool results are written to spans at all, so nothing of it leaves Cloudflare.
STORE_MESSAGES?: string;
STORE_TOOLS?: string;
};
// One instance (Durable Object) per conversation: POST /agents/weather-agent/<conversation> {"question": "..."}
export class WeatherAgent extends Agent<Env> {
async onRequest(request: Request): Promise<Response> {
if (request.method !== "POST") return new Response("POST {\"question\": \"...\"}", { status: 405 });
const { question } = (await request.json()) as { question?: string };
if (!question) return new Response("question is required", { status: 400 });
const { generateText } = wrapAISDK(ai, {
storeMessages: this.env.STORE_MESSAGES === "true",
storeTools: this.env.STORE_TOOLS === "true",
});
const openai = createOpenAI({ apiKey: this.env.OPENAI_API_KEY, baseURL: this.env.OPENAI_BASE_URL });
try {
// Await the turn so its spans end before this invocation finishes.
const result = await generateText({
model: openai.chat(this.env.MODEL ?? "gpt-4o-mini"),
system: "You answer weather questions. Call get_weather once, then answer in one sentence.",
prompt: question,
tools: {
get_weather: ai.tool({
description: "Current weather for a city",
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => {
// Demo failure path: the tool throws, the turn continues, and execute_tool carries error.type.
if (city.toLowerCase().includes("atlantis")) throw new Error(`no weather station for ${city}`);
return { city, forecast: "sunny", celsius: 21 };
},
}),
},
// A server-side approval policy (no human in the loop). Cloudflare records it as a tool_approval span.
toolApproval: { get_weather: "approved" },
stopWhen: ai.isStepCount(3),
// Agent identity on invoke_agent, chat and execute_tool (gen_ai.agent.name / .id, gen_ai.conversation.id).
// The Durable Object id is the conversation: the Agent's own spans carry the same id, so one session.
runtimeContext: { agentId: this.name, conversationId: this.ctx.id.toString() },
telemetry: { functionId: "weather-agent", includeRuntimeContext: { agentId: true, conversationId: true } },
});
return Response.json({ answer: result.text, steps: result.steps.length });
} catch (error) {
// The spans already carry error.type (the error class). Do not echo provider error text to the caller.
return Response.json({ error: (error as Error)?.name ?? "Error" }, { status: 502 });
}
}
}
export default {
async fetch(request: Request, env: Env) {
const response = await routeAgentRequest(request, env, {
// Optional, recommended: put the conversation on the Worker's request span (the root of the trace), so
// Future AGI lists the whole trace under the conversation's session. Same value the Agent's spans carry.
onBeforeRequest: (_request, { name }) => {
tracing.getActiveSpan()?.setAttribute("gen_ai.conversation.id", env.WeatherAgent.idFromName(name).toString());
},
});
return response ?? new Response("Not found", { status: 404 });
},
} satisfies ExportedHandler<Env>;
STORE_MESSAGESandSTORE_TOOLSare this example’s names for Cloudflare’sstoreMessagesandstoreToolsoptions. Both default to off.conversationIdis the Durable Object id, the same value theAgentwrites on its lifecycle spans (cloudflare.agents.session.id), so a whole instance is one session. Do not put credentials, tokens or user input inruntimeContextkeys you include in telemetry: they become span attributes.- The
onBeforeRequestline is optional. It writes the conversation id on the Worker’s request span, the root of the trace, so Future AGI lists the trace under the session. Without it the spans still carry the session, but the trace row does not. toolApprovalis optional. A server-side policy (no human) is recorded as atool_approvalspan underexecute_tool.
3. Turn on tracing and name the destination
// The Worker as you deploy it.
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "weather-agent",
"main": "src/index.ts",
"compatibility_date": "2026-10-01",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": { "bindings": [{ "name": "WeatherAgent", "class_name": "WeatherAgent" }] },
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["WeatherAgent"] }],
"observability": {
"traces": {
"enabled": true,
// The name of the Traces destination you create in the Cloudflare dashboard (Workers Observability).
"destinations": ["future-agi"],
// Every turn; lower it for busy Workers.
"head_sampling_rate": 1,
// Export to your collector only, keep no copy in the Cloudflare dashboard.
"persist": false
}
}
// Secrets, never in this file: wrangler secret put OPENAI_API_KEY
// Optional vars: MODEL, OPENAI_BASE_URL, STORE_MESSAGES / STORE_TOOLS ("true" opts in to payloads).
}
destinations must match the destination name in the dashboard (step 5). With persist: false Cloudflare keeps no copy in its own dashboard. See Flushing: there is no flush call to add.
4. Run the collector
The collector accepts OTLP/HTTP from Cloudflare only with your bearer token, drops the Durable Object storage spans, sets project_name, maps span types, errors and sessions, keeps content out unless you opt in, and forwards to Future AGI. Keep these processors in place; direct export does not replace their mapping and privacy rules. One collector configuration serves one project.
# OpenTelemetry Collector (contrib) between a Cloudflare Workers OTLP traces destination and Future AGI,
# for Workers built on the Cloudflare Agents SDK (npm `agents`, `Agent` class, AI SDK wrapped with wrapAISDK).
#
# A Workers destination has a name, a type (Traces), an OTLP endpoint and optional custom headers. It has no
# field for resource attributes, and Future AGI rejects any batch whose resource has no `project_name`. This
# collector fills that gap and keeps your Future AGI keys out of Cloudflare:
# 1. accepts OTLP/HTTP from the destination, only with your bearer token (a custom header on the destination),
# 2. drops the destination's pre-flight test span and the per-call Durable Object storage spans,
# 3. sets the Observe project,
# 4. maps the Agents SDK span names onto Observe span types, errors and sessions,
# 5. keeps prompts, completions, tool arguments/results, error text and the caller's location OUT unless you opt in,
# 6. forwards plain (uncompressed) OTLP to Future AGI with the two Future AGI key headers.
#
# Use otel/opentelemetry-collector-contrib 0.162.0.
#
# Environment:
# CF_DESTINATION_TOKEN secret you invent; on the destination add the header `Authorization: Bearer <token>`
# FI_API_KEY, FI_SECRET_KEY Future AGI keys (they never go to Cloudflare)
# FI_PROJECT_NAME Observe project every Worker trace goes to
# FI_TRACES_URL full traces URL: https://api.futureagi.com/tracer/v1/traces
# or, self-hosted, http://<your-host>:<port>/v1/traces
# FI_CAPTURE_CONTENT optional, default false. `true` lets message text, tool arguments/results, error
# text and the caller's location through. Cloudflare only records messages and tool
# payloads when wrapAISDK gets storeMessages / storeTools, so content needs both switches.
extensions:
# Cloudflare sends from its network, so this endpoint must be reachable from the internet. Never leave it open.
bearertokenauth/cloudflare:
scheme: Bearer
token: ${env:CF_DESTINATION_TOKEN}
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
auth:
authenticator: bearertokenauth/cloudflare
# Cloudflare sends OTLP/HTTP JSON with Content-Encoding: gzip; this receiver accepts both by default.
# Put TLS in front of this (load balancer or reverse proxy), or set `tls:` here with cert_file and key_file.
processors:
# When you create a destination, Cloudflare sends one test span ("Hello from Cloudflare ...", resource
# telemetry.sdk.name=pre-flight-check) to check the endpoint. The 200 answers it; the span itself is not forwarded.
filter/preflight:
error_mode: ignore
traces:
span:
- resource.attributes["telemetry.sdk.name"] == "pre-flight-check"
# durable_object_storage_get / _put / _exec ...: one span per storage call. They are leaves (no children), so
# dropping them keeps every other parent link intact. Delete this processor to keep them.
filter/storage_spans:
error_mode: ignore
traces:
span:
- IsMatch(name, "^durable_object_storage_")
resource/future_agi:
attributes:
- key: project_name
value: ${env:FI_PROJECT_NAME}
action: upsert
- key: project_type
value: observe
action: upsert
transform/cloudflare_agents:
error_mode: ignore
trace_statements:
- context: span
statements:
# Span types. chat -> LLM and execute_tool -> TOOL are read by Future AGI directly. invoke_agent carries the
# turn's model and token totals, so it is pinned to AGENT (otherwise it would be stored as an unknown span).
# tool_approval spans reuse gen_ai.operation.name=execute_tool; they are a policy decision, not a second
# tool run, so they become chain steps. Everything else (agent_start, fetch, Durable Object invocations)
# is a chain step too.
- set(attributes["fi.span.kind"], "AGENT") where attributes["gen_ai.operation.name"] == "invoke_agent"
- set(attributes["fi.span.kind"], "CHAIN") where attributes["cloudflare.agents.operation.name"] == "tool.approval"
- set(attributes["fi.span.kind"], "CHAIN") where attributes["gen_ai.operation.name"] == nil
# invoke_agent repeats the sum of its chat spans' usage. Session and project totals add up every span, so
# keep usage on the chat spans only. The turn total stays readable as cloudflare.agents.usage.total_tokens.
- delete_matching_keys(attributes, "^gen_ai\\.usage\\.") where attributes["gen_ai.operation.name"] == "invoke_agent"
# Sessions: one Agent instance (Durable Object) is one conversation. The Agent's own lifecycle spans carry
# cloudflare.agents.session.id (the Durable Object id); wrapAISDK spans carry gen_ai.conversation.id.
- set(attributes["session.id"], attributes["cloudflare.agents.session.id"]) where attributes["cloudflare.agents.session.id"] != nil
- set(attributes["session.id"], attributes["gen_ai.conversation.id"]) where attributes["gen_ai.conversation.id"] != nil
# Errors. The Workers custom-span API has no status setter, so the Agents SDK records a failure as the
# attribute error.type (an error class name, never the message). Turn that into span status ERROR.
- set(status.message, "error") where status.message != "" and "${env:FI_CAPTURE_CONTENT:-false}" != "true"
- set(status.code, STATUS_CODE_ERROR) where attributes["error.type"] != nil
- set(status.message, attributes["error.type"]) where attributes["error.type"] != nil
# Content ON: fill the Input and Output panes from the OpenTelemetry GenAI JSON strings.
- set(attributes["input.value"], attributes["gen_ai.input.messages"]) where attributes["gen_ai.input.messages"] != nil and "${env:FI_CAPTURE_CONTENT:-false}" == "true"
- set(attributes["output.value"], attributes["gen_ai.output.messages"]) where attributes["gen_ai.output.messages"] != nil and "${env:FI_CAPTURE_CONTENT:-false}" == "true"
- set(attributes["input.value"], attributes["gen_ai.tool.call.arguments"]) where attributes["gen_ai.tool.call.arguments"] != nil and "${env:FI_CAPTURE_CONTENT:-false}" == "true"
- set(attributes["output.value"], attributes["gen_ai.tool.call.result"]) where attributes["gen_ai.tool.call.result"] != nil and "${env:FI_CAPTURE_CONTENT:-false}" == "true"
# Content OFF (the default): messages, system instructions, tool arguments and results, in every spelling.
- delete_matching_keys(attributes, "^gen_ai\\.(input\\.messages|output\\.messages|system_instructions|tool\\.call\\.arguments|tool\\.call\\.result|prompt|completion)(\\..*)?$") where "${env:FI_CAPTURE_CONTENT:-false}" != "true"
- delete_matching_keys(attributes, "^(input|output)\\.value$") where "${env:FI_CAPTURE_CONTENT:-false}" != "true"
# Request URLs: keep the path, drop scheme, host, query string and fragment (tokens often ride in queries).
# Cloudflare also sends the query string on its own as url.query.
- replace_pattern(attributes["url.full"], "^[A-Za-z][A-Za-z0-9+.-]*://[^/?#]*([^?#]*).*$", "$$1")
- delete_key(attributes, "url.query")
# The caller's approximate location (geo.locality.name is the city; also region, country, continent,
# timezone) and network (cloudflare.asn), which Cloudflare adds to request spans: out unless you opt in.
- delete_matching_keys(attributes, "^geo\\.") where "${env:FI_CAPTURE_CONTENT:-false}" != "true"
- delete_key(attributes, "cloudflare.asn") where "${env:FI_CAPTURE_CONTENT:-false}" != "true"
# E-mail-shaped values (agent instance names, URLs) are replaced in both modes.
- replace_all_patterns(attributes, "value", "[A-Za-z0-9._+-]+(@|%40)[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*\\.[A-Za-z]{2,}", "<email>")
- context: spanevent
statements:
# Exception events can carry messages, paths and stack traces: keep only the exception type.
- keep_keys(attributes, ["exception.type"]) where "${env:FI_CAPTURE_CONTENT:-false}" != "true"
# E-mail-shaped values are replaced here too, in both modes.
- replace_all_patterns(attributes, "value", "[A-Za-z0-9._+-]+(@|%40)[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*\\.[A-Za-z]{2,}", "<email>")
# Content OFF: drop every other span event. Workers attributes console.log() output to the active span, and
# Cloudflare does not document how those lines are encoded in the export, so none of them pass by default.
filter/span_events:
error_mode: ignore
traces:
spanevent:
- name != "exception" and "${env:FI_CAPTURE_CONTENT:-false}" != "true"
# Future AGI reads at most 16 MiB per request, so batching keeps requests small.
batch:
send_batch_size: 256
send_batch_max_size: 512
timeout: 2s
exporters:
otlp_http/future_agi:
traces_endpoint: ${env:FI_TRACES_URL}
encoding: proto
compression: none # optional: Future AGI also accepts gzip bodies
headers:
X-Api-Key: ${env:FI_API_KEY}
X-Secret-Key: ${env:FI_SECRET_KEY}
service:
extensions: [bearertokenauth/cloudflare]
telemetry:
metrics:
level: none # no self-metrics endpoint; this collector handles traces only
logs:
level: warn
pipelines:
traces:
receivers: [otlp]
processors: [filter/preflight, filter/storage_spans, resource/future_agi, transform/cloudflare_agents, filter/span_events, batch]
exporters: [otlp_http/future_agi]
export CF_DESTINATION_TOKEN="$(openssl rand -hex 24)" # paste into the destination header in step 5
export FI_API_KEY="your-futureagi-api-key"
export FI_SECRET_KEY="your-futureagi-secret-key"
export FI_PROJECT_NAME="weather-agent"
export FI_TRACES_URL="https://api.futureagi.com/tracer/v1/traces" # self-hosted: http://<host>:<port>/v1/traces
docker run -d --name fi-cloudflare-collector -p 4318:4318 \
-e CF_DESTINATION_TOKEN -e FI_API_KEY -e FI_SECRET_KEY -e FI_PROJECT_NAME -e FI_TRACES_URL -e FI_CAPTURE_CONTENT \
-v "$PWD/otelcol.yaml:/etc/otelcol-contrib/config.yaml:ro" \
otel/opentelemetry-collector-contrib:0.162.0
Cloudflare sends from its network, so the collector needs a public HTTPS address (TLS in front of port 4318). Cloudflare sends OTLP/HTTP JSON, gzip-compressed (Content-Encoding: gzip); the collector accepts it and forwards uncompressed protobuf.
5. Create the destination and deploy
In the Cloudflare dashboard, open Workers Observability, add a destination, and fill in:
| Field | Value |
|---|---|
| Destination Name | future-agi (the name in wrangler.jsonc) |
| Destination Type | Traces |
| OTLP Endpoint | https://<your-collector-host>/v1/traces |
| Custom Headers | Authorization: Bearer <CF_DESTINATION_TOKEN> |
When you save the destination, Cloudflare sends one test span (Hello from Cloudflare 👋 (<destination name>)) to check the endpoint. The collector answers it and does not forward it, so it never appears as a trace.
You can also create the destination with the Cloudflare API. The API token needs Workers Observability Write. <TOKEN> is a Cloudflare API token, <CF_DESTINATION_TOKEN> your collector’s bearer token:
curl -X POST "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/workers/observability/destinations" \
-H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
-d '{"name": "future-agi", "enabled": true, "configuration": {"type": "logpush",
"logpushDataset": "opentelemetry-traces", "url": "https://<your-collector-host>/v1/traces",
"headers": {"Authorization": "Bearer <CF_DESTINATION_TOKEN>"}}}'
A GET on the same URL lists your destinations; configuration.jobStatus shows the last delivery (last_complete) and the last error.
Then deploy, with the model key as a secret binding (never a literal in wrangler.jsonc):
npx wrangler secret put OPENAI_API_KEY
npx wrangler deploy
Cloudflare may take several minutes to deliver spans. Create a Traces destination, not a Logs destination: this collector pipeline accepts traces only.
6. Run a turn and check the trace
Send a request to your deployed Worker, replacing the host with your Worker URL:
curl -X POST "https://<your-worker-host>/agents/weather-agent/demo" \
-H "Content-Type: application/json" \
-d '{"question":"What is the weather in Paris?"}'
Open the project named in FI_PROJECT_NAME under Observe > Tracing. Each request to the Agent is one trace named POST. Expand it down to invoke_agent. Requests to the same Agent instance share a session.
What is captured
| Span | Type in Future AGI | What it carries |
|---|---|---|
invoke_agent <functionId> | agent | agent name/id, conversation id, requested model, provider, finish reason, tool count, cloudflare.agents.usage.total_tokens. Its gen_ai.usage.* copies are removed by the collector, so sessions do not count a turn’s tokens twice. |
chat <model> | llm | requested and response model, provider, response id, input/output/cache/reasoning token counts. Cost appears when Future AGI knows the model’s price. |
execute_tool <tool> | tool | tool name, type and call id |
tool_approval <tool> | chain | approval state (approved, denied or requested) and call id. Cloudflare marks it gen_ai.operation.name=execute_tool; the collector keeps it from counting as a second tool run. |
agent_initialization, agent_start, restore_agent_state, schedule_agent_alarm, … | chain | Agent lifecycle: class name, Durable Object id and instance name |
POST, fetch, durable_object_subrequest | chain | Workers runtime: trigger, method, URL path, status code, body sizes, a few request headers (accept encoding, content type, content length), the user agent, server.address (the Worker’s or the model provider’s host name), the Cloudflare data center (cloudflare.colo), ray id, CPU and wall time. The caller’s location (geo.*, city level) and network (cloudflare.asn) are removed unless you opt in; the query string (url.query) is always removed. |
durable_object_storage_* | (dropped) | one span per storage call, removed by the collector’s filter |
Resource attributes from Cloudflare: service.name and faas.name (the Worker name), faas.version (the deployed version id), cloud.provider, cloud.platform, cloudflare.script_name, cloudflare.entrypoint (the Agent class, on its Durable Object’s spans), cloudflare.colo, faas.invoked_region, telemetry.sdk.name=workers-observability. The collector adds project_name and project_type.
Sessions: the collector copies gen_ai.conversation.id (else cloudflare.agents.session.id, the Durable Object id) to session.id.
Not captured: message text, system instructions, tool arguments and results, raw error messages and console output (off by default, see below); user ids (Cloudflare records user.id only from AI SDK v6 telemetry metadata); retrieval spans; cost from Cloudflare.
Privacy
Content needs two switches, and both are off by default. Set FI_CAPTURE_CONTENT=true before creating the Docker container to opt in; remove and recreate an existing container with the command above to change this environment value. Restart alone does not update it.
| Switch | Where | Default | Effect |
|---|---|---|---|
storeMessages / storeTools | wrapAISDK(ai, {...}) in the Worker (STORE_MESSAGES / STORE_TOOLS in the example) | off | Cloudflare writes gen_ai.input.messages / gen_ai.output.messages on chat and gen_ai.tool.call.arguments / gen_ai.tool.call.result on execute_tool. Off: these never leave Cloudflare. |
FI_CAPTURE_CONTENT | collector environment | false | true lets those attributes, error text, exception details, console output and the caller’s location and network (geo.*, cloudflare.asn) through and fills the Input and Output panes. false deletes them, replaces status messages with the error class, keeps only exception.type on exception events and drops every other span event. |
So turning on Cloudflare’s payloads (for example for its own session replay) does not by itself send them to Future AGI. In both modes the collector replaces e-mail-shaped values (for example an Agent instance named after a user’s e-mail, which also appears in the URL, in gen_ai.agent.id and in the lifecycle spans) with <email>, and keeps only the path of request URLs (no host, query string or fragment). The runtime’s user agent, the Cloudflare data center and the few request headers listed above pass in both modes. Cloudflare records only the error class (error.type) on a failing span, never the error text. With storeMessages on, a tool’s error text is part of the next chat span’s gen_ai.input.messages (it is the tool result the model sees), so the collector removes it with the other messages unless you opt in.
Flushing and background turns
Workers tracing has no flush call. Cloudflare exports the spans after the invocation ends, so there is no ctx.waitUntil(flush()) to add and no exporter to shut down. What matters is that each span ends inside its invocation: the runtime force-closes a span that is still open when its invocation ends, and such a span arrives with no attributes, so it is stored without model, tokens or type (cloudflare/agents#2539). Treat that as data loss for the span.
- Await the turn in the handler, as the example does. The invocation stays open until
generateTextreturns, and every span closes with its attributes. - Background turns are outside this recipe. Retain the example’s
await.
How it behaves
- Tool throws: the turn continues (the model sees the tool error).
execute_toolis ERROR with the messageError(the class name), not the error text. - Model call fails:
chatandinvoke_agentare ERROR withAI_APICallError. The example answers HTTP 502 with the error class only. Cloudflare also marks failed model providerfetchand Worker request spans ERROR, without a message, so the trace row shows the error. - Status: the Workers custom-span API has no status setter, so the Agents SDK records failures as
error.type. The collector turns that into span status ERROR. Cloudflare sets status itself only on runtime spans: ERROR for afetchthat got a 4xx or 5xx and for a request answered with 5xx. A turn whose tool throws still answers 200, so its trace row stays UNSET; open the trace to see the failing span. - Trace name and latency: the root is the Worker’s
POST. Because the example awaits the turn, its latency is the turn’s latency. - Span kinds: Future AGI types spans from their names and attributes, not from their OpenTelemetry kind.
Troubleshooting
- No project in Future AGI. The collector sets
project_name; check its logs (docker logs fi-cloudflare-collector). A 401 in the logs means wrongFI_API_KEY/FI_SECRET_KEY. Cloudflare can take minutes to deliver, and the destination status in the dashboard (orconfiguration.jobStatusin the API) shows the last delivery or an error. - Destination shows an error. The endpoint must be the full
/v1/tracesURL on a public HTTPS address, and the header must be exactlyAuthorization: Bearer <token>with the token the collector has. Without it the collector answers 401. - Only
POST,fetchandagent_*spans, noinvoke_agent,chatorexecute_tool. The Agent calls the plain AI SDK. Call the functions returned bywrapAISDK(ai). invoke_agentstored as unknown, or session token totals doubled. Thetransform/cloudflare_agentsprocessor is missing or edited; it pinsinvoke_agentto agent and removes its token copies.- Two tool spans per tool call. The
tool_approvalline of the processor is missing. invoke_agentorchatstored without model or tokens. The span was still open when its invocation ended (see Flushing).- The trace has no session, but its spans do. The
onBeforeRequestline in the Worker is missing. - Dozens of
durable_object_storage_*spans. Thefilter/storage_spansprocessor is not in the pipeline. - A trace named
Hello from Cloudflare 👋 (...). That is the destination’s test span; thefilter/preflightprocessor is missing from the pipeline. - One-span
GETtraces. Requests to other paths of your Worker are traced too. Lowerhead_sampling_rate, or add a filter on the span name to the collector, if you do not want them. - Input and Output panes are empty. That is the default. Set
STORE_MESSAGES/STORE_TOOLSin the Worker andFI_CAPTURE_CONTENT=trueon the collector.
Questions & Discussion