@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.
Files changed (88) hide show
  1. package/README.md +0 -4
  2. package/README.ru.md +56 -66
  3. package/README.zh-CN.md +61 -65
  4. package/package.json +5 -5
  5. package/src/bridge/add-context.ts +1 -1
  6. package/src/bridge/handlers/await.ts +13 -22
  7. package/src/bridge/handlers/completion.ts +27 -5
  8. package/src/bridge/handlers/emitting.ts +2 -2
  9. package/src/bridge/handlers/llm-query.ts +46 -68
  10. package/src/bridge/handlers/rlm-query.ts +14 -84
  11. package/src/bridge/handlers/task-registry.ts +22 -17
  12. package/src/bridge/handlers/types.ts +8 -6
  13. package/src/bridge/model.ts +6 -3
  14. package/src/commands/rlm-llm.ts +1 -10
  15. package/src/commands/rlm-rlm.ts +1 -8
  16. package/src/config/defaults.ts +32 -12
  17. package/src/config/settings.ts +53 -31
  18. package/src/config/skillstate.ts +465 -0
  19. package/src/context/md-cache.ts +1 -1
  20. package/src/context/merge.ts +1 -1
  21. package/src/context/namespace.ts +2 -2
  22. package/src/context/refresh.ts +1 -1
  23. package/src/context/source-dir.ts +21 -11
  24. package/src/context/source-doc.ts +1 -1
  25. package/src/context/source-git.ts +3 -15
  26. package/src/context/source-text.ts +1 -1
  27. package/src/context/walk.ts +6 -14
  28. package/src/core/budget.ts +107 -21
  29. package/src/core/compaction.ts +44 -1
  30. package/src/core/engine.ts +141 -84
  31. package/src/core/iteration.ts +1 -1
  32. package/src/core/ledger.ts +10 -13
  33. package/src/core/limits.ts +1 -1
  34. package/src/core/model-registry.ts +1 -1
  35. package/src/core/resource-limits.ts +1 -1
  36. package/src/core/root-context.ts +184 -0
  37. package/src/core/root-digest.ts +213 -0
  38. package/src/core/root-state.ts +310 -0
  39. package/src/core/run-state.ts +587 -0
  40. package/src/core/types.ts +51 -12
  41. package/src/index.ts +220 -39
  42. package/src/mode/llm-model.ts +13 -1
  43. package/src/mode/native-guards.ts +0 -6
  44. package/src/mode/rlm-mode.ts +34 -11
  45. package/src/mode/subagent.ts +5 -5
  46. package/src/prompts/glossary.ts +46 -25
  47. package/src/prompts/native.ts +27 -5
  48. package/src/prompts/system.ts +12 -4
  49. package/src/sandbox/context-file.ts +1 -1
  50. package/src/sandbox/interrupts.ts +25 -31
  51. package/src/sandbox/protocol.ts +14 -20
  52. package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
  53. package/src/sandbox/py/__pycache__/scaffold.cpython-314.pyc +0 -0
  54. package/src/sandbox/py/__pycache__/worker.cpython-314.pyc +0 -0
  55. package/src/sandbox/py/guards.py +1 -1
  56. package/src/sandbox/py/scaffold.py +24 -31
  57. package/src/sandbox/py/worker.py +3 -1
  58. package/src/sandbox/sandbox-manager.ts +2 -2
  59. package/src/sandbox/sandbox.ts +21 -4
  60. package/src/text/agent-text.ts +58 -0
  61. package/src/text/parsing.ts +35 -3
  62. package/src/text/preview.ts +3 -0
  63. package/src/text/repl-output.ts +1 -1
  64. package/src/tool/background-tasks.ts +1 -1
  65. package/src/tool/repl-render.ts +1 -1
  66. package/src/tool/repl-result.ts +1 -1
  67. package/src/tool/repl-tool.ts +50 -26
  68. package/src/tool/rlm-tool.ts +4 -5
  69. package/src/tool/subcall-render.ts +1 -1
  70. package/src/tool/subcall-store.ts +2 -2
  71. package/src/tool/tool-utils.ts +5 -5
  72. package/src/ui/config-panel.ts +12 -0
  73. package/src/ui/intro.ts +1 -1
  74. package/src/ui/modal/timeline-store.ts +1 -1
  75. package/src/ui/model-picker/drilldown.ts +1 -1
  76. package/src/ui/model-picker/levels.ts +1 -1
  77. package/src/ui/panel/run-registry.ts +1 -1
  78. package/src/ui/status.ts +39 -4
  79. package/src/ui/tree/tree-rows.ts +1 -1
  80. package/src/ui/tree/tree-widget.ts +1 -1
  81. package/src/util/bm25.ts +97 -0
  82. package/src/util/concurrency.ts +1 -1
  83. package/src/util/errors.ts +1 -1
  84. package/src/util/retry.ts +22 -7
  85. package/src/util/state-merge.ts +34 -0
  86. package/src/util/throttle.ts +1 -1
  87. package/src/util/type-guards.ts +6 -0
  88. package/src/core/memory.ts +0 -589
@@ -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
- export const RETRIEVAL_GLOSSARY_LINES: readonly string[] = Object.freeze([
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
- export const DELEGATION_SURFACE_LINES: readonly string[] = Object.freeze([
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
- export const DELEGATION_GLOSSARY_LINES: readonly string[] = Object.freeze([
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
- export const CHUNKED_GLOSSARY_LINES: readonly string[] = Object.freeze([
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
- export const SPAWN_GLOSSARY_LINES: readonly string[] = Object.freeze([
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
- export const SPAWN_EXAMPLE_RETRIEVAL: readonly string[] = Object.freeze([
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
- export const SPAWN_EXAMPLE_DELEGATION: readonly string[] = Object.freeze([
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
- export const RECURSION_CONTEXT_LINES: readonly string[] = Object.freeze([
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
- export const RECURSION_DELEGATION_LINES: readonly string[] = Object.freeze([
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
- export const CHILD_CONTEXT_LINES: readonly string[] = Object.freeze([
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
- export const CONTEXT_EXCLUSION_NOTE = [
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`.',
@@ -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
- "- `memory.query(q)` / `memory.add(text, paths=…, tags=…)` durable notes under `.rlm/` that survive",
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
- export function buildNativeSystemPrompt(): string {
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
 
@@ -17,14 +17,17 @@ import {
17
17
 
18
18
  export { contextKindOf, type ContextKind } from "./glossary.ts";
19
19
 
20
- export interface PromptMeta {
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
- export interface SystemPromptOptions {
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 / memory.* /",
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), ENV_TIPS bounds UNDER-recursion (solving it yourself).
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
- export interface ContextTempFile {
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
- /** v5: durable memory surface for the sandbox's `memory.query/add/stats` object. */
57
- memoryOp(
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
- memoryOp: async () => UNCONFIGURED,
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: String(collected ?? "") };
170
+ return { response: typeof collected === "string" ? collected : "" };
175
171
  }
176
172
  // Unexpected shape — surface as text rather than crash the worker.
177
- return { response: String(raw ?? "") };
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: String(result ?? "") });
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 "memory": {
356
- const out = await h.memoryOp(
357
- msg.op,
358
- { query: msg.query, k: msg.k, content: msg.content, paths: msg.paths, tags: msg.tags },
359
- d,
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
  }
@@ -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
- export interface LlmReply {
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
- export interface Heartbeat {
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 + memory). */
75
- export type InterruptKind =
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
- | "memory";
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
- export interface AddContextInterrupt extends InterruptBase {
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
- export interface LedgerClaimsInterrupt extends InterruptBase {
127
+ interface LedgerClaimsInterrupt extends InterruptBase {
128
128
  readonly type: "ledger_claims";
129
129
  }
130
130
 
131
- /** v5: sandbox reaches the durable MemoryStore (`memory.query/add/stats`). */
132
- export interface MemoryInterrupt extends InterruptBase {
133
- readonly type: "memory";
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
- | MemoryInterrupt;
146
+ | SkillSearchInterrupt;
151
147
 
152
148
  export type WorkerMessage = WorkerResponse | WorkerInterrupt;
153
149
 
154
- export const INTERRUPT_KINDS = Object.freeze(
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
- "memory",
160
+ "skill_search",
165
161
  ]),
166
162
  );
167
163
 
168
- function isRecord(value: unknown): value is Record<string, unknown> {
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";
@@ -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", "memory",
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 --------------------------------------------------------------
@@ -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
- export interface SandboxManagerConfig {
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;