@ai-sdk/harness-pi 1.0.65 → 1.0.67

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.65",
3
+ "version": "1.0.67",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -30,8 +30,8 @@
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/provider-utils": "5.0.26",
34
- "@ai-sdk/harness": "1.0.65"
33
+ "@ai-sdk/harness": "1.0.66",
34
+ "@ai-sdk/provider-utils": "5.0.26"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "zod": "^3.25.76 || ^4.1.8"
@@ -11,6 +11,30 @@ type PiModel = ReturnType<ModelRegistry['getAll']>[number];
11
11
  */
12
12
  export const DEFAULT_PI_GATEWAY_MODEL_ID = 'anthropic/claude-sonnet-4.6';
13
13
 
14
+ /*
15
+ * A `"<provider>/<id>"` reference normally resolves under that provider, not
16
+ * any entry whose flat `id` happens to equal the whole string. Routing proxies
17
+ * (`vercel-ai-gateway`, `openrouter`) can carry another provider's id verbatim
18
+ * as their own flat `id` (e.g. `id: "xai/grok-4.3"`), so this tier normally
19
+ * wins over flat matching. An authenticated literal-id match remains usable
20
+ * when the scoped provider is unauthenticated, matching Pi's own resolver.
21
+ * Gateway preference stays first because gateway credentials explicitly opt
22
+ * the harness into routing matching models through the gateway.
23
+ */
24
+ const findScopedMatch = (
25
+ effectiveId: string,
26
+ models: PiModel[],
27
+ ): PiModel | undefined => {
28
+ const slashIndex = effectiveId.indexOf('/');
29
+ if (slashIndex === -1) return undefined;
30
+
31
+ const prefix = effectiveId.slice(0, slashIndex);
32
+ const bareId = effectiveId.slice(slashIndex + 1);
33
+ return models.find(
34
+ m => m.provider === prefix && (m.id === bareId || m.name === bareId),
35
+ );
36
+ };
37
+
14
38
  export function createPiModelResolver({
15
39
  modelRegistry,
16
40
  env = process.env,
@@ -47,10 +71,21 @@ export function createPiModelResolver({
47
71
  // (e.g. `anthropic/claude-sonnet-4.6` exists under both `openrouter` and
48
72
  // `vercel-ai-gateway`); without this preference Pi would dispatch through
49
73
  // a provider we didn't register, which fails with "No API key found".
50
- return (
51
- (useGateway &&
52
- models.find(m => m.provider === 'vercel-ai-gateway' && matches(m))) ||
53
- models.find(matches)
54
- );
74
+ const gatewayMatch = useGateway
75
+ ? models.find(m => m.provider === 'vercel-ai-gateway' && matches(m))
76
+ : undefined;
77
+ if (gatewayMatch) return gatewayMatch;
78
+
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
+ }
87
+ }
88
+
89
+ return scopedMatch ?? models.find(matches);
55
90
  };
56
91
  }
package/src/pi-session.ts CHANGED
@@ -55,7 +55,6 @@ import {
55
55
  import { toolSpecToTypeBoxParameters } from './pi-typebox-adapter';
56
56
  import {
57
57
  extractUserText,
58
- frameInstructions,
59
58
  safePiMetadataSegment,
60
59
  serializeToolOutput,
61
60
  } from './pi-utils';
@@ -395,6 +394,8 @@ export async function createPiSession(
395
394
  });
396
395
  const hasMcpServers = Object.keys(mcpServers).length > 0;
397
396
 
397
+ let sessionInstructions: string | undefined;
398
+
398
399
  /*
399
400
  * Configured MCP servers are served by an inline Pi extension, so they share
400
401
  * the extension runtime with the caller-supplied factories: both are loaded
@@ -426,11 +427,13 @@ export async function createPiSession(
426
427
  let currentExtensionsResult:
427
428
  | ReturnType<DefaultResourceLoader['getExtensions']>
428
429
  | undefined;
430
+
429
431
  const resourceLoader = new DefaultResourceLoader({
430
432
  cwd: sessionWorkDir,
431
433
  agentDir: hostAgentDir,
432
434
  settingsManager,
433
- appendSystemPromptOverride: () => [],
435
+ appendSystemPromptOverride: () =>
436
+ sessionInstructions ? [sessionInstructions] : [],
434
437
  extensionFactories,
435
438
  ...(hasExtensionFactories
436
439
  ? {
@@ -498,12 +501,6 @@ export async function createPiSession(
498
501
  * from the persisted journal.
499
502
  */
500
503
  let suspending = false;
501
- /*
502
- * Instructions are prepended to the first user message of a fresh session
503
- * only. A resumed session already carried them in its original first
504
- * message (preserved in the persisted session file), so it starts "applied".
505
- */
506
- let instructionsApplied = input.isResume;
507
504
  const pendingToolResults = new Map<string, PendingToolResult>();
508
505
  const pendingToolApprovals = new Map<string, PendingToolApproval>();
509
506
 
@@ -521,6 +518,15 @@ export async function createPiSession(
521
518
  */
522
519
  const pendingCompactionParts: HarnessV1StreamPart[] = [];
523
520
 
521
+ async function applySessionInstructions(
522
+ instructions: string | undefined,
523
+ ): Promise<void> {
524
+ if (instructions === sessionInstructions) return;
525
+ sessionInstructions = instructions;
526
+ await reloadResourcesOnly();
527
+ piSession?.setActiveToolsByName(piSession.getActiveToolNames());
528
+ }
529
+
524
530
  const remoteOps = createPiRemoteOps({
525
531
  sandbox,
526
532
  paths,
@@ -939,14 +945,10 @@ export async function createPiSession(
939
945
  doPromptTurn: async (
940
946
  promptOpts: HarnessV1PromptTurnOptions,
941
947
  ): Promise<HarnessV1PromptControl> => {
942
- let text = extractUserText(promptOpts.prompt);
943
- if (!instructionsApplied && promptOpts.instructions) {
944
- text = frameInstructions(promptOpts.instructions, text);
945
- }
946
- instructionsApplied = true;
948
+ await applySessionInstructions(promptOpts.instructions);
947
949
 
948
950
  return runTurn({
949
- text,
951
+ text: extractUserText(promptOpts.prompt),
950
952
  tools: promptOpts.tools ?? [],
951
953
  emit: promptOpts.emit,
952
954
  abortSignal: promptOpts.abortSignal,
@@ -972,6 +974,7 @@ export async function createPiSession(
972
974
  * flight at the slice boundary is recomputed because a host-resident
973
975
  * runtime cannot do a lossless attach.
974
976
  */
977
+ await applySessionInstructions(continueOpts.instructions);
975
978
  return runTurn({
976
979
  text: '',
977
980
  tools: continueOpts.tools ?? [],