@ai-sdk/harness-pi 1.0.109 → 1.0.111

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.109",
3
+ "version": "1.0.111",
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.107",
30
- "@ai-sdk/provider-utils": "5.0.39",
29
+ "@ai-sdk/harness": "1.0.109",
30
+ "@ai-sdk/provider-utils": "5.0.40",
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.107",
40
+ "@ai-sdk/sandbox-just-bash": "1.0.109",
41
41
  "@types/node": "22.19.19",
42
42
  "@vercel/ai-tsconfig": "0.0.0",
43
43
  "tsup": "^8.5.1",
package/src/pi-harness.ts CHANGED
@@ -4,7 +4,10 @@ import {
4
4
  type HarnessV1BuiltinTool,
5
5
  } from '@ai-sdk/harness';
6
6
  import { tool } from '@ai-sdk/provider-utils';
7
- import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
7
+ import type {
8
+ ExtensionFactory,
9
+ ProviderConfig,
10
+ } from '@earendil-works/pi-coding-agent';
8
11
  import { z } from 'zod/v4';
9
12
  import type { PiAuthenticationMode } from './pi-auth';
10
13
  import { piResumeStateSchema } from './pi-resume-state';
@@ -23,6 +26,12 @@ const PI_CLIENT_APP = `ai-sdk/harness-pi/${VERSION}`;
23
26
  export type PiHarnessSettings = {
24
27
  /** Where Pi sources API keys / gateway credentials from. */
25
28
  readonly auth?: PiAuthenticationMode;
29
+ /**
30
+ * Explicit Pi provider configurations keyed by provider id. Use this to
31
+ * register custom models and their API protocol without coupling model
32
+ * metadata to authentication environment variables.
33
+ */
34
+ readonly providers?: Readonly<Record<string, ProviderConfig>>;
26
35
  /**
27
36
  * Pi's extended-thinking budget level. Maps directly to the SDK's
28
37
  * `thinkingLevel` option on `createAgentSession`.
@@ -30,9 +39,9 @@ export type PiHarnessSettings = {
30
39
  readonly thinkingLevel?: PiThinkingLevel;
31
40
  /**
32
41
  * Directory holding Pi's global agent config (auth.json, models.json,
33
- * settings.json). When omitted, a per-session temp dir is used. Pass the
34
- * user's agent dir (e.g. `~/.pi/agent/`) to reuse their CLI auth and
35
- * model settings.
42
+ * settings.json). When omitted, native subscription auth is discovered from
43
+ * Pi's default agent directory while model and general settings remain
44
+ * isolated per session.
36
45
  */
37
46
  readonly agentDir?: string;
38
47
  /**
@@ -149,6 +158,7 @@ export function createPi(
149
158
  ? { thinkingLevel: settings.thinkingLevel }
150
159
  : {}),
151
160
  ...(settings.mcpServers ? { mcpServers: settings.mcpServers } : {}),
161
+ ...(settings.providers ? { providers: settings.providers } : {}),
152
162
  ...(settings.extensionFactories
153
163
  ? { extensionFactories: settings.extensionFactories }
154
164
  : {}),
@@ -77,13 +77,15 @@ export function createPiModelResolver({
77
77
  if (gatewayMatch) return gatewayMatch;
78
78
 
79
79
  const scopedMatch = findScopedMatch(effectiveId, models);
80
- if (scopedMatch && !modelRegistry.hasConfiguredAuth(scopedMatch)) {
81
- const authenticatedFlatMatches = models.filter(
82
- m => m.id === effectiveId && modelRegistry.hasConfiguredAuth(m),
83
- );
84
- if (authenticatedFlatMatches.length === 1) {
85
- return authenticatedFlatMatches[0];
86
- }
80
+ if (scopedMatch && modelRegistry.hasConfiguredAuth(scopedMatch)) {
81
+ return scopedMatch;
82
+ }
83
+
84
+ const authenticatedFlatMatches = models.filter(
85
+ m => matches(m) && modelRegistry.hasConfiguredAuth(m),
86
+ );
87
+ if (authenticatedFlatMatches.length === 1) {
88
+ return authenticatedFlatMatches[0];
87
89
  }
88
90
 
89
91
  return scopedMatch ?? models.find(matches);
package/src/pi-session.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  type AgentSession,
9
9
  type AgentToolResult,
10
10
  type ExtensionFactory,
11
+ type ProviderConfig,
11
12
  type Skill,
12
13
  type ToolDefinition,
13
14
  } from '@earendil-works/pi-coding-agent';
@@ -43,6 +44,7 @@ import {
43
44
  resolvePiEnv,
44
45
  type PiAuthenticationMode,
45
46
  } from './pi-auth';
47
+ import { resolvePiSubscriptionAgentDir } from './pi-subscription';
46
48
  import { getPiTerminalError, parseNativeEvent } from './pi-events';
47
49
  import { createPiModelResolver } from './pi-model-resolver';
48
50
  import { createPiPathMapper } from './pi-paths';
@@ -222,6 +224,7 @@ export interface PiSessionSettings {
222
224
  readonly headers?: Readonly<Record<string, string>>;
223
225
  readonly thinkingLevel?: PiThinkingLevel;
224
226
  readonly mcpServers?: Record<string, unknown>;
227
+ readonly providers?: Readonly<Record<string, ProviderConfig>>;
225
228
  readonly extensionFactories?: ReadonlyArray<ExtensionFactory>;
226
229
  }
227
230
 
@@ -238,9 +241,9 @@ export interface CreatePiSessionInput {
238
241
  readonly abortSignal?: AbortSignal;
239
242
  /**
240
243
  * Directory holding Pi's global agent config (auth.json, models.json,
241
- * settings.json). When omitted, a per-session temp dir is used (the
242
- * harness cannot reuse existing CLI logins). Pass the user's agent dir
243
- * (e.g. `~/.pi/agent/`) to reuse their CLI auth and model settings.
244
+ * settings.json). Native auth from this directory is considered after
245
+ * applicable environment credentials. Model and general settings are only
246
+ * reused when this option is explicit.
244
247
  */
245
248
  readonly agentDir?: string;
246
249
  }
@@ -428,9 +431,14 @@ export async function createPiSession(
428
431
  * outside that record. General Pi settings still use agentDir below.
429
432
  */
430
433
  const agentDir = input.agentDir ?? hostAgentDir;
434
+ const nativeAgentDir = resolvePiSubscriptionAgentDir({
435
+ options: input.settings.auth,
436
+ env: process.env,
437
+ agentDir: input.agentDir,
438
+ });
431
439
  const modelRuntime = await createPiModelRuntime({
432
440
  auth: input.settings.auth,
433
- authPath: path.join(agentDir, 'auth.json'),
441
+ authPath: path.join(nativeAgentDir ?? hostAgentDir, 'auth.json'),
434
442
  modelsPath: path.join(agentDir, 'models.json'),
435
443
  });
436
444
  const modelRegistry = new ModelRegistry(modelRuntime);
@@ -454,6 +462,14 @@ export async function createPiSession(
454
462
  clientApp: input.clientApp,
455
463
  headers: input.settings.headers,
456
464
  });
465
+ for (const [provider, config] of Object.entries(
466
+ input.settings.providers ?? {},
467
+ )) {
468
+ modelRegistry.registerProvider(provider, {
469
+ ...modelRegistry.getRegisteredProviderConfig(provider),
470
+ ...config,
471
+ });
472
+ }
457
473
  const resolveModel = createPiModelResolver({
458
474
  modelRegistry,
459
475
  env: resolverEnv,
@@ -1468,13 +1484,21 @@ export async function createPiSession(
1468
1484
  if (stopped) {
1469
1485
  throw new Error('Pi session has been stopped.');
1470
1486
  }
1487
+ if (piSession == null) {
1488
+ await rebuildPiSession([], true);
1489
+ lastToolsSignature = JSON.stringify([]);
1490
+ }
1491
+ const session = piSession;
1492
+ if (session == null) {
1493
+ throw new Error('Pi session failed to initialize.');
1494
+ }
1471
1495
  /*
1472
1496
  * Pi owns the compaction. We just request it; the resulting
1473
1497
  * `compaction_end` event is observed by the session subscription and
1474
1498
  * translated into a `compaction` stream part. The returned
1475
1499
  * `CompactionResult` is intentionally discarded here.
1476
1500
  */
1477
- await piSession?.compact(customInstructions);
1501
+ await session.compact(customInstructions);
1478
1502
  },
1479
1503
 
1480
1504
  doDestroy: async () => {
@@ -0,0 +1,27 @@
1
+ import { homedir } from 'node:os';
2
+ import { resolve } from 'node:path';
3
+ import { isHarnessAuthenticationEnvironment } from '@ai-sdk/harness/utils';
4
+ import { resolvePiEnv, type PiAuthenticationMode } from './pi-auth';
5
+
6
+ export function resolvePiSubscriptionAgentDir({
7
+ options,
8
+ env,
9
+ agentDir,
10
+ homeDirectory = homedir(),
11
+ }: {
12
+ options: PiAuthenticationMode | undefined;
13
+ env: NodeJS.ProcessEnv;
14
+ agentDir?: string;
15
+ homeDirectory?: string;
16
+ }): string | undefined {
17
+ if (isHarnessAuthenticationEnvironment(options) || options === 'ai-gateway') {
18
+ return undefined;
19
+ }
20
+ const resolvedEnvironment = resolvePiEnv({ options, env });
21
+ if (resolvedEnvironment.AI_GATEWAY_API_KEY != null) {
22
+ return undefined;
23
+ }
24
+ return resolve(
25
+ agentDir ?? env.PI_CODING_AGENT_DIR ?? `${homeDirectory}/.pi/agent`,
26
+ );
27
+ }
@@ -1,7 +1,11 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import type { HarnessV1StreamPart } from '@ai-sdk/harness';
3
3
  import { secureJsonParse } from '@ai-sdk/provider-utils';
4
- import { extractAssistantText, type PiSessionEvent } from './pi-events';
4
+ import {
5
+ extractAssistantText,
6
+ getPiTerminalError,
7
+ type PiSessionEvent,
8
+ } from './pi-events';
5
9
  import { serializeToolOutput } from './pi-utils';
6
10
 
7
11
  /**
@@ -411,7 +415,9 @@ export function translatePiEvent(
411
415
  } else {
412
416
  state.pendingStepToolCallIds.clear();
413
417
  state.stepToolCallCount = undefined;
414
- parts.push(...finishStep(state));
418
+ if (!getPiTerminalError(event)) {
419
+ parts.push(...finishStep(state));
420
+ }
415
421
  }
416
422
  return parts;
417
423
  }