@ai-sdk/harness-pi 1.0.71 → 1.0.73
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 +9 -6
- package/dist/index.js +229 -40
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +1 -1
- package/src/pi-auth.ts +252 -13
- package/src/pi-resume-state.ts +39 -19
- package/src/pi-session.ts +12 -6
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.73",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@earendil-works/pi-coding-agent": "^0.80.10",
|
|
31
31
|
"pi-mcp-adapter": "2.12.1",
|
|
32
32
|
"typebox": "^1.1.38",
|
|
33
|
-
"@ai-sdk/harness": "1.0.
|
|
33
|
+
"@ai-sdk/harness": "1.0.72",
|
|
34
34
|
"@ai-sdk/provider-utils": "5.0.27"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -9,4 +9,4 @@ export const pi = createPi();
|
|
|
9
9
|
export { createPi } from './pi-harness';
|
|
10
10
|
export { VERSION } from './version';
|
|
11
11
|
export type { PiHarnessSettings } from './pi-harness';
|
|
12
|
-
export type { PiAuthOptions } from './pi-auth';
|
|
12
|
+
export type { PiAuthenticationMode, PiAuthOptions } from './pi-auth';
|
package/src/pi-auth.ts
CHANGED
|
@@ -8,12 +8,20 @@ import { VERSION } from './version';
|
|
|
8
8
|
type ProviderConfigInput = Parameters<ModelRegistry['registerProvider']>[1];
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Pi auth options.
|
|
12
|
-
*
|
|
13
|
-
* gateway from `process.env`). To use multiple providers, use `customEnv`
|
|
14
|
-
* with the standard `<PREFIX>_API_KEY` / `<PREFIX>_BASE_URL` pattern.
|
|
11
|
+
* Pi auth options. Choose an explicit mode or rely on 'auto' (precedence:
|
|
12
|
+
* explicit gateway, then OpenAI / Anthropic / custom environment variables).
|
|
15
13
|
*/
|
|
16
|
-
export type
|
|
14
|
+
export type PiAuthenticationMode =
|
|
15
|
+
| 'auto'
|
|
16
|
+
| 'openai'
|
|
17
|
+
| 'anthropic'
|
|
18
|
+
| 'custom'
|
|
19
|
+
| 'ai-gateway';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @deprecated Passing an object to auth options is deprecated. Use a `PiAuthenticationMode` string value ("auto" | "openai" | "anthropic" | "custom" | "ai-gateway") instead, and pass credentials via environment variables.
|
|
23
|
+
*/
|
|
24
|
+
export type LegacyPiAuthOptions = {
|
|
17
25
|
readonly gateway?: {
|
|
18
26
|
readonly apiKey?: string;
|
|
19
27
|
readonly baseUrl?: string;
|
|
@@ -31,6 +39,8 @@ export type PiAuthOptions = {
|
|
|
31
39
|
readonly customEnv?: Record<string, string>;
|
|
32
40
|
};
|
|
33
41
|
|
|
42
|
+
export type PiAuthOptions = PiAuthenticationMode | LegacyPiAuthOptions;
|
|
43
|
+
|
|
34
44
|
const DEFAULT_GATEWAY_BASE_URL = 'https://ai-gateway.vercel.sh';
|
|
35
45
|
const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';
|
|
36
46
|
const DEFAULT_ANTHROPIC_BASE_URL = 'https://api.anthropic.com';
|
|
@@ -90,22 +100,79 @@ export function resolvePiEnv({
|
|
|
90
100
|
options: PiAuthOptions | undefined;
|
|
91
101
|
env: NodeJS.ProcessEnv;
|
|
92
102
|
}): Record<string, string> {
|
|
93
|
-
const
|
|
103
|
+
const normalizedOptions = normalizePiAuthToLegacyAuth(options);
|
|
104
|
+
const customEnvConfigured = hasConfiguredValue(normalizedOptions?.customEnv);
|
|
94
105
|
if (customEnvConfigured) {
|
|
95
|
-
return resolveCustomEnv({ customEnv:
|
|
106
|
+
return resolveCustomEnv({ customEnv: normalizedOptions!.customEnv ?? {} });
|
|
96
107
|
}
|
|
97
108
|
|
|
98
|
-
const gatewayConfigured = hasConfiguredValue(
|
|
109
|
+
const gatewayConfigured = hasConfiguredValue(normalizedOptions?.gateway);
|
|
99
110
|
const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({ env });
|
|
100
111
|
if (gatewayConfigured) {
|
|
101
|
-
const apiKey =
|
|
102
|
-
|
|
112
|
+
const apiKey =
|
|
113
|
+
normalizedOptions!.gateway?.apiKey ?? gatewayAuthFromEnv.apiKey;
|
|
114
|
+
const baseUrl =
|
|
115
|
+
normalizedOptions!.gateway?.baseUrl ?? gatewayAuthFromEnv.baseUrl;
|
|
103
116
|
if (apiKey) {
|
|
104
117
|
return { AI_GATEWAY_API_KEY: apiKey, AI_GATEWAY_BASE_URL: baseUrl };
|
|
105
118
|
}
|
|
106
119
|
return {};
|
|
107
120
|
}
|
|
108
121
|
|
|
122
|
+
// Handle explicit string modes with process env
|
|
123
|
+
if (typeof options === 'string') {
|
|
124
|
+
switch (options) {
|
|
125
|
+
case 'openai':
|
|
126
|
+
if (env.OPENAI_API_KEY) {
|
|
127
|
+
return {
|
|
128
|
+
OPENAI_API_KEY: env.OPENAI_API_KEY,
|
|
129
|
+
...(env.OPENAI_BASE_URL
|
|
130
|
+
? { OPENAI_BASE_URL: env.OPENAI_BASE_URL }
|
|
131
|
+
: {}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {};
|
|
135
|
+
case 'anthropic':
|
|
136
|
+
if (env.ANTHROPIC_API_KEY) {
|
|
137
|
+
return {
|
|
138
|
+
ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY,
|
|
139
|
+
...(env.ANTHROPIC_BASE_URL
|
|
140
|
+
? { ANTHROPIC_BASE_URL: env.ANTHROPIC_BASE_URL }
|
|
141
|
+
: {}),
|
|
142
|
+
...(env.ANTHROPIC_AUTH_TOKEN
|
|
143
|
+
? { ANTHROPIC_AUTH_TOKEN: env.ANTHROPIC_AUTH_TOKEN }
|
|
144
|
+
: {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {};
|
|
148
|
+
case 'custom': {
|
|
149
|
+
const result: Record<string, string> = {};
|
|
150
|
+
for (const [key, value] of Object.entries(env)) {
|
|
151
|
+
if (
|
|
152
|
+
value &&
|
|
153
|
+
(key.endsWith('_API_KEY') ||
|
|
154
|
+
key.endsWith('_BASE_URL') ||
|
|
155
|
+
key === 'ANTHROPIC_AUTH_TOKEN')
|
|
156
|
+
) {
|
|
157
|
+
result[key] = value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
case 'ai-gateway':
|
|
163
|
+
if (gatewayAuthFromEnv.apiKey) {
|
|
164
|
+
return {
|
|
165
|
+
AI_GATEWAY_API_KEY: gatewayAuthFromEnv.apiKey,
|
|
166
|
+
AI_GATEWAY_BASE_URL: gatewayAuthFromEnv.baseUrl,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {};
|
|
170
|
+
case 'auto':
|
|
171
|
+
default:
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
109
176
|
// Ambient gateway fallback.
|
|
110
177
|
if (gatewayAuthFromEnv.apiKey) {
|
|
111
178
|
return {
|
|
@@ -114,7 +181,19 @@ export function resolvePiEnv({
|
|
|
114
181
|
};
|
|
115
182
|
}
|
|
116
183
|
|
|
117
|
-
|
|
184
|
+
// 'auto' fallback: pick up any other provider credentials from the env.
|
|
185
|
+
const ambient: Record<string, string> = {};
|
|
186
|
+
for (const [key, value] of Object.entries(env)) {
|
|
187
|
+
if (
|
|
188
|
+
value &&
|
|
189
|
+
(key.endsWith('_API_KEY') ||
|
|
190
|
+
key.endsWith('_BASE_URL') ||
|
|
191
|
+
key === 'ANTHROPIC_AUTH_TOKEN')
|
|
192
|
+
) {
|
|
193
|
+
ambient[key] = value;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return ambient;
|
|
118
197
|
}
|
|
119
198
|
|
|
120
199
|
export async function registerPiProviders({
|
|
@@ -128,15 +207,105 @@ export async function registerPiProviders({
|
|
|
128
207
|
registries: PiRegistries;
|
|
129
208
|
clientApp?: string;
|
|
130
209
|
}): Promise<void> {
|
|
131
|
-
|
|
210
|
+
const normalizedOptions = normalizePiAuthToLegacyAuth(options);
|
|
211
|
+
if (hasConfiguredValue(normalizedOptions?.customEnv)) {
|
|
132
212
|
await registerCustomProviders({
|
|
133
|
-
customEnv:
|
|
213
|
+
customEnv: normalizedOptions!.customEnv ?? {},
|
|
134
214
|
registries,
|
|
135
215
|
clientApp,
|
|
136
216
|
});
|
|
137
217
|
return;
|
|
138
218
|
}
|
|
139
219
|
|
|
220
|
+
// Legacy customEnv was handled above. Everything else reduces to a mode:
|
|
221
|
+
// string modes pass through, `undefined` is 'auto', and legacy gateway
|
|
222
|
+
// objects fall through to the trailing gateway-registration block.
|
|
223
|
+
const mode =
|
|
224
|
+
typeof options === 'string' ? options : options == null ? 'auto' : 'legacy';
|
|
225
|
+
|
|
226
|
+
switch (mode) {
|
|
227
|
+
case 'openai': {
|
|
228
|
+
const env = pickOpenAIEnv(resolvedEnv);
|
|
229
|
+
await registerCustomProviders({
|
|
230
|
+
customEnv: { ...pickOpenAIEnv(process.env), ...env },
|
|
231
|
+
registries,
|
|
232
|
+
clientApp,
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
case 'anthropic': {
|
|
237
|
+
const env = pickAnthropicEnv(resolvedEnv);
|
|
238
|
+
await registerCustomProviders({
|
|
239
|
+
customEnv: { ...pickAnthropicEnv(process.env), ...env },
|
|
240
|
+
registries,
|
|
241
|
+
clientApp,
|
|
242
|
+
});
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
case 'custom': {
|
|
246
|
+
// 'custom' registers every provider with credentials in the env.
|
|
247
|
+
const env = pickProviderEnv(resolvedEnv);
|
|
248
|
+
await registerCustomProviders({
|
|
249
|
+
customEnv: { ...pickProviderEnv(process.env), ...env },
|
|
250
|
+
registries,
|
|
251
|
+
clientApp,
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
case 'ai-gateway': {
|
|
256
|
+
const gatewayAuth = getAiGatewayAuthFromEnv({ env: process.env });
|
|
257
|
+
const gatewayApiKey =
|
|
258
|
+
resolvedEnv.AI_GATEWAY_API_KEY ?? gatewayAuth.apiKey;
|
|
259
|
+
const gatewayBaseUrl =
|
|
260
|
+
resolvedEnv.AI_GATEWAY_BASE_URL ?? gatewayAuth.baseUrl;
|
|
261
|
+
if (!gatewayApiKey) return;
|
|
262
|
+
await register({
|
|
263
|
+
registries,
|
|
264
|
+
provider: 'vercel-ai-gateway',
|
|
265
|
+
apiKey: gatewayApiKey,
|
|
266
|
+
config: createGatewayProviderConfig({
|
|
267
|
+
apiKey: gatewayApiKey,
|
|
268
|
+
baseUrl: gatewayBaseUrl,
|
|
269
|
+
clientApp,
|
|
270
|
+
}),
|
|
271
|
+
});
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
case 'legacy':
|
|
275
|
+
break; // handled below
|
|
276
|
+
case 'auto':
|
|
277
|
+
default: {
|
|
278
|
+
// 'auto' (the default): prefer the AI Gateway; only when no gateway
|
|
279
|
+
// credentials exist, fall back to other providers found in the env.
|
|
280
|
+
const gatewayAuth = getAiGatewayAuthFromEnv({ env: process.env });
|
|
281
|
+
const gatewayApiKey =
|
|
282
|
+
resolvedEnv.AI_GATEWAY_API_KEY ?? gatewayAuth.apiKey;
|
|
283
|
+
const gatewayBaseUrl =
|
|
284
|
+
resolvedEnv.AI_GATEWAY_BASE_URL ?? gatewayAuth.baseUrl;
|
|
285
|
+
if (gatewayApiKey) {
|
|
286
|
+
await register({
|
|
287
|
+
registries,
|
|
288
|
+
provider: 'vercel-ai-gateway',
|
|
289
|
+
apiKey: gatewayApiKey,
|
|
290
|
+
config: createGatewayProviderConfig({
|
|
291
|
+
apiKey: gatewayApiKey,
|
|
292
|
+
baseUrl: gatewayBaseUrl,
|
|
293
|
+
clientApp,
|
|
294
|
+
}),
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const env = pickProviderEnv(resolvedEnv);
|
|
299
|
+
await registerCustomProviders({
|
|
300
|
+
customEnv: { ...pickProviderEnv(process.env), ...env },
|
|
301
|
+
registries,
|
|
302
|
+
clientApp,
|
|
303
|
+
});
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Legacy explicit gateway object options.
|
|
140
309
|
const apiKey = resolvedEnv.AI_GATEWAY_API_KEY;
|
|
141
310
|
const baseUrl = resolvedEnv.AI_GATEWAY_BASE_URL;
|
|
142
311
|
if (!apiKey || !baseUrl) return;
|
|
@@ -149,6 +318,76 @@ export async function registerPiProviders({
|
|
|
149
318
|
});
|
|
150
319
|
}
|
|
151
320
|
|
|
321
|
+
function pickOpenAIEnv(
|
|
322
|
+
env: NodeJS.ProcessEnv | Record<string, string>,
|
|
323
|
+
): Record<string, string> {
|
|
324
|
+
const result: Record<string, string> = {};
|
|
325
|
+
if (env.OPENAI_API_KEY) result.OPENAI_API_KEY = env.OPENAI_API_KEY;
|
|
326
|
+
if (env.OPENAI_BASE_URL) result.OPENAI_BASE_URL = env.OPENAI_BASE_URL;
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function pickAnthropicEnv(
|
|
331
|
+
env: NodeJS.ProcessEnv | Record<string, string>,
|
|
332
|
+
): Record<string, string> {
|
|
333
|
+
const result: Record<string, string> = {};
|
|
334
|
+
if (env.ANTHROPIC_API_KEY) result.ANTHROPIC_API_KEY = env.ANTHROPIC_API_KEY;
|
|
335
|
+
if (env.ANTHROPIC_BASE_URL)
|
|
336
|
+
result.ANTHROPIC_BASE_URL = env.ANTHROPIC_BASE_URL;
|
|
337
|
+
if (env.ANTHROPIC_AUTH_TOKEN)
|
|
338
|
+
result.ANTHROPIC_AUTH_TOKEN = env.ANTHROPIC_AUTH_TOKEN;
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Filters an env object down to provider-credential keys (`*_API_KEY`,
|
|
344
|
+
* `*_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`). Pi does not read provider
|
|
345
|
+
* credentials from the environment itself — providers are only registered
|
|
346
|
+
* through `registerProvider` / `setRuntimeApiKey` — so we must extract the
|
|
347
|
+
* relevant entries before handing them to `registerCustomProviders`.
|
|
348
|
+
*/
|
|
349
|
+
function pickProviderEnv(
|
|
350
|
+
env: NodeJS.ProcessEnv | Record<string, string>,
|
|
351
|
+
): Record<string, string> {
|
|
352
|
+
const result: Record<string, string> = {};
|
|
353
|
+
for (const [key, value] of Object.entries(env)) {
|
|
354
|
+
if (
|
|
355
|
+
value &&
|
|
356
|
+
(key.endsWith('_API_KEY') ||
|
|
357
|
+
key.endsWith('_BASE_URL') ||
|
|
358
|
+
key === 'ANTHROPIC_AUTH_TOKEN')
|
|
359
|
+
) {
|
|
360
|
+
result[key] = value;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function normalizePiAuthToLegacyAuth(
|
|
367
|
+
options: PiAuthOptions | undefined,
|
|
368
|
+
): LegacyPiAuthOptions | undefined {
|
|
369
|
+
if (options == null || options === 'auto') {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
if (typeof options === 'string') {
|
|
373
|
+
switch (options) {
|
|
374
|
+
case 'ai-gateway':
|
|
375
|
+
return { gateway: {} };
|
|
376
|
+
case 'custom':
|
|
377
|
+
case 'openai':
|
|
378
|
+
case 'anthropic':
|
|
379
|
+
return { customEnv: {} };
|
|
380
|
+
default:
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
console.warn(
|
|
386
|
+
'[pi] Passing an object to auth options is deprecated. Use a string mode ("auto" | "openai" | "anthropic" | "custom" | "ai-gateway") instead, and pass credentials via environment variables.',
|
|
387
|
+
);
|
|
388
|
+
return options;
|
|
389
|
+
}
|
|
390
|
+
|
|
152
391
|
function resolveCustomEnv({
|
|
153
392
|
customEnv,
|
|
154
393
|
}: {
|
package/src/pi-resume-state.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
|
-
import { shellQuote } from '@ai-sdk/harness/utils';
|
|
4
4
|
import type { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
|
|
5
5
|
import { z } from 'zod/v4';
|
|
6
6
|
|
|
@@ -24,8 +24,8 @@ const piSessionFileNameSchema = z
|
|
|
24
24
|
* Schema for the adapter-specific portion of lifecycle state `data` produced
|
|
25
25
|
* by Pi's resumable lifecycle methods. Carries the basename
|
|
26
26
|
* (including extension) of the Pi session file. The actual session bytes live
|
|
27
|
-
* in
|
|
28
|
-
*
|
|
27
|
+
* in a private, session-scoped directory under sandbox HOME so they survive
|
|
28
|
+
* cross-process resume without appearing in the agent workspace.
|
|
29
29
|
*/
|
|
30
30
|
export const piResumeStateSchema = z.looseObject({
|
|
31
31
|
sessionFileName: piSessionFileNameSchema.optional(),
|
|
@@ -33,7 +33,32 @@ export const piResumeStateSchema = z.looseObject({
|
|
|
33
33
|
|
|
34
34
|
export type PiResumeStateData = z.infer<typeof piResumeStateSchema>;
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
export function resolvePiPrivateSessionDirectory(input: {
|
|
37
|
+
readonly sandboxHomeDir: string;
|
|
38
|
+
readonly sessionWorkDir: string;
|
|
39
|
+
readonly sessionId: string;
|
|
40
|
+
}): string {
|
|
41
|
+
const sessionKey = createHash('sha256').update(input.sessionId).digest('hex');
|
|
42
|
+
const privateSessionDir = path.posix.join(
|
|
43
|
+
input.sandboxHomeDir,
|
|
44
|
+
'.ai-sdk',
|
|
45
|
+
'harness-pi',
|
|
46
|
+
sessionKey,
|
|
47
|
+
);
|
|
48
|
+
const relativePath = path.posix.relative(
|
|
49
|
+
input.sessionWorkDir,
|
|
50
|
+
privateSessionDir,
|
|
51
|
+
);
|
|
52
|
+
if (
|
|
53
|
+
relativePath === '' ||
|
|
54
|
+
(!relativePath.startsWith('../') && !path.posix.isAbsolute(relativePath))
|
|
55
|
+
) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Pi private session directory ${JSON.stringify(privateSessionDir)} must be outside sessionWorkDir ${JSON.stringify(input.sessionWorkDir)}.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return privateSessionDir;
|
|
61
|
+
}
|
|
37
62
|
|
|
38
63
|
function resolveContainedHostPath(input: {
|
|
39
64
|
readonly baseDir: string;
|
|
@@ -56,10 +81,10 @@ function resolveContainedHostPath(input: {
|
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
function resolveContainedSandboxPath(input: {
|
|
59
|
-
readonly
|
|
84
|
+
readonly privateSessionDir: string;
|
|
60
85
|
readonly sessionFileName: string;
|
|
61
86
|
}): string {
|
|
62
|
-
const sessionDir = path.posix.resolve(input.
|
|
87
|
+
const sessionDir = path.posix.resolve(input.privateSessionDir);
|
|
63
88
|
const filePath = path.posix.resolve(
|
|
64
89
|
sessionDir,
|
|
65
90
|
safePiSessionFileName(input.sessionFileName),
|
|
@@ -76,13 +101,13 @@ function resolveContainedSandboxPath(input: {
|
|
|
76
101
|
}
|
|
77
102
|
|
|
78
103
|
/**
|
|
79
|
-
* Copy the Pi session file from the host's local mirror to
|
|
80
|
-
*
|
|
81
|
-
*
|
|
104
|
+
* Copy the Pi session file from the host's local mirror to private sandbox
|
|
105
|
+
* state. Called during resumable lifecycle methods so the session survives a
|
|
106
|
+
* sandbox snapshot or a process handoff.
|
|
82
107
|
*/
|
|
83
108
|
export async function persistSessionFileToSandbox(args: {
|
|
84
109
|
readonly sandbox: Experimental_SandboxSession;
|
|
85
|
-
readonly
|
|
110
|
+
readonly privateSessionDir: string;
|
|
86
111
|
readonly hostSessionDir: string;
|
|
87
112
|
readonly sessionFileName: string;
|
|
88
113
|
readonly abortSignal?: AbortSignal;
|
|
@@ -93,14 +118,9 @@ export async function persistSessionFileToSandbox(args: {
|
|
|
93
118
|
});
|
|
94
119
|
const content = await readFile(hostPath);
|
|
95
120
|
const remotePath = resolveContainedSandboxPath({
|
|
96
|
-
|
|
121
|
+
privateSessionDir: args.privateSessionDir,
|
|
97
122
|
sessionFileName: args.sessionFileName,
|
|
98
123
|
});
|
|
99
|
-
// Ensure the parent dir exists in the sandbox before writing.
|
|
100
|
-
await args.sandbox.run({
|
|
101
|
-
command: `mkdir -p ${shellQuote(path.posix.dirname(remotePath))}`,
|
|
102
|
-
...(args.abortSignal ? { abortSignal: args.abortSignal } : {}),
|
|
103
|
-
});
|
|
104
124
|
await args.sandbox.writeBinaryFile({
|
|
105
125
|
path: remotePath,
|
|
106
126
|
content,
|
|
@@ -116,20 +136,20 @@ export async function persistSessionFileToSandbox(args: {
|
|
|
116
136
|
*/
|
|
117
137
|
export async function pullSessionFileFromSandbox(args: {
|
|
118
138
|
readonly sandbox: Experimental_SandboxSession;
|
|
119
|
-
readonly
|
|
139
|
+
readonly privateSessionDir: string;
|
|
120
140
|
readonly hostSessionDir: string;
|
|
121
141
|
readonly sessionFileName: string;
|
|
122
142
|
readonly abortSignal?: AbortSignal;
|
|
123
143
|
}): Promise<string | undefined> {
|
|
124
144
|
const remotePath = resolveContainedSandboxPath({
|
|
125
|
-
|
|
145
|
+
privateSessionDir: args.privateSessionDir,
|
|
126
146
|
sessionFileName: args.sessionFileName,
|
|
127
147
|
});
|
|
128
148
|
const bytes = await args.sandbox.readBinaryFile({
|
|
129
149
|
path: remotePath,
|
|
130
150
|
...(args.abortSignal ? { abortSignal: args.abortSignal } : {}),
|
|
131
151
|
});
|
|
132
|
-
if (
|
|
152
|
+
if (bytes == null) return undefined;
|
|
133
153
|
await mkdir(args.hostSessionDir, { recursive: true });
|
|
134
154
|
const hostPath = resolveContainedHostPath({
|
|
135
155
|
baseDir: args.hostSessionDir,
|
package/src/pi-session.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { writePiSkills } from './pi-skills';
|
|
|
44
44
|
import {
|
|
45
45
|
persistSessionFileToSandbox,
|
|
46
46
|
pullSessionFileFromSandbox,
|
|
47
|
+
resolvePiPrivateSessionDirectory,
|
|
47
48
|
safePiSessionFileName,
|
|
48
49
|
} from './pi-resume-state';
|
|
49
50
|
import {
|
|
@@ -289,16 +290,21 @@ export async function createPiSession(
|
|
|
289
290
|
await mkdir(hostSessionDir, { recursive: true });
|
|
290
291
|
|
|
291
292
|
const sandbox = input.sandboxSession.restricted();
|
|
293
|
+
const sandboxHomeDir = await resolveSandboxHomeDir({
|
|
294
|
+
sandbox,
|
|
295
|
+
...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
|
|
296
|
+
});
|
|
297
|
+
const privateSessionDir = resolvePiPrivateSessionDirectory({
|
|
298
|
+
sandboxHomeDir,
|
|
299
|
+
sessionWorkDir: input.sessionWorkDir,
|
|
300
|
+
sessionId: input.sessionId,
|
|
301
|
+
});
|
|
292
302
|
const permissionMode = input.permissionMode ?? 'allow-all';
|
|
293
303
|
let sandboxSkillRootDir: string | undefined;
|
|
294
304
|
let harnessSkills: Skill[] = [];
|
|
295
305
|
|
|
296
306
|
// Materialise harness-provided skills into sandbox HOME, not the workspace.
|
|
297
307
|
if (input.skills.length > 0) {
|
|
298
|
-
const sandboxHomeDir = await resolveSandboxHomeDir({
|
|
299
|
-
sandbox,
|
|
300
|
-
...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
|
|
301
|
-
});
|
|
302
308
|
sandboxSkillRootDir = path.posix.join(sandboxHomeDir, '.agents', 'skills');
|
|
303
309
|
harnessSkills = createHarnessPiSkills({
|
|
304
310
|
skills: input.skills,
|
|
@@ -321,7 +327,7 @@ export async function createPiSession(
|
|
|
321
327
|
);
|
|
322
328
|
resumeSessionFilePath = await pullSessionFileFromSandbox({
|
|
323
329
|
sandbox,
|
|
324
|
-
|
|
330
|
+
privateSessionDir,
|
|
325
331
|
hostSessionDir,
|
|
326
332
|
sessionFileName: resumeSessionFileName,
|
|
327
333
|
...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
|
|
@@ -553,7 +559,7 @@ export async function createPiSession(
|
|
|
553
559
|
if (!sessionFileName) return;
|
|
554
560
|
await persistSessionFileToSandbox({
|
|
555
561
|
sandbox,
|
|
556
|
-
|
|
562
|
+
privateSessionDir,
|
|
557
563
|
hostSessionDir,
|
|
558
564
|
sessionFileName,
|
|
559
565
|
});
|