@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.
Files changed (86) 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 +28 -12
  17. package/src/config/settings.ts +41 -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 +126 -0
  37. package/src/core/root-digest.ts +213 -0
  38. package/src/core/root-state.ts +240 -0
  39. package/src/core/run-state.ts +577 -0
  40. package/src/core/types.ts +51 -12
  41. package/src/index.ts +167 -36
  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 +41 -25
  47. package/src/prompts/native.ts +1 -3
  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/intro.ts +1 -1
  73. package/src/ui/modal/timeline-store.ts +1 -1
  74. package/src/ui/model-picker/drilldown.ts +1 -1
  75. package/src/ui/model-picker/levels.ts +1 -1
  76. package/src/ui/panel/run-registry.ts +1 -1
  77. package/src/ui/tree/tree-rows.ts +1 -1
  78. package/src/ui/tree/tree-widget.ts +1 -1
  79. package/src/util/bm25.ts +97 -0
  80. package/src/util/concurrency.ts +1 -1
  81. package/src/util/errors.ts +1 -1
  82. package/src/util/retry.ts +22 -7
  83. package/src/util/state-merge.ts +34 -0
  84. package/src/util/throttle.ts +1 -1
  85. package/src/util/type-guards.ts +6 -0
  86. package/src/core/memory.ts +0 -589
@@ -4,29 +4,20 @@
4
4
 
5
5
  import type { Usage } from "@earendil-works/pi-ai";
6
6
  import { modelRef } from "../../config/settings.ts";
7
- import { complete1, type Complete1Deps } from "./completion.ts";
8
- import { emitting, summarizeLeaf, throttleHooks } from "./emitting.ts";
7
+ import { complete1, completeDeps } from "./completion.ts";
8
+ import { emitting, summarizeLeaf, throttleHooks, type EmitNote } from "./emitting.ts";
9
9
  import { formatError, errorMessage } from "../../util/errors.ts";
10
10
  import { previewText } from "../../text/preview.ts";
11
- import type { SpawnResult, SubcallHandlerDeps } from "./types.ts";
11
+ import type { Invocation, SpawnResult, SubcallHandlerDeps } from "./types.ts";
12
12
  import type { SubcallOpts } from "../../sandbox/interrupts.ts";
13
13
  import { SPAWN_HINT, spawnAndRun, type SpawnDeps } from "./task-registry.ts";
14
14
  import { ECHO_STUB, taskKey, type TaskLedger } from "../../core/ledger.ts";
15
15
 
16
- const UNWIRED = formatError("RLM bridge not wired for this invocation");
16
+ /** Shared with rlm-query.ts the unwired rejection sentinel (AGENTS DRY). */
17
+ export const UNWIRED = formatError("RLM bridge not wired for this invocation");
17
18
 
18
- function completeDeps(deps: SubcallHandlerDeps): Complete1Deps {
19
- return {
20
- leafGate: deps.gates.leaf,
21
- registry: deps.registry,
22
- getLlmModel: deps.getLlmModel,
23
- getConfig: deps.getConfig,
24
- signal: deps.signal,
25
- onUsage: deps.onUsage,
26
- };
27
- }
28
-
29
- function displayModel(deps: SubcallHandlerDeps): string | undefined {
19
+ /** Shared with rlm-query.ts — one display-model resolution (AGENTS DRY #3). */
20
+ export function displayModel(deps: SubcallHandlerDeps): string | undefined {
30
21
  try {
31
22
  const m = deps.getLlmModel();
32
23
  return modelRef(m) ?? m.id;
@@ -36,10 +27,36 @@ function displayModel(deps: SubcallHandlerDeps): string | undefined {
36
27
  }
37
28
 
38
29
  /** The ledger active for leaf calls — undefined when disabled by config or not threaded in. */
39
- function activeLedger(deps: SubcallHandlerDeps): TaskLedger | undefined {
30
+ export function activeLedger(deps: SubcallHandlerDeps): TaskLedger | undefined {
40
31
  return deps.getConfig().enableLedger ? deps.ledger : undefined;
41
32
  }
42
33
 
34
+ /** DRY: one unwired-spawn shape — kind/n vary, everything else is the same rejection.
35
+ * Shared with rlm-query.ts. */
36
+ export function unwiredSpawn(kind: SpawnResult["kind"], n: number): SpawnResult {
37
+ return { ok: false, task_id: null, kind, n, status: "pending", hint: SPAWN_HINT, error: UNWIRED };
38
+ }
39
+
40
+ /** DRY: the shared leaf recipe — one UI node, one complete1 execution, one summarizer. */
41
+ function emitLeaf(
42
+ inv: Invocation,
43
+ deps: SubcallHandlerDeps,
44
+ prompt: string,
45
+ exec: (track: (u: Usage) => void, note: EmitNote) => Promise<string>,
46
+ ): Promise<string> {
47
+ return emitting(
48
+ inv,
49
+ {
50
+ kind: "llm",
51
+ label: "llm_query",
52
+ args: `prompt: ${previewText(prompt)}`,
53
+ model: displayModel(deps),
54
+ },
55
+ (track, note) => exec(track, note),
56
+ summarizeLeaf,
57
+ );
58
+ }
59
+
43
60
  /** v5 TaskLedger routing for ONE leaf prompt (audit H3 — shared by llm_query and every
44
61
  * llm_batch item, which v5 routed through `_spawn_single` too): echo → stub string,
45
62
  * coalesce → the twin's result (bounded wait), run → caller executes then finish/fail.
@@ -84,31 +101,12 @@ export function createLlmQueryHandler(
84
101
  opts: SubcallOpts,
85
102
  ): Promise<SpawnResult> => {
86
103
  const inv = deps.resolve(opts, depth);
87
- if (inv === null) {
88
- return {
89
- ok: false,
90
- task_id: null,
91
- kind: "llm",
92
- n: 1,
93
- status: "pending",
94
- hint: SPAWN_HINT,
95
- error: UNWIRED,
96
- };
97
- }
104
+ if (inv === null) return unwiredSpawn("llm", 1);
98
105
 
99
106
  const cdeps = completeDeps(deps);
100
107
  const runLeaf = (): Promise<string> =>
101
- emitting(
102
- inv,
103
- {
104
- kind: "llm",
105
- label: "llm_query",
106
- args: `prompt: ${previewText(prompt)}`,
107
- model: displayModel(deps),
108
- },
109
- (track: (u: Usage) => void, note) => complete1(inv, prompt, track, cdeps, throttleHooks(note)),
110
- summarizeLeaf,
111
- );
108
+ emitLeaf(inv, deps, prompt, (track, note) =>
109
+ complete1(inv, prompt, track, cdeps, throttleHooks(note)));
112
110
  // v5 TaskLedger for leaves: identical prompts coalesce onto one completion (key has no
113
111
  // context — a leaf's entire world is the prompt text itself).
114
112
  return spawnAndRun(
@@ -132,17 +130,7 @@ export function createLlmBatchHandler(
132
130
  opts: SubcallOpts,
133
131
  ): Promise<SpawnResult> => {
134
132
  const inv = deps.resolve(opts, depth);
135
- if (inv === null) {
136
- return {
137
- ok: false,
138
- task_id: null,
139
- kind: "llm_batch",
140
- n: prompts.length,
141
- status: "pending",
142
- hint: SPAWN_HINT,
143
- error: UNWIRED,
144
- };
145
- }
133
+ if (inv === null) return unwiredSpawn("llm_batch", prompts.length);
146
134
 
147
135
  const cdeps = completeDeps(deps);
148
136
  const ledger = activeLedger(deps);
@@ -155,27 +143,17 @@ export function createLlmBatchHandler(
155
143
  () =>
156
144
  Promise.all(
157
145
  prompts.map((p) =>
158
- emitting(
159
- inv,
160
- {
161
- kind: "llm",
162
- label: "llm_query",
163
- args: `prompt: ${previewText(p)}`,
164
- model: displayModel(deps),
165
- },
146
+ emitLeaf(inv, deps, p, (track, note) =>
166
147
  // NO outer gate — complete1 takes the single leaf slot per prompt.
167
148
  // v5 (audit H3): every item routes through the ledger — duplicate prompts inside
168
149
  // one batch (or twins of other in-flight leaves) coalesce instead of paying N times.
169
- (track: (u: Usage) => void, note) =>
170
- runClaimedLeaf(
171
- ledger,
172
- ledger === undefined ? undefined : leafClaimKey(deps, p),
173
- p,
174
- inv.depth,
175
- () => complete1(inv, p, track, cdeps, throttleHooks(note)),
176
- ),
177
- summarizeLeaf,
178
- ),
150
+ runClaimedLeaf(
151
+ ledger,
152
+ ledger === undefined ? undefined : leafClaimKey(deps, p),
153
+ p,
154
+ inv.depth,
155
+ () => complete1(inv, p, track, cdeps, throttleHooks(note)),
156
+ )),
179
157
  ),
180
158
  ),
181
159
  deps.trackDetached,
@@ -4,7 +4,7 @@
4
4
  * AGENTS.md DRY #2: childRun exists once, here.
5
5
  */
6
6
 
7
- import { modelRef } from "../../config/settings.ts";
7
+ import { modelLabelOf } from "../../config/settings.ts";
8
8
  import { errorMessage, formatError } from "../../util/errors.ts";
9
9
  import { filterContextByPaths } from "../../context/merge.ts";
10
10
  import { previewText } from "../../text/preview.ts";
@@ -13,12 +13,11 @@ import { checkResourceLimits } from "../../core/resource-limits.ts";
13
13
  import { contextSig, ECHO_STUB, taskKey } from "../../core/ledger.ts";
14
14
  import type { Invocation, SpawnResult, SubcallHandlerDeps } from "./types.ts";
15
15
  import type { SubcallOpts } from "../../sandbox/interrupts.ts";
16
- import { SPAWN_HINT, spawnAndRun, type SpawnDeps } from "./task-registry.ts";
17
- import { complete1, type Complete1Deps } from "./completion.ts";
16
+ import { spawnAndRun, type SpawnDeps } from "./task-registry.ts";
17
+ import { complete1, completeDeps } from "./completion.ts";
18
18
  import { emitting, summarizeLeaf, throttleHooks } from "./emitting.ts";
19
- import { leafClaimKey, runClaimedLeaf } from "./llm-query.ts";
19
+ import { activeLedger, leafClaimKey, runClaimedLeaf, unwiredSpawn } from "./llm-query.ts";
20
20
 
21
- const UNWIRED = formatError("RLM bridge not wired for this invocation");
22
21
  const NO_UNMATCHED: readonly string[] = Object.freeze([]);
23
22
 
24
23
  function emptyResult(answer: string): RlmResult {
@@ -56,25 +55,9 @@ function childContextFor(
56
55
  });
57
56
  }
58
57
 
59
- function completeDeps(deps: SubcallHandlerDeps): Complete1Deps {
60
- return {
61
- leafGate: deps.gates.leaf,
62
- registry: deps.registry,
63
- getLlmModel: deps.getLlmModel,
64
- getConfig: deps.getConfig,
65
- signal: deps.signal,
66
- onUsage: deps.onUsage,
67
- };
68
- }
69
-
70
- /** Ledger active for this call — undefined when disabled by config or not threaded in. */
71
- function activeLedger(deps: SubcallHandlerDeps) {
72
- return deps.getConfig().enableLedger ? deps.ledger : undefined;
73
- }
74
-
75
58
  function claimKeyFor(deps: SubcallHandlerDeps, kind: "llm" | "rlm", prompt: string, paths: readonly string[], ctx: string): string {
76
59
  const rootModel = deps.getModel?.();
77
- const modelId = rootModel === undefined ? "" : (modelRef(rootModel) ?? rootModel.id);
60
+ const modelId = rootModel === undefined ? "" : modelLabelOf(rootModel);
78
61
  return taskKey(kind, prompt, paths, modelId, ctx);
79
62
  }
80
63
 
@@ -111,31 +94,8 @@ async function childRun(
111
94
  ? prompt
112
95
  : `${prompt}\n\n[rlm] paths=${child.unmatched.join(", ")} matched no files; you received the full context.`;
113
96
 
114
- // ── v5 memory replay: an identical, still-fresh child answer replays for zero API calls ──
115
- const memory = deps.memory;
116
97
  const sig = contextSig(child.context);
117
98
  const key = claimKeyFor(deps, "rlm", prompt, paths ?? [], sig);
118
- if (memory !== undefined && deps.getConfig().enableMemory !== false) {
119
- const hit = memory.replay(key);
120
- if (hit !== undefined) {
121
- const replayId = inv.emitter.emitSubcallCreated({
122
- kind: "rlm",
123
- parentId: inv.parentId,
124
- label: "rlm_query (replay)",
125
- detail: prompt.slice(0, 60),
126
- depth: childDepth,
127
- });
128
- inv.emitter.emitSubcallUpdated({ id: replayId, status: "done", resultPreview: hit.result.slice(0, 200) });
129
- return {
130
- answer: hit.result,
131
- iterations: 0,
132
- costUsd: 0,
133
- inputTokens: 0,
134
- outputTokens: 0,
135
- durationMs: 0,
136
- };
137
- }
138
- }
139
99
 
140
100
  // ── v5 TaskLedger: echo → stub; duplicate → coalesce onto the existing runner ──────
141
101
  const ledger = activeLedger(deps);
@@ -148,8 +108,7 @@ async function childRun(
148
108
  // ONE subcall node per childRun (audit C2 / DRY #5): the decision branch reuses it, the
149
109
  // run branch reports the engine's turns/cost on it. Never a second emit below.
150
110
  const rootModel = deps.getModel?.();
151
- const modelLabel =
152
- rootModel === undefined ? undefined : (modelRef(rootModel) ?? rootModel.id);
111
+ const modelLabel = rootModel === undefined ? undefined : modelLabelOf(rootModel);
153
112
  const subId = inv.emitter.emitSubcallCreated({
154
113
  kind: "rlm",
155
114
  parentId: inv.parentId,
@@ -168,13 +127,14 @@ async function childRun(
168
127
  const twin = await ledger
169
128
  .waitFor(decision.key)
170
129
  .catch((err: unknown) => errorMessage(err));
171
- inv.emitter.emitSubcallUpdated({ id: subId, status: "done", resultPreview: previewText(String(twin).slice(0, 80)) });
172
- return emptyResult(String(twin));
130
+ inv.emitter.emitSubcallUpdated({ id: subId, status: "done", resultPreview: previewText(twin.slice(0, 80)) });
131
+ return emptyResult(twin);
173
132
  }
174
133
  if (ledger !== undefined && claimKey !== undefined) {
175
134
  ledger.markRunning(claimKey);
176
135
  }
177
136
 
137
+ const skillBlock = deps.getSkillBlock?.(prompt);
178
138
  const input: RlmInput = {
179
139
  rootPrompt,
180
140
  context: child.context,
@@ -182,6 +142,9 @@ async function childRun(
182
142
  parentNodeId: subId,
183
143
  remainingTimeoutMs: remTimeout,
184
144
  ledger, // DRY #6: the one seam — children share the parent's blackboard
145
+ // SKILL.state Ξ (Workstream C, DRY #6): the parent's block rides along — the only
146
+ // child-RlmInput construction site, so inheritance cannot grow a second path.
147
+ ...(skillBlock === undefined ? {} : { skillBlock }),
185
148
  };
186
149
 
187
150
  try {
@@ -189,19 +152,6 @@ async function childRun(
189
152
  inv.limits.addRaw(res.costUsd, res.inputTokens, res.outputTokens);
190
153
  deps.onChildUsage?.(res.costUsd, res.inputTokens, res.outputTokens);
191
154
  if (ledger !== undefined && claimKey !== undefined) ledger.finish(claimKey, res.answer);
192
- // v5: child answers persist unconditionally — this is what later identical runs replay.
193
- if (memory !== undefined && deps.getConfig().enableMemory !== false) {
194
- memory.recordEpisode({
195
- key,
196
- kind: "rlm",
197
- model: modelLabel ?? "",
198
- prompt,
199
- paths: paths ?? [],
200
- result: res.answer,
201
- tokensIn: res.inputTokens,
202
- tokensOut: res.outputTokens,
203
- });
204
- }
205
155
  inv.emitter.emitSubcallUpdated({
206
156
  id: subId,
207
157
  status: "done",
@@ -225,17 +175,7 @@ export function createRlmQueryHandler(deps: SubcallHandlerDeps, sd: SpawnDeps) {
225
175
  opts: SubcallOpts,
226
176
  ): Promise<SpawnResult> => {
227
177
  const inv = deps.resolve(opts, depth);
228
- if (inv === null) {
229
- return {
230
- ok: false,
231
- task_id: null,
232
- kind: "rlm",
233
- n: 1,
234
- status: "pending",
235
- hint: SPAWN_HINT,
236
- error: UNWIRED,
237
- };
238
- }
178
+ if (inv === null) return unwiredSpawn("rlm", 1);
239
179
 
240
180
  const pathArg = opts.paths;
241
181
 
@@ -297,17 +237,7 @@ export function createRlmBatchHandler(deps: SubcallHandlerDeps, sd: SpawnDeps) {
297
237
  opts: SubcallOpts,
298
238
  ): Promise<SpawnResult> => {
299
239
  const inv = deps.resolve(opts, depth);
300
- if (inv === null) {
301
- return {
302
- ok: false,
303
- task_id: null,
304
- kind: "rlm_batch",
305
- n: tasks.length,
306
- status: "pending",
307
- hint: SPAWN_HINT,
308
- error: UNWIRED,
309
- };
310
- }
240
+ if (inv === null) return unwiredSpawn("rlm_batch", tasks.length);
311
241
 
312
242
  const pathArg = opts.paths;
313
243
  // No wrapper "rlm_batch ×N" node: every task already gets its own rlm_query node from
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { AwaitResult, SpawnResult, TaskEntry } from "./types.ts";
10
+ import { formatError, isErrorText } from "../../util/errors.ts";
10
11
 
11
12
  export const SPAWN_HINT =
12
13
  "Call await_task(task_id=...) to get the result — this is NOT the answer.";
@@ -24,6 +25,22 @@ export interface AwaitDeps {
24
25
  unawaitedIds(): readonly string[];
25
26
  }
26
27
 
28
+ /** DRY: the ONE TaskEntry → AwaitResult mapping — shared by the await handler and the registry. */
29
+ export function entryToAwaitResult(entry: TaskEntry): AwaitResult {
30
+ const status = entry.status === "pending" ? "error" : entry.status;
31
+ return {
32
+ ok: entry.status === "done",
33
+ task_id: entry.taskId,
34
+ kind: entry.kind,
35
+ status,
36
+ result: entry.result,
37
+ results: entry.results,
38
+ error:
39
+ entry.error ??
40
+ (entry.status === "pending" ? "Task still pending" : undefined),
41
+ };
42
+ }
43
+
27
44
  export interface TaskRegistry {
28
45
  readonly spawnDeps: SpawnDeps;
29
46
  readonly awaitDeps: AwaitDeps;
@@ -32,8 +49,8 @@ export interface TaskRegistry {
32
49
  }
33
50
 
34
51
  interface Waiter {
35
- resolve: (entry: TaskEntry) => void;
36
- reject: (err: Error) => void;
52
+ readonly resolve: (entry: TaskEntry) => void;
53
+ readonly reject: (err: Error) => void;
37
54
  timer?: ReturnType<typeof setTimeout>;
38
55
  }
39
56
 
@@ -146,19 +163,7 @@ export function createTaskRegistry(): TaskRegistry {
146
163
  error: `Task ${taskId} not found`,
147
164
  };
148
165
  }
149
- const status =
150
- entry.status === "pending" ? "error" : entry.status;
151
- return {
152
- ok: entry.status === "done",
153
- task_id: entry.taskId,
154
- kind: entry.kind,
155
- status,
156
- result: entry.result,
157
- results: entry.results,
158
- error:
159
- entry.error ??
160
- (entry.status === "pending" ? "Task still pending" : undefined),
161
- };
166
+ return entryToAwaitResult(entry);
162
167
  },
163
168
  };
164
169
  }
@@ -185,14 +190,14 @@ export function spawnAndRun(
185
190
  sd.resolve(taskId, result);
186
191
  } catch (err: unknown) {
187
192
  const message = err instanceof Error ? err.message : String(err);
188
- sd.reject(taskId, message.startsWith("Error:") ? message : `Error: ${message}`);
193
+ sd.reject(taskId, isErrorText(message) ? message : formatError(message));
189
194
  }
190
195
  };
191
196
 
192
197
  if (trackDetached !== undefined && detached) {
193
198
  void trackDetached(run).catch((err: unknown) => {
194
199
  const message = err instanceof Error ? err.message : String(err);
195
- sd.reject(taskId, message.startsWith("Error:") ? message : `Error: ${message}`);
200
+ sd.reject(taskId, isErrorText(message) ? message : formatError(message));
196
201
  });
197
202
  } else {
198
203
  void run();
@@ -13,7 +13,6 @@ import type { SubcallGates } from "../../util/concurrency.ts";
13
13
  import type { SubcallOpts } from "../../sandbox/interrupts.ts";
14
14
  import type { RlmEmitter } from "../../tool/rlm-events.ts";
15
15
  import type { TaskLedger } from "../../core/ledger.ts";
16
- import type { MemoryStore } from "../../core/memory.ts";
17
16
 
18
17
  // ---------------------------------------------------------------------------
19
18
  // Spawn / Await / Finish — the three shapes the model sees
@@ -73,7 +72,7 @@ export function limitsFromRemaining(
73
72
 
74
73
  export interface Invocation {
75
74
  readonly emitter: RlmEmitter;
76
- readonly parentId: string | undefined;
75
+ readonly parentId?: string;
77
76
  readonly depth: number;
78
77
  readonly limits: InvocationLimits;
79
78
  }
@@ -91,8 +90,6 @@ export interface SubcallConfig {
91
90
  readonly enableLedger?: boolean;
92
91
  /** v5: real rlm spawns before demotion to llm (0 = never demote). */
93
92
  readonly rlmBudget?: number;
94
- /** v5 durable memory gates (optional; omitted → memory off). */
95
- readonly enableMemory?: boolean;
96
93
  /** v5.1 retry knobs — structural slice of RlmConfig so retryPolicy() can read them. */
97
94
  readonly retryMaxAttempts?: number;
98
95
  readonly rateLimitMaxAttempts?: number;
@@ -121,8 +118,13 @@ export interface SubcallHandlerDeps {
121
118
  readonly trackDetached?: <T>(run: () => Promise<T>) => Promise<T>;
122
119
  /** v5 TaskLedger blackboard shared across the whole run tree (claim/coalesce/echo/demote). */
123
120
  readonly ledger?: TaskLedger;
124
- /** v5 durable memory (session-wide store) for child replay + episode persistence. */
125
- readonly memory?: MemoryStore;
121
+ /** SKILL.state (Workstream D): ground a leaf prompt with verified facts. Applied ONCE in
122
+ * complete1 (DRY #1) so every leaf path inherits it; returns the prompt unchanged when
123
+ * nothing clears the score threshold — below it, byte-identical. */
124
+ readonly groundLeaf?: (prompt: string) => string;
125
+ /** SKILL.state (Workstream C): the parent run's Ξ block source, for childRun to copy
126
+ * into the child RlmInput (DRY #6 — one construction site). */
127
+ readonly getSkillBlock?: (task: string) => string | undefined;
126
128
  }
127
129
 
128
130
  // ---------------------------------------------------------------------------
@@ -14,7 +14,7 @@ import { type Api, completeSimple, type Message, type Model, type ThinkingLevel,
14
14
  import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
15
15
  import { completeWithRetry, DEFAULT_RETRY_POLICY, type RetryPolicy } from "../util/retry.ts";
16
16
 
17
- export type Role = "system" | "user" | "assistant";
17
+ type Role = "system" | "user" | "assistant";
18
18
  export interface ChatMsg {
19
19
  readonly role: Role;
20
20
  readonly content: string;
@@ -55,17 +55,20 @@ function assistantMessage(text: string, model: Model<Api>): Message {
55
55
  }
56
56
 
57
57
  function toPiMessages(messages: readonly ChatMsg[], model: Model<Api>): { readonly systemPrompt?: string; readonly messages: Message[] } {
58
- let systemPrompt: string | undefined;
58
+ // System segments joined once below — no quadratic re-copy of the accumulated prompt (rule:
59
+ // never build large text with `+`/template concat inside a loop).
60
+ const systemParts: string[] = [];
59
61
  const out: Message[] = [];
60
62
  for (const m of messages) {
61
63
  if (m.role === "system") {
62
- systemPrompt = systemPrompt ? `${systemPrompt}\n\n${m.content}` : m.content;
64
+ systemParts.push(m.content);
63
65
  } else if (m.role === "user") {
64
66
  out.push({ role: "user", content: m.content, timestamp: Date.now() });
65
67
  } else {
66
68
  out.push(assistantMessage(m.content, model));
67
69
  }
68
70
  }
71
+ const systemPrompt = systemParts.length > 0 ? systemParts.join("\n\n") : undefined;
69
72
  return { systemPrompt, messages: out };
70
73
  }
71
74
 
@@ -1,22 +1,13 @@
1
1
  /** `/rlm-llm` — pin the leaf-LLM model (llm_query / llm_batch / map_files). */
2
2
 
3
- import type { Api, Model } from "@earendil-works/pi-ai";
4
3
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
4
  import { modelRef } from "../config/settings.ts";
6
- import { cheapestModel } from "../mode/llm-model.ts";
5
+ import { cheapestModel, sessionScopedModels } from "../mode/llm-model.ts";
7
6
  import type { RlmController } from "../mode/rlm-mode.ts";
8
7
  import { pickableModels, selectModel } from "../ui/model-picker.ts";
9
8
  import { setRlmModeStatus } from "../ui/status.ts";
10
9
  import { applyLlmSelection } from "./pins.ts";
11
10
 
12
- /** Newer Pi hosts expose session-scoped models; 0.79 peers do not — duck-type safely. */
13
- function sessionScopedModels(
14
- ctx: ExtensionContext,
15
- ): readonly { readonly model: Model<Api> }[] | undefined {
16
- const scoped: unknown = Reflect.get(ctx, "scopedModels");
17
- return Array.isArray(scoped) ? scoped as readonly { readonly model: Model<Api> }[] : undefined;
18
- }
19
-
20
11
  async function runRlmLlm(controller: RlmController, ctx: ExtensionContext): Promise<void> {
21
12
  try {
22
13
  await ctx.modelRegistry.refresh();
@@ -4,21 +4,14 @@
4
4
  * the pre-pin behavior, now an explicit picker row.
5
5
  */
6
6
 
7
- import type { Api, Model } from "@earendil-works/pi-ai";
8
7
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
9
8
  import { modelRef } from "../config/settings.ts";
9
+ import { sessionScopedModels } from "../mode/llm-model.ts";
10
10
  import type { RlmController } from "../mode/rlm-mode.ts";
11
11
  import { pickableModels, selectModel } from "../ui/model-picker.ts";
12
12
  import { setRlmModeStatus } from "../ui/status.ts";
13
13
  import { applyRlmSelection } from "./pins.ts";
14
14
 
15
- function sessionScopedModels(
16
- ctx: ExtensionContext,
17
- ): readonly { readonly model: Model<Api> }[] | undefined {
18
- const scoped: unknown = Reflect.get(ctx, "scopedModels");
19
- return Array.isArray(scoped) ? scoped as readonly { readonly model: Model<Api> }[] : undefined;
20
- }
21
-
22
15
  async function runRlmRlm(controller: RlmController, ctx: ExtensionContext): Promise<void> {
23
16
  try {
24
17
  await ctx.modelRegistry.refresh();
@@ -20,11 +20,12 @@ export const DEFAULT_CONFIG: Readonly<RlmConfig> = Object.freeze({
20
20
  // of the context it inherited, where a leaf is one HTTP request. Worst case is
21
21
  // (maxDepth - 1) × this many concurrent child engines. Default 4.
22
22
  maxConcurrentChildren: 4,
23
- // v5.1 rate-limit resilience (util/retry.ts): 3 total attempts, 500ms→15s backoff,
24
- // 2s→60s adaptive per-provider cooldown. All overridable in rlm.json.
25
- retryMaxAttempts: 3,
26
- // 429s park on the cooldown instead of dying — up to 8 windows (2s→4s→…≤60s ≈ 4 min).
27
- rateLimitMaxAttempts: 8,
23
+ // v5.1 rate-limit resilience (util/retry.ts): 15 total attempts on the SAME model —
24
+ // 500ms→15s backoff, 2s→60s adaptive per-provider cooldown. DOCTRINE: NO fallback
25
+ // when the attempts are exhausted the call fails loudly; model/provider never switch.
26
+ retryMaxAttempts: 15,
27
+ // 429s park on the cooldown instead of dying — up to 15 windows (2s→4s→…≤60s).
28
+ rateLimitMaxAttempts: 15,
28
29
  retryBaseDelayMs: 500,
29
30
  retryMaxDelayMs: 15_000,
30
31
  throttleBaseMs: 2_000,
@@ -51,14 +52,29 @@ export const DEFAULT_CONFIG: Readonly<RlmConfig> = Object.freeze({
51
52
  // v5 TaskLedger blackboard
52
53
  enableLedger: true,
53
54
  rlmBudget: 8,
54
- // v5 durable memory
55
- enableMemory: true,
56
- injectNoteTokens: 2_000,
57
- evolveEvery: 8,
58
- memoryDir: null,
59
- // v5 role separation: children delegate (llm + memory/ledger); "legacy" = full child surface.
60
- childSurface: "delegation",
61
55
  // Verification-discipline nudge — deliberately OFF (plan guardrail): when on, an early
62
56
  // bare-number finalize gets one coached redo instead of being accepted. Opt-in via rlm.json.
63
57
  enableVerificationNudge: false,
58
+ // SKILL.state integration: Σ_t execution state + cross-session distilled knowledge.
59
+ enableRunState: true,
60
+ runStateRetryMax: 2,
61
+ enableSkillState: true,
62
+ // Default ON (bench rec #3): deterministic harvest — one cheap distill leaf per finalize
63
+ // replaces the stochastic fence-emission harvest (0 vs 4 notes across identical ON arms).
64
+ enableSkillStateDistill: true,
65
+ skillStateMaxTokens: 1_200,
66
+ skillStateLeafTokens: 200,
67
+ skillStateMinScore: 4.0,
68
+ skillStateNotesPerProject: 128,
69
+ // Root Σ integration (WS-2..WS-4): digest compaction ON (it only swaps the summarizer for
70
+ // a deterministic digest — zero tokens, strictly less latency); the context transform and
71
+ // model-proposed fences soak with flags OFF until the A/B says otherwise.
72
+ enableRootDigestCompaction: true,
73
+ rootDigestKeepRecentChars: 12_000,
74
+ rootDigestMaxChars: 8_000,
75
+ enableRootContextTransform: false,
76
+ rootContextKeepTurns: 2,
77
+ rootContextElideChars: 1_500,
78
+ rootContextSnapshot: true,
79
+ enableRootStateFences: false,
64
80
  });