@hicaru/pi-rlm 0.3.1 → 0.3.2
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 +34 -5
- package/README.ru.md +5 -5
- package/README.zh-CN.md +5 -5
- package/package.json +1 -1
- package/src/bridge/handlers/await.ts +148 -0
- package/src/bridge/handlers/completion.ts +72 -0
- package/src/bridge/handlers/emitting.ts +104 -0
- package/src/bridge/handlers/finish.ts +45 -0
- package/src/bridge/handlers/index.ts +48 -0
- package/src/bridge/handlers/llm-query.ts +130 -0
- package/src/bridge/handlers/rlm-query.ts +227 -0
- package/src/bridge/handlers/task-registry.ts +202 -0
- package/src/bridge/handlers/types.ts +136 -0
- package/src/commands/rlm-config.ts +33 -14
- package/src/context/listing.ts +2 -2
- package/src/context/refresh.ts +141 -0
- package/src/core/engine.ts +16 -18
- package/src/core/types.ts +1 -3
- package/src/index.ts +54 -42
- package/src/mode/native-guards.ts +4 -4
- package/src/mode/subagent.ts +1 -1
- package/src/prompts/glossary.ts +71 -74
- package/src/prompts/native.ts +127 -85
- package/src/prompts/system.ts +29 -15
- package/src/sandbox/interrupts.ts +258 -68
- package/src/sandbox/protocol.ts +53 -30
- package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/hostio.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/tasks.cpython-314.pyc +0 -0
- package/src/sandbox/py/guards.py +8 -5
- package/src/sandbox/py/retrieval.py +17 -8
- package/src/sandbox/py/tasks.py +1 -1
- package/src/sandbox/py/worker.py +106 -79
- package/src/sandbox/sandbox-manager.ts +26 -1
- package/src/sandbox/sandbox.ts +1 -1
- package/src/tool/background-tasks.ts +1 -1
- package/src/tool/repl-result.ts +2 -2
- package/src/tool/repl-tool.ts +13 -14
- package/src/ui/config-panel.ts +1 -1
- package/src/ui/intro.ts +1 -4
- package/src/ui/model-picker.ts +28 -2
- package/src/util/concurrency.ts +1 -1
- package/src/bridge/subcall-handlers.ts +0 -382
|
@@ -2,8 +2,15 @@
|
|
|
2
2
|
* The interrupt surface: what the worker can ask the host for mid-exec, and how each request is
|
|
3
3
|
* turned into a reply frame.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Canonical api_v5 kinds only: llm_query | llm_batch | rlm_query | rlm_batch | await | finish | add_context.
|
|
6
|
+
*
|
|
7
|
+
* Wire reply shapes the Python worker reduces:
|
|
8
|
+
* - single: { response: string } or { error }
|
|
9
|
+
* - batch: { responses: string[] } or { error }
|
|
10
|
+
*
|
|
11
|
+
* Host handlers may return either:
|
|
12
|
+
* - plain string / string[] (tests, sync stubs)
|
|
13
|
+
* - SpawnResult { task_id } (createSubcallHandlers) — this layer awaits to final content
|
|
7
14
|
*/
|
|
8
15
|
|
|
9
16
|
import type { WorkerInterrupt } from "./protocol.ts";
|
|
@@ -12,47 +19,40 @@ import { errorMessage, formatError } from "../util/errors.ts";
|
|
|
12
19
|
|
|
13
20
|
/** Result of a host-side pack requested by `add_context`. */
|
|
14
21
|
export interface AddContextResult {
|
|
15
|
-
readonly payload: unknown;
|
|
22
|
+
readonly payload: unknown;
|
|
16
23
|
readonly files?: number;
|
|
17
24
|
readonly chars: number;
|
|
18
25
|
readonly sourceId: string;
|
|
19
26
|
readonly pathPrefix: string;
|
|
20
|
-
/** Host already has this source — no pack, empty payload. */
|
|
21
27
|
readonly alreadyLoaded?: boolean;
|
|
22
|
-
/** Document-type files in the payload (fresh + cache hits). */
|
|
23
28
|
readonly documents?: number;
|
|
24
|
-
/** Documents freshly converted this call (cache hits excluded). */
|
|
25
29
|
readonly converted?: number;
|
|
26
|
-
/** Paths skipped during packing (model-facing). */
|
|
27
30
|
readonly skipped?: readonly { readonly path: string; readonly reason: string }[];
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
/**
|
|
31
|
-
* Per-interrupt routing context for the sub-LLM handlers.
|
|
32
|
-
*
|
|
33
|
-
* Only the four sub-call kinds can be spawned, so only they carry it; add_context is
|
|
34
|
-
* always synchronous within one exec.
|
|
35
|
-
*/
|
|
36
33
|
export interface SubcallOpts {
|
|
37
|
-
/** Started via `spawn()` — route to session-scoped state, not the current invocation. */
|
|
38
34
|
readonly detached: boolean;
|
|
39
|
-
/**
|
|
40
|
-
* `rlm_query(paths=[…])` — path prefixes narrowing the child's inherited context.
|
|
41
|
-
* Absent on every other path; `llm_query` never carries it.
|
|
42
|
-
*/
|
|
35
|
+
/** Path prefixes for rlm_query / rlm_batch child context. */
|
|
43
36
|
readonly paths?: readonly string[];
|
|
44
37
|
}
|
|
45
38
|
|
|
46
|
-
/** Handlers the bridge installs
|
|
39
|
+
/** Handlers the bridge installs — canonical names only. */
|
|
47
40
|
export interface SubLlmHandlers {
|
|
48
|
-
llmQuery(prompt: string,
|
|
49
|
-
|
|
50
|
-
rlmQuery(
|
|
51
|
-
|
|
41
|
+
llmQuery(prompt: string, depth: number, opts: SubcallOpts): Promise<unknown>;
|
|
42
|
+
llmBatch(prompts: readonly string[], depth: number, opts: SubcallOpts): Promise<unknown>;
|
|
43
|
+
rlmQuery(task: string, depth: number, opts: SubcallOpts): Promise<unknown>;
|
|
44
|
+
rlmBatch(tasks: readonly string[], depth: number, opts: SubcallOpts): Promise<unknown>;
|
|
45
|
+
awaitTask(
|
|
46
|
+
taskId: string | undefined,
|
|
47
|
+
taskIds: readonly string[] | undefined,
|
|
48
|
+
timeoutS: number | undefined,
|
|
49
|
+
depth: number,
|
|
50
|
+
opts: SubcallOpts,
|
|
51
|
+
): Promise<unknown>;
|
|
52
|
+
finishTask(summary: string, depth: number, opts: SubcallOpts): Promise<unknown>;
|
|
52
53
|
addContext(source: string, depth: number): Promise<AddContextResult>;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
/** Narrow an unknown JSON value to a frozen string array. Non-strings and blanks are dropped. */
|
|
56
56
|
function toStringArray(value: unknown): readonly string[] | undefined {
|
|
57
57
|
if (!Array.isArray(value)) return undefined;
|
|
58
58
|
const out = new Array<string>(value.length);
|
|
@@ -65,16 +65,23 @@ function toStringArray(value: unknown): readonly string[] | undefined {
|
|
|
65
65
|
return n > 0 ? Object.freeze(out) : undefined;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
const UNCONFIGURED = formatError("sub-LLM bridge not configured");
|
|
69
|
+
|
|
70
|
+
const rejectBatch = async (items: readonly string[]): Promise<readonly string[]> =>
|
|
71
|
+
Object.freeze(items.map(() => UNCONFIGURED));
|
|
72
|
+
|
|
73
|
+
export const REJECT: SubLlmHandlers = Object.freeze({
|
|
74
|
+
llmQuery: async () => UNCONFIGURED,
|
|
75
|
+
llmBatch: rejectBatch,
|
|
76
|
+
rlmQuery: async () => UNCONFIGURED,
|
|
77
|
+
rlmBatch: rejectBatch,
|
|
78
|
+
awaitTask: async () => UNCONFIGURED,
|
|
79
|
+
finishTask: async () => UNCONFIGURED,
|
|
80
|
+
addContext: async () => {
|
|
81
|
+
throw new Error("add_context not configured");
|
|
82
|
+
},
|
|
83
|
+
});
|
|
76
84
|
|
|
77
|
-
/** Body of a reply frame — the union of every handler's payload shape. */
|
|
78
85
|
export interface ReplyBody {
|
|
79
86
|
response?: string;
|
|
80
87
|
responses?: string[];
|
|
@@ -91,11 +98,138 @@ export interface ReplyBody {
|
|
|
91
98
|
error?: string;
|
|
92
99
|
}
|
|
93
100
|
|
|
101
|
+
const RLM_PATH_TYPES = new Set(["rlm_query", "rlm_batch"]);
|
|
102
|
+
|
|
103
|
+
/** Narrow unknown to SpawnResult-shaped object from createSubcallHandlers. */
|
|
104
|
+
function isSpawnResult(
|
|
105
|
+
value: unknown,
|
|
106
|
+
): value is {
|
|
107
|
+
readonly ok: boolean;
|
|
108
|
+
readonly task_id: string | null;
|
|
109
|
+
readonly kind: string;
|
|
110
|
+
readonly error?: string;
|
|
111
|
+
} {
|
|
112
|
+
if (typeof value !== "object" || value === null) return false;
|
|
113
|
+
const o = value as Record<string, unknown>;
|
|
114
|
+
return (
|
|
115
|
+
typeof o.ok === "boolean" &&
|
|
116
|
+
(typeof o.task_id === "string" || o.task_id === null) &&
|
|
117
|
+
typeof o.kind === "string" &&
|
|
118
|
+
typeof o.status === "string"
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Narrow unknown to AwaitResult-shaped object. */
|
|
123
|
+
function isAwaitResult(
|
|
124
|
+
value: unknown,
|
|
125
|
+
): value is {
|
|
126
|
+
readonly ok: boolean;
|
|
127
|
+
readonly result?: string;
|
|
128
|
+
readonly results?: readonly string[];
|
|
129
|
+
readonly error?: string;
|
|
130
|
+
} {
|
|
131
|
+
if (typeof value !== "object" || value === null) return false;
|
|
132
|
+
const o = value as Record<string, unknown>;
|
|
133
|
+
return typeof o.ok === "boolean" && typeof o.task_id === "string" && typeof o.status === "string";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resolve a handler return value to a single `response` string for the worker.
|
|
138
|
+
* Accepts plain string stubs OR SpawnResult (awaits to completion).
|
|
139
|
+
*/
|
|
140
|
+
async function resolveSingle(
|
|
141
|
+
h: SubLlmHandlers,
|
|
142
|
+
raw: unknown,
|
|
143
|
+
depth: number,
|
|
144
|
+
opts: SubcallOpts,
|
|
145
|
+
): Promise<ReplyBody> {
|
|
146
|
+
if (typeof raw === "string") {
|
|
147
|
+
return { response: raw };
|
|
148
|
+
}
|
|
149
|
+
if (isSpawnResult(raw)) {
|
|
150
|
+
if (!raw.ok || raw.task_id === null) {
|
|
151
|
+
const err = raw.error ?? "spawn failed";
|
|
152
|
+
return { error: err, response: formatError(err) };
|
|
153
|
+
}
|
|
154
|
+
const collected = await h.awaitTask(raw.task_id, undefined, undefined, depth, opts);
|
|
155
|
+
if (typeof collected === "string") {
|
|
156
|
+
return { response: collected };
|
|
157
|
+
}
|
|
158
|
+
if (isAwaitResult(collected)) {
|
|
159
|
+
if (!collected.ok && collected.error !== undefined) {
|
|
160
|
+
return { error: collected.error, response: formatError(collected.error) };
|
|
161
|
+
}
|
|
162
|
+
return { response: collected.result ?? "" };
|
|
163
|
+
}
|
|
164
|
+
return { response: String(collected ?? "") };
|
|
165
|
+
}
|
|
166
|
+
// Unexpected shape — surface as text rather than crash the worker.
|
|
167
|
+
return { response: String(raw ?? "") };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Resolve a handler return value to `responses: string[]` for the worker batch reducer.
|
|
172
|
+
*/
|
|
173
|
+
async function resolveBatch(
|
|
174
|
+
h: SubLlmHandlers,
|
|
175
|
+
raw: unknown,
|
|
176
|
+
expectedN: number,
|
|
177
|
+
depth: number,
|
|
178
|
+
opts: SubcallOpts,
|
|
179
|
+
): Promise<ReplyBody> {
|
|
180
|
+
if (Array.isArray(raw)) {
|
|
181
|
+
const responses = raw.map((x) => (typeof x === "string" ? x : String(x)));
|
|
182
|
+
return { responses };
|
|
183
|
+
}
|
|
184
|
+
if (isSpawnResult(raw)) {
|
|
185
|
+
if (!raw.ok || raw.task_id === null) {
|
|
186
|
+
const err = raw.error ?? "spawn failed";
|
|
187
|
+
const msg = formatError(err);
|
|
188
|
+
return {
|
|
189
|
+
error: err,
|
|
190
|
+
responses: Array.from({ length: Math.max(1, expectedN) }, () => msg),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const collected = await h.awaitTask(raw.task_id, undefined, undefined, depth, opts);
|
|
194
|
+
if (Array.isArray(collected)) {
|
|
195
|
+
return { responses: collected.map(String) };
|
|
196
|
+
}
|
|
197
|
+
if (isAwaitResult(collected)) {
|
|
198
|
+
if (collected.results !== undefined) {
|
|
199
|
+
return { responses: [...collected.results] };
|
|
200
|
+
}
|
|
201
|
+
if (!collected.ok && collected.error !== undefined) {
|
|
202
|
+
const msg = formatError(collected.error);
|
|
203
|
+
return {
|
|
204
|
+
error: collected.error,
|
|
205
|
+
responses: Array.from({ length: Math.max(1, expectedN) }, () => msg),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (collected.result !== undefined) {
|
|
209
|
+
return { responses: [collected.result] };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
error: "malformed batch await result",
|
|
214
|
+
responses: Array.from({ length: Math.max(1, expectedN) }, () =>
|
|
215
|
+
formatError("malformed batch await result"),
|
|
216
|
+
),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (typeof raw === "string") {
|
|
220
|
+
return { responses: [raw] };
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
error: "malformed batch handler result",
|
|
224
|
+
responses: Array.from({ length: Math.max(1, expectedN) }, () =>
|
|
225
|
+
formatError("malformed batch handler result"),
|
|
226
|
+
),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
94
230
|
/**
|
|
95
231
|
* Service one interrupt and hand the reply body to `reply`.
|
|
96
|
-
*
|
|
97
|
-
* Errors are replied, never thrown: the caller invokes this from the stdio pump, where a
|
|
98
|
-
* rejection would surface as an unhandled promise and leave the worker parked forever.
|
|
232
|
+
* Errors are replied, never thrown.
|
|
99
233
|
*/
|
|
100
234
|
export async function serviceInterrupt(
|
|
101
235
|
msg: WorkerInterrupt,
|
|
@@ -103,44 +237,93 @@ export async function serviceInterrupt(
|
|
|
103
237
|
reply: (rid: string, body: ReplyBody) => void,
|
|
104
238
|
): Promise<void> {
|
|
105
239
|
const d = msg.depth;
|
|
240
|
+
const paths =
|
|
241
|
+
"paths" in msg && RLM_PATH_TYPES.has(msg.type)
|
|
242
|
+
? toStringArray(msg.paths)
|
|
243
|
+
: undefined;
|
|
106
244
|
const opts: SubcallOpts = Object.freeze({
|
|
107
245
|
detached: msg.detached === true,
|
|
108
|
-
|
|
109
|
-
paths: msg.type === "rlm_query" || msg.type === "rlm_query_batched"
|
|
110
|
-
? toStringArray(msg.paths)
|
|
111
|
-
: undefined,
|
|
246
|
+
paths,
|
|
112
247
|
});
|
|
248
|
+
|
|
113
249
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
250
|
+
switch (msg.type) {
|
|
251
|
+
case "llm_query": {
|
|
252
|
+
const raw = await h.llmQuery(msg.prompt ?? "", d, opts);
|
|
253
|
+
reply(msg.rid, await resolveSingle(h, raw, d, opts));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
case "rlm_query": {
|
|
257
|
+
const raw = await h.rlmQuery(msg.prompt ?? "", d, opts);
|
|
258
|
+
reply(msg.rid, await resolveSingle(h, raw, d, opts));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
case "llm_batch": {
|
|
262
|
+
const prompts = msg.prompts ?? [];
|
|
263
|
+
const raw = await h.llmBatch(prompts, d, opts);
|
|
264
|
+
reply(msg.rid, await resolveBatch(h, raw, prompts.length, d, opts));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
case "rlm_batch": {
|
|
268
|
+
const tasks = msg.tasks ?? msg.prompts ?? [];
|
|
269
|
+
const raw = await h.rlmBatch(tasks, d, opts);
|
|
270
|
+
reply(msg.rid, await resolveBatch(h, raw, tasks.length, d, opts));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
case "await": {
|
|
274
|
+
// Host-level await (orchestrator tools). Worker uses Task + await_task in-process.
|
|
275
|
+
const result = await h.awaitTask(
|
|
276
|
+
msg.task_id,
|
|
277
|
+
msg.task_ids,
|
|
278
|
+
msg.timeout_s,
|
|
279
|
+
d,
|
|
280
|
+
opts,
|
|
281
|
+
);
|
|
282
|
+
if (typeof result === "string") {
|
|
283
|
+
reply(msg.rid, { response: result });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (isAwaitResult(result)) {
|
|
287
|
+
if (result.results !== undefined) {
|
|
288
|
+
reply(msg.rid, { responses: [...result.results] });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
reply(msg.rid, {
|
|
292
|
+
response: result.result ?? "",
|
|
293
|
+
error: result.ok ? undefined : result.error,
|
|
294
|
+
});
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
reply(msg.rid, { response: String(result ?? "") });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
case "finish": {
|
|
301
|
+
const result = await h.finishTask(msg.summary ?? "", d, opts);
|
|
302
|
+
// finish is not reduced by the worker as content — stringify is fine
|
|
130
303
|
reply(msg.rid, {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
path_prefix: lib.pathPrefix,
|
|
136
|
-
documents: lib.documents ?? 0,
|
|
137
|
-
converted: lib.converted ?? 0,
|
|
138
|
-
skipped: lib.skipped,
|
|
304
|
+
response:
|
|
305
|
+
typeof result === "string"
|
|
306
|
+
? result
|
|
307
|
+
: JSON.stringify(result ?? { ok: true, finished: true }),
|
|
139
308
|
});
|
|
140
|
-
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
case "add_context": {
|
|
312
|
+
const lib = await h.addContext(msg.source ?? "", d);
|
|
313
|
+
if (lib.alreadyLoaded) {
|
|
314
|
+
reply(msg.rid, {
|
|
315
|
+
already_loaded: true,
|
|
316
|
+
files: 0,
|
|
317
|
+
chars: lib.chars,
|
|
318
|
+
source_id: lib.sourceId,
|
|
319
|
+
path_prefix: lib.pathPrefix,
|
|
320
|
+
documents: lib.documents ?? 0,
|
|
321
|
+
converted: lib.converted ?? 0,
|
|
322
|
+
skipped: lib.skipped,
|
|
323
|
+
});
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
141
326
|
const { path, json: isJson } = await writeContextTempFile(lib.payload);
|
|
142
|
-
// Worker reads then unlinks (worker._add_context). Host must not unlink here —
|
|
143
|
-
// if the worker is SIGKILLed before os.remove, the temp file leaks in tmpdir (acceptable).
|
|
144
327
|
reply(msg.rid, {
|
|
145
328
|
path,
|
|
146
329
|
json: isJson,
|
|
@@ -152,9 +335,16 @@ export async function serviceInterrupt(
|
|
|
152
335
|
converted: lib.converted ?? 0,
|
|
153
336
|
skipped: lib.skipped,
|
|
154
337
|
});
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
default: {
|
|
341
|
+
const _exhaustive: never = msg;
|
|
342
|
+
reply((_exhaustive as WorkerInterrupt).rid, {
|
|
343
|
+
error: "unknown interrupt type",
|
|
344
|
+
});
|
|
155
345
|
}
|
|
156
346
|
}
|
|
157
|
-
} catch (err) {
|
|
347
|
+
} catch (err: unknown) {
|
|
158
348
|
reply(msg.rid, { error: errorMessage(err) });
|
|
159
349
|
}
|
|
160
350
|
}
|
package/src/sandbox/protocol.ts
CHANGED
|
@@ -4,12 +4,20 @@
|
|
|
4
4
|
* Newline-delimited JSON over the worker's stdin/stdout — no sockets, no HTTP.
|
|
5
5
|
* Parent -> worker: requests (exec/load_context/shutdown) and llm replies.
|
|
6
6
|
* Worker -> parent: request responses and mid-exec sub-LLM interrupts.
|
|
7
|
+
*
|
|
8
|
+
* Canonical api_v5 kinds only — no legacy `*_query_batched` wire names.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
/** Requests the parent sends to the worker. */
|
|
10
12
|
export type WorkerRequest =
|
|
11
13
|
| { readonly id: string; readonly type: "exec"; readonly code: string }
|
|
12
|
-
| {
|
|
14
|
+
| {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly type: "load_context";
|
|
17
|
+
readonly path: string;
|
|
18
|
+
readonly index?: number;
|
|
19
|
+
readonly json: boolean;
|
|
20
|
+
}
|
|
13
21
|
| { readonly id: string; readonly type: "shutdown" };
|
|
14
22
|
|
|
15
23
|
/** Reply the parent sends to satisfy a sub-LLM interrupt. */
|
|
@@ -50,27 +58,27 @@ export interface WorkerResponse {
|
|
|
50
58
|
readonly answer_content?: string;
|
|
51
59
|
readonly raised?: boolean;
|
|
52
60
|
readonly execution_time?: number;
|
|
53
|
-
// user-created variable names after this exec
|
|
61
|
+
// user-created variable names after this exec
|
|
54
62
|
readonly var_names?: readonly string[];
|
|
55
63
|
// load_context:
|
|
56
64
|
readonly index?: number;
|
|
57
65
|
}
|
|
58
66
|
|
|
59
|
-
/**
|
|
67
|
+
/** Canonical interrupt kinds (api_v5). */
|
|
60
68
|
export type InterruptKind =
|
|
61
69
|
| "llm_query"
|
|
62
|
-
| "llm_query_batched"
|
|
63
70
|
| "rlm_query"
|
|
64
|
-
| "
|
|
71
|
+
| "llm_batch"
|
|
72
|
+
| "rlm_batch"
|
|
73
|
+
| "await"
|
|
74
|
+
| "finish"
|
|
65
75
|
| "add_context";
|
|
66
76
|
|
|
67
77
|
interface InterruptBase {
|
|
68
78
|
readonly rid: string;
|
|
69
79
|
readonly depth: number;
|
|
70
80
|
/**
|
|
71
|
-
*
|
|
72
|
-
* must not attach it to that invocation's emitter or LimitGuard. Absent on the
|
|
73
|
-
* synchronous path.
|
|
81
|
+
* Detached work may outlive the exec that issued it.
|
|
74
82
|
*/
|
|
75
83
|
readonly detached?: boolean;
|
|
76
84
|
}
|
|
@@ -78,22 +86,29 @@ interface InterruptBase {
|
|
|
78
86
|
interface PromptInterrupt extends InterruptBase {
|
|
79
87
|
readonly type: "llm_query" | "rlm_query";
|
|
80
88
|
readonly prompt?: string;
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* `rlm_query` only — path prefixes narrowing the child's inherited context. Never sent for
|
|
84
|
-
* `llm_query`, whose frame stays byte-identical to before.
|
|
85
|
-
*/
|
|
89
|
+
/** `rlm_query` only — path prefixes narrowing the child's inherited context. */
|
|
86
90
|
readonly paths?: readonly string[];
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
interface
|
|
90
|
-
readonly type: "
|
|
93
|
+
interface BatchInterrupt extends InterruptBase {
|
|
94
|
+
readonly type: "llm_batch" | "rlm_batch";
|
|
91
95
|
readonly prompts?: readonly string[];
|
|
92
|
-
readonly
|
|
93
|
-
/** `rlm_query_batched` only — one prefix set shared by every prompt in the batch. */
|
|
96
|
+
readonly tasks?: readonly string[];
|
|
94
97
|
readonly paths?: readonly string[];
|
|
95
98
|
}
|
|
96
99
|
|
|
100
|
+
interface AwaitInterrupt extends InterruptBase {
|
|
101
|
+
readonly type: "await";
|
|
102
|
+
readonly task_id?: string;
|
|
103
|
+
readonly task_ids?: readonly string[];
|
|
104
|
+
readonly timeout_s?: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface FinishInterrupt extends InterruptBase {
|
|
108
|
+
readonly type: "finish";
|
|
109
|
+
readonly summary?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
97
112
|
export interface AddContextInterrupt extends InterruptBase {
|
|
98
113
|
readonly type: "add_context";
|
|
99
114
|
readonly source?: string;
|
|
@@ -102,18 +117,24 @@ export interface AddContextInterrupt extends InterruptBase {
|
|
|
102
117
|
/** A mid-exec sub-LLM/tool request from the worker. */
|
|
103
118
|
export type WorkerInterrupt =
|
|
104
119
|
| PromptInterrupt
|
|
105
|
-
|
|
|
120
|
+
| BatchInterrupt
|
|
121
|
+
| AwaitInterrupt
|
|
122
|
+
| FinishInterrupt
|
|
106
123
|
| AddContextInterrupt;
|
|
107
124
|
|
|
108
125
|
export type WorkerMessage = WorkerResponse | WorkerInterrupt;
|
|
109
126
|
|
|
110
|
-
export const INTERRUPT_KINDS = Object.freeze(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
127
|
+
export const INTERRUPT_KINDS = Object.freeze(
|
|
128
|
+
new Set<InterruptKind>([
|
|
129
|
+
"llm_query",
|
|
130
|
+
"rlm_query",
|
|
131
|
+
"llm_batch",
|
|
132
|
+
"rlm_batch",
|
|
133
|
+
"await",
|
|
134
|
+
"finish",
|
|
135
|
+
"add_context",
|
|
136
|
+
]),
|
|
137
|
+
);
|
|
117
138
|
|
|
118
139
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
119
140
|
return typeof value === "object" && value !== null;
|
|
@@ -124,11 +145,13 @@ function isWorkerResponse(value: unknown): value is WorkerResponse {
|
|
|
124
145
|
}
|
|
125
146
|
|
|
126
147
|
export function isInterrupt(msg: unknown): msg is WorkerInterrupt {
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
148
|
+
return (
|
|
149
|
+
isRecord(msg) &&
|
|
150
|
+
typeof msg.type === "string" &&
|
|
151
|
+
INTERRUPT_KINDS.has(msg.type as InterruptKind) &&
|
|
152
|
+
typeof msg.rid === "string" &&
|
|
153
|
+
typeof msg.depth === "number"
|
|
154
|
+
);
|
|
132
155
|
}
|
|
133
156
|
|
|
134
157
|
export function isWorkerMessage(msg: unknown): msg is WorkerMessage {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/sandbox/py/guards.py
CHANGED
|
@@ -88,10 +88,13 @@ for _blocked in ("eval", "exec", "compile", "input", "globals", "locals"):
|
|
|
88
88
|
|
|
89
89
|
RESERVED = frozenset(
|
|
90
90
|
{
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
91
|
+
# Canonical api_v5
|
|
92
|
+
"llm_query", "llm_batch",
|
|
93
|
+
"rlm_query", "rlm_batch",
|
|
94
|
+
"await_task", "finish",
|
|
95
|
+
"spawn",
|
|
96
|
+
# Helpers (not the old *_query_batched API)
|
|
97
|
+
"llm_query_chunked", "map_files", "llm_map_reduce",
|
|
95
98
|
"search", "grep_context", "outline",
|
|
96
99
|
"add_context",
|
|
97
100
|
"SHOW_VARS", "answer", "context",
|
|
@@ -126,7 +129,7 @@ def _stall_alarm(exec_timeout_s: float, stall_timeout_s: float):
|
|
|
126
129
|
def _fire(signum, frame): # noqa: ARG001
|
|
127
130
|
raise _StallTimeout(
|
|
128
131
|
f"sub-call stalled — no reply from the host for {stall_timeout_s:g}s "
|
|
129
|
-
"(the task may still be running;
|
|
132
|
+
"(the task may still be running; await_task it again in a later block)"
|
|
130
133
|
)
|
|
131
134
|
|
|
132
135
|
old = signal.signal(signal.SIGALRM, _fire) if use else None
|
|
@@ -15,7 +15,7 @@ from typing import Any
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
_CHUNK_HEADER_OVERHEAD = 64
|
|
18
|
-
_MAX_CHUNK_BATCH = 20 # fan-out per
|
|
18
|
+
_MAX_CHUNK_BATCH = 20 # fan-out per llm_batch call (matches prompt guidance)
|
|
19
19
|
_MAX_CHUNKS = 500 # ceiling: above this, force pre-filtering in Python
|
|
20
20
|
_NUDGE_CHARS = 500_000 # str/bytes vars above this trigger a one-time stdout hint
|
|
21
21
|
|
|
@@ -167,11 +167,14 @@ class _Bm25Index:
|
|
|
167
167
|
out: list[dict[str, Any]] = [None] * len(top) # type: ignore[list-item]
|
|
168
168
|
for i, (idx, score) in enumerate(top):
|
|
169
169
|
text = self.texts[idx]
|
|
170
|
+
snip = text[:_SNIPPET_CHARS]
|
|
171
|
+
# Both `snippet` and `text` so agents never KeyError mixing search vs grep shapes.
|
|
170
172
|
out[i] = {
|
|
171
173
|
"path": self.paths[idx],
|
|
172
174
|
"line": self.starts[idx],
|
|
173
175
|
"score": round(score, 3),
|
|
174
|
-
"snippet":
|
|
176
|
+
"snippet": snip,
|
|
177
|
+
"text": snip,
|
|
175
178
|
}
|
|
176
179
|
return out
|
|
177
180
|
|
|
@@ -179,8 +182,8 @@ class _Bm25Index:
|
|
|
179
182
|
def search(entries: list[tuple[str, str]], index: _Bm25Index, query: str, k: int = 10, path_glob: str | None = None) -> list[dict[str, Any]]:
|
|
180
183
|
"""Rank `context` windows against a natural-language query (BM25).
|
|
181
184
|
|
|
182
|
-
Returns [{path, line, score, snippet}] — pointers, not bodies.
|
|
183
|
-
|
|
185
|
+
Returns [{path, line, score, snippet, text}] — pointers, not bodies.
|
|
186
|
+
`text` is an alias of `snippet` (same as grep_context hits).
|
|
184
187
|
"""
|
|
185
188
|
terms = _tokenize(str(query))
|
|
186
189
|
if not terms:
|
|
@@ -201,9 +204,9 @@ def grep_context(
|
|
|
201
204
|
) -> dict[str, Any]:
|
|
202
205
|
"""Regex over `context`, capped and shaped.
|
|
203
206
|
|
|
204
|
-
Returns {"hits": [{path, line, text}], "counts": {path: n}, "total": n, "truncated": bool}.
|
|
205
|
-
`
|
|
206
|
-
|
|
207
|
+
Returns {"hits": [{path, line, text, snippet}], "counts": {path: n}, "total": n, "truncated": bool}.
|
|
208
|
+
`snippet` is an alias of `text` (same as search hits) to avoid KeyError footguns.
|
|
209
|
+
`counts` is complete even when `hits` is capped.
|
|
207
210
|
"""
|
|
208
211
|
try:
|
|
209
212
|
rx = re.compile(pattern)
|
|
@@ -234,7 +237,13 @@ def grep_context(
|
|
|
234
237
|
continue
|
|
235
238
|
lo = max(0, i - pad_before)
|
|
236
239
|
hi = min(len(lines), i + pad_after + 1)
|
|
237
|
-
|
|
240
|
+
body = "\n".join(lines[lo:hi])[:_SNIPPET_CHARS]
|
|
241
|
+
hits.append({
|
|
242
|
+
"path": path,
|
|
243
|
+
"line": i + 1,
|
|
244
|
+
"text": body,
|
|
245
|
+
"snippet": body,
|
|
246
|
+
})
|
|
238
247
|
return {"hits": hits, "counts": counts, "total": total, "truncated": total > len(hits)}
|
|
239
248
|
|
|
240
249
|
def outline(entries: list[tuple[str, str]], path: str) -> str:
|
package/src/sandbox/py/tasks.py
CHANGED
|
@@ -42,7 +42,7 @@ def _reduce_batch(n: int):
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
def _reduce_chunked(sizes: list[int], drop_empty: bool = True):
|
|
45
|
-
"""Concatenate several
|
|
45
|
+
"""Concatenate several llm_batch replies back into one flat chunk list.
|
|
46
46
|
|
|
47
47
|
drop_empty=True (llm_query_chunked): filter "" replies so results never degrade
|
|
48
48
|
to blank entries; the flattened list may then be shorter than the chunk count,
|