Runtime and hardware
Node 24 or newer, declared by the CLI and by every scaffolded project. 16 GB of RAM or unified memory is the working recommendation: the recommended trunk model is 2.6 GB of weights, the research template adds a 630 MB reranker, and the remainder covers KV at the recommended 32k context plus the OS and whichever surface is running.
| Model | Role | Size | Context |
|---|---|---|---|
qwen3.5-4b · Q4_K_M | llm | 2.6 GB | 32768 |
qwen3-reranker-0.6b-q8 | reranker | 630 MB | — |
packages/rig/src/models.ts
Concurrent agents do not multiply this. They share one context, so cost tracks KV fullness, not agent count — four agents is not four times the model.
Retrieval and scoring
One cross-encoder serves four roles. Three different queries are in play — the per-call tool query, the per-agent task, and the original research query — and conflating them produces wrong scores.
interface EntailmentScorer {
scoreEntailmentBatch(texts: string[]): Promise<number[]>;
scoreRelevanceBatch(texts: string[], localQuery: string): Promise<number[]>;
scoreSimilarityBatch(reference: string, texts: string[]): Promise<number[]>;
shouldProceed(score: number): boolean;
}
interface ScorerReranker {
scoreBatch(query: string, texts: string[]): Promise<number[]>;
}| Method | Scores against | Used at |
|---|---|---|
scoreEntailmentBatch | the original query | content prefill boundaries |
scoreRelevanceBatch | min(local, original) | exploit mode, when pressure tightens focus |
scoreSimilarityBatch | an arbitrary reference | echo detection at delegation |
shouldProceed | the floor | the gate itself |
The score is a logit-diff — logit(yes) − logit(no), unbounded, with the sign carrying the meaning. Source._entailmentFloor defaults to 0: a hit passes when the cross-encoder leans yes. Raise it for noise-heavy corpora, lower it for sparse ones.
abstract class Source<TCtx = unknown, TChunk = unknown> {
abstract readonly name: string;
abstract get tools(): Tool[];
protected _reranker: ScorerReranker | null;
protected _entailmentFloor: number; // default 0
createScorer(originalQuery: string): EntailmentScorer;
promptData(): Record<string, unknown>;
getChunks(): TChunk[];
}promptData() is what the per-spawn skill template reads, so an agent learns what kind of source it holds before it searches.
packages/agents/src/source.ts
Consent and protected tools
The gate for consequential actions runs at DISPATCH, inside the agent runtime — below any application or interface code that could misreport it.
interface GrantStore {
has(toolName: string): Operation<boolean>;
grant(toolName: string): Operation<void>;
revoke(toolName: string): Operation<void>;
}
GrantStoreCtx // createContext<GrantStore>('lloyal.grantStore')Fail-closed. A tool marked protected is denied unless the session holds a grant, and no grant store configured means every protected tool is denied. Credentials never enter the model's context — the model can trigger a gated call, never see or replay the secret.
packages/agents/src/grant-store.ts · context.ts
Trace events
Pass a writer to initAgents to capture the inference graph as newline-delimited JSON. With none passed, NullTraceWriter makes every trace call a no-op at zero cost. Events form a tree through parentTraceId; the TraceParent context connects inner pools to the tool dispatch that spawned them without manual wiring.
| Event | Captures |
|---|---|
scope:open / close | named boundaries with duration — pools, tools, spines |
prompt:format | the exact prompt the model saw, and the task before formatting |
agent:turn | raw output, parsed content, parsed tool calls per turn |
tool:dispatch | args, toolkit position, explore flag, pressure |
tool:result / error / retry | result, prefill token count, duration, transient parking |
tool:authReject | protected-tool denial with full lineage |
pool:open / close / agentDrop | agent count, pressure snapshot, findings, drop reason |
pool:recovery* | recovery extraction — every attempt ends in exactly one outcome |
spine:extend | orchestrator extensions, delta tokens, position after |
branch:create / prefill / prune | KV lifecycle with token counts and roles |
entailment:* | scoring decisions at search, delegation, and content boundaries |
packages/agents/src/trace-types.ts — the full event union
Six failure modes
Start from the symptom; the trace answers the rest.
| Symptom | Start from |
|---|---|
| Agents not using tools | tool:dispatch — is anything dispatched at all? |
| Early termination — reporting too soon | agent:turn — what did the model actually emit? |
| Agents killed by pressure | pool:agentDrop — the drop reason |
| Recovery extraction fails | pool:recovery* — every attempt ends in one outcome |
| Synthesis ignores research findings | spine:extend — did the findings reach the spine? |
| Plan produces poor sub-questions | prompt:format — the prompt the planner saw |
Where things live
Wrong import paths were the single most common staleness in older material. These are the current homes.
| Symbol | Package |
|---|---|
withSpine · agentPool · dag | @lloyal-labs/lloyal-agents |
Source · EntailmentScorer · ScorerReranker | @lloyal-labs/lloyal-agents |
GrantStore · GrantStoreCtx | @lloyal-labs/lloyal-agents |
composePrompt · renderPrompt · renderTemplate | @lloyal-labs/lloyal-agents |
renderSpine | @lloyal-labs/rig |
reportTool | @lloyal-labs/rig |
Prompt templates live in prompts/ beside the harness — .eta for templates with conditionals, loaded as raw strings at startup and rendered at call time.