@three-ws/agent-runtime
v0.1.1
Published
The three.ws agent engine: a plan/execute decision loop with human-approval gates, a seven-layer GuardChain (security blacklist, intervention, capability, permission, trade guard, spend envelope, x402 budget), a tamper-evident hash-chained action ledger,
Maintainers
Readme
The runtime that sits between a model and anything irreversible. The decision loop turns an LLM into an agent (plan, call tools, pause for humans, finish); the GuardChain composes every enforcement layer into one deterministic verdict with a per-layer trace; the action ledger makes the resulting history tamper-evident. It is pure engine: no chain SDKs, no database, no HTTP - every external fact is injected, which is why the same code path serves the live
/api/agent/guardpreflight, simulators, and tests. Powers the transaction-guard preflight in three.ws/chat.
Why
An agent that can call solana_transfer is one bad tool call away from an
empty wallet. Most stacks scatter their defenses: an approval modal here, a
spend cap there, a blacklist somewhere else - each invoked from a different
place, with different inputs, and nothing that can answer which layer would
stop a given call, or which layers would silently not evaluate it at all.
A guard that never runs looks exactly like a guard that passed.
This package makes enforcement composable and observable:
| Piece | What it does |
|---|---|
| AgentRuntime | Plan → execute loop: call_llm, call_tool, batches, finish, and three human-in-the-loop instructions (approve / prompt / select), with cost limits, interrupts, and resume |
| GeneralChatAgent | The default "brain": routes phases to instructions, splits tool calls into execute-now vs. needs-approval, compresses context when it outgrows the window |
| GuardChain | Runs all seven layers over one call, returns decision + per-layer trace + blind spots (enforcement that should have applied but didn't) + a coverage score |
| TradeGuard | Default domain guard: tier per-tx caps, rolling 24h/7d windows, auto-execute ceiling, MEV slippage clamp, protocol audit lookup |
| SpendGuard | Per-agent spend envelope: per-tx / rolling / daily caps, reserve floor, token + destination firewall, custody-breach latch |
| InterventionChecker | Policy engine for human approval (never / required / always, argument-level rules, security blacklist) |
| ActionLedger | Hash-chained (sha256) append-only ledger primitives: computeEntryHash, verifyChain, drift audits |
| TransactionPipeline | Multi-step plan execution: dependency order, parallel independent steps, rollback on failure, approval gates |
| DecisionJournal | Fire-and-forget reasoning journal with an injected sink |
| Reasoning utilities | SelfReflection (structured retry prompts), ExitDecisionEngine, ResponseQualityEvaluator, ToolRelevanceScorer, token counting + context-compression checks |
Install
npm install @three-ws/agent-runtimeESM, Node 18+. One runtime dependency (tokenx, for token estimation).
Quick start
Preflight a tool call through every guard layer
import { GuardChain, SpendGuard, TradeGuard, createX402Hook } from '@three-ws/agent-runtime';
const chain = new GuardChain({
defiGuard: new TradeGuard(), // tier caps + slippage clamp
spendGuard: new SpendGuard({ perTxMaxUsd: 5_000 }), // hard envelope
x402Hook: createX402Hook(5), // $5/hour autonomy budget
});
const verdict = await chain.evaluate({
identifier: 'solana_swap',
apiName: 'solana_swap',
arguments: { inputMint: 'So11111111111111111111111111111111111111112', amount: 120, slippageBps: 300 },
valueUsd: 18_400,
userTier: 'pro',
});
verdict.decision; // 'block' ($18,400 is over the $5,000 per-tx envelope)
verdict.blockedBy; // 'spend_guard' the layer that decided it
verdict.modifiedArguments; // { ..., slippageBps: 100 } (MEV clamp, still applied)
verdict.coverageScore; // 74 two layers were left unwired below
verdict.blindSpots; // [CAPABILITY_UNWIRED, PERMISSION_UNWIRED]The capability and permission layers report themselves as blind spots because
this chain was built without a checkCapability / checkPermission resolver.
Inject both (each an async (request) => ({ allowed }) backed by your
capability-token and permission records) and the same call scores 100 with no
blind spots. Raise perTxMaxUsd above the notional and the decision becomes
require_approval instead of block, driven by the trade guard's
auto-execute ceiling.
The layer order is security_blacklist → intervention → capability →
permission → defi_guard → spend_guard → x402. The chain never
short-circuits: a call blocked by the spend envelope still reports the MEV
exposure the trade guard saw, and every layer that could not evaluate the
call says so instead of reading as "green".
Run the decision loop
import { AgentRuntime, GeneralChatAgent } from '@three-ws/agent-runtime';
const agent = new GeneralChatAgent({
modelRuntimeConfig: { model: 'llama-3.3-70b-versatile', provider: 'groq' },
});
agent.modelRuntime = myStreamingLlm; // async generator: yields { content?, tool_calls? }
agent.tools = { get_price: async (args) => fetchPrice(args) };
let state = AgentRuntime.createInitialState({ operationId: 'op-1' });
state.messages.push({ role: 'user', content: 'What is SOL at?' });
const runtime = new AgentRuntime(agent);
let context;
while (state.status !== 'done' && state.status !== 'error') {
const step = await runtime.step(state, context);
state = step.newState;
context = step.nextContext;
if (state.status === 'waiting_for_human') break; // surface state.pendingToolsCalling to the user
}When the user approves a pending call, continue with
runtime.approveToolCall(state, approvedCall). runtime.interrupt() /
runtime.resume() cover cancellation.
Verify a ledger
import { computeEntryHash, verifyChain } from '@three-ws/agent-runtime';
// Each row is a ledger entry plus the two chain columns: `entryHash`
// (computeEntryHash(entry, prevHash)) and `prevHash`, the first seeded with
// LEDGER_GENESIS_HASH. verifyChain sorts by `seq`, so store order is fine.
const rows = await loadAgentLedger(agentId);
const result = verifyChain(rows);
result.valid; // false if any historical row was edited or deleted
result.brokenAtIndex; // index of the first broken link, -1 when the chain is intact
result.brokenAtSeq; // that row's `seq`, or null when intact
result.reason; // why it broke (null when intact)The hashed fields are fixed and ordered (LEDGER_CANONICAL_FIELD_ORDER:
seq, ts, userId, agentId, event, target, amountWei, valueUsd, txHash, reason,
balanceBeforeWei, balanceAfterWei, network, detail), so editing any one of
them after the fact breaks that row's link and every link after it.
Registering your tools
The guard layers key off two module-level registries seeded with the three.ws
tool surface (solana_transfer, solana_swap, pumpfunBuy, ...). A host
adding a new fund-moving tool registers it at boot:
import { registerFundMovingTool, registerMutatingApi } from '@three-ws/agent-runtime';
registerFundMovingTool('my_bridge_tool');
registerMutatingApi('executeBridgeV2');An unregistered fund-moving tool is not "unguarded by design" - the GuardChain
reports it as a critical TOOL_UNREGISTERED blind spot.
Where it runs on three.ws
POST /api/agent/guardpreflights tool calls for the chat client: the chat wallet tools (solana_transfer,solana_swap,evm_*, pump.fun trades) are evaluated through this exact chain before their tool body runs, ablockverdict never reaches the wallet, and the transaction-approval modal renders the verdict (decision, warnings, unchecked blind spots).- The server resolves SOL notionals (amount × live SOL price) before evaluating, so dollar caps compare against real numbers.
Relation to @three-ws/agent-guards
@three-ws/agent-guards is the client for the platform's
custodial wallet policy (it wraps /api/agents/:id/trade limits). This
package is the engine: it evaluates arbitrary tool calls before execution
anywhere - client wallets, custodial paths, simulators - and is what
/api/agent/guard runs. The custodial trade path keeps its own enforcement;
both speak the same language of per-tx/daily caps and deny-lists.
Provenance
Core of the engine (decision loop, guard composition, spend envelope, pipeline) was battle-tested in a sibling project of the same author and ported here de-branded; the action ledger's hash-chain construction originated in the three.ws economy master ledger, so this is that design coming home. All chain-specific analysis was left behind: this package is chain-agnostic by construction, and three.ws wires Solana-first specifics (notional resolution, tool registries) at the edges.
License
See LICENSE.
