@hicaru/pi-rlm 0.3.16 → 0.3.18
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 +0 -4
- package/README.ru.md +56 -66
- package/README.zh-CN.md +61 -65
- package/package.json +5 -5
- package/src/bridge/add-context.ts +1 -1
- package/src/bridge/handlers/await.ts +13 -22
- package/src/bridge/handlers/completion.ts +27 -5
- package/src/bridge/handlers/emitting.ts +2 -2
- package/src/bridge/handlers/llm-query.ts +46 -68
- package/src/bridge/handlers/rlm-query.ts +14 -84
- package/src/bridge/handlers/task-registry.ts +22 -17
- package/src/bridge/handlers/types.ts +8 -6
- package/src/bridge/model.ts +6 -3
- package/src/commands/rlm-llm.ts +1 -10
- package/src/commands/rlm-rlm.ts +1 -8
- package/src/config/defaults.ts +32 -12
- package/src/config/settings.ts +53 -31
- package/src/config/skillstate.ts +465 -0
- package/src/context/md-cache.ts +1 -1
- package/src/context/merge.ts +1 -1
- package/src/context/namespace.ts +2 -2
- package/src/context/refresh.ts +1 -1
- package/src/context/source-dir.ts +21 -11
- package/src/context/source-doc.ts +1 -1
- package/src/context/source-git.ts +3 -15
- package/src/context/source-text.ts +1 -1
- package/src/context/walk.ts +6 -14
- package/src/core/budget.ts +107 -21
- package/src/core/compaction.ts +44 -1
- package/src/core/engine.ts +141 -84
- package/src/core/iteration.ts +1 -1
- package/src/core/ledger.ts +10 -13
- package/src/core/limits.ts +1 -1
- package/src/core/model-registry.ts +1 -1
- package/src/core/resource-limits.ts +1 -1
- package/src/core/root-context.ts +184 -0
- package/src/core/root-digest.ts +213 -0
- package/src/core/root-state.ts +310 -0
- package/src/core/run-state.ts +587 -0
- package/src/core/types.ts +51 -12
- package/src/index.ts +220 -39
- package/src/mode/llm-model.ts +13 -1
- package/src/mode/native-guards.ts +0 -6
- package/src/mode/rlm-mode.ts +34 -11
- package/src/mode/subagent.ts +5 -5
- package/src/prompts/glossary.ts +46 -25
- package/src/prompts/native.ts +27 -5
- package/src/prompts/system.ts +12 -4
- package/src/sandbox/context-file.ts +1 -1
- package/src/sandbox/interrupts.ts +25 -31
- package/src/sandbox/protocol.ts +14 -20
- package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/worker.cpython-314.pyc +0 -0
- package/src/sandbox/py/guards.py +1 -1
- package/src/sandbox/py/scaffold.py +24 -31
- package/src/sandbox/py/worker.py +3 -1
- package/src/sandbox/sandbox-manager.ts +2 -2
- package/src/sandbox/sandbox.ts +21 -4
- package/src/text/agent-text.ts +58 -0
- package/src/text/parsing.ts +35 -3
- package/src/text/preview.ts +3 -0
- package/src/text/repl-output.ts +1 -1
- package/src/tool/background-tasks.ts +1 -1
- package/src/tool/repl-render.ts +1 -1
- package/src/tool/repl-result.ts +1 -1
- package/src/tool/repl-tool.ts +50 -26
- package/src/tool/rlm-tool.ts +4 -5
- package/src/tool/subcall-render.ts +1 -1
- package/src/tool/subcall-store.ts +2 -2
- package/src/tool/tool-utils.ts +5 -5
- package/src/ui/config-panel.ts +12 -0
- package/src/ui/intro.ts +1 -1
- package/src/ui/modal/timeline-store.ts +1 -1
- package/src/ui/model-picker/drilldown.ts +1 -1
- package/src/ui/model-picker/levels.ts +1 -1
- package/src/ui/panel/run-registry.ts +1 -1
- package/src/ui/status.ts +39 -4
- package/src/ui/tree/tree-rows.ts +1 -1
- package/src/ui/tree/tree-widget.ts +1 -1
- package/src/util/bm25.ts +97 -0
- package/src/util/concurrency.ts +1 -1
- package/src/util/errors.ts +1 -1
- package/src/util/retry.ts +22 -7
- package/src/util/state-merge.ts +34 -0
- package/src/util/throttle.ts +1 -1
- package/src/util/type-guards.ts +6 -0
- package/src/core/memory.ts +0 -589
package/src/prompts/glossary.ts
CHANGED
|
@@ -27,7 +27,7 @@ export function promptCapTokensK(maxPromptChars: number): number {
|
|
|
27
27
|
* well, small ones guess keywords badly, and the first decomposition disproportionately decides
|
|
28
28
|
* the outcome (§5, Fig. 4a). These cost no tokens and no sub-calls.
|
|
29
29
|
*/
|
|
30
|
-
|
|
30
|
+
const RETRIEVAL_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
31
31
|
"- `search(query: str, k=10, path_glob=None)`: BM25 ranking over `context`. Returns",
|
|
32
32
|
" [{path, line, score, snippet, text}] — POINTERS, not bodies (`text` aliases `snippet`).",
|
|
33
33
|
" **Start here.** Free: no sub-LLM call. Use before guessing filenames.",
|
|
@@ -42,15 +42,49 @@ export const RETRIEVAL_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
|
42
42
|
/** v5 delegation doctrine (audit C5): children have NO retrieval tools — their world is the
|
|
43
43
|
* sliced `context` they were handed. This REPLACES the retrieval lines in child prompts so
|
|
44
44
|
* the prompt and the runtime sandbox agree (a child taught to `search` burns turns on NameError). */
|
|
45
|
-
|
|
45
|
+
const DELEGATION_SURFACE_LINES: readonly string[] = Object.freeze([
|
|
46
46
|
"- **No `search` / `grep_context` / `outline` / `add_context` in this REPL** (delegation",
|
|
47
47
|
" surface, v5 doctrine): your task arrived WITH its world in `context`. Explore it with",
|
|
48
48
|
" Python (list comprehensions, string matching, slicing) and delegate slices to sub-LLMs —",
|
|
49
49
|
" never re-ask the parent for retrieval.",
|
|
50
50
|
]);
|
|
51
51
|
|
|
52
|
+
/** Workstream E: the one new sandbox function (prime-agent rule — surface grows by one). */
|
|
53
|
+
const SKILL_SEARCH_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
54
|
+
"- `skill_search(query, k=8) -> [{id, text, tags, score}]`: BM25 over distilled project facts",
|
|
55
|
+
" from PRIOR sessions (SkillState). Free: no sub-LLM call. Use when a config/gotcha/symbol",
|
|
56
|
+
" smells like something already learned — do not re-discover it.",
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
/** Single source of wording for the injected SkillState block (headless + native, Workstream C).
|
|
60
|
+
* Takes the dynamic body as an argument — the glossary itself stays static-only. */
|
|
61
|
+
/** One wording source for the skill_search recall hint (Ξ block + root Σ snapshot). */
|
|
62
|
+
export const SKILL_RECALL_LINE =
|
|
63
|
+
"Recall more anytime inside repl: `skill_search(query, k=8)` → [{id, text, tags, score}].";
|
|
64
|
+
|
|
65
|
+
/** R5 (G4, /tmp/ROOT_FULL_SKILLSTATE_PLAN.md): the one-line replacement for assistant prose
|
|
66
|
+
* older than the keep window — durable facts live in Σ, the full text in the session log. */
|
|
67
|
+
export const ROOT_TURN_ELIDED_LINE =
|
|
68
|
+
"… turn elided — durable facts live in Σ; full text in session log";
|
|
69
|
+
|
|
70
|
+
export function skillStateLines(noteCount: number, body: string): string {
|
|
71
|
+
return [
|
|
72
|
+
`[Project facts — SkillState, ${noteCount} note${noteCount === 1 ? "" : "s"}, distilled from prior sessions]`,
|
|
73
|
+
body,
|
|
74
|
+
SKILL_RECALL_LINE,
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Root Σ WS-2 digest wording — the single source for the header and section labels. */
|
|
79
|
+
export const ROOT_DIGEST_HEADER =
|
|
80
|
+
"[Root digest — deterministic structural compaction (no model call). Older turns are " +
|
|
81
|
+
"superseded by this digest plus the verbatim tail that follows; fresh tool results " +
|
|
82
|
+
"outrank the digest when they disagree.]";
|
|
83
|
+
export const ROOT_DIGEST_SECTIONS: Readonly<Record<"task" | "findings" | "state" | "next" | "facts", string>> =
|
|
84
|
+
Object.freeze({ task: "Task", findings: "Findings", state: "State", next: "Next", facts: "Project facts" });
|
|
85
|
+
|
|
52
86
|
/** One-line delegation helpers — orchestrating must be cheaper than solving. */
|
|
53
|
-
|
|
87
|
+
const DELEGATION_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
54
88
|
"- `map_files(files, prompt) -> Task`: always spawn. `await_task(t)` → dict[path, answer].",
|
|
55
89
|
" Accepts context entries or paths; packs into cap-sized batches; splits oversized files.",
|
|
56
90
|
" **Default way to read many files** — fire independent `map_files` Tasks, free work, then await.",
|
|
@@ -59,14 +93,14 @@ export const DELEGATION_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
|
59
93
|
]);
|
|
60
94
|
|
|
61
95
|
/** Shared glossary entry for the chunked-query helper (headless + native). */
|
|
62
|
-
|
|
96
|
+
const CHUNKED_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
63
97
|
"- `llm_query_chunked(text: str, prompt: str) -> Task`: always spawn. `await_task(t)` → list[str]",
|
|
64
98
|
" (one answer per chunk, order preserved). Auto-splits text to the sub-LLM prompt cap.",
|
|
65
99
|
" Use for ANY text too large for a single `llm_query` — open()ed files, oversized sub-results.",
|
|
66
100
|
]);
|
|
67
101
|
|
|
68
102
|
/** Non-blocking fan-out: spawn now, collect later (headless glossary). */
|
|
69
|
-
|
|
103
|
+
const SPAWN_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
70
104
|
"- **ALWAYS SPAWN (Task + ↗bg):** `llm_query` / `llm_batch` / `rlm_query` / `rlm_batch` /",
|
|
71
105
|
" `map_files` / `llm_query_chunked`. Never treat the return as the answer.",
|
|
72
106
|
" Collect with `await_task(t)`, `await_task([t1,t2,…])`, or `await_task()` (every still-running Task).",
|
|
@@ -81,7 +115,7 @@ export const SPAWN_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
|
81
115
|
]);
|
|
82
116
|
|
|
83
117
|
/** v5 (audit C5): the spawn worked example, retrieval flavor — root surface only. */
|
|
84
|
-
|
|
118
|
+
const SPAWN_EXAMPLE_RETRIEVAL: readonly string[] = Object.freeze([
|
|
85
119
|
"",
|
|
86
120
|
" ```python",
|
|
87
121
|
" # Multi-area study: one rlm_batch (parallel workers), free locate, then await",
|
|
@@ -96,7 +130,7 @@ export const SPAWN_EXAMPLE_RETRIEVAL: readonly string[] = Object.freeze([
|
|
|
96
130
|
]);
|
|
97
131
|
|
|
98
132
|
/** v5 (audit C5): the spawn worked example, delegation flavor — no retrieval, slice instead. */
|
|
99
|
-
|
|
133
|
+
const SPAWN_EXAMPLE_DELEGATION: readonly string[] = Object.freeze([
|
|
100
134
|
"",
|
|
101
135
|
" ```python",
|
|
102
136
|
" # Multi-area study: one rlm_batch (parallel workers), slice your world while they run",
|
|
@@ -109,7 +143,7 @@ export const SPAWN_EXAMPLE_DELEGATION: readonly string[] = Object.freeze([
|
|
|
109
143
|
" # One-shot extracts: map_files / llm_batch also return Task → await_task",
|
|
110
144
|
" ```",
|
|
111
145
|
]);
|
|
112
|
-
|
|
146
|
+
const RECURSION_CONTEXT_LINES: readonly string[] = Object.freeze([
|
|
113
147
|
"",
|
|
114
148
|
" **What a child sees:** it inherits YOUR `context` — every file you have loaded, including",
|
|
115
149
|
" sources under `ctx/<id>/…` — and runs `search` / `grep_context` / `outline` / `map_files`",
|
|
@@ -125,7 +159,7 @@ export const RECURSION_CONTEXT_LINES: readonly string[] = Object.freeze([
|
|
|
125
159
|
|
|
126
160
|
/** v5 recursion section, delegation variant (audit C5): describes what a delegation child
|
|
127
161
|
* receives — the narrowed pack as text, no retrieval of its own. */
|
|
128
|
-
|
|
162
|
+
const RECURSION_DELEGATION_LINES: readonly string[] = Object.freeze([
|
|
129
163
|
"",
|
|
130
164
|
" **What a child sees:** it inherits YOUR `context` (narrowed by `paths=` when given) and works",
|
|
131
165
|
" on it as text — it has NO retrieval tools, so put what matters in your prompt and `paths`,",
|
|
@@ -140,14 +174,14 @@ export const RECURSION_DELEGATION_LINES: readonly string[] = Object.freeze([
|
|
|
140
174
|
* Sub-RLM orientation. Emitted only at depth > 0, where `context` is the parent's world rather
|
|
141
175
|
* than a repository the run packed for itself.
|
|
142
176
|
*/
|
|
143
|
-
|
|
177
|
+
const CHILD_CONTEXT_LINES: readonly string[] = Object.freeze([
|
|
144
178
|
" You are a sub-RLM. This `context` is your parent's world — every file it has loaded (cwd",
|
|
145
179
|
" paths un-prefixed; external sources under `ctx/<id>/…`). Answer only the question above;",
|
|
146
180
|
" your REPL and anything you load die with you, and only your final answer string returns.",
|
|
147
181
|
]);
|
|
148
182
|
|
|
149
183
|
/** Why a file the user mentioned may be missing from `context`. */
|
|
150
|
-
|
|
184
|
+
const CONTEXT_EXCLUSION_NOTE = [
|
|
151
185
|
" NOTE: `context` holds only the files you have loaded (starts empty; cwd seeds on first use).",
|
|
152
186
|
" Gitignored files and files larger than 1MB of plain text are skipped. Binary documents",
|
|
153
187
|
" (PDF, DOCX, XLSX, PPTX, CSV, …) ARE included — converted to Markdown on the way in.",
|
|
@@ -231,9 +265,6 @@ export function envTips(delegation = false): string {
|
|
|
231
265
|
].join("\n");
|
|
232
266
|
}
|
|
233
267
|
|
|
234
|
-
/** Root-surface doctrine (back-compat alias of `envTips(false)`). */
|
|
235
|
-
export const ENV_TIPS = envTips(false);
|
|
236
|
-
|
|
237
268
|
/** Native-mode variant of the doctrine — same rules, sized for the native prompt budget. */
|
|
238
269
|
export const ENV_TIPS_CONDENSED = [
|
|
239
270
|
"### Decomposition doctrine",
|
|
@@ -314,6 +345,7 @@ export function replGlossary(
|
|
|
314
345
|
" await_task → ordered list[str]. NEVER pass bare file paths as if the worker can open them.",
|
|
315
346
|
...CHUNKED_GLOSSARY_LINES,
|
|
316
347
|
...SPAWN_GLOSSARY_LINES,
|
|
348
|
+
...SKILL_SEARCH_GLOSSARY_LINES,
|
|
317
349
|
...(delegation ? SPAWN_EXAMPLE_DELEGATION : SPAWN_EXAMPLE_RETRIEVAL),
|
|
318
350
|
...DELEGATION_GLOSSARY_LINES,
|
|
319
351
|
);
|
|
@@ -357,17 +389,6 @@ export function replGlossary(
|
|
|
357
389
|
" verified result in `answers` — see the decomposition doctrine below.",
|
|
358
390
|
"- `SHOW_VARS() -> str`: list every variable currently in the REPL (Task handles show as `<Task …>`).",
|
|
359
391
|
"- `list_tasks()`: every Task this REPL created — [{kind, label, done, var}].",
|
|
360
|
-
...(delegation
|
|
361
|
-
? [
|
|
362
|
-
"- `memory.query(q) -> str`: durable notes under `.rlm/` that survive across sessions.",
|
|
363
|
-
" **READ-ONLY here** — `memory.add` is root-only. Query before re-studying a known area;",
|
|
364
|
-
" your own final answer is recorded as an episode automatically.",
|
|
365
|
-
]
|
|
366
|
-
: [
|
|
367
|
-
"- `memory.query(q) -> str` / `memory.add(text, paths=…, tags=…)`: durable notes under `.rlm/`",
|
|
368
|
-
" that survive across sessions. Query before re-studying a known area; add concise findings",
|
|
369
|
-
" (facts, locations, decisions) — never secrets or API keys (notes persist on disk).",
|
|
370
|
-
]),
|
|
371
392
|
"- `list_claims()`: the live `[ledger]` table of inflight/done agent work.",
|
|
372
393
|
'- `answer`: a dict initialized to {"content": "", "ready": False}. To submit your final answer,',
|
|
373
394
|
' set `answer["content"]` to the answer text and `answer["ready"] = True`.',
|
package/src/prompts/native.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
DEFAULT_PROMPT_CAP,
|
|
13
13
|
promptCapTokensK,
|
|
14
14
|
} from "./glossary.ts";
|
|
15
|
+
import { STATE_FENCE_INSTRUCTION } from "../core/run-state.ts";
|
|
15
16
|
|
|
16
17
|
/** Adapts the REPL glossary for native mode — agent calls `repl({code})` instead of writing ```repl``` blocks. */
|
|
17
18
|
function nativeReplGlossary(): string {
|
|
@@ -45,9 +46,7 @@ function nativeReplGlossary(): string {
|
|
|
45
46
|
"### Memo / finalize",
|
|
46
47
|
"- `answers` / `plan` — persistent dicts for **collected** results. Task handles are REPL vars (`t`), not `answers` keys.",
|
|
47
48
|
"- `add_context(source) -> dict` — append external dir/file/doc/git under `ctx/<id>/…` (metadata only).",
|
|
48
|
-
"- `
|
|
49
|
-
" sessions. Query before re-studying a known area; add concise findings — never secrets or API",
|
|
50
|
-
" keys (notes persist on disk). `list_claims()` — the live `[ledger]` table of agent work.",
|
|
49
|
+
"- `list_claims()` — the live `[ledger]` table of agent work.",
|
|
51
50
|
"- `SHOW_VARS()` — list REPL vars (Tasks as `<Task …>`). `list_tasks()` finds Task handles. `answer[\"ready\"]=True` only for headless finalize (native: write a normal message).",
|
|
52
51
|
"",
|
|
53
52
|
ENV_TIPS_CONDENSED,
|
|
@@ -69,8 +68,15 @@ function nativeReplGlossary(): string {
|
|
|
69
68
|
].join("\n");
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
/** Build the native-mode system prompt for the main Pi agent.
|
|
73
|
-
|
|
71
|
+
/** Build the native-mode system prompt for the main Pi agent.
|
|
72
|
+
*
|
|
73
|
+
* R1 (G1, /tmp/ROOT_FULL_SKILLSTATE_PLAN.md): `stateFences` appends the ONE Σ fence contract
|
|
74
|
+
* (STATE_FENCE_INSTRUCTION verbatim — one wording source, engine and native share it) so the
|
|
75
|
+
* native model can author ΔΣ_t through ```state fences. The fence text is STATIC, so it may
|
|
76
|
+
* ride call-time composition; the FLAG decision happens at the call site — NATIVE_PROMPT_STATIC
|
|
77
|
+
* (the frozen module-load snapshot) is composed with no options and stays contract-free.
|
|
78
|
+
*/
|
|
79
|
+
export function buildNativeSystemPrompt(opts?: { readonly stateFences?: boolean }): string {
|
|
74
80
|
return [
|
|
75
81
|
"╔══════════════════════════════════════════════════════════════════╗",
|
|
76
82
|
"║ NATIVE RLM MODE — YOU ARE AN ORCHESTRATOR, NOT A READER ║",
|
|
@@ -164,6 +170,22 @@ export function buildNativeSystemPrompt(): string {
|
|
|
164
170
|
"</rules>",
|
|
165
171
|
"",
|
|
166
172
|
nativeReplGlossary(),
|
|
173
|
+
...(opts?.stateFences === true
|
|
174
|
+
? [
|
|
175
|
+
"",
|
|
176
|
+
STATE_FENCE_INSTRUCTION,
|
|
177
|
+
// Soak-B finding (R3): models ignore the contract when it only speaks headless
|
|
178
|
+
// \u201c```repl block(s)\u201d \u2014 in native mode those are repl({code}) TOOL calls. The engine
|
|
179
|
+
// wording above stays byte-identical (one source); this line maps it 1:1 onto the
|
|
180
|
+
// native tool-call loop so the fence obligation is unambiguous.
|
|
181
|
+
"NATIVE MODE: you emit repl({code}) as TOOL calls, not ```repl text blocks \u2014 the " +
|
|
182
|
+
"contract above maps 1:1 onto this loop. In ANY reply where you learned something " +
|
|
183
|
+
"durable (a path, a fact, a failed approach, the next step), ALSO emit a ```state " +
|
|
184
|
+
'fenced block in that same reply: {"state_patch": {"verifiedFacts[+]": ' +
|
|
185
|
+
'"src/x.ts \u2014 what you just verified"}}. Deltas only; one small patch per turn; ' +
|
|
186
|
+
"never restate unchanged keys.",
|
|
187
|
+
]
|
|
188
|
+
: []),
|
|
167
189
|
].join("\n");
|
|
168
190
|
}
|
|
169
191
|
|
package/src/prompts/system.ts
CHANGED
|
@@ -17,14 +17,17 @@ import {
|
|
|
17
17
|
|
|
18
18
|
export { contextKindOf, type ContextKind } from "./glossary.ts";
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
interface PromptMeta {
|
|
21
21
|
readonly contextType: string;
|
|
22
22
|
readonly contextChars: number;
|
|
23
23
|
readonly contextStats?: ContextSizeStats;
|
|
24
24
|
readonly rootPrompt?: string;
|
|
25
|
+
/** SKILL.state Ξ (Workstream C): verified project facts from prior sessions. Per-run
|
|
26
|
+
* argument only — never baked into any module-load snapshot. */
|
|
27
|
+
readonly skillBlock?: string;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
interface SystemPromptOptions {
|
|
28
31
|
readonly orchestrator?: boolean;
|
|
29
32
|
readonly recursion?: boolean;
|
|
30
33
|
readonly maxPromptChars?: number;
|
|
@@ -88,7 +91,7 @@ export function buildRlmSystemPrompt(meta: PromptMeta, opts: SystemPromptOptions
|
|
|
88
91
|
parts.push(
|
|
89
92
|
"",
|
|
90
93
|
"**REPL API (ONLY these):** llm_query / llm_batch / llm_query_chunked / map_files /",
|
|
91
|
-
"llm_map_reduce / rlm_query / rlm_batch / spawn / await_task / list_tasks /
|
|
94
|
+
"llm_map_reduce / rlm_query / rlm_batch / spawn / await_task / list_tasks /",
|
|
92
95
|
"list_claims. There is no search/grep_context/outline here — your task arrived WITH its",
|
|
93
96
|
"world in `context`; slice it into llm prompts. rlm_query only for a disjoint path set.",
|
|
94
97
|
);
|
|
@@ -110,12 +113,17 @@ export function buildRlmSystemPrompt(meta: PromptMeta, opts: SystemPromptOptions
|
|
|
110
113
|
);
|
|
111
114
|
if (opts.orchestrator ?? true) {
|
|
112
115
|
// Two counterweights, both required (paper App. B): the addendum bounds OVER-recursion
|
|
113
|
-
// (batching/cost),
|
|
116
|
+
// (batching/cost), envTips() bounds UNDER-recursion (solving it yourself).
|
|
114
117
|
parts.push("", orchestratorAddendum(maxPromptChars, opts.delegation ?? false), "", envTips(opts.delegation ?? false));
|
|
115
118
|
}
|
|
116
119
|
if (kind === "files") {
|
|
117
120
|
parts.push("", LARGE_FILE_RULE_LINES.join("\n"));
|
|
118
121
|
}
|
|
122
|
+
// Ξ (Workstream C): lands BEFORE the metadata line so the task header stays last — the model
|
|
123
|
+
// reads it freshest. Verified-fact grounding precedes the fresh task statement.
|
|
124
|
+
if (meta.skillBlock !== undefined && meta.skillBlock !== "") {
|
|
125
|
+
parts.push("", meta.skillBlock);
|
|
126
|
+
}
|
|
119
127
|
parts.push("", buildMetadataLine(meta, maxPromptChars));
|
|
120
128
|
return parts.join("\n");
|
|
121
129
|
}
|
|
@@ -23,7 +23,7 @@ import { join } from "node:path";
|
|
|
23
23
|
const SERIALIZE_CHUNK = 64;
|
|
24
24
|
|
|
25
25
|
/** A temp file on disk holding a serialized context payload. */
|
|
26
|
-
|
|
26
|
+
interface ContextTempFile {
|
|
27
27
|
readonly path: string;
|
|
28
28
|
/** True when the file holds JSON; false when the payload was a raw string. */
|
|
29
29
|
readonly json: boolean;
|
|
@@ -53,12 +53,8 @@ export interface SubLlmHandlers {
|
|
|
53
53
|
addContext(source: string, depth: number): Promise<AddContextResult>;
|
|
54
54
|
/** v5: the `[ledger]` claims table for the sandbox's `list_claims()` REPL call. */
|
|
55
55
|
ledgerClaims(): Promise<string>;
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
op: "query" | "add" | "stats",
|
|
59
|
-
args: { readonly query?: string; readonly k?: number; readonly content?: string; readonly paths?: readonly string[]; readonly tags?: readonly string[] },
|
|
60
|
-
depth: number,
|
|
61
|
-
): Promise<string>;
|
|
56
|
+
/** SKILL.state (Workstream E): BM25 over the session SkillState store; JSON hits reply. */
|
|
57
|
+
skillSearch(query: string, k: number, depth: number): Promise<unknown>;
|
|
62
58
|
}
|
|
63
59
|
|
|
64
60
|
function toStringArray(value: unknown): readonly string[] | undefined {
|
|
@@ -89,23 +85,23 @@ export const REJECT: SubLlmHandlers = Object.freeze({
|
|
|
89
85
|
throw new Error("add_context not configured");
|
|
90
86
|
},
|
|
91
87
|
ledgerClaims: async () => UNCONFIGURED,
|
|
92
|
-
|
|
88
|
+
skillSearch: async () => UNCONFIGURED,
|
|
93
89
|
});
|
|
94
90
|
|
|
95
91
|
export interface ReplyBody {
|
|
96
|
-
response?: string;
|
|
97
|
-
responses?: string[];
|
|
98
|
-
path?: string;
|
|
99
|
-
json?: boolean;
|
|
100
|
-
files?: number;
|
|
101
|
-
chars?: number;
|
|
102
|
-
source_id?: string;
|
|
103
|
-
path_prefix?: string;
|
|
104
|
-
already_loaded?: boolean;
|
|
105
|
-
documents?: number;
|
|
106
|
-
converted?: number;
|
|
107
|
-
skipped?: readonly { readonly path: string; readonly reason: string }[];
|
|
108
|
-
error?: string;
|
|
92
|
+
readonly response?: string;
|
|
93
|
+
readonly responses?: readonly string[];
|
|
94
|
+
readonly path?: string;
|
|
95
|
+
readonly json?: boolean;
|
|
96
|
+
readonly files?: number;
|
|
97
|
+
readonly chars?: number;
|
|
98
|
+
readonly source_id?: string;
|
|
99
|
+
readonly path_prefix?: string;
|
|
100
|
+
readonly already_loaded?: boolean;
|
|
101
|
+
readonly documents?: number;
|
|
102
|
+
readonly converted?: number;
|
|
103
|
+
readonly skipped?: readonly { readonly path: string; readonly reason: string }[];
|
|
104
|
+
readonly error?: string;
|
|
109
105
|
}
|
|
110
106
|
|
|
111
107
|
const RLM_PATH_TYPES = new Set(["rlm_query", "rlm_batch"]);
|
|
@@ -171,10 +167,10 @@ async function resolveSingle(
|
|
|
171
167
|
}
|
|
172
168
|
return { response: collected.result ?? "" };
|
|
173
169
|
}
|
|
174
|
-
return { response:
|
|
170
|
+
return { response: typeof collected === "string" ? collected : "" };
|
|
175
171
|
}
|
|
176
172
|
// Unexpected shape — surface as text rather than crash the worker.
|
|
177
|
-
return { response:
|
|
173
|
+
return { response: typeof raw === "string" ? raw : "" };
|
|
178
174
|
}
|
|
179
175
|
|
|
180
176
|
/**
|
|
@@ -304,7 +300,7 @@ export async function serviceInterrupt(
|
|
|
304
300
|
});
|
|
305
301
|
return;
|
|
306
302
|
}
|
|
307
|
-
reply(msg.rid, { response:
|
|
303
|
+
reply(msg.rid, { response: typeof result === "string" ? result : "" });
|
|
308
304
|
return;
|
|
309
305
|
}
|
|
310
306
|
case "finish": {
|
|
@@ -352,13 +348,11 @@ export async function serviceInterrupt(
|
|
|
352
348
|
reply(msg.rid, { response: table });
|
|
353
349
|
return;
|
|
354
350
|
}
|
|
355
|
-
case "
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
);
|
|
361
|
-
reply(msg.rid, { response: out });
|
|
351
|
+
case "skill_search": {
|
|
352
|
+
// SKILL.state (Workstream E): the store serializes its own hits; plain strings
|
|
353
|
+
// (tests/stubs) pass through. Errors are replied, never thrown.
|
|
354
|
+
const raw = await h.skillSearch(msg.query ?? "", msg.k ?? 8, d);
|
|
355
|
+
reply(msg.rid, { response: typeof raw === "string" ? raw : JSON.stringify(raw) });
|
|
362
356
|
return;
|
|
363
357
|
}
|
|
364
358
|
default: {
|
|
@@ -369,6 +363,6 @@ export async function serviceInterrupt(
|
|
|
369
363
|
}
|
|
370
364
|
}
|
|
371
365
|
} catch (err: unknown) {
|
|
372
|
-
reply(msg.rid, { error: errorMessage(err) });
|
|
366
|
+
reply(msg.rid, { error: formatError(errorMessage(err)) });
|
|
373
367
|
}
|
|
374
368
|
}
|
package/src/sandbox/protocol.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type WorkerRequest =
|
|
|
21
21
|
| { readonly id: string; readonly type: "shutdown" };
|
|
22
22
|
|
|
23
23
|
/** Reply the parent sends to satisfy a sub-LLM interrupt. */
|
|
24
|
-
|
|
24
|
+
interface LlmReply {
|
|
25
25
|
readonly type: "llm_reply";
|
|
26
26
|
readonly rid: string;
|
|
27
27
|
readonly response?: string;
|
|
@@ -45,7 +45,7 @@ export interface LlmReply {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/** Keep-alive while the host is working and has nothing else to write. */
|
|
48
|
-
|
|
48
|
+
interface Heartbeat {
|
|
49
49
|
readonly type: "heartbeat";
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -71,8 +71,8 @@ export interface WorkerResponse {
|
|
|
71
71
|
readonly index?: number;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/** Canonical interrupt kinds (api_v5 + v5 ledger +
|
|
75
|
-
|
|
74
|
+
/** Canonical interrupt kinds (api_v5 + v5 ledger + SKILL.state). */
|
|
75
|
+
type InterruptKind =
|
|
76
76
|
| "llm_query"
|
|
77
77
|
| "rlm_query"
|
|
78
78
|
| "llm_batch"
|
|
@@ -81,7 +81,7 @@ export type InterruptKind =
|
|
|
81
81
|
| "finish"
|
|
82
82
|
| "add_context"
|
|
83
83
|
| "ledger_claims"
|
|
84
|
-
| "
|
|
84
|
+
| "skill_search";
|
|
85
85
|
|
|
86
86
|
interface InterruptBase {
|
|
87
87
|
readonly rid: string;
|
|
@@ -118,25 +118,21 @@ interface FinishInterrupt extends InterruptBase {
|
|
|
118
118
|
readonly summary?: string;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
interface AddContextInterrupt extends InterruptBase {
|
|
122
122
|
readonly type: "add_context";
|
|
123
123
|
readonly source?: string;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/** v5: sandbox asks the host for the TaskLedger claims table (`list_claims()`). */
|
|
127
|
-
|
|
127
|
+
interface LedgerClaimsInterrupt extends InterruptBase {
|
|
128
128
|
readonly type: "ledger_claims";
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
readonly type: "
|
|
134
|
-
readonly op: "query" | "add" | "stats";
|
|
131
|
+
/** SKILL.state (Workstream E): BM25 recall over the session SkillState store. */
|
|
132
|
+
interface SkillSearchInterrupt extends InterruptBase {
|
|
133
|
+
readonly type: "skill_search";
|
|
135
134
|
readonly query?: string;
|
|
136
135
|
readonly k?: number;
|
|
137
|
-
readonly content?: string;
|
|
138
|
-
readonly paths?: readonly string[];
|
|
139
|
-
readonly tags?: readonly string[];
|
|
140
136
|
}
|
|
141
137
|
|
|
142
138
|
/** A mid-exec sub-LLM/tool request from the worker. */
|
|
@@ -147,11 +143,11 @@ export type WorkerInterrupt =
|
|
|
147
143
|
| FinishInterrupt
|
|
148
144
|
| AddContextInterrupt
|
|
149
145
|
| LedgerClaimsInterrupt
|
|
150
|
-
|
|
|
146
|
+
| SkillSearchInterrupt;
|
|
151
147
|
|
|
152
148
|
export type WorkerMessage = WorkerResponse | WorkerInterrupt;
|
|
153
149
|
|
|
154
|
-
|
|
150
|
+
const INTERRUPT_KINDS = Object.freeze(
|
|
155
151
|
new Set<InterruptKind>([
|
|
156
152
|
"llm_query",
|
|
157
153
|
"rlm_query",
|
|
@@ -161,13 +157,11 @@ export const INTERRUPT_KINDS = Object.freeze(
|
|
|
161
157
|
"finish",
|
|
162
158
|
"add_context",
|
|
163
159
|
"ledger_claims",
|
|
164
|
-
"
|
|
160
|
+
"skill_search",
|
|
165
161
|
]),
|
|
166
162
|
);
|
|
167
163
|
|
|
168
|
-
|
|
169
|
-
return typeof value === "object" && value !== null;
|
|
170
|
-
}
|
|
164
|
+
import { isRecord } from "../util/type-guards.ts";
|
|
171
165
|
|
|
172
166
|
function isWorkerResponse(value: unknown): value is WorkerResponse {
|
|
173
167
|
return isRecord(value) && typeof value.id === "string" && typeof value.ok === "boolean";
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/sandbox/py/guards.py
CHANGED
|
@@ -96,7 +96,7 @@ RESERVED = frozenset(
|
|
|
96
96
|
# Helpers (not the old *_query_batched API)
|
|
97
97
|
"llm_query_chunked", "map_files", "llm_map_reduce",
|
|
98
98
|
"search", "grep_context", "outline",
|
|
99
|
-
"add_context", "list_claims", "
|
|
99
|
+
"add_context", "list_claims", "skill_search",
|
|
100
100
|
"SHOW_VARS", "answer", "context",
|
|
101
101
|
}
|
|
102
102
|
)
|
|
@@ -10,6 +10,7 @@ without any packaging step (same mechanism as guards.py / retrieval.py / tasks.p
|
|
|
10
10
|
"""
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import json
|
|
13
14
|
import os
|
|
14
15
|
from typing import Any
|
|
15
16
|
|
|
@@ -37,28 +38,6 @@ from tasks import (
|
|
|
37
38
|
)
|
|
38
39
|
|
|
39
40
|
|
|
40
|
-
class _MemoryApi:
|
|
41
|
-
"""v5 durable memory surface — thin shims over the `memory` interrupt.
|
|
42
|
-
|
|
43
|
-
memory.query(q, k=8) → retrieved notes; memory.add(text, paths=…, tags=…) → status;
|
|
44
|
-
memory.stats() → store counters. All calls block on the host RPC like add_context does.
|
|
45
|
-
"""
|
|
46
|
-
|
|
47
|
-
def __init__(self, worker: "WorkerScaffold"):
|
|
48
|
-
self._w = worker
|
|
49
|
-
|
|
50
|
-
def query(self, q, k: int = 8) -> str:
|
|
51
|
-
return self._w._memory_rpc("query", {"query": str(q), "k": int(k)})
|
|
52
|
-
|
|
53
|
-
def add(self, text, paths=None, tags=None) -> str:
|
|
54
|
-
p = [str(x) for x in (paths or [])]
|
|
55
|
-
t = [str(x) for x in (tags or [])]
|
|
56
|
-
return self._w._memory_rpc("add", {"content": str(text), "paths": p, "tags": t})
|
|
57
|
-
|
|
58
|
-
def stats(self) -> str:
|
|
59
|
-
return self._w._memory_rpc("stats", {})
|
|
60
|
-
|
|
61
|
-
|
|
62
41
|
class WorkerScaffold:
|
|
63
42
|
"""Mixin: the model-facing REPL API. Requires the host methods from Worker
|
|
64
43
|
(`_post`, `_rpc`, `_drain_until`, `_take`, `self.inbox`, `self.ns`, `self._handles`)."""
|
|
@@ -596,6 +575,29 @@ class WorkerScaffold:
|
|
|
596
575
|
)
|
|
597
576
|
return self._start_rlm_batch(list(p), paths)
|
|
598
577
|
|
|
578
|
+
def _skill_search(self, query: str, k: int = 8) -> list[dict[str, Any]]:
|
|
579
|
+
"""SkillState recall: BM25 over distilled facts from PRIOR sessions (host-side store).
|
|
580
|
+
|
|
581
|
+
Free: no sub-LLM call. Returns [{id, text, tags, score}] — "symbol"/"config" notes
|
|
582
|
+
about this exact project learned in earlier runs. Errors surface as a text hit, never
|
|
583
|
+
a raise: a missing store must not crash a cell.
|
|
584
|
+
"""
|
|
585
|
+
if not isinstance(query, str) or not query.strip():
|
|
586
|
+
raise TypeError("skill_search() needs a non-empty query string")
|
|
587
|
+
r = self._rpc("skill_search", {"query": query, "k": max(1, min(32, int(k)))})
|
|
588
|
+
if r.get("error"):
|
|
589
|
+
return [{"id": "", "text": f"Error: {r['error']}", "tags": [], "score": 0.0}]
|
|
590
|
+
raw = r.get("response")
|
|
591
|
+
if isinstance(raw, str) and raw:
|
|
592
|
+
if raw.startswith("Error:"):
|
|
593
|
+
return [{"id": "", "text": raw, "tags": [], "score": 0.0}]
|
|
594
|
+
try:
|
|
595
|
+
parsed = json.loads(raw)
|
|
596
|
+
except ValueError:
|
|
597
|
+
return [{"id": "", "text": "Error: malformed skill_search reply", "tags": [], "score": 0.0}]
|
|
598
|
+
return parsed if isinstance(parsed, list) else []
|
|
599
|
+
return []
|
|
600
|
+
|
|
599
601
|
def _list_claims(self) -> str:
|
|
600
602
|
"""v5 blackboard: the host TaskLedger's claims table (inflight + done work)."""
|
|
601
603
|
r = self._rpc("ledger_claims", {})
|
|
@@ -603,13 +605,4 @@ class WorkerScaffold:
|
|
|
603
605
|
return f"Error: {r['error']}"
|
|
604
606
|
return str(r.get("response") or "ledger: no claims")
|
|
605
607
|
|
|
606
|
-
def _memory_rpc(self, op: str, payload: dict[str, Any]) -> str:
|
|
607
|
-
r = self._rpc("memory", {"op": op, **payload})
|
|
608
|
-
if r.get("error"):
|
|
609
|
-
return f"Error: {r['error']}"
|
|
610
|
-
return str(r.get("response") or "")
|
|
611
|
-
|
|
612
|
-
def _memory_api(self) -> "_MemoryApi":
|
|
613
|
-
return _MemoryApi(self)
|
|
614
|
-
|
|
615
608
|
# ---- context + execution --------------------------------------------------------------
|
package/src/sandbox/py/worker.py
CHANGED
|
@@ -139,6 +139,9 @@ class Worker(WorkerScaffold):
|
|
|
139
139
|
ns["list_tasks"] = self._list_tasks
|
|
140
140
|
ns["map_files"] = self._map_files
|
|
141
141
|
ns["llm_map_reduce"] = self._llm_map_reduce
|
|
142
|
+
# SKILL.state recall (Workstream E): distilled facts are NOT repo retrieval — they are
|
|
143
|
+
# project knowledge, so delegation children keep them too (unconditional binding).
|
|
144
|
+
ns["skill_search"] = self._skill_search
|
|
142
145
|
if self.surface != "child":
|
|
143
146
|
ns["search"] = self._search
|
|
144
147
|
ns["grep_context"] = self._grep_context
|
|
@@ -152,7 +155,6 @@ class Worker(WorkerScaffold):
|
|
|
152
155
|
if self.surface != "child":
|
|
153
156
|
ns["add_context"] = self._add_context
|
|
154
157
|
ns["list_claims"] = self._list_claims
|
|
155
|
-
ns["memory"] = self._memory_api()
|
|
156
158
|
ns["SHOW_VARS"] = self._show_vars
|
|
157
159
|
if not isinstance(ns.get("answer"), _AnswerDict):
|
|
158
160
|
cur = ns.get("answer")
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "../context/refresh.ts";
|
|
14
14
|
|
|
15
15
|
/** Static configuration for sandbox creation — set once, reused across getOrCreate calls. */
|
|
16
|
-
|
|
16
|
+
interface SandboxManagerConfig {
|
|
17
17
|
readonly execTimeoutS: number;
|
|
18
18
|
readonly requestTimeoutMs: number;
|
|
19
19
|
readonly python: string;
|
|
@@ -127,7 +127,7 @@ export class SandboxManager {
|
|
|
127
127
|
this.sandbox = s;
|
|
128
128
|
this.initPromise = null;
|
|
129
129
|
return s;
|
|
130
|
-
}).catch((err) => {
|
|
130
|
+
}).catch((err: unknown) => {
|
|
131
131
|
this.contextLoaded = false;
|
|
132
132
|
this.initPromise = null;
|
|
133
133
|
throw err;
|