@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.
- package/README.md +0 -4
- package/README.ru.md +56 -66
- package/README.zh-CN.md +61 -65
- package/package.json +5 -5
- package/src/bridge/add-context.ts +1 -1
- package/src/bridge/handlers/await.ts +13 -22
- package/src/bridge/handlers/completion.ts +27 -5
- package/src/bridge/handlers/emitting.ts +2 -2
- package/src/bridge/handlers/llm-query.ts +46 -68
- package/src/bridge/handlers/rlm-query.ts +14 -84
- package/src/bridge/handlers/task-registry.ts +22 -17
- package/src/bridge/handlers/types.ts +8 -6
- package/src/bridge/model.ts +6 -3
- package/src/commands/rlm-llm.ts +1 -10
- package/src/commands/rlm-rlm.ts +1 -8
- package/src/config/defaults.ts +32 -12
- package/src/config/settings.ts +53 -31
- package/src/config/skillstate.ts +465 -0
- package/src/context/md-cache.ts +1 -1
- package/src/context/merge.ts +1 -1
- package/src/context/namespace.ts +2 -2
- package/src/context/refresh.ts +1 -1
- package/src/context/source-dir.ts +21 -11
- package/src/context/source-doc.ts +1 -1
- package/src/context/source-git.ts +3 -15
- package/src/context/source-text.ts +1 -1
- package/src/context/walk.ts +6 -14
- package/src/core/budget.ts +107 -21
- package/src/core/compaction.ts +44 -1
- package/src/core/engine.ts +141 -84
- package/src/core/iteration.ts +1 -1
- package/src/core/ledger.ts +10 -13
- package/src/core/limits.ts +1 -1
- package/src/core/model-registry.ts +1 -1
- package/src/core/resource-limits.ts +1 -1
- package/src/core/root-context.ts +184 -0
- package/src/core/root-digest.ts +213 -0
- package/src/core/root-state.ts +310 -0
- package/src/core/run-state.ts +587 -0
- package/src/core/types.ts +51 -12
- package/src/index.ts +220 -39
- package/src/mode/llm-model.ts +13 -1
- package/src/mode/native-guards.ts +0 -6
- package/src/mode/rlm-mode.ts +34 -11
- package/src/mode/subagent.ts +5 -5
- package/src/prompts/glossary.ts +46 -25
- package/src/prompts/native.ts +27 -5
- package/src/prompts/system.ts +12 -4
- package/src/sandbox/context-file.ts +1 -1
- package/src/sandbox/interrupts.ts +25 -31
- package/src/sandbox/protocol.ts +14 -20
- package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/worker.cpython-314.pyc +0 -0
- package/src/sandbox/py/guards.py +1 -1
- package/src/sandbox/py/scaffold.py +24 -31
- package/src/sandbox/py/worker.py +3 -1
- package/src/sandbox/sandbox-manager.ts +2 -2
- package/src/sandbox/sandbox.ts +21 -4
- package/src/text/agent-text.ts +58 -0
- package/src/text/parsing.ts +35 -3
- package/src/text/preview.ts +3 -0
- package/src/text/repl-output.ts +1 -1
- package/src/tool/background-tasks.ts +1 -1
- package/src/tool/repl-render.ts +1 -1
- package/src/tool/repl-result.ts +1 -1
- package/src/tool/repl-tool.ts +50 -26
- package/src/tool/rlm-tool.ts +4 -5
- package/src/tool/subcall-render.ts +1 -1
- package/src/tool/subcall-store.ts +2 -2
- package/src/tool/tool-utils.ts +5 -5
- package/src/ui/config-panel.ts +12 -0
- package/src/ui/intro.ts +1 -1
- package/src/ui/modal/timeline-store.ts +1 -1
- package/src/ui/model-picker/drilldown.ts +1 -1
- package/src/ui/model-picker/levels.ts +1 -1
- package/src/ui/panel/run-registry.ts +1 -1
- package/src/ui/status.ts +39 -4
- package/src/ui/tree/tree-rows.ts +1 -1
- package/src/ui/tree/tree-widget.ts +1 -1
- package/src/util/bm25.ts +97 -0
- package/src/util/concurrency.ts +1 -1
- package/src/util/errors.ts +1 -1
- package/src/util/retry.ts +22 -7
- package/src/util/state-merge.ts +34 -0
- package/src/util/throttle.ts +1 -1
- package/src/util/type-guards.ts +6 -0
- package/src/core/memory.ts +0 -589
package/src/util/bm25.ts
ADDED
|
@@ -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
|
+
}
|
package/src/util/concurrency.ts
CHANGED
|
@@ -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
|
-
|
|
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. */
|
package/src/util/errors.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
}
|
package/src/util/throttle.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|