@hicaru/pi-rlm 0.3.6 → 0.3.8
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/bridge/handlers/llm-query.ts +73 -17
- package/src/bridge/handlers/rlm-query.ts +130 -8
- package/src/bridge/handlers/types.ts +12 -0
- package/src/config/defaults.ts +17 -0
- package/src/config/settings.ts +45 -2
- package/src/core/answer.ts +7 -10
- package/src/core/budget.ts +182 -0
- package/src/core/compaction.ts +46 -0
- package/src/core/engine.ts +171 -5
- package/src/core/ledger.ts +451 -0
- package/src/core/memory.ts +577 -0
- package/src/core/model-registry.ts +88 -0
- package/src/core/types.ts +44 -3
- package/src/index.ts +60 -9
- package/src/mode/rlm-mode.ts +47 -9
- package/src/prompts/glossary.ts +139 -57
- package/src/prompts/native.ts +12 -7
- package/src/prompts/system.ts +22 -7
- package/src/prompts/user.ts +6 -3
- package/src/sandbox/interrupts.ts +24 -0
- package/src/sandbox/protocol.ts +69 -5
- 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/guards.py +11 -6
- package/src/sandbox/py/scaffold.py +615 -0
- package/src/sandbox/py/worker.py +53 -506
- package/src/sandbox/sandbox.ts +21 -3
- package/src/text/repl-output.ts +15 -0
- package/src/tool/repl-result.ts +54 -10
- package/src/tool/repl-tool.ts +43 -9
- package/src/ui/status.ts +4 -1
- package/src/util/concurrency.ts +47 -0
package/src/prompts/system.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { ContextSizeStats } from "../text/tokens.ts";
|
|
|
8
8
|
import {
|
|
9
9
|
contextKindOf,
|
|
10
10
|
DEFAULT_PROMPT_CAP,
|
|
11
|
-
|
|
11
|
+
envTips,
|
|
12
12
|
howToRunCode,
|
|
13
13
|
LARGE_FILE_RULE_LINES,
|
|
14
14
|
promptCapTokensK,
|
|
@@ -33,23 +33,29 @@ export interface SystemPromptOptions {
|
|
|
33
33
|
readonly child?: boolean;
|
|
34
34
|
/** The recursion depth of this run (0 = root, 1 = first child, etc.). */
|
|
35
35
|
readonly depth?: number;
|
|
36
|
+
/** v5 doctrine: the child sandbox has the delegation-only surface (no retrieval tools). */
|
|
37
|
+
readonly delegation?: boolean;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
function orchestratorAddendum(maxPromptChars: number): string {
|
|
40
|
+
function orchestratorAddendum(maxPromptChars: number, delegation: boolean): string {
|
|
39
41
|
return [
|
|
40
42
|
"As an RLM you are an **orchestrator, not a solver**. Probe `context`, plan decomposition, then",
|
|
41
43
|
"fan out — do not solve multi-step module work yourself in a long chain of thought.",
|
|
42
44
|
"",
|
|
43
45
|
"<contract> llm_query / llm_batch / map_files / rlm_query / rlm_batch return Task (not the answer).",
|
|
44
|
-
|
|
46
|
+
delegation
|
|
47
|
+
? "Only await_task returns content. Fire independent Tasks first, slice your `context` meanwhile, then await_task."
|
|
48
|
+
: "Only await_task returns content. Fire independent Tasks first, free search, then await_task.",
|
|
45
49
|
"Do not await after every independent spawn.</contract>",
|
|
46
50
|
"",
|
|
47
51
|
"<routing> one-shot facts → llm_query/llm_batch/map_files; one multi-step study → rlm_query;",
|
|
48
52
|
"≥2 independent multi-step areas → rlm_batch (prefer over serial rlm_query). NEVER print file",
|
|
49
53
|
"bodies into your own stream when a Task tool can read them.</routing>",
|
|
50
54
|
"",
|
|
51
|
-
"Your own context window is small. Push long-context work into sub-calls.
|
|
52
|
-
|
|
55
|
+
"Your own context window is small. Push long-context work into sub-calls.",
|
|
56
|
+
delegation
|
|
57
|
+
? "If a slice of your `context` already pins a tiny fact, quote it — do not re-ask."
|
|
58
|
+
: "If free search/grep already pins a tiny fact, use that. Aggregate small results in Python / `answers`.",
|
|
53
59
|
"",
|
|
54
60
|
`Sub-call budget: (1) per-prompt < ${maxPromptChars.toLocaleString()} chars (≈${promptCapTokensK(maxPromptChars)}K tok);`,
|
|
55
61
|
"(2) ~20 prompts per llm_batch. Fat prompts in small batches beat thousands of tiny prompts.",
|
|
@@ -78,13 +84,22 @@ export function buildRlmSystemPrompt(meta: PromptMeta, opts: SystemPromptOptions
|
|
|
78
84
|
`**Recursion depth: ${opts.depth}.** You are a sub-RLM — focus narrowly on your assigned`,
|
|
79
85
|
"task. Delegate (rlm_query/rlm_batch) only if the task itself must decompose further.",
|
|
80
86
|
);
|
|
87
|
+
if (opts.delegation ?? false) {
|
|
88
|
+
parts.push(
|
|
89
|
+
"",
|
|
90
|
+
"**REPL API (ONLY these):** llm_query / llm_batch / llm_query_chunked / map_files /",
|
|
91
|
+
"llm_map_reduce / rlm_query / rlm_batch / spawn / await_task / list_tasks / memory.* /",
|
|
92
|
+
"list_claims. There is no search/grep_context/outline here — your task arrived WITH its",
|
|
93
|
+
"world in `context`; slice it into llm prompts. rlm_query only for a disjoint path set.",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
81
96
|
}
|
|
82
97
|
parts.push(
|
|
83
98
|
"",
|
|
84
99
|
howToRunCode(),
|
|
85
100
|
"",
|
|
86
101
|
replGlossary(
|
|
87
|
-
kind, recursion, opts.contextLoader ?? false, opts.child ?? false,
|
|
102
|
+
kind, recursion, opts.contextLoader ?? false, opts.child ?? false, opts.delegation ?? false,
|
|
88
103
|
),
|
|
89
104
|
"",
|
|
90
105
|
"REPL stdout over ~800 characters is truncated to a short excerpt — large results stay in your",
|
|
@@ -96,7 +111,7 @@ export function buildRlmSystemPrompt(meta: PromptMeta, opts: SystemPromptOptions
|
|
|
96
111
|
if (opts.orchestrator ?? true) {
|
|
97
112
|
// Two counterweights, both required (paper App. B): the addendum bounds OVER-recursion
|
|
98
113
|
// (batching/cost), ENV_TIPS bounds UNDER-recursion (solving it yourself).
|
|
99
|
-
parts.push("", orchestratorAddendum(maxPromptChars), "",
|
|
114
|
+
parts.push("", orchestratorAddendum(maxPromptChars, opts.delegation ?? false), "", envTips(opts.delegation ?? false));
|
|
100
115
|
}
|
|
101
116
|
if (kind === "files") {
|
|
102
117
|
parts.push("", LARGE_FILE_RULE_LINES.join("\n"));
|
package/src/prompts/user.ts
CHANGED
|
@@ -19,7 +19,10 @@ export function buildTurnPrompt(
|
|
|
19
19
|
return `${prefix}${body}`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/** Asked once when the engine runs out of turns without a submitted answer.
|
|
22
|
+
/** Asked once when the engine runs out of turns without a submitted answer. Same finalize
|
|
23
|
+
* dialect as the budget wrap-up note (audit M6): answer-ready first, plain text only as an
|
|
24
|
+
* explicit fallback the engine still accepts. */
|
|
23
25
|
export const FINALIZE_PROMPT =
|
|
24
|
-
"You are out of turns.
|
|
25
|
-
|
|
26
|
+
"You are out of turns. Finalize NOW: set `answer[\"content\"]` and `answer[\"ready\"] = True` " +
|
|
27
|
+
"(fenced ```repl```) with your best final answer from everything you have gathered. " +
|
|
28
|
+
"Only if the REPL is unavailable, answer as plain text.";
|
|
@@ -51,6 +51,14 @@ export interface SubLlmHandlers {
|
|
|
51
51
|
): Promise<unknown>;
|
|
52
52
|
finishTask(summary: string, depth: number, opts: SubcallOpts): Promise<unknown>;
|
|
53
53
|
addContext(source: string, depth: number): Promise<AddContextResult>;
|
|
54
|
+
/** v5: the `[ledger]` claims table for the sandbox's `list_claims()` REPL call. */
|
|
55
|
+
ledgerClaims(): Promise<string>;
|
|
56
|
+
/** v5: durable memory surface for the sandbox's `memory.query/add/stats` object. */
|
|
57
|
+
memoryOp(
|
|
58
|
+
op: "query" | "add" | "stats",
|
|
59
|
+
args: { readonly query?: string; readonly k?: number; readonly content?: string; readonly paths?: readonly string[]; readonly tags?: readonly string[] },
|
|
60
|
+
depth: number,
|
|
61
|
+
): Promise<string>;
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
function toStringArray(value: unknown): readonly string[] | undefined {
|
|
@@ -80,6 +88,8 @@ export const REJECT: SubLlmHandlers = Object.freeze({
|
|
|
80
88
|
addContext: async () => {
|
|
81
89
|
throw new Error("add_context not configured");
|
|
82
90
|
},
|
|
91
|
+
ledgerClaims: async () => UNCONFIGURED,
|
|
92
|
+
memoryOp: async () => UNCONFIGURED,
|
|
83
93
|
});
|
|
84
94
|
|
|
85
95
|
export interface ReplyBody {
|
|
@@ -337,6 +347,20 @@ export async function serviceInterrupt(
|
|
|
337
347
|
});
|
|
338
348
|
return;
|
|
339
349
|
}
|
|
350
|
+
case "ledger_claims": {
|
|
351
|
+
const table = await h.ledgerClaims();
|
|
352
|
+
reply(msg.rid, { response: table });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
case "memory": {
|
|
356
|
+
const out = await h.memoryOp(
|
|
357
|
+
msg.op,
|
|
358
|
+
{ query: msg.query, k: msg.k, content: msg.content, paths: msg.paths, tags: msg.tags },
|
|
359
|
+
d,
|
|
360
|
+
);
|
|
361
|
+
reply(msg.rid, { response: out });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
340
364
|
default: {
|
|
341
365
|
const _exhaustive: never = msg;
|
|
342
366
|
reply((_exhaustive as WorkerInterrupt).rid, {
|
package/src/sandbox/protocol.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Wire protocol for the RLM Python sandbox.
|
|
3
3
|
*
|
|
4
4
|
* Newline-delimited JSON over the worker's stdin/stdout — no sockets, no HTTP.
|
|
5
|
-
* Parent -> worker: requests (exec/load_context/shutdown)
|
|
5
|
+
* Parent -> worker: requests (exec/load_context/shutdown), llm replies, and heartbeats.
|
|
6
6
|
* Worker -> parent: request responses and mid-exec sub-LLM interrupts.
|
|
7
7
|
*
|
|
8
8
|
* Canonical api_v5 kinds only — no legacy `*_query_batched` wire names.
|
|
@@ -44,7 +44,12 @@ export interface LlmReply {
|
|
|
44
44
|
readonly error?: string;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
/** Keep-alive while the host is working and has nothing else to write. */
|
|
48
|
+
export interface Heartbeat {
|
|
49
|
+
readonly type: "heartbeat";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type ParentMessage = WorkerRequest | LlmReply | Heartbeat;
|
|
48
53
|
|
|
49
54
|
/** A normal response to a request (keyed by the request `id`). */
|
|
50
55
|
export interface WorkerResponse {
|
|
@@ -60,11 +65,13 @@ export interface WorkerResponse {
|
|
|
60
65
|
readonly execution_time?: number;
|
|
61
66
|
// user-created variable names after this exec
|
|
62
67
|
readonly var_names?: readonly string[];
|
|
68
|
+
/** Unsettled Task handles still in the worker (native pending-line / await-all). */
|
|
69
|
+
readonly pending_tasks?: readonly PendingTaskInfo[];
|
|
63
70
|
// load_context:
|
|
64
71
|
readonly index?: number;
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
/** Canonical interrupt kinds (api_v5). */
|
|
74
|
+
/** Canonical interrupt kinds (api_v5 + v5 ledger + memory). */
|
|
68
75
|
export type InterruptKind =
|
|
69
76
|
| "llm_query"
|
|
70
77
|
| "rlm_query"
|
|
@@ -72,7 +79,9 @@ export type InterruptKind =
|
|
|
72
79
|
| "rlm_batch"
|
|
73
80
|
| "await"
|
|
74
81
|
| "finish"
|
|
75
|
-
| "add_context"
|
|
82
|
+
| "add_context"
|
|
83
|
+
| "ledger_claims"
|
|
84
|
+
| "memory";
|
|
76
85
|
|
|
77
86
|
interface InterruptBase {
|
|
78
87
|
readonly rid: string;
|
|
@@ -114,13 +123,31 @@ export interface AddContextInterrupt extends InterruptBase {
|
|
|
114
123
|
readonly source?: string;
|
|
115
124
|
}
|
|
116
125
|
|
|
126
|
+
/** v5: sandbox asks the host for the TaskLedger claims table (`list_claims()`). */
|
|
127
|
+
export interface LedgerClaimsInterrupt extends InterruptBase {
|
|
128
|
+
readonly type: "ledger_claims";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** v5: sandbox reaches the durable MemoryStore (`memory.query/add/stats`). */
|
|
132
|
+
export interface MemoryInterrupt extends InterruptBase {
|
|
133
|
+
readonly type: "memory";
|
|
134
|
+
readonly op: "query" | "add" | "stats";
|
|
135
|
+
readonly query?: string;
|
|
136
|
+
readonly k?: number;
|
|
137
|
+
readonly content?: string;
|
|
138
|
+
readonly paths?: readonly string[];
|
|
139
|
+
readonly tags?: readonly string[];
|
|
140
|
+
}
|
|
141
|
+
|
|
117
142
|
/** A mid-exec sub-LLM/tool request from the worker. */
|
|
118
143
|
export type WorkerInterrupt =
|
|
119
144
|
| PromptInterrupt
|
|
120
145
|
| BatchInterrupt
|
|
121
146
|
| AwaitInterrupt
|
|
122
147
|
| FinishInterrupt
|
|
123
|
-
| AddContextInterrupt
|
|
148
|
+
| AddContextInterrupt
|
|
149
|
+
| LedgerClaimsInterrupt
|
|
150
|
+
| MemoryInterrupt;
|
|
124
151
|
|
|
125
152
|
export type WorkerMessage = WorkerResponse | WorkerInterrupt;
|
|
126
153
|
|
|
@@ -133,6 +160,8 @@ export const INTERRUPT_KINDS = Object.freeze(
|
|
|
133
160
|
"await",
|
|
134
161
|
"finish",
|
|
135
162
|
"add_context",
|
|
163
|
+
"ledger_claims",
|
|
164
|
+
"memory",
|
|
136
165
|
]),
|
|
137
166
|
);
|
|
138
167
|
|
|
@@ -158,6 +187,39 @@ export function isWorkerMessage(msg: unknown): msg is WorkerMessage {
|
|
|
158
187
|
return isWorkerResponse(msg) || isInterrupt(msg);
|
|
159
188
|
}
|
|
160
189
|
|
|
190
|
+
/** Unsettled Task still in the worker, optionally bound to a REPL variable. */
|
|
191
|
+
export interface PendingTaskInfo {
|
|
192
|
+
readonly var: string | null;
|
|
193
|
+
readonly kind: string;
|
|
194
|
+
readonly label: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const EMPTY_PENDING: readonly PendingTaskInfo[] = Object.freeze([]);
|
|
198
|
+
|
|
199
|
+
function isPendingTaskInfo(value: unknown): value is PendingTaskInfo {
|
|
200
|
+
if (!isRecord(value)) return false;
|
|
201
|
+
const bound = value["var"];
|
|
202
|
+
return (typeof bound === "string" || bound === null)
|
|
203
|
+
&& typeof value["kind"] === "string"
|
|
204
|
+
&& typeof value["label"] === "string";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Narrow a worker `pending_tasks` payload; drop malformed entries. */
|
|
208
|
+
export function parsePendingTasks(value: unknown): readonly PendingTaskInfo[] {
|
|
209
|
+
if (!Array.isArray(value)) return EMPTY_PENDING;
|
|
210
|
+
const out = new Array<PendingTaskInfo>(value.length);
|
|
211
|
+
let n = 0;
|
|
212
|
+
for (let i = 0; i < value.length; i++) {
|
|
213
|
+
const item: unknown = value[i];
|
|
214
|
+
if (isPendingTaskInfo(item)) {
|
|
215
|
+
out[n] = item;
|
|
216
|
+
n += 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
out.length = n;
|
|
220
|
+
return n === 0 ? EMPTY_PENDING : Object.freeze(out);
|
|
221
|
+
}
|
|
222
|
+
|
|
161
223
|
/** Result of a single `repl` block execution, surfaced to the engine/tool. */
|
|
162
224
|
export interface ReplResult {
|
|
163
225
|
readonly stdout: string;
|
|
@@ -168,4 +230,6 @@ export interface ReplResult {
|
|
|
168
230
|
readonly executionTimeMs: number;
|
|
169
231
|
/** User-created variable names after this exec (builtins/context filtered out). */
|
|
170
232
|
readonly varNames: readonly string[];
|
|
233
|
+
/** Unsettled Task handles still in the worker after this exec. */
|
|
234
|
+
readonly pendingTasks: readonly PendingTaskInfo[];
|
|
171
235
|
}
|
|
Binary file
|
|
Binary file
|
package/src/sandbox/py/guards.py
CHANGED
|
@@ -91,12 +91,12 @@ RESERVED = frozenset(
|
|
|
91
91
|
# Canonical api_v5
|
|
92
92
|
"llm_query", "llm_batch",
|
|
93
93
|
"rlm_query", "rlm_batch",
|
|
94
|
-
"await_task", "finish",
|
|
94
|
+
"await_task", "list_tasks", "finish",
|
|
95
95
|
"spawn",
|
|
96
96
|
# Helpers (not the old *_query_batched API)
|
|
97
97
|
"llm_query_chunked", "map_files", "llm_map_reduce",
|
|
98
98
|
"search", "grep_context", "outline",
|
|
99
|
-
"add_context",
|
|
99
|
+
"add_context", "list_claims", "memory",
|
|
100
100
|
"SHOW_VARS", "answer", "context",
|
|
101
101
|
}
|
|
102
102
|
)
|
|
@@ -114,6 +114,14 @@ class _StallTimeout(Exception):
|
|
|
114
114
|
"""No frame from the host while a sub-call was pending."""
|
|
115
115
|
|
|
116
116
|
|
|
117
|
+
def _stall_message(stall_timeout_s: float) -> str:
|
|
118
|
+
"""Contract text for a stall — raised internally, returned as Error: … to the model."""
|
|
119
|
+
return (
|
|
120
|
+
f"sub-call still running — no reply from the host for {stall_timeout_s:g}s; "
|
|
121
|
+
"await_task it again in a later block"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
117
125
|
@contextmanager
|
|
118
126
|
def _stall_alarm(exec_timeout_s: float, stall_timeout_s: float):
|
|
119
127
|
"""Swap the per-cell alarm for a stall alarm while blocked on the parent.
|
|
@@ -127,10 +135,7 @@ def _stall_alarm(exec_timeout_s: float, stall_timeout_s: float):
|
|
|
127
135
|
remaining = signal.getitimer(signal.ITIMER_REAL)[0] if (use and exec_timeout_s > 0) else 0.0
|
|
128
136
|
|
|
129
137
|
def _fire(signum, frame): # noqa: ARG001
|
|
130
|
-
raise _StallTimeout(
|
|
131
|
-
f"sub-call stalled — no reply from the host for {stall_timeout_s:g}s "
|
|
132
|
-
"(the task may still be running; await_task it again in a later block)"
|
|
133
|
-
)
|
|
138
|
+
raise _StallTimeout(_stall_message(stall_timeout_s))
|
|
134
139
|
|
|
135
140
|
old = signal.signal(signal.SIGALRM, _fire) if use else None
|
|
136
141
|
|