The ReactiveAgentBuilder is the primary entry point for creating agents. It provides a fluent API for composing capabilities.
Jump to section
Section titled “Jump to section”| Category | Methods |
|---|---|
| ReactiveAgents factory | create, quick, fromConfig, fromJSON |
| Core Identity | withName, withAgentId, withPersona, withSystemPrompt, withEnvironment |
| Model & Provider | withModel, withProvider |
| Reasoning & Context | withReasoning, withMemory, withContextProfile, withMaxIterations, withMinIterations |
| Tools & MCP | withTools, withRequiredTools, withMCP, withMetaTools, withSkills, withAgentTool, withDynamicSubAgents, withRemoteAgent |
| Observability & Telemetry | withObservability, withCortex, withStreaming, withLogging, withEvents |
| Safety & Resilience | withGuardrails, withKillSwitch, withBehavioralContracts, withVerification, withGrounding, withReceiptSigning, withCircuitBreaker, withRateLimiting |
| Cost & Performance | withCostTracking, withModelRouting, withBudget, withModelPricing, withDynamicPricing, withRetryPolicy, withTimeout, withLlmTimeout |
| Lifecycle & Hooks | withHook, withHealthCheck, withErrorHandler, withFallbacks, withAudit |
| Advanced | withA2A, withGateway, withReactiveIntelligence, withPrompts, withUserInteraction, withLazyValidation, withDocuments, withTaskContext, withLayers |
| Building & Running | build, buildEffect, runOnce |
| Agent Methods | run, runStream, chat, session, health, cancel, pause, resume, dispose |
| Result Reference | AgentResult, AgentDebrief, stream event types |
Method ↔ config correspondence
Section titled “Method ↔ config correspondence”Every builder method and the AgentConfig key(s) it sets. config methods are
expressible declaratively via createAgent(config);
overlay methods are code-only (functions/secrets/registries) with the reason
recorded. This table is generated from the single source
(AgentConfigSchema + the builder prototype) — see the Configuration
reference for the declarative field list.
| Method | Config key(s) | Kind | Description |
|---|---|---|---|
withA2A | overlay — multi-agent transport topology primitive (not data) | overlay | Agent-to-Agent server. |
withAdaptiveHarness | adaptiveHarness | config | Adaptive harness / policy compiler. |
withAgentId | agentId | config | Stable agent identifier. |
withAgentTool | overlay — code-only sub-agent registry | overlay | Register a sub-agent as a tool. |
withApprovalPolicy | overlay — carries an approval predicate (HITL durability rail) | overlay | Human-in-the-loop tool approval gate. |
withAudit | features.audit | config | Per-tool-call rationale auditing. |
withBehavioralContracts | overlay — behavioral-contract overlay (folds into withContract) | overlay | Behavioral contracts. |
withBudget | budget | config | Declarative token/cost budget caps. |
withCalibration | overlay — runtime-probed calibration (not static data) | overlay | Model calibration mode. |
withChannels | overlay — messaging transport wiring (not data) | overlay | Messaging channels. |
withCircuitBreaker | circuitBreaker | config | Circuit-breaker thresholds (false disables). |
withContextProfile | overlay — cross-field side-effect profile (not orthogonal data) | overlay | Context window profile. |
withContract | overlay — behavioral-contract overlay (not JSON data) | overlay | Behavioral contract. |
withCortex | overlay — Cortex desk integration — observability alias (see withObservability({cortex})) | overlay | Emit events to a Cortex desk. |
withCostTracking | costTracking, features.costTracking | config | Cost budget caps. |
withCustomTermination | overlay — carries a termination predicate (folds into withReasoning) | overlay | Custom termination predicate. |
withDocuments | overlay — ingestion side-effect (folds into withTools({documents})) | overlay | RAG document ingestion. |
withDurableRuns | durableRuns | config | Crash-resume durable execution. |
withDynamicPricing | overlay — pricing overlay (folds into withCostTracking) | overlay | Dynamic pricing overlay. |
withDynamicSubAgents | overlay — code-only dynamic sub-agent registry | overlay | Dynamic sub-agent spawning. |
withEnvironment | overlay — carries secrets/env (never serialized) | overlay | Environment secrets. |
withErrorHandler | overlay — carries an error-handler function (not JSON) | overlay | Custom error handler. |
withEvents | overlay — carries an event stream/callback (folds into withObservability) | overlay | Event stream sink. |
withExperienceLearning | memory.experienceLearning | config | Learn from prior-run experience summaries. |
withFabricationGuard | fabricationGuard | config | Fabrication-guard mode (off/warn/block). |
withFallbacks | fallbacks | config | Provider/model fallbacks. |
withGateway | gateway | config | Gateway (cron/webhook/access-control) config. |
withGrounding | grounding | config | Opt-in numeric evidence grounding. |
withGuardrails | guardrails, features.guardrails | config | Injection/PII/toxicity guardrails. |
withHarness | overlay — compose-power-tier harness injection (not data) | overlay | Inject a composed harness. |
withHealthCheck | features.healthCheck | config | Enable agent.health() probes. |
withHook | overlay — carries a lifecycle callback function (not JSON) | overlay | Lifecycle hook. |
withKillSwitch | features.killSwitch | config | Emergency stop / terminate control. |
withLayers | overlay — Effect Layer DI escape hatch (not data) | overlay | Provide custom Effect layers. |
withLazyValidation | overlay — no schema field (folds into withVerification timing) | overlay | Lazy output validation. |
withLeanHarness | overlay — cross-field profile patch — use withProfile(lean()) | overlay | Lean-harness mode. |
withLearning | memory, skillPersistence | config | Compounding-intelligence bundle (memory + skill persistence). |
withLlmTimeout | _overlay — sets ollamaTimeoutMs; no schema field (G3, folds into withBudget) | overlay | LLM request timeout (ms). |
withLogging | logging | config | Structured logging config. |
withLongHorizon | horizonProfile | config | Long-horizon guard profile. |
withMCP | mcpServers | config | Connect MCP servers. |
withMaxIterations | execution.maxIterations | config | Iteration cap. |
withMemory | memory, features.memory | config | Enable memory layers + tier/dbPath/capacity/experienceLearning/consolidation. |
withMemoryConsolidation | memory.memoryConsolidation | config | Background memory consolidation/decay/prune. |
withMetaTools | overlay — code-only meta-tool registry | overlay | Conductor’s-suite meta-tools. |
withMinIterations | execution.minIterations | config | Minimum iterations before termination. |
withModel | model, thinking, temperature, maxTokens, numCtx | config | Model id + params (thinking/temperature/maxTokens/numCtx). |
withModelPricing | pricingRegistry | config | Custom model pricing registry. |
withModelRouting | overlay — cost-aware routing capability with no config representation (G4) | overlay | Cost-aware model routing. |
withName | name | config | Agent name. |
withObservability | observability, features.observability | config | Observability umbrella (verbosity/live/cortex/tracing/logging/costs/…). |
withOutputSchema | outputSchemaOptions | config | Typed structured output (schema object is code-only; options serialize). |
withOutputValidator | overlay — carries a validator function (folds into withVerification) | overlay | Custom output validator. |
withPersona | persona | config | Role/tone/instructions persona. |
withProfile | profile | config | Preset baseline capability profile. |
withPrompts | features.prompts | config | Register custom prompt templates. |
withProvider | provider | config | LLM provider. |
withRateLimiting | rateLimiting | config | Outbound LLM rate limiting. |
withReactiveIntelligence | reactiveIntelligence, features.reactiveIntelligence | config | Reactive intelligence posture. |
withReasoning | reasoning, features.reasoning | config | Reasoning strategy + options. |
withReceiptSigning | overlay — carries a private signing key (secret, never serialized) | overlay | Ed25519 receipt signing. |
withRemoteAgent | overlay — code-only remote-agent registry | overlay | Register a remote agent. |
withReplayLLM | overlay — deterministic replay test rig (not data) | overlay | Replay recorded LLM responses. |
withRequiredTools | requiredTools | config | Tools that must be called before success. |
withRetryPolicy | execution.retryPolicy | config | LLM retry policy (maxRetries/backoff). |
withSelfImprovement | features.selfImprovement | config | Enable self-improvement loop. |
withSkillPersistence | skillPersistence | config | Persist evolved skills across runs. |
withSkills | overlay — code-only SKILL.md directory registry | overlay | Living SKILL.md directories. |
withStallPolicy | stallPolicy | config | Stall/no-progress escalation policy. |
withStreaming | features.streaming | config | Enable event streaming. |
withStrictValidation | execution.strictValidation | config | Strict output validation. |
withSystemPrompt | systemPrompt | config | System prompt. |
withTaskContext | taskContext | config | Background key/value facts for reasoning. |
withTestScenario | overlay — test-scenario rig (not data) | overlay | Load a test scenario. |
withThinking | thinking | config | Extended thinking / reasoning effort. |
withTimeout | execution.timeoutMs | config | Run timeout (ms). |
withTools | tools, features.tools | config | Tools layer + allowed/focused/adaptive/terminal/required options. |
withTracing | overlay — trace persistence — observability alias (see withObservability({tracing})) | overlay | JSONL trace persistence. |
withUserInteraction | overlay — durable ask overlay (channel adapters are code-only) | overlay | Durable user interaction. |
withVerification | verification, features.verification | config | Verification package (entropy/nli/thresholds/useLLMTier/onReject). |
withVerificationStep | overlay — carries a verification-step function (folds into withVerification) | overlay | Single post-answer reflect pass. |
ReactiveAgents factory
Section titled “ReactiveAgents factory”| API | Description |
|---|---|
ReactiveAgents.create() | New empty builder (defaults: name: "agent", provider: "test"). |
ReactiveAgents.quick(options?) | Async — resolve provider + model + maxIterations from the environment and build() a ready-to-run agent in one call. |
ReactiveAgents.fromConfig(config) | Async — rebuild a builder from an AgentConfig object (agentConfigToBuilder). |
ReactiveAgents.fromJSON(json) | Async — parse JSON → validate → same as fromConfig. |
import { ReactiveAgents } from 'reactive-agents'// or: import { ReactiveAgents } from "@reactive-agents/runtime";
const builder = ReactiveAgents.create()ReactiveAgents.quick()
Section titled “ReactiveAgents.quick()”The two-line first-touch entry point — returns a built ReactiveAgent, not a builder:
const agent = await ReactiveAgents.quick()const result = await agent.run('Say hello')Every field resolves from an environment variable, then a sensible default, so quick() with no arguments works out of the box:
interface QuickOptions { provider?: ProviderName // Default: REACTIVE_AGENTS_PROVIDER, else the first of anthropic/openai/gemini/groq/xai whose key is present, else "ollama" model?: string // Default: REACTIVE_AGENTS_MODEL, else the provider's default model maxIterations?: number // Default: REACTIVE_AGENTS_MAX_ITERATIONS, else 10}A misconfigured environment (e.g. missing key) warns at build and surfaces a clean typed error at run() time; use ReactiveAgents.create()....withStrictValidation() when you want a hard failure at build instead.
Agent as Data (toConfig / serialization)
Section titled “Agent as Data (toConfig / serialization)”On a configured builder:
toConfig()→AgentConfig(plain object, JSON-serializable except documented exceptions).- Use
agentConfigToJSON/agentConfigFromJSONfromreactive-agentsor@reactive-agents/runtimefor string round-trips.
Builder methods
Section titled “Builder methods”All chain methods return this unless noted.
Identity & prompts
Section titled “Identity & prompts”| Method | Signature | Description |
|---|---|---|
withName | (name: string) => this | Display name / agentId basis |
withAgentId | (id: string) => this | Pin a stable agentId instead of the generated ${name}-${Date.now()}. All memory and run data keyed on agentId accumulates across builds that share the ID (e.g. a UUID or Cortex session ID) |
withPersona | (persona: AgentPersona) => this | Structured steering: { name?, role?, background?, instructions?, tone? } |
withSystemPrompt | (prompt: string) => this | Custom system prompt; if persona is set, persona text is prepended |
withEnvironment | (context: Record<string, string>) => this | Extra key/value context merged into the system prompt (framework already injects date/time/tz/platform) |
Model & Provider
Section titled “Model & Provider”| Method | Signature | Description |
|---|---|---|
withModel | (model: string) => this | Set the LLM model by name (e.g., "claude-sonnet-4-6") |
withModel | (params: ModelParams) => this | Set model with advanced parameters: thinking, temperature, maxTokens, numCtx |
withThinking | (options?: boolean | ThinkingOptions) => this | Enable native thinking / reasoning mode with optional effort + budget. The rich-config home for thinking; .withModel({ thinking }) remains the quick boolean. true / absent enables, false disables, or pass { effort, budgetTokens }. Off unless enabled. |
withProvider | (provider: "anthropic" | "openai" | "ollama" | "gemini" | "groq" | "xai" | "litellm" | "test") => this | Set the LLM provider |
ModelParams
Section titled “ModelParams”interface ModelParams { model: string // Model identifier (provider-specific) thinking?: boolean // Enable thinking/reasoning mode (auto-detected if omitted) temperature?: number // Sampling temperature 0.0–1.0 maxTokens?: number // Maximum output tokens numCtx?: number // Exact provider context window (Ollama num_ctx); ignored by providers without a context knob}// String form — simple model selection.withModel("claude-opus-4-8")
// ModelParams form — local model with thinking mode.withModel({ model: "qwen3:14b", thinking: true, temperature: 0.7 })
// ModelParams form — cap token budget.withModel({ model: "gpt-4o", maxTokens: 2048 })
// ModelParams form — pin the exact context window sent to the provider.withModel({ model: "qwen3:14b", numCtx: 32768 })numCtx overrides the assumed/maximum context length with the exact window the
provider receives. Honored by providers that expose a context-window knob
(Ollama maps it to num_ctx); cloud providers that don’t expose one ignore it.
It is also a first-class AgentConfig field, so it
round-trips through toConfig() / fromJSON() and the config-driven path.
ThinkingOptions
Section titled “ThinkingOptions”.withThinking() is the rich-config home for native reasoning mode across all
providers. .withModel({ thinking: true }) remains the quick boolean shortcut.
Thinking stays off unless explicitly enabled — undefined never
auto-enables.
interface ThinkingOptions { enabled?: boolean // Tri-state mirror of the thinking flag effort?: "low" | "medium" | "high" // OpenAI reasoning_effort; advisory for other providers budgetTokens?: number // Explicit thinking budget in tokens (still clamped)}// Boolean form — enable / disable.withThinking() // enable.withThinking(false) // disable
// Rich form — effort + budget.withThinking({ effort: "high", budgetTokens: 4096 })Memory
Section titled “Memory”| Method | Signature | Description |
|---|---|---|
withMemory | (options?: MemoryOptions | "1" | "2") => this | Enable memory — OFF by default as of v0.12 (a bare build is stateless). Prefer .withMemory() or .withMemory({ tier: "enhanced", ... }). Strings "1" / "2" still work with a deprecation warning ("1" → standard, "2" → enhanced). Also enabled by HarnessProfile.balanced() / .intelligent(). |
MemoryOptions
Section titled “MemoryOptions”| Field | Type | Default / notes |
|---|---|---|
tier | "standard" | "enhanced" | "standard" — enhanced = 4-layer memory + embeddings |
dbPath | string | SQLite path (default under .reactive-agents/memory/{agentId}/) |
maxEntries | number | Compaction cap |
capacity | number | Working memory slots (default 7) |
evictionPolicy | "fifo" | "lru" | "importance" | Working set eviction |
retainDays | number | Episodic retention |
importanceThreshold | number | Semantic inclusion threshold |
Execution
Section titled “Execution”| Method | Signature | Description |
|---|---|---|
withMaxIterations | (n: number) => this | Max agent loop iterations (default: 10) |
withMinIterations | (n: number) => this | Minimum iterations before final-answer is permitted — prevents fast-path exit on complex tasks |
withContextProfile | (profile: Partial<ContextProfile>) => this | Model-adaptive context overrides: tool result size/preview limits, tool schema verbosity, iterations, temperature, context-window tokens |
withStrictValidation | () => this | Throw at build time if required config is missing (provider, model, etc.) |
withLazyValidation | () => this | Keep the missing-API-key and unknown-for-provider-model checks as warnings even under withStrictValidation — build() succeeds and the clean typed failure surfaces at run() time. Useful when keys are injected after construction, or in tooling that eagerly constructs many configs. Keyless providers (ollama, test) are exempt from the key gate anyway. Env equivalent: REACTIVE_AGENTS_LAZY_VALIDATION=1 |
withTimeout | (ms: number) => this | Execution timeout in milliseconds for the whole agent run (all iterations combined). Throws TimeoutError if exceeded |
withLlmTimeout | (ms: number) => this | Per-LLM-call timeout in milliseconds — bounds a single provider request, distinct from withTimeout. Honored by the Ollama/local provider (maps to LLMConfig.ollamaTimeoutMs; equivalent to the OLLAMA_TIMEOUT_MS env var but scoped to this agent — useful to tolerate cold model loads, e.g. .withLlmTimeout(600_000)). Hosted providers (Anthropic/OpenAI/Gemini) ignore it |
withRetryPolicy | (policy: RetryPolicy) => this | Retry on transient LLM failures. { maxRetries: number, backoffMs: number } |
ContextProfile fields
Section titled “ContextProfile fields”| Field | Type | Description |
|---|---|---|
tier | "local" | "mid" | "large" | "frontier" | Model tier — controls which defaults are applied |
toolResultMaxChars | number | Max characters per compressed tool result before overflow compression |
toolResultPreviewItems | number | Array items shown in a compressed tool result preview |
toolSchemaDetail | "names-only" | "names-and-types" | "full" | Tool schema verbosity in the system prompt |
maxIterations | number (optional) | Max kernel iterations before failing |
temperature | number (optional) | LLM sampling temperature |
maxTokens | number (optional) | Context-window token cap used by pressure gates and message compaction |
recentObservationsLimit | number (optional) | When > 0, append the last N tool observations to the system prompt (default: 0) |
// Lean context for local small models.withContextProfile({ tier: "local" })
// Manual overrides for a specific task.withContextProfile({ maxTokens: 4000, toolResultMaxChars: 800, toolResultPreviewItems: 3, toolSchemaDetail: "names-and-types",})See Context Engineering for full tier defaults.
Optional features
Section titled “Optional features”| Method | Description |
|---|---|
withGuardrails(options?) | Toggle detectors: { injection?, pii?, toxicity?, customBlocklist? }. All default on when guardrails are enabled. |
withKillSwitch() | Pause / resume / stop / terminate via KillSwitchService |
withBehavioralContracts(contract) | Rules such as deniedTools, allowedTools, maxIterations, etc. |
withContract(contract) | Declare a TaskContract: { prompt, tools: ToolRequirement[], fixtures?, modelFloor?, success }. Required tools become an execute-time gate; forbidden tools are excluded from the tool schema and enforced at the shared tool-execution gate on every strategy (plan-execute/blueprint planned steps and the code-action sandbox included) — a violating call is blocked and recorded, never executed. Validated at build(). The declared contract is now load-bearing: it is compiled into the run’s typed goal, the terminal gate checks requirement satisfaction against the evidence ledger, and result.receipt.deliverables[] reports each declared output as produced or missing. |
withVerification(options?) | Post-output checks — toggles and thresholds: semanticEntropy, factDecomposition, multiSource, hallucinationDetection, passThreshold, … |
withGrounding(options) | Opt-in numeric evidence-grounding (off by default): { mode: "block" | "warn", tolerance?, maxRetries? }. Checks figures in the final answer against the full tool data with rounding tolerance. warn = advisory; block = one corrective retry then degrade to warn (never hard-fails). Scaffold-leak detection ([STORED:]/_tool_result_N echoed as the answer) is always-on, independent of this. |
withFabricationGuard(mode?) | Configure the always-on verifier check that rejects invented empirical performance measurements (benchmark timings, % speed-ups) absent from the tool-observation corpus. On by default ("block") — no call needed for protection. Use this only to soften ("warn", advisory) or disable ("off"). High-precision: only perf measurements are policed; counts, prices, and Big-O are ignored, and a claim grounded by a real benchmark/execution tool always passes. Also settable via RA_FABRICATION_GUARD env var (this method wins). mode: "block" | "warn" | "off". |
withStallPolicy(policy) | Tune the stall / no-progress policy — how the harness reacts when the model ignores required-tool nudges. Sensible defaults apply when unset: tolerate 2 consecutive ignored nudges before fast-escalating (deliver accumulated artifacts, else fail) instead of looping to the full nudge cap, and escalate nudge wording on repeats. Bounds wasted iterations/tokens on stuck runs; legitimately-progressing runs are untouched (progress resets the ignored streak). { ignoredNudgeTolerance?, escalateNudgeContent? }. |
withReceiptSigning(options) | Opt in to an Ed25519 provenance signature on every trust receipt (off by default — receipts are unsigned). { privateKeyJwk }; also settable via the RA_RECEIPT_KEY env var (this option wins when both are present). The signature certifies this receipt, this run, untampered — it never certifies the answer’s correctness. Generate a keypair with generateReceiptKeyPair() and verify with verifyReceipt(result.receipt!). See The Process Model. |
withCostTracking(options?) | Budgets in USD: { perRequest?, perSession?, daily?, monthly? } plus cost estimation / analytics |
withBudget(limits) | Hard in-loop killswitch: { tokenLimit?, costLimit? }. Caps cumulative tokens / USD and stops the loop when hit — distinct from withCostTracking() accounting. Also set by HarnessProfile budget composition. |
withModelRouting(options?) | Opt-in cost-aware model routing (off by default). Routes each run to the cheapest capable model of the configured provider, picked by task complexity, on both the inline and reasoning paths. Stays within the provider’s tiers (haiku/sonnet/opus cost ladder → the provider’s models); capability-gated (never routes a large-input task below a model whose context window fits); advisory (degrades to the configured model on any error). { tierModels?: Partial<Record<"haiku"|"sonnet"|"opus", string>>, minTier? }. |
withLongHorizon() | Opt-in, off by default. Mode toggle (no arguments). Scales the reasoning kernel’s guard thresholds (stall, consecutive-thoughts, redirect/nudge budgets) proportionally to maxIterations instead of using absolute counts, so a run configured for 40+ iterations of tool work isn’t tripped by guards tuned for short runs. Verified to let a long-horizon task run to completion; not yet lift-gated for default-on. When not called, horizonProfile stays unset and behavior is byte-identical to the default. |
withAdaptiveHarness() | Opt-in and experimental. Mode toggle (no arguments). A policy compiler derives the run’s harness (strategy, budget class, guard/horizon profile, tool surface, verifier tier, memory posture) at run-start from the model’s capability tier + calibration, the compiled contract’s horizon, and the task classification; the plan supplies DEFAULTS while any explicit .withX() you set OVERRIDES the corresponding field. Mid-run it recompiles on live progress evidence — deepening scaffolding when the run struggles, leaning when it flows. Under active validation: the cross-tier ablation was inconclusive (n=1 dev-hardware noise), so it is not default-on and sits under the project lift-gate veto. Zero cost when not called. |
withModelPricing(registry) | Per-model $/1M tokens: { "model-id": { input, output } } |
withDynamicPricing(provider) | Remote pricing (openRouterPricingProvider, etc.) fetched at build time |
withCircuitBreaker(config?) | LLM call circuit breaker (@reactive-agents/llm-provider CircuitBreakerConfig) |
withRateLimiting(config?) | Throttle LLM requests (requestsPerMinute, tokensPerMinute, concurrency, …) |
withReasoning(options?) | Strategies + ICS — see ReasoningOptions |
withTools(options?) | Tool layer — see ToolsOptions below |
withDocuments(docs) | Chunk + index DocumentSpec[] for RAG; retrieval is served through the unified find meta-tool. Enables tools if needed |
withRequiredTools(config) | Tools that must run before success — { tools?, adaptive?, maxRetries? }. When adaptive: true, the framework also auto-sets a per-tool call budget of 3 for search-type tools to prevent infinite research loops. |
withObservability(options?) | Metrics dashboard, tracing, verbosity. Options: verbosity ("minimal"|"normal"|"verbose"|"debug"), live (stream phase events), file (JSONL path), logPrefix, logModelIO (when true or when verbosity: "debug", logs the complete FC conversation thread with role labels [USER]/[ASSISTANT]/[TOOL] and raw LLM response for every iteration — essential for debugging prompt issues). Note: observability is enabled at "normal" verbosity by default — you only need .withObservability() to customize the verbosity level or output format. Also the single entry point for run telemetry (telemetry: true | TelemetryConfig — privacy modes, default isolated) and trace-file control (tracing: false disables; tracing: { dir } sets the directory). |
withCortex(url?) | Enable best-effort Cortex reporting. Streams all EventBus events to the Cortex local studio over WebSocket (/ws/ingest). URL priority: explicit url arg → CORTEX_URL env → http://localhost:4321. Connection is non-blocking — if Cortex is unreachable the agent continues normally. See Cortex Studio for the full feature reference. |
withStreaming(options?) | Default density for agent.runStream(): { density?: "tokens" | "full" } |
withPrompts(options?) | { templates?: PromptTemplate[] } |
withExperienceLearning() | ExperienceStore cross-agent tips |
withLearning(opts?) | Enable the cross-run learning store: { tier?: "standard" | "enhanced", dbPath? }. Experience + skill learning that compounds across sessions. |
withSkillPersistence(enabled?) | Persist learned SkillRecords across process restarts (SQLite-backed). Defaults to true when called; also enabled by HarnessProfile.intelligent(). |
withMemoryConsolidation(config?) | Background consolidation: { threshold?, decayFactor?, pruneThreshold? } |
withSelfImprovement() | Strategy outcome logging for later bootstrap hints |
withAudit() | Audit trail |
withEvents() | Ensures EventBus wiring for agent.subscribe() |
withGateway(options?) | Heartbeats, crons, webhooks, policies, port, accessControl, … |
withErrorHandler(handler) | Observe-only callback on agent.run() failures — does not swallow errors |
withFallbacks(config) | { providers } — an ordered provider cascade. The primary provider runs first; on any error the next provider in the list is tried, in order (no error threshold, no 429/cost-specific logic). |
withLogging(config) | makeLoggerService — { level?, format?, output?: "console" | "file" | WritableStream, filePath?, maxFileSizeBytes?, maxFiles? } |
withHealthCheck() | Enables agent.health() |
withVerificationStep(config?) | Post-answer LLM self-review. { mode: "reflect" | "loop", prompt? }. Reflect mode runs one LLM review; on a REVISE verdict it re-runs the answer once with the verification feedback so the verdict shapes the final answer. Loop mode (V1.1) re-enters the ReAct loop. |
withOutputValidator(fn, opts?) | Validate output before accepting. fn(output) => { valid, feedback? }. Failed validation injects feedback and retries (opts.maxRetries, default 2) |
withCustomTermination(fn) | Re-run until fn({ output }) === true, up to 3 additional times. For domain-specific completion criteria |
withTaskContext(record) | Record<string, string> of background facts injected into reasoning context — distinct from system prompt instructions |
withReactiveIntelligence(false) | Disable the Reactive Intelligence layer (enabled by default). |
withReactiveIntelligence(options?) | Entropy, controller, telemetry, hooks (onEntropyScored, onControllerDecision, …). See Reactive Intelligence |
withSkills(config) | { paths } — one or more SKILL.md directories (required). A path-less call, or the removed packages / evolution / overrides keys, now throws. |
withMetaTools(config?) | Conductor meta-tools; pass false to turn off defaults when using .withTools(). See MetaToolsConfig |
withHarness(fn) | Alias for .compose(fn) — attach a harness transform over tagged chokepoints. See Compose API. |
withProfile(profile) | Apply a HarnessProfile preset (lean() / balanced() / intelligent()) — the canonical one-line capability composition. Later .withX() calls override the preset. |
withLeanHarness() | Disable the default harness capabilities. Superseded by HarnessProfile.lean(), which additionally disables reactive intelligence (this method does not). Still functional. |
withCalibration(mode) | Control per-model adaptive calibration: CalibrationMode ("auto" | "off" | "skip" | …). When unset, calibration auto-enables if reasoning is on; passing "off"/"skip" is an explicit opt-out that is now honored even when reasoning is enabled. See LLM Providers. |
withTracing(opts?) | Write structured trace files ({ dir? }) for rax diagnose. Governed separately from the metrics dashboard; also toggled by REACTIVE_AGENTS_TRACE. Disable with .withObservability({ tracing: false }). |
withChannels(config) | Wire @reactive-agents/channels sender-policy access control (ChannelsConfig). See Messaging Channels. |
withOutputSchema(schema, options?) | Typed structured output. Attach any Standard Schema (Zod / Valibot / ArkType) or Effect Schema; the result carries a typed result.object (or result.objectError on parse failure). Options: { mode?: "auto" | "grounded", onParseFail?: "lenient" | "throw" }. Builder-only — set before .build(). See Typed Structured Output. |
withDurableRuns(options?) | Persist run state so a crashed or paused run can resume from its last checkpoint. Exposes agent.resumeRun(runId) and agent.listRuns({ status? }). Config hash = system prompt + provider. See Durable Execution. |
withApprovalPolicy(policy) | Durable human-in-the-loop. { tools?, requireFor?, mode? } — gated tool calls pause the run (mode: "detach", default with durable runs) and persist awaiting-approval. Exposes agent.approveRun/denyRun/listPendingApprovals. detach requires .withDurableRuns(). See Durable HITL. |
withUserInteraction() | Enable agent-initiated user interaction (Agentic UI). The model may call request_user_input to pause the run durably and ask the human for a form / choice / confirmation; agent.respondToInteraction(...) resumes it. Requires .withDurableRuns() (interaction pauses persist to the durable store). Distinct from withApprovalPolicy(), which gates tool calls the model already chose (human approves/denies) — withUserInteraction() lets the agent proactively request input. Zero cost when not called. |
ToolsOptions
Section titled “ToolsOptions”| Field | Description |
|---|---|
tools | { definition: ToolDefinition, handler: (args) => Effect.Effect<unknown> }[] — custom tools (handlers return Effect) |
resultCompression | ResultCompressionConfig — previews, overflow keys, transforms |
allowedTools | If set, only these tool names are exposed to the model (others filtered) — and the allowlist is enforced at execution on every strategy: a call outside it is blocked, never run |
adaptive | Adaptive tool listing from task text (heuristic), reduces noise for small models |
terminal | true | ShellExecuteConfig — opt in to the sandboxed shell-execute tool (command allowlist, blocklist, locked cwd) |
MetaToolsConfig
Section titled “MetaToolsConfig”| Field | Description |
|---|---|
brief, find, pulse, recall | Enable each Conductor meta-tool |
harnessSkill | boolean, path string, or { frontier?, local? } for harness skill source |
findConfig, pulseConfig, recallConfig | Fine-tuning (scopes, previews, LLM pulse behavior, …) |
ReasoningOptions
Section titled “ReasoningOptions”interface ReasoningOptions { /** * Which strategy to use. Defaults to "reactive". * "adaptive" requires adaptive.enabled: true. */ defaultStrategy?: | 'reactive' | 'reflexion' | 'plan-execute-reflect' | 'tree-of-thought' | 'adaptive'
/** * Per-strategy overrides (iterations, temperatures, plan knobs, etc.). * Each bundle may also set ICS fields (`synthesis`, `synthesisModel`, `synthesisProvider`, * `synthesisStrategy`, `synthesisTemperature`) — they override the top-level synthesis * options for that strategy only (see Intelligent Context Synthesis). */ strategies?: Partial<{ reactive: ReasoningConfig['strategies']['reactive'] & StrategySynthesisFields planExecute: ReasoningConfig['strategies']['planExecute'] & StrategySynthesisFields treeOfThought: ReasoningConfig['strategies']['treeOfThought'] & StrategySynthesisFields reflexion: ReasoningConfig['strategies']['reflexion'] & StrategySynthesisFields }>
/** Adaptive strategy config. Must set enabled: true when defaultStrategy is "adaptive". */ adaptive?: { enabled?: boolean // Required for adaptive strategy learning?: boolean // Enable cross-run learning (default: false) }
/** Max iterations of the reasoning loop (default: 10). */ maxIterations?: number
/** * Automatically switch to a better-suited strategy when the current one appears stuck * (repeated tool calls, repeated thoughts, or consecutive think-only steps). * Default: false. */ enableStrategySwitching?: boolean
/** * Maximum number of strategy switches allowed in a single run. * Default: 1. */ maxStrategySwitches?: number
/** * When set, bypasses the LLM evaluator and always switches to this strategy on loop * detection. Useful when you want deterministic switching without the extra LLM call. * Example: "plan-execute-reflect" */ fallbackStrategy?: string
/** ICS default mode: auto (heuristic), fast (templates), deep (LLM), custom, or off. */ synthesis?: 'auto' | 'fast' | 'deep' | 'custom' | 'off' /** Model for deep synthesis when different from the executing model. */ synthesisModel?: string /** Provider for the synthesis model when different from the executing provider. */ synthesisProvider?: string /** Custom synthesis pipeline when `synthesis: "custom"`. */ synthesisStrategy?: SynthesisStrategy /** Temperature for deep synthesis LLM calls. */ synthesisTemperature?: number}
/** ICS-only fields allowed on each `strategies.*` bundle (merged with top-level synthesis). */interface StrategySynthesisFields { synthesis?: 'auto' | 'fast' | 'deep' | 'custom' | 'off' synthesisModel?: string synthesisProvider?: string synthesisStrategy?: SynthesisStrategy synthesisTemperature?: number}Per-strategy objects under strategies also accept strategy-specific fields from @reactive-agents/reasoning (for example kernelMaxIterations on the reflexion bundle).
At runtime, ReasoningOptions may also include a non-JSON synthesisStrategy function when using synthesis: "custom" (omitted from toConfig() / JSON).
Examples:
// Default: ReAct with no options.withReasoning()
// Switch to Plan-Execute-Reflect strategy.withReasoning({ defaultStrategy: "plan-execute-reflect" })
// Adaptive strategy (must set adaptive.enabled).withReasoning({ defaultStrategy: "adaptive", adaptive: { enabled: true } })
// Auto-switch when stuck, up to 2 times, via LLM evaluator.withReasoning({ enableStrategySwitching: true, maxStrategySwitches: 2 })
// Auto-switch deterministically (no extra LLM call) to plan-execute-reflect.withReasoning({ enableStrategySwitching: true, fallbackStrategy: "plan-execute-reflect" })
// ICS: fast templates globally, but deep LLM synthesis when running ReAct.withReasoning({ synthesis: "fast", strategies: { reactive: { synthesis: "deep", synthesisModel: "claude-haiku-4-5-20251001" } },})When enableStrategySwitching is active, two EventBus events are emitted around each switch:
StrategySwitchEvaluated— after the evaluator runs, before the switch (includeswillSwitch,rationale,recommendedStrategy)StrategySwitched— after the new strategy takes over (includesfromStrategy,toStrategy,switchNumber,stepsCarriedOver)
See Automatic Strategy Switching for full details on loop detection triggers, handoff context, and EventBus subscription examples.
RequiredToolsConfig
Section titled “RequiredToolsConfig”interface RequiredToolsConfig { /** Static list of tool names the agent MUST call before answering. */ tools?: string[] /** Enable adaptive inference — LLM analyzes task + tools to determine required tools. */ adaptive?: boolean /** Number of retry loops if required tools are missed (default: 2). */ maxRetries?: number}Examples:
// Static required tools — agent must call web-search before answering.withRequiredTools({ tools: ["web-search"] })
// Adaptive inference — LLM determines which tools are required per-task.withRequiredTools({ adaptive: true })
// Both — static list as baseline, adaptive for additional inference.withRequiredTools({ tools: ["web-search"], adaptive: true, maxRetries: 3 })When adaptive: true, the framework calls the LLM with the task description and available tool schemas to infer which tools are required. The inferred list is merged with any static tools list. A hallucination guard ensures only actual tool names are included.
A2A protocol
Section titled “A2A protocol”| Method | Signature | Description |
|---|---|---|
withA2A | (options?: A2AOptions) => this | A2A JSON-RPC server — port (default 3000), basePath (default /) |
withAgentTool | (name: string, agent: { name: string; description?: string; provider?: string; model?: string; tools?: string[]; maxIterations?: number; systemPrompt?: string; persona?: AgentPersona }) => this | Static sub-agent as a tool |
withDynamicSubAgents | (options?: { maxIterations?: number }) => this | spawn-agent for runtime sub-agents |
withRemoteAgent | (name: string, remoteUrl: string) => this | Remote A2A agent as a tool |
| Method | Signature | Description |
|---|---|---|
withMCP | (config: MCPServerConfig | MCPServerConfig[]) => this | Connect to MCP servers. Accepts a single config or array. Automatically enables .withTools(). |
MCPServerConfig
Section titled “MCPServerConfig”| Field | Type | Transport | Description |
|---|---|---|---|
name | string | all | Unique name for this server. Tool names are prefixed {name}/ |
transport | "stdio" | "streamable-http" | "sse" | "websocket" | all | Protocol to use. Use "streamable-http" for modern remote servers, "stdio" for local subprocesses |
command | string | stdio | Executable to launch ("bunx", "docker", "python", absolute path, etc.) |
args | string[] | stdio | Arguments passed to command. Includes package names, flags, Docker image, etc. |
env | Record<string, string> | stdio | Extra env vars merged on top of the parent process environment. Use for per-server secrets |
cwd | string | stdio | Working directory for the subprocess. Defaults to parent process cwd |
endpoint | string | streamable-http, sse, websocket | HTTP/WebSocket URL ("https://mcp.example.com", "ws://localhost:8000/mcp") |
headers | Record<string, string> | streamable-http, sse | HTTP headers sent on every request. Use for Authorization, x-api-key, etc. |
Examples:
// stdio: npm package via bunx{ name: "filesystem", transport: "stdio", command: "bunx", args: ["-y", "@modelcontextprotocol/server-filesystem", "."] }
// stdio: with per-server secret{ name: "github", transport: "stdio", command: "bunx", args: ["-y", "@modelcontextprotocol/server-github"], env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GH_TOKEN ?? "" } }
// stdio: Docker container with networking{ name: "my-server", transport: "stdio", command: "docker", args: ["run", "-i", "--rm", "--network", "host", "ghcr.io/org/mcp-server"] }
// streamable-http: modern cloud server with Bearer auth{ name: "stripe", transport: "streamable-http", endpoint: "https://mcp.stripe.com", headers: { Authorization: `Bearer ${process.env.STRIPE_KEY}` } }
// sse: legacy remote server with API key{ name: "legacy", transport: "sse", endpoint: "https://api.example.com/mcp", headers: { "x-api-key": process.env.API_KEY ?? "" } }Lifecycle
Section titled “Lifecycle”| Method | Signature | Description |
|---|---|---|
withHook | (hook: LifecycleHook) => this | Register a lifecycle hook |
LifecycleHook
Section titled “LifecycleHook”Use the exported LifecycleHook type from @reactive-agents/runtime. Handlers return Effect.Effect<ExecutionContext, ExecutionError> (import Effect from "effect").
LifecyclePhase values include: bootstrap, guardrail, cost-route, strategy-select, think, act, observe, verify, memory-flush, cost-track, audit, complete.
Testing
Section titled “Testing”| Method | Signature | Description |
|---|---|---|
withTestScenario | (turns: TestTurn[]) => this | Deterministic test provider. Forces provider: "test". Turns are TestTurn values from @reactive-agents/llm-provider: { text? }, { toolCall? }, { toolCalls? }, { json? }, { error? }, optional match? (regex) per turn |
See Testing agents and Configuration for examples.
Advanced
Section titled “Advanced”| Method | Signature | Description |
|---|---|---|
withLayers | (layers: Layer<any, any>) => this | Add custom Effect Layers to the runtime |
Build Methods
Section titled “Build Methods”build()
Section titled “build()”async build(): Promise<ReactiveAgent>Creates the agent, resolving the full Layer stack. Returns a ReactiveAgent instance.
buildEffect()
Section titled “buildEffect()”buildEffect(): Effect.Effect<ReactiveAgent, Error>Creates the agent as an Effect for composition in Effect programs.
runOnce(input: string): Promise<AgentResult>
Section titled “runOnce(input: string): Promise<AgentResult>”Builds the agent, runs a single task, disposes all resources, and returns the result — in one call. Use this for one-shot scripts where you don’t need to hold a reference to the agent.
const result = await ReactiveAgents.create() .withProvider('anthropic') .withReasoning() .runOnce('Summarize the README in one paragraph')
console.log(result.output)// Resources are already cleaned upReactiveAgent
Section titled “ReactiveAgent”The facade returned by build().
Resource Management
Section titled “Resource Management”Agents that use MCP servers (stdio transport) or other subprocess-based resources must be disposed after use, otherwise the process will hang on open pipes. Three patterns are available:
Pattern 1 — await using (recommended)
Section titled “Pattern 1 — await using (recommended)”Uses the Explicit Resource Management protocol introduced in TypeScript 5.2. The agent is disposed automatically when the enclosing block exits, whether normally or via an exception.
await using agent = await ReactiveAgents.create() .withProvider("anthropic") .withMCP({ name: "filesystem", transport: "stdio", command: "npx", args: ["@modelcontextprotocol/server-filesystem", "."] }) .withReasoning() .build();
const result = await agent.run("List the project files.");console.log(result.output);// agent.dispose() is called automatically hereRequires "lib": ["ES2022", "ESNext"] or "target": "ES2022" in your tsconfig.json.
Pattern 2 — runOnce() (one-shot)
Section titled “Pattern 2 — runOnce() (one-shot)”If you only need a single result and don’t want to manage the agent handle at all, use the builder’s runOnce() method. It builds, runs, and disposes in one call.
const result = await ReactiveAgents.create() .withProvider('anthropic') .withMCP({ name: 'filesystem', transport: 'stdio', command: 'npx', args: ['@modelcontextprotocol/server-filesystem', '.'], }) .withReasoning() .runOnce('List the project files.')
console.log(result.output)// Resources already cleaned upPattern 3 — dispose() (explicit)
Section titled “Pattern 3 — dispose() (explicit)”Call dispose() manually in a finally block when you need to reuse the agent across multiple calls before cleaning up.
const agent = await ReactiveAgents.create() .withProvider('anthropic') .withReasoning() .build()
try { const r1 = await agent.run('First task') const r2 = await agent.run('Second task') console.log(r1.output, r2.output)} finally { await agent.dispose()}| Pattern | When to use |
|---|---|
await using | General purpose — automatic cleanup, works with try/catch |
runOnce() | Single-shot scripts and one-liners |
dispose() | Multiple sequential runs before teardown |
run(input, options?): Promise<AgentResult>
Section titled “run(input, options?): Promise<AgentResult>”Run a task with the given input. Returns the result with output and metadata.
Options: { taskId?, history?, onApproval? }. On a durable agent with
.withApprovalPolicy({ mode: "detach" }), a gated tool call pauses: the
result carries status: "awaiting-approval" + pendingApproval (resume with
approveRun/denyRun). Pass onApproval to handle the pause→decide→resume loop
in this one call — (pending) => boolean | { approve, reason } (sync or async);
returns the final result. See Durable HITL.
runStream(input, options?): AsyncGenerator<AgentStreamEvent>
Section titled “runStream(input, options?): AsyncGenerator<AgentStreamEvent>”Token and phase streaming. Options: { density?: "tokens" | "full", signal?: AbortSignal }. Default density comes from .withStreaming() or "tokens". Ends with StreamCompleted, StreamError, or StreamCancelled.
runEffect(input: string): Effect.Effect<AgentResult, Error>
Section titled “runEffect(input: string): Effect.Effect<AgentResult, Error>”Run a task as an Effect for composition (see Effect-TS primer).
streamObject(input): AsyncGenerator<{ object: DeepPartial<T> }>
Section titled “streamObject(input): AsyncGenerator<{ object: DeepPartial<T> }>”Stream typed structured output field-by-field as it fills in. Requires .withOutputSchema(). Each yield carries a deep-partial of the schema type; the final yield is the validated object. See Typed Structured Output.
resumeRun(runId: string): Promise<AgentResult>
Section titled “resumeRun(runId: string): Promise<AgentResult>”Resume a crashed or paused durable run from its last checkpoint. Requires .withDurableRuns(). See Durable Execution.
listRuns(filter?: { status? }): Promise<readonly RunRecord[]>
Section titled “listRuns(filter?: { status? }): Promise<readonly RunRecord[]>”List persisted durable runs, optionally filtered by lifecycle status (e.g. { status: "running" }). Requires .withDurableRuns().
listPendingApprovals(): Promise<readonly PendingApproval[]>
Section titled “listPendingApprovals(): Promise<readonly PendingApproval[]>”List runs paused awaiting a human decision, each with the pending action (runId, gateId, toolName, args, task). Requires .withDurableRuns(). See Durable HITL.
approveRun(runId, opts?): Promise<AgentResult>
Section titled “approveRun(runId, opts?): Promise<AgentResult>”Approve a paused run and resume it to completion — the agent executes the gated call. Callable from any process. Throws ApprovalStateError if the run has no pending approval.
denyRun(runId, reason): Promise<AgentResult>
Section titled “denyRun(runId, reason): Promise<AgentResult>”Deny a paused run’s action and resume to completion — the agent observes the denial and continues without running the call.
Dynamic tools & RAG (runtime)
Section titled “Dynamic tools & RAG (runtime)”| Method | Description |
|---|---|
registerTool(definition, handler) | Register a tool after build; handler returns Effect |
unregisterTool(name) | Remove a previously registered custom tool |
ingest(content, { source, format?, ... }) | Ingest text into RAG when tools / withDocuments enabled |
chat(message: string, options?: ChatOptions): Promise<ChatReply>
Section titled “chat(message: string, options?: ChatOptions): Promise<ChatReply>”Conversational Q&A with the agent. Routes automatically:
- Direct LLM path — for questions, summaries, and status checks (fast, no tools)
- ReAct loop path — for tool-capable requests (search, fetch, write, create, etc.)
Injects context from the last run’s debrief so the agent can answer “what did you do last time?” accurately.
const reply = await agent.chat('What did you accomplish last run?')console.log(reply.message)
// Force tool-capable pathconst reply2 = await agent.chat('Search for the latest AI news', { useTools: true,})console.log(reply2.toolsUsed) // ["web-search"]interface ChatReply { message: string toolsUsed?: string[] // Set when tools were invoked fromMemory?: boolean // Set when answered from debrief context}
interface ChatOptions { useTools?: boolean // Override auto-routing maxIterations?: number // Cap for tool-capable path (default: 5)}session(options?): AgentSession
Section titled “session(options?): AgentSession”Start a multi-turn conversation session with auto-managed history. Conversation history is forwarded to the LLM on every subsequent turn.
Pass { persist: true, id: "my-session" } to persist conversation history to SQLite via SessionStoreService. Persistent sessions survive process restarts and can be resumed by passing the same id. Persistence requires the memory layer (.withMemory()): the session store is wired only when memory is enabled — without it, persist: true silently no-ops and the session stays in-memory only.
// In-memory session (default)const session = agent.session()
const r1 = await session.chat('What are the key findings from your last run?')const r2 = await session.chat('Tell me more about the first finding')// r2 has full context of r1
// Persisted session — survives process restartsconst persistedSession = agent.session({ persist: true, id: 'research-session-1',})await persistedSession.chat('Start researching quantum computing')// On next run, restore the session:const restoredSession = agent.session({ persist: true, id: 'research-session-1',})await restoredSession.chat('Continue where we left off')
const history = session.history() // ChatMessage[]await session.end() // Flushes history to storage (if persisted) and clears the in-memory copy — the DB record is kept// session() options{ persist?: boolean // Persist history to SQLite via SessionStoreService (requires .withMemory()) id?: string // Session ID for persistence (auto-generated if omitted)}
interface AgentSession { chat(message: string): Promise<ChatReply> history(): ChatMessage[] end(): Promise<void>}health(): Promise<HealthResult>
Section titled “health(): Promise<HealthResult>”Requires .withHealthCheck() to be enabled.
Returns a structured health snapshot of all agent subsystems. Use for readiness probes, liveness checks, and monitoring dashboards.
const health = await agent.health()console.log(health.status) // "healthy" | "degraded" | "unhealthy"
for (const check of health.checks) { console.log(`${check.name}: ${check.status} — ${check.message}`)}interface HealthResult { status: 'healthy' | 'degraded' | 'unhealthy' checks: Array<{ name: string status: 'pass' | 'warn' | 'fail' message?: string durationMs?: number }>}cancel(taskId: string): Promise<void>
Section titled “cancel(taskId: string): Promise<void>”Cancel a running task by its ID.
getContext(taskId: string): Promise<unknown>
Section titled “getContext(taskId: string): Promise<unknown>”Get the execution context of a running or completed task.
Lifecycle Control
Section titled “Lifecycle Control”Requires .withKillSwitch() to be enabled.
| Method | Signature | Description |
|---|---|---|
pause() | () => Promise<void> | Pause execution at the next phase boundary. Blocks until resume() is called |
resume() | () => Promise<void> | Resume a paused agent |
stop(reason) | (reason: string) => Promise<void> | Graceful stop — signals intent; agent completes current phase then exits |
terminate(reason) | (reason: string) => Promise<void> | Immediate termination (also triggers kill switch) |
Event Subscription
Section titled “Event Subscription”Requires an EventBus to be wired (any feature that enables it, e.g., .withObservability()).
subscribe is overloaded — pass a tag for type-narrowed access, or omit it for a catch-all:
// ── Tag-filtered: event is narrowed to the exact payload type ──────────────const unsub = await agent.subscribe('AgentCompleted', (event) => { // TypeScript knows event has: taskId, agentId, success, totalIterations, // totalTokens, durationMs — no _tag check, no cast needed console.log(`Done in ${event.durationMs}ms, ${event.totalTokens} tokens`)})unsub()
// ── Catch-all: receives the full AgentEvent union ──────────────────────────const unsub2 = await agent.subscribe((event) => { // Discriminate via event._tag when handling multiple types in one handler if (event._tag === 'ToolCallStarted') console.log(`Tool: ${event.toolName}`) if (event._tag === 'LLMRequestStarted') console.log(`Model: ${event.model}`)})unsub2()TypeScript signatures:
// Tag-filtered — event type is automatically narrowedsubscribe<T extends AgentEventTag>( tag: T, handler: (event: Extract<AgentEvent, { _tag: T }>) => void,): Promise<() => void>;
// Catch-all — full AgentEvent unionsubscribe(handler: (event: AgentEvent) => void): Promise<() => void>;The AgentEventTag and TypedEventHandler<T> helpers are exported from @reactive-agents/core for use in your own service code:
import { Effect } from 'effect'import type { AgentEventTag, TypedEventHandler } from '@reactive-agents/core'
// Build a typed handler outside of an inline callbackconst onStepComplete: TypedEventHandler<'ReasoningStepCompleted'> = (event) => { // event.thought, event.action, event.observation — all typed return Effect.log(`Step ${event.step}: ${event.thought ?? event.action}`)}
yield * eventBus.on('ReasoningStepCompleted', onStepComplete)Subscribable event tags:
| Tag | Payload fields |
|---|---|
AgentStarted | taskId, agentId, provider, model, timestamp |
AgentCompleted | taskId, agentId, success, totalIterations, totalTokens, durationMs |
LLMRequestStarted | taskId, requestId, model, provider, contextSize |
LLMRequestCompleted | taskId, requestId, tokensUsed, durationMs |
ReasoningStepCompleted | taskId, strategy, step, thought|action|observation |
ToolCallStarted | taskId, toolName, callId |
ToolCallCompleted | taskId, toolName, callId, success, durationMs |
FinalAnswerProduced | taskId, strategy, answer, iteration, totalTokens |
GuardrailViolationDetected | taskId, violations, score, blocked |
ExecutionPhaseEntered | taskId, phase |
ExecutionPhaseCompleted | taskId, phase, durationMs |
ExecutionHookFired | taskId, phase, timing |
ExecutionCancelled | taskId |
MemoryBootstrapped | agentId, tier |
MemoryFlushed | agentId |
AgentPaused | agentId, taskId |
AgentResumed | agentId, taskId |
AgentStopped | agentId, taskId, reason |
TaskCompleted | taskId, success |
GatewayStarted | agentId, timestamp |
GatewayStopped | agentId, reason |
GatewayEventReceived | agentId, eventId, source, category |
ProactiveActionInitiated | agentId, eventId, action |
ProactiveActionCompleted | agentId, eventId, success, durationMs |
ProactiveActionSuppressed | agentId, eventId, reason |
PolicyDecisionMade | agentId, eventId, action, policyTag |
HeartbeatSkipped | agentId, consecutiveSkips, reason |
EventsMerged | agentId, mergedCount, mergeKey |
BudgetExhausted | agentId, tokensUsed, dailyBudget |
StrategySwitchEvaluated | taskId, fromStrategy, recommendedStrategy, rationale, willSwitch |
StrategySwitched | taskId, fromStrategy, toStrategy, switchNumber, stepsCarriedOver |
ProviderFallbackActivated | taskId, fromProvider, toProvider, reason, attemptNumber |
DebriefCompleted | taskId, agentId, debrief |
ChatTurn | taskId, sessionId, role, content, routedVia, tokensUsed? |
MemorySnapshot | taskId, iteration, working, episodicCount, semanticCount, skillsActive |
ContextPressure | taskId, utilizationPct, tokensUsed, tokensAvailable, level |
AgentHealthReport | agentId, status, checks[], uptimeMs |
AgentConnected | agentId, runId, cortexUrl |
AgentDisconnected | agentId, runId, reason |
AgentResult
Section titled “AgentResult”interface AgentResult { output: string // The agent's response success: boolean // Whether the task completed successfully taskId: string // Unique task identifier agentId: string // Agent that ran the task metadata: { duration: number // Execution time in milliseconds cost: number // Estimated cost in USD tokensUsed: number // Total tokens consumed across all LLM calls strategyUsed?: string // Reasoning strategy used (if reasoning enabled) stepsCount: number // Number of reasoning steps / iterations confidence?: 'high' | 'medium' | 'low' // From final-answer tool }
// Enriched fields (present when reasoning is enabled) format?: 'text' | 'json' | 'markdown' | 'csv' | 'html' // Output format declared by agent terminatedBy?: | 'final_answer_tool' // Exited via the final-answer tool call | 'final_answer' // Exited via inline FINAL ANSWER: text | 'max_iterations' // Hit the iteration/llmCalls ceiling | 'end_turn' // LLM stopped generating (no tool call, no final answer) | 'llm_error' // LLM request or stream failed (provider error, network, etc.) | 'abstained' // Agent honestly declined — could not ground an answer (see abstention below) llmCalls?: number // Number of LLM calls made during the kernel loop (available when reasoning is enabled)
// Abstention (present iff terminatedBy === 'abstained') abstention?: { reason: string // Why the agent declined rather than fabricating missing: string[] // What was needed, e.g. "tool:web-search", a clarification }
// Durable HITL (present when a run paused for human approval — see Durable HITL guide) status?: 'completed' | 'awaiting-approval' | 'failed' // defaults to 'completed' when absent pendingApproval?: { runId: string // pass to approveRun(runId) / denyRun(runId, reason) gateId: string toolName: string // the gated tool call awaiting a decision args: unknown }
// Debrief (present when .withMemory() + .withReasoning() are enabled) debrief?: AgentDebrief
// Trust receipt — graded evidence about HOW the answer was produced (see The Process Model). // `receipt.deliverables[]` names each declared deliverable as produced or missing when the run's // compiled contract declared at least one concrete output (absent for pure Q&A runs). receipt?: TrustReceipt}receipt.deliverables is { spec: string; produced: boolean }[] — a partial
multi-file run lists exactly which outputs never landed (produced: false)
instead of claiming success. The full receipt shape, verdicts, and optional
Ed25519 signing are documented in The Process Model.
Abstention (terminatedBy: "abstained")
Section titled “Abstention (terminatedBy: "abstained")”When grounding an answer is structurally impossible — a declared required tool is
absent from the registered tool set, or synthesis was repeatedly rejected as
ungrounded — the harness forces an honest abstained terminal instead of
grinding to max_iterations or letting fabrication through. A genuine
deliverable is never overridden. When this happens, result.terminatedBy is
"abstained" and result.abstention carries the reason plus what was missing:
const result = await agent.run('Summarize the current HN front page')if (result.terminatedBy === 'abstained') { console.log(result.abstention?.reason) // "required tool unavailable; could not ground an answer" console.log(result.abstention?.missing) // ["tool:web-search"]}goalAchieved is false for abstained runs (honest non-achievement). This
run-level surface is distinct from the per-field structured-output abstained
map (.withOutputSchema({ abstainBelow })) — the two are unrelated and may coexist.
AgentDebrief
Section titled “AgentDebrief”A structured post-run synthesis produced automatically when memory is enabled:
interface AgentDebrief { outcome: 'success' | 'partial' | 'failed' summary: string // 2-3 sentence narrative keyFindings: string[] errorsEncountered: string[] lessonsLearned: string[] // Auto-fed to ExperienceStore confidence: 'high' | 'medium' | 'low' caveats?: string toolsUsed: { name: string; calls: number; successRate: number }[] metrics: { tokens: number duration: number iterations: number cost: number } markdown: string // Pre-rendered Markdown version}Access it from any run result:
const result = await agent.run('Fetch the latest commits and summarize')
// result.debrief — instant deterministic fallback (never blocks run()).if (result.debrief) { console.log(result.debrief.summary) console.log(result.debrief.markdown)}
// result.debriefRich() — awaits the LLM-synthesized rich debrief, which the// engine forks off the critical path (v0.12.0+). Returns undefined when no// debrief was scheduled (e.g. .withoutMemory()).const rich = await result.debriefRich?.()console.log(rich?.markdown)Full Example
Section titled “Full Example”import { ReactiveAgents } from "reactive-agents";import { Effect } from "effect";
// await using — agent is disposed automatically when this block exitsawait using agent = await ReactiveAgents.create() .withName("research-assistant") .withProvider("anthropic") .withModel("claude-sonnet-4-6") .withPersona({ role: "CRISPR Research Specialist", background: "Expert in gene editing and molecular biology", instructions: "Provide detailed technical analysis with citations", tone: "professional", }) .withMemory() .withReasoning({ defaultStrategy: "adaptive", adaptive: { enabled: true } }) .withTools() // Built-in tools (web search, file I/O, etc.) .withGuardrails() .withVerification() .withCostTracking() .withObservability() .withAudit() .withMaxIterations(15) .withHook({ phase: "think", timing: "after", handler: (ctx) => { console.log(`Iteration ${ctx.iteration}, tokens: ${ctx.tokensUsed}`); return Effect.succeed(ctx); }, }) .build();
// Run a taskconst result = await agent.run("Research the latest advances in CRISPR gene editing");console.log(result.output);console.log(`Cost: $${result.metadata.cost.toFixed(4)}`);console.log(`Tokens: ${result.metadata.tokensUsed}`);console.log(`Strategy: ${result.metadata.strategyUsed}`);// agent.dispose() is called automatically here