@ai-sdk/harness-pi 1.0.103 → 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 +23 -0
- package/dist/index.js +137 -98
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/pi-auth.ts +23 -2
- package/src/pi-harness.ts +1 -0
- package/src/pi-session.ts +75 -25
- package/src/pi-skills.ts +0 -25
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-auth.ts
CHANGED
|
@@ -174,16 +174,19 @@ function createGatewayProviderConfig({
|
|
|
174
174
|
apiKey,
|
|
175
175
|
baseUrl,
|
|
176
176
|
clientApp,
|
|
177
|
+
headers,
|
|
177
178
|
}: {
|
|
178
179
|
apiKey: string;
|
|
179
180
|
baseUrl: string;
|
|
180
181
|
clientApp: string;
|
|
182
|
+
headers?: Readonly<Record<string, string>>;
|
|
181
183
|
}): ProviderConfigInput {
|
|
182
184
|
return {
|
|
183
185
|
apiKey,
|
|
184
186
|
baseUrl,
|
|
185
187
|
authHeader: true,
|
|
186
188
|
headers: {
|
|
189
|
+
...headers,
|
|
187
190
|
'User-Agent': clientApp,
|
|
188
191
|
'x-client-app': clientApp,
|
|
189
192
|
},
|
|
@@ -311,11 +314,13 @@ export async function registerPiProviders({
|
|
|
311
314
|
resolvedEnv,
|
|
312
315
|
registries,
|
|
313
316
|
clientApp = HARNESS_CLIENT_APP,
|
|
317
|
+
headers,
|
|
314
318
|
}: {
|
|
315
319
|
options: PiAuthenticationMode | undefined;
|
|
316
320
|
resolvedEnv: Record<string, string>;
|
|
317
321
|
registries: PiRegistries;
|
|
318
322
|
clientApp?: string;
|
|
323
|
+
headers?: Readonly<Record<string, string>>;
|
|
319
324
|
}): Promise<void> {
|
|
320
325
|
const suppliedEnvironment = isHarnessAuthenticationEnvironment(options);
|
|
321
326
|
const authenticationEnvironment = suppliedEnvironment ? options : process.env;
|
|
@@ -328,6 +333,7 @@ export async function registerPiProviders({
|
|
|
328
333
|
customEnv: { ...pickOpenAIEnv(authenticationEnvironment), ...env },
|
|
329
334
|
registries,
|
|
330
335
|
clientApp,
|
|
336
|
+
headers,
|
|
331
337
|
});
|
|
332
338
|
return;
|
|
333
339
|
}
|
|
@@ -337,6 +343,7 @@ export async function registerPiProviders({
|
|
|
337
343
|
customEnv: { ...pickAnthropicEnv(authenticationEnvironment), ...env },
|
|
338
344
|
registries,
|
|
339
345
|
clientApp,
|
|
346
|
+
headers,
|
|
340
347
|
});
|
|
341
348
|
return;
|
|
342
349
|
}
|
|
@@ -347,6 +354,7 @@ export async function registerPiProviders({
|
|
|
347
354
|
customEnv: { ...pickProviderEnv(authenticationEnvironment), ...env },
|
|
348
355
|
registries,
|
|
349
356
|
clientApp,
|
|
357
|
+
headers,
|
|
350
358
|
});
|
|
351
359
|
return;
|
|
352
360
|
}
|
|
@@ -367,6 +375,7 @@ export async function registerPiProviders({
|
|
|
367
375
|
apiKey: gatewayApiKey,
|
|
368
376
|
baseUrl: gatewayBaseUrl,
|
|
369
377
|
clientApp,
|
|
378
|
+
headers,
|
|
370
379
|
}),
|
|
371
380
|
});
|
|
372
381
|
return;
|
|
@@ -391,6 +400,7 @@ export async function registerPiProviders({
|
|
|
391
400
|
apiKey: gatewayApiKey,
|
|
392
401
|
baseUrl: gatewayBaseUrl,
|
|
393
402
|
clientApp,
|
|
403
|
+
headers,
|
|
394
404
|
}),
|
|
395
405
|
});
|
|
396
406
|
return;
|
|
@@ -400,6 +410,7 @@ export async function registerPiProviders({
|
|
|
400
410
|
customEnv: { ...pickProviderEnv(authenticationEnvironment), ...env },
|
|
401
411
|
registries,
|
|
402
412
|
clientApp,
|
|
413
|
+
headers,
|
|
403
414
|
});
|
|
404
415
|
return;
|
|
405
416
|
}
|
|
@@ -455,10 +466,12 @@ async function registerCustomProviders({
|
|
|
455
466
|
customEnv,
|
|
456
467
|
registries,
|
|
457
468
|
clientApp,
|
|
469
|
+
headers,
|
|
458
470
|
}: {
|
|
459
471
|
customEnv: Record<string, string>;
|
|
460
472
|
registries: PiRegistries;
|
|
461
473
|
clientApp: string;
|
|
474
|
+
headers?: Readonly<Record<string, string>>;
|
|
462
475
|
}): Promise<void> {
|
|
463
476
|
const gatewayKey = customEnv.AI_GATEWAY_API_KEY;
|
|
464
477
|
if (gatewayKey) {
|
|
@@ -471,6 +484,7 @@ async function registerCustomProviders({
|
|
|
471
484
|
apiKey: gatewayKey,
|
|
472
485
|
baseUrl,
|
|
473
486
|
clientApp,
|
|
487
|
+
headers,
|
|
474
488
|
}),
|
|
475
489
|
});
|
|
476
490
|
}
|
|
@@ -485,6 +499,7 @@ async function registerCustomProviders({
|
|
|
485
499
|
apiKey: customEnv.OPENAI_API_KEY,
|
|
486
500
|
baseUrl,
|
|
487
501
|
authHeader: true,
|
|
502
|
+
...(headers ? { headers } : {}),
|
|
488
503
|
},
|
|
489
504
|
});
|
|
490
505
|
}
|
|
@@ -498,10 +513,15 @@ async function registerCustomProviders({
|
|
|
498
513
|
config: {
|
|
499
514
|
apiKey: customEnv.ANTHROPIC_API_KEY,
|
|
500
515
|
baseUrl,
|
|
501
|
-
...(customEnv.ANTHROPIC_AUTH_TOKEN
|
|
516
|
+
...(headers || customEnv.ANTHROPIC_AUTH_TOKEN
|
|
502
517
|
? {
|
|
503
518
|
headers: {
|
|
504
|
-
|
|
519
|
+
...headers,
|
|
520
|
+
...(customEnv.ANTHROPIC_AUTH_TOKEN
|
|
521
|
+
? {
|
|
522
|
+
authorization: `Bearer ${customEnv.ANTHROPIC_AUTH_TOKEN}`,
|
|
523
|
+
}
|
|
524
|
+
: {}),
|
|
505
525
|
},
|
|
506
526
|
}
|
|
507
527
|
: {}),
|
|
@@ -534,6 +554,7 @@ async function registerCustomProviders({
|
|
|
534
554
|
apiKey,
|
|
535
555
|
baseUrl,
|
|
536
556
|
authHeader: true,
|
|
557
|
+
...(headers ? { headers } : {}),
|
|
537
558
|
},
|
|
538
559
|
});
|
|
539
560
|
}
|
package/src/pi-harness.ts
CHANGED
|
@@ -161,6 +161,7 @@ export function createPi(
|
|
|
161
161
|
...(settings.extensionFactories
|
|
162
162
|
? { extensionFactories: settings.extensionFactories }
|
|
163
163
|
: {}),
|
|
164
|
+
...(startOpts.headers ? { headers: startOpts.headers } : {}),
|
|
164
165
|
},
|
|
165
166
|
clientApp: PI_CLIENT_APP,
|
|
166
167
|
isResume: lifecycleState != null,
|
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';
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
import {
|
|
34
35
|
getRestrictedSandboxSession,
|
|
35
36
|
resolveSandboxHomeDir,
|
|
37
|
+
writeSkills,
|
|
36
38
|
} from '@ai-sdk/harness/utils';
|
|
37
39
|
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
38
40
|
import {
|
|
@@ -45,7 +47,7 @@ import { getPiTerminalError, parseNativeEvent } from './pi-events';
|
|
|
45
47
|
import { createPiModelResolver } from './pi-model-resolver';
|
|
46
48
|
import { createPiPathMapper } from './pi-paths';
|
|
47
49
|
import { createPiRemoteOps, type PiRemoteOps } from './pi-remote-ops';
|
|
48
|
-
|
|
50
|
+
|
|
49
51
|
import {
|
|
50
52
|
persistSessionFileToSandbox,
|
|
51
53
|
pullSessionFileFromSandbox,
|
|
@@ -217,6 +219,7 @@ export type PiThinkingLevel =
|
|
|
217
219
|
|
|
218
220
|
export interface PiSessionSettings {
|
|
219
221
|
readonly auth?: PiAuthenticationMode;
|
|
222
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
220
223
|
readonly model?: string;
|
|
221
224
|
readonly thinkingLevel?: PiThinkingLevel;
|
|
222
225
|
readonly mcpServers?: Record<string, unknown>;
|
|
@@ -280,6 +283,36 @@ interface DeferredRerunBarrier {
|
|
|
280
283
|
readonly cancel: (reason?: unknown) => void;
|
|
281
284
|
}
|
|
282
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
|
+
|
|
283
316
|
export async function createPiSession(
|
|
284
317
|
input: CreatePiSessionInput,
|
|
285
318
|
): Promise<HarnessV1Session> {
|
|
@@ -299,28 +332,34 @@ export async function createPiSession(
|
|
|
299
332
|
// sub-directory tree on disk.
|
|
300
333
|
const safeSessionId = input.sessionId.replace(/[\\/: ]/g, '-');
|
|
301
334
|
const hostRoot = path.join(tmpdir(), 'ai-sdk-harness', 'pi', safeSessionId);
|
|
302
|
-
const hostWorkDir = path.join(hostRoot, 'workspace');
|
|
303
335
|
const hostAgentDir = path.join(hostRoot, 'agent');
|
|
304
336
|
const hostSessionDir = path.join(hostRoot, 'sessions');
|
|
337
|
+
const toolSafeSandboxSession = getRestrictedSandboxSession(
|
|
338
|
+
input.sandboxSession,
|
|
339
|
+
);
|
|
305
340
|
|
|
306
341
|
// Pi runs in this host process but must behave as though it lives in the
|
|
307
342
|
// sandbox workspace: its working directory is the real `sessionWorkDir`
|
|
308
343
|
// (where `setup()` clones and where the sandbox-backed tools operate), so the
|
|
309
344
|
// paths Pi advertises to the model — most notably the "Current working
|
|
310
|
-
// directory" line in its system prompt — resolve inside the sandbox.
|
|
311
|
-
// workspace VFS maps that sandbox path
|
|
312
|
-
// `fs`-based resource loading
|
|
313
|
-
//
|
|
314
|
-
//
|
|
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.
|
|
315
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');
|
|
316
358
|
|
|
317
359
|
await mkdir(hostWorkDir, { recursive: true });
|
|
318
360
|
await mkdir(hostAgentDir, { recursive: true });
|
|
319
361
|
await mkdir(hostSessionDir, { recursive: true });
|
|
320
362
|
|
|
321
|
-
const toolSafeSandboxSession = getRestrictedSandboxSession(
|
|
322
|
-
input.sandboxSession,
|
|
323
|
-
);
|
|
324
363
|
const sandboxHomeDir = await resolveSandboxHomeDir({
|
|
325
364
|
sandbox: toolSafeSandboxSession,
|
|
326
365
|
...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
|
|
@@ -356,11 +395,13 @@ export async function createPiSession(
|
|
|
356
395
|
|
|
357
396
|
// Snapshot sandbox state into the host mirror BEFORE the VFS goes live so
|
|
358
397
|
// Pi sees the workspace as soon as it boots.
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
398
|
+
if (!workspaceAvailableOnHost) {
|
|
399
|
+
await syncHostWorkspaceFromSandbox({
|
|
400
|
+
sandbox: toolSafeSandboxSession,
|
|
401
|
+
sandboxWorkDir: input.sessionWorkDir,
|
|
402
|
+
hostWorkDir,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
364
405
|
|
|
365
406
|
// Mount only the workspace: the model's view of the workspace lives at
|
|
366
407
|
// `sessionWorkDir` and is backed by `hostWorkDir`. The agent and session
|
|
@@ -368,7 +409,9 @@ export async function createPiSession(
|
|
|
368
409
|
// Pi state (auth, model registry, session journal) that must never surface
|
|
369
410
|
// in the sandbox or the workspace mirror.
|
|
370
411
|
const workspaceVfs = new PiWorkspaceVfs();
|
|
371
|
-
|
|
412
|
+
if (!workspaceAvailableOnHost) {
|
|
413
|
+
workspaceVfs.mount(hostWorkDir, sessionWorkDir);
|
|
414
|
+
}
|
|
372
415
|
|
|
373
416
|
const paths = createPiPathMapper({
|
|
374
417
|
hostWorkDir,
|
|
@@ -410,6 +453,7 @@ export async function createPiSession(
|
|
|
410
453
|
modelRuntime,
|
|
411
454
|
},
|
|
412
455
|
clientApp: input.clientApp,
|
|
456
|
+
headers: input.settings.headers,
|
|
413
457
|
});
|
|
414
458
|
const resolveModel = createPiModelResolver({
|
|
415
459
|
modelRegistry,
|
|
@@ -1008,7 +1052,7 @@ export async function createPiSession(
|
|
|
1008
1052
|
settingsManager,
|
|
1009
1053
|
resourceLoader,
|
|
1010
1054
|
customTools,
|
|
1011
|
-
...(
|
|
1055
|
+
...(hasExtensionFactories
|
|
1012
1056
|
? { noTools: 'builtin' as const }
|
|
1013
1057
|
: { tools: toolNames }),
|
|
1014
1058
|
...(input.settings.thinkingLevel
|
|
@@ -1071,11 +1115,15 @@ export async function createPiSession(
|
|
|
1071
1115
|
throw new Error('Pi session has been stopped.');
|
|
1072
1116
|
}
|
|
1073
1117
|
|
|
1074
|
-
const skillWriteResult = await
|
|
1118
|
+
const skillWriteResult = await writeSkills({
|
|
1075
1119
|
sandbox: toolSafeSandboxSession,
|
|
1076
|
-
sandboxHomeDir,
|
|
1120
|
+
homePath: sandboxHomeDir,
|
|
1121
|
+
skillsDir: '.agents/skills',
|
|
1077
1122
|
skills: turnOpts.skills,
|
|
1078
1123
|
abortSignal: turnOpts.abortSignal,
|
|
1124
|
+
invalidSkillNameMessage: ({ name }) => `Invalid Pi skill name: ${name}`,
|
|
1125
|
+
invalidSkillFilePathMessage: ({ skillName, filePath }) =>
|
|
1126
|
+
`Invalid Pi skill file path for ${skillName}: ${filePath}`,
|
|
1079
1127
|
});
|
|
1080
1128
|
harnessSkills = createHarnessPiSkills({
|
|
1081
1129
|
skills: turnOpts.skills,
|
|
@@ -1140,11 +1188,13 @@ export async function createPiSession(
|
|
|
1140
1188
|
await reloadResourcesOnly();
|
|
1141
1189
|
turnAbortController.signal.throwIfAborted();
|
|
1142
1190
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1191
|
+
if (!workspaceAvailableOnHost) {
|
|
1192
|
+
await syncHostWorkspaceFromSandbox({
|
|
1193
|
+
sandbox: toolSafeSandboxSession,
|
|
1194
|
+
sandboxWorkDir: input.sessionWorkDir,
|
|
1195
|
+
hostWorkDir,
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1148
1198
|
turnAbortController.signal.throwIfAborted();
|
|
1149
1199
|
|
|
1150
1200
|
// Fresh translator state for the new turn — keep the tool sets the
|
package/src/pi-skills.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import type { HarnessV1Skill } from '@ai-sdk/harness';
|
|
3
|
-
import { writeSkills, type WriteSkillsResult } from '@ai-sdk/harness/utils';
|
|
4
|
-
import type { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Materialize Pi skills as files under
|
|
8
|
-
* `$HOME/.agents/skills/<name>/SKILL.md` inside the sandbox.
|
|
9
|
-
*/
|
|
10
|
-
export async function writePiSkills(args: {
|
|
11
|
-
readonly sandbox: Experimental_SandboxSession;
|
|
12
|
-
readonly sandboxHomeDir: string;
|
|
13
|
-
readonly skills: ReadonlyArray<HarnessV1Skill>;
|
|
14
|
-
readonly abortSignal?: AbortSignal;
|
|
15
|
-
}): Promise<WriteSkillsResult> {
|
|
16
|
-
return writeSkills({
|
|
17
|
-
sandbox: args.sandbox,
|
|
18
|
-
rootDir: path.posix.join(args.sandboxHomeDir, '.agents', 'skills'),
|
|
19
|
-
skills: args.skills,
|
|
20
|
-
abortSignal: args.abortSignal,
|
|
21
|
-
invalidSkillNameMessage: ({ name }) => `Invalid Pi skill name: ${name}`,
|
|
22
|
-
invalidSkillFilePathMessage: ({ skillName, filePath }) =>
|
|
23
|
-
`Invalid Pi skill file path for ${skillName}: ${filePath}`,
|
|
24
|
-
});
|
|
25
|
-
}
|