@hicaru/pi-rlm 0.3.16 → 0.3.17
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 +28 -12
- package/src/config/settings.ts +41 -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 +126 -0
- package/src/core/root-digest.ts +213 -0
- package/src/core/root-state.ts +240 -0
- package/src/core/run-state.ts +577 -0
- package/src/core/types.ts +51 -12
- package/src/index.ts +167 -36
- 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 +41 -25
- package/src/prompts/native.ts +1 -3
- 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/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/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,44 @@ 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
|
+
export function skillStateLines(noteCount: number, body: string): string {
|
|
66
|
+
return [
|
|
67
|
+
`[Project facts — SkillState, ${noteCount} note${noteCount === 1 ? "" : "s"}, distilled from prior sessions]`,
|
|
68
|
+
body,
|
|
69
|
+
SKILL_RECALL_LINE,
|
|
70
|
+
].join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Root Σ WS-2 digest wording — the single source for the header and section labels. */
|
|
74
|
+
export const ROOT_DIGEST_HEADER =
|
|
75
|
+
"[Root digest — deterministic structural compaction (no model call). Older turns are " +
|
|
76
|
+
"superseded by this digest plus the verbatim tail that follows; fresh tool results " +
|
|
77
|
+
"outrank the digest when they disagree.]";
|
|
78
|
+
export const ROOT_DIGEST_SECTIONS: Readonly<Record<"task" | "findings" | "state" | "next" | "facts", string>> =
|
|
79
|
+
Object.freeze({ task: "Task", findings: "Findings", state: "State", next: "Next", facts: "Project facts" });
|
|
80
|
+
|
|
52
81
|
/** One-line delegation helpers — orchestrating must be cheaper than solving. */
|
|
53
|
-
|
|
82
|
+
const DELEGATION_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
54
83
|
"- `map_files(files, prompt) -> Task`: always spawn. `await_task(t)` → dict[path, answer].",
|
|
55
84
|
" Accepts context entries or paths; packs into cap-sized batches; splits oversized files.",
|
|
56
85
|
" **Default way to read many files** — fire independent `map_files` Tasks, free work, then await.",
|
|
@@ -59,14 +88,14 @@ export const DELEGATION_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
|
59
88
|
]);
|
|
60
89
|
|
|
61
90
|
/** Shared glossary entry for the chunked-query helper (headless + native). */
|
|
62
|
-
|
|
91
|
+
const CHUNKED_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
63
92
|
"- `llm_query_chunked(text: str, prompt: str) -> Task`: always spawn. `await_task(t)` → list[str]",
|
|
64
93
|
" (one answer per chunk, order preserved). Auto-splits text to the sub-LLM prompt cap.",
|
|
65
94
|
" Use for ANY text too large for a single `llm_query` — open()ed files, oversized sub-results.",
|
|
66
95
|
]);
|
|
67
96
|
|
|
68
97
|
/** Non-blocking fan-out: spawn now, collect later (headless glossary). */
|
|
69
|
-
|
|
98
|
+
const SPAWN_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
70
99
|
"- **ALWAYS SPAWN (Task + ↗bg):** `llm_query` / `llm_batch` / `rlm_query` / `rlm_batch` /",
|
|
71
100
|
" `map_files` / `llm_query_chunked`. Never treat the return as the answer.",
|
|
72
101
|
" Collect with `await_task(t)`, `await_task([t1,t2,…])`, or `await_task()` (every still-running Task).",
|
|
@@ -81,7 +110,7 @@ export const SPAWN_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
|
81
110
|
]);
|
|
82
111
|
|
|
83
112
|
/** v5 (audit C5): the spawn worked example, retrieval flavor — root surface only. */
|
|
84
|
-
|
|
113
|
+
const SPAWN_EXAMPLE_RETRIEVAL: readonly string[] = Object.freeze([
|
|
85
114
|
"",
|
|
86
115
|
" ```python",
|
|
87
116
|
" # Multi-area study: one rlm_batch (parallel workers), free locate, then await",
|
|
@@ -96,7 +125,7 @@ export const SPAWN_EXAMPLE_RETRIEVAL: readonly string[] = Object.freeze([
|
|
|
96
125
|
]);
|
|
97
126
|
|
|
98
127
|
/** v5 (audit C5): the spawn worked example, delegation flavor — no retrieval, slice instead. */
|
|
99
|
-
|
|
128
|
+
const SPAWN_EXAMPLE_DELEGATION: readonly string[] = Object.freeze([
|
|
100
129
|
"",
|
|
101
130
|
" ```python",
|
|
102
131
|
" # Multi-area study: one rlm_batch (parallel workers), slice your world while they run",
|
|
@@ -109,7 +138,7 @@ export const SPAWN_EXAMPLE_DELEGATION: readonly string[] = Object.freeze([
|
|
|
109
138
|
" # One-shot extracts: map_files / llm_batch also return Task → await_task",
|
|
110
139
|
" ```",
|
|
111
140
|
]);
|
|
112
|
-
|
|
141
|
+
const RECURSION_CONTEXT_LINES: readonly string[] = Object.freeze([
|
|
113
142
|
"",
|
|
114
143
|
" **What a child sees:** it inherits YOUR `context` — every file you have loaded, including",
|
|
115
144
|
" sources under `ctx/<id>/…` — and runs `search` / `grep_context` / `outline` / `map_files`",
|
|
@@ -125,7 +154,7 @@ export const RECURSION_CONTEXT_LINES: readonly string[] = Object.freeze([
|
|
|
125
154
|
|
|
126
155
|
/** v5 recursion section, delegation variant (audit C5): describes what a delegation child
|
|
127
156
|
* receives — the narrowed pack as text, no retrieval of its own. */
|
|
128
|
-
|
|
157
|
+
const RECURSION_DELEGATION_LINES: readonly string[] = Object.freeze([
|
|
129
158
|
"",
|
|
130
159
|
" **What a child sees:** it inherits YOUR `context` (narrowed by `paths=` when given) and works",
|
|
131
160
|
" on it as text — it has NO retrieval tools, so put what matters in your prompt and `paths`,",
|
|
@@ -140,14 +169,14 @@ export const RECURSION_DELEGATION_LINES: readonly string[] = Object.freeze([
|
|
|
140
169
|
* Sub-RLM orientation. Emitted only at depth > 0, where `context` is the parent's world rather
|
|
141
170
|
* than a repository the run packed for itself.
|
|
142
171
|
*/
|
|
143
|
-
|
|
172
|
+
const CHILD_CONTEXT_LINES: readonly string[] = Object.freeze([
|
|
144
173
|
" You are a sub-RLM. This `context` is your parent's world — every file it has loaded (cwd",
|
|
145
174
|
" paths un-prefixed; external sources under `ctx/<id>/…`). Answer only the question above;",
|
|
146
175
|
" your REPL and anything you load die with you, and only your final answer string returns.",
|
|
147
176
|
]);
|
|
148
177
|
|
|
149
178
|
/** Why a file the user mentioned may be missing from `context`. */
|
|
150
|
-
|
|
179
|
+
const CONTEXT_EXCLUSION_NOTE = [
|
|
151
180
|
" NOTE: `context` holds only the files you have loaded (starts empty; cwd seeds on first use).",
|
|
152
181
|
" Gitignored files and files larger than 1MB of plain text are skipped. Binary documents",
|
|
153
182
|
" (PDF, DOCX, XLSX, PPTX, CSV, …) ARE included — converted to Markdown on the way in.",
|
|
@@ -231,9 +260,6 @@ export function envTips(delegation = false): string {
|
|
|
231
260
|
].join("\n");
|
|
232
261
|
}
|
|
233
262
|
|
|
234
|
-
/** Root-surface doctrine (back-compat alias of `envTips(false)`). */
|
|
235
|
-
export const ENV_TIPS = envTips(false);
|
|
236
|
-
|
|
237
263
|
/** Native-mode variant of the doctrine — same rules, sized for the native prompt budget. */
|
|
238
264
|
export const ENV_TIPS_CONDENSED = [
|
|
239
265
|
"### Decomposition doctrine",
|
|
@@ -314,6 +340,7 @@ export function replGlossary(
|
|
|
314
340
|
" await_task → ordered list[str]. NEVER pass bare file paths as if the worker can open them.",
|
|
315
341
|
...CHUNKED_GLOSSARY_LINES,
|
|
316
342
|
...SPAWN_GLOSSARY_LINES,
|
|
343
|
+
...SKILL_SEARCH_GLOSSARY_LINES,
|
|
317
344
|
...(delegation ? SPAWN_EXAMPLE_DELEGATION : SPAWN_EXAMPLE_RETRIEVAL),
|
|
318
345
|
...DELEGATION_GLOSSARY_LINES,
|
|
319
346
|
);
|
|
@@ -357,17 +384,6 @@ export function replGlossary(
|
|
|
357
384
|
" verified result in `answers` — see the decomposition doctrine below.",
|
|
358
385
|
"- `SHOW_VARS() -> str`: list every variable currently in the REPL (Task handles show as `<Task …>`).",
|
|
359
386
|
"- `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
387
|
"- `list_claims()`: the live `[ledger]` table of inflight/done agent work.",
|
|
372
388
|
'- `answer`: a dict initialized to {"content": "", "ready": False}. To submit your final answer,',
|
|
373
389
|
' set `answer["content"]` to the answer text and `answer["ready"] = True`.',
|
package/src/prompts/native.ts
CHANGED
|
@@ -45,9 +45,7 @@ function nativeReplGlossary(): string {
|
|
|
45
45
|
"### Memo / finalize",
|
|
46
46
|
"- `answers` / `plan` — persistent dicts for **collected** results. Task handles are REPL vars (`t`), not `answers` keys.",
|
|
47
47
|
"- `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.",
|
|
48
|
+
"- `list_claims()` — the live `[ledger]` table of agent work.",
|
|
51
49
|
"- `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
50
|
"",
|
|
53
51
|
ENV_TIPS_CONDENSED,
|
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;
|
package/src/sandbox/sandbox.ts
CHANGED
|
@@ -27,6 +27,10 @@ import { trace, traceEnabled } from "../util/trace.ts";
|
|
|
27
27
|
|
|
28
28
|
export type { AddContextResult, SubcallOpts, SubLlmHandlers } from "./interrupts.ts";
|
|
29
29
|
|
|
30
|
+
/** Event-loop guard: frames are model/summary-sized by construction; payloads travel via temp
|
|
31
|
+
* files, never the wire. Anything near this cap is a runaway producer — drop it. */
|
|
32
|
+
const MAX_FRAME_CHARS = 8_000_000;
|
|
33
|
+
|
|
30
34
|
export interface SandboxOptions {
|
|
31
35
|
/** Sandbox recursion depth label (passed to the worker, used in interrupt routing). */
|
|
32
36
|
readonly depth?: number;
|
|
@@ -139,7 +143,7 @@ export class PythonSandbox {
|
|
|
139
143
|
// windowsHide: without it each sandbox flashes a console window on Windows (pi sets
|
|
140
144
|
// this on every spawn — bash.ts / shell.ts). Same Windows surface as issue #7.
|
|
141
145
|
{ stdio: ["pipe", "pipe", "pipe"], env: sanitizedEnv(), windowsHide: true },
|
|
142
|
-
)
|
|
146
|
+
);
|
|
143
147
|
|
|
144
148
|
this.proc.stdout.setEncoding("utf8");
|
|
145
149
|
this.proc.stdout.on("data", (chunk: string) => this.onData(chunk));
|
|
@@ -262,7 +266,8 @@ export class PythonSandbox {
|
|
|
262
266
|
this.pending.set("_init", {
|
|
263
267
|
resolve: (res) => {
|
|
264
268
|
clearTimeout(timer);
|
|
265
|
-
res.ok
|
|
269
|
+
if (res.ok) resolve();
|
|
270
|
+
else reject(new Error(res.error ?? "worker init failed"));
|
|
266
271
|
},
|
|
267
272
|
reject,
|
|
268
273
|
timer,
|
|
@@ -299,7 +304,7 @@ export class PythonSandbox {
|
|
|
299
304
|
settle(value);
|
|
300
305
|
};
|
|
301
306
|
this.pending.set(id, { resolve: once(resolve), reject: once(reject), timer, requestType: payload.type });
|
|
302
|
-
this.send({ id, ...payload }
|
|
307
|
+
this.send({ id, ...payload });
|
|
303
308
|
});
|
|
304
309
|
}
|
|
305
310
|
|
|
@@ -356,7 +361,12 @@ export class PythonSandbox {
|
|
|
356
361
|
this.appendStderr(`[rlm] dropped '${msg.type}' frame: worker ${this.exitDescription()}\n`);
|
|
357
362
|
return;
|
|
358
363
|
}
|
|
359
|
-
|
|
364
|
+
const frame = JSON.stringify(msg);
|
|
365
|
+
if (frame.length > MAX_FRAME_CHARS) {
|
|
366
|
+
this.appendStderr(`[rlm] dropped '${msg.type}' frame: ${frame.length} chars exceed the frame cap\n`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
this.proc.stdin.write(`${frame}\n`);
|
|
360
370
|
}
|
|
361
371
|
|
|
362
372
|
/**
|
|
@@ -391,6 +401,13 @@ export class PythonSandbox {
|
|
|
391
401
|
|
|
392
402
|
private onData(chunk: string): void {
|
|
393
403
|
this.buf += chunk;
|
|
404
|
+
// Untrusted-stream guard: a worker that stops emitting newlines would otherwise balloon
|
|
405
|
+
// this buffer without bound and stall the pump. Reset (both cursors) and keep draining.
|
|
406
|
+
if (this.buf.length > MAX_FRAME_CHARS) {
|
|
407
|
+
this.appendStderr(`\n[protocol] stdout buffer exceeded ${MAX_FRAME_CHARS} chars without a newline — truncated\n`);
|
|
408
|
+
this.buf = "";
|
|
409
|
+
this.scanOffset = 0;
|
|
410
|
+
}
|
|
394
411
|
let nl: number;
|
|
395
412
|
while ((nl = this.buf.indexOf("\n", this.scanOffset)) >= 0) {
|
|
396
413
|
const line = this.buf.slice(this.scanOffset, nl).trim();
|