Polyweave

@polyweave/bt-harness (1.0.16)

Published 2026-07-17 15:48:51 +00:00 by Dvorak

Installation

@polyweave:registry=https://hub.kl1.tenere.ai/api/packages/Polyweave/npm/
npm install @polyweave/bt-harness@1.0.16
"@polyweave/bt-harness": "1.0.16"

About this package

@polyweave/bt-harness

Behavior tree LLM agent harness — tick-stepped FSM with CoC-typed DSL, dual-model loop (agent + evaluator), multi-state blackboard (fact-graph + document + tree), and entity projection system.

Features

  • Tick-stepped FSM — Every node is a finite state machine. harness.sink.write({ type: 'TICK' }) advances one step. The scheduler owns timing.
  • Dual-model loop — Agent executes tasks with tool grants; evaluator independently verifies against checklists. Evaluator never sees the agent's task description.
  • Declarative DSLbt\...`` tagged template: PARAMETERS, BLACKBOARD, SYMBOLS, POSTCONDITION, CONDITIONS, ACTIONS, TREE.
  • Multi-state blackboard — Fact-graph + document store + tree store in one transactionally-consistent multi-state store. Entity projection system maps DSL entities to store fields.
  • CoC-typed parametersSigmaType/BaseType/UnionType; checkValue(), sigmaToJsonSchema(), DESCRIBE serialization.
  • WAITING state — Nodes wait on preconditions (when @symbol == value). Composites re-evaluate each tick. External symbol changes unblock WAITING nodes.
  • Postcondition loop — Root postcondition gates tree completion (video game AI pattern). Tree loops from root until postcondition met. Per-node until postcondition for action-level gating.
  • Permission systemreads/writes on actions restrict agent access to blackboard entities and symbols. evaluator_writes restricts which symbols the evaluator can set.
  • Timeout — Agent/evaluator deadline timer (default 120s). Emits bt:node_timed_out on expiry.
  • Behavior tree — Sequence, Selector, Parallel, Action, Condition nodes with WAITING propagation, forked symbolics per parallel child, snapshot isolation.
  • MVCC — Snapshot isolation on forked transactions. checkConflicts(), commit/abort return status.
  • CANCEL / ABORT / CLOSE — Per-node abort controllers, timer cleanup, tree reset on cancel.
  • Built-in store_write/store_read tools — Document store tools txns thread through the multi-state blackboard. Additional transactional systems (e.g., JJ) layer on top alongside built-in tools.
  • validateBtSpec() — Cross-reference validator checks symbols, transitions, preconditions, postconditions, entities, permissions.

Installation

npm install @polyweave/bt-harness

Requires @polyweave/core, @polyweave/state, @polyweave/tools, @polyweave/deepseek, @polyweave/tool-adapter.

Quick Start

import { createBtHarness, createEchoScheduler, bt } from '@polyweave/bt-harness';

var spec = bt`
  ─── PARAMETERS ───
  param cuisine : string = "Roman"

  ─── BLACKBOARD ───
  entity recipe {
    content : document
  }

  ─── SYMBOLS ───
  @done : boolean = false

  ─── ACTIONS ───
  write_recipe (action):
    task "Write a recipe for {{cuisine}} cuisine. store_write('recipe', content)"
    tools store_write, store_read
    writes recipe.content
    evaluator_writes @done
    check "Recipe saved to store"
    on success: @done = true

  ─── TREE ───
  root = sequence { action write_recipe }
`;

var tools = [
  createStoreWriteTool(store),
  createStoreReadTool(store)
];

var harness = createBtHarness({
  spec,
  tools,
  apiKey: process.env.DEEPSEEK_API_KEY,
});

harness.source.pipe(createEchoScheduler(harness, createFrameSink(frame => {
  if (frame.type === 'bt:tree_complete') console.log('DONE');
  if (frame.type === 'bt:tree_failed') console.log('FAILED');
})));

harness.sink.write({ type: 'TICK' });

API

createBtHarness(options)

Option Type Default Description
spec string|object BT DSL text or parsed spec (from bt\...``)
tree object Raw BT definition (alternative to spec)
store object internal External multi-state store for blackboard (enables cross-harness coordination)
tools Duplex[] [] Tool duplexes (DESCRIBE-capable). Auto-classified to agent/evaluator sets.
apiKey string required DeepSeek API key
params object {} Parameter values (matched against spec's SigmaType)
initialSymbolics object {} Initial symbolic values
agentFactory function auto Custom agent factory { start(prompt) → { collector } }
evaluatorFactory function auto Custom evaluator factory
agentModel string 'deepseek-v4-flash' Model for agent LLM
evalModel string 'deepseek-v4-flash' Model for evaluator LLM
agentSystem string built-in System prompt for agent
evalSystem string built-in System prompt for evaluator
agentTemperature number 0.3 Agent temperature
evalTemperature number 0.1 Evaluator temperature
agentMaxTokens number 4096 Agent max tokens
evalMaxTokens number 4096 Evaluator max tokens
timeoutMs number 120000 Agent/evaluator deadline timer
maxTicksPerIteration number 1000 Max sync loop iterations per tick
maxTreeLoops number 3 Max postcondition-recheck loops before going IDLE
debug boolean false Enable debug logging

Returns { source, sink, valid, errors, warnings, symbolics, root, context, tick(), getState(), getWaitingOn(), cancel(), abort(), close() }.

If valid === false, the source is pre-loaded with an ERROR frame and ended. Check harness.valid before starting.

createEchoScheduler(harness, downstreamSink)

Creates a pass-through sink that auto-sends TICK frames on bt:agent_done / bt:eval_done. Frames are forwarded to downstreamSink. Usage:

harness.source.pipe(createEchoScheduler(harness, yourLogSink));

createBtHarnessFromSpec(spec, options)

Convenience wrapper that merges a spec into options:

var harness = createBtHarnessFromSpec(bt\`...\`, { tools, apiKey });

validateBtSpec(spec)

Validates a parsed spec. Returns { valid, errors, warnings }. Checks: tree references, symbol cross-references in conditions/transitions/preconditions/postconditions, entity field references, permission symbol references, SigmaType params.

createBtHarnessDuplex(options)

Wraps a BT spec as a DESCRIBE-capable tool duplex. Responds to DESCRIBE and TOOL_CALL frames. The harness runs once per tool call. Returns { source, sink, _toolName }.

Frame Protocol

Sink (into harness):

Frame Effect
{ type: 'TICK' } Advance one step
{ type: 'CANCEL' } Abort controllers, reset tree, emit bt:cancelled
{ type: 'ABORT' } Abort controllers, reset tree, emit bt:aborted, end source
{ type: 'END' } Close all resources, clean up symbolics

Source (out of harness):

Frame Content Meaning
bt:tree_running {} Transitioned IDLE → ACTIVE, work starting
bt:send_agent { nodeId, turn, promptChars } Agent prompt sent
bt:agent_done { nodeId, turn, textChars, toolCalls } Agent responded
bt:send_eval { nodeId, turn } Evaluator prompt sent
bt:eval_done { nodeId, turn } Evaluator responded
bt:node_started { nodeId } Node execution began
bt:node_completed { nodeId, evidence, status } Node finished (SUCCESS)
bt:node_failed { nodeId, evidence } Node finished (FAILURE)
bt:node_retry { nodeId, turn } Node retrying
bt:node_waiting { nodeId, evidence } Node is WAITING
bt:node_loop { nodeId, turn } Node postcondition not met, looping
bt:node_timed_out { nodeId, side, timeoutMs } Agent or evaluator deadline expired
bt:tree_loop { loop } Tree completed but root postcondition not met, looping from root
bt:tree_complete {} Tree succeeded (root postcondition met)
bt:tree_failed { reason } Tree failed
bt:symbolic_asserted { name, value } Symbol transition applied
bt:error { code, message } Internal error (safety limit, etc.)
bt:cancelled {} Harness cancelled
bt:aborted {} Harness aborted
bt:closed {} Harness closed

Behavior Tree Node Types

Type Behavior
action Agent executes taskDescription; evaluator judges result. Has precondition, until postcondition, onSuccess, onFailure, reads/writes/evaluatorWrites.
sequence Runs children in order. Child WAITING → parent WAITING. Child SUCCESS → advance. Child FAILURE → parent FAILURE.
selector Runs children until SUCCESS. Skips WAITING, tries next. All FAILURE → parent FAILURE.
parallel Runs all children concurrently with forked symbolics (snapshot isolation). All SUCCESS → parent SUCCESS. Any FAILURE → parent FAILURE.
condition Evaluates predicate(symbolics). null/undefined → WAITING; true → SUCCESS; false → FAILURE. Supports >, <, >=, <=, ==, !=.

DSL Reference

bt\...`` Tagged Template

─── PARAMETERS ───
param name : type [= default]

─── BLACKBOARD ───
entity name {
  key : document|fact|tree
}

─── SYMBOLS ───
@name : type = init

─── POSTCONDITION ───
when @symbol == value
when @count >= 3 && @passed == true

─── CONDITIONS ───
name:
  when @symbol == value
  when @a + @b >= @threshold

─── ACTIONS ───
name (action):
  when @symbol == value         # precondition — WAITING if not met
  until @symbol == value        # per-node postcondition — loop until met
  task "multiline text with {{param}}, {{@symbol}}, {{entity.field}}"
  tools toolA, toolB
  check "checklist item"         # quoted → LLM evaluator
  check called('toolA')          # unquoted → Jessie expression
  on success: @symbol = @a + @b # Jessie expression RHS
  on failure: @symbol = value
  reads entity.key
  reads @symbol
  writes entity.key
  writes @symbol
  evaluator_writes @sym1, @sym2

─── TREE ───
root = sequence {
  selector "id" {
    sequence { condition gate, action fix }
    action default
  }
  parallel "id" { action a, action b }
}

Interpolation

Syntax Resolves from Example
{{param}} Action parameters {{cuisine}}"Roman"
{{@symbol}} Symbol current value {{@step}}2
{{entity.field}} Entity projection {{recipe.content}}"..."
{{expr}} Any Jessie expression {{@total / max(@count, 1)}}

All {{...}} interpolation is compiled through Jessie. @ distinguishes symbols from params. Bare identifiers without @ resolve to params, then symbols.

{{@symbol}} resolves via symbolics.getValue(). {{entity.key}} resolves via the multi-state's entity projection system — it queries the appropriate store (document, fact-graph, or tree) based on the entity's BLACKBOARD definition.

Parameter types

string, number, boolean, null, or union "A" | "B" | "C".

Transition operators

on success: @symbol = expression — any Jessie expression evaluated against the symbol scope. Value is asserted on the target symbol. Only applied if the action's writes permission includes the symbol.

Predicates (when/until/conditions)

when @symbol == value (comparison sugar) or when expr (raw Jessie). Full JS expression subset — logical operators, arithmetic, FFI calls, property access. All predicates compile through Jessie implicitly.

Permissions

Three-tier access control on actions:

Declaration Enforces
reads entity.key Only these entities/fields appear in prompt context
reads @symbol Agent prompt includes this symbol's value
writes entity.key Agent tools can write to this entity
writes @symbol Transitions (on success:/on failure:) only apply for declared symbols
evaluator_writes @sym1, @sym2 Evaluator's symbolicUpdates filtered to these symbols only

If writes is empty/absent, all transitions are allowed (backward compatible). If evaluator_writes is empty/absent, all evaluator symbol updates are allowed.

Architecture

Blackboard (multi-state store)

Every harness creates a multi-state store with three sub-stores:

createMultiStateStore({
  graphStore: createFactGraph(),      → symbolic state (key-value)
  docStore:   createDocumentStore(),   → document sections (structured content)
  treeStore:  createTreeStore()        → tree nodes (hierarchical data)
})

All three share the same transaction. TXN_BEGIN/COMMIT/ABORT propagate atomically across all stores. A forked parallel child's writes to documents, facts, and tree nodes commit or abort together.

Built-in tools vs. external transactional systems

The built-in store_write/store_read tools write to the blackboard's document store. Additional transactional systems (e.g., JJ, encyclopedia, custom state machines) can layer on top via custom tool duplexes. These external systems operate on their own stores but share the blackboard's entity namespace for interoperability.

Tick-stepped execution — unified clock

Every node and every tool is a push-stream duplex. One clock drives everything. Ticks flow down through tools to nested harnesses. Frames flow up through source chains. The top-level scheduler owns the rhythm.

Top-level scheduler
  │
  ├─ TICK → parent harness sink
  │   ├─ handleInput: forward TICK to all tools
  │   ├─ tickHarness(): advance one step
  │   │   ├─ condition → evaluate synchronously → advance tree
  │   │   ├─ action → start agent/evaluator → AWAIT → yield
  │   │   └─ node completed → advance to next sibling, continue
  │   ├─ TICK forwarded to tools:
  │   │   ├─ simple tool → no-op (accept and ignore)
  │   │   └─ BT harness tool → forward to internal harness sink
  │   │       └─ internal tickHarness() → advance one step
  │   └─ post-tree postcondition: if not met → bt:tree_loop → re-enter
  │
  └─ Source frames flow outward from all levels
      ├─ child frames → tee'd to tool's source → parent's source → observer
      └─ observer sees bt:agent_done → sends next TICK

Each TICK advances every harness in the tree by one step. Deeply nested BT harnesses (harness → tool → harness → tool → ...) all step together on the same clock.

Tick propagation through tools

  • Every tool accepts TICK on its sink. createToolDuplex tools ignore TICK as a no-op. createBtHarnessDuplex tools forward TICK to their internal harness.
  • The harness forwards TICK to all tools on every received tick (handleInput at bt-harness.js:1032). This ensures tools receive ticks even when the parent harness is in AWAIT state (blocked on an agent call).
  • Frames flow outward: BT harness tools tee their internal harness's source to the tool's external source. Every frame from deeply nested harnesses is visible at the top level.
  • CANCEL/ABORT propagate: Parent forwards CANCEL/ABORT to all tools. BT harness tools forward to internal harness. Simple tools ignore.

Legacy: Echo scheduler

For standalone single-harness scripts, createEchoScheduler provides a simple auto-tick wrapper. It watches for bt:agent_done/bt:eval_done and re-ticks. For nested/composed harnesses, prefer the unified clock model above.

Harness State

State Meaning
IDLE No work in progress — waiting for TICK or external symbol change
ACTIVE Work in progress (agent/evaluator call in flight)
SUCCESS Behavior tree completed
FAILURE Behavior tree failed
ERROR Spec validation failed (harness created with valid: false)

Exports

Export Description
createBtHarness Tick-stepped harness (canonical)
createBtHarnessDuplex BT spec as DESCRIBE-capable tool duplex
createBtHarnessFromSpec Convenience wrapper (spec + options)
createEchoScheduler (deprecated) Pass-through sink that auto-TICKs for standalone scripts
bt Tagged template literal for DSL
parseBtSpec Parse DSL text → { params, entities, symbols, tree, ... }
validateBtSpec Cross-reference validator → { valid, errors, warnings }
createSymbolicState(initial, store?, entities?) Multi-state-backed symbolic state
entitiesToProjections Map DSL entities → multi-state projection config
createBaseType, createUnionType, createSigmaType CoC type constructors
buildRecordType, checkValue, applyDefaults Type helpers
sigmaToJsonSchema, typeToJSON, typeFromJSON Type serialization
createBehaviorTree, tickTree, resetTree, snapshotTree BT tree operations
findActiveNode Find next active node in tree
NodeType, NodeStatus, FsmState Enum constants
createToolCycleLimiter, createCollector Tool utilities
createAgentFactory, createEvaluatorFactory Session factories

Examples

In-tree (correct usage)

File Description
recipe-pipeline.js Pipeline — two nodes, selector, condition gate, transactional state, retry loop
fusion-menu-bt.js Complex BT — 9 actions, 4 selectors, parameter interpolation, symbolic routing
advanced-patterns.js PARALLEL + POSTCONDITION + echo scheduler — two concurrent agents, postcondition gate
conflict-resolution.js Conflict as a BT condition — parallel writers collide, detector finds conflicts, resolver merges
document-store-tools.js Library — DESCRIBE-capable store write/read tool duplexes (txn-threaded)
unified-tick-nesting.js Unified clock — parent harness calls child BT harness as tool, single tick loop drives both
nested-harnesses.js Composition — FD refine tool used by summary BT, content extraction from child harness

Real-world packages (to be refactored)

Package Issue Guide
@polyweave/grpo External frame counting replaces POSTCONDITION + PARALLEL ../grpo/SUGGESTION.md
@polyweave/feedback-descent External stagnation tracking replaces CONDITIONS + SYMBOLS ../feedback-descent/SUGGESTION.md

Packages with BT-based alternatives available

Package BT replacement File
@polyweave/state-search createTreeSearchHarness — recursive tree traversal as POSTCONDITION loop ../state-search/src/tree-search-harness.js
@polyweave/raptor createSummaryRefinerBt — summary refinement with local scoring, no LLM loop ../raptor/src/summary-refiner-bt.js

Run examples:

node examples/recipe-pipeline.js
node examples/fusion-menu-bt.js
node examples/advanced-patterns.js

Anti-pattern: External frame counting

A common mistake is to count bt:node_completed / bt:node_failed frames from outside the tree and manually track iteration state — cancelling and re-ticking the harness in an external loop. This bypasses the BT's own control-flow system entirely.

Don't do this:

// ANTI-PATTERN — fragile, har to maintain, duplicates BT features
var iteration = 0, nodeDone = 0;
harness.source.pipe(listener(frame => {
  if (frame.type === 'bt:node_completed' || frame.type === 'bt:node_failed') {
    nodeDone++;
    if (nodeDone >= 2) {            // ← hard-coded coupling to tree structure
      nodeDone = 0;
      iteration++;
      if (iteration >= max) {
        harness.symbolics.assertValue('terminate', true);
        harness.sink.write(TICK);
        return;
      }
      harness.cancel();             // ← unnecessary reset
      harness.sink.write(TICK);
    }
  }
}));

Do this instead — push control flow into the DS L:

// SYMBOLS track iteration natively
// POSTCONDITION gates termination:  when @iteration >= @max
// on success: @iteration = @iteration + 1 transitions advance state
// SELECTOR fallthrough re-enters the tree naturally

harness.source.pipe(createEchoScheduler(harness, logSink));
harness.sink.write(TICK);
// Done. No frame counting. No cancel/retick. The tree owns scheduling.

Symptoms you're doing it wrong:

  • Counting bt:node_completed frames to decide when to advance
  • Calling harness.cancel() between iterations
  • Setting @terminate from outside the tree
  • Disabling maxTreeLoops: 0
  • Tracking epoch/index/etc. in JS variables mirroring @symbols
  • Reading the document store directly instead of checking symbols

Packages that currently do this: @polyweave/grpo, @polyweave/feedback-descent. See their SUGGESTION.md for how to fix.

Running Tests

Tests

node --test test/*.test.js

93 tests, 92 pass (1 flaky fake-adapter test under concurrency).

Requirements

Node.js 18 or later. ESM only.

Depends on @polyweave/core, @polyweave/state, @polyweave/tools, @polyweave/tool-adapter.

License

MIT

Dependencies

Dependencies

ID Version
@polyweave/harness-base *
@polyweave/state *
@polyweave/tool-adapter *
subscript ^10.4.13

Development Dependencies

ID Version
@polyweave/tools *
@rigor/core *

Peer Dependencies

ID Version
@polyweave/core *

Keywords

polyweave behavior-tree harness durational multimodal
Details
npm
2026-07-17 15:48:51 +00:00
250
John Dvorak
SEE LICENSE IN LICENSE
latest
38 KiB
Assets (1)
Versions (5) View all
1.0.16 2026-07-17
1.0.15 2026-07-17
1.0.4 2026-07-10
1.0.3 2026-07-10
1.0.2 2026-07-10