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/CHANGELOG.md +6 -0
- package/README.md +37 -2
- package/package.json +2 -2
- package/src/index.ts +13 -0
- package/src/pi-broker.ts +353 -6
- package/src/pi-container-driver.ts +74 -3
- package/src/pi-protocol.ts +118 -0
- package/src/pi-runtime.ts +67 -11
- package/worker/pi-compaction.ts +225 -0
- package/worker/pi-main.ts +96 -13
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
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
}
|