Skip to content

Compose API

The Compose API lets you intercept and reshape any signal the agent kernel emits — from system prompts to tool results to nudges — using a declarative composition model.

import { ReactiveAgents } from 'reactive-agents';
import { maxIterations, budgetLimit } from 'reactive-agents/compose/killswitches';
const agent = await ReactiveAgents.create()
.withProvider('anthropic')
.compose(budgetLimit({ maxTokens: 50_000 }))
.compose(maxIterations(20))
.compose((harness) => {
harness.tap('observation.tool-result', (result, ctx) => {
console.log(`[iter ${ctx.iteration}] tool result:`, result.content);
});
})
.build();

Signature: compose(fn: (harness: Harness) => void): this

Registers a composition block. Multiple .compose() calls accumulate in registration order.

fn receives a Harness instance with methods to register transforms, taps, and phase hooks. All registrations are compiled once at .build() time.

.compose() is the canonical entry point. .withHarness() is an identical alias.

Intercept and replace an emission’s payload.

Signature:

harness.on(
pattern: TagPattern | TagPattern[],
fn: (payload: PayloadFor<P>, ctx: ContextFor<P>) =>
| PayloadFor<P> // replace payload
| undefined // keep current payload
| null // suppress emission
| Promise<...>
): Harness

Pattern types:

PatternMatches
'prompt.system'Exact tag
'prompt.*'All single-segment prompt.X tags
'nudge.**'All nudge.X and nudge.X.Y tags (multi-segment)
'**'Every tag
(tag) => booleanCustom predicate

Transform semantics:

  • Return a value → replaces current payload
  • Return undefinedkeeps current payload (pass-through)
  • Return nullsuppresses the emission (removed from pipeline)
  • Multiple transforms on same tag chain in order: broadest pattern first, most-specific last

Example — suppress all nudges in a bare-LLM ablation:

harness.on('nudge.*', () => null)

Example — localize system prompt:

harness.on('prompt.system', (text, ctx) => `[locale: fr]\n${text}`)

Observe an emission without changing it. Runs after all transforms.

Signature:

harness.tap(
pattern: TagPattern | TagPattern[],
fn: (payload: PayloadFor<P>, ctx: ContextFor<P>) => void | Promise<void>
): Harness

Taps run in registration order, after transforms are finalized. A tap that throws is a bug — they run unconditionally with the final value.

Example — telemetry:

harness.tap('**', (payload, ctx) => {
otel.record(ctx.phase, ctx.iteration, payload);
});

harness.before(phase, fn) — Phase Pre-Hook

Section titled “harness.before(phase, fn) — Phase Pre-Hook”

Run before a kernel phase. Can abort or skip the iteration.

Signature:

harness.before(
phase: Phase,
fn: (ctx: { phase: Phase; iteration: number; state: KernelStateLike }) =>
| void
| Promise<void>
| { readonly abort: 'stop' | 'terminate'; readonly reason?: string }
| { readonly skip: true }
): Harness

Return values:

ReturnEffect
void / undefinedContinue normally
{ abort: 'stop' }End loop gracefully (status: done)
{ abort: 'terminate' }End loop as failure (status: failed)
{ skip: true }Skip this iteration, continue loop

Example — custom iteration limit:

harness.before('think', (ctx) => {
if (ctx.iteration >= 15) return { abort: 'stop', reason: 'custom-limit' };
});

harness.after(phase, fn) — Phase Post-Hook

Section titled “harness.after(phase, fn) — Phase Post-Hook”

Run after a kernel phase completes. Same signature as .before() but fires after.

Run when a phase throws. Can optionally recover by returning a replacement state.

Signature:

harness.onError(
phase: Phase | '*',
fn: (error: unknown, ctx: { phase: Phase | '*'; iteration: number }) =>
| void
| Promise<void>
| { readonly recover: KernelStateLike }
): Harness

Use '*' to catch errors from any phase. Return { recover: newState } to inject a replacement state and continue the loop.

harness.emit(tag, payload) — Inject at Build Time

Section titled “harness.emit(tag, payload) — Inject at Build Time”

Inject a payload directly at build time. Use for initial seeding.

Nest a composition block. Useful for reusable plugin patterns.

harness.use((h) => {
h.tap('observation.tool-result', logFn);
h.before('act', approvalFn);
});
bootstrap → guardrail → cost-route → strategy-select → think → act
→ observe → verify → memory-flush → cost-track → audit → complete

Phase hooks fire in this order per iteration. bootstrap and complete fire once per run.

All hook/transform callbacks receive a ctx with at minimum:

{
iteration: number; // 0-indexed
phase: Phase; // current phase name
state: KernelStateLike; // current kernel state snapshot
strategy: string; // active reasoning strategy ('reactive', 'tot', etc.)
}

Some tags carry richer contexts — see Harness Tag Reference.

Prebuilt compositions from reactive-agents/compose/killswitches:

import {
budgetLimit, timeoutAfter, maxIterations,
requireApprovalFor, watchdog
} from 'reactive-agents/compose/killswitches';

See Composition Recipes for usage examples.