@ai-sdk/harness-pi 1.0.104 → 1.0.106
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 +19 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.js +53 -23
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/pi-harness.ts +0 -9
- package/src/pi-model-resolver.ts +1 -1
- package/src/pi-session.ts +66 -24
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.106",
|
|
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.104",
|
|
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.104",
|
|
41
41
|
"@types/node": "22.19.19",
|
|
42
42
|
"@vercel/ai-tsconfig": "0.0.0",
|
|
43
43
|
"tsup": "^8.5.1",
|
package/src/pi-harness.ts
CHANGED
|
@@ -23,14 +23,6 @@ const PI_CLIENT_APP = `ai-sdk/harness-pi/${VERSION}`;
|
|
|
23
23
|
export type PiHarnessSettings = {
|
|
24
24
|
/** Where Pi sources API keys / gateway credentials from. */
|
|
25
25
|
readonly auth?: PiAuthenticationMode;
|
|
26
|
-
/**
|
|
27
|
-
* Pi model id (or name). Leaving this unset falls back to the AI Gateway
|
|
28
|
-
* default when `AI_GATEWAY_API_KEY` / `VERCEL_OIDC_TOKEN` is set, and to
|
|
29
|
-
* Pi's own resolution otherwise.
|
|
30
|
-
*
|
|
31
|
-
* @deprecated Use `model` on `HarnessAgent` instead.
|
|
32
|
-
*/
|
|
33
|
-
readonly model?: string;
|
|
34
26
|
/**
|
|
35
27
|
* Pi's extended-thinking budget level. Maps directly to the SDK's
|
|
36
28
|
* `thinkingLevel` option on `createAgentSession`.
|
|
@@ -153,7 +145,6 @@ export function createPi(
|
|
|
153
145
|
sessionWorkDir: startOpts.sessionWorkDir,
|
|
154
146
|
settings: {
|
|
155
147
|
...(settings.auth ? { auth: settings.auth } : {}),
|
|
156
|
-
...(settings.model == null ? {} : { model: settings.model }),
|
|
157
148
|
...(settings.thinkingLevel
|
|
158
149
|
? { thinkingLevel: settings.thinkingLevel }
|
|
159
150
|
: {}),
|
package/src/pi-model-resolver.ts
CHANGED
|
@@ -56,7 +56,7 @@ export function createPiModelResolver({
|
|
|
56
56
|
return cachedModels;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
return (modelId
|
|
59
|
+
return (modelId?: string): PiModel | undefined => {
|
|
60
60
|
const useGateway = Boolean(getAiGatewayAuthFromEnv({ env }).apiKey);
|
|
61
61
|
const effectiveId =
|
|
62
62
|
modelId ?? (useGateway ? DEFAULT_PI_GATEWAY_MODEL_ID : undefined);
|
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';
|
|
@@ -219,7 +220,6 @@ export type PiThinkingLevel =
|
|
|
219
220
|
export interface PiSessionSettings {
|
|
220
221
|
readonly auth?: PiAuthenticationMode;
|
|
221
222
|
readonly headers?: Readonly<Record<string, string>>;
|
|
222
|
-
readonly model?: string;
|
|
223
223
|
readonly thinkingLevel?: PiThinkingLevel;
|
|
224
224
|
readonly mcpServers?: Record<string, unknown>;
|
|
225
225
|
readonly extensionFactories?: ReadonlyArray<ExtensionFactory>;
|
|
@@ -282,6 +282,36 @@ interface DeferredRerunBarrier {
|
|
|
282
282
|
readonly cancel: (reason?: unknown) => void;
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
async function isWorkspaceAvailableOnHost({
|
|
286
|
+
sandbox,
|
|
287
|
+
sessionWorkDir,
|
|
288
|
+
}: {
|
|
289
|
+
sandbox: SandboxSession;
|
|
290
|
+
sessionWorkDir: string;
|
|
291
|
+
}): Promise<boolean> {
|
|
292
|
+
// A host path existing at the same location is not enough to prove that it
|
|
293
|
+
// belongs to the sandbox. Round-trip a unique marker through the sandbox
|
|
294
|
+
// filesystem API before using the workspace directly.
|
|
295
|
+
const probePath = path.join(
|
|
296
|
+
sessionWorkDir,
|
|
297
|
+
`.ai-sdk-harness-pi-${randomUUID()}`,
|
|
298
|
+
);
|
|
299
|
+
const probeContent = randomUUID();
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
await writeFile(probePath, probeContent, { flag: 'wx' });
|
|
303
|
+
const sandboxContent = await sandbox.readBinaryFile({ path: probePath });
|
|
304
|
+
return (
|
|
305
|
+
sandboxContent != null &&
|
|
306
|
+
Buffer.from(sandboxContent).equals(Buffer.from(probeContent))
|
|
307
|
+
);
|
|
308
|
+
} catch {
|
|
309
|
+
return false;
|
|
310
|
+
} finally {
|
|
311
|
+
await rm(probePath, { force: true }).catch(() => {});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
285
315
|
export async function createPiSession(
|
|
286
316
|
input: CreatePiSessionInput,
|
|
287
317
|
): Promise<HarnessV1Session> {
|
|
@@ -301,28 +331,34 @@ export async function createPiSession(
|
|
|
301
331
|
// sub-directory tree on disk.
|
|
302
332
|
const safeSessionId = input.sessionId.replace(/[\\/: ]/g, '-');
|
|
303
333
|
const hostRoot = path.join(tmpdir(), 'ai-sdk-harness', 'pi', safeSessionId);
|
|
304
|
-
const hostWorkDir = path.join(hostRoot, 'workspace');
|
|
305
334
|
const hostAgentDir = path.join(hostRoot, 'agent');
|
|
306
335
|
const hostSessionDir = path.join(hostRoot, 'sessions');
|
|
336
|
+
const toolSafeSandboxSession = getRestrictedSandboxSession(
|
|
337
|
+
input.sandboxSession,
|
|
338
|
+
);
|
|
307
339
|
|
|
308
340
|
// Pi runs in this host process but must behave as though it lives in the
|
|
309
341
|
// sandbox workspace: its working directory is the real `sessionWorkDir`
|
|
310
342
|
// (where `setup()` clones and where the sandbox-backed tools operate), so the
|
|
311
343
|
// 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
|
-
//
|
|
344
|
+
// directory" line in its system prompt — resolve inside the sandbox. When
|
|
345
|
+
// the sandbox filesystem is remote, the workspace VFS maps that sandbox path
|
|
346
|
+
// to a scoped host mirror for Pi's own `fs`-based resource loading. When the
|
|
347
|
+
// sandbox and harness share a filesystem, Pi uses the workspace directly so
|
|
348
|
+
// extensions can inspect project files beyond the scoped resource paths.
|
|
317
349
|
const sessionWorkDir = input.sessionWorkDir;
|
|
350
|
+
const workspaceAvailableOnHost = await isWorkspaceAvailableOnHost({
|
|
351
|
+
sandbox: toolSafeSandboxSession,
|
|
352
|
+
sessionWorkDir,
|
|
353
|
+
});
|
|
354
|
+
const hostWorkDir = workspaceAvailableOnHost
|
|
355
|
+
? path.resolve(sessionWorkDir)
|
|
356
|
+
: path.join(hostRoot, 'workspace');
|
|
318
357
|
|
|
319
358
|
await mkdir(hostWorkDir, { recursive: true });
|
|
320
359
|
await mkdir(hostAgentDir, { recursive: true });
|
|
321
360
|
await mkdir(hostSessionDir, { recursive: true });
|
|
322
361
|
|
|
323
|
-
const toolSafeSandboxSession = getRestrictedSandboxSession(
|
|
324
|
-
input.sandboxSession,
|
|
325
|
-
);
|
|
326
362
|
const sandboxHomeDir = await resolveSandboxHomeDir({
|
|
327
363
|
sandbox: toolSafeSandboxSession,
|
|
328
364
|
...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
|
|
@@ -358,11 +394,13 @@ export async function createPiSession(
|
|
|
358
394
|
|
|
359
395
|
// Snapshot sandbox state into the host mirror BEFORE the VFS goes live so
|
|
360
396
|
// Pi sees the workspace as soon as it boots.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
397
|
+
if (!workspaceAvailableOnHost) {
|
|
398
|
+
await syncHostWorkspaceFromSandbox({
|
|
399
|
+
sandbox: toolSafeSandboxSession,
|
|
400
|
+
sandboxWorkDir: input.sessionWorkDir,
|
|
401
|
+
hostWorkDir,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
366
404
|
|
|
367
405
|
// Mount only the workspace: the model's view of the workspace lives at
|
|
368
406
|
// `sessionWorkDir` and is backed by `hostWorkDir`. The agent and session
|
|
@@ -370,7 +408,9 @@ export async function createPiSession(
|
|
|
370
408
|
// Pi state (auth, model registry, session journal) that must never surface
|
|
371
409
|
// in the sandbox or the workspace mirror.
|
|
372
410
|
const workspaceVfs = new PiWorkspaceVfs();
|
|
373
|
-
|
|
411
|
+
if (!workspaceAvailableOnHost) {
|
|
412
|
+
workspaceVfs.mount(hostWorkDir, sessionWorkDir);
|
|
413
|
+
}
|
|
374
414
|
|
|
375
415
|
const paths = createPiPathMapper({
|
|
376
416
|
hostWorkDir,
|
|
@@ -418,7 +458,7 @@ export async function createPiSession(
|
|
|
418
458
|
modelRegistry,
|
|
419
459
|
env: resolverEnv,
|
|
420
460
|
});
|
|
421
|
-
let activeResolvedModel = resolveModel(
|
|
461
|
+
let activeResolvedModel = resolveModel();
|
|
422
462
|
const mcpServers = resolvePiMcpServers({
|
|
423
463
|
mcpServers: input.settings.mcpServers,
|
|
424
464
|
});
|
|
@@ -1011,7 +1051,7 @@ export async function createPiSession(
|
|
|
1011
1051
|
settingsManager,
|
|
1012
1052
|
resourceLoader,
|
|
1013
1053
|
customTools,
|
|
1014
|
-
...(
|
|
1054
|
+
...(hasExtensionFactories
|
|
1015
1055
|
? { noTools: 'builtin' as const }
|
|
1016
1056
|
: { tools: toolNames }),
|
|
1017
1057
|
...(input.settings.thinkingLevel
|
|
@@ -1147,11 +1187,13 @@ export async function createPiSession(
|
|
|
1147
1187
|
await reloadResourcesOnly();
|
|
1148
1188
|
turnAbortController.signal.throwIfAborted();
|
|
1149
1189
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1190
|
+
if (!workspaceAvailableOnHost) {
|
|
1191
|
+
await syncHostWorkspaceFromSandbox({
|
|
1192
|
+
sandbox: toolSafeSandboxSession,
|
|
1193
|
+
sandboxWorkDir: input.sessionWorkDir,
|
|
1194
|
+
hostWorkDir,
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1155
1197
|
turnAbortController.signal.throwIfAborted();
|
|
1156
1198
|
|
|
1157
1199
|
// Fresh translator state for the new turn — keep the tool sets the
|