@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
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Root Σ (WS-3b/WS-4) — RootStateTracker: a digest-level Σ_t for the NATIVE Pi session.
3
+ *
4
+ * This is NOT the engine's RunStateTracker (core/engine turn loop); it is the root
5
+ * orchestrator's sufficient statistic, living in the extension closure next to
6
+ * controller/sandboxManager. It is runtime-derived — tool outcomes, engine-run mirrors,
7
+ * the user's latest prompt — zero model cooperation required (paper §5.3: observation
8
+ * override; §5.7: small models must not be the state's author by default). The optional
9
+ * fence protocol (enableRootStateFences — ENFORCED ON since Root Σ v2 R0) is the only
10
+ * model-proposed input and
11
+ * rides the SAME V(ΔΣ_t,Σ_t) validator + retry/degrade ladder as engine runs.
12
+ *
13
+ * Caps/dedup/serialization are the engine's own machinery: RUN_STATE_LIMITS, dedupStrings,
14
+ * enforceCaps, applyPatch — reused, never re-implemented here.
15
+ */
16
+
17
+ import {
18
+ applyPatch,
19
+ dedupStrings,
20
+ enforceCaps,
21
+ freshRunState,
22
+ malformedFenceProblem,
23
+ patchErrorText,
24
+ RUN_STATE_LIMITS,
25
+ statePatchObservation,
26
+ type ApproachOutcome,
27
+ type MutableState,
28
+ type PatchError,
29
+ type RunState,
30
+ } from "./run-state.ts";
31
+ import type { Result } from "../util/errors.ts";
32
+ import type { StateFenceResult } from "../text/parsing.ts";
33
+
34
+ /** Consecutive failed outcomes on one key before the rectify hint fires (MAS2 Eq. 5 parity). */
35
+ const RECTIFY_FAILURE_THRESHOLD = 2;
36
+ /** Task restatement cap — mirrors run-state.ts TASK_MAX_CHARS (kept in sync by comment). */
37
+ const ROOT_TASK_MAX_CHARS = 200;
38
+
39
+ /** R4: idle-degrade threshold for the NATIVE root tracker — deliberately ROOT-SPECIFIC
40
+ * (soak finding, 2025-09-08 live sessions: qwen3.8-27b ×3, qwen3-30b/32b, gemini-flash).
41
+ * The engine's `RUN_STATE_IDLE_DEGRADE_TURNS = 4` is bench-tuned for runs conditioned on Σ
42
+ * from turn 1; NATIVE sessions have a cold-start ramp — first fences land on turn 3 (short
43
+ * tasks) or turn 5–6 (study tasks), so 4 amputated exactly before the first commit (2/2
44
+ * study sessions degraded at 4, then fenced at 5). 6 clears the observed ramp while still
45
+ * bounding the fence tax. The engine const and its tuning are untouched.
46
+ */
47
+ export const ROOT_IDLE_DEGRADE_TURNS = 6;
48
+
49
+ /** R3 soak observability: per-turn fence outcome, returned by applyFences. */
50
+ export interface FenceOutcome {
51
+ readonly fences: number;
52
+ readonly accepted: number;
53
+ readonly problems: number;
54
+ }
55
+
56
+ /** Root Σ mode — same discriminated union shape as the engine's (active | degraded). */
57
+ type RootStateMode =
58
+ | { readonly kind: "active"; readonly retries: number }
59
+ | { readonly kind: "degraded"; readonly reason: string };
60
+
61
+ export class RootStateTracker {
62
+ private draft: MutableState;
63
+ private mode: RootStateMode = { kind: "active", retries: 0 };
64
+ private opCounter = 0;
65
+ private dirtyFlag = false;
66
+ /** R4: consecutive fence-eligible turns with zero accepted deltas (idle streak). */
67
+ private idleFenceTurns = 0;
68
+ private readonly failures = new Map<string, number>();
69
+ private pendingObservation: string | undefined;
70
+ private readonly retryMax: number;
71
+
72
+ private constructor(task: string, retryMax: number) {
73
+ const fresh = freshRunState(task);
74
+ this.draft = {
75
+ task: fresh.task,
76
+ nextStep: fresh.nextStep,
77
+ updatedAt: fresh.updatedAt,
78
+ findings: [...fresh.findings],
79
+ verifiedFacts: [...fresh.verifiedFacts],
80
+ openQuestions: [...fresh.openQuestions],
81
+ testedApproaches: { ...fresh.testedApproaches },
82
+ artifacts: { ...fresh.artifacts },
83
+ };
84
+ this.retryMax = Math.max(0, Math.floor(retryMax));
85
+ }
86
+
87
+ static fresh(task: string, retryMax = 2): RootStateTracker {
88
+ return new RootStateTracker(task.trim().slice(0, ROOT_TASK_MAX_CHARS), retryMax);
89
+ }
90
+
91
+ /** True when nothing worth splicing has accumulated — the context transform no-ops. */
92
+ get isEmpty(): boolean {
93
+ return (
94
+ this.draft.findings.length === 0 &&
95
+ this.draft.verifiedFacts.length === 0 &&
96
+ Object.keys(this.draft.testedApproaches).length === 0 &&
97
+ this.draft.nextStep === ""
98
+ );
99
+ }
100
+
101
+ get dirty(): boolean {
102
+ return this.dirtyFlag;
103
+ }
104
+
105
+ /** R4: true while the tracker accepts fences and the context transform may splice Σ. */
106
+ get isActive(): boolean {
107
+ return this.mode.kind === "active";
108
+ }
109
+
110
+ /** R4 telemetry: consecutive fence-eligible turns with zero accepted deltas. */
111
+ get idleTurns(): number {
112
+ return this.idleFenceTurns;
113
+ }
114
+
115
+ /** R4 telemetry: the degrade reason while degraded; undefined while active. */
116
+ get degradeReason(): string | undefined {
117
+ return this.mode.kind === "degraded" ? this.mode.reason : undefined;
118
+ }
119
+
120
+ snapshot(): RunState {
121
+ const capped = enforceCaps(this.draft);
122
+ // enforceCaps never fails in practice; the fallback keeps the tracker fail-soft anyway.
123
+ return capped.ok ? capped.value : freshRunState(this.draft.task);
124
+ }
125
+
126
+ noteFinding(text: string): void {
127
+ const trimmed = text.trim();
128
+ if (trimmed === "") return;
129
+ this.draft.findings = dedupStrings([...this.draft.findings, trimmed]).slice(-RUN_STATE_LIMITS.findings);
130
+ this.touch();
131
+ }
132
+
133
+ noteFact(text: string): void {
134
+ const trimmed = text.trim();
135
+ if (trimmed === "") return;
136
+ this.draft.verifiedFacts = dedupStrings([...this.draft.verifiedFacts, trimmed])
137
+ .slice(-RUN_STATE_LIMITS.verifiedFacts);
138
+ this.touch();
139
+ }
140
+
141
+ noteOutcome(key: string, outcome: ApproachOutcome): void {
142
+ const k = key.trim();
143
+ if (k === "") return;
144
+ this.draft.testedApproaches[k] = outcome;
145
+ if (outcome.status === "failed") {
146
+ this.failures.set(k, (this.failures.get(k) ?? 0) + 1);
147
+ } else {
148
+ this.failures.delete(k);
149
+ }
150
+ this.touch();
151
+ }
152
+
153
+ /** Tool-outcome feed (WS-4 v1 source): failures become approach outcomes, verbatim. */
154
+ observeToolResult(toolName: string, isError: boolean, reasonFirstLine: string): void {
155
+ if (isError) {
156
+ this.noteOutcome(`tool:${toolName}`, { status: "failed", reason: reasonFirstLine });
157
+ } else {
158
+ // A success clears the tool's failure streak — the state reflects the newest truth.
159
+ if (this.draft.testedApproaches[`tool:${toolName}`] !== undefined) {
160
+ this.noteOutcome(`tool:${toolName}`, { status: "succeeded", evidence: reasonFirstLine });
161
+ } else {
162
+ this.failures.delete(`tool:${toolName}`);
163
+ }
164
+ }
165
+ }
166
+
167
+ /** The user's latest ask IS the next step by definition (WS-4 v1 source). Empty is a no-op. */
168
+ setNextStep(text: string): void {
169
+ const trimmed = text.trim();
170
+ if (trimmed === "") return;
171
+ this.draft.nextStep = trimmed.slice(0, 300);
172
+ this.touch();
173
+ }
174
+
175
+ /**
176
+ * WS-4 mirror: absorb a finished engine run's Σ (same object the SkillState harvest
177
+ * consumes — one source, two sinks). Findings/facts dedup through the shared key; the
178
+ * engine's approach outcomes ride along under their own keys.
179
+ */
180
+ absorbEngineState(engine: RunState): void {
181
+ this.draft.findings = dedupStrings([...this.draft.findings, ...engine.findings])
182
+ .slice(-RUN_STATE_LIMITS.findings);
183
+ this.draft.verifiedFacts = dedupStrings([...this.draft.verifiedFacts, ...engine.verifiedFacts])
184
+ .slice(-RUN_STATE_LIMITS.verifiedFacts);
185
+ for (const [key, outcome] of Object.entries(engine.testedApproaches)) {
186
+ this.draft.testedApproaches[key] = outcome;
187
+ }
188
+ if (this.draft.task === "") this.draft.task = engine.task;
189
+ this.touch();
190
+ }
191
+
192
+ /**
193
+ * WS-4.2 (default OFF): model-proposed ΔΣ_t fences through the ONE validator. EXACT ladder
194
+ * parity with the engine's `applyStatePatches` (N3): every fence is processed — accepted
195
+ * deltas land sequentially, ALL problems accumulate into ONE observation (error-as-
196
+ * observation), and only past `runStateRetryMax` total rejections the tracker degrades and
197
+ * fences stop being applied. Wording delegates to run-state.ts (N1) — one source.
198
+ *
199
+ * R4 (G6, /tmp/ROOT_FULL_SKILLSTATE_PLAN.md): idle-degrade parity with the engine — once
200
+ * the native prompt teaches the fence contract, EVERY finalized assistant turn is
201
+ * fence-eligible; a turn with zero accepted deltas grows `idleFenceTurns` and
202
+ * `ROOT_IDLE_DEGRADE_TURNS` consecutive idle turns degrade the tracker (an idle Σ is
203
+ * pure input tax — bench rec #2, paper §5.7; root threshold is 6, not the engine's 4 —
204
+ * see the const's soak citation). Any accepted delta resets the streak. In
205
+ * degraded mode fences stop applying and the context transform stops splicing (`isActive`),
206
+ * while runtime `observeToolResult` remains the Σ floor (degrade, never crash).
207
+ */
208
+ applyFences(fences: readonly StateFenceResult[]): FenceOutcome {
209
+ if (this.mode.kind !== "active") return { fences: fences.length, accepted: 0, problems: 0 };
210
+ if (fences.length === 0) {
211
+ // R4 (G6): a fence-free turn on a conditioned loop is IDLE — the contract rode the
212
+ // prompt for nothing. Grow the streak; degrade at the engine's threshold.
213
+ this.idleFenceTurns += 1;
214
+ this.degradeIfIdle();
215
+ return { fences: 0, accepted: 0, problems: 0 };
216
+ }
217
+ let state = this.snapshot();
218
+ const problems: string[] = [];
219
+ let accepted = 0;
220
+ for (const fence of fences) {
221
+ if (!fence.ok) {
222
+ problems.push(malformedFenceProblem(fence.error));
223
+ continue;
224
+ }
225
+ const next: Result<RunState, PatchError> = applyPatch(state, fence.value, ++this.opCounter);
226
+ if (next.ok) {
227
+ state = next.value;
228
+ accepted += 1;
229
+ } else {
230
+ problems.push(patchErrorText(next.error));
231
+ }
232
+ }
233
+ // Accepted deltas reset the idle streak — even in a partially-failing batch (engine
234
+ // parity: real work is never punished for a sibling's malformed fence).
235
+ this.idleFenceTurns = accepted > 0 ? 0 : this.idleFenceTurns + 1;
236
+ this.degradeIfIdle();
237
+ if (problems.length === 0) {
238
+ this.mode = { kind: "active", retries: 0 };
239
+ this.pendingObservation = undefined;
240
+ this.draft = this.toMutable(state);
241
+ this.touch();
242
+ return { fences: fences.length, accepted, problems: 0 };
243
+ }
244
+ const retries = this.mode.retries + problems.length;
245
+ this.pendingObservation = statePatchObservation(problems);
246
+ // Accepted deltas in a partially-failing batch still land — engine parity: real work is
247
+ // never rolled back just because a sibling fence was malformed.
248
+ this.draft = this.toMutable(state);
249
+ this.touch();
250
+ if (retries > this.retryMax) {
251
+ this.mode = { kind: "degraded", reason: `state-patch retry cap exceeded (${retries} rejected)` };
252
+ } else if (this.mode.kind === "active") {
253
+ // An idle degrade fired earlier in this call wins over re-activating — degrade is
254
+ // sticky; the runtime observation floor keeps Σ alive until the session ends.
255
+ this.mode = { kind: "active", retries };
256
+ }
257
+ return { fences: fences.length, accepted, problems: problems.length };
258
+ }
259
+
260
+ /** R4: fire the idle degrade at the root threshold (active trackers only). */
261
+ private degradeIfIdle(): void {
262
+ if (this.mode.kind === "active" && this.idleFenceTurns >= ROOT_IDLE_DEGRADE_TURNS) {
263
+ this.mode = {
264
+ kind: "degraded",
265
+ reason: `idle degrade — ${this.idleFenceTurns} consecutive turns with zero accepted deltas`,
266
+ };
267
+ }
268
+ }
269
+
270
+ /** Consume (and clear) the pending fence-rejection observation, if any. */
271
+ takePendingObservation(): string | undefined {
272
+ const observation = this.pendingObservation;
273
+ this.pendingObservation = undefined;
274
+ return observation;
275
+ }
276
+
277
+ /**
278
+ * WS-4 rectify parity (budget.ts:rectify ideas, deterministic): after N consecutive
279
+ * failed outcomes on one key, suggest the local fix — never a model switch.
280
+ */
281
+ rectifyHint(): string | undefined {
282
+ if (this.mode.kind !== "active") return undefined;
283
+ for (const [key, count] of this.failures) {
284
+ if (count >= RECTIFY_FAILURE_THRESHOLD) {
285
+ return `[rectify] '${key}' failed ${count}× consecutively — narrow the approach ` +
286
+ "(different path/tool/slice) instead of retrying blind (MAS2 Eq. 5).";
287
+ }
288
+ }
289
+ return undefined;
290
+ }
291
+
292
+ private toMutable(state: RunState): MutableState {
293
+ return {
294
+ task: state.task,
295
+ nextStep: state.nextStep,
296
+ updatedAt: state.updatedAt,
297
+ findings: [...state.findings],
298
+ verifiedFacts: [...state.verifiedFacts],
299
+ openQuestions: [...state.openQuestions],
300
+ testedApproaches: { ...state.testedApproaches },
301
+ artifacts: { ...state.artifacts },
302
+ };
303
+ }
304
+
305
+ private touch(): void {
306
+ this.opCounter += 1;
307
+ this.draft.updatedAt = this.opCounter;
308
+ this.dirtyFlag = true;
309
+ }
310
+ }