@ai-sdk/harness-pi 1.0.64 → 1.0.66

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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @ai-sdk/harness-pi
2
2
 
3
+ ## 1.0.66
4
+
5
+ ### Patch Changes
6
+
7
+ - b6642fa: fix(harness-pi): resolve compound provider/id model ids under their scoped provider
8
+
9
+ ## 1.0.65
10
+
11
+ ### Patch Changes
12
+
13
+ - c20a315: feat(harness-pi): support caller-supplied inline Pi extension factories
14
+ - a03ff6c: feat(harness): add support for per-harness MCP servers
15
+ - Updated dependencies [401a4ba]
16
+ - Updated dependencies [a03ff6c]
17
+ - @ai-sdk/provider-utils@5.0.26
18
+ - @ai-sdk/harness@1.0.65
19
+
3
20
  ## 1.0.64
4
21
 
5
22
  ### Patch Changes
package/README.md CHANGED
@@ -50,3 +50,23 @@ try {
50
50
  ```
51
51
 
52
52
  The adapter requires a `HarnessV1SandboxProvider`. Pi has no in-sandbox bridge, so the sandbox doesn't need to expose any ports — `@ai-sdk/sandbox-vercel` or `@ai-sdk/sandbox-just-bash` both work.
53
+
54
+ ## Inline extensions
55
+
56
+ Use `extensionFactories` to load trusted inline Pi extensions for each harness session:
57
+
58
+ ```ts
59
+ import { createPi } from '@ai-sdk/harness-pi';
60
+
61
+ const harness = createPi({
62
+ extensionFactories: [
63
+ pi => {
64
+ pi.on('agent_start', () => {
65
+ console.log('Pi agent started');
66
+ });
67
+ },
68
+ ],
69
+ });
70
+ ```
71
+
72
+ Routine resource refreshes between turns do not reinitialize extension factories. If the underlying Pi session is rebuilt, factories initialize for the new Pi runtime. Extension factories execute in the host Node.js process, so only pass factories you trust. This option does not enable filesystem extension discovery: user, project, personal, and settings-based Pi extensions remain disabled. Themes and prompt templates also remain disabled.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as _ai_sdk_harness from '@ai-sdk/harness';
2
2
  import { HarnessV1, HarnessV1BuiltinTool } from '@ai-sdk/harness';
3
+ import { ExtensionFactory } from '@earendil-works/pi-coding-agent';
3
4
 
4
5
  /**
5
6
  * Pi auth options. Exactly one of `gateway` or `customEnv` is honoured
@@ -52,6 +53,17 @@ type PiHarnessSettings = {
52
53
  * model settings.
53
54
  */
54
55
  readonly agentDir?: string;
56
+ /**
57
+ * MCP server definitions keyed by server name. Each definition uses the
58
+ * underlying runtime's native MCP server configuration format.
59
+ */
60
+ readonly mcpServers?: Record<string, unknown>;
61
+ /**
62
+ * Trusted inline Pi extensions loaded for each harness session.
63
+ *
64
+ * Filesystem-discovered user and project extensions remain disabled.
65
+ */
66
+ readonly extensionFactories?: ReadonlyArray<ExtensionFactory>;
55
67
  };
56
68
  declare const PI_BUILTIN_TOOLS: {
57
69
  readonly read: HarnessV1BuiltinTool<{
package/dist/index.js CHANGED
@@ -108,7 +108,7 @@ import { resolveSandboxHomeDir } from "@ai-sdk/harness/utils";
108
108
  import { getAiGatewayAuthFromEnv } from "@ai-sdk/harness/utils";
109
109
 
110
110
  // src/version.ts
111
- var VERSION = true ? "1.0.64" : "0.0.0-test";
111
+ var VERSION = true ? "1.0.66" : "0.0.0-test";
112
112
 
113
113
  // src/pi-auth.ts
114
114
  var DEFAULT_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";
@@ -368,6 +368,15 @@ function extractAssistantText(message) {
368
368
  // src/pi-model-resolver.ts
369
369
  import { getAiGatewayAuthFromEnv as getAiGatewayAuthFromEnv2 } from "@ai-sdk/harness/utils";
370
370
  var DEFAULT_PI_GATEWAY_MODEL_ID = "anthropic/claude-sonnet-4.6";
371
+ var findScopedMatch = (effectiveId, models) => {
372
+ const slashIndex = effectiveId.indexOf("/");
373
+ if (slashIndex === -1) return void 0;
374
+ const prefix = effectiveId.slice(0, slashIndex);
375
+ const bareId = effectiveId.slice(slashIndex + 1);
376
+ return models.find(
377
+ (m) => m.provider === prefix && (m.id === bareId || m.name === bareId)
378
+ );
379
+ };
371
380
  function createPiModelResolver({
372
381
  modelRegistry,
373
382
  env = process.env
@@ -390,7 +399,18 @@ function createPiModelResolver({
390
399
  if (!effectiveId) return void 0;
391
400
  const models = loadModels();
392
401
  const matches = (m) => m.id === effectiveId || m.name === effectiveId;
393
- return useGateway && models.find((m) => m.provider === "vercel-ai-gateway" && matches(m)) || models.find(matches);
402
+ const gatewayMatch = useGateway ? models.find((m) => m.provider === "vercel-ai-gateway" && matches(m)) : void 0;
403
+ if (gatewayMatch) return gatewayMatch;
404
+ const scopedMatch = findScopedMatch(effectiveId, models);
405
+ if (scopedMatch && !modelRegistry.hasConfiguredAuth(scopedMatch)) {
406
+ const authenticatedFlatMatches = models.filter(
407
+ (m) => m.id === effectiveId && modelRegistry.hasConfiguredAuth(m)
408
+ );
409
+ if (authenticatedFlatMatches.length === 1) {
410
+ return authenticatedFlatMatches[0];
411
+ }
412
+ }
413
+ return scopedMatch ?? models.find(matches);
394
414
  };
395
415
  }
396
416
 
@@ -740,6 +760,7 @@ async function writePiSkills(args) {
740
760
 
741
761
  // src/pi-translate.ts
742
762
  import { randomBytes } from "crypto";
763
+ import { secureJsonParse } from "@ai-sdk/provider-utils";
743
764
 
744
765
  // src/pi-utils.ts
745
766
  import {
@@ -804,7 +825,9 @@ function createPiTranslatorState(options = {}) {
804
825
  pendingStepToolCallIds: /* @__PURE__ */ new Set(),
805
826
  stepOpen: false,
806
827
  hostToolResults: /* @__PURE__ */ new Map(),
828
+ dynamicToolCallIds: /* @__PURE__ */ new Set(),
807
829
  builtinToolNames: new Set(options.builtinToolNames ?? []),
830
+ hostToolNames: new Set(options.hostToolNames ?? []),
808
831
  nativeToCommonNameMap: map
809
832
  };
810
833
  }
@@ -830,6 +853,14 @@ function unwrapPiToolResult(event) {
830
853
  if (typeof event.content === "string") return event.content;
831
854
  return event.result ?? event.content ?? null;
832
855
  }
856
+ function parseMcpToolResult(content) {
857
+ if (typeof content !== "string") return content;
858
+ try {
859
+ return secureJsonParse(content);
860
+ } catch {
861
+ return content;
862
+ }
863
+ }
833
864
  function resolveToolName(state, nativeName) {
834
865
  const common = state.nativeToCommonNameMap.get(nativeName);
835
866
  return { wire: common ?? nativeName, native: nativeName };
@@ -969,7 +1000,9 @@ function translatePiEvent(event, state) {
969
1000
  if (!event.toolCallId || !event.toolName) return [];
970
1001
  const { wire, native } = resolveToolName(state, event.toolName);
971
1002
  state.observedToolNames.set(event.toolCallId, wire);
972
- const providerExecuted = state.builtinToolNames.has(native);
1003
+ const isMcpTool = !state.hostToolNames.has(native) && (native === "mcp" || native.startsWith("mcp__"));
1004
+ const providerExecuted = state.builtinToolNames.has(native) || isMcpTool;
1005
+ if (isMcpTool) state.dynamicToolCallIds.add(event.toolCallId);
973
1006
  const input = serializeToolOutput(event.args ?? event.input ?? {});
974
1007
  return [
975
1008
  {
@@ -978,7 +1011,8 @@ function translatePiEvent(event, state) {
978
1011
  toolName: wire,
979
1012
  input,
980
1013
  ...wire !== native ? { nativeName: native } : {},
981
- ...providerExecuted ? { providerExecuted: true } : {}
1014
+ ...providerExecuted ? { providerExecuted: true } : {},
1015
+ ...isMcpTool ? { dynamic: true } : {}
982
1016
  }
983
1017
  ];
984
1018
  }
@@ -989,7 +1023,8 @@ function translatePiEvent(event, state) {
989
1023
  const nativeName = event.toolName;
990
1024
  const wire = recordedName ?? (nativeName ? resolveToolName(state, nativeName).wire : void 0);
991
1025
  if (!wire) return [];
992
- const result = state.hostToolResults.has(event.toolCallId) ? state.hostToolResults.get(event.toolCallId) ?? null : unwrapPiToolResult(event);
1026
+ const dynamic = state.dynamicToolCallIds.delete(event.toolCallId);
1027
+ const result = state.hostToolResults.has(event.toolCallId) ? state.hostToolResults.get(event.toolCallId) ?? null : dynamic ? parseMcpToolResult(unwrapPiToolResult(event)) : unwrapPiToolResult(event);
993
1028
  state.hostToolResults.delete(event.toolCallId);
994
1029
  state.pendingStepToolCallIds.delete(event.toolCallId);
995
1030
  return [
@@ -998,7 +1033,8 @@ function translatePiEvent(event, state) {
998
1033
  toolCallId: event.toolCallId,
999
1034
  toolName: wire,
1000
1035
  result,
1001
- ...event.isError ? { isError: true } : {}
1036
+ ...event.isError ? { isError: true } : {},
1037
+ ...dynamic ? { dynamic: true } : {}
1002
1038
  },
1003
1039
  ...finishStep(state)
1004
1040
  ];
@@ -1505,6 +1541,7 @@ async function syncHostWorkspaceFromSandbox(args) {
1505
1541
 
1506
1542
  // src/pi-session.ts
1507
1543
  var HARNESS_ID2 = "pi";
1544
+ var PI_MCP_ADAPTER_PACKAGE = "pi-mcp-adapter";
1508
1545
  var parkedPiSessions = /* @__PURE__ */ new Map();
1509
1546
  function isWithinDirectory(parent, child) {
1510
1547
  const rel = path7.relative(parent, child);
@@ -1670,18 +1707,56 @@ async function createPiSession(input) {
1670
1707
  env: resolverEnv
1671
1708
  });
1672
1709
  const resolvedModel = resolveModel(input.settings.model);
1710
+ const mcpServers = resolvePiMcpServers({
1711
+ mcpServers: input.settings.mcpServers
1712
+ });
1713
+ const hasMcpServers = Object.keys(mcpServers).length > 0;
1714
+ const extensionFactories = [
1715
+ ...input.settings.extensionFactories ?? []
1716
+ ];
1717
+ if (hasMcpServers) {
1718
+ const { createMcpAdapter } = await import(PI_MCP_ADAPTER_PACKAGE);
1719
+ extensionFactories.push(
1720
+ createMcpAdapter({
1721
+ config: {
1722
+ mcpServers,
1723
+ settings: {
1724
+ directTools: true,
1725
+ toolPrefix: "mcp",
1726
+ disableProxyTool: true
1727
+ }
1728
+ }
1729
+ })
1730
+ );
1731
+ }
1732
+ const hasExtensionFactories = extensionFactories.length > 0;
1733
+ let preserveExtensionsResult = false;
1734
+ let currentExtensionsResult;
1673
1735
  const resourceLoader = new DefaultResourceLoader({
1674
1736
  cwd: sessionWorkDir,
1675
1737
  agentDir: hostAgentDir,
1676
1738
  settingsManager,
1677
1739
  appendSystemPromptOverride: () => [],
1678
- extensionFactories: [],
1740
+ extensionFactories,
1741
+ ...hasExtensionFactories ? {
1742
+ // DefaultResourceLoader invokes inline factories on every reload.
1743
+ // Resource-only reloads retain the active extension runtime, while a
1744
+ // genuine Pi session rebuild is allowed to replace that runtime.
1745
+ extensionsOverride: (extensions) => {
1746
+ if (preserveExtensionsResult && currentExtensionsResult != null) {
1747
+ return currentExtensionsResult;
1748
+ }
1749
+ currentExtensionsResult = extensions;
1750
+ return extensions;
1751
+ }
1752
+ } : {},
1679
1753
  // Pi runs in the host process, so its default resource discovery reaches
1680
1754
  // the host developer's personal config (`~/.pi/agent/*`, `~/.agents/*`).
1681
- // The harness does not expose extensions, themes, or prompt templates, so
1682
- // disable those entirely this also avoids loading and executing a host
1683
- // developer's personal Pi extensions inside the server process. Skills are
1684
- // kept but filtered to workspace project skills plus harness-provided
1755
+ // The harness exposes only explicitly supplied inline extension factories;
1756
+ // disable filesystem extension discovery entirely to avoid loading and
1757
+ // executing a host developer's personal or project Pi extensions inside
1758
+ // the server process. Themes and prompt templates stay disabled. Skills
1759
+ // are kept but filtered to workspace project skills plus harness-provided
1685
1760
  // skills whose files live in sandbox HOME.
1686
1761
  noExtensions: true,
1687
1762
  noThemes: true,
@@ -1697,6 +1772,20 @@ async function createPiSession(input) {
1697
1772
  })
1698
1773
  });
1699
1774
  await resourceLoader.reload();
1775
+ async function reloadResourcesOnly() {
1776
+ if (!hasExtensionFactories) {
1777
+ await resourceLoader.reload();
1778
+ return;
1779
+ }
1780
+ const factories = extensionFactories.splice(0);
1781
+ preserveExtensionsResult = true;
1782
+ try {
1783
+ await resourceLoader.reload();
1784
+ } finally {
1785
+ preserveExtensionsResult = false;
1786
+ extensionFactories.push(...factories);
1787
+ }
1788
+ }
1700
1789
  let piSession;
1701
1790
  let unsubscribe;
1702
1791
  let lastToolsSignature;
@@ -1824,13 +1913,27 @@ async function createPiSession(input) {
1824
1913
  builtinNames: [...builtinNames]
1825
1914
  };
1826
1915
  }
1916
+ async function disposePiSession() {
1917
+ unsubscribe?.();
1918
+ unsubscribe = void 0;
1919
+ const session = piSession;
1920
+ piSession = void 0;
1921
+ if (!session) return;
1922
+ if (hasMcpServers) {
1923
+ await session.reload().catch(() => {
1924
+ });
1925
+ }
1926
+ session.dispose();
1927
+ }
1827
1928
  async function rebuildPiSession(userTools, isFirstBuild) {
1929
+ let resourcesReloaded = false;
1828
1930
  if (piSession) {
1829
- unsubscribe?.();
1830
- unsubscribe = void 0;
1831
- piSession.dispose();
1832
- piSession = void 0;
1931
+ await disposePiSession();
1833
1932
  await new Promise((resolve) => setTimeout(resolve, 25));
1933
+ if (hasExtensionFactories) {
1934
+ await resourceLoader.reload();
1935
+ resourcesReloaded = true;
1936
+ }
1834
1937
  }
1835
1938
  const { customTools, builtinNames } = buildToolDefinitions(userTools);
1836
1939
  const toolNames = customTools.map((t) => t.name);
@@ -1847,17 +1950,21 @@ async function createPiSession(input) {
1847
1950
  settingsManager,
1848
1951
  resourceLoader,
1849
1952
  customTools,
1850
- tools: toolNames,
1953
+ ...hasMcpServers ? { noTools: "builtin" } : { tools: toolNames },
1851
1954
  ...input.settings.thinkingLevel ? { thinkingLevel: input.settings.thinkingLevel } : {},
1852
1955
  ...resolvedModel ? { model: resolvedModel } : {}
1853
1956
  });
1854
1957
  piSession = session;
1958
+ if (hasMcpServers) {
1959
+ await piSession.bindExtensions({ mode: "print" });
1960
+ }
1855
1961
  const candidatePath = sessionManager.getSessionFile();
1856
1962
  if (candidatePath) {
1857
1963
  sessionFileName = safePiSessionFileName(path7.basename(candidatePath));
1858
1964
  }
1859
1965
  translatorState = createPiTranslatorState({
1860
1966
  builtinToolNames: builtinNames,
1967
+ hostToolNames: userTools.map((tool2) => tool2.name),
1861
1968
  nativeToCommon: NATIVE_TO_COMMON
1862
1969
  });
1863
1970
  unsubscribe = piSession.subscribe((rawEvent) => {
@@ -1872,6 +1979,7 @@ async function createPiSession(input) {
1872
1979
  }
1873
1980
  }
1874
1981
  });
1982
+ return resourcesReloaded;
1875
1983
  }
1876
1984
  async function runTurn(turnOpts) {
1877
1985
  if (stopped) {
@@ -1880,11 +1988,14 @@ async function createPiSession(input) {
1880
1988
  const userTools = turnOpts.tools;
1881
1989
  const signature = JSON.stringify(userTools.map((t) => t.name).sort());
1882
1990
  const needsRebuild = piSession == null || signature !== lastToolsSignature;
1991
+ let resourcesReloaded = false;
1883
1992
  if (needsRebuild) {
1884
- await rebuildPiSession(userTools, piSession == null);
1993
+ resourcesReloaded = await rebuildPiSession(userTools, piSession == null);
1885
1994
  lastToolsSignature = signature;
1886
1995
  }
1887
- await resourceLoader.reload();
1996
+ if (!resourcesReloaded) {
1997
+ await reloadResourcesOnly();
1998
+ }
1888
1999
  await syncHostWorkspaceFromSandbox({
1889
2000
  sandbox,
1890
2001
  sandboxWorkDir: input.sessionWorkDir,
@@ -1893,6 +2004,7 @@ async function createPiSession(input) {
1893
2004
  currentEmit = turnOpts.emit;
1894
2005
  translatorState = createPiTranslatorState({
1895
2006
  builtinToolNames: [...PI_NATIVE_BUILTIN_NAMES],
2007
+ hostToolNames: userTools.map((tool2) => tool2.name),
1896
2008
  nativeToCommon: NATIVE_TO_COMMON
1897
2009
  });
1898
2010
  turnOpts.emit({ type: "stream-start" });
@@ -1974,10 +2086,7 @@ async function createPiSession(input) {
1974
2086
  } catch {
1975
2087
  }
1976
2088
  }
1977
- unsubscribe?.();
1978
- unsubscribe = void 0;
1979
- piSession?.dispose();
1980
- piSession = void 0;
2089
+ await disposePiSession();
1981
2090
  workspaceVfs.unmount();
1982
2091
  await rm2(hostRoot, { recursive: true, force: true });
1983
2092
  return {
@@ -2036,10 +2145,7 @@ async function createPiSession(input) {
2036
2145
  parkedPiSessions.delete(input.sessionId);
2037
2146
  settlePendingToolResults("Pi session stopped");
2038
2147
  settlePendingToolApprovals("Pi session stopped");
2039
- unsubscribe?.();
2040
- unsubscribe = void 0;
2041
- piSession?.dispose();
2042
- piSession = void 0;
2148
+ await disposePiSession();
2043
2149
  workspaceVfs.unmount();
2044
2150
  await rm2(hostRoot, { recursive: true, force: true });
2045
2151
  },
@@ -2094,10 +2200,7 @@ async function createPiSession(input) {
2094
2200
  parkedPiSessions.delete(input.sessionId);
2095
2201
  settlePendingToolResults("Pi session suspended");
2096
2202
  settlePendingToolApprovals("Pi session suspended");
2097
- unsubscribe?.();
2098
- unsubscribe = void 0;
2099
- piSession?.dispose();
2100
- piSession = void 0;
2203
+ await disposePiSession();
2101
2204
  workspaceVfs.unmount();
2102
2205
  await rm2(hostRoot, { recursive: true, force: true });
2103
2206
  return {
@@ -2110,6 +2213,19 @@ async function createPiSession(input) {
2110
2213
  };
2111
2214
  return sessionImpl;
2112
2215
  }
2216
+ function resolvePiMcpServers({
2217
+ mcpServers
2218
+ }) {
2219
+ if (mcpServers == null) return {};
2220
+ for (const [name, value] of Object.entries(mcpServers)) {
2221
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
2222
+ throw new Error(
2223
+ `Pi MCP server ${JSON.stringify(name)} must be configured with an object value.`
2224
+ );
2225
+ }
2226
+ }
2227
+ return mcpServers;
2228
+ }
2113
2229
  function isAbortError(value) {
2114
2230
  if (value == null) return false;
2115
2231
  if (typeof value === "object" && value.name === "AbortError") {
@@ -2428,7 +2544,9 @@ function createPi(settings = {}) {
2428
2544
  settings: {
2429
2545
  ...settings.auth ? { auth: settings.auth } : {},
2430
2546
  ...settings.model ? { model: settings.model } : {},
2431
- ...settings.thinkingLevel ? { thinkingLevel: settings.thinkingLevel } : {}
2547
+ ...settings.thinkingLevel ? { thinkingLevel: settings.thinkingLevel } : {},
2548
+ ...settings.mcpServers ? { mcpServers: settings.mcpServers } : {},
2549
+ ...settings.extensionFactories ? { extensionFactories: settings.extensionFactories } : {}
2432
2550
  },
2433
2551
  clientApp: PI_CLIENT_APP,
2434
2552
  isResume: lifecycleState != null,