@ai-sdk/harness-pi 1.0.120 → 1.0.121

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.120",
3
+ "version": "1.0.121",
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.118",
29
+ "@ai-sdk/harness": "1.0.119",
30
30
  "@ai-sdk/provider-utils": "5.0.45",
31
31
  "@earendil-works/pi-ai": "0.74.2",
32
- "@earendil-works/pi-coding-agent": "^0.84.3",
32
+ "@earendil-works/pi-coding-agent": "^0.85.1",
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.118",
40
+ "@ai-sdk/sandbox-just-bash": "1.0.119",
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 } from './pi-auth';
12
+ export type { PiAuthenticationMode, PiCredentialStore } from './pi-auth';
package/src/pi-auth.ts CHANGED
@@ -12,7 +12,10 @@ import { access } from 'node:fs/promises';
12
12
  import { VERSION } from './version';
13
13
 
14
14
  type ProviderConfigInput = Parameters<ModelRegistry['registerProvider']>[1];
15
- type PiCredentialStore = NonNullable<CreateModelRuntimeOptions['credentials']>;
15
+ /** Persistent credential storage consumed by Pi's model runtime. */
16
+ export type PiCredentialStore = NonNullable<
17
+ CreateModelRuntimeOptions['credentials']
18
+ >;
16
19
  type PiCredential = Exclude<
17
20
  Awaited<ReturnType<PiCredentialStore['read']>>,
18
21
  undefined
@@ -116,24 +119,27 @@ function scopePiProviderEnvironment({
116
119
 
117
120
  export async function createPiModelRuntime({
118
121
  auth,
122
+ credentials,
119
123
  authPath,
120
124
  modelsPath,
121
125
  }: {
122
126
  auth: PiAuthenticationMode | undefined;
127
+ credentials?: PiCredentialStore;
123
128
  authPath: string;
124
129
  modelsPath: string;
125
130
  }): Promise<ModelRuntime> {
126
131
  if (!isHarnessAuthenticationEnvironment(auth)) {
127
132
  return ModelRuntime.create({
128
- authPath,
133
+ ...(credentials ? { credentials } : { authPath }),
129
134
  modelsPath,
130
135
  allowModelNetwork: false,
131
136
  });
132
137
  }
133
138
 
134
- const isolatedCredentials = createIsolatedPiCredentialStore();
139
+ const isolatedCredentials =
140
+ credentials == null ? createIsolatedPiCredentialStore() : undefined;
135
141
  const modelRuntime = await ModelRuntime.create({
136
- credentials: isolatedCredentials.credentials,
142
+ credentials: credentials ?? isolatedCredentials!.credentials,
137
143
  modelsPath: null,
138
144
  allowModelNetwork: false,
139
145
  });
@@ -164,7 +170,7 @@ export async function createPiModelRuntime({
164
170
  modelRuntime,
165
171
  authenticationEnvironment: auth,
166
172
  });
167
- isolatedCredentials.finishInitialization();
173
+ isolatedCredentials?.finishInitialization();
168
174
  await modelRuntime.refresh({ allowNetwork: false });
169
175
 
170
176
  return modelRuntime;
package/src/pi-harness.ts CHANGED
@@ -9,7 +9,7 @@ import type {
9
9
  ProviderConfig,
10
10
  } from '@earendil-works/pi-coding-agent';
11
11
  import { z } from 'zod/v4';
12
- import type { PiAuthenticationMode } from './pi-auth';
12
+ import type { PiAuthenticationMode, PiCredentialStore } from './pi-auth';
13
13
  import { piResumeStateSchema } from './pi-resume-state';
14
14
  import { createPiSession, type PiThinkingLevel } from './pi-session';
15
15
  import { VERSION } from './version';
@@ -26,6 +26,19 @@ const PI_CLIENT_APP = `ai-sdk/harness-pi/${VERSION}`;
26
26
  export type PiHarnessSettings = {
27
27
  /** Where Pi sources API keys / gateway credentials from. */
28
28
  readonly auth?: PiAuthenticationMode;
29
+ /**
30
+ * Application-owned credential storage for Pi's model runtime. When set,
31
+ * this replaces Pi's file-backed auth.json credential storage.
32
+ */
33
+ readonly credentials?: PiCredentialStore;
34
+ /**
35
+ * Whether a suspended turn may reuse its live Pi session in this process.
36
+ * Disable this in stateless or multi-replica applications so every request
37
+ * restores from persisted lifecycle state with the current settings.
38
+ *
39
+ * @default true
40
+ */
41
+ readonly reattachInProcess?: boolean;
29
42
  /**
30
43
  * Explicit Pi provider configurations keyed by provider id. Use this to
31
44
  * register custom models and their API protocol without coupling model
@@ -154,6 +167,12 @@ export function createPi(
154
167
  sessionWorkDir: startOpts.sessionWorkDir,
155
168
  settings: {
156
169
  ...(settings.auth ? { auth: settings.auth } : {}),
170
+ ...(settings.credentials
171
+ ? { credentials: settings.credentials }
172
+ : {}),
173
+ ...(settings.reattachInProcess != null
174
+ ? { reattachInProcess: settings.reattachInProcess }
175
+ : {}),
157
176
  ...(settings.thinkingLevel
158
177
  ? { thinkingLevel: settings.thinkingLevel }
159
178
  : {}),
@@ -166,6 +185,7 @@ export function createPi(
166
185
  },
167
186
  clientApp: PI_CLIENT_APP,
168
187
  isResume: lifecycleState != null,
188
+ ...(lifecycleState ? { resumeStateType: lifecycleState.type } : {}),
169
189
  permissionMode: startOpts.permissionMode,
170
190
  builtinToolFiltering: startOpts.builtinToolFiltering,
171
191
  ...(resumeData?.sessionFileName
package/src/pi-session.ts CHANGED
@@ -43,6 +43,7 @@ import {
43
43
  registerPiProviders,
44
44
  resolvePiEnv,
45
45
  type PiAuthenticationMode,
46
+ type PiCredentialStore,
46
47
  } from './pi-auth';
47
48
  import { resolvePiSubscriptionAgentDir } from './pi-subscription';
48
49
  import { getPiTerminalError, parseNativeEvent } from './pi-events';
@@ -100,7 +101,14 @@ type PiMcpAdapterModule = {
100
101
  * same-process resume instead of stopping it and resolving the promise as an
101
102
  * error. Cross-process resume still falls back to the persisted session file.
102
103
  */
103
- const parkedPiSessions = new Map<string, HarnessV1Session>();
104
+ const parkedPiSessions = new Map<
105
+ string,
106
+ {
107
+ session: HarnessV1Session;
108
+ input: CreatePiSessionInput;
109
+ stateType: 'continue-turn' | 'resume-session';
110
+ }
111
+ >();
104
112
 
105
113
  /**
106
114
  * Whether a discovered resource path belongs to a specific directory.
@@ -221,6 +229,8 @@ export type PiThinkingLevel =
221
229
 
222
230
  export interface PiSessionSettings {
223
231
  readonly auth?: PiAuthenticationMode;
232
+ readonly credentials?: PiCredentialStore;
233
+ readonly reattachInProcess?: boolean;
224
234
  readonly headers?: Readonly<Record<string, string>>;
225
235
  readonly thinkingLevel?: PiThinkingLevel;
226
236
  readonly mcpServers?: Record<string, unknown>;
@@ -235,6 +245,9 @@ export interface CreatePiSessionInput {
235
245
  readonly settings: PiSessionSettings;
236
246
  readonly clientApp: string;
237
247
  readonly isResume: boolean;
248
+ readonly resumeStateType?:
249
+ | HarnessV1ContinueTurnState['type']
250
+ | HarnessV1ResumeSessionState['type'];
238
251
  readonly permissionMode?: HarnessV1PermissionMode;
239
252
  readonly builtinToolFiltering?: HarnessV1BuiltinToolFiltering;
240
253
  readonly resumeSessionFileName?: string;
@@ -248,6 +261,28 @@ export interface CreatePiSessionInput {
248
261
  readonly agentDir?: string;
249
262
  }
250
263
 
264
+ function hasCompatibleReattachSettings(
265
+ parked: CreatePiSessionInput,
266
+ current: CreatePiSessionInput,
267
+ ): boolean {
268
+ return (
269
+ parked.sandboxSession === current.sandboxSession &&
270
+ parked.sessionWorkDir === current.sessionWorkDir &&
271
+ parked.clientApp === current.clientApp &&
272
+ parked.permissionMode === current.permissionMode &&
273
+ parked.builtinToolFiltering === current.builtinToolFiltering &&
274
+ parked.abortSignal === current.abortSignal &&
275
+ parked.agentDir === current.agentDir &&
276
+ parked.settings.auth === current.settings.auth &&
277
+ parked.settings.credentials === current.settings.credentials &&
278
+ parked.settings.headers === current.settings.headers &&
279
+ parked.settings.thinkingLevel === current.settings.thinkingLevel &&
280
+ parked.settings.mcpServers === current.settings.mcpServers &&
281
+ parked.settings.providers === current.settings.providers &&
282
+ parked.settings.extensionFactories === current.settings.extensionFactories
283
+ );
284
+ }
285
+
251
286
  interface PendingToolResult {
252
287
  resolve: (value: unknown) => void;
253
288
  }
@@ -319,13 +354,27 @@ export async function createPiSession(
319
354
  input: CreatePiSessionInput,
320
355
  ): Promise<HarnessV1Session> {
321
356
  if (input.isResume) {
322
- const parkedSession = parkedPiSessions.get(input.sessionId);
323
- if (parkedSession) {
357
+ const parked = parkedPiSessions.get(input.sessionId);
358
+ if (parked) {
324
359
  parkedPiSessions.delete(input.sessionId);
325
- return {
326
- ...parkedSession,
327
- isResume: true,
328
- };
360
+ if (
361
+ input.settings.reattachInProcess !== false &&
362
+ (input.resumeStateType == null ||
363
+ input.resumeStateType === parked.stateType) &&
364
+ hasCompatibleReattachSettings(parked.input, input)
365
+ ) {
366
+ return {
367
+ ...parked.session,
368
+ isResume: true,
369
+ };
370
+ }
371
+
372
+ // The caller explicitly requested cold restoration, supplied settings
373
+ // that cannot safely reuse the live runtime, or supplied a resume-session
374
+ // state that represents progress beyond the parked continue-turn. Retire
375
+ // the stale live session before rebuilding from the persisted journal
376
+ // with this request's runtime settings.
377
+ await parked.session.doDestroy();
329
378
  }
330
379
  }
331
380
 
@@ -438,6 +487,7 @@ export async function createPiSession(
438
487
  });
439
488
  const modelRuntime = await createPiModelRuntime({
440
489
  auth: input.settings.auth,
490
+ credentials: input.settings.credentials,
441
491
  authPath: path.join(nativeAgentDir ?? hostAgentDir, 'auth.json'),
442
492
  modelsPath: path.join(agentDir, 'models.json'),
443
493
  });
@@ -1521,7 +1571,11 @@ export async function createPiSession(
1521
1571
 
1522
1572
  doDetach: async (): Promise<HarnessV1ResumeSessionState> => {
1523
1573
  if (activeTurn != null || pendingToolResults.size > 0) {
1524
- parkedPiSessions.set(input.sessionId, sessionImpl);
1574
+ parkedPiSessions.set(input.sessionId, {
1575
+ session: sessionImpl,
1576
+ input,
1577
+ stateType: 'resume-session',
1578
+ });
1525
1579
  if (sessionFileName) {
1526
1580
  try {
1527
1581
  await persistSessionFile();
@@ -1551,7 +1605,11 @@ export async function createPiSession(
1551
1605
  activeTurn != null &&
1552
1606
  (pendingToolResults.size > 0 || pendingToolApprovals.size > 0)
1553
1607
  ) {
1554
- parkedPiSessions.set(input.sessionId, sessionImpl);
1608
+ parkedPiSessions.set(input.sessionId, {
1609
+ session: sessionImpl,
1610
+ input,
1611
+ stateType: 'continue-turn',
1612
+ });
1555
1613
  if (sessionFileName) {
1556
1614
  try {
1557
1615
  await persistSessionFile();