events
Every value yielded by axon.stream() or held in result.entries is a ThreadEntry.
This page is the complete vocabulary.
type ThreadEntry<TKind extends string, TPayload> = {
id: string // stable — deltas share the ID of their final entry
time: EventTime // monotonic sequence within the thread
threadId: string
type: TKind // the discriminant — narrow the union on this
payload: TPayload
billing?: BillingSnapshot // only on entries that consumed LLM tokens
}
Narrow on type and the payload follows:
for await (const entry of stream) {
if (entry.type === "axon:agent:message:delta") {
process.stdout.write(entry.payload.content)
}
}
Shared shapes
type EventTime = {
ms: number // wall-clock ms (Unix epoch)
seq: number // monotonic counter — resolves ordering within the same ms
}
type BillingSnapshot = { // on any entry that made an LLM call
provider: string
model: string
tokens: { in: number; out: number; total: number }
cost: { in: number; out: number; total: number } // USD
durationMs: number
}
type BillingTotal = { // accumulated — on thread:end and pathway:complete
tokens: { in: number; out: number; total: number }
cost: { in: number; out: number; total: number }
calls: number
}
Thread lifecycle
First and last entry on every thread, both emitted by the runtime.
type ThreadStart = ThreadEntry<"thread:start", {
schemaVersion: 1
agentId: string
agentName?: string
agentVersion?: string
clientId: string
threadName?: string // "__exec__" | "__script__:{name}:{uuid}" | user-supplied
capsuleSessionId?: string // links to capsule-side events when one is attached
authenticated: boolean
context?: Record<string, unknown>
}>
type ThreadEnd = ThreadEntry<"thread:end", {
reason: "client:disconnect" | "axon:agent:shutdown" | "error" | "timeout"
billing: BillingTotal
}>
Conversation
The exchange between user and agent. Deltas share the id of their final entry —
accumulate in place, then replace when the final arrives.
type MessageUser = ThreadEntry<"user:message", {
content: string
attachments?: Array<{
kind: "image" | "file" | "url"
name?: string
url?: string
data?: string // base64, for inline attachments
mimeType?: string
size?: number // bytes
}>
}>
type MessageAgent = ThreadEntry<"axon:agent:message", {
content: string // final, accumulated
attachments?: Array<{ kind: "image" | "file" | "url"; name?: string; url?: string; mimeType?: string }>
}>
type MessageAgentDelta = ThreadEntry<"axon:agent:message:delta", {
content: string // a partial chunk, not a whole message
}>
// Reasoning text — only from models with extended thinking. Same delta pattern.
type AgentThinking = ThreadEntry<"axon:agent:thinking", { content: string }>
type AgentThinkingDelta = ThreadEntry<"axon:agent:thinking:delta", { content: string }>
Pathway
One pathway invocation wraps each axon.request() or axon.stream(). Every entry
carries the same invocationId.
type PathwayStart = ThreadEntry<"pathway:start", {
pathway: string // route name, e.g. "api/chat"
invocationId: string
}>
type PathwayComplete = ThreadEntry<"pathway:complete", {
pathway: string
invocationId: string
durationMs: number
billing: BillingTotal | null // null when no engine was called
}>
type PathwayAbort = ThreadEntry<"pathway:abort", { // client cancelled — e.g. Escape
pathway: string
invocationId: string
reason?: string
}>
type PathwayError = ThreadEntry<"pathway:error", { // handler threw
pathway: string
invocationId: string
error: string
}>
// Each transient engine failure being retried. If attempts are exhausted, an
// axon:agent:error with kind "engine:error" follows.
type EngineRetry = ThreadEntry<"engine:retry", {
engine: string
attempt: number // 1-based
maxAttempts: number
status?: number // HTTP status that triggered it
delayMs: number // wait before the next attempt
}>
Capsule
Emitted automatically whenever the agent executes code. No agent code required.
commandId links every entry from one command.
type CapsuleStdin = ThreadEntry<"capsule:stdin", {
commandId: string
lang: "shell" | "ts"
code: string
cwd?: string
}>
type CapsuleStdout = ThreadEntry<"capsule:stdout", { commandId: string; data: string }>
type CapsuleStderr = ThreadEntry<"capsule:stderr", { commandId: string; data: string }>
// Structured call trace for a completed command — every tool function it invoked.
type CapsuleCalls = ThreadEntry<"capsule:calls", {
commandId: string
calls: Array<{
module: string
fn: string
args: unknown[]
result?: unknown
error?: string
durationMs: number
}>
}>
Policy decisions surface here too — escalating is waiting on a human, denied was
refused outright. See policy.
type CapsuleEscalating = ThreadEntry<"capsule:escalating", {
commandId: string
escalationId: string
module: string
fn: string
args: unknown[]
rule: string // the rule that triggered it
}>
type CapsuleDenied = ThreadEntry<"capsule:denied", {
commandId: string
module: string
fn: string
args: unknown[]
}>
Modules entering and leaving scope at runtime:
type CapsuleManifest = ThreadEntry<"capsule:manifest", {
module: string
description?: string
exports: Array<{ name: string; declaration: string; jsdoc?: string }>
}>
type CapsuleModuleInstalled = ThreadEntry<"capsule:module:installed", { module: string; npmPackage: string }>
type CapsuleModuleRemoved = ThreadEntry<"capsule:module:removed", { module: string; npmPackage: string }>
Proc lifecycle
For subprocesses started with process.spawn().
type CapsuleProcSpawned = ThreadEntry<"capsule:proc:spawned", {
procId: string
command: string
cwd?: string
pid?: number // available once spawn completes
}>
type CapsuleProcStdout = ThreadEntry<"capsule:proc:stdout", {
procId: string
data: string // raw chunk — may be a partial line
}>
type CapsuleProcStderr = ThreadEntry<"capsule:proc:stderr", { procId: string; data: string }>
type CapsuleProcExit = ThreadEntry<"capsule:proc:exit", {
procId: string
command: string // repeated so display needs no spawned lookup
exitCode: number
ok: boolean
durationMs?: number
}>
type CapsuleProcDenied = ThreadEntry<"capsule:proc:denied", {
procId: string
command: string
error: string
}>
Subagent
Emitted when a capsule command calls subagent.request() or subagent.stream(). All
four share an id, and a commandId linking back to the parent capsule:stdin.
type SubagentStart = ThreadEntry<"subagent:start", {
id: string
commandId: string
prompt: string
mode: "request" | "stream"
}>
// One per inner entry — stream mode only.
type SubagentEntry = ThreadEntry<"subagent:entry", {
id: string
commandId: string
entry: AnyThreadEntry
}>
type SubagentComplete = ThreadEntry<"subagent:complete", {
id: string
commandId: string
text?: string // final message — request mode only
durationMs: number
}>
type SubagentError = ThreadEntry<"subagent:error", { id: string; commandId: string; error: string }>
Errors
Every runtime error arrives as one entry type, discriminated by payload.kind.
type AgentError = ThreadEntry<"axon:agent:error", AnyAgentError>
kind | Means | Payload beyond message |
|---|---|---|
engine:error | Upstream provider failed — 503, rate limit, auth, timeout | code?, engine? |
engine:not-found | $engine() named an unregistered engine. Always fatal | engine, available[] |
capsule:error | Capsule crashed, timed out, or was denied by policy | commandId? |
middleware:rejected | Middleware refused the request; the handler never ran | pathway, middleware? |
pathway:not-found | No such pathway on this agent | pathway, available[] |
pathway:error | Unhandled error escaped the handler | pathway |
script:error | A script threw during axon.run | script? |
auth:error | Authentication failed or the session expired | — |
rate:error | Client is being rate limited | retryAfterMs? |
balance:error | Insufficient Ψ balance | topUpUrl |
timeout:error | An operation exceeded its limit | operation?, limitMs? |
if (entry.type === "axon:agent:error" && entry.payload.kind === "balance:error") {
console.log(`Top up: ${entry.payload.topUpUrl}`)
}