Events

Request Cleanup

The onion's finally blocks ARE the cleanup mechanism:

import { ctxTarget } from 'agent-apps';

async function dbMiddleware(ctx) {
  const target = ctxTarget(ctx);
  const conn = await db.connect();
  target.locals.db = conn;
  try { await target.manager.next(); }
  finally { conn.close(); }
}

Event Bus

A general-purpose pub/sub system built on globals.internals.events. Any skill can publish events to a named topic. Any skill can subscribe to a topic to receive events.

Event envelope:

interface Event {
  id: string;           // auto-assigned unique ID (8-char UUID prefix)
  topic: string;        // which topic this was published to
  type: string;         // event kind — arbitrary string
  source?: string;      // who emitted it (tool name)
  data?: unknown;       // arbitrary payload
  timestamp: number;    // Date.now() at emission
  rootContextId?: string; // root context ID for correlation
}

EventHandler — function form for in-process use, tool-ref form for the public skill API:

type EventHandler = ((event: Event) => unknown) | { ref: string; args?: unknown };

Skills:

Skill Description
event-emit Publish an event to a topic. Assigns an auto-generated ID. Pushes to all subscribed queues synchronously, then fires function handlers async. Tool-ref handlers invoke through the pipeline.
event-subscribe Subscribe to a topic. Pass ref to invoke a skill on each event, or omit to create a queue for event-wait. Supports named consumer groups (queue) and replay of buffered events (replay). Idempotent — subscribing to an existing queue is a no-op.
event-unsubscribe Remove handlers for a topic. Pass ref to remove a specific skill handler, or omit to remove all.
event-wait Block until an event arrives on a topic or a timeout expires. Drains available events immediately if the queue is non-empty. Supports named consumer groups (queue) and manual acknowledgment (ack: false).
event-ack Acknowledge or reject events from manual ack mode. Ack removes events from pending. Nack returns them to the front of the queue for reprocessing.

Event state lives on globals.internals.events (handler map), globals.internals.eventQueues (named queues for event-wait), and globals.internals.eventBuffers (per-topic replay buffers).

Metadata middleware:

  • event — subscribe skills to event topics via centralized routing config. Takes a routes: array of { topic, ref } entries. Also accepts route: (singular) — declares the current skill as an event endpoint with ref implicit. When events arrive on those topics, the referenced skill is invoked with the event envelope as args.
metadata:
  event:
    routes:
      - topic: deploy
        ref: on-deploy
      - topic: rollback
        ref: on-rollback

Standard topics:

Topic Emitted by Description
shutdown CLI Fired on SIGINT/SIGTERM. The CLI awaits the event-emit invocation, but individual tool-ref handlers are dispatched asynchronously (fire-and-forget) like any other event. Function handlers are also async. The process exits after event-emit returns, so handlers have limited time to complete.
gateway-message email, slack, websocket, webhook middleware Fired when an inbound message arrives on a gateway transport. Consumers can use event-wait with this topic to observe messages without affecting route dispatch. Event source is the gateway name; data includes transport-specific fields (e.g., from, subject, body for email).

Hooks

The hooks system is a cascade-based extensibility mechanism. Skills declare hook bindings via the $hook annotation in their frontmatter metadata. Hooks wrap pipeline joints ($next and $invoke), forming an onion around each operation. They are the foundation for debug, profile, and observe.

$hook Annotation

Skills declare hook bindings in their frontmatter metadata:

metadata:
  $hook: { $self: "$next" }           # bind to $next when used as a metadata key
  $hook: { $self: ["$next", "$invoke"] }  # bind to both trigger points
  $hook: { my-tool: "$invoke" }       # bind my-tool to $invoke

The $self sentinel means "this tool, when it appears as a metadata key in another skill's cascade." When a skill with $hook: { $self: "$next" } appears as a metadata key, it is separated from the normal middleware chain and instead wraps each chain entry's execution.

Trigger Points

Trigger When it fires Hook receives
$next Every ctx.manager.next() call — wraps each chain entry The chain entry's args
$invoke Pipeline start — wraps the entire pipeline execution The tool's args

How It Works

During cascade resolution, the orchestrator extracts $hook annotations and separates hook entries from the normal middleware chain. At runtime:

  • $next hooks wrap each chain entry. When ctx.manager.next() advances to an entry, the hook is invoked as middleware serving the target context. The hook calls target.manager.next() to execute the real entry.
  • $invoke hooks wrap the entire pipeline. They are invoked before the chain starts, serving the target context.

Debug, Profile, and Observe

Debug, profile, and observe are not part of the default pipeline. They are activated by adding their metadata keys to a skill's cascade:

metadata:
  debug: true                    # activate pipeline debugging
  profile: true                  # activate timing collection
  observe: { topic: "my-topic" } # emit invoke events to a topic

Each declares $hook: { $self: ... } in its own frontmatter, so when it appears as a metadata key, it binds as a hook rather than a normal middleware entry.

Debug hooks emit events for pipeline-enter, pipeline-exit, next-enter, next-exit, and breakpoint-hit. Profile hooks collect timing data for every pipeline invocation and chain entry. Observe hooks invoke a callback ref with { phase: "enter" }, { phase: "exit" }, and { phase: "error" } for each skill invocation, passing the tool name, args, result, and context ID.

Because hooks are resolved from the cascade, they propagate through inheritance — adding debug: true to a main skill instruments the entire project. Setting nonlocals.debug = false on a child context suppresses instrumentation for that subtree.

Hooks are null by default (zero overhead when unused) and are per-context, not global — instrumenting one pipeline does not affect others.

Ask AI