The Context Object

All information flows through the context. There are no side channels.

Properties

The Context interface has these top-level properties: envelope, args, run, locals, nonlocals, globals, and manager. The table below shows the full paths used in code.

Property Mutability Description
envelope.id Immutable Unique request ID.
args Set-once Original invocation args. Never overwritten by middleware.
locals.history Append-only Stack of { tool, args, timestamp } frames.
locals.result Mutable Final output. Starts undefined.
locals.error Mutable Captured exception.
locals.config Mutable Per-invocation configuration. Starts with { cwd, library, runtime } from globals; populated by config middleware (cwd, workspace, paths, mcp, model).
globals Mutable Long-lived state shared across invocations. Contains config (global config), tools (programmatic tools, MCP connections), and cache. Event state lives on globals.internals.events (managed by event skills).
locals Mutable Per-invocation scratchpad for middleware coordination. Named after the Koa convention.
nonlocals Mutable Inherited state — shallow-copied from parent to child on every invoke, so child contexts see parent values while maintaining their own overrides. Used for cross-invocation state like gateway configuration. Well-known keys: nonlocals.rootContextId (set automatically to the first context's ID — used for event correlation and debug session management), nonlocals.gateway (gateway — set by the gateway middleware and providers), nonlocals.request (HTTP request data — set by the web middleware, contains parsed request fields plus the raw res object for streaming), nonlocals.agent (agent namespace — set by the agent skill, contains events for the event topic, contextId/ancestorContextId for agent tree linking).
run.origin Mutable Tool source: { uri, frontmatter }. uri is file:///path for files, mcp://server/tool for MCP, null for builtins. frontmatter is the parsed YAML metadata object.
run.middleware Mutable The fully resolved cascade metadata for this invocation. Set by the orchestrator after cascade resolution. Skills can read it to inspect their effective configuration.
locals.prompt Mutable { raw, expanded }. raw is the markdown body after frontmatter extraction. expanded is the result after directive processing. Set from the tool definition for markdown tools.
run.tool Mutable Tool declaration: { name, description, params, returns, visibility, allowedTools, role, tags }. Closed type — no arbitrary keys.
envelope.parent Immutable Reference to the caller's context, or null for root.
envelope.target Immutable The context this skill operates on. For normal skills, target is self (the skill's own context). For middleware, target is the served skill's context (set by the chain invoker). Analogous to event.target in DOM events.
envelope.hasOtherTarget Immutable True when this context serves another context (target !== self).
run.signal Immutable Cancellation signal — aborted when ctx.manager.abort() is called or parent is aborted. Composed from own AbortController, parent signal, and seed signal via manual abort propagation (event listeners, not AbortSignal.any() — to avoid FinalizationRegistry pressure that causes GC freezes under high context volume).
run.interactive Immutable True when this invocation is a direct human-facing CLI session with a real TTY. Set by the CLI entry point when stdin.isTTY && stdout.isTTY. Skills that require terminal interaction (tutor, repl) gate on this. Does not inherit — child invocations default to false.

The Manager

The context is a pure data object. All operations live on ctx.manager — an immutable ContextManager instance created at context construction time. The manager is non-enumerable (excluded from serialization and Object.keys).

  • ctx.manager.next() — Pass control to the next middleware. Returns ctx.locals.result on unwind.
  • ctx.manager.invoke(ref, args?, options?) — Invoke a tool by reference or inline ToolDef. Always creates an isolated child context. Options: context (partial Context seed — any context property can be seeded, including target for chain entries, locals for initial locals, nonlocals for inherited state), detached (fire and forget — don't await the result, swallow errors silently). Middleware injection is done via the context seed: context: { locals: { middleware: { ... } } } for non-cascading or context: { nonlocals: { middleware: { ... } } } for cascading.
  • ctx.manager.refresh() — Re-resolve the current tool using ctx.locals.config.paths. Updates ctx.run.tool, ctx.locals.prompt, and the internal toolDef. Called by paths middleware after changing the search paths.
  • ctx.manager.inject(entries) — Dynamically insert middleware entries into the chain after the current position. Used by directive middleware and custom middleware.
  • ctx.manager.finish(value) — Set result and stop the chain.
  • ctx.manager.fail(error) — Set ctx.locals.error and throw to begin unwinding.
  • ctx.manager.abort(reason?) — Abort this context's signal. Cascades to all child contexts.
  • ctx.manager.get(path) — Read a property by dotted path.
  • ctx.manager.set(path, value) — Write a property by dotted path.
  • ctx.manager.log(event) — Emit a log event. Accepts a string (shorthand for a user-facing message) or a full event object with type, tag, level, text, etc. Defaults: type=message, tag=user, level=error. rootContextId is always set from the current context.
  • ctx.manager.tail(opts?) — Query recent log events from the ring buffer, scoped to the current run. Options: tag (filter by tag), type (filter by type or array of types), last (max events to return).
  • ctx.manager.configure(opts?) — Configure log settings. No args returns current config. Options: level, scope (tag filter array), reporter (activate a reporter), global (target global settings vs. current run). Returns { level, scope, reporters }.

The Target Context

Every tool invocation creates its own context. ctx.envelope.target answers: "what context should this skill operate on?"

  • Normal skills: target is self (the skill's own context). The skill works on ctx directly and can ignore target entirely.
  • Middleware skills: target is the served skill's context, set by the chain invoker when ctx.manager.next() dispatches a chain entry. Middleware reads from and writes to ctx.envelope.target: setting target.locals.config.paths, calling target.manager.next() to advance the served tool's pipeline, calling target.manager.finish(value) to set the served tool's result. The middleware's own ctx is its private workspace.

The analogy is DOM events: event.target is the element that originated the event, event.currentTarget is the handler. Similarly, ctx.envelope.target is the skill being served, ctx is the middleware handling it.

The ctxTarget(ctx) helper (exported from agent-apps) is a runtime guard: it returns ctx.envelope.target, throwing if target === ctx (meaning the tool was not invoked as middleware). All built-in middleware uses it.

Serialization

On-demand, not automatic. Non-serializable values (functions, sockets) are replaced with undefined. Circular references are replaced with "[Circular]". Nesting is depth-limited (default 8 levels). The in-memory context retains real values.

Ask AI