bare-agent 0.20.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/bareagent.context.md +108 -1
- package/package.json +1 -1
- package/src/recurse.d.ts +21 -0
- package/src/recurse.js +34 -5
package/README.md
CHANGED
|
@@ -94,9 +94,16 @@ Every piece works alone — take what you need, ignore the rest. Two axes: **Act
|
|
|
94
94
|
|
|
95
95
|
### Recurse — break a hard task into a tree *(the RLM primitive)*
|
|
96
96
|
|
|
97
|
-
`recurse(task, ctx, opts)` does **decompose → fan-out → verify → synthesize** in one call — Recursive Language Models as a single import, composed *around* the Loop (never a new engine). The default is **model-driven**: the worker is handed a `spawn_child` tool and decides whether to split, bounded by depth + bareguard (no second guard layer). Forced fan-out (`count` / `mode:'fanout'`) and data-driven width (`mode:'partition'`, measured from a corpus) are opt-in. The headline guarantee: **aggregation is code, never a model-stated number**, and a dead worker or exhausted guard returns an honest `{ incomplete, missingSlices }` — never a faked pass.
|
|
97
|
+
`recurse(task, ctx, opts)` does **decompose → fan-out → verify → synthesize** in one call — Recursive Language Models as a single import, composed *around* the Loop (never a new engine). The default is **model-driven**: the worker is handed a `spawn_child` tool and decides whether to split, bounded by depth + bareguard (no second guard layer). Forced fan-out (`count` / `mode:'fanout'`) and data-driven width (`mode:'partition'`, measured from a corpus) are opt-in. Give workers a stance with `opts.persona` (prepended to every worker, carries down the tree, deliberately kept out of the isolated verifier). The headline guarantee: **aggregation is code, never a model-stated number**, and a dead worker or exhausted guard returns an honest `{ incomplete, missingSlices }` — never a faked pass.
|
|
98
98
|
|
|
99
|
-
Over a corpus, context reaches a worker as a **handle routed by question shape
|
|
99
|
+
Over a corpus, context reaches a worker as a **handle routed by question shape** (`opts.retrieval`):
|
|
100
|
+
|
|
101
|
+
| `retrieval` | Use it for | How |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `'scan'` *(default over a corpus)* | "how many / all / count" | scans every slice, LLM-judges each, **code-counts** the union — the only path that can't silently undercount |
|
|
104
|
+
| `'search'` | find a needle (few matches) | litectx `recall` handle tool, embeddings on — **cannot count** |
|
|
105
|
+
| `'exact'` | rule / exact-term match | code-side AND-filter, embeddings off |
|
|
106
|
+
| `'tools'` | mixed task (needle *and* count) | offers all three; the worker picks per sub-query by tool description |
|
|
100
107
|
|
|
101
108
|
```js
|
|
102
109
|
const { recurse } = require('bare-agent');
|
package/bareagent.context.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# bareagent — Integration Guide
|
|
2
2
|
|
|
3
3
|
> For AI assistants and developers wiring bareagent into a project.
|
|
4
|
-
> v0.
|
|
4
|
+
> v0.21.1 | Node.js >= 18 | zero required deps (`bareguard ^0.9.0` optional peer for governance) | Apache 2.0
|
|
5
5
|
>
|
|
6
6
|
> Full human guide with composition examples, design philosophy, and recipes: [Usage Guide](docs/02-features/usage-guide.md)
|
|
7
7
|
|
|
@@ -34,6 +34,9 @@ Eight entry points:
|
|
|
34
34
|
| Size a goal before planning (no LLM) | assessComplexity — `needsPlanning` gates a Planner pass |
|
|
35
35
|
| Kill a spawned child that hangs silently | createSpawnTool / spawnChild `{ idleTimeoutMs }` |
|
|
36
36
|
| Execute a step DAG with parallelism | runPlan + executeFn |
|
|
37
|
+
| Decompose a hard task into a verified tree (RLM) | recurse — decompose → fan-out → verify → synthesize in one call (**wire a gate**, cost is open by design) |
|
|
38
|
+
| Count / answer "how many / all" over a corpus, honestly | recurse(task, ctx, `{ corpus, retrieval: 'scan' }`) — scans every slice, CODE-counts |
|
|
39
|
+
| Give recurse workers a persona/role (senior-dev stance) | recurse(task, ctx, `{ persona }`) — prepended to every worker, carries down the tree; not applied to the verifier |
|
|
37
40
|
| Track task state (pending/running/done/failed) | StateMachine |
|
|
38
41
|
| Run agent turns on a schedule (cron, timers) | Scheduler |
|
|
39
42
|
| Require human approval before dangerous actions | Checkpoint |
|
|
@@ -599,6 +602,107 @@ const results = await runPlan(steps, async (step) => {
|
|
|
599
602
|
// results: [{ id: 's1', status: 'done', result: '...' }, { id: 's2', status: 'failed', error: '...' }, ...]
|
|
600
603
|
```
|
|
601
604
|
|
|
605
|
+
## Wiring with recurse (RLM — decompose → fan-out → verify → synthesize)
|
|
606
|
+
|
|
607
|
+
`recurse(task, ctx, opts)` is the **Recursive Language Models** primitive (v0.20.0): one import that decomposes a hard task into fresh-context workers, verifies against a setpoint, and synthesizes one result. It is **thin glue composed around `Loop`/`Planner`/`runPlan`/`Evaluator`/`spawn`** — not a new engine, never imported by `loop.js`. Returns `{ result, verdict, receipts }` on convergence, or `{ incomplete, best, missingSlices, receipts }` on guard exhaustion / a dead worker — **never a fabricated success** (RC-9).
|
|
608
|
+
|
|
609
|
+
> **⚠️ Cost is open by DESIGN — wire a gate.** `recurse()` adds NO intrinsic total-work cap. On the **Family-A default** (model-driven) a node can spawn up to ~100 children per level, each recursing to `opts.maxDepth` (default 3) — so token/$ spend compounds and is bounded **only by your gate**, not by recurse. A live POC saw a weak model do 40–117 calls in one run. **Always wire bareguard** (`ctx.policy` via `wireGate`) for any non-trivial or untrusted run — it enforces depth/budget/call caps and turns a runaway into a clean `{ incomplete }` (proven: a wired `Gate` cut a 43–117-call runaway to 4–5 calls). The local brake without a gate is `opts.maxDepth: 1` (flat, no nesting). The forced modes (`mode:'fanout'`/`'partition'`) ARE bounded (deterministic count + concurrency cap).
|
|
610
|
+
|
|
611
|
+
```javascript
|
|
612
|
+
const { recurse, wireGate } = require('bare-agent');
|
|
613
|
+
const { Gate } = require('bareguard');
|
|
614
|
+
|
|
615
|
+
// ALWAYS run governed for real work — the gate is the total-work bound.
|
|
616
|
+
const gate = new Gate({ budget: { maxCostUsd: 0.50 }, limits: { maxTurns: 30, maxDepth: 3 }, humanChannel: async () => ({ decision: 'deny' }) });
|
|
617
|
+
await gate.init();
|
|
618
|
+
const { policy, onLlmResult } = wireGate(gate);
|
|
619
|
+
|
|
620
|
+
// ctx = runtime wiring threaded down the whole tree; opts = policy knobs.
|
|
621
|
+
const ctx = { provider, policy, onLlmResult }; // policy + onLlmResult = the gate over every node + the verifier
|
|
622
|
+
const out = await recurse('Audit this 2000-line module for security bugs and rank them', ctx, { maxDepth: 3 });
|
|
623
|
+
|
|
624
|
+
if (out.incomplete) {
|
|
625
|
+
console.warn('did not converge:', out.missingSlices, '— best partial:', out.best); // honest, never a faked pass
|
|
626
|
+
} else {
|
|
627
|
+
console.log(out.result, out.verdict); // the synthesized answer + the verifier's gap report
|
|
628
|
+
}
|
|
629
|
+
console.log(out.receipts.spawned.length); // RC-10 audit tree: parent→child lineage, per-node tokens/verdict
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
**Control families (how the tree is shaped):**
|
|
633
|
+
|
|
634
|
+
- **Family A — model-driven (default).** The worker is handed an in-process `spawn_child` tool and *decides* whether to split. `assessComplexity` is a **hint, not a gate** (only `simple → single-shot`, and the non-overridable `critical → force adversarial verify` safety floor). Nothing extra to set.
|
|
635
|
+
- **Family B — forced fan-out (opt-in).** `{ count: N }` → exactly N independent parallel workers via `Planner`→`runPlan`; or `{ mode: 'fanout' }` → count derived from the complexity tier (medium/complex/critical → 2/4/6). Deterministic + concurrency-capped.
|
|
636
|
+
- **`{ mode: 'partition', corpus, workerBudget }`** — data-driven WIDTH: measure the corpus, split into `max(count floor, ⌈size/workerBudget⌉)` parallel scan-workers, union-count. A pre-wave `recurse_partition` policy checkpoint fires before any worker spends.
|
|
637
|
+
|
|
638
|
+
**Retrieval over a corpus** (`opts.corpus = {id,text}[]` or an async `() => Promise<Slice[]>`, e.g. `litectxCorpus(litectx, {kind})`). Context reaches a worker as a HANDLE routed by question shape:
|
|
639
|
+
|
|
640
|
+
| `retrieval` | For | Note |
|
|
641
|
+
|---|---|---|
|
|
642
|
+
| `'scan'` *(default when `corpus` present)* | "how many / all / count" | scans every slice, LLM-judges, **CODE-counts** the union — the only path that can't undercount; `window` 8, `passes` 2 |
|
|
643
|
+
| `'search'` | find a needle | litectx `recall` tool (needs `ctx.litectx`), embeddings on — **cannot count** |
|
|
644
|
+
| `'exact'` | rule / exact-term match | code-side AND-filter, embeddings off |
|
|
645
|
+
| `'tools'` | mixed task (needle *and* count) | offers `scan_count` + `search_memory` + `exact_match`; worker picks per sub-query by tool description |
|
|
646
|
+
|
|
647
|
+
A completeness guard **upgrades** a `'search'` on a "how many / all" ask to `'scan'` (upgrade-only, never a silent downgrade). Aggregation is **always code**, never a model-stated number.
|
|
648
|
+
|
|
649
|
+
```javascript
|
|
650
|
+
// Honest count over a corpus — scans every slice, code-counts the matches.
|
|
651
|
+
const { result } = await recurse(
|
|
652
|
+
'How many of these support tickets are billing disputes?',
|
|
653
|
+
{ provider, policy }, // still wire the gate
|
|
654
|
+
{ corpus: tickets /* {id,text}[] */, retrieval: 'scan' },
|
|
655
|
+
);
|
|
656
|
+
console.log(result.count, result.matchedIds); // a code-derived count + the ids that back it
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
**Synthesis (`opts.synthesize`):** a **function** (deterministic code-reduce over child `results` — use for arithmetic/aggregation; LLM arithmetic over partials carried ~10–15% error), or `'concat'` (lossless no-LLM join), or `'merge'` (isolated Loop-driven subjective merge). Default = the parent model's own closing-turn synthesis. **`opts.contract`** = a definition-of-done the verifier grades against (instead of the loose task); **`opts.evaluate`** overrides the verifier. Exported helpers for the per-query face: `buildScanTool`, `buildSearchTool`, `buildExactTool`, `litectxCorpus`.
|
|
660
|
+
|
|
661
|
+
**Worker persona (`opts.persona`, v0.21.0):** a string PREPENDED to every Family-A worker's system prompt — give workers a stance (`persona: 'You are a senior security engineer; be specific and cite the exact file:line'`). It **augments**, never replaces, the built-in decomposition policy + depth-scrub (those drive the spawn mechanics), and **carries down the whole tree** (a child of a "senior security engineer" is still one). It is deliberately **not** applied to the isolated verifier (that isolation is what defeats self-grading sycophancy) nor the deterministic scan judge. Absent ⇒ the worker prompt is unchanged from pre-0.21. **Security:** treat `persona` like a system prompt — it is prepended ahead of the decomposition policy and can override it for every worker, so pass caller-trusted text only, never untrusted/end-user input.
|
|
662
|
+
|
|
663
|
+
```javascript
|
|
664
|
+
const out = await recurse('Audit auth.js, billing.js, gateway.js for authz bugs', ctx, {
|
|
665
|
+
persona: 'You are a blunt senior application-security engineer. Report each finding as file:line + impact + fix.',
|
|
666
|
+
});
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
**What a delegated child inherits (important — the setpoint is the TOP node's job):** when a worker delegates with `spawn_child`, the child runs a **fresh `recurse`** that inherits `tools`, `synthesize`, `maxDepth`, and `persona` — but the parent's **`contract`/`evaluate` are stripped** (and the forced `count`/`mode` + the corpus `retrieval` knobs). A slice is not graded against the *whole*-task definition-of-done (that verdict is the top node's, and a slice satisfying the whole DoD is the wrong question); only the top `recurse` verifies the synthesized result. The non-overridable `critical → force-verify` safety floor still fires per node (it keys on the task text, not the contract). So: set `contract`/`evaluate` once at the top; they do not — and should not — re-run per intermediate node.
|
|
670
|
+
|
|
671
|
+
## Wiring with Evaluator + refine (output-side verification)
|
|
672
|
+
|
|
673
|
+
`Evaluator` is the output-side judge (the mirror of `Planner`): it grades a result against a goal and returns a tri-state `Verdict`. `refine` is the bounded generate → evaluate → regenerate loop. Both compose *around* a Loop — neither lives inside `loop.js`.
|
|
674
|
+
|
|
675
|
+
```javascript
|
|
676
|
+
const { Evaluator, refine } = require('bare-agent');
|
|
677
|
+
|
|
678
|
+
const evaluator = new Evaluator({ provider }); // provider REQUIRED for rubric/agentic; predicate needs none
|
|
679
|
+
|
|
680
|
+
// Three criteria types — pass EXACTLY ONE:
|
|
681
|
+
const v1 = await evaluator.evaluate(goal, result, { predicate: (r) => r.includes('DONE') }); // deterministic, 0 tokens
|
|
682
|
+
const v2 = await evaluator.evaluate(goal, result, { rubric: 'Cites a source for every claim.' }); // isolated adversarial LLM grader
|
|
683
|
+
const v3 = await evaluator.evaluate(goal, url, { agentic: 'Open the page, click Submit, check the console for errors.' }); // tool-running critic that EXERCISES the artifact
|
|
684
|
+
|
|
685
|
+
// Verdict: { status: 'satisfied' | 'needs_revision' | 'failed', pass, score, critique, suggestions }
|
|
686
|
+
// pass = (status === 'satisfied'); needs_revision is retryable; failed is terminal (stop spending).
|
|
687
|
+
if (!v2.pass) console.log(v2.critique, v2.suggestions);
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
Key invariants:
|
|
691
|
+
- The **rubric path runs an isolated adversarial grader** — a separate context window with a harsh, independent prompt, never the generator's transcript. That isolation (not a feedback knob) is what defeats the self-evaluation trap; the grader treats the RESULT as untrusted DATA (judge prompt-injection defence).
|
|
692
|
+
- **`agentic`** (the third type) spins up a fresh Loop with scoped tools (set on the Evaluator, or per-call `opts.tools`) that **exercises** the live artifact — clicks, reads console/network — rather than reading text. Each critic round forwards to `onLlmResult`; a governance `HaltError` re-throws clean.
|
|
693
|
+
- **`contract`** (a definition of done) is graded against instead of the loose goal: `evaluate(goal, result, { rubric, contract })`. Judge tokens forward to the gate via `onLlmResult` (`kind:'evaluate'`) so verification spend is visible to the budget.
|
|
694
|
+
|
|
695
|
+
**`refine`** drives a caller-supplied `attempt`/`evaluate` until a satisfied verdict, a terminal `failed`, or `maxIterations` (the real bound is bareguard maxTurns/budget). It threads the latest `critique` into the next attempt (fresh-feedback, not anchoring on a failed answer) and a shared `contract` to both sides.
|
|
696
|
+
|
|
697
|
+
```javascript
|
|
698
|
+
const { result, verdict, iterations, history } = await refine({
|
|
699
|
+
attempt: ({ critique, contract }) => generate(prompt, { critique, contract }), // critique = null on the first pass
|
|
700
|
+
evaluate: (result, { contract }) => evaluator.evaluate(goal, result, { rubric, contract }),
|
|
701
|
+
contract: 'No TODOs; every public fn has a JSDoc; tests pass.',
|
|
702
|
+
maxIterations: 3, // hard cap; the REAL bound is the gate
|
|
703
|
+
});
|
|
704
|
+
```
|
|
705
|
+
|
|
602
706
|
## Provider options
|
|
603
707
|
|
|
604
708
|
```javascript
|
|
@@ -608,6 +712,9 @@ new OpenAI({ apiKey, model: 'gpt-4o-mini', baseUrl: 'https://api.openai.com/v1'
|
|
|
608
712
|
// Anthropic
|
|
609
713
|
new Anthropic({ apiKey, model: 'claude-haiku-4-5-20251001' })
|
|
610
714
|
|
|
715
|
+
// Gemini (native generateContent — needed for prompt-cache token tiers; the OpenAI-compat endpoint drops them)
|
|
716
|
+
new Gemini({ apiKey, model: 'gemini-2.5-flash', baseUrl: 'https://generativelanguage.googleapis.com/v1beta' })
|
|
717
|
+
|
|
611
718
|
// Ollama (local, no key needed)
|
|
612
719
|
new Ollama({ model: 'llama3.2', url: 'http://localhost:11434' })
|
|
613
720
|
|
package/package.json
CHANGED
package/src/recurse.d.ts
CHANGED
|
@@ -47,6 +47,18 @@ export type RecurseOptions = {
|
|
|
47
47
|
* and actual depth is always ≤ `limits.maxDepth`.
|
|
48
48
|
*/
|
|
49
49
|
maxDepth?: number | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* - (Gap 3 / 0.21.0) An optional caller stance PREPENDED to every Family-A worker's
|
|
52
|
+
* system prompt (e.g. "You are a senior security engineer; …"). It AUGMENTS the built-in decomposition policy +
|
|
53
|
+
* depth-scrub, never replaces them (that text drives the spawn mechanics), and CARRIES DOWN the whole tree
|
|
54
|
+
* (preserved by `forChild` — a durable worker stance, unlike the top-only `contract`/`evaluate`). Deliberately
|
|
55
|
+
* NOT applied to the isolated verifier (would defeat the anti-sycophancy isolation, A1) nor the deterministic
|
|
56
|
+
* scan judge. Absent/blank ⇒ the worker prompt is byte-identical to pre-0.21 (backward-compatible).
|
|
57
|
+
* **SECURITY:** this is a PRIVILEGED system-prompt seam — treat `persona` like a system prompt. Do NOT pass
|
|
58
|
+
* untrusted / end-user-controlled text here; a hostile persona is prepended ahead of the decomposition policy
|
|
59
|
+
* and can override it (and any safety framing) for every worker in the tree. Caller-trusted input only.
|
|
60
|
+
*/
|
|
61
|
+
persona?: string | undefined;
|
|
50
62
|
/**
|
|
51
63
|
* - Handle tools offered to EVERY worker (RC-5 pull-default: litectx
|
|
52
64
|
* `recall`/`get`, wired at build step 7). Workers query on demand; never the whole corpus.
|
|
@@ -252,6 +264,15 @@ export type Slice = {
|
|
|
252
264
|
* @property {number} [maxDepth=3] - Open topology ceiling (§1): the depth past which the `spawn_child` tool is
|
|
253
265
|
* no longer offered (`maxDepth=1` ⇒ flat fan-out, no nesting). NOT the safety halt — that is bareguard's,
|
|
254
266
|
* and actual depth is always ≤ `limits.maxDepth`.
|
|
267
|
+
* @property {string} [persona] - (Gap 3 / 0.21.0) An optional caller stance PREPENDED to every Family-A worker's
|
|
268
|
+
* system prompt (e.g. "You are a senior security engineer; …"). It AUGMENTS the built-in decomposition policy +
|
|
269
|
+
* depth-scrub, never replaces them (that text drives the spawn mechanics), and CARRIES DOWN the whole tree
|
|
270
|
+
* (preserved by `forChild` — a durable worker stance, unlike the top-only `contract`/`evaluate`). Deliberately
|
|
271
|
+
* NOT applied to the isolated verifier (would defeat the anti-sycophancy isolation, A1) nor the deterministic
|
|
272
|
+
* scan judge. Absent/blank ⇒ the worker prompt is byte-identical to pre-0.21 (backward-compatible).
|
|
273
|
+
* **SECURITY:** this is a PRIVILEGED system-prompt seam — treat `persona` like a system prompt. Do NOT pass
|
|
274
|
+
* untrusted / end-user-controlled text here; a hostile persona is prepended ahead of the decomposition policy
|
|
275
|
+
* and can override it (and any safety framing) for every worker in the tree. Caller-trusted input only.
|
|
255
276
|
* @property {ToolDef[]} [tools] - Handle tools offered to EVERY worker (RC-5 pull-default: litectx
|
|
256
277
|
* `recall`/`get`, wired at build step 7). Workers query on demand; never the whole corpus.
|
|
257
278
|
* @property {string} [contract] - Definition of done (A3). When present, the verifier grades against THIS,
|
package/src/recurse.js
CHANGED
|
@@ -69,6 +69,19 @@ function partitionInto(arr, n) {
|
|
|
69
69
|
return chunks;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* The persona prefix for a worker system prompt (Gap 3 / 0.21.0). A caller-supplied `opts.persona` AUGMENTS the
|
|
74
|
+
* decomposition policy + scrub (never replaces them — that text drives the spawn mechanics). Returns '' when
|
|
75
|
+
* absent/blank, so the default worker prompt is byte-identical to pre-0.21 (backward-compatible). Validated live
|
|
76
|
+
* by `poc/rlm-persona-seam.mjs` (the worker still decomposes, adopts the persona, and carries it to children).
|
|
77
|
+
* @param {unknown} persona
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
function workerPersonaPrefix(persona) {
|
|
81
|
+
const p = typeof persona === 'string' ? persona.trim() : '';
|
|
82
|
+
return p ? p + '\n\n' : '';
|
|
83
|
+
}
|
|
84
|
+
|
|
72
85
|
/**
|
|
73
86
|
* The opts a delegated child inherits. Strips the parent's TOP-LEVEL SETPOINT — `contract`/`evaluate` grade
|
|
74
87
|
* the WHOLE task's final answer; a child grading its own slice against the whole definition-of-done is wasted
|
|
@@ -78,7 +91,9 @@ function partitionInto(arr, n) {
|
|
|
78
91
|
* answered over the parent's corpus; a child has its own subtask and must not re-scan the parent's full corpus
|
|
79
92
|
* (that would fan a whole-corpus count out under every child). The `critical → force-verify` SAFETY FLOOR is
|
|
80
93
|
* unaffected — it keys on the task text via `isCritical`, not the contract, so a critical child still
|
|
81
|
-
* self-verifies. Handle tools (`opts.tools`), `synthesize`,
|
|
94
|
+
* self-verifies. Handle tools (`opts.tools`), `synthesize`, `maxDepth`, and **`persona`** carry down — the
|
|
95
|
+
* persona is a DURABLE worker stance (a child of a "senior security engineer" is still one), unlike the
|
|
96
|
+
* top-only `contract`/`evaluate` setpoint. It rides through the `...opts` spread (not in the strip list).
|
|
82
97
|
* @param {RecurseOptions} opts
|
|
83
98
|
* @returns {RecurseOptions}
|
|
84
99
|
*/
|
|
@@ -120,6 +135,15 @@ function forChild(opts) {
|
|
|
120
135
|
* @property {number} [maxDepth=3] - Open topology ceiling (§1): the depth past which the `spawn_child` tool is
|
|
121
136
|
* no longer offered (`maxDepth=1` ⇒ flat fan-out, no nesting). NOT the safety halt — that is bareguard's,
|
|
122
137
|
* and actual depth is always ≤ `limits.maxDepth`.
|
|
138
|
+
* @property {string} [persona] - (Gap 3 / 0.21.0) An optional caller stance PREPENDED to every Family-A worker's
|
|
139
|
+
* system prompt (e.g. "You are a senior security engineer; …"). It AUGMENTS the built-in decomposition policy +
|
|
140
|
+
* depth-scrub, never replaces them (that text drives the spawn mechanics), and CARRIES DOWN the whole tree
|
|
141
|
+
* (preserved by `forChild` — a durable worker stance, unlike the top-only `contract`/`evaluate`). Deliberately
|
|
142
|
+
* NOT applied to the isolated verifier (would defeat the anti-sycophancy isolation, A1) nor the deterministic
|
|
143
|
+
* scan judge. Absent/blank ⇒ the worker prompt is byte-identical to pre-0.21 (backward-compatible).
|
|
144
|
+
* **SECURITY:** this is a PRIVILEGED system-prompt seam — treat `persona` like a system prompt. Do NOT pass
|
|
145
|
+
* untrusted / end-user-controlled text here; a hostile persona is prepended ahead of the decomposition policy
|
|
146
|
+
* and can override it (and any safety framing) for every worker in the tree. Caller-trusted input only.
|
|
123
147
|
* @property {ToolDef[]} [tools] - Handle tools offered to EVERY worker (RC-5 pull-default: litectx
|
|
124
148
|
* `recall`/`get`, wired at build step 7). Workers query on demand; never the whole corpus.
|
|
125
149
|
* @property {string} [contract] - Definition of done (A3). When present, the verifier grades against THIS,
|
|
@@ -312,10 +336,15 @@ async function recurse(task, ctx = {}, opts = {}) {
|
|
|
312
336
|
// and what makes `maxDepth=1` flat (RC-11): top spawns, children cannot (no nesting).
|
|
313
337
|
const canSpawn = depth < maxDepth && assessment.level !== 'simple';
|
|
314
338
|
|
|
315
|
-
// Capability-scrub (NB-4 / RC-12): the worker system prompt is the
|
|
316
|
-
// depth-conservative suffix that nudges deeper workers toward direct
|
|
317
|
-
//
|
|
318
|
-
|
|
339
|
+
// Capability-scrub (NB-4 / RC-12): the worker system prompt is the OPTIONAL caller persona (`opts.persona`) +
|
|
340
|
+
// the decomposition policy (NB-5) + a depth-conservative suffix that nudges deeper workers toward direct
|
|
341
|
+
// action. The persona AUGMENTS, never replaces — the decomposition + scrub text is load-bearing for the spawn
|
|
342
|
+
// mechanics, so a persona that replaced it would break decomposition (POC `rlm-persona-seam.mjs` validated the
|
|
343
|
+
// prepend: the worker still decomposes AND adopts the persona). Persona CARRIES DOWN the tree (it is preserved
|
|
344
|
+
// by `forChild`, unlike contract/evaluate) — a durable worker stance, not a top-only setpoint. It is deliberately
|
|
345
|
+
// NOT applied to the isolated verifier (that would defeat the anti-sycophancy isolation) nor the scan judge.
|
|
346
|
+
// Tool set is monotone: a child's tools ⊆ its parent's (same handle tools, spawn dropped at the cap).
|
|
347
|
+
const system = workerPersonaPrefix(opts.persona) + DECOMPOSITION_POLICY + capabilityScrub(depth, maxDepth);
|
|
319
348
|
|
|
320
349
|
// Handle tools (RC-5 pull-default) = caller-supplied `opts.tools` + the retrieval handle for `search`/`exact`/
|
|
321
350
|
// `tools` (offered so the Family-A worker pulls context per sub-query, never the whole corpus). `search` needs
|