In-Loop Processors

A Processor is a loop-internal observer/guardrail for the agent run. It watches the model's streamed output and the tool calls it requests as the run progresses, keeps its own scratch state, and can abort the run before a "done" is claimed. This is the structural prerequisite for real-time guardrails (block disallowed output mid-stream) and a proof-of-done / coverage gate (inspect what the model is about to do and halt it).

Where the three hooks fire in the run
stream chunks
processOutputStream
per model response
processOutputStep — gate tool calls
run end
processOutputResult — verdict
abort() → TripWire → tripwire event

processOutputStream watches every chunk, processOutputStep gates tool calls per response, processOutputResult records a verdict at the end. Any hook can abort with a TripWire.

A processor is configuration, not a tool, not an action, and not an authoring DSL. Processors only observe, mutate their own stream-scoped state, and abort(). They never define app behavior, replace actions, or appear to the model. App operations belong in actions.

The hooks

A processor implements any subset of three optional lifecycle hooks (the shape is borrowed from Mastra's output processors):

Hook Fires… Use it to…
processOutputStream per streamed chunk (text / thinking deltas) while the model generates react to output before the full turn lands
processOutputStep once per model response, around tool execution inspect the tool calls the model is about to run; gate them
processOutputResult once at run end, with the final assistant text record a verdict / proof-of-done over the completed answer

Each processor gets its own mutable, run-scoped state object that persists across every one of its hook invocations within a single run and is isolated from other processors' state.

import type { Processor } from "@agent-native/core";

const noSecretsInOutput: Processor = {
  name: "no-secrets",
  processOutputStream({ part, abort }) {
    if (part.type === "text" && /sk-live_/.test(part.text)) {
      abort("Model attempted to emit a live secret token.", {
        kind: "secret-leak",
      });
    }
  },
};

const coverageGate: Processor = {
  name: "proof-of-done",
  processOutputStep({ toolCalls, state }) {
    // Track what the model has actually done this run...
    for (const call of toolCalls) {
      (state.ran ??= new Set<string>()).add(call.name);
    }
  },
  processOutputResult({ text, state }) {
    // ...and record a verdict over the final answer.
    const ran = state.ran as Set<string> | undefined;
    state.verdict = ran?.has("run-tests") ? "verified" : "unverified";
  },
};

Aborting with TripWire

A hook halts the run by calling abort(reason, meta?), which throws a TripWire. The loop catches it, emits a single tripwire event, stops cleanly, and surfaces the reason as the final assistant message.

import { TripWire } from "@agent-native/core";

The tripwire event carries:

Field Type Notes
reason string The human-readable reason passed to abort.
processor string Name of the processor that aborted, when it declared a name.

TripWire also carries optional structured meta and the originating processor name for programmatic consumers that instanceof-check it. Because a halt is graceful, processOutputResult still fires on the (halted) final text so a proof-of-done processor can record its verdict even when the run was aborted.

Wiring processors

Processors are configured in code via the processors array on runAgentLoop:

await runAgentLoop({
  engine,
  model,
  systemPrompt,
  tools,
  messages,
  actions,
  send,
  signal,
  processors: [noSecretsInOutput, coverageGate],
});

Zero-overhead when unused. The loop builds the processor chain only when at least one processor is supplied; when processors is omitted or empty, none of the seam code runs and the loop is byte-for-byte unchanged. Hooks run in registration order and may be sync or async.

The loop-level seam is the deliverable today and is callable directly by sub-agents, A2A, MCP, and tests. Threading processors through the HTTP chat handler (so a per-request resolver can configure them without calling runAgentLoop directly) is convenience plumbing that is not yet wired — configure processors at the runAgentLoop call site for now.

What's next

  • Durable Resume — how the loop survives interruptions without re-running completed side effects.
  • Custom Agents & Teams — sub-agents run the same loop and can carry their own processors.
  • Observability — record processor verdicts alongside run traces.