@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tshu-w/pi-agents",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Multi-agent runtime for Pi: persistent async agents, dynamic workflows, and inter-agent messaging",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -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`. */
@@ -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 proto = (await loadCodemode()).CodemodeSandbox.prototype as unknown as Record<string, any>;
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
  });