@ai-sdk/harness-pi 1.0.104 → 1.0.105
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 +11 -0
- package/dist/index.js +52 -21
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/pi-session.ts +65 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness-pi",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.105",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ai-sdk/harness": "1.0.
|
|
30
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
29
|
+
"@ai-sdk/harness": "1.0.103",
|
|
30
|
+
"@ai-sdk/provider-utils": "5.0.37",
|
|
31
31
|
"@earendil-works/pi-ai": "0.74.2",
|
|
32
32
|
"@earendil-works/pi-coding-agent": "^0.84.3",
|
|
33
33
|
"pi-mcp-adapter": "2.12.1",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"zod": "^3.25.76 || ^4.1.8"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@ai-sdk/sandbox-just-bash": "1.0.
|
|
40
|
+
"@ai-sdk/sandbox-just-bash": "1.0.103",
|
|
41
41
|
"@types/node": "22.19.19",
|
|
42
42
|
"@vercel/ai-tsconfig": "0.0.0",
|
|
43
43
|
"tsup": "^8.5.1",
|
package/src/pi-session.ts
CHANGED
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
type Skill,
|
|
12
12
|
type ToolDefinition,
|
|
13
13
|
} from '@earendil-works/pi-coding-agent';
|
|
14
|
-
import {
|
|
14
|
+
import { randomUUID } from 'node:crypto';
|
|
15
|
+
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
15
16
|
import { tmpdir } from 'node:os';
|
|
16
17
|
import path from 'node:path';
|
|
17
18
|
import { Type } from 'typebox';
|
|
@@ -282,6 +283,36 @@ interface DeferredRerunBarrier {
|
|
|
282
283
|
readonly cancel: (reason?: unknown) => void;
|
|
283
284
|
}
|
|
284
285
|
|
|
286
|
+
async function isWorkspaceAvailableOnHost({
|
|
287
|
+
sandbox,
|
|
288
|
+
sessionWorkDir,
|
|
289
|
+
}: {
|
|
290
|
+
sandbox: SandboxSession;
|
|
291
|
+
sessionWorkDir: string;
|
|
292
|
+
}): Promise<boolean> {
|
|
293
|
+
// A host path existing at the same location is not enough to prove that it
|
|
294
|
+
// belongs to the sandbox. Round-trip a unique marker through the sandbox
|
|
295
|
+
// filesystem API before using the workspace directly.
|
|
296
|
+
const probePath = path.join(
|
|
297
|
+
sessionWorkDir,
|
|
298
|
+
`.ai-sdk-harness-pi-${randomUUID()}`,
|
|
299
|
+
);
|
|
300
|
+
const probeContent = randomUUID();
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
await writeFile(probePath, probeContent, { flag: 'wx' });
|
|
304
|
+
const sandboxContent = await sandbox.readBinaryFile({ path: probePath });
|
|
305
|
+
return (
|
|
306
|
+
sandboxContent != null &&
|
|
307
|
+
Buffer.from(sandboxContent).equals(Buffer.from(probeContent))
|
|
308
|
+
);
|
|
309
|
+
} catch {
|
|
310
|
+
return false;
|
|
311
|
+
} finally {
|
|
312
|
+
await rm(probePath, { force: true }).catch(() => {});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
285
316
|
export async function createPiSession(
|
|
286
317
|
input: CreatePiSessionInput,
|
|
287
318
|
): Promise<HarnessV1Session> {
|
|
@@ -301,28 +332,34 @@ export async function createPiSession(
|
|
|
301
332
|
// sub-directory tree on disk.
|
|
302
333
|
const safeSessionId = input.sessionId.replace(/[\\/: ]/g, '-');
|
|
303
334
|
const hostRoot = path.join(tmpdir(), 'ai-sdk-harness', 'pi', safeSessionId);
|
|
304
|
-
const hostWorkDir = path.join(hostRoot, 'workspace');
|
|
305
335
|
const hostAgentDir = path.join(hostRoot, 'agent');
|
|
306
336
|
const hostSessionDir = path.join(hostRoot, 'sessions');
|
|
337
|
+
const toolSafeSandboxSession = getRestrictedSandboxSession(
|
|
338
|
+
input.sandboxSession,
|
|
339
|
+
);
|
|
307
340
|
|
|
308
341
|
// Pi runs in this host process but must behave as though it lives in the
|
|
309
342
|
// sandbox workspace: its working directory is the real `sessionWorkDir`
|
|
310
343
|
// (where `setup()` clones and where the sandbox-backed tools operate), so the
|
|
311
344
|
// paths Pi advertises to the model — most notably the "Current working
|
|
312
|
-
// directory" line in its system prompt — resolve inside the sandbox.
|
|
313
|
-
// workspace VFS maps that sandbox path
|
|
314
|
-
// `fs`-based resource loading
|
|
315
|
-
//
|
|
316
|
-
//
|
|
345
|
+
// directory" line in its system prompt — resolve inside the sandbox. When
|
|
346
|
+
// the sandbox filesystem is remote, the workspace VFS maps that sandbox path
|
|
347
|
+
// to a scoped host mirror for Pi's own `fs`-based resource loading. When the
|
|
348
|
+
// sandbox and harness share a filesystem, Pi uses the workspace directly so
|
|
349
|
+
// extensions can inspect project files beyond the scoped resource paths.
|
|
317
350
|
const sessionWorkDir = input.sessionWorkDir;
|
|
351
|
+
const workspaceAvailableOnHost = await isWorkspaceAvailableOnHost({
|
|
352
|
+
sandbox: toolSafeSandboxSession,
|
|
353
|
+
sessionWorkDir,
|
|
354
|
+
});
|
|
355
|
+
const hostWorkDir = workspaceAvailableOnHost
|
|
356
|
+
? path.resolve(sessionWorkDir)
|
|
357
|
+
: path.join(hostRoot, 'workspace');
|
|
318
358
|
|
|
319
359
|
await mkdir(hostWorkDir, { recursive: true });
|
|
320
360
|
await mkdir(hostAgentDir, { recursive: true });
|
|
321
361
|
await mkdir(hostSessionDir, { recursive: true });
|
|
322
362
|
|
|
323
|
-
const toolSafeSandboxSession = getRestrictedSandboxSession(
|
|
324
|
-
input.sandboxSession,
|
|
325
|
-
);
|
|
326
363
|
const sandboxHomeDir = await resolveSandboxHomeDir({
|
|
327
364
|
sandbox: toolSafeSandboxSession,
|
|
328
365
|
...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
|
|
@@ -358,11 +395,13 @@ export async function createPiSession(
|
|
|
358
395
|
|
|
359
396
|
// Snapshot sandbox state into the host mirror BEFORE the VFS goes live so
|
|
360
397
|
// Pi sees the workspace as soon as it boots.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
398
|
+
if (!workspaceAvailableOnHost) {
|
|
399
|
+
await syncHostWorkspaceFromSandbox({
|
|
400
|
+
sandbox: toolSafeSandboxSession,
|
|
401
|
+
sandboxWorkDir: input.sessionWorkDir,
|
|
402
|
+
hostWorkDir,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
366
405
|
|
|
367
406
|
// Mount only the workspace: the model's view of the workspace lives at
|
|
368
407
|
// `sessionWorkDir` and is backed by `hostWorkDir`. The agent and session
|
|
@@ -370,7 +409,9 @@ export async function createPiSession(
|
|
|
370
409
|
// Pi state (auth, model registry, session journal) that must never surface
|
|
371
410
|
// in the sandbox or the workspace mirror.
|
|
372
411
|
const workspaceVfs = new PiWorkspaceVfs();
|
|
373
|
-
|
|
412
|
+
if (!workspaceAvailableOnHost) {
|
|
413
|
+
workspaceVfs.mount(hostWorkDir, sessionWorkDir);
|
|
414
|
+
}
|
|
374
415
|
|
|
375
416
|
const paths = createPiPathMapper({
|
|
376
417
|
hostWorkDir,
|
|
@@ -1011,7 +1052,7 @@ export async function createPiSession(
|
|
|
1011
1052
|
settingsManager,
|
|
1012
1053
|
resourceLoader,
|
|
1013
1054
|
customTools,
|
|
1014
|
-
...(
|
|
1055
|
+
...(hasExtensionFactories
|
|
1015
1056
|
? { noTools: 'builtin' as const }
|
|
1016
1057
|
: { tools: toolNames }),
|
|
1017
1058
|
...(input.settings.thinkingLevel
|
|
@@ -1147,11 +1188,13 @@ export async function createPiSession(
|
|
|
1147
1188
|
await reloadResourcesOnly();
|
|
1148
1189
|
turnAbortController.signal.throwIfAborted();
|
|
1149
1190
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1191
|
+
if (!workspaceAvailableOnHost) {
|
|
1192
|
+
await syncHostWorkspaceFromSandbox({
|
|
1193
|
+
sandbox: toolSafeSandboxSession,
|
|
1194
|
+
sandboxWorkDir: input.sessionWorkDir,
|
|
1195
|
+
hostWorkDir,
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1155
1198
|
turnAbortController.signal.throwIfAborted();
|
|
1156
1199
|
|
|
1157
1200
|
// Fresh translator state for the new turn — keep the tool sets the
|