@ai-sdk/harness-pi 1.0.93 → 1.0.95

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.93",
3
+ "version": "1.0.95",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -26,10 +26,10 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@ai-sdk/harness": "1.0.91",
30
- "@ai-sdk/provider-utils": "5.0.32",
29
+ "@ai-sdk/harness": "1.0.93",
30
+ "@ai-sdk/provider-utils": "5.0.33",
31
31
  "@earendil-works/pi-ai": "0.74.2",
32
- "@earendil-works/pi-coding-agent": "^0.80.10",
32
+ "@earendil-works/pi-coding-agent": "^0.84.3",
33
33
  "pi-mcp-adapter": "2.12.1",
34
34
  "typebox": "^1.1.38"
35
35
  },
@@ -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.91",
40
+ "@ai-sdk/sandbox-just-bash": "1.0.93",
41
41
  "@types/node": "22.19.19",
42
42
  "@vercel/ai-tsconfig": "0.0.0",
43
43
  "tsup": "^8.5.1",
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 { PiAuthenticationMode, PiAuthOptions } from './pi-auth';
12
+ export type { PiAuthenticationMode } from './pi-auth';
package/src/pi-auth.ts CHANGED
@@ -1,51 +1,175 @@
1
- import type {
2
- ModelRegistry,
1
+ import {
3
2
  ModelRuntime,
3
+ type CreateModelRuntimeOptions,
4
+ type ModelRegistry,
4
5
  } from '@earendil-works/pi-coding-agent';
5
- import { getAiGatewayAuthFromEnv } from '@ai-sdk/harness/utils';
6
+ import type { HarnessV1Authentication } from '@ai-sdk/harness';
7
+ import {
8
+ getAiGatewayAuthFromEnv,
9
+ isHarnessAuthenticationEnvironment,
10
+ } from '@ai-sdk/harness/utils';
11
+ import { access } from 'node:fs/promises';
6
12
  import { VERSION } from './version';
7
13
 
8
14
  type ProviderConfigInput = Parameters<ModelRegistry['registerProvider']>[1];
15
+ type PiCredentialStore = NonNullable<CreateModelRuntimeOptions['credentials']>;
16
+ type PiCredential = Exclude<
17
+ Awaited<ReturnType<PiCredentialStore['read']>>,
18
+ undefined
19
+ >;
20
+ type PiModelRuntimeInternals = {
21
+ models: {
22
+ authContext: {
23
+ env(name: string): Promise<string | undefined>;
24
+ fileExists(path: string): Promise<boolean>;
25
+ };
26
+ };
27
+ };
28
+ type PiMutableProvider = {
29
+ auth: ReturnType<ModelRuntime['getProviders']>[number]['auth'];
30
+ };
9
31
 
10
32
  /**
11
33
  * Pi auth options. Choose an explicit mode or rely on 'auto' (precedence:
12
34
  * explicit gateway, then OpenAI / Anthropic / custom environment variables).
13
35
  */
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 = {
25
- readonly gateway?: {
26
- readonly apiKey?: string;
27
- readonly baseUrl?: string;
28
- };
29
- /**
30
- * Resolved environment-variable pairs of the form `<PREFIX>_API_KEY` and
31
- * (optionally) `<PREFIX>_BASE_URL`. Special-cased prefixes:
32
- * - `AI_GATEWAY` → registers `vercel-ai-gateway`
33
- * - `OPENAI` → registers `openai`
34
- * - `ANTHROPIC` → registers `anthropic` (`ANTHROPIC_AUTH_TOKEN` adds a
35
- * bearer auth header)
36
- * Any other `<PREFIX>_API_KEY` with a matching `<PREFIX>_BASE_URL` is
37
- * registered as the lowercased, dash-separated prefix.
38
- */
39
- readonly customEnv?: Record<string, string>;
40
- };
41
-
42
- export type PiAuthOptions = PiAuthenticationMode | LegacyPiAuthOptions;
36
+ export type PiAuthenticationMode = HarnessV1Authentication<
37
+ 'openai' | 'anthropic' | 'custom'
38
+ >;
43
39
 
44
40
  const DEFAULT_GATEWAY_BASE_URL = 'https://ai-gateway.vercel.sh';
45
41
  const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';
46
42
  const DEFAULT_ANTHROPIC_BASE_URL = 'https://api.anthropic.com';
47
43
  const HARNESS_CLIENT_APP = `ai-sdk/harness-pi/${VERSION}`;
48
44
 
45
+ function createIsolatedPiCredentialStore(): {
46
+ credentials: PiCredentialStore;
47
+ finishInitialization(): void;
48
+ } {
49
+ let initializing = true;
50
+ const bootstrapEnvironment = new Proxy<Record<string, string>>(
51
+ {},
52
+ {
53
+ get: (_target, property) =>
54
+ typeof property === 'string'
55
+ ? 'harness-pi-authentication-bootstrap'
56
+ : undefined,
57
+ },
58
+ );
59
+ const bootstrapCredential = {
60
+ type: 'api_key',
61
+ key: 'harness-pi-authentication-bootstrap',
62
+ env: bootstrapEnvironment,
63
+ } satisfies PiCredential;
64
+ const credentials: PiCredentialStore = {
65
+ async read() {
66
+ return initializing ? bootstrapCredential : undefined;
67
+ },
68
+ async list() {
69
+ return [];
70
+ },
71
+ async modify(..._input: Parameters<PiCredentialStore['modify']>) {
72
+ return undefined;
73
+ },
74
+ async delete() {},
75
+ };
76
+
77
+ return {
78
+ credentials,
79
+ finishInitialization() {
80
+ initializing = false;
81
+ },
82
+ };
83
+ }
84
+
85
+ function scopePiProviderEnvironment({
86
+ modelRuntime,
87
+ authenticationEnvironment,
88
+ }: {
89
+ modelRuntime: ModelRuntime;
90
+ authenticationEnvironment: Record<string, string>;
91
+ }): void {
92
+ for (const provider of modelRuntime.getProviders()) {
93
+ const apiKeyAuthentication = provider.auth.apiKey;
94
+ if (!apiKeyAuthentication) continue;
95
+
96
+ (provider as unknown as PiMutableProvider).auth = {
97
+ ...provider.auth,
98
+ apiKey: {
99
+ ...apiKeyAuthentication,
100
+ resolve: async input => {
101
+ const result = await apiKeyAuthentication.resolve(input);
102
+ return result
103
+ ? {
104
+ ...result,
105
+ env: {
106
+ ...authenticationEnvironment,
107
+ ...result.env,
108
+ },
109
+ }
110
+ : undefined;
111
+ },
112
+ },
113
+ };
114
+ }
115
+ }
116
+
117
+ export async function createPiModelRuntime({
118
+ auth,
119
+ authPath,
120
+ modelsPath,
121
+ }: {
122
+ auth: PiAuthenticationMode | undefined;
123
+ authPath: string;
124
+ modelsPath: string;
125
+ }): Promise<ModelRuntime> {
126
+ if (!isHarnessAuthenticationEnvironment(auth)) {
127
+ return ModelRuntime.create({
128
+ authPath,
129
+ modelsPath,
130
+ allowModelNetwork: false,
131
+ });
132
+ }
133
+
134
+ const isolatedCredentials = createIsolatedPiCredentialStore();
135
+ const modelRuntime = await ModelRuntime.create({
136
+ credentials: isolatedCredentials.credentials,
137
+ modelsPath: null,
138
+ allowModelNetwork: false,
139
+ });
140
+
141
+ /*
142
+ * ModelRuntime creates its internal model collection with a process-backed
143
+ * authentication context and does not expose an authentication-context
144
+ * option. The bootstrap credential prevents construction-time provider
145
+ * checks from consulting that context. Once constructed, authentication is
146
+ * scoped to the supplied record and availability is recomputed with an
147
+ * empty in-memory credential store.
148
+ */
149
+ (modelRuntime as unknown as PiModelRuntimeInternals).models.authContext = {
150
+ async env(name) {
151
+ return auth[name];
152
+ },
153
+ async fileExists(filePath) {
154
+ if (filePath !== auth.GOOGLE_APPLICATION_CREDENTIALS) return false;
155
+ try {
156
+ await access(filePath);
157
+ return true;
158
+ } catch {
159
+ return false;
160
+ }
161
+ },
162
+ };
163
+ scopePiProviderEnvironment({
164
+ modelRuntime,
165
+ authenticationEnvironment: auth,
166
+ });
167
+ isolatedCredentials.finishInitialization();
168
+ await modelRuntime.refresh({ allowNetwork: false });
169
+
170
+ return modelRuntime;
171
+ }
172
+
49
173
  function createGatewayProviderConfig({
50
174
  apiKey,
51
175
  baseUrl,
@@ -86,68 +210,54 @@ async function register({
86
210
  await registries.modelRuntime.setRuntimeApiKey(provider, apiKey);
87
211
  }
88
212
 
89
- function hasConfiguredValue(value: unknown): boolean {
90
- if (value == null) return false;
91
- if (typeof value === 'string') return value.length > 0;
92
- if (typeof value !== 'object') return true;
93
- return Object.values(value).some(hasConfiguredValue);
94
- }
95
-
96
213
  export function resolvePiEnv({
97
214
  options,
98
215
  env,
99
216
  }: {
100
- options: PiAuthOptions | undefined;
217
+ options: PiAuthenticationMode | undefined;
101
218
  env: NodeJS.ProcessEnv;
102
219
  }): Record<string, string> {
103
- const normalizedOptions = normalizePiAuthToLegacyAuth(options);
104
- const customEnvConfigured = hasConfiguredValue(normalizedOptions?.customEnv);
105
- if (customEnvConfigured) {
106
- return resolveCustomEnv({ customEnv: normalizedOptions!.customEnv ?? {} });
107
- }
108
-
109
- const gatewayConfigured = hasConfiguredValue(normalizedOptions?.gateway);
110
- const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({ env });
111
- if (gatewayConfigured) {
112
- const apiKey =
113
- normalizedOptions!.gateway?.apiKey ?? gatewayAuthFromEnv.apiKey;
114
- const baseUrl =
115
- normalizedOptions!.gateway?.baseUrl ?? gatewayAuthFromEnv.baseUrl;
116
- if (apiKey) {
117
- return { AI_GATEWAY_API_KEY: apiKey, AI_GATEWAY_BASE_URL: baseUrl };
118
- }
119
- return {};
120
- }
220
+ const suppliedEnvironment = isHarnessAuthenticationEnvironment(options);
221
+ const authenticationEnvironment = suppliedEnvironment ? options : env;
222
+ const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({
223
+ env: authenticationEnvironment,
224
+ });
121
225
 
122
226
  // Handle explicit string modes with process env
123
227
  if (typeof options === 'string') {
124
228
  switch (options) {
125
229
  case 'openai':
126
- if (env.OPENAI_API_KEY) {
230
+ if (authenticationEnvironment.OPENAI_API_KEY) {
127
231
  return {
128
- OPENAI_API_KEY: env.OPENAI_API_KEY,
129
- ...(env.OPENAI_BASE_URL
130
- ? { OPENAI_BASE_URL: env.OPENAI_BASE_URL }
232
+ OPENAI_API_KEY: authenticationEnvironment.OPENAI_API_KEY,
233
+ ...(authenticationEnvironment.OPENAI_BASE_URL
234
+ ? { OPENAI_BASE_URL: authenticationEnvironment.OPENAI_BASE_URL }
131
235
  : {}),
132
236
  };
133
237
  }
134
238
  return {};
135
239
  case 'anthropic':
136
- if (env.ANTHROPIC_API_KEY) {
240
+ if (authenticationEnvironment.ANTHROPIC_API_KEY) {
137
241
  return {
138
- ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY,
139
- ...(env.ANTHROPIC_BASE_URL
140
- ? { ANTHROPIC_BASE_URL: env.ANTHROPIC_BASE_URL }
242
+ ANTHROPIC_API_KEY: authenticationEnvironment.ANTHROPIC_API_KEY,
243
+ ...(authenticationEnvironment.ANTHROPIC_BASE_URL
244
+ ? {
245
+ ANTHROPIC_BASE_URL:
246
+ authenticationEnvironment.ANTHROPIC_BASE_URL,
247
+ }
141
248
  : {}),
142
- ...(env.ANTHROPIC_AUTH_TOKEN
143
- ? { ANTHROPIC_AUTH_TOKEN: env.ANTHROPIC_AUTH_TOKEN }
249
+ ...(authenticationEnvironment.ANTHROPIC_AUTH_TOKEN
250
+ ? {
251
+ ANTHROPIC_AUTH_TOKEN:
252
+ authenticationEnvironment.ANTHROPIC_AUTH_TOKEN,
253
+ }
144
254
  : {}),
145
255
  };
146
256
  }
147
257
  return {};
148
258
  case 'custom': {
149
259
  const result: Record<string, string> = {};
150
- for (const [key, value] of Object.entries(env)) {
260
+ for (const [key, value] of Object.entries(authenticationEnvironment)) {
151
261
  if (
152
262
  value &&
153
263
  (key.endsWith('_API_KEY') ||
@@ -183,7 +293,7 @@ export function resolvePiEnv({
183
293
 
184
294
  // 'auto' fallback: pick up any other provider credentials from the env.
185
295
  const ambient: Record<string, string> = {};
186
- for (const [key, value] of Object.entries(env)) {
296
+ for (const [key, value] of Object.entries(authenticationEnvironment)) {
187
297
  if (
188
298
  value &&
189
299
  (key.endsWith('_API_KEY') ||
@@ -202,32 +312,20 @@ export async function registerPiProviders({
202
312
  registries,
203
313
  clientApp = HARNESS_CLIENT_APP,
204
314
  }: {
205
- options: PiAuthOptions | undefined;
315
+ options: PiAuthenticationMode | undefined;
206
316
  resolvedEnv: Record<string, string>;
207
317
  registries: PiRegistries;
208
318
  clientApp?: string;
209
319
  }): Promise<void> {
210
- const normalizedOptions = normalizePiAuthToLegacyAuth(options);
211
- if (hasConfiguredValue(normalizedOptions?.customEnv)) {
212
- await registerCustomProviders({
213
- customEnv: normalizedOptions!.customEnv ?? {},
214
- registries,
215
- clientApp,
216
- });
217
- return;
218
- }
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';
320
+ const suppliedEnvironment = isHarnessAuthenticationEnvironment(options);
321
+ const authenticationEnvironment = suppliedEnvironment ? options : process.env;
322
+ const mode = typeof options === 'string' ? options : 'auto';
225
323
 
226
324
  switch (mode) {
227
325
  case 'openai': {
228
326
  const env = pickOpenAIEnv(resolvedEnv);
229
327
  await registerCustomProviders({
230
- customEnv: { ...pickOpenAIEnv(process.env), ...env },
328
+ customEnv: { ...pickOpenAIEnv(authenticationEnvironment), ...env },
231
329
  registries,
232
330
  clientApp,
233
331
  });
@@ -236,7 +334,7 @@ export async function registerPiProviders({
236
334
  case 'anthropic': {
237
335
  const env = pickAnthropicEnv(resolvedEnv);
238
336
  await registerCustomProviders({
239
- customEnv: { ...pickAnthropicEnv(process.env), ...env },
337
+ customEnv: { ...pickAnthropicEnv(authenticationEnvironment), ...env },
240
338
  registries,
241
339
  clientApp,
242
340
  });
@@ -246,14 +344,16 @@ export async function registerPiProviders({
246
344
  // 'custom' registers every provider with credentials in the env.
247
345
  const env = pickProviderEnv(resolvedEnv);
248
346
  await registerCustomProviders({
249
- customEnv: { ...pickProviderEnv(process.env), ...env },
347
+ customEnv: { ...pickProviderEnv(authenticationEnvironment), ...env },
250
348
  registries,
251
349
  clientApp,
252
350
  });
253
351
  return;
254
352
  }
255
353
  case 'ai-gateway': {
256
- const gatewayAuth = getAiGatewayAuthFromEnv({ env: process.env });
354
+ const gatewayAuth = getAiGatewayAuthFromEnv({
355
+ env: authenticationEnvironment,
356
+ });
257
357
  const gatewayApiKey =
258
358
  resolvedEnv.AI_GATEWAY_API_KEY ?? gatewayAuth.apiKey;
259
359
  const gatewayBaseUrl =
@@ -271,13 +371,13 @@ export async function registerPiProviders({
271
371
  });
272
372
  return;
273
373
  }
274
- case 'legacy':
275
- break; // handled below
276
374
  case 'auto':
277
375
  default: {
278
376
  // 'auto' (the default): prefer the AI Gateway; only when no gateway
279
377
  // credentials exist, fall back to other providers found in the env.
280
- const gatewayAuth = getAiGatewayAuthFromEnv({ env: process.env });
378
+ const gatewayAuth = getAiGatewayAuthFromEnv({
379
+ env: authenticationEnvironment,
380
+ });
281
381
  const gatewayApiKey =
282
382
  resolvedEnv.AI_GATEWAY_API_KEY ?? gatewayAuth.apiKey;
283
383
  const gatewayBaseUrl =
@@ -297,25 +397,13 @@ export async function registerPiProviders({
297
397
  }
298
398
  const env = pickProviderEnv(resolvedEnv);
299
399
  await registerCustomProviders({
300
- customEnv: { ...pickProviderEnv(process.env), ...env },
400
+ customEnv: { ...pickProviderEnv(authenticationEnvironment), ...env },
301
401
  registries,
302
402
  clientApp,
303
403
  });
304
404
  return;
305
405
  }
306
406
  }
307
-
308
- // Legacy explicit gateway object options.
309
- const apiKey = resolvedEnv.AI_GATEWAY_API_KEY;
310
- const baseUrl = resolvedEnv.AI_GATEWAY_BASE_URL;
311
- if (!apiKey || !baseUrl) return;
312
-
313
- await register({
314
- registries,
315
- provider: 'vercel-ai-gateway',
316
- apiKey,
317
- config: createGatewayProviderConfig({ apiKey, baseUrl, clientApp }),
318
- });
319
407
  }
320
408
 
321
409
  function pickOpenAIEnv(
@@ -363,46 +451,6 @@ function pickProviderEnv(
363
451
  return result;
364
452
  }
365
453
 
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
-
391
- function resolveCustomEnv({
392
- customEnv,
393
- }: {
394
- customEnv: Record<string, string>;
395
- }): Record<string, string> {
396
- const apiKey = customEnv.AI_GATEWAY_API_KEY;
397
- if (!apiKey) return {};
398
-
399
- return {
400
- AI_GATEWAY_API_KEY: apiKey,
401
- AI_GATEWAY_BASE_URL:
402
- customEnv.AI_GATEWAY_BASE_URL ?? DEFAULT_GATEWAY_BASE_URL,
403
- };
404
- }
405
-
406
454
  async function registerCustomProviders({
407
455
  customEnv,
408
456
  registries,
package/src/pi-harness.ts CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  import { tool } from '@ai-sdk/provider-utils';
7
7
  import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
8
8
  import { z } from 'zod/v4';
9
- import type { PiAuthOptions } from './pi-auth';
9
+ import type { PiAuthenticationMode } from './pi-auth';
10
10
  import { piResumeStateSchema } from './pi-resume-state';
11
11
  import { createPiSession, type PiThinkingLevel } from './pi-session';
12
12
  import { VERSION } from './version';
@@ -22,11 +22,13 @@ const PI_CLIENT_APP = `ai-sdk/harness-pi/${VERSION}`;
22
22
  */
23
23
  export type PiHarnessSettings = {
24
24
  /** Where Pi sources API keys / gateway credentials from. */
25
- readonly auth?: PiAuthOptions;
25
+ readonly auth?: PiAuthenticationMode;
26
26
  /**
27
27
  * Pi model id (or name). Leaving this unset falls back to the AI Gateway
28
28
  * default when `AI_GATEWAY_API_KEY` / `VERCEL_OIDC_TOKEN` is set, and to
29
29
  * Pi's own resolution otherwise.
30
+ *
31
+ * @deprecated Use `model` on `HarnessAgent` instead.
30
32
  */
31
33
  readonly model?: string;
32
34
  /**
@@ -140,6 +142,7 @@ export function createPi(
140
142
  supportsBuiltinToolFiltering: true,
141
143
  lifecycleStateSchema: piResumeStateSchema,
142
144
  doStart: async startOpts => {
145
+ const model = startOpts.model ?? settings.model;
143
146
  const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
144
147
  const resumeData = lifecycleState?.data as
145
148
  | { sessionFileName?: string }
@@ -149,10 +152,9 @@ export function createPi(
149
152
  sessionId: startOpts.sessionId,
150
153
  sandboxSession: startOpts.sandboxSession,
151
154
  sessionWorkDir: startOpts.sessionWorkDir,
152
- skills: startOpts.skills ?? [],
153
155
  settings: {
154
156
  ...(settings.auth ? { auth: settings.auth } : {}),
155
- ...(settings.model ? { model: settings.model } : {}),
157
+ ...(model == null ? {} : { model }),
156
158
  ...(settings.thinkingLevel
157
159
  ? { thinkingLevel: settings.thinkingLevel }
158
160
  : {}),
package/src/pi-session.ts CHANGED
@@ -3,7 +3,6 @@ import {
3
3
  DefaultResourceLoader,
4
4
  defineTool,
5
5
  ModelRegistry,
6
- ModelRuntime,
7
6
  SessionManager,
8
7
  SettingsManager,
9
8
  type AgentSession,
@@ -37,9 +36,10 @@ import {
37
36
  } from '@ai-sdk/harness/utils';
38
37
  import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
39
38
  import {
39
+ createPiModelRuntime,
40
40
  registerPiProviders,
41
41
  resolvePiEnv,
42
- type PiAuthOptions,
42
+ type PiAuthenticationMode,
43
43
  } from './pi-auth';
44
44
  import { getPiTerminalError, parseNativeEvent } from './pi-events';
45
45
  import { createPiModelResolver } from './pi-model-resolver';
@@ -212,10 +212,11 @@ export type PiThinkingLevel =
212
212
  | 'low'
213
213
  | 'medium'
214
214
  | 'high'
215
- | 'xhigh';
215
+ | 'xhigh'
216
+ | 'max';
216
217
 
217
218
  export interface PiSessionSettings {
218
- readonly auth?: PiAuthOptions;
219
+ readonly auth?: PiAuthenticationMode;
219
220
  readonly model?: string;
220
221
  readonly thinkingLevel?: PiThinkingLevel;
221
222
  readonly mcpServers?: Record<string, unknown>;
@@ -226,7 +227,6 @@ export interface CreatePiSessionInput {
226
227
  readonly sessionId: string;
227
228
  readonly sandboxSession: HarnessV1NetworkSandboxSession | SandboxSession;
228
229
  readonly sessionWorkDir: string;
229
- readonly skills: ReadonlyArray<HarnessV1Skill>;
230
230
  readonly settings: PiSessionSettings;
231
231
  readonly clientApp: string;
232
232
  readonly isResume: boolean;
@@ -331,24 +331,13 @@ export async function createPiSession(
331
331
  sessionId: input.sessionId,
332
332
  });
333
333
  const permissionMode = input.permissionMode ?? 'allow-all';
334
- let sandboxSkillRootDir: string | undefined;
334
+ const sandboxSkillRootDir = path.posix.join(
335
+ sandboxHomeDir,
336
+ '.agents',
337
+ 'skills',
338
+ );
335
339
  let harnessSkills: Skill[] = [];
336
340
 
337
- // Materialise harness-provided skills into sandbox HOME, not the workspace.
338
- if (input.skills.length > 0) {
339
- sandboxSkillRootDir = path.posix.join(sandboxHomeDir, '.agents', 'skills');
340
- harnessSkills = createHarnessPiSkills({
341
- skills: input.skills,
342
- sandboxSkillRootDir,
343
- });
344
- await writePiSkills({
345
- sandbox: toolSafeSandboxSession,
346
- sandboxHomeDir,
347
- skills: input.skills,
348
- ...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
349
- });
350
- }
351
-
352
341
  // On resume: pull the Pi session file out of the sandbox into the fresh
353
342
  // host mirror so SessionManager.open can read it.
354
343
  let resumeSessionFilePath: string | undefined;
@@ -384,20 +373,23 @@ export async function createPiSession(
384
373
  const paths = createPiPathMapper({
385
374
  hostWorkDir,
386
375
  sandboxWorkDir: sessionWorkDir,
387
- readableRoots: sandboxSkillRootDir
388
- ? [{ sandboxDir: sandboxSkillRootDir }]
389
- : [],
376
+ readableRoots: [{ sandboxDir: sandboxSkillRootDir }],
390
377
  });
391
378
 
392
379
  // Pi auth + model registry are global to this Pi session. These live on the
393
380
  // real host filesystem, never in the sandbox/workspace.
394
381
  // When `agentDir` is provided, use it instead so the harness can reuse
395
382
  // existing CLI logins and model/settings config.
383
+ /*
384
+ * A record-shaped authentication override makes createPiModelRuntime ignore
385
+ * auth.json and models.json because both files can supply credentials from
386
+ * outside that record. General Pi settings still use agentDir below.
387
+ */
396
388
  const agentDir = input.agentDir ?? hostAgentDir;
397
- const modelRuntime = await ModelRuntime.create({
389
+ const modelRuntime = await createPiModelRuntime({
390
+ auth: input.settings.auth,
398
391
  authPath: path.join(agentDir, 'auth.json'),
399
392
  modelsPath: path.join(agentDir, 'models.json'),
400
- allowModelNetwork: false,
401
393
  });
402
394
  const modelRegistry = new ModelRegistry(modelRuntime);
403
395
  const settingsManager =
@@ -792,6 +784,7 @@ export async function createPiSession(
792
784
  // rebuilding the Pi session from it.
793
785
  const control = await runTurn({
794
786
  text: '',
787
+ skills: continueOpts.skills,
795
788
  tools: continueOpts.tools ?? [],
796
789
  instructions: continueOpts.instructions,
797
790
  emit: continueOpts.emit,
@@ -1068,6 +1061,7 @@ export async function createPiSession(
1068
1061
  */
1069
1062
  async function runTurn(turnOpts: {
1070
1063
  text: string;
1064
+ skills: ReadonlyArray<HarnessV1Skill>;
1071
1065
  tools: ReadonlyArray<HarnessV1ToolSpec>;
1072
1066
  instructions?: string;
1073
1067
  emit: (part: HarnessV1StreamPart) => void;
@@ -1077,6 +1071,20 @@ export async function createPiSession(
1077
1071
  throw new Error('Pi session has been stopped.');
1078
1072
  }
1079
1073
 
1074
+ const skillWriteResult = await writePiSkills({
1075
+ sandbox: toolSafeSandboxSession,
1076
+ sandboxHomeDir,
1077
+ skills: turnOpts.skills,
1078
+ abortSignal: turnOpts.abortSignal,
1079
+ });
1080
+ harnessSkills = createHarnessPiSkills({
1081
+ skills: turnOpts.skills,
1082
+ sandboxSkillRootDir,
1083
+ });
1084
+ if (piSession != null && skillWriteResult.changed) {
1085
+ await reloadResourcesOnly();
1086
+ }
1087
+
1080
1088
  const userTools = turnOpts.tools;
1081
1089
  currentEmit = turnOpts.emit;
1082
1090
  const turnAbortController = new AbortController();
@@ -1329,6 +1337,7 @@ export async function createPiSession(
1329
1337
  }
1330
1338
  return runTurn({
1331
1339
  text: extractUserText(promptOpts.prompt),
1340
+ skills: promptOpts.skills,
1332
1341
  tools: promptOpts.tools ?? [],
1333
1342
  instructions: promptOpts.instructions,
1334
1343
  emit: promptOpts.emit,
@@ -1385,6 +1394,7 @@ export async function createPiSession(
1385
1394
  */
1386
1395
  return runTurn({
1387
1396
  text: '',
1397
+ skills: continueOpts.skills,
1388
1398
  tools: continueOpts.tools ?? [],
1389
1399
  instructions: continueOpts.instructions,
1390
1400
  emit: continueOpts.emit,