@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/CHANGELOG.md +24 -0
- package/dist/index.d.ts +7 -26
- package/dist/index.js +166 -113
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +1 -1
- package/src/pi-auth.ts +194 -146
- package/src/pi-harness.ts +6 -4
- package/src/pi-session.ts +36 -26
- package/src/pi-skills.ts +3 -3
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.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.
|
|
30
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
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.
|
|
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.
|
|
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
|
|
12
|
+
export type { PiAuthenticationMode } from './pi-auth';
|
package/src/pi-auth.ts
CHANGED
|
@@ -1,51 +1,175 @@
|
|
|
1
|
-
import
|
|
2
|
-
ModelRegistry,
|
|
1
|
+
import {
|
|
3
2
|
ModelRuntime,
|
|
3
|
+
type CreateModelRuntimeOptions,
|
|
4
|
+
type ModelRegistry,
|
|
4
5
|
} from '@earendil-works/pi-coding-agent';
|
|
5
|
-
import {
|
|
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
|
-
| '
|
|
16
|
-
|
|
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:
|
|
217
|
+
options: PiAuthenticationMode | undefined;
|
|
101
218
|
env: NodeJS.ProcessEnv;
|
|
102
219
|
}): Record<string, string> {
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
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 (
|
|
230
|
+
if (authenticationEnvironment.OPENAI_API_KEY) {
|
|
127
231
|
return {
|
|
128
|
-
OPENAI_API_KEY:
|
|
129
|
-
...(
|
|
130
|
-
? { 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 (
|
|
240
|
+
if (authenticationEnvironment.ANTHROPIC_API_KEY) {
|
|
137
241
|
return {
|
|
138
|
-
ANTHROPIC_API_KEY:
|
|
139
|
-
...(
|
|
140
|
-
? {
|
|
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
|
-
...(
|
|
143
|
-
? {
|
|
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(
|
|
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(
|
|
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:
|
|
315
|
+
options: PiAuthenticationMode | undefined;
|
|
206
316
|
resolvedEnv: Record<string, string>;
|
|
207
317
|
registries: PiRegistries;
|
|
208
318
|
clientApp?: string;
|
|
209
319
|
}): Promise<void> {
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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({
|
|
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({
|
|
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(
|
|
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 {
|
|
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?:
|
|
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
|
-
...(
|
|
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
|
|
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?:
|
|
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
|
-
|
|
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
|
|
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,
|