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>
kindMeansPayload beyond message
engine:errorUpstream provider failed — 503, rate limit, auth, timeoutcode?, engine?
engine:not-found$engine() named an unregistered engine. Always fatalengine, available[]
capsule:errorCapsule crashed, timed out, or was denied by policycommandId?
middleware:rejectedMiddleware refused the request; the handler never ranpathway, middleware?
pathway:not-foundNo such pathway on this agentpathway, available[]
pathway:errorUnhandled error escaped the handlerpathway
script:errorA script threw during axon.runscript?
auth:errorAuthentication failed or the session expired
rate:errorClient is being rate limitedretryAfterMs?
balance:errorInsufficient Ψ balancetopUpUrl
timeout:errorAn operation exceeded its limitoperation?, limitMs?
if (entry.type === "axon:agent:error" && entry.payload.kind === "balance:error") {
    console.log(`Top up: ${entry.payload.topUpUrl}`)
}