@hicaru/pi-rlm 0.3.20 → 0.3.21
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/package.json +1 -1
- package/src/commands/rlm.ts +14 -7
- package/src/config/defaults.ts +33 -10
- package/src/config/settings.ts +6 -0
- package/src/config/skillstate.ts +236 -44
- package/src/core/budget.ts +7 -3
- package/src/core/compaction.ts +2 -2
- package/src/core/engine.ts +87 -19
- package/src/core/root-context.ts +74 -21
- package/src/core/root-digest.ts +48 -11
- package/src/core/root-state.ts +39 -12
- package/src/core/run-state.ts +86 -14
- package/src/core/session-archive.ts +174 -0
- package/src/core/types.ts +6 -0
- package/src/index.ts +142 -12
- package/src/mode/rlm-mode.ts +2 -2
- package/src/prompts/glossary.ts +34 -5
- package/src/prompts/native.ts +8 -2
- package/src/prompts/user.ts +4 -3
- package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/src/sandbox/py/retrieval.py +202 -36
- package/src/sandbox/py/scaffold.py +20 -5
- package/src/sandbox/py/worker.py +1 -1
- package/src/sandbox/sandbox-manager.ts +19 -0
- package/src/text/parsing.ts +133 -2
- package/src/text/tokens.ts +39 -4
- package/src/tool/repl-render.ts +38 -2
- package/src/tool/repl-tool.ts +34 -18
- package/src/tool/subcall-render.ts +7 -4
- package/src/ui/config-panel.ts +2 -2
- package/src/ui/intro.ts +1 -1
- package/src/ui/python-highlight.ts +49 -0
- package/src/ui/stage-cards.ts +192 -0
- package/src/ui/tree/tree-model.ts +69 -19
- package/src/ui/tree/tree-rows.ts +2 -1
- package/src/util/abort.ts +34 -0
- package/src/util/bm25.ts +170 -21
- package/src/util/errors.ts +1 -1
package/src/index.ts
CHANGED
|
@@ -29,11 +29,18 @@ import type { AddContextHandlerBundle } from "./bridge/add-context.ts";
|
|
|
29
29
|
import { buildNativeSystemPrompt } from "./prompts/native.ts";
|
|
30
30
|
import { SkillStore, notesFromRunState, xiQuery } from "./config/skillstate.ts";
|
|
31
31
|
import { buildRootDigestCompaction } from "./core/root-digest.ts";
|
|
32
|
-
import { RootStateTracker } from "./core/root-state.ts";
|
|
32
|
+
import { ROOT_IDLE_DEGRADE_TURNS, RootStateTracker } from "./core/root-state.ts";
|
|
33
33
|
import { elideStalePayloads, spliceSigmaSnapshot } from "./core/root-context.ts";
|
|
34
|
+
import { SessionArchive } from "./core/session-archive.ts";
|
|
34
35
|
import { agentMessageText, firstLine, textContentOf } from "./text/agent-text.ts";
|
|
35
36
|
import { findStatePatches } from "./text/parsing.ts";
|
|
36
37
|
import { capToolResultText } from "./mode/native-guards.ts";
|
|
38
|
+
import {
|
|
39
|
+
STAGE_CUSTOM_TYPE,
|
|
40
|
+
renderStageCard,
|
|
41
|
+
stageCardMarkdown,
|
|
42
|
+
type StageCardDetails,
|
|
43
|
+
} from "./ui/stage-cards.ts";
|
|
37
44
|
import {
|
|
38
45
|
isSubagentChildBypass,
|
|
39
46
|
commitSubagentForceActivation,
|
|
@@ -108,17 +115,36 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
108
115
|
hideWhenEmpty: true,
|
|
109
116
|
});
|
|
110
117
|
let treePanelInstalled = false;
|
|
118
|
+
/**
|
|
119
|
+
* Native-mode abort: repl cells' child engines, detached spawn() tasks and add_context
|
|
120
|
+
* loads read this signal lazily (createReplTool getSignal), so /rlm-stop aborts AND
|
|
121
|
+
* rotates the controller mid-session — work started after a stop sees a fresh signal.
|
|
122
|
+
*/
|
|
123
|
+
let nativeAbort = new AbortController();
|
|
124
|
+
const stopNativeWork = (): boolean => {
|
|
125
|
+
const hadWork = runRegistry.hasActive() || background.pending > 0;
|
|
126
|
+
nativeAbort.abort();
|
|
127
|
+
nativeAbort = new AbortController();
|
|
128
|
+
return hadWork;
|
|
129
|
+
};
|
|
111
130
|
/** SKILL.state (Workstream B): session store — hydrated at session_start, flushed at shutdown. */
|
|
112
131
|
let skillStore: SkillStore | undefined;
|
|
113
132
|
/** Root Σ (WS-3/4): the native session's digest-level Σ_t — runtime-derived (tool outcomes,
|
|
114
133
|
* engine mirrors, prompts); lazily born on the first prompt, harvested + dropped at shutdown. */
|
|
115
134
|
let rootTracker: RootStateTracker | undefined;
|
|
135
|
+
/** Recall W1: elided turns archive here and materialize into the sandbox
|
|
136
|
+
* (ctx/session-log/*) so search()/grep_context() recall them. Per-session, closure-only. */
|
|
137
|
+
const sessionArchive = new SessionArchive(config.rootArchiveMaxChars);
|
|
138
|
+
/** Archive gate: enabled (chars > 0) AND the context transform actually running. */
|
|
139
|
+
const archiveActive = (): boolean =>
|
|
140
|
+
controller.config.rootArchiveMaxChars > 0 && rootContextActive();
|
|
116
141
|
// Root Σ WS-5.1 telemetry — journal counters (trace lines + status widget when tracing).
|
|
117
142
|
let xiCompositions = 0;
|
|
118
143
|
let rootDigests = 0;
|
|
119
144
|
let elidedMessages = 0;
|
|
120
145
|
let sigmaSplices = 0;
|
|
121
146
|
let idleDegrades = 0;
|
|
147
|
+
let archivedTurns = 0;
|
|
122
148
|
/** R6: Σ counter snapshot for the status line — a fresh readonly object per render. */
|
|
123
149
|
const sigmaTelemetry = (): RootSigmaTelemetry => ({
|
|
124
150
|
xiCompositions,
|
|
@@ -127,6 +153,21 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
127
153
|
sigmaSplices,
|
|
128
154
|
idleDegrades,
|
|
129
155
|
});
|
|
156
|
+
/** [rlm.stage]: post one stage-transition card into the transcript (persisted, ctrl+o-expandable). */
|
|
157
|
+
const postStageCard = (details: StageCardDetails): void => {
|
|
158
|
+
try {
|
|
159
|
+
pi.sendMessage({ customType: STAGE_CUSTOM_TYPE, content: stageCardMarkdown(details), display: true, details });
|
|
160
|
+
} catch (err) {
|
|
161
|
+
// Stage cards are decoration — never fail the hook that produced the transition.
|
|
162
|
+
if (traceEnabled) trace("stage-card.fail", { error: errorMessage(err) });
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* Compaction-hook re-entrancy guard: session_before_compact builds the digest card but does
|
|
167
|
+
* NOT post it from inside the hook (a message appended mid-compaction would fold into the
|
|
168
|
+
* very span being digested) — the pending card flushes at the next turn_start instead.
|
|
169
|
+
*/
|
|
170
|
+
let pendingDigestCard: StageCardDetails | undefined;
|
|
130
171
|
// A detached child works in its OWN sandbox, so this one sees no frames and its request
|
|
131
172
|
// watchdog would fire mid-await and SIGKILL a healthy worker, taking the REPL namespace
|
|
132
173
|
// with it. Keep it alive while detached work is genuinely in flight.
|
|
@@ -173,7 +214,9 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
173
214
|
const cfg = controller.config;
|
|
174
215
|
// R0: enableSkillState is enforced (validateEnforcedOn) — no config check remains.
|
|
175
216
|
if (skillStore === undefined) return undefined;
|
|
176
|
-
|
|
217
|
+
// Recall W3: the Ξ block honors the configured score floor (was MIN_VALUE — any
|
|
218
|
+
// positively-scored stale note rode every prompt).
|
|
219
|
+
const block = skillStore.blockFor(query, cfg.skillStateMaxTokens, cfg.skillStateXiMinScore);
|
|
177
220
|
return block === "" ? undefined : block;
|
|
178
221
|
};
|
|
179
222
|
|
|
@@ -189,6 +232,8 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
189
232
|
pi.registerMessageRenderer("rlm-intro", (message, _options, theme) =>
|
|
190
233
|
new Markdown(textContentOf(message.content), 1, 0, markdownTheme(theme)),
|
|
191
234
|
);
|
|
235
|
+
// [rlm.stage] cards — orchestrator stage transitions, collapsed until the user's ctrl+o.
|
|
236
|
+
pi.registerMessageRenderer(STAGE_CUSTOM_TYPE, renderStageCard);
|
|
192
237
|
|
|
193
238
|
// ── CLI flag: `pi --rlm` / `pi --rlm=false` overrides the persisted mode for this run ──
|
|
194
239
|
pi.registerFlag("rlm", {
|
|
@@ -197,7 +242,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
197
242
|
});
|
|
198
243
|
|
|
199
244
|
// ── Commands ──
|
|
200
|
-
registerRlmCommand(pi, controller);
|
|
245
|
+
registerRlmCommand(pi, controller, stopNativeWork);
|
|
201
246
|
registerRlmConfigCommand(pi, controller);
|
|
202
247
|
registerRlmLlmCommand(pi, controller);
|
|
203
248
|
registerRlmRlmCommand(pi, controller);
|
|
@@ -322,6 +367,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
322
367
|
background,
|
|
323
368
|
runRegistry,
|
|
324
369
|
skillStore,
|
|
370
|
+
getSignal: () => nativeAbort.signal,
|
|
325
371
|
onRunState: (state) => { rootTracker?.absorbEngineState(state); },
|
|
326
372
|
getSkillBlock: composeSkillBlock,
|
|
327
373
|
registerDiscardHook: (reset) => { onSandboxDiscardExtra = reset; },
|
|
@@ -362,6 +408,14 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
362
408
|
setRlmModeStatus(ctx, controller, ctx.getContextUsage(), sigmaTelemetry());
|
|
363
409
|
});
|
|
364
410
|
|
|
411
|
+
// Deferred [rlm.stage] digest card — built inside session_before_compact, posted here.
|
|
412
|
+
pi.on("turn_start", async () => {
|
|
413
|
+
const card = pendingDigestCard;
|
|
414
|
+
if (card === undefined) return;
|
|
415
|
+
pendingDigestCard = undefined;
|
|
416
|
+
postStageCard(card);
|
|
417
|
+
});
|
|
418
|
+
|
|
365
419
|
/** True when the native-mode trade holds: enabled AND repl is in the active tool set. */
|
|
366
420
|
const nativeTradeHolds = (): boolean =>
|
|
367
421
|
shouldEnforceNativeReaderBlock({
|
|
@@ -423,8 +477,13 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
423
477
|
const tracker = rootTracker;
|
|
424
478
|
if (tracker === undefined || !controller.config.enableRootStateFences) return;
|
|
425
479
|
if (event.message.role !== "assistant") return;
|
|
480
|
+
// Recall W2: a turn that ran TOOLS did work — it is never fence-idle (file-editing
|
|
481
|
+
// sessions were degrading before their first fence landed). Productive turns neither
|
|
482
|
+
// grow nor reset the idle streak; prose-only turns keep the R4 ladder.
|
|
483
|
+
const productiveTurn = Array.isArray(event.message.content) &&
|
|
484
|
+
(event.message.content as Array<{ type?: string }>).some((b) => b?.type === "toolCall");
|
|
426
485
|
const wasActive = tracker.isActive;
|
|
427
|
-
const outcome = tracker.applyFences(findStatePatches(agentMessageText(event.message)));
|
|
486
|
+
const outcome = tracker.applyFences(findStatePatches(agentMessageText(event.message)), { productiveTurn });
|
|
428
487
|
// R3 soak observability: per-turn fence outcomes — the soak-B bars (≥50% of turns commit
|
|
429
488
|
// ≥1 accepted delta, rejection storms <10%) are computed from these journal lines.
|
|
430
489
|
if (traceEnabled) {
|
|
@@ -439,6 +498,12 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
439
498
|
}
|
|
440
499
|
if (wasActive && !tracker.isActive) {
|
|
441
500
|
idleDegrades += 1;
|
|
501
|
+
postStageCard({
|
|
502
|
+
kind: "degrade",
|
|
503
|
+
reason: tracker.degradeReason ?? "unknown",
|
|
504
|
+
idleTurns: tracker.idleTurns,
|
|
505
|
+
idleMax: ROOT_IDLE_DEGRADE_TURNS,
|
|
506
|
+
});
|
|
442
507
|
if (traceEnabled) {
|
|
443
508
|
const reason = tracker.degradeReason ?? "unknown";
|
|
444
509
|
trace(reason.startsWith("idle") ? "root-state.idle-degrade" : "root-state.degrade", {
|
|
@@ -446,6 +511,9 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
446
511
|
reason,
|
|
447
512
|
});
|
|
448
513
|
}
|
|
514
|
+
} else if (!wasActive && tracker.isActive) {
|
|
515
|
+
// R7-fix recovery observability, user-visible: a degraded tracker accepted a clean batch.
|
|
516
|
+
postStageCard({ kind: "recover", fencesAccepted: outcome.accepted, fencesTotal: outcome.fences });
|
|
449
517
|
}
|
|
450
518
|
});
|
|
451
519
|
|
|
@@ -462,6 +530,14 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
462
530
|
});
|
|
463
531
|
if (result !== undefined) {
|
|
464
532
|
rootDigests += 1;
|
|
533
|
+
pendingDigestCard = {
|
|
534
|
+
kind: "digest",
|
|
535
|
+
index: rootDigests,
|
|
536
|
+
turnsFolded: event.preparation.messagesToSummarize.length,
|
|
537
|
+
tokensBefore: result.compaction.tokensBefore,
|
|
538
|
+
tokensBeforeRecomputed: result.tokensBeforeRecomputed,
|
|
539
|
+
summary: result.compaction.summary,
|
|
540
|
+
};
|
|
465
541
|
if (traceEnabled) {
|
|
466
542
|
// V1 soak probe: host-consumed tokensBefore vs our recomputation over the same span.
|
|
467
543
|
trace("root-digest.built", {
|
|
@@ -486,7 +562,7 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
486
562
|
// ── Context injection: listing of whatever is currently loaded ──
|
|
487
563
|
// Re-inject only when the payload identity changes (seed / add_context), not every turn —
|
|
488
564
|
// the listing can be up to 200 file lines and the plugin exists to shrink the root window.
|
|
489
|
-
pi.on("context", async (event) => {
|
|
565
|
+
pi.on("context", async (event, ctx) => {
|
|
490
566
|
const filtered = event.messages.filter(
|
|
491
567
|
(message) =>
|
|
492
568
|
!(message.role === "custom" && message.customType === "rlm-intro")
|
|
@@ -499,10 +575,23 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
499
575
|
// then splice exactly one fresh Σ snapshot. Fail-soft: a throw here must never break a turn.
|
|
500
576
|
if (rootContextActive()) {
|
|
501
577
|
try {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
578
|
+
// Recall W1: every destroyed message's full text rides the sink into the session
|
|
579
|
+
// archive BEFORE the stub replaces it — elision stays dereferenceable.
|
|
580
|
+
const sink = archiveActive()
|
|
581
|
+
? (entry: { role: "assistant" | "toolResult"; toolName: string | undefined; text: string }) => {
|
|
582
|
+
const seq = sessionArchive.record(entry);
|
|
583
|
+
if (seq !== undefined) archivedTurns += 1;
|
|
584
|
+
}
|
|
585
|
+
: undefined;
|
|
586
|
+
const elided = elideStalePayloads(
|
|
587
|
+
filtered,
|
|
588
|
+
{
|
|
589
|
+
keepTurns: controller.config.rootContextKeepTurns,
|
|
590
|
+
elideChars: controller.config.rootContextElideChars,
|
|
591
|
+
archiveActive: sink !== undefined,
|
|
592
|
+
},
|
|
593
|
+
sink,
|
|
594
|
+
);
|
|
506
595
|
elidedMessages += elided;
|
|
507
596
|
const tracker = rootTracker;
|
|
508
597
|
// R4 REV (amnesia fix): degrade suspends fence WRITES (applyFences gate) — never Σ
|
|
@@ -523,8 +612,31 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
523
612
|
});
|
|
524
613
|
sigmaSplices += 1;
|
|
525
614
|
}
|
|
615
|
+
// Recall W1 flush: materialize new archive segments into the sandbox — ONLY when a
|
|
616
|
+
// worker already exists (never spawn Python from a context event) and fail-soft.
|
|
617
|
+
const pendingSegment = archiveActive() && sandboxManager.isAlive
|
|
618
|
+
? sessionArchive.renderPending()
|
|
619
|
+
: undefined;
|
|
620
|
+
if (pendingSegment !== undefined) {
|
|
621
|
+
const cwd = resolve(ctx?.cwd ?? process.cwd());
|
|
622
|
+
const written = await sandboxManager.upsertArchiveSegment(pendingSegment.path, pendingSegment.text, cwd);
|
|
623
|
+
if (traceEnabled) {
|
|
624
|
+
trace("root-archive.flush", {
|
|
625
|
+
path: pendingSegment.path,
|
|
626
|
+
chars: pendingSegment.text.length,
|
|
627
|
+
written,
|
|
628
|
+
stats: sessionArchive.stats,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
526
632
|
if (traceEnabled && (elided > 0 || sigmaSplices > 0)) {
|
|
527
|
-
trace("root-context.transform", {
|
|
633
|
+
trace("root-context.transform", {
|
|
634
|
+
elided,
|
|
635
|
+
total: elidedMessages,
|
|
636
|
+
splices: sigmaSplices,
|
|
637
|
+
archived: archivedTurns,
|
|
638
|
+
archiveChars: sessionArchive.stats.chars,
|
|
639
|
+
});
|
|
528
640
|
}
|
|
529
641
|
} catch (err) {
|
|
530
642
|
if (traceEnabled) trace("root-context.fail", { error: errorMessage(err) });
|
|
@@ -571,6 +683,13 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
571
683
|
event.isError,
|
|
572
684
|
event.isError ? firstLine(textContentOf(event.content)) : "",
|
|
573
685
|
);
|
|
686
|
+
// Recall W2 deterministic harvest (commit-at-first-sight, paper §7): successful reads
|
|
687
|
+
// are Σ facts the moment they happen — previously the only runtime feeds were tool
|
|
688
|
+
// ERRORS and edits, so the early turns (the ones elided first) never reached Σ.
|
|
689
|
+
if (!event.isError && event.toolName === "read") {
|
|
690
|
+
const path = extractEditPaths(event.input)[0];
|
|
691
|
+
if (path !== undefined) tracker.noteFact(`read ${path}`);
|
|
692
|
+
}
|
|
574
693
|
}
|
|
575
694
|
|
|
576
695
|
// ── Keep RLM context fresh after native file mutations ──
|
|
@@ -617,14 +736,25 @@ export default function rlmExtension(pi: ExtensionAPI): void {
|
|
|
617
736
|
const tracker = rootTracker;
|
|
618
737
|
if (skillStore !== undefined && tracker !== undefined && tracker.dirty) {
|
|
619
738
|
try {
|
|
620
|
-
|
|
621
|
-
|
|
739
|
+
const merged = notesFromRunState(tracker.snapshot());
|
|
740
|
+
skillStore.merge(merged);
|
|
741
|
+
if (merged.length > 0) {
|
|
742
|
+
// One-line stage card: what this session taught the store (tag histogram via stats()).
|
|
743
|
+
postStageCard({
|
|
744
|
+
kind: "distill",
|
|
745
|
+
merged,
|
|
746
|
+
total: skillStore.noteCount,
|
|
747
|
+
byTag: skillStore.stats().byTag,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
if (traceEnabled) trace("root-harvest.merged", { notes: merged.length });
|
|
622
751
|
} catch (err) {
|
|
623
752
|
if (traceEnabled) trace("root-harvest.fail", { error: errorMessage(err) });
|
|
624
753
|
}
|
|
625
754
|
}
|
|
626
755
|
await skillStore?.flush(); // SKILL.state (Workstream B): persist distilled notes
|
|
627
756
|
controller.abort();
|
|
757
|
+
nativeAbort.abort(); // detach engines/spawns still holding the session signal
|
|
628
758
|
clearInterval(watchdogHeartbeat);
|
|
629
759
|
background.dispose();
|
|
630
760
|
await sandboxManager.dispose();
|
package/src/mode/rlm-mode.ts
CHANGED
|
@@ -120,10 +120,10 @@ export class RlmController {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/** Ξ (Workstream C): BM25 slice of the session SkillState for a root prompt; undefined when
|
|
123
|
-
* the store is absent/disabled or nothing is relevant. */
|
|
123
|
+
* the store is absent/disabled or nothing is relevant. Recall W3: honors the Ξ score floor. */
|
|
124
124
|
private skillBlockFor(query: string): string | undefined {
|
|
125
125
|
if (this.skillStore === undefined || !this.config.enableSkillState) return undefined;
|
|
126
|
-
const block = this.skillStore.blockFor(query, this.config.skillStateMaxTokens);
|
|
126
|
+
const block = this.skillStore.blockFor(query, this.config.skillStateMaxTokens, this.config.skillStateXiMinScore);
|
|
127
127
|
return block === "" ? undefined : block;
|
|
128
128
|
}
|
|
129
129
|
|
package/src/prompts/glossary.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* about functions that do not exist, or not told about ones that do.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { ARCHIVE_NAMESPACE } from "../core/session-archive.ts";
|
|
11
|
+
|
|
10
12
|
export type ContextKind = "files" | "text";
|
|
11
13
|
|
|
12
14
|
/** "str" (raw string context, e.g. rlm_query children) → text; everything else → files. */
|
|
@@ -28,9 +30,11 @@ export function promptCapTokensK(maxPromptChars: number): number {
|
|
|
28
30
|
* the outcome (§5, Fig. 4a). These cost no tokens and no sub-calls.
|
|
29
31
|
*/
|
|
30
32
|
const RETRIEVAL_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
31
|
-
"- `search(query: str, k=10, path_glob=None)`: BM25 ranking over `context
|
|
32
|
-
"
|
|
33
|
-
"
|
|
33
|
+
"- `search(query: str, k=10, path_glob=None)`: BM25 ranking over `context` (stemmed, with",
|
|
34
|
+
" query expansion — try plain words, not just exact identifiers). Returns",
|
|
35
|
+
" [{path, line, end, score, snippet, text}] — POINTERS, not bodies (`text` aliases",
|
|
36
|
+
" `snippet`; line..end is the match span; `index_truncated: true` means the context tail",
|
|
37
|
+
" is NOT indexed — narrow with path_glob). **Start here.** Free: no sub-LLM call.",
|
|
34
38
|
"- `grep_context(pattern, k=50, path_glob=None, before=0, after=0) -> dict`: regex over",
|
|
35
39
|
" `context`. Returns {hits: [{path, line, text, snippet}], counts, total, truncated} —",
|
|
36
40
|
" `counts` is complete even when `hits` is capped, so a wide pattern reports its shape",
|
|
@@ -56,6 +60,15 @@ const SKILL_SEARCH_GLOSSARY_LINES: readonly string[] = Object.freeze([
|
|
|
56
60
|
" smells like something already learned — do not re-discover it.",
|
|
57
61
|
]);
|
|
58
62
|
|
|
63
|
+
/** W3 recall discoverability: the condensed NATIVE twins. glossary doctrine — divergence
|
|
64
|
+
* between the headless and native surfaces is a bug; `skill_search` had no native twin, so
|
|
65
|
+
* native models could not discover cross-session recall at all (and the archive line
|
|
66
|
+
* teaches the ctx/session-log recall that honest elision stubs point at). */
|
|
67
|
+
export const SKILL_SEARCH_LINE_NATIVE =
|
|
68
|
+
"- `skill_search(query, k=8) -> [{id, text, tags, score}]` — BM25 over distilled project facts from PRIOR sessions. Free; use before re-discovering a learned config/gotcha/symbol.";
|
|
69
|
+
export const ARCHIVE_RECALL_LINE_NATIVE =
|
|
70
|
+
`- Elided chat turns are archived in the sandbox: \`search("<keywords>", path_glob="${ARCHIVE_NAMESPACE}*")\` / \`grep_context()\` recall text that scrolled out of your context — free, no sub-LLM call.`;
|
|
71
|
+
|
|
59
72
|
/** Single source of wording for the injected SkillState block (headless + native, Workstream C).
|
|
60
73
|
* Takes the dynamic body as an argument — the glossary itself stays static-only. */
|
|
61
74
|
/** One wording source for the skill_search recall hint (Ξ block + root Σ snapshot). */
|
|
@@ -64,10 +77,21 @@ export const SKILL_RECALL_LINE =
|
|
|
64
77
|
|
|
65
78
|
/** R5 (G4, /tmp/ROOT_FULL_SKILLSTATE_PLAN.md): the one-line replacement for assistant prose
|
|
66
79
|
* older than the keep window — durable facts live in Σ; the repl sandbox (answers/vars) is
|
|
67
|
-
* the model-reachable recovery channel
|
|
68
|
-
*
|
|
80
|
+
* the model-reachable recovery channel for repl-owned payloads. Stubs must promise only a
|
|
81
|
+
* channel that actually holds the bytes (recall W1): repl-owned payloads → the repl line,
|
|
82
|
+
* native payloads → the archive line (searchable `ctx/session-log/`), else the plain line. */
|
|
69
83
|
export const ROOT_TURN_ELIDED_LINE =
|
|
70
84
|
"… turn elided — durable facts live in Σ; your repl sandbox persists: print(answers) / SHOW_VARS() to re-derive";
|
|
85
|
+
export const ROOT_TURN_ELIDED_ARCHIVE_LINE =
|
|
86
|
+
`… turn elided — durable facts live in Σ; the full text is archived in the sandbox: ` +
|
|
87
|
+
`search('<keywords>', path_glob='${ARCHIVE_NAMESPACE}*') or grep_context() recalls it`;
|
|
88
|
+
export const ROOT_TURN_ELIDED_PLAIN_LINE =
|
|
89
|
+
"… turn elided — durable facts live in Σ";
|
|
90
|
+
/** Preview mark twin: payloads kept as head+tail previews point at the same archive. */
|
|
91
|
+
export const ROOT_ELIDE_PREVIEW_MARK =
|
|
92
|
+
`chars elided — full text archived under ${ARCHIVE_NAMESPACE} (search/grep_context it)`;
|
|
93
|
+
export const ROOT_ELIDE_PREVIEW_MARK_REPL =
|
|
94
|
+
"chars elided — repl sandbox persists: print(answers[k]) or re-run repl to re-derive";
|
|
71
95
|
|
|
72
96
|
export function skillStateLines(noteCount: number, body: string): string {
|
|
73
97
|
return [
|
|
@@ -343,6 +367,9 @@ export function replGlossary(
|
|
|
343
367
|
lines.push(
|
|
344
368
|
"- `llm_query(prompt: str) -> Task`: spawn one sub-LLM (await_task for str). The prompt must",
|
|
345
369
|
" **contain the text** to analyze — this call has no filesystem and no `context`.",
|
|
370
|
+
" Leaf convention: a sub-LLM answers exactly `NOT_FOUND` when its slice lacks the answer —",
|
|
371
|
+
" treat that as \"not in this slice\": slice differently, search elsewhere, or narrow the ask.",
|
|
372
|
+
" Never re-send an identical prompt hoping for a different verdict.",
|
|
346
373
|
"- `llm_batch(prompts: list[str]) -> Task`: many parallel one-shots (same rule: embed text).",
|
|
347
374
|
" await_task → ordered list[str]. NEVER pass bare file paths as if the worker can open them.",
|
|
348
375
|
...CHUNKED_GLOSSARY_LINES,
|
|
@@ -395,6 +422,8 @@ export function replGlossary(
|
|
|
395
422
|
'- `answer`: a dict initialized to {"content": "", "ready": False}. To submit your final answer,',
|
|
396
423
|
' set `answer["content"]` to the answer text and `answer["ready"] = True`.',
|
|
397
424
|
' **You MUST flip `answer["ready"] = True` — runs that never finalize are discarded.**',
|
|
425
|
+
' Never write FINAL(...) / FINAL_VAR(...) prose and never emit a ```state fence in the',
|
|
426
|
+
' reply that finalizes — `answer` is the only finalize channel; Σ bookkeeping waits.',
|
|
398
427
|
);
|
|
399
428
|
return lines.join("\n");
|
|
400
429
|
}
|
package/src/prompts/native.ts
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
ARCHIVE_RECALL_LINE_NATIVE,
|
|
9
10
|
CHUNKED_GLOSSARY_LINE_NATIVE,
|
|
10
11
|
ENV_TIPS_CONDENSED,
|
|
11
12
|
LARGE_FILE_RULE_NATIVE,
|
|
12
13
|
DEFAULT_PROMPT_CAP,
|
|
13
14
|
promptCapTokensK,
|
|
15
|
+
SKILL_SEARCH_LINE_NATIVE,
|
|
14
16
|
} from "./glossary.ts";
|
|
15
17
|
import { STATE_FENCE_INSTRUCTION } from "../core/run-state.ts";
|
|
16
18
|
|
|
@@ -25,6 +27,7 @@ function nativeReplGlossary(): string {
|
|
|
25
27
|
"- `search(query, k=10, path_glob=None) -> [{path, line, score, snippet, text}]` — BM25 pointers, not bodies",
|
|
26
28
|
"- `grep_context(pattern, k=50, …) -> {hits, counts, total, truncated}` — regex / lexical needles",
|
|
27
29
|
"- `outline(path) -> str` — definition skeleton (~200 chars)",
|
|
30
|
+
ARCHIVE_RECALL_LINE_NATIVE,
|
|
28
31
|
"",
|
|
29
32
|
"### Always-spawn fan-out (return Task + run ↯bg — NEVER the answer)",
|
|
30
33
|
"| Call | await_task → | When | NOT for |",
|
|
@@ -47,6 +50,7 @@ function nativeReplGlossary(): string {
|
|
|
47
50
|
"- `answers` / `plan` — persistent dicts for **collected** results. Task handles are REPL vars (`t`), not `answers` keys.",
|
|
48
51
|
"- `add_context(source) -> dict` — append external dir/file/doc/git under `ctx/<id>/…` (metadata only).",
|
|
49
52
|
"- `list_claims()` — the live `[ledger]` table of agent work.",
|
|
53
|
+
SKILL_SEARCH_LINE_NATIVE,
|
|
50
54
|
"- `SHOW_VARS()` — list REPL vars (Tasks as `<Task …>`). `list_tasks()` finds Task handles. `answer[\"ready\"]=True` only for headless finalize (native: write a normal message).",
|
|
51
55
|
"",
|
|
52
56
|
ENV_TIPS_CONDENSED,
|
|
@@ -189,8 +193,10 @@ export function buildNativeSystemPrompt(opts?: { readonly stateFences?: boolean
|
|
|
189
193
|
].join("\n");
|
|
190
194
|
}
|
|
191
195
|
|
|
192
|
-
/** Soft cap on the static native prompt. Raised for v5-style contract/routing/examples
|
|
193
|
-
|
|
196
|
+
/** Soft cap on the static native prompt. Raised for v5-style contract/routing/examples;
|
|
197
|
+
* recall W3 raised it again (+200) for the archive-recall + skill_search twin lines —
|
|
198
|
+
* the model cannot recall elided turns or prior-session facts it is never told about. */
|
|
199
|
+
export const NATIVE_PROMPT_BUDGET = 9_700;
|
|
194
200
|
|
|
195
201
|
/** Exported for tests — prompt length without context metadata (which is injected separately). */
|
|
196
202
|
export const NATIVE_PROMPT_STATIC = buildNativeSystemPrompt();
|
package/src/prompts/user.ts
CHANGED
|
@@ -26,6 +26,7 @@ export function buildTurnPrompt(
|
|
|
26
26
|
export const FINALIZE_PROMPT =
|
|
27
27
|
"You are out of turns. Finalize NOW: set `answer[\"content\"]` and `answer[\"ready\"] = True` " +
|
|
28
28
|
"(fenced ```repl```) with your best final answer from everything you have gathered. " +
|
|
29
|
+
"Finalize ONLY through `answer` — no FINAL(...) prose and no ```state fence in this reply. " +
|
|
29
30
|
"Only if the REPL is unavailable, answer as plain text.";
|
|
30
31
|
|
|
31
32
|
/** One-shot retrieval-discipline nudge (the engine owns the when — see core/engine.ts). The
|
|
@@ -44,9 +45,9 @@ export const REASONING_BUDGET_HINT =
|
|
|
44
45
|
"the completion budget with the answer, so long thought may be cut off mid-reasoning. " +
|
|
45
46
|
"Keep thought concise, or raise rootSampling.maxTokens.";
|
|
46
47
|
|
|
47
|
-
/** One-shot verification-discipline nudge (default
|
|
48
|
-
* the root finalizes suspiciously early with a bare
|
|
49
|
-
* coached redo instead of having the answer accepted. */
|
|
48
|
+
/** One-shot verification-discipline nudge (default ON — enableVerificationNudge): fired when
|
|
49
|
+
* the root finalizes suspiciously early with a bare answer, or without having inspected its
|
|
50
|
+
* context at all. The model gets ONE coached redo instead of having the answer accepted. */
|
|
50
51
|
export const VERIFICATION_NUDGE =
|
|
51
52
|
"[coach] That answer was submitted suspiciously early and looks under-verified. Before " +
|
|
52
53
|
"finalizing: recompute the key quantity inside a ```repl block (show the actual computation, " +
|
|
Binary file
|
|
Binary file
|