@polyweave/agent-loop (1.0.4)
Installation
@polyweave:registry=https://hub.kl1.tenere.ai/api/packages/Polyweave/npm/npm install @polyweave/agent-loop@1.0.4"@polyweave/agent-loop": "1.0.4"About this package
@polyweave/agent-loop
Agent-evaluator loop with symbolic state, deterministic checks, lifecycle wrapping, and iterative retry orchestration for Polyweave behavior trees.
Synopsis
import { createAgentLoop, createLoopingAgentLoop, createSymbolicState, createDefaultDeterministicChecks, createDefaultEvaluatorParser, NodeStatus } from '@polyweave/agent-loop';
import { createOpenAIAdapter } from '@polyweave/openai';
const agentAdapter = createOpenAIAdapter({ apiKey: process.env.OPENAI_API_KEY });
const evaluatorAdapter = createOpenAIAdapter({ apiKey: process.env.OPENAI_API_KEY });
const loop = createAgentLoop({
agentAdapter,
evaluatorAdapter,
buildAgentPrompt: (ctx) => ({
type: 'TEXT',
content: { text: `You are an agent. Task: ${ctx.runtime.task}` }
}),
buildEvaluatorPrompt: (ctx, history) => ({
type: 'TEXT',
content: { text: `Evaluate the agent's work. History: ${JSON.stringify(history)}` }
}),
parseEvaluatorOutput: createDefaultEvaluatorParser(),
deterministicChecks: createDefaultDeterministicChecks({ workspaceDir: '/tmp/workspace' }),
checksList: ['file_exists:output.txt', 'exit_code:0'],
maxRetries: 3,
maxIterations: 10
});
loop.source.pipe(mySink);
loop.sink.write({ type: 'TICK' });
Install
npm install @polyweave/agent-loop
Why
The agent-evaluator loop is the core orchestration pattern in Polyweave behavior trees. Rather than making a single LLM call, the loop iterates: the agent produces output (with tool calls), the evaluator assesses quality against criteria, and the cycle repeats until success, failure, or exhaustion. Symbolic state persists facts across cycles. Deterministic checks short-circuit the evaluator for testable conditions (file existence, exit codes, stdout patterns). Lifecycle wrapping enforces iteration limits.
API
createAgentLoop(options)
Returns { source, sink, symbolics, context, abort }. A frame duplex that runs agent-evaluator cycles.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agentAdapter |
IFrameDuplex |
Yes | — | LLM adapter producing agent output |
evaluatorAdapter |
IFrameDuplex |
Yes | — | LLM adapter producing evaluator judgments |
buildAgentPrompt(context) |
function |
Yes | — | Returns a frame prompting the agent. Receives { symbolics, runtime, node, iteration, retryCount } |
buildEvaluatorPrompt(context, history) |
function |
Yes | — | Returns a frame prompting the evaluator. Receives context and agent history array |
parseEvaluatorOutput(frames) |
function |
Yes | — | Parses evaluator output frames. Must return { status, evidence, symbolicUpdates? }. Use createDefaultEvaluatorParser() for JSON format |
deterministicChecks(checksList, toolTrace) |
function |
No | null |
Runs before evaluator. If all pass, skips evaluator entirely. Use createDefaultDeterministicChecks() |
checksList |
string[] |
No | null |
List of check strings passed to deterministicChecks |
maxRetries |
number |
No | 5 |
Max retries for deterministic check failures before falling through to evaluator |
maxIterations |
number |
No | 10 |
Max total agent-evaluator cycles |
toolAdapters |
object |
No | {} |
Map of tool name to duplex adapter (e.g. { filesystem: fsAdapter }) |
toolTimeoutMs |
number |
No | 30000 |
Timeout for tool calls routed through the tool adapter wrapper |
initialSymbolics |
object |
No | {} |
Initial values for the symbolic state store |
context |
object |
No | — | Additional context passed to prompt builders (symbolics and runtime are auto-extracted) |
onProgress(type, data) |
function |
No | null |
Progress callback. Fires on agent_loop:node_started, agent_loop:node_completed, agent_loop:node_retry, agent_loop:node_failed, agent_loop:tree_failed, agent_loop:symbolic_asserted |
maxBufferedOutputFrames |
number |
No | null |
Max frames buffered in output source before overflow |
outputOverflowStrategy |
string |
No | 'drop-oldest' |
Strategy for output overflow: 'drop-oldest' or 'drop-newest' |
Output frames on source:
| Frame | When |
|---|---|
agent_loop:node_started |
Cycle begins; content: { iteration, agentText } |
agent_loop:node_completed |
Evaluator returns SUCCESS; content: { evidence, iteration } |
agent_loop:node_retry |
Retry triggered; content: { evidence, retryCount } |
agent_loop:node_failed |
Evaluator returns FAILURE; content: { evidence, iteration } |
agent_loop:tree_failed |
Max iterations or abort; content: { reason, evidence? } |
agent_loop:symbolic_asserted |
Symbolic update applied; content: { name, value, op } |
TEXT |
Agent or evaluator text output (forwarded from adapters) |
ABORT |
Execution aborted via sink CANCEL/ABORT or loop.abort() |
Errors:
Error('createAgentLoop requires an agentAdapter')— missing agent adapterError('createAgentLoop requires an evaluatorAdapter')— missing evaluator adapterError('createAgentLoop requires buildAgentPrompt')— missing prompt builderError('createAgentLoop requires buildEvaluatorPrompt')— missing evaluator prompt builderError('createAgentLoop requires parseEvaluatorOutput')— missing parser
createLoopingAgentLoop(options)
Returns { source, sink, abort }. Wraps createAgentLoop in an iterative outer loop that runs multiple inner agent-evaluator cycles, driven by a node provider and a continuation predicate.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
innerLoopFactory(innerOpts) |
function |
No | createAgentLoop |
Factory to create each inner loop instance |
shouldContinue(result, iteration) |
function |
No | () => false |
Return true to continue looping |
nodeProvider(iteration) |
function |
No | null |
Returns a node descriptor for this iteration |
maxIterations |
number |
No | 10 |
Max outer loop iterations |
onProgress(type, data) |
function |
No | null |
Progress callback |
All createAgentLoop options (agentAdapter, evaluatorAdapter, buildAgentPrompt, buildEvaluatorPrompt, parseEvaluatorOutput, toolAdapters, determincheckChecks, etc.) are forwarded through to each inner loop.
createSymbolicState(initial?)
Returns { assertValue, retractValue, getValue, getAll, applyUpdate, applyUpdates, subscribe }. A reactive key-value store with change notifications. Used to persist facts across agent-evaluator cycles (e.g., git commit hashes, file paths, task completion status).
| Method | Description |
|---|---|
assertValue(name, value, metadata?) |
Set a value. Returns true if changed |
retractValue(name) |
Remove a value. Returns true if existed |
getValue(name) |
Get current value or undefined |
getAll() |
Returns shallow copy of all values |
applyUpdate({ name, value, op? }) |
Apply a single update. op: 'retract' removes the value |
applyUpdates(updates[]) |
Apply array of updates. Returns changed count |
subscribe(fn) |
Subscribe to changes. fn({ op, name, value, metadata }) |
createDefaultDeterministicChecks(options)
Returns a function(checksList, toolTrace) suitable for the deterministicChecks option. Runs fast file/process checks without calling the evaluator LLM.
| Option | Type | Default | Description |
|---|---|---|---|
workspaceDir |
string |
null |
Prepended to relative paths in file_exists: checks |
symbolics |
SymbolicState |
null |
Used for symbolic:name=value checks |
Check syntax:
| Check | Description |
|---|---|
file_exists:path |
Asserts the file exists (workspaceDir-relative if set) |
symbolic:name=value |
Asserts symbolic state has the given value |
stdout_contains:text |
Asserts any tool result stdout contains the text |
exit_code:N |
Asserts any tool result has exit code N |
Returns { allPassed, evidence }.
createDefaultEvaluatorParser()
Returns a function(frames) that parses evaluator JSON output. Expects evaluator to emit a TEXT frame containing JSON:
{ "status": "SUCCESS", "evidence": "Task completed", "symbolicUpdates": [] }
| Status value | Mapped to |
|---|---|
"SUCCESS" |
NodeStatus.SUCCESS |
"FAILURE" |
NodeStatus.FAILURE |
"RUNNING" |
NodeStatus.RUNNING |
| anything else | NodeStatus.FAILURE |
Returns { status, evidence, symbolicUpdates }.
NodeStatus
Enum: { PENDING, RUNNING, SUCCESS, FAILURE }. Status values returned by evaluator parsers.
Tests
npm test
20 native tests + 500 js-rigor property cases covering agent-evaluator cycle lifecycle, symbolic state CRUD, deterministic checks (file_exists, symbolic, stdout_contains, exit_code), evaluator parser JSON variants, looping agent loop iteration control, frame buffering overflow, and abort/cancel behavior.
Requirements
Node.js 22 or later. ESM only.
Peer dependencies: @polyweave/core, @polyweave/tool-adapter, @polyweave/harness-base.
Dependencies
Dependencies
| ID | Version |
|---|---|
| @polyweave/bt-model | * |
| @polyweave/harness-base | * |
| @polyweave/tool-adapter | * |
Development Dependencies
| ID | Version |
|---|---|
| @rigor/core | * |
Peer Dependencies
| ID | Version |
|---|---|
| @polyweave/core | * |