pi-roundtable-sandbox 0.7.8 → 0.7.10

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/worker/pi-main.ts CHANGED
@@ -9,7 +9,6 @@ import {
9
9
  DefaultResourceLoader,
10
10
  ModelRuntime,
11
11
  SessionManager,
12
- SettingsManager,
13
12
  } from "@earendil-works/pi-coding-agent";
14
13
  import {
15
14
  activeToolsExtension,
@@ -29,6 +28,7 @@ import {
29
28
  type PiTurnRequest as PartyTurnRequest,
30
29
  type PiTurnResponse as PartyTurnResponse,
31
30
  PI_MEDIA_LIMITS,
31
+ type PiWorkerConfig,
32
32
  safeFileName,
33
33
  type PiTurnContext as TurnContext,
34
34
  validatePiTurn,
@@ -36,6 +36,7 @@ import {
36
36
  PI_OUTBOX as WORKSPACE_OUTBOX,
37
37
  } from "../src/pi-protocol.ts";
38
38
  import { boundedText } from "../src/protocol.ts";
39
+ import { WorkerCompaction } from "./pi-compaction.ts";
39
40
  import type { PiWorkerContent } from "./pi-content.ts";
40
41
  import { speakerMemoryExtension } from "./pi-memory.ts";
41
42
  import {
@@ -46,6 +47,8 @@ import {
46
47
  import { boundedFile, brokerToolsExtension } from "./pi-tools.ts";
47
48
 
48
49
  let content: PiWorkerContent;
50
+ /** The session's compaction, whose reports must reach the host before the turn's result. */
51
+ let workerCompaction: WorkerCompaction | undefined;
49
52
  let baseTools: string[] = [];
50
53
  const THINKING = "low";
51
54
  const MCP_CONNECT_TIMEOUT_MS = 45_000;
@@ -94,6 +97,7 @@ function startForwarder(): void {
94
97
  // pi-lens-ignore: unchecked-throwing-call -- the server builds request.url, always an absolute URL
95
98
  const url = new URL(request.url);
96
99
  const hasBody = request.method !== "GET" && request.method !== "HEAD";
100
+ const started = Date.now();
97
101
  const response = await fetch(
98
102
  `http://broker${url.pathname}${url.search}`,
99
103
  {
@@ -105,7 +109,20 @@ function startForwarder(): void {
105
109
  : undefined,
106
110
  signal: request.signal,
107
111
  },
108
- );
112
+ ).catch((error: unknown) => {
113
+ log("broker call failed", {
114
+ path: url.pathname,
115
+ latencyMs: Date.now() - started,
116
+ error: String(error),
117
+ });
118
+ throw error;
119
+ });
120
+ if (response.status >= 400)
121
+ log("broker call failed", {
122
+ path: url.pathname,
123
+ status: response.status,
124
+ latencyMs: Date.now() - started,
125
+ });
109
126
  return new Response(response.body, {
110
127
  status: response.status,
111
128
  statusText: response.statusText,
@@ -127,6 +144,22 @@ async function mcpServers(): Promise<McpToolsResponse["servers"]> {
127
144
  return ((await response.json()) as McpToolsResponse).servers;
128
145
  }
129
146
 
147
+ /** Whether the host compacts for this worker; an older broker without the route has no compactor. */
148
+ async function workerConfig(): Promise<PiWorkerConfig> {
149
+ // Local Unix-socket transport; no TCP connection or DNS lookup is made.
150
+ // nosemgrep: typescript.react.security.react-insecure-request.react-insecure-request
151
+ const response = await fetch("http://broker/worker/config", {
152
+ unix: join(CONTAINER_RUN_DIR, BROKER_SOCKET),
153
+ });
154
+ if (response.status === 404) {
155
+ await response.body?.cancel();
156
+ return {};
157
+ }
158
+ if (!response.ok)
159
+ throw new Error(`the broker answered ${response.status} for worker/config`);
160
+ return (await response.json()) as PiWorkerConfig;
161
+ }
162
+
130
163
  /** pi-mcp-adapter registers tools only after its eager connection completes. */
131
164
  async function waitForTools(
132
165
  session: AgentSession,
@@ -142,6 +175,7 @@ async function waitForTools(
142
175
 
143
176
  async function createSession(
144
177
  servers: McpToolsResponse["servers"],
178
+ config: PiWorkerConfig,
145
179
  ): Promise<AgentSession> {
146
180
  const SKILLS = content.skillsDir ? loadSkillIndex(content.skillsDir) : [];
147
181
  const agentDir = process.env.PI_CODING_AGENT_DIR ?? "/tmp/pi-agent";
@@ -151,6 +185,19 @@ async function createSession(
151
185
  const modelRuntime = await ModelRuntime.create({
152
186
  authPath: join(agentDir, "auth.json"),
153
187
  });
188
+ const sessionManager = SessionManager.continueRecent(
189
+ CONTAINER_WORKSPACE,
190
+ sessionDir,
191
+ );
192
+ const compaction = new WorkerCompaction({
193
+ history: sessionManager,
194
+ contextWindow: (provider, id) =>
195
+ modelRuntime.getModel(provider, id)?.contextWindow,
196
+ config,
197
+ socket: join(CONTAINER_RUN_DIR, BROKER_SOCKET),
198
+ log,
199
+ });
200
+ workerCompaction = compaction;
154
201
  const resourceLoader = new DefaultResourceLoader({
155
202
  cwd: CONTAINER_WORKSPACE,
156
203
  agentDir,
@@ -189,6 +236,7 @@ async function createSession(
189
236
  factory: speakerMemoryExtension(() => turnContext),
190
237
  },
191
238
  ...(content.extensions?.(() => turnContext) ?? []),
239
+ ...compaction.extensions(),
192
240
  ...(servers.length > 0
193
241
  ? [
194
242
  {
@@ -218,11 +266,10 @@ async function createSession(
218
266
  thinkingLevel: THINKING,
219
267
  modelRuntime,
220
268
  resourceLoader,
221
- sessionManager: SessionManager.continueRecent(
222
- CONTAINER_WORKSPACE,
223
- sessionDir,
224
- ),
225
- settingsManager: SettingsManager.inMemory({}),
269
+ sessionManager,
270
+ // Large windows compact at the soft threshold through the host's compactor, and through Pi's
271
+ // summary past the hard ceiling, as the host's own sessions do.
272
+ settingsManager: compaction.settings(),
226
273
  noTools: "builtin",
227
274
  });
228
275
  const model = modelRuntime.getModel(content.model.provider, content.model.id);
@@ -240,6 +287,17 @@ async function createSession(
240
287
  );
241
288
  if (missing.length > 0)
242
289
  throw new Error(`tools are not registered: ${missing.join(", ")}`);
290
+ session.subscribe((event) => {
291
+ if (event.type === "compaction_end") void compaction.ended(event, session);
292
+ else if (event.type === "auto_retry_start")
293
+ log("model call retried", {
294
+ attempt: event.attempt,
295
+ error: event.errorMessage,
296
+ });
297
+ });
298
+ log("compaction ready", {
299
+ hostCompactor: config.compaction?.engine ?? null,
300
+ });
243
301
  return session;
244
302
  }
245
303
 
@@ -262,6 +320,21 @@ async function runTurn(
262
320
  const toolCalls: string[] = [];
263
321
  const unsubscribe = session.subscribe((event) => {
264
322
  if (event.type === "tool_execution_start") toolCalls.push(event.toolName);
323
+ else if (event.type === "tool_execution_end" && event.isError)
324
+ log("tool failed", {
325
+ turnId: turn.turnId,
326
+ tool: event.toolName,
327
+ error: textOf(event.result?.content ?? []).slice(0, 2000),
328
+ });
329
+ else if (
330
+ event.type === "message_end" &&
331
+ event.message.role === "assistant" &&
332
+ event.message.stopReason === "error"
333
+ )
334
+ log("model call failed", {
335
+ turnId: turn.turnId,
336
+ error: event.message.errorMessage,
337
+ });
265
338
  });
266
339
  try {
267
340
  await session.prompt(
@@ -276,6 +349,7 @@ async function runTurn(
276
349
  : undefined,
277
350
  );
278
351
  } catch (error) {
352
+ log("prompt failed", { turnId: turn.turnId, error: String(error) });
279
353
  return { ok: false, error: `prompt failed: ${String(error)}` };
280
354
  } finally {
281
355
  clearTimeout(timer);
@@ -327,10 +401,12 @@ async function main(): Promise<void> {
327
401
 
328
402
  startForwarder();
329
403
  const servers = await mcpServers();
330
- const session = await createSession(servers);
404
+ const session = await createSession(servers, await workerConfig());
331
405
 
332
406
  const brokerSocket = join(CONTAINER_RUN_DIR, BROKER_SOCKET);
333
407
  log("sandbox worker ready", { profile, tools: activeTools.length });
408
+ // A broker that stays unreachable is logged once, not every quarter second.
409
+ let pollFailing = false;
334
410
  for (;;) {
335
411
  try {
336
412
  // Local Unix-socket transport; no TCP connection or DNS lookup is made.
@@ -356,10 +432,15 @@ async function main(): Promise<void> {
356
432
  await boundedText(next.body, 96 * 1024 * 1024),
357
433
  );
358
434
  validatePiTurn(turn);
359
- const result = await runTurn(session, turn).catch(() => ({
360
- ok: false as const,
361
- error: "Worker output failed",
362
- }));
435
+ pollFailing = false;
436
+ const result = await runTurn(session, turn).catch((error: unknown) => {
437
+ log("turn output failed", {
438
+ turnId: turn.turnId,
439
+ error: String(error),
440
+ });
441
+ return { ok: false as const, error: "Worker output failed" };
442
+ });
443
+ await workerCompaction?.settled();
363
444
  // Local Unix-socket transport; no TCP connection or DNS lookup is made.
364
445
  // nosemgrep: typescript.react.security.react-insecure-request.react-insecure-request
365
446
  const sent = await fetch("http://broker/worker/result", {
@@ -370,7 +451,9 @@ async function main(): Promise<void> {
370
451
  signal: AbortSignal.timeout(60_000),
371
452
  });
372
453
  await sent.body?.cancel();
373
- } catch {
454
+ } catch (error) {
455
+ if (!pollFailing) log("broker poll failed", { error: String(error) });
456
+ pollFailing = true;
374
457
  await Bun.sleep(250);
375
458
  }
376
459
  }