@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-pi",
3
- "version": "1.0.104",
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.102",
30
- "@ai-sdk/provider-utils": "5.0.36",
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.102",
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
  : {}),
@@ -56,7 +56,7 @@ export function createPiModelResolver({
56
56
  return cachedModels;
57
57
  };
58
58
 
59
- return (modelId: string | undefined): PiModel | undefined => {
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 { mkdir, rm } from 'node:fs/promises';
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. The
313
- // workspace VFS maps that sandbox path to the host-side mirror so Pi's own
314
- // `fs`-based resource loading (`.pi/`, `AGENTS.md`) still works on the host.
315
- // `sessionWorkDir` is a sandbox path (e.g. `/vercel/sandbox/...`) that does
316
- // not exist on the host, so it is a safe, collision-free VFS mount point.
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
- await syncHostWorkspaceFromSandbox({
362
- sandbox: toolSafeSandboxSession,
363
- sandboxWorkDir: input.sessionWorkDir,
364
- hostWorkDir,
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
- workspaceVfs.mount(hostWorkDir, sessionWorkDir);
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(input.settings.model);
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
- ...(hasMcpServers
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
- await syncHostWorkspaceFromSandbox({
1151
- sandbox: toolSafeSandboxSession,
1152
- sandboxWorkDir: input.sessionWorkDir,
1153
- hostWorkDir,
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