@hicaru/pi-rlm 0.3.15 → 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 (90) hide show
  1. package/README.md +96 -70
  2. package/README.ru.md +86 -59
  3. package/README.zh-CN.md +95 -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 +21 -4
  14. package/src/commands/rlm-llm.ts +1 -10
  15. package/src/commands/rlm-rlm.ts +1 -8
  16. package/src/config/defaults.ts +31 -12
  17. package/src/config/settings.ts +47 -33
  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 +192 -94
  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 +56 -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/prompts/user.ts +17 -0
  50. package/src/sandbox/context-file.ts +1 -1
  51. package/src/sandbox/interrupts.ts +25 -31
  52. package/src/sandbox/protocol.ts +14 -20
  53. package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
  54. package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
  55. package/src/sandbox/py/__pycache__/scaffold.cpython-314.pyc +0 -0
  56. package/src/sandbox/py/__pycache__/worker.cpython-314.pyc +0 -0
  57. package/src/sandbox/py/guards.py +1 -1
  58. package/src/sandbox/py/retrieval.py +4 -1
  59. package/src/sandbox/py/scaffold.py +24 -31
  60. package/src/sandbox/py/worker.py +3 -1
  61. package/src/sandbox/sandbox-manager.ts +2 -2
  62. package/src/sandbox/sandbox.ts +35 -5
  63. package/src/text/agent-text.ts +58 -0
  64. package/src/text/parsing.ts +35 -3
  65. package/src/text/preview.ts +3 -0
  66. package/src/text/repl-output.ts +1 -1
  67. package/src/tool/background-tasks.ts +1 -1
  68. package/src/tool/repl-render.ts +1 -1
  69. package/src/tool/repl-result.ts +1 -1
  70. package/src/tool/repl-tool.ts +50 -26
  71. package/src/tool/rlm-tool.ts +4 -5
  72. package/src/tool/subcall-render.ts +1 -1
  73. package/src/tool/subcall-store.ts +2 -2
  74. package/src/tool/tool-utils.ts +5 -5
  75. package/src/ui/config-panel.ts +39 -0
  76. package/src/ui/intro.ts +1 -1
  77. package/src/ui/modal/timeline-store.ts +1 -1
  78. package/src/ui/model-picker/drilldown.ts +1 -1
  79. package/src/ui/model-picker/levels.ts +1 -1
  80. package/src/ui/panel/run-registry.ts +1 -1
  81. package/src/ui/tree/tree-rows.ts +1 -1
  82. package/src/ui/tree/tree-widget.ts +1 -1
  83. package/src/util/bm25.ts +97 -0
  84. package/src/util/concurrency.ts +1 -1
  85. package/src/util/errors.ts +1 -1
  86. package/src/util/retry.ts +22 -7
  87. package/src/util/state-merge.ts +34 -0
  88. package/src/util/throttle.ts +1 -1
  89. package/src/util/type-guards.ts +6 -0
  90. package/src/core/memory.ts +0 -589
@@ -9,11 +9,12 @@
9
9
  * RLM root (full history).
10
10
  */
11
11
 
12
+ import { clampThinkingLevel } from "@earendil-works/pi-ai";
12
13
  import { type Api, completeSimple, type Message, type Model, type ThinkingLevel, type Usage } from "@earendil-works/pi-ai/compat";
13
14
  import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
14
15
  import { completeWithRetry, DEFAULT_RETRY_POLICY, type RetryPolicy } from "../util/retry.ts";
15
16
 
16
- export type Role = "system" | "user" | "assistant";
17
+ type Role = "system" | "user" | "assistant";
17
18
  export interface ChatMsg {
18
19
  readonly role: Role;
19
20
  readonly content: string;
@@ -54,20 +55,36 @@ function assistantMessage(text: string, model: Model<Api>): Message {
54
55
  }
55
56
 
56
57
  function toPiMessages(messages: readonly ChatMsg[], model: Model<Api>): { readonly systemPrompt?: string; readonly messages: Message[] } {
57
- 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[] = [];
58
61
  const out: Message[] = [];
59
62
  for (const m of messages) {
60
63
  if (m.role === "system") {
61
- systemPrompt = systemPrompt ? `${systemPrompt}\n\n${m.content}` : m.content;
64
+ systemParts.push(m.content);
62
65
  } else if (m.role === "user") {
63
66
  out.push({ role: "user", content: m.content, timestamp: Date.now() });
64
67
  } else {
65
68
  out.push(assistantMessage(m.content, model));
66
69
  }
67
70
  }
71
+ const systemPrompt = systemParts.length > 0 ? systemParts.join("\n\n") : undefined;
68
72
  return { systemPrompt, messages: out };
69
73
  }
70
74
 
75
+ /**
76
+ * Explicit reasoning gate. pi-ai clamps unsupported levels too (a model with `reasoning:
77
+ * false` supports only "off", so any requested level clamps to it and nothing goes out on
78
+ * the wire) — owning the gate here makes the behavior engine policy, testable without a
79
+ * provider, and drops the option outright instead of forwarding a level the model cannot
80
+ * honor. Capability comes from the registry entry, never from config.
81
+ */
82
+ export function effectiveReasoning(model: Model<Api>, level: ThinkingLevel | undefined): ThinkingLevel | undefined {
83
+ if (level === undefined) return undefined;
84
+ const clamped = clampThinkingLevel(model, level);
85
+ return clamped === "off" ? undefined : clamped;
86
+ }
87
+
71
88
  /** Extract the assistant's plain text from a completion. */
72
89
  function extractText(content: readonly { readonly type: string; readonly text?: string }[]): string {
73
90
  return content
@@ -97,7 +114,7 @@ export async function modelComplete(messages: readonly ChatMsg[], opts: Complete
97
114
  headers: auth.headers,
98
115
  maxTokens: opts.maxTokens,
99
116
  temperature: opts.temperature,
100
- reasoning: opts.reasoning,
117
+ reasoning: effectiveReasoning(opts.model, opts.reasoning),
101
118
  signal: opts.signal,
102
119
  onResponse: (res) => { note(res.status, res.headers); },
103
120
  },
@@ -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,11 +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",
55
+ // Verification-discipline nudge — deliberately OFF (plan guardrail): when on, an early
56
+ // bare-number finalize gets one coached redo instead of being accepted. Opt-in via rlm.json.
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,
61
80
  });
@@ -7,7 +7,7 @@ import type { Api, Model, ThinkingLevel } from "@earendil-works/pi-ai";
7
7
  import type { RlmConfig } from "../core/types.ts";
8
8
  import { DEFAULT_CONFIG } from "./defaults.ts";
9
9
 
10
- export interface PersistedSettings {
10
+ interface PersistedSettings {
11
11
  readonly config: Partial<RlmConfig>;
12
12
  /** "provider/id" of the pinned sub-LLM, or undefined for "cheapest (auto)".
13
13
  * `null` = explicit "cheapest" clear (omit key on disk). */
@@ -38,9 +38,10 @@ function validateString(v: unknown): string | undefined {
38
38
  /**
39
39
  * Every value pi-ai accepts for `reasoning`. Keyed by the union so a new level added upstream
40
40
  * is a compile error here rather than a silently-rejected setting. Note `off` is NOT a
41
- * ThinkingLevel — a hand-edited rlm.json carrying one is dropped, not forwarded.
41
+ * ThinkingLevel — a hand-edited rlm.json carrying one is dropped, not forwarded. Exported so
42
+ * the config panel derives its choices from the same source of truth.
42
43
  */
43
- const THINKING_LEVELS: Readonly<Record<ThinkingLevel, true>> = Object.freeze({
44
+ export const THINKING_LEVELS: Readonly<Record<ThinkingLevel, true>> = Object.freeze({
44
45
  minimal: true, low: true, medium: true, high: true, xhigh: true, max: true,
45
46
  });
46
47
 
@@ -103,8 +104,7 @@ export function validateConfig(raw: unknown): Partial<RlmConfig> {
103
104
  if (subSystemPrompt !== undefined) out.subSystemPrompt = subSystemPrompt;
104
105
  const sandboxInitTimeoutMs = validateNumber(r.sandboxInitTimeoutMs, 100);
105
106
  if (sandboxInitTimeoutMs !== undefined) out.sandboxInitTimeoutMs = sandboxInitTimeoutMs;
106
- // `libraryLoader` is the pre-rename key — still read so an existing rlm.json survives the upgrade.
107
- const contextLoader = validateBoolean(r.contextLoader) ?? validateBoolean(r.libraryLoader);
107
+ const contextLoader = validateBoolean(r.contextLoader);
108
108
  if (contextLoader !== undefined) out.contextLoader = contextLoader;
109
109
  const autoSeedCwd = validateBoolean(r.autoSeedCwd);
110
110
  if (autoSeedCwd !== undefined) out.autoSeedCwd = autoSeedCwd;
@@ -126,18 +126,6 @@ export function validateConfig(raw: unknown): Partial<RlmConfig> {
126
126
  if (enableLedger !== undefined) out.enableLedger = enableLedger;
127
127
  const rlmBudget = validateNumber(r.rlmBudget, 0);
128
128
  if (rlmBudget !== undefined) out.rlmBudget = rlmBudget;
129
- // v5 durable memory
130
- const enableMemory = validateBoolean(r.enableMemory);
131
- if (enableMemory !== undefined) out.enableMemory = enableMemory;
132
- const injectNoteTokens = validateNumber(r.injectNoteTokens, 100);
133
- if (injectNoteTokens !== undefined) out.injectNoteTokens = injectNoteTokens;
134
- const evolveEvery = validateNumber(r.evolveEvery, 0);
135
- if (evolveEvery !== undefined) out.evolveEvery = evolveEvery;
136
- if (r.memoryDir === null) out.memoryDir = null;
137
- else {
138
- const memoryDir = validateString(r.memoryDir);
139
- if (memoryDir !== undefined) out.memoryDir = memoryDir;
140
- }
141
129
  // v5 provider concurrency caps: { provider: minConcurrent }
142
130
  if (typeof r.providerMaxConcurrent === "object" && r.providerMaxConcurrent !== null) {
143
131
  const caps: Record<string, number> = {};
@@ -147,8 +135,43 @@ export function validateConfig(raw: unknown): Partial<RlmConfig> {
147
135
  }
148
136
  if (Object.keys(caps).length > 0) out.providerMaxConcurrent = Object.freeze(caps);
149
137
  }
150
- // v5 child surface doctrine
151
- if (r.childSurface === "delegation" || r.childSurface === "legacy") out.childSurface = r.childSurface;
138
+ // Verification-discipline nudge (default OFF).
139
+ const enableVerificationNudge = validateBoolean(r.enableVerificationNudge);
140
+ if (enableVerificationNudge !== undefined) out.enableVerificationNudge = enableVerificationNudge;
141
+ // SKILL.state integration (Workstreams A–F)
142
+ const enableRunState = validateBoolean(r.enableRunState);
143
+ if (enableRunState !== undefined) out.enableRunState = enableRunState;
144
+ const runStateRetryMax = validateNumber(r.runStateRetryMax, 0);
145
+ if (runStateRetryMax !== undefined) out.runStateRetryMax = runStateRetryMax;
146
+ const enableSkillState = validateBoolean(r.enableSkillState);
147
+ if (enableSkillState !== undefined) out.enableSkillState = enableSkillState;
148
+ const enableSkillStateDistill = validateBoolean(r.enableSkillStateDistill);
149
+ if (enableSkillStateDistill !== undefined) out.enableSkillStateDistill = enableSkillStateDistill;
150
+ const skillStateMaxTokens = validateNumber(r.skillStateMaxTokens, 50);
151
+ if (skillStateMaxTokens !== undefined) out.skillStateMaxTokens = skillStateMaxTokens;
152
+ const skillStateLeafTokens = validateNumber(r.skillStateLeafTokens, 0);
153
+ if (skillStateLeafTokens !== undefined) out.skillStateLeafTokens = skillStateLeafTokens;
154
+ const skillStateMinScore = validateNumber(r.skillStateMinScore, 0);
155
+ if (skillStateMinScore !== undefined) out.skillStateMinScore = skillStateMinScore;
156
+ const skillStateNotesPerProject = validateNumber(r.skillStateNotesPerProject, 1);
157
+ if (skillStateNotesPerProject !== undefined) out.skillStateNotesPerProject = skillStateNotesPerProject;
158
+ // Root Σ integration (WS-2..WS-4)
159
+ const enableRootDigestCompaction = validateBoolean(r.enableRootDigestCompaction);
160
+ if (enableRootDigestCompaction !== undefined) out.enableRootDigestCompaction = enableRootDigestCompaction;
161
+ const rootDigestKeepRecentChars = validateNumber(r.rootDigestKeepRecentChars, 200);
162
+ if (rootDigestKeepRecentChars !== undefined) out.rootDigestKeepRecentChars = rootDigestKeepRecentChars;
163
+ const rootDigestMaxChars = validateNumber(r.rootDigestMaxChars, 200);
164
+ if (rootDigestMaxChars !== undefined) out.rootDigestMaxChars = rootDigestMaxChars;
165
+ const enableRootContextTransform = validateBoolean(r.enableRootContextTransform);
166
+ if (enableRootContextTransform !== undefined) out.enableRootContextTransform = enableRootContextTransform;
167
+ const rootContextKeepTurns = validateNumber(r.rootContextKeepTurns, 0);
168
+ if (rootContextKeepTurns !== undefined) out.rootContextKeepTurns = rootContextKeepTurns;
169
+ const rootContextElideChars = validateNumber(r.rootContextElideChars, 100);
170
+ if (rootContextElideChars !== undefined) out.rootContextElideChars = rootContextElideChars;
171
+ const rootContextSnapshot = validateBoolean(r.rootContextSnapshot);
172
+ if (rootContextSnapshot !== undefined) out.rootContextSnapshot = rootContextSnapshot;
173
+ const enableRootStateFences = validateBoolean(r.enableRootStateFences);
174
+ if (enableRootStateFences !== undefined) out.enableRootStateFences = enableRootStateFences;
152
175
  if (typeof r.subSampling === "object" && r.subSampling !== null) {
153
176
  const ss = r.subSampling as Record<string, unknown>;
154
177
  const sampling: { maxTokens?: number; temperature?: number; reasoning?: ThinkingLevel } = {};
@@ -181,8 +204,7 @@ export async function loadSettings(): Promise<PersistedSettings> {
181
204
  const r = raw as Record<string, unknown>;
182
205
  return {
183
206
  config: validateConfig(r.config),
184
- // `worker` is the pre-rename key — still read so an existing pin survives the upgrade.
185
- llm: validateString(r.llm) ?? validateString(r.worker),
207
+ llm: validateString(r.llm),
186
208
  rlm: validateString(r.rlm),
187
209
  };
188
210
  } catch {
@@ -238,16 +260,8 @@ export function modelRef(model: Model<Api> | undefined): string | undefined {
238
260
  return model ? `${model.provider}/${model.id}` : undefined;
239
261
  }
240
262
 
241
- /**
242
- * Human-readable "provider/id" for a sub-call node: the resolved override when one was
243
- * supplied and resolves, otherwise the fallback model. Shared by the llm and rlm bridges
244
- * so sub-call trees label their nodes identically.
245
- */
246
- export function displayModelRef(
247
- registry: ModelRegistry,
248
- override: string | null,
249
- fallback: Model<Api>,
250
- ): string {
251
- const resolved = override ? (resolveModelId(registry, override) ?? fallback) : fallback;
252
- return modelRef(resolved) ?? fallback.id;
263
+ /** Single "provider/id" projection for sub-call node labels — shared by the llm and rlm
264
+ * bridges so sub-call trees label their nodes identically. */
265
+ export function modelLabelOf(model: Model<Api>): string {
266
+ return modelRef(model) ?? model.id;
253
267
  }