bare-agent 0.18.0 → 0.20.0
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 +25 -4
- package/bareagent.context.md +2 -2
- package/index.d.ts +6 -1
- package/index.js +7 -0
- package/package.json +10 -8
- package/src/planner.d.ts +21 -1
- package/src/planner.js +24 -4
- package/src/recurse-prompts.d.ts +22 -0
- package/src/recurse-prompts.js +71 -0
- package/src/recurse-retrieval.d.ts +174 -0
- package/src/recurse-retrieval.js +372 -0
- package/src/recurse-synthesize.d.ts +53 -0
- package/src/recurse-synthesize.js +97 -0
- package/src/recurse.d.ts +361 -0
- package/src/recurse.js +886 -0
package/README.md
CHANGED
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
<img src="https://img.shields.io/badge/license-Apache%202.0-2a4f8c" alt="license: Apache 2.0">
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
|
-
**Lightweight agent orchestration.
|
|
20
|
+
**Lightweight agent orchestration. Zero required deps — optional [bareguard](https://npmjs.com/package/bareguard) peer for single-gate governance.**
|
|
21
21
|
|
|
22
|
-
Lightweight enough to understand completely. Complete enough to not reinvent wheels. Not a framework, not 50,000 lines of opinions — just composable building blocks for agents.
|
|
22
|
+
Lightweight enough to understand completely. Complete enough to not reinvent wheels. Not a framework, not 50,000 lines of opinions — just composable building blocks for agents. The core imports nothing; when you want governance, wire bareguard and every tool call traverses one policy hook, one audit log, one budget cap.
|
|
23
23
|
|
|
24
24
|
## Quick start
|
|
25
25
|
|
|
@@ -62,7 +62,7 @@ and show me the wiring code.
|
|
|
62
62
|
|
|
63
63
|
## What's inside
|
|
64
64
|
|
|
65
|
-
Every piece works alone — take what you need, ignore the rest. Two axes: **Act** (get work done) and **Verify** (check it, keep context clean), with **one gate** over both.
|
|
65
|
+
Every piece works alone — take what you need, ignore the rest. Two axes: **Act** (get work done) and **Verify** (check it, keep context clean), with **one gate** over both — plus **`recurse`**, an Act-side primitive big enough to earn its own spotlight below.
|
|
66
66
|
|
|
67
67
|
### Act — get work done
|
|
68
68
|
|
|
@@ -72,6 +72,7 @@ Every piece works alone — take what you need, ignore the rest. Two axes: **Act
|
|
|
72
72
|
| **Planner** | Break a goal into a step DAG. Cached |
|
|
73
73
|
| **assessComplexity** | Rate a goal `simple`→`critical` from its text — no LLM. Gates whether to plan |
|
|
74
74
|
| **runPlan** | Run plan steps in parallel waves. Dependency-aware, per-step retry |
|
|
75
|
+
| **recurse** | RLM decompose→fan-out→verify→synthesize in one call. Model-driven (`spawn_child`) or forced fan-out (`count`); `retrieval:'scan'` answers "how many / all" over a corpus by scanning every slice and code-counting — never a faked pass (honest `{incomplete, missingSlices}`) |
|
|
75
76
|
| **Memory** | Persist + recall across sessions via a swappable `Store` — zero-dep JSON, SQLite, or [litectx](https://npmjs.com/package/litectx) in a one-line swap |
|
|
76
77
|
| **StateMachine** | Task lifecycle: `pending → running → done / failed / waiting / cancelled` |
|
|
77
78
|
| **Scheduler** | Cron or relative triggers. Jobs survive restarts |
|
|
@@ -91,6 +92,26 @@ Every piece works alone — take what you need, ignore the rest. Two axes: **Act
|
|
|
91
92
|
| **SkillRegistry** | Surface skills on demand: one meta-tool catalog; activating a skill injects its instructions and unlocks its tools |
|
|
92
93
|
| **stash** | Compact finished work out of the live window (restorable), or auto-fold the middle under token pressure |
|
|
93
94
|
|
|
95
|
+
### Recurse — break a hard task into a tree *(the RLM primitive)*
|
|
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.
|
|
98
|
+
|
|
99
|
+
Over a corpus, context reaches a worker as a **handle routed by question shape**: `'scan'` answers *"how many / all"* by scanning every slice and **code-counting** the matches (the only path that can't silently undercount); `'search'` / `'exact'` are per-query handle tools for needles and rules; `'tools'` offers all three and lets the worker pick per sub-query.
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
const { recurse } = require('bare-agent');
|
|
103
|
+
|
|
104
|
+
// Honest count over a corpus: scans every slice, LLM-judges each, CODE-counts the union.
|
|
105
|
+
const { result } = await recurse(
|
|
106
|
+
'How many of these support tickets are billing disputes?',
|
|
107
|
+
{ provider },
|
|
108
|
+
{ corpus: tickets /* {id,text}[] */, retrieval: 'scan' },
|
|
109
|
+
);
|
|
110
|
+
console.log(result.count, result.matchedIds); // a code-derived count + the ids that back it
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
> **⚠️ Cost is open by design — wire a cap.** `recurse()` adds no intrinsic total-work limit. On the model-driven default a node can spawn up to ~100 children per level, each recursing to `maxDepth` (default 3), so **token / $ spend compounds and is bounded only by your gate** — not by recurse. Run it **with bareguard** (`ctx.policy`, which enforces depth/budget/call caps) **or with some token/USD cap** for any non-trivial or untrusted task; ungoverned, a weak model that over-decomposes *will* burn tokens. For a hard local brake without a gate, set `maxDepth: 1` (flat, no nesting). The forced modes (`mode:'fanout'` / `'partition'`) are bounded by a deterministic count + concurrency cap; the open path is the model-driven default.
|
|
114
|
+
|
|
94
115
|
**Govern — one gate over both axes.** `wireGate(gate)` routes every LLM + tool call through one bareguard policy + audit + budget. Denied tools never reach the model; halts (turn / budget / content caps) exit cleanly. `require('bare-agent/bareguard')`
|
|
95
116
|
|
|
96
117
|
**Providers:** OpenAI-compatible (OpenAI, OpenRouter, Groq, vLLM, LM Studio), Anthropic, Gemini (native), Ollama, CLIPipe, Fallback — or bring your own (one `generate` method). All return the same shape; swap freely. Usage including prompt-cache tiers is normalized, so `result.metrics` reports honest cumulative tokens + cost — and `null`, never a silent `0`, for a model it couldn't price.
|
|
@@ -99,7 +120,7 @@ Every piece works alone — take what you need, ignore the rest. Two axes: **Act
|
|
|
99
120
|
|
|
100
121
|
**Cross-language:** Run as a subprocess; talk JSONL over stdin/stdout from Python, Go, Rust, Ruby, or Java. Wrappers in [`contrib/`](contrib/README.md).
|
|
101
122
|
|
|
102
|
-
**Deps:**
|
|
123
|
+
**Deps:** none required — the core imports nothing. Optional peers: `bareguard ^0.9.0` (governance), `better-sqlite3` (SQLite store); optional: `cron-parser`, `barebrowse`, `baremobile`, `wearehere`.
|
|
103
124
|
|
|
104
125
|
This table is the map, not the manual — per-component wiring and API detail live in the [Integration Guide](bareagent.context.md) and [Usage Guide](docs/02-features/usage-guide.md).
|
|
105
126
|
|
package/bareagent.context.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# bareagent — Integration Guide
|
|
2
2
|
|
|
3
3
|
> For AI assistants and developers wiring bareagent into a project.
|
|
4
|
-
> v0.
|
|
4
|
+
> v0.20.0 | 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
|
|
|
8
8
|
## What this is
|
|
9
9
|
|
|
10
|
-
bareagent is a lightweight agent orchestration library (
|
|
10
|
+
bareagent is a lightweight agent orchestration library (small core, zero required runtime deps — `bareguard` is an optional peer for governance). It provides composable components for LLM tool-calling loops, goal planning, state tracking, scheduled actions, human approval gates, persistent memory, circuit breaking, provider fallback, single-gate governance via [bareguard](https://npmjs.com/package/bareguard), cross-platform shell tools, and an MCP bridge. All components are independent — use one, use all, or bring your own.
|
|
11
11
|
|
|
12
12
|
```
|
|
13
13
|
npm install bare-agent
|
package/index.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { Loop } from "./src/loop";
|
|
|
2
2
|
import { Planner } from "./src/planner";
|
|
3
3
|
import { Evaluator } from "./src/evaluator";
|
|
4
4
|
import { refine } from "./src/refine";
|
|
5
|
+
import { recurse } from "./src/recurse";
|
|
6
|
+
import { buildSearchTool } from "./src/recurse-retrieval";
|
|
7
|
+
import { buildExactTool } from "./src/recurse-retrieval";
|
|
8
|
+
import { buildScanTool } from "./src/recurse-retrieval";
|
|
9
|
+
import { litectxCorpus } from "./src/recurse-retrieval";
|
|
5
10
|
import { remember } from "./src/remember";
|
|
6
11
|
import { assessComplexity } from "./src/complexity";
|
|
7
12
|
import { isCritical } from "./src/complexity";
|
|
@@ -29,4 +34,4 @@ import { TimeoutError } from "./src/errors";
|
|
|
29
34
|
import { ValidationError } from "./src/errors";
|
|
30
35
|
import { CircuitOpenError } from "./src/errors";
|
|
31
36
|
import { HaltError } from "./src/errors";
|
|
32
|
-
export { Loop, Planner, Evaluator, refine, remember, assessComplexity, isCritical, SkillRegistry, createStashSkill, StateMachine, Scheduler, Checkpoint, Memory, Stream, Retry, runPlan, CircuitBreaker, wireGate, defaultActionTranslator, toUnits, fromUnits, unitAssembler, unitTrimmer, harvestKey, BareAgentError, ProviderError, ToolError, TimeoutError, ValidationError, CircuitOpenError, HaltError };
|
|
37
|
+
export { Loop, Planner, Evaluator, refine, recurse, buildSearchTool, buildExactTool, buildScanTool, litectxCorpus, remember, assessComplexity, isCritical, SkillRegistry, createStashSkill, StateMachine, Scheduler, Checkpoint, Memory, Stream, Retry, runPlan, CircuitBreaker, wireGate, defaultActionTranslator, toUnits, fromUnits, unitAssembler, unitTrimmer, harvestKey, BareAgentError, ProviderError, ToolError, TimeoutError, ValidationError, CircuitOpenError, HaltError };
|
package/index.js
CHANGED
|
@@ -4,6 +4,8 @@ const { Loop } = require('./src/loop');
|
|
|
4
4
|
const { Planner } = require('./src/planner');
|
|
5
5
|
const { Evaluator } = require('./src/evaluator');
|
|
6
6
|
const { refine } = require('./src/refine');
|
|
7
|
+
const { recurse } = require('./src/recurse');
|
|
8
|
+
const { buildSearchTool, buildExactTool, buildScanTool, litectxCorpus } = require('./src/recurse-retrieval');
|
|
7
9
|
const { remember } = require('./src/remember');
|
|
8
10
|
const { assessComplexity, isCritical } = require('./src/complexity');
|
|
9
11
|
const { SkillRegistry } = require('./src/skills');
|
|
@@ -33,6 +35,11 @@ module.exports = {
|
|
|
33
35
|
Planner,
|
|
34
36
|
Evaluator,
|
|
35
37
|
refine,
|
|
38
|
+
recurse,
|
|
39
|
+
buildSearchTool,
|
|
40
|
+
buildExactTool,
|
|
41
|
+
buildScanTool,
|
|
42
|
+
litectxCorpus,
|
|
36
43
|
remember,
|
|
37
44
|
assessComplexity,
|
|
38
45
|
isCritical,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bare-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"files": [
|
|
5
5
|
"index.js",
|
|
6
6
|
"index.d.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"LICENSE",
|
|
14
14
|
"NOTICE"
|
|
15
15
|
],
|
|
16
|
-
"description": "Lightweight, composable agent orchestration for autonomous agents. Multi-agent primitives (spawn, defer, MCP meta-tools), single-gate governance via bareguard, cross-platform shell tools, MCP bridge.
|
|
16
|
+
"description": "Lightweight, composable agent orchestration for autonomous agents. Multi-agent primitives (spawn, defer, MCP meta-tools), single-gate governance via an optional bareguard peer, cross-platform shell tools, MCP bridge. Zero required runtime deps.",
|
|
17
17
|
"license": "Apache-2.0",
|
|
18
18
|
"author": "hamr0",
|
|
19
19
|
"repository": {
|
|
@@ -74,25 +74,26 @@
|
|
|
74
74
|
"bareguard",
|
|
75
75
|
"governance"
|
|
76
76
|
],
|
|
77
|
-
"dependencies": {
|
|
78
|
-
"bareguard": "^0.9.0"
|
|
79
|
-
},
|
|
80
77
|
"optionalDependencies": {
|
|
81
78
|
"barebrowse": "^0.5.0",
|
|
82
79
|
"baremobile": "^0.7.0",
|
|
83
80
|
"cron-parser": "^4.9.0"
|
|
84
81
|
},
|
|
85
82
|
"peerDependencies": {
|
|
83
|
+
"bareguard": "^0.9.0",
|
|
86
84
|
"better-sqlite3": ">=9.0.0"
|
|
87
85
|
},
|
|
88
86
|
"peerDependenciesMeta": {
|
|
87
|
+
"bareguard": {
|
|
88
|
+
"optional": true
|
|
89
|
+
},
|
|
89
90
|
"better-sqlite3": {
|
|
90
91
|
"optional": true
|
|
91
92
|
}
|
|
92
93
|
},
|
|
93
94
|
"scripts": {
|
|
94
|
-
"test": "node --test
|
|
95
|
-
"test:unit": "node --test
|
|
95
|
+
"test": "node --test test/**/*.test.js",
|
|
96
|
+
"test:unit": "node --test \"test/!(integration*|*mcp*|spawn*).test.js\"",
|
|
96
97
|
"typecheck": "tsc --noEmit",
|
|
97
98
|
"prebuild:types": "node scripts/clean-types.js",
|
|
98
99
|
"build:types": "tsc",
|
|
@@ -100,7 +101,8 @@
|
|
|
100
101
|
},
|
|
101
102
|
"devDependencies": {
|
|
102
103
|
"@types/node": "^22.19.19",
|
|
103
|
-
"
|
|
104
|
+
"bareguard": "^0.9.0",
|
|
105
|
+
"litectx": "^0.26.0",
|
|
104
106
|
"typescript": "^5.7.0"
|
|
105
107
|
}
|
|
106
108
|
}
|
package/src/planner.d.ts
CHANGED
|
@@ -30,6 +30,17 @@ export type PlannerOptions = {
|
|
|
30
30
|
* - Cache time-to-live in ms. 0 disables caching.
|
|
31
31
|
*/
|
|
32
32
|
cacheTTL?: number | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* - Budget hook
|
|
35
|
+
* (mirror of Evaluator's). Forwards the planning call's `usage` to the gate so decomposition spend is
|
|
36
|
+
* visible — without it the plan call is invisible to bareguard's budget (the RLM Family-B meter gap). A
|
|
37
|
+
* cache hit does NOT forward (no LLM call happened).
|
|
38
|
+
*/
|
|
39
|
+
onLlmResult?: ((payload: {
|
|
40
|
+
usage: any;
|
|
41
|
+
model: string | null;
|
|
42
|
+
kind: "plan";
|
|
43
|
+
}) => any) | undefined;
|
|
33
44
|
};
|
|
34
45
|
export class Planner {
|
|
35
46
|
/**
|
|
@@ -41,10 +52,18 @@ export class Planner {
|
|
|
41
52
|
prompt: string;
|
|
42
53
|
_cacheTTL: number;
|
|
43
54
|
_cache: Map<any, any>;
|
|
55
|
+
onLlmResult: ((payload: {
|
|
56
|
+
usage: any;
|
|
57
|
+
model: string | null;
|
|
58
|
+
kind: "plan";
|
|
59
|
+
}) => any) | null;
|
|
44
60
|
/**
|
|
45
61
|
* Generate a step DAG from a goal.
|
|
46
62
|
* @param {string} goal - The user's goal to decompose.
|
|
47
|
-
* @param {{info?: string}} [context={}] - Optional context
|
|
63
|
+
* @param {{info?: string, count?: number}} [context={}] - Optional context. `info` is prior
|
|
64
|
+
* context to factor in. `count` (RLM NB-2 seam): when a positive integer, forces the plan to
|
|
65
|
+
* exactly that many INDEPENDENT, parallelizable steps (all `dependsOn: []`) instead of the
|
|
66
|
+
* model's free 2–7 — lets `recurse()` impose the deterministic tier→count for forced fan-out.
|
|
48
67
|
* @returns {Promise<Step[]>}
|
|
49
68
|
* @throws {Error} `[Planner] could not parse plan` — when LLM output is not parseable JSON.
|
|
50
69
|
* @throws {Error} `[Planner] expected JSON array` — when parsed result is not an array.
|
|
@@ -52,6 +71,7 @@ export class Planner {
|
|
|
52
71
|
*/
|
|
53
72
|
plan(goal: string, context?: {
|
|
54
73
|
info?: string;
|
|
74
|
+
count?: number;
|
|
55
75
|
}): Promise<Step[]>;
|
|
56
76
|
clearCache(): void;
|
|
57
77
|
/**
|
package/src/planner.js
CHANGED
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
* @property {Provider} provider - LLM provider (must implement generate()).
|
|
16
16
|
* @property {string} [prompt] - Custom planning prompt override.
|
|
17
17
|
* @property {number} [cacheTTL] - Cache time-to-live in ms. 0 disables caching.
|
|
18
|
+
* @property {(payload: {usage: any, model: string|null, kind: 'plan'}) => any} [onLlmResult] - Budget hook
|
|
19
|
+
* (mirror of Evaluator's). Forwards the planning call's `usage` to the gate so decomposition spend is
|
|
20
|
+
* visible — without it the plan call is invisible to bareguard's budget (the RLM Family-B meter gap). A
|
|
21
|
+
* cache hit does NOT forward (no LLM call happened).
|
|
18
22
|
*/
|
|
19
23
|
|
|
20
24
|
const PLAN_PROMPT = `You are a planning agent. Break the user's goal into concrete steps.
|
|
@@ -42,28 +46,38 @@ class Planner {
|
|
|
42
46
|
this.prompt = options.prompt || PLAN_PROMPT;
|
|
43
47
|
this._cacheTTL = options.cacheTTL || 0;
|
|
44
48
|
this._cache = new Map();
|
|
49
|
+
this.onLlmResult = options.onLlmResult || null;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
/**
|
|
48
53
|
* Generate a step DAG from a goal.
|
|
49
54
|
* @param {string} goal - The user's goal to decompose.
|
|
50
|
-
* @param {{info?: string}} [context={}] - Optional context
|
|
55
|
+
* @param {{info?: string, count?: number}} [context={}] - Optional context. `info` is prior
|
|
56
|
+
* context to factor in. `count` (RLM NB-2 seam): when a positive integer, forces the plan to
|
|
57
|
+
* exactly that many INDEPENDENT, parallelizable steps (all `dependsOn: []`) instead of the
|
|
58
|
+
* model's free 2–7 — lets `recurse()` impose the deterministic tier→count for forced fan-out.
|
|
51
59
|
* @returns {Promise<Step[]>}
|
|
52
60
|
* @throws {Error} `[Planner] could not parse plan` — when LLM output is not parseable JSON.
|
|
53
61
|
* @throws {Error} `[Planner] expected JSON array` — when parsed result is not an array.
|
|
54
62
|
* @throws {Error} `[Planner] step missing id or action` — when a step lacks required fields.
|
|
55
63
|
*/
|
|
56
64
|
async plan(goal, context = {}) {
|
|
65
|
+
// NB-2: a forced fan-out count (positive integer only — a 0/NaN/negative falls back to free planning).
|
|
66
|
+
const count = Number.isInteger(context.count) && /** @type {number} */ (context.count) > 0
|
|
67
|
+
? /** @type {number} */ (context.count) : null;
|
|
57
68
|
if (this._cacheTTL > 0) {
|
|
58
|
-
const cacheKey = JSON.stringify({ goal, info: context.info || '' });
|
|
69
|
+
const cacheKey = JSON.stringify({ goal, info: context.info || '', count });
|
|
59
70
|
const cached = this._cache.get(cacheKey);
|
|
60
71
|
if (cached && Date.now() < cached.expiresAt) {
|
|
61
72
|
return cached.result;
|
|
62
73
|
}
|
|
63
74
|
}
|
|
64
75
|
|
|
76
|
+
const system = count
|
|
77
|
+
? `${this.prompt}\n\nOVERRIDE: ignore the "2-7 steps" guidance. Decompose into EXACTLY ${count} independent, parallelizable steps, each with "dependsOn": []. Split the goal into ${count} disjoint slices of comparable size that together cover it with no overlap.`
|
|
78
|
+
: this.prompt;
|
|
65
79
|
const messages = [
|
|
66
|
-
{ role: 'system', content:
|
|
80
|
+
{ role: 'system', content: system },
|
|
67
81
|
];
|
|
68
82
|
if (context.info) {
|
|
69
83
|
messages.push({ role: 'user', content: `Context: ${context.info}` });
|
|
@@ -75,10 +89,16 @@ class Planner {
|
|
|
75
89
|
temperature: 0,
|
|
76
90
|
});
|
|
77
91
|
|
|
92
|
+
// Budget visibility: forward the planning call's usage to the gate (mirror of Evaluator). Only on a real
|
|
93
|
+
// LLM call — a cache hit returned earlier without reaching here, so it never double-counts.
|
|
94
|
+
if (this.onLlmResult) {
|
|
95
|
+
await this.onLlmResult({ usage: result.usage || null, model: result.model || this.provider.model || null, kind: 'plan' });
|
|
96
|
+
}
|
|
97
|
+
|
|
78
98
|
const steps = this._parse(result.text);
|
|
79
99
|
|
|
80
100
|
if (this._cacheTTL > 0) {
|
|
81
|
-
const cacheKey = JSON.stringify({ goal, info: context.info || '' });
|
|
101
|
+
const cacheKey = JSON.stringify({ goal, info: context.info || '', count });
|
|
82
102
|
this._cache.set(cacheKey, { result: steps, expiresAt: Date.now() + this._cacheTTL });
|
|
83
103
|
}
|
|
84
104
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NB-5 — the decomposition-policy system blurb + few-shot. Prepended to a Family-A worker's system prompt so
|
|
3
|
+
* the model has an in-context example of HOW to split before it is offered the `spawn_child` A-tool. Flat-first
|
|
4
|
+
* (§4.2/§8): the model fans out flat over a window-sized batch and only escalates to a nested `spawn_child`
|
|
5
|
+
* when a single sub-task is genuinely too large to handle directly. Worked splits are deliberately small —
|
|
6
|
+
* the lift is from showing the SHAPE of a good split, not from volume.
|
|
7
|
+
* @type {string}
|
|
8
|
+
*/
|
|
9
|
+
export const DECOMPOSITION_POLICY: string;
|
|
10
|
+
/**
|
|
11
|
+
* NB-4 / RC-12 — the depth-aware capability-scrub suffix. Deeper workers get a more conservative prompt:
|
|
12
|
+
* "prefer direct action, only delegate if truly necessary." Combined with the inline `canSpawn` check (the
|
|
13
|
+
* tool half of the scrub: `depth < maxDepth` withholds `spawn_child` at the cap) in recurse.js, this realizes
|
|
14
|
+
* guard #5's prompt+tool-shaping half (the part bareguard's blind
|
|
15
|
+
* `policy` cap cannot express). At depth 0 there is no suffix (the top-level worker decomposes freely);
|
|
16
|
+
* from depth 1 on, each level nudges harder toward answering directly so recursion contracts toward its base
|
|
17
|
+
* case rather than fanning out without bound.
|
|
18
|
+
* @param {number} depth - The worker's depth (0 = top level).
|
|
19
|
+
* @param {number} maxDepth - The topology ceiling; at `depth >= maxDepth` the `spawn_child` tool is withheld.
|
|
20
|
+
* @returns {string} A suffix to append to the worker system prompt ('' at depth 0).
|
|
21
|
+
*/
|
|
22
|
+
export function capabilityScrub(depth: number, maxDepth: number): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// NB-5 + NB-4 prompt assets for src/recurse.js (RLM_PRD §4.3). Pure text, ZERO runtime — kept in their own
|
|
4
|
+
// file so the decomposition policy (which the RLM paper Fig 4 shows directly lifts accuracy and the
|
|
5
|
+
// first-split-correct rate) is inspectable and editable without touching the glue. Two assets:
|
|
6
|
+
// - DECOMPOSITION_POLICY (NB-5): the worker's system-prompt blurb + 1-2 worked splits.
|
|
7
|
+
// - capabilityScrub (NB-4 / RC-12): the depth-conservative suffix deeper workers receive.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* NB-5 — the decomposition-policy system blurb + few-shot. Prepended to a Family-A worker's system prompt so
|
|
11
|
+
* the model has an in-context example of HOW to split before it is offered the `spawn_child` A-tool. Flat-first
|
|
12
|
+
* (§4.2/§8): the model fans out flat over a window-sized batch and only escalates to a nested `spawn_child`
|
|
13
|
+
* when a single sub-task is genuinely too large to handle directly. Worked splits are deliberately small —
|
|
14
|
+
* the lift is from showing the SHAPE of a good split, not from volume.
|
|
15
|
+
* @type {string}
|
|
16
|
+
*/
|
|
17
|
+
const DECOMPOSITION_POLICY = [
|
|
18
|
+
'You solve a task by DECOMPOSING it, not by swallowing everything at once.',
|
|
19
|
+
'',
|
|
20
|
+
'How to decompose:',
|
|
21
|
+
'- Break the task into independent sub-tasks, each small enough to handle in one focused pass.',
|
|
22
|
+
'- Prefer a FLAT split (several sibling sub-tasks) over a deep one. Only nest — split a sub-task again —',
|
|
23
|
+
' when that single sub-task is itself too large to handle directly.',
|
|
24
|
+
'- When you have a sub-task that is large or independent, delegate it with the `spawn_child` tool: it runs',
|
|
25
|
+
' in a FRESH context window and returns only its result. Do the small/glue parts yourself.',
|
|
26
|
+
'- After the sub-results come back, COMBINE them into one final answer for the original task.',
|
|
27
|
+
'- If you can answer directly without splitting, just do so — decomposition is for tasks too big for one pass.',
|
|
28
|
+
'',
|
|
29
|
+
'Worked example 1 (flat split):',
|
|
30
|
+
' Task: "Summarize the security posture of services A, B, and C."',
|
|
31
|
+
' Good split: spawn_child("Summarize the security posture of service A"), same for B, same for C,',
|
|
32
|
+
' then combine the three summaries into one posture report.',
|
|
33
|
+
'',
|
|
34
|
+
'Worked example 2 (nest only on overflow):',
|
|
35
|
+
' Task: "Count matching records across a 10,000-line log."',
|
|
36
|
+
' The log is too large for one pass, so split it into chunks and spawn_child a count for each chunk;',
|
|
37
|
+
' if a chunk is STILL too large, that child splits again. Sum the per-chunk counts in your final answer.',
|
|
38
|
+
].join('\n');
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* NB-4 / RC-12 — the depth-aware capability-scrub suffix. Deeper workers get a more conservative prompt:
|
|
42
|
+
* "prefer direct action, only delegate if truly necessary." Combined with the inline `canSpawn` check (the
|
|
43
|
+
* tool half of the scrub: `depth < maxDepth` withholds `spawn_child` at the cap) in recurse.js, this realizes
|
|
44
|
+
* guard #5's prompt+tool-shaping half (the part bareguard's blind
|
|
45
|
+
* `policy` cap cannot express). At depth 0 there is no suffix (the top-level worker decomposes freely);
|
|
46
|
+
* from depth 1 on, each level nudges harder toward answering directly so recursion contracts toward its base
|
|
47
|
+
* case rather than fanning out without bound.
|
|
48
|
+
* @param {number} depth - The worker's depth (0 = top level).
|
|
49
|
+
* @param {number} maxDepth - The topology ceiling; at `depth >= maxDepth` the `spawn_child` tool is withheld.
|
|
50
|
+
* @returns {string} A suffix to append to the worker system prompt ('' at depth 0).
|
|
51
|
+
*/
|
|
52
|
+
function capabilityScrub(depth, maxDepth) {
|
|
53
|
+
if (depth <= 0) return '';
|
|
54
|
+
if (depth >= maxDepth) {
|
|
55
|
+
return [
|
|
56
|
+
'',
|
|
57
|
+
`DEPTH ${depth} of ${maxDepth} — this is the deepest level. You CANNOT delegate further; there is no`,
|
|
58
|
+
'spawn tool here. Answer this sub-task DIRECTLY and concisely from what you have. If the sub-task is',
|
|
59
|
+
'still too large to answer faithfully, say so explicitly rather than guessing — an honest "incomplete"',
|
|
60
|
+
'is correct; a fabricated answer is not.',
|
|
61
|
+
].join('\n');
|
|
62
|
+
}
|
|
63
|
+
return [
|
|
64
|
+
'',
|
|
65
|
+
`DEPTH ${depth} of ${maxDepth} — you are already inside a delegated sub-task. PREFER DIRECT ACTION:`,
|
|
66
|
+
'answer this sub-task yourself if you reasonably can. Only delegate further (spawn_child) when a part of',
|
|
67
|
+
'it is genuinely too large to handle in one pass. Keep the recursion shallow.',
|
|
68
|
+
].join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { DECOMPOSITION_POLICY, capabilityScrub };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
export type Slice = {
|
|
2
|
+
/**
|
|
3
|
+
* - Stable, word-like id (no whitespace/commas — it is echoed back by the judge).
|
|
4
|
+
*/
|
|
5
|
+
id: string;
|
|
6
|
+
/**
|
|
7
|
+
* - The item content shown to the judge.
|
|
8
|
+
*/
|
|
9
|
+
text: string;
|
|
10
|
+
};
|
|
11
|
+
export type Provider = import("../types").Provider;
|
|
12
|
+
export type ToolDef = import("../types").ToolDef;
|
|
13
|
+
/**
|
|
14
|
+
* SCAN — process every slice, LLM-judge each window, union matching ids across windows AND passes, CODE-count
|
|
15
|
+
* the union. The default reliability mechanism (§9.2.1): the only path that does not silently undercount.
|
|
16
|
+
* RC-9: a dead window is recorded in `missingSlices`, never folded into the count as a zero.
|
|
17
|
+
* @param {string} predicate - The task the slices are judged against.
|
|
18
|
+
* @param {Slice[]} corpus - The array slice-source (already validated/normalized by the caller).
|
|
19
|
+
* @param {object} opts
|
|
20
|
+
* @param {Provider} opts.provider
|
|
21
|
+
* @param {number} [opts.window]
|
|
22
|
+
* @param {number} [opts.passes]
|
|
23
|
+
* @param {object} [opts.ctx]
|
|
24
|
+
* @param {Function} [opts.onLlmResult]
|
|
25
|
+
* @param {Function} [opts.policy]
|
|
26
|
+
* @returns {Promise<{matchedIds: string[], count: number, missingSlices: string[], window: number, passes: number, scanned: number}>}
|
|
27
|
+
* @throws {HaltError} a governance cap halted a window judge.
|
|
28
|
+
*/
|
|
29
|
+
export function scanCount(predicate: string, corpus: Slice[], opts: {
|
|
30
|
+
provider: Provider;
|
|
31
|
+
window?: number | undefined;
|
|
32
|
+
passes?: number | undefined;
|
|
33
|
+
ctx?: object;
|
|
34
|
+
onLlmResult?: Function | undefined;
|
|
35
|
+
policy?: Function | undefined;
|
|
36
|
+
}): Promise<{
|
|
37
|
+
matchedIds: string[];
|
|
38
|
+
count: number;
|
|
39
|
+
missingSlices: string[];
|
|
40
|
+
window: number;
|
|
41
|
+
passes: number;
|
|
42
|
+
scanned: number;
|
|
43
|
+
}>;
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {object} Slice
|
|
46
|
+
* @property {string} id - Stable, word-like id (no whitespace/commas — it is echoed back by the judge).
|
|
47
|
+
* @property {string} text - The item content shown to the judge.
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* Judge ONE window: run an isolated Loop with the classify prompt over the window's items, and intersect the
|
|
51
|
+
* returned ids with the ids actually SHOWN this window (RC-2 — a window's judge can only "match" what it was
|
|
52
|
+
* given; a hallucinated id from another window is dropped). A governance HaltError propagates (the caller turns
|
|
53
|
+
* it into a clean incomplete); any other Loop fault marks the window DEAD (→ RC-9 missingSlices), never a
|
|
54
|
+
* silent zero that would undercount.
|
|
55
|
+
* @param {string} predicate
|
|
56
|
+
* @param {Slice[]} window
|
|
57
|
+
* @param {{provider: Provider, ctx?: object, onLlmResult?: Function, policy?: Function, nonce: number}} opts
|
|
58
|
+
* @returns {Promise<{ids: string[]|null, dead: boolean}>}
|
|
59
|
+
* @throws {HaltError}
|
|
60
|
+
*/
|
|
61
|
+
export function judgeWindow(predicate: string, window: Slice[], opts: {
|
|
62
|
+
provider: Provider;
|
|
63
|
+
ctx?: object;
|
|
64
|
+
onLlmResult?: Function;
|
|
65
|
+
policy?: Function;
|
|
66
|
+
nonce: number;
|
|
67
|
+
}): Promise<{
|
|
68
|
+
ids: string[] | null;
|
|
69
|
+
dead: boolean;
|
|
70
|
+
}>;
|
|
71
|
+
/**
|
|
72
|
+
* The §9.2-validated classify system prompt, GENERALIZED from the POC's hardcoded "SPORTS news" predicate to
|
|
73
|
+
* an arbitrary one. The load-bearing wording is verbatim ("Examine EACH item individually", "Output ONLY the
|
|
74
|
+
* IDs", "Comma-separated. If none, output 'none'. No count, no prose.") — the predicate is the only variable.
|
|
75
|
+
* It MUST return ids, not a count: counting is CODE's job (RC-5 / §9.1), so the judge never does arithmetic.
|
|
76
|
+
* @param {string} predicate - The task/goal the items are judged against.
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
export function classifySystem(predicate: string): string;
|
|
80
|
+
/**
|
|
81
|
+
* Does this text imply a completeness ("all / every / count / how many") ask? Used to upgrade a capped search
|
|
82
|
+
* to a scan (the only complete path). Conservative by design — a false positive only costs a thorough scan.
|
|
83
|
+
* @param {unknown} text
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
export function impliesCompleteness(text: unknown): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Normalize a raw slice-source into validated `{id, text}` slices (drops malformed entries — a slice with no
|
|
89
|
+
* string id/text cannot be judged or counted, so it is excluded rather than silently miscounted).
|
|
90
|
+
* @param {unknown} corpus
|
|
91
|
+
* @returns {Slice[]}
|
|
92
|
+
*/
|
|
93
|
+
export function normalizeCorpus(corpus: unknown): Slice[];
|
|
94
|
+
/**
|
|
95
|
+
* The `search` handle tool (RC-5 needle path): litectx `recall` — embeddings ON, `fact`/`episode` (the
|
|
96
|
+
* KNN-nominate kinds), capped at `KNN_K`. For FINDING the relevant few; the description tells the worker it
|
|
97
|
+
* CANNOT count (the completeness guard catches a "how many" ask before this tool is ever offered). Returns the
|
|
98
|
+
* matched items' bodies as text the worker reads; never the whole corpus.
|
|
99
|
+
* @param {{recall: Function}} litectx
|
|
100
|
+
* @param {{kinds?: string[], n?: number}} [opts]
|
|
101
|
+
* @returns {ToolDef}
|
|
102
|
+
*/
|
|
103
|
+
export function buildSearchTool(litectx: {
|
|
104
|
+
recall: Function;
|
|
105
|
+
}, opts?: {
|
|
106
|
+
kinds?: string[];
|
|
107
|
+
n?: number;
|
|
108
|
+
}): ToolDef;
|
|
109
|
+
/**
|
|
110
|
+
* The `exact` handle tool (RC-5 rule path): a deterministic, embeddings-free code-side predicate filter over
|
|
111
|
+
* the slice-source — returns every record whose text contains ALL given terms (case-insensitive AND). This is
|
|
112
|
+
* the "code-side predicate filter" half of §9.2.1; it is complete over the slices it is given (no recall cap)
|
|
113
|
+
* but only as good as a lexical rule. (FTS-AND over a litectx instance is the alternative, but needs embeddings
|
|
114
|
+
* OFF to stay exact — deferred; the code-side filter is the embeddings-free path shipped now.)
|
|
115
|
+
* @param {Slice[]} corpus - The validated slice-source.
|
|
116
|
+
* @returns {ToolDef}
|
|
117
|
+
*/
|
|
118
|
+
export function buildExactTool(corpus: Slice[]): ToolDef;
|
|
119
|
+
/**
|
|
120
|
+
* The `scan` handle tool (RC-5 COMPLETE path, the per-query Family-A face of §10 step 7) — the deterministic
|
|
121
|
+
* counterpart to `search`/`exact` as a TOOL a worker may call per sub-query. Where `search_memory` returns the
|
|
122
|
+
* top FEW (capped, cannot count) and `exact_match` is a lexical rule, `scan_count` runs the full §9.2.1 scan
|
|
123
|
+
* (`scanCount`) over EVERY record and returns an exact, CODE-counted total — the only tool that does not silently
|
|
124
|
+
* undercount. The completeness routing lives in the DESCRIPTIONS, not a code-guard: this tool says "use for how
|
|
125
|
+
* many / all / count"; `search_memory` says "never use to count" — so a worker picks the complete path per
|
|
126
|
+
* sub-query (the shape can differ per sub-query with no adopter declaration). RC-9 honesty is preserved at the
|
|
127
|
+
* tool boundary: a dead window surfaces as an explicit `INCOMPLETE — the count is a floor`, never a clean number
|
|
128
|
+
* over a hole. A governance `HaltError` from the inner scan PROPAGATES (the Loop turns it into a clean halt —
|
|
129
|
+
* never wrapped to a `ToolError`).
|
|
130
|
+
* @param {Slice[] | (() => Promise<Slice[]>)} corpus - The slice-source (array or async, like `litectxCorpus`);
|
|
131
|
+
* materialized lazily on first call and cached for the tool's lifetime.
|
|
132
|
+
* @param {{provider: Provider, window?: number, passes?: number, ctx?: object, onLlmResult?: Function, policy?: Function}} opts
|
|
133
|
+
* @returns {ToolDef}
|
|
134
|
+
*/
|
|
135
|
+
export function buildScanTool(corpus: Slice[] | (() => Promise<Slice[]>), opts: {
|
|
136
|
+
provider: Provider;
|
|
137
|
+
window?: number;
|
|
138
|
+
passes?: number;
|
|
139
|
+
ctx?: object;
|
|
140
|
+
onLlmResult?: Function;
|
|
141
|
+
policy?: Function;
|
|
142
|
+
}): ToolDef;
|
|
143
|
+
/**
|
|
144
|
+
* Build a SCAN slice-source backed by a litectx-RESIDENT corpus (facts/episodes the agent already accrued) —
|
|
145
|
+
* the §10-step-7 deferral un-blocked by litectx 0.26's `enumerate` verb (spec:
|
|
146
|
+
* docs/01-product/litectx-enumerate-spec.md). Returns the generic async slice-source recurse's scan reads: a
|
|
147
|
+
* `() => Promise<Slice[]>` that pages through EVERY row of `kind` via `enumerate` (exhaustive — the rank-free
|
|
148
|
+
* read `recall` structurally cannot do) and maps each to `{id: item.path, text: item.body}`. recurse stays
|
|
149
|
+
* litectx-agnostic: it depends on this source SHAPE, never on litectx (same stance as `remember`'s Store
|
|
150
|
+
* socket) — an adopter can hand any `() => Promise<Slice[]>` (a DB, a file, an API) instead.
|
|
151
|
+
*
|
|
152
|
+
* Only for a corpus ALREADY in litectx for its own reasons — never ingest a fresh corpus just to enumerate it
|
|
153
|
+
* back (strictly worse than scanning the in-hand array; spec §1.1).
|
|
154
|
+
* @param {{enumerate: Function}} litectx
|
|
155
|
+
* @param {{kind?: 'fact'|'episode', pageSize?: number}} [opts]
|
|
156
|
+
* @returns {() => Promise<Slice[]>}
|
|
157
|
+
*/
|
|
158
|
+
export function litectxCorpus(litectx: {
|
|
159
|
+
enumerate: Function;
|
|
160
|
+
}, opts?: {
|
|
161
|
+
kind?: "fact" | "episode";
|
|
162
|
+
pageSize?: number;
|
|
163
|
+
}): () => Promise<Slice[]>;
|
|
164
|
+
/**
|
|
165
|
+
* Deterministic rotation by `k` — the shuffled-boundary mechanism for multi-pass union WITHOUT an RNG (keeps
|
|
166
|
+
* RC-3 determinism: same corpus + same passes ⇒ identical scan). Rotating the array changes which items share a
|
|
167
|
+
* window (and each item's within-window position), so an item under-recalled at one window's tail in pass 0
|
|
168
|
+
* lands mid-window in pass 1 — the §9.2.1 mechanism that lifts recall ~0.85 → ~0.93.
|
|
169
|
+
* @template T @param {T[]} arr @param {number} k @returns {T[]}
|
|
170
|
+
*/
|
|
171
|
+
export function rotate<T>(arr: T[], k: number): T[];
|
|
172
|
+
export const SCAN_WINDOW: 8;
|
|
173
|
+
export const SCAN_PASSES: 2;
|
|
174
|
+
export const KNN_K: 8;
|