@tshu-w/pi-agents 0.2.1 → 0.2.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/package.json +1 -1
- package/src/programs/codemode.ts +14 -0
- package/src/programs/execute.ts +49 -2
package/package.json
CHANGED
package/src/programs/codemode.ts
CHANGED
|
@@ -25,6 +25,20 @@ export function loadCodemode(): Promise<Codemode> {
|
|
|
25
25
|
return loaded;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Where Pi's executor starts codemode workers from. */
|
|
29
|
+
export interface PiWorkerSource {
|
|
30
|
+
getQuickJSWasmPath(): string;
|
|
31
|
+
getCodemodeWorkerSpecifier(): string | URL | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let workerSource: Promise<PiWorkerSource> | undefined;
|
|
35
|
+
|
|
36
|
+
/** Loads Pi's `dist/config.js`, the module its executor reads them from. */
|
|
37
|
+
export function loadPiWorkerSource(): Promise<PiWorkerSource> {
|
|
38
|
+
workerSource ??= import(pathToFileURL(join(getPackageDir(), "dist", "config.js")).href);
|
|
39
|
+
return workerSource;
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
/** Parts of Pi's `codemode` tool that its package entry does not export. */
|
|
29
43
|
export interface PiCodemode {
|
|
30
44
|
/** Pi's `codemode` tool definition; `program` reuses its `prepareLoadout` and, since Pi 1.2.0, `reachesTools`. */
|
package/src/programs/execute.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
7
|
import type { CodemodeResult, CodemodeTool } from "@earendil-works/pi-codemode";
|
|
8
8
|
import type { AgentToolResult, AgentToolUpdateCallback, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { loadCodemode, loadPiCodemode } from "./codemode.ts";
|
|
9
|
+
import { loadCodemode, loadPiCodemode, loadPiWorkerSource } from "./codemode.ts";
|
|
10
10
|
import type { DetachedCallOptions } from "./host-patches.ts";
|
|
11
11
|
|
|
12
12
|
export const PROGRAM_TOOL_NAME = "program";
|
|
@@ -104,20 +104,67 @@ interface SandboxExtras {
|
|
|
104
104
|
const sandboxExtras = new AsyncLocalStorage<SandboxExtras>();
|
|
105
105
|
let sandboxPatch: Promise<void> | undefined;
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Since Pi 1.3.0, its executor admits at most two codemode workers per process, which background
|
|
109
|
+
* Programs waiting on Agents use up. A Program's worker is admitted on its own terms instead: any
|
|
110
|
+
* number may run, and none starts while this many are still exiting after their scripts ended.
|
|
111
|
+
*/
|
|
112
|
+
const MAX_EXITING_WORKERS = 2;
|
|
113
|
+
const exitingWorkers = new Set<{ exited: boolean }>();
|
|
114
|
+
|
|
115
|
+
/** A launcher for one Program's worker, from where Pi's executor starts its own. */
|
|
116
|
+
function programWorker(codemode: Awaited<ReturnType<typeof loadCodemode>>, wasmPath: string, workerUrl: string | URL | undefined) {
|
|
117
|
+
let worker: { exited: boolean } | undefined;
|
|
118
|
+
let ended = false;
|
|
119
|
+
const options = {
|
|
120
|
+
wasm: codemode.loadQuickJSWasm(wasmPath),
|
|
121
|
+
workerUrl,
|
|
122
|
+
acquireWorker: () => {
|
|
123
|
+
// The script can end while the QuickJS module loads, before its worker starts.
|
|
124
|
+
if (ended) throw new Error("The Program ended before its worker started.");
|
|
125
|
+
if (exitingWorkers.size >= MAX_EXITING_WORKERS) {
|
|
126
|
+
throw new Error(`${exitingWorkers.size} workers of ended Programs are still shutting down. Retry after they exit.`);
|
|
127
|
+
}
|
|
128
|
+
const started = { exited: false };
|
|
129
|
+
worker = started;
|
|
130
|
+
return () => {
|
|
131
|
+
started.exited = true;
|
|
132
|
+
exitingWorkers.delete(started);
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
const sandbox = new codemode.CodemodeSandbox(options) as unknown as Record<string, unknown>;
|
|
137
|
+
return {
|
|
138
|
+
launch: sandbox.launch,
|
|
139
|
+
ended() {
|
|
140
|
+
ended = true;
|
|
141
|
+
if (worker && !worker.exited) exitingWorkers.add(worker);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
107
146
|
async function patchSandbox(): Promise<void> {
|
|
108
|
-
const
|
|
147
|
+
const [codemode, source] = await Promise.all([loadCodemode(), loadPiWorkerSource()]);
|
|
148
|
+
const wasmPath = source.getQuickJSWasmPath();
|
|
149
|
+
const workerUrl = source.getCodemodeWorkerSpecifier();
|
|
150
|
+
const proto = codemode.CodemodeSandbox.prototype as unknown as Record<string, any>;
|
|
109
151
|
const execute = proto.execute;
|
|
110
152
|
proto.execute = function (this: Record<string, any>, code: string, options: Record<string, unknown> = {}) {
|
|
111
153
|
const current = sandboxExtras.getStore();
|
|
112
154
|
if (!current || current.used) return execute.call(this, code, options);
|
|
113
155
|
current.used = true;
|
|
114
156
|
for (const global of current.globals) this.globalsByName.set(global.name, global);
|
|
157
|
+
// Before Pi 1.2.0, the sandbox starts its worker itself, with no limit.
|
|
158
|
+
const worker = typeof this.launch === "function" ? programWorker(codemode, wasmPath, workerUrl) : undefined;
|
|
159
|
+
if (worker) this.launch = worker.launch;
|
|
115
160
|
const timeoutMs = Math.min(current.timeoutMs, (options.timeoutMs as number | undefined) ?? this.timeoutMs);
|
|
116
161
|
return execute.call(this, current.prepare(code), { ...options, timeoutMs }).then((result: CodemodeResult) => {
|
|
162
|
+
worker?.ended();
|
|
117
163
|
current.result = result;
|
|
118
164
|
current.ended();
|
|
119
165
|
return result;
|
|
120
166
|
}, (error: unknown) => {
|
|
167
|
+
worker?.ended();
|
|
121
168
|
current.ended();
|
|
122
169
|
throw error;
|
|
123
170
|
});
|