@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,97 @@
1
+ /**
2
+ * Host-side Okapi BM25 (Workstreams C/D/E ranking).
3
+ *
4
+ * BM25 DUALITY, documented not accidental: this file and `sandbox/py/retrieval.py`
5
+ * (`_Bm25Index`) implement the SAME scoring for two runtimes — identical constants, identical
6
+ * tokenizer, identical idf/norm formulas — so a note ranked here lands in the same order the
7
+ * sandbox-side `search` would put it. Keep the two files in lockstep (AGENTS.md convention).
8
+ */
9
+
10
+ const BM25_K1 = 1.2; // mirrors retrieval.py:_BM25_K1
11
+ const BM25_B = 0.75; // mirrors retrieval.py:_BM25_B
12
+ // Mirrors retrieval.py:_TOKEN_SPLIT / _CAMEL_SPLIT: lowercased alphanumeric runs plus
13
+ // camelCase parts, so `resolveModelId`-style identifiers match their prose spellings.
14
+ const TOKEN_SPLIT = /[^0-9A-Za-z]+/;
15
+ const CAMEL_SPLIT = /(?<=[a-z0-9])(?=[A-Z])/;
16
+
17
+ export function bm25Tokenize(text: string): readonly string[] {
18
+ const out: string[] = [];
19
+ for (const raw of text.split(TOKEN_SPLIT)) {
20
+ if (raw === "") continue;
21
+ const lowered = raw.toLowerCase();
22
+ out.push(lowered);
23
+ if (raw.length > 3) {
24
+ const parts = raw.split(CAMEL_SPLIT);
25
+ if (parts.length > 1) {
26
+ for (const part of parts) {
27
+ const piece = part.toLowerCase();
28
+ if (piece !== "" && piece !== lowered) out.push(piece);
29
+ }
30
+ }
31
+ }
32
+ }
33
+ return out;
34
+ }
35
+
36
+ export interface Bm25Entry<T> {
37
+ readonly item: T;
38
+ readonly text: string;
39
+ }
40
+
41
+ export interface Bm25Hit<T> {
42
+ readonly item: T;
43
+ readonly score: number;
44
+ }
45
+
46
+ /**
47
+ * Rank entries against a query, best first, top-k, score > 0 only.
48
+ * Pre-allocated score/index arrays; no growth in the scoring loops.
49
+ */
50
+ export function bm25Rank<T>(
51
+ query: string,
52
+ entries: readonly Bm25Entry<T>[],
53
+ k: number,
54
+ ): readonly Bm25Hit<T>[] {
55
+ const n = entries.length;
56
+ if (n === 0 || k <= 0) return [];
57
+ const docLens = new Array<number>(n);
58
+ const postings = new Map<string, Array<readonly [number, number]>>();
59
+ for (let i = 0; i < n; i++) {
60
+ const terms = bm25Tokenize(entries[i].text);
61
+ docLens[i] = terms.length;
62
+ const freq = new Map<string, number>();
63
+ for (const term of terms) freq.set(term, (freq.get(term) ?? 0) + 1);
64
+ for (const [term, tf] of freq) {
65
+ const list = postings.get(term);
66
+ if (list !== undefined) list.push([i, tf]);
67
+ else postings.set(term, [[i, tf]]);
68
+ }
69
+ }
70
+ let totalLen = 0;
71
+ for (let i = 0; i < n; i++) totalLen += docLens[i];
72
+ const avgLen = totalLen > 0 ? totalLen / n : 1.0;
73
+
74
+ const scores = new Array<number>(n).fill(0);
75
+ const seen = new Set<string>();
76
+ for (const term of bm25Tokenize(query)) {
77
+ if (seen.has(term)) continue; // Python twin scores set(terms)
78
+ seen.add(term);
79
+ const posting = postings.get(term);
80
+ if (posting === undefined) continue;
81
+ const df = posting.length;
82
+ const idf = Math.log(1.0 + (n - df + 0.5) / (df + 0.5));
83
+ for (const [idx, tf] of posting) {
84
+ const norm = BM25_K1 * (1.0 - BM25_B + BM25_B * (docLens[idx] / avgLen));
85
+ scores[idx] += (idf * (tf * (BM25_K1 + 1.0))) / (tf + norm);
86
+ }
87
+ }
88
+
89
+ const order = Array.from({ length: n }, (_, i) => i);
90
+ order.sort((a, b) => scores[b] - scores[a] || a - b); // deterministic tie-break
91
+ const out: Bm25Hit<T>[] = [];
92
+ for (let i = 0; i < n && out.length < k; i++) {
93
+ const idx = order[i];
94
+ if (scores[idx] > 0) out.push({ item: entries[idx].item, score: scores[idx] });
95
+ }
96
+ return out;
97
+ }
@@ -93,7 +93,7 @@ export function createSubcallGates(leafLimit: number, childLimit: number = leafL
93
93
  }
94
94
 
95
95
  /** Provider-capped config slice (RlmConfig satisfies this structurally). */
96
- export interface ProviderCapConfig {
96
+ interface ProviderCapConfig {
97
97
  readonly maxConcurrentSubcalls: number;
98
98
  readonly maxConcurrentChildren: number;
99
99
  /** v5: per-provider concurrent-request caps, e.g. { zai: 4 }. Caps only ever LOWER a limit. */
@@ -12,7 +12,7 @@ export function err<T = never, E = string>(error: E): Result<T, E> {
12
12
  return { ok: false, error };
13
13
  }
14
14
 
15
- export const ERROR_PREFIX = "Error:";
15
+ const ERROR_PREFIX = "Error:";
16
16
 
17
17
  export function formatError(message: string): string {
18
18
  return `${ERROR_PREFIX} ${message}`;
package/src/util/retry.ts CHANGED
@@ -95,8 +95,10 @@ export interface RetryPolicy {
95
95
  }
96
96
 
97
97
  export const DEFAULT_RETRY_POLICY: Readonly<RetryPolicy> = Object.freeze({
98
- maxAttempts: 3,
99
- rateLimitMaxAttempts: 8,
98
+ // DOCTRINE: 15 attempts on the SAME model, then the call fails loudly. No cross-model
99
+ // or cross-provider fallback exists anywhere in the runtime — by design.
100
+ maxAttempts: 15,
101
+ rateLimitMaxAttempts: 15,
100
102
  baseDelayMs: 500,
101
103
  maxDelayMs: 15_000,
102
104
  throttleBaseMs: 2_000,
@@ -104,7 +106,7 @@ export const DEFAULT_RETRY_POLICY: Readonly<RetryPolicy> = Object.freeze({
104
106
  });
105
107
 
106
108
  /** Shape of the optional retry knobs on RlmConfig — kept structural to avoid a cycle. */
107
- export interface RetryConfigNumbers {
109
+ interface RetryConfigNumbers {
108
110
  readonly retryMaxAttempts?: number;
109
111
  readonly rateLimitMaxAttempts?: number;
110
112
  readonly retryBaseDelayMs?: number;
@@ -142,7 +144,16 @@ export async function completeWithRetry<T>(
142
144
  readonly onRelease?: () => void;
143
145
  },
144
146
  ): Promise<T> {
145
- const { policy, provider, signal, onPark, onRelease } = opts;
147
+ const { policy, provider, signal } = opts;
148
+ // Bench rec #5: a silent park is indistinguishable from a hang (the campaign watched 15
149
+ // attempts × 15 cooldown windows with zero output). Callers may own observability via
150
+ // opts.onPark; otherwise one [rlm]-prefixed warn fires per park — fail-soft, never throws.
151
+ const onPark =
152
+ opts.onPark ??
153
+ ((ms: number): void => {
154
+ console.warn(`[rlm] ${provider} parked ${Math.round(ms)}ms on provider cooldown`);
155
+ });
156
+ const { onRelease } = opts;
146
157
  const cooldown = policy.cooldown ?? sharedCooldown;
147
158
  let status: number | undefined;
148
159
  let headers: Record<string, string> | undefined;
@@ -174,10 +185,14 @@ export async function completeWithRetry<T>(
174
185
  }
175
186
  if (tries + 1 >= policy.maxAttempts) throw err;
176
187
  if (!retryableError(status, msg)) throw err;
177
- await sleepMs(
178
- Math.min(retryAfterMs(headers) ?? backoffMs(tries, policy.baseDelayMs, policy.maxDelayMs), policy.maxDelayMs),
179
- signal,
188
+ const delay = Math.min(
189
+ retryAfterMs(headers) ?? backoffMs(tries, policy.baseDelayMs, policy.maxDelayMs),
190
+ policy.maxDelayMs,
180
191
  );
192
+ console.warn(
193
+ `[rlm] ${provider} attempt ${tries + 1}/${policy.maxAttempts} failed (${msg.slice(0, 140)}) — retrying in ${delay}ms`,
194
+ );
195
+ await sleepMs(delay, signal);
181
196
  }
182
197
  }
183
198
  }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * THE deep-merge with null-deletion (SKILL.state paper §3.2, the `⊕` operator) — one
3
+ * implementation shared by the RunState patch path (core/run-state.ts, Workstream A) and the
4
+ * SkillState note merge (config/skillstate.ts, Workstream B). Never inline a second merge.
5
+ *
6
+ * x ⊕ {k: null} = x without k (delete — must be explicit, never silent)
7
+ * x ⊕ {k: v} = x[k] ⊕ v if both are plain objects (deep merge)
8
+ * else v (replace)
9
+ *
10
+ * Arrays are replaced wholesale: array semantics (append, index-set, dedup, eviction) are
11
+ * domain policy and live with the callers that understand them (see core/run-state.ts).
12
+ * A non-object patch replaces the base as-is.
13
+ */
14
+
15
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+
19
+ export function deepMergeWithNullDeletion(base: unknown, patch: unknown): unknown {
20
+ if (!isPlainObject(base) || !isPlainObject(patch)) return patch;
21
+ const out: Record<string, unknown> = { ...base };
22
+ for (const [key, value] of Object.entries(patch)) {
23
+ if (value === null) {
24
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- ⊕ explicit null = delete (paper §3.2 +)
25
+ delete out[key]; // explicit null = delete (paper §3.2)
26
+ continue;
27
+ }
28
+ const current = out[key];
29
+ out[key] = isPlainObject(current) && isPlainObject(value)
30
+ ? deepMergeWithNullDeletion(current, value)
31
+ : value;
32
+ }
33
+ return out;
34
+ }
@@ -81,7 +81,7 @@ export class ProviderCooldown {
81
81
  }
82
82
 
83
83
  /** Mirrored into DEFAULT_RETRY_POLICY (util/retry.ts imports these — keep one-way). */
84
- export const THROTTLE_DEFAULTS = Object.freeze({ baseMs: 2_000, maxMs: 60_000 } as const);
84
+ const THROTTLE_DEFAULTS = Object.freeze({ baseMs: 2_000, maxMs: 60_000 } as const);
85
85
 
86
86
  /** Process-wide instance: every completion in this pi session shares it. */
87
87
  export const sharedCooldown: ProviderCooldown = new ProviderCooldown(
@@ -0,0 +1,6 @@
1
+ /** Shared runtime type guards — one implementation, imported everywhere (DRY). */
2
+
3
+ /** True for any non-null object value; the standard probe before field checks. */
4
+ export function isRecord(value: unknown): value is Record<string, unknown> {
5
+ return typeof value === "object" && value !== null;
6
+ }