Skip to content

Observability

Agents emit structured events for every significant operation — RPC calls, state changes, schedule execution, workflow transitions, MCP connections, and more. These events are published to diagnostics channels and are silent by default (zero overhead when nobody is listening).

Event structure

Every event has these fields:

TypeScript
{
type: "rpc", // what happened
agent: "MyAgent", // which agent class emitted it
name: "user-123", // which agent instance (Durable Object name)
payload: { method: "getWeather" }, // details
timestamp: 1758005142787 // when (ms since epoch)
}

agent and name identify the source agent — agent is the class name and name is the Durable Object instance name.

Channels

Events are routed to named channels based on their type:

ChannelEvent typesDescription
agents:statestate:updateState sync events
agents:rpcrpc, rpc:errorRPC method calls and failures
agents:messagemessage:request, message:response, message:clear, message:cancel, message:error, tool:result, tool:approval, submission:create, submission:status, submission:errorChat message, tool, and Think submission lifecycle
agents:chatchat:request:failed, chat:recovery:*, chat:stream:stalledChat request, recovery, and stream-stall lifecycle
agents:transcriptchat:transcript:repairedTranscript repair events
agents:fiberfiber:run:*, fiber:recovery:*Durable fiber lifecycle
agents:agent_toolagent_tool:recovery:*Parent/child agent-tool recovery
agents:scheduleschedule:create, schedule:execute, schedule:cancel, schedule:retry, schedule:error, schedule:duplicate_warning, queue:create, queue:retry, queue:errorScheduled and queued task lifecycle
agents:lifecycleconnect, disconnect, destroyAgent connection and teardown
agents:workflowworkflow:start, workflow:event, workflow:approved, workflow:rejected, workflow:terminated, workflow:paused, workflow:resumed, workflow:restartedWorkflow state transitions
agents:mcpmcp:client:preconnect, mcp:client:connect, mcp:client:authorize, mcp:client:discoverMCP client operations
agents:emailemail:receive, email:reply, email:sendEmail processing

Subscribing to events

Typed subscribe helper

The subscribe() function from agents/observability provides type-safe access to events on a specific channel:

JavaScript
import { subscribe } from "agents/observability";
const unsub = subscribe("rpc", (event) => {
if (event.type === "rpc") {
console.log(`RPC call: ${event.payload.method}`);
}
if (event.type === "rpc:error") {
console.error(
`RPC failed: ${event.payload.method}${event.payload.error}`,
);
}
});
// Clean up when done
unsub();

The callback is fully typed — event is narrowed to only the event types that flow through that channel.

The typed helper uses camelCase keys, so agent-tool recovery is subscribe("agentTool", ...). Raw diagnostics channel subscribers should use the emitted channel name, agents:agent_tool.

Raw diagnostics_channel

You can also subscribe directly using the Node.js API:

JavaScript
import { subscribe } from "node:diagnostics_channel";
subscribe("agents:schedule", (event) => {
console.log(event);
});

Tail Workers (production)

In production, all diagnostics channel messages are automatically forwarded to Tail Workers. No subscription code is needed in the agent itself — attach a Tail Worker and access events via event.diagnosticsChannelEvents:

JavaScript
export default {
async tail(events) {
for (const event of events) {
for (const msg of event.diagnosticsChannelEvents) {
// msg.channel is "agents:rpc", "agents:workflow", etc.
// msg.message is the typed event payload
console.log(msg.timestamp, msg.channel, msg.message);
}
}
},
};

This gives you structured, filterable observability in production with zero overhead in the agent hot path.

Custom observability

You can override the default implementation by providing your own Observability interface:

JavaScript
import { Agent } from "agents";
const myObservability = {
emit(event) {
// Send to your logging service, filter events, etc.
if (event.type === "rpc:error") {
console.error(event.payload.method, event.payload.error);
}
},
};
class MyAgent extends Agent {
observability = myObservability;
}

Set observability to undefined to disable all event emission:

JavaScript
import { Agent } from "agents";
class MyAgent extends Agent {
observability = undefined;
}

Event reference

RPC events

TypePayloadWhen
rpc{ method, streaming? }A @callable method is invoked
rpc:error{ method, error }A @callable method throws

State events

TypePayloadWhen
state:update{}setState() is called

Message, tool, and submission events

These events track chat message lifecycle, client-side tool interactions, and Think durable submissions.

TypePayloadWhen
message:request{}A chat message is received
message:response{}A chat response stream completes
message:clear{}Chat history is cleared
message:cancel{ requestId }A streaming request is cancelled
message:error{ error }A chat stream fails
tool:result{ toolCallId, toolName }A client tool result is received
tool:approval{ toolCallId, approved }A tool call is approved or rejected
submission:create{ submissionId }A Think submission is accepted
submission:status{ submissionId, status }A Think submission status changes
submission:error{ submissionId, error }A Think submission fails

Chat recovery events

TypePayloadWhen
chat:request:failed{ requestId?, stage, messagesPersisted?, error }A Think chat request fails while parsing, persisting, running, or streaming
chat:recovery:detected{ incidentId, requestId, attempt, maxAttempts, recoveryKind }An interrupted chat fiber is first observed
chat:recovery:attempt{ incidentId, requestId, attempt, maxAttempts, recoveryKind }The framework begins a recovery attempt
chat:recovery:scheduled{ incidentId, requestId, attempt, maxAttempts, recoveryKind }A retry or continuation callback is scheduled
chat:recovery:completed{ incidentId, requestId, attempt, maxAttempts, recoveryKind }Recovery completed successfully
chat:recovery:skipped{ incidentId, requestId, attempt, maxAttempts, recoveryKind, reason? }Recovery was skipped because the conversation changed or was no longer recoverable
chat:recovery:failed{ incidentId, requestId, attempt, maxAttempts, recoveryKind, reason? }Recovery ran but failed
chat:recovery:exhausted{ incidentId, requestId, attempt, maxAttempts, recoveryKind, reason }Recovery exceeded its configured attempt budget
chat:stream:stalled{ requestId, timeoutMs }The inactivity watchdog fired — no stream chunk arrived within chatStreamStallTimeoutMs. With chatRecovery on, the turn routes into recovery

recoveryKind is "retry" when recovery replays an unanswered user turn and "continue" when it continues a partial assistant turn.

Transcript events

TypePayloadWhen
chat:transcript:repaired{ requestId?, removedToolCalls, normalizedInputs, toolCallIds? }Think repairs a persisted transcript before sending it to the provider. removedToolCalls counts orphaned tool calls healed; normalizedInputs counts stringified or missing tool inputs repaired

Fiber events

TypePayloadWhen
fiber:run:started{ fiberId, fiberName, managed? }A durable fiber starts
fiber:run:completed{ fiberId, fiberName, managed?, elapsedMs? }A durable fiber completes
fiber:run:failed{ fiberId, fiberName, managed?, error, elapsedMs? }A durable fiber throws
fiber:run:interrupted{ fiberId, fiberName, managed?, recoveryReason, elapsedMs? }Startup finds an interrupted fiber
fiber:recovery:detected{ fiberId, fiberName, managed?, recoveryReason, elapsedMs? }Recovery sees an interrupted fiber
fiber:recovery:attempt{ fiberId, fiberName, managed?, recoveryReason }A recovery hook starts
fiber:recovery:handled{ fiberId, fiberName, managed?, recoveryReason, status, elapsedMs? }Recovery handling completes
fiber:recovery:skipped{ fiberId, fiberName, managed?, reason, elapsedMs? }A recovery scan skips remaining work
fiber:recovery:failed{ fiberId, fiberName, managed?, error, reason?, elapsedMs? }A recovery hook fails

Agent-tool recovery events

TypePayloadWhen
agent_tool:recovery:begin{ runCount, totalTimeoutMs? }Parent recovery starts scanning stale agent-tool runs
agent_tool:recovery:row{ runId, agentType, status, reason?, elapsedMs? }One stale run is reconciled
agent_tool:recovery:deadline{ runId, agentType, elapsedMs? }Total recovery deadline is exhausted before inspecting a row
agent_tool:recovery:complete{ runCount, elapsedMs? }Parent recovery finishes scanning rows
agent_tool:recovery:failed{ error }Parent recovery fails unexpectedly

Schedule and queue events

TypePayloadWhen
schedule:create{ callback, id }A schedule is created
schedule:execute{ callback, id }A scheduled callback starts
schedule:cancel{ callback, id }A schedule is cancelled
schedule:retry{ callback, id, attempt, maxAttempts }A scheduled callback is retried
schedule:error{ callback, id, error, attempts }A scheduled callback fails after all retries
schedule:duplicate_warning{ callback }A non-idempotent schedule may duplicate work
queue:create{ callback, id }A task is enqueued
queue:retry{ callback, id, attempt, maxAttempts }A queued callback is retried
queue:error{ callback, id, error, attempts }A queued callback fails after all retries

Lifecycle events

TypePayloadWhen
connect{ connectionId }A WebSocket connection is established
disconnect{ connectionId, code, reason }A WebSocket connection is closed
destroy{}The agent is destroyed

Workflow events

TypePayloadWhen
workflow:start{ workflowId, workflowName? }A workflow instance is started
workflow:event{ workflowId, eventType? }An event is sent to a workflow
workflow:approved{ workflowId, reason? }A workflow is approved
workflow:rejected{ workflowId, reason? }A workflow is rejected
workflow:terminated{ workflowId, workflowName? }A workflow is terminated
workflow:paused{ workflowId, workflowName? }A workflow is paused
workflow:resumed{ workflowId, workflowName? }A workflow is resumed
workflow:restarted{ workflowId, workflowName? }A workflow is restarted

MCP events

TypePayloadWhen
mcp:client:preconnect{ serverId }Before connecting to an MCP server
mcp:client:connect{ url, transport, state, error? }An MCP connection attempt completes or fails
mcp:client:authorize{ serverId, authUrl, clientId? }An MCP OAuth flow begins
mcp:client:discover{ url?, state?, error?, capability? }MCP capability discovery succeeds or fails

Email events

TypePayloadWhen
email:receive{ from, to, subject? }An email is received
email:reply{ from, to, subject? }A reply email is sent
email:send{ from, to, subject? }An email is sent

Next steps