@ai-sdk/harness-pi 1.0.63 → 1.0.65

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,24 @@
1
1
  # @ai-sdk/harness-pi
2
2
 
3
+ ## 1.0.65
4
+
5
+ ### Patch Changes
6
+
7
+ - c20a315: feat(harness-pi): support caller-supplied inline Pi extension factories
8
+ - a03ff6c: feat(harness): add support for per-harness MCP servers
9
+ - Updated dependencies [401a4ba]
10
+ - Updated dependencies [a03ff6c]
11
+ - @ai-sdk/provider-utils@5.0.26
12
+ - @ai-sdk/harness@1.0.65
13
+
14
+ ## 1.0.64
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [81cd026]
19
+ - @ai-sdk/provider-utils@5.0.25
20
+ - @ai-sdk/harness@1.0.64
21
+
3
22
  ## 1.0.63
4
23
 
5
24
  ### 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.63" : "0.0.0-test";
111
+ var VERSION = true ? "1.0.65" : "0.0.0-test";
112
112
 
113
113
  // src/pi-auth.ts
114
114
  var DEFAULT_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";
@@ -740,6 +740,7 @@ async function writePiSkills(args) {
740
740
 
741
741
  // src/pi-translate.ts
742
742
  import { randomBytes } from "crypto";
743
+ import { secureJsonParse } from "@ai-sdk/provider-utils";
743
744
 
744
745
  // src/pi-utils.ts
745
746
  import {
@@ -804,7 +805,9 @@ function createPiTranslatorState(options = {}) {
804
805
  pendingStepToolCallIds: /* @__PURE__ */ new Set(),
805
806
  stepOpen: false,
806
807
  hostToolResults: /* @__PURE__ */ new Map(),
808
+ dynamicToolCallIds: /* @__PURE__ */ new Set(),
807
809
  builtinToolNames: new Set(options.builtinToolNames ?? []),
810
+ hostToolNames: new Set(options.hostToolNames ?? []),
808
811
  nativeToCommonNameMap: map
809
812
  };
810
813
  }
@@ -830,6 +833,14 @@ function unwrapPiToolResult(event) {
830
833
  if (typeof event.content === "string") return event.content;
831
834
  return event.result ?? event.content ?? null;
832
835
  }
836
+ function parseMcpToolResult(content) {
837
+ if (typeof content !== "string") return content;
838
+ try {
839
+ return secureJsonParse(content);
840
+ } catch {
841
+ return content;
842
+ }
843
+ }
833
844
  function resolveToolName(state, nativeName) {
834
845
  const common = state.nativeToCommonNameMap.get(nativeName);
835
846
  return { wire: common ?? nativeName, native: nativeName };
@@ -969,7 +980,9 @@ function translatePiEvent(event, state) {
969
980
  if (!event.toolCallId || !event.toolName) return [];
970
981
  const { wire, native } = resolveToolName(state, event.toolName);
971
982
  state.observedToolNames.set(event.toolCallId, wire);
972
- const providerExecuted = state.builtinToolNames.has(native);
983
+ const isMcpTool = !state.hostToolNames.has(native) && (native === "mcp" || native.startsWith("mcp__"));
984
+ const providerExecuted = state.builtinToolNames.has(native) || isMcpTool;
985
+ if (isMcpTool) state.dynamicToolCallIds.add(event.toolCallId);
973
986
  const input = serializeToolOutput(event.args ?? event.input ?? {});
974
987
  return [
975
988
  {
@@ -978,7 +991,8 @@ function translatePiEvent(event, state) {
978
991
  toolName: wire,
979
992
  input,
980
993
  ...wire !== native ? { nativeName: native } : {},
981
- ...providerExecuted ? { providerExecuted: true } : {}
994
+ ...providerExecuted ? { providerExecuted: true } : {},
995
+ ...isMcpTool ? { dynamic: true } : {}
982
996
  }
983
997
  ];
984
998
  }
@@ -989,7 +1003,8 @@ function translatePiEvent(event, state) {
989
1003
  const nativeName = event.toolName;
990
1004
  const wire = recordedName ?? (nativeName ? resolveToolName(state, nativeName).wire : void 0);
991
1005
  if (!wire) return [];
992
- const result = state.hostToolResults.has(event.toolCallId) ? state.hostToolResults.get(event.toolCallId) ?? null : unwrapPiToolResult(event);
1006
+ const dynamic = state.dynamicToolCallIds.delete(event.toolCallId);
1007
+ const result = state.hostToolResults.has(event.toolCallId) ? state.hostToolResults.get(event.toolCallId) ?? null : dynamic ? parseMcpToolResult(unwrapPiToolResult(event)) : unwrapPiToolResult(event);
993
1008
  state.hostToolResults.delete(event.toolCallId);
994
1009
  state.pendingStepToolCallIds.delete(event.toolCallId);
995
1010
  return [
@@ -998,7 +1013,8 @@ function translatePiEvent(event, state) {
998
1013
  toolCallId: event.toolCallId,
999
1014
  toolName: wire,
1000
1015
  result,
1001
- ...event.isError ? { isError: true } : {}
1016
+ ...event.isError ? { isError: true } : {},
1017
+ ...dynamic ? { dynamic: true } : {}
1002
1018
  },
1003
1019
  ...finishStep(state)
1004
1020
  ];
@@ -1505,6 +1521,7 @@ async function syncHostWorkspaceFromSandbox(args) {
1505
1521
 
1506
1522
  // src/pi-session.ts
1507
1523
  var HARNESS_ID2 = "pi";
1524
+ var PI_MCP_ADAPTER_PACKAGE = "pi-mcp-adapter";
1508
1525
  var parkedPiSessions = /* @__PURE__ */ new Map();
1509
1526
  function isWithinDirectory(parent, child) {
1510
1527
  const rel = path7.relative(parent, child);
@@ -1670,18 +1687,56 @@ async function createPiSession(input) {
1670
1687
  env: resolverEnv
1671
1688
  });
1672
1689
  const resolvedModel = resolveModel(input.settings.model);
1690
+ const mcpServers = resolvePiMcpServers({
1691
+ mcpServers: input.settings.mcpServers
1692
+ });
1693
+ const hasMcpServers = Object.keys(mcpServers).length > 0;
1694
+ const extensionFactories = [
1695
+ ...input.settings.extensionFactories ?? []
1696
+ ];
1697
+ if (hasMcpServers) {
1698
+ const { createMcpAdapter } = await import(PI_MCP_ADAPTER_PACKAGE);
1699
+ extensionFactories.push(
1700
+ createMcpAdapter({
1701
+ config: {
1702
+ mcpServers,
1703
+ settings: {
1704
+ directTools: true,
1705
+ toolPrefix: "mcp",
1706
+ disableProxyTool: true
1707
+ }
1708
+ }
1709
+ })
1710
+ );
1711
+ }
1712
+ const hasExtensionFactories = extensionFactories.length > 0;
1713
+ let preserveExtensionsResult = false;
1714
+ let currentExtensionsResult;
1673
1715
  const resourceLoader = new DefaultResourceLoader({
1674
1716
  cwd: sessionWorkDir,
1675
1717
  agentDir: hostAgentDir,
1676
1718
  settingsManager,
1677
1719
  appendSystemPromptOverride: () => [],
1678
- extensionFactories: [],
1720
+ extensionFactories,
1721
+ ...hasExtensionFactories ? {
1722
+ // DefaultResourceLoader invokes inline factories on every reload.
1723
+ // Resource-only reloads retain the active extension runtime, while a
1724
+ // genuine Pi session rebuild is allowed to replace that runtime.
1725
+ extensionsOverride: (extensions) => {
1726
+ if (preserveExtensionsResult && currentExtensionsResult != null) {
1727
+ return currentExtensionsResult;
1728
+ }
1729
+ currentExtensionsResult = extensions;
1730
+ return extensions;
1731
+ }
1732
+ } : {},
1679
1733
  // Pi runs in the host process, so its default resource discovery reaches
1680
1734
  // 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
1735
+ // The harness exposes only explicitly supplied inline extension factories;
1736
+ // disable filesystem extension discovery entirely to avoid loading and
1737
+ // executing a host developer's personal or project Pi extensions inside
1738
+ // the server process. Themes and prompt templates stay disabled. Skills
1739
+ // are kept but filtered to workspace project skills plus harness-provided
1685
1740
  // skills whose files live in sandbox HOME.
1686
1741
  noExtensions: true,
1687
1742
  noThemes: true,
@@ -1697,6 +1752,20 @@ async function createPiSession(input) {
1697
1752
  })
1698
1753
  });
1699
1754
  await resourceLoader.reload();
1755
+ async function reloadResourcesOnly() {
1756
+ if (!hasExtensionFactories) {
1757
+ await resourceLoader.reload();
1758
+ return;
1759
+ }
1760
+ const factories = extensionFactories.splice(0);
1761
+ preserveExtensionsResult = true;
1762
+ try {
1763
+ await resourceLoader.reload();
1764
+ } finally {
1765
+ preserveExtensionsResult = false;
1766
+ extensionFactories.push(...factories);
1767
+ }
1768
+ }
1700
1769
  let piSession;
1701
1770
  let unsubscribe;
1702
1771
  let lastToolsSignature;
@@ -1824,13 +1893,27 @@ async function createPiSession(input) {
1824
1893
  builtinNames: [...builtinNames]
1825
1894
  };
1826
1895
  }
1896
+ async function disposePiSession() {
1897
+ unsubscribe?.();
1898
+ unsubscribe = void 0;
1899
+ const session = piSession;
1900
+ piSession = void 0;
1901
+ if (!session) return;
1902
+ if (hasMcpServers) {
1903
+ await session.reload().catch(() => {
1904
+ });
1905
+ }
1906
+ session.dispose();
1907
+ }
1827
1908
  async function rebuildPiSession(userTools, isFirstBuild) {
1909
+ let resourcesReloaded = false;
1828
1910
  if (piSession) {
1829
- unsubscribe?.();
1830
- unsubscribe = void 0;
1831
- piSession.dispose();
1832
- piSession = void 0;
1911
+ await disposePiSession();
1833
1912
  await new Promise((resolve) => setTimeout(resolve, 25));
1913
+ if (hasExtensionFactories) {
1914
+ await resourceLoader.reload();
1915
+ resourcesReloaded = true;
1916
+ }
1834
1917
  }
1835
1918
  const { customTools, builtinNames } = buildToolDefinitions(userTools);
1836
1919
  const toolNames = customTools.map((t) => t.name);
@@ -1847,17 +1930,21 @@ async function createPiSession(input) {
1847
1930
  settingsManager,
1848
1931
  resourceLoader,
1849
1932
  customTools,
1850
- tools: toolNames,
1933
+ ...hasMcpServers ? { noTools: "builtin" } : { tools: toolNames },
1851
1934
  ...input.settings.thinkingLevel ? { thinkingLevel: input.settings.thinkingLevel } : {},
1852
1935
  ...resolvedModel ? { model: resolvedModel } : {}
1853
1936
  });
1854
1937
  piSession = session;
1938
+ if (hasMcpServers) {
1939
+ await piSession.bindExtensions({ mode: "print" });
1940
+ }
1855
1941
  const candidatePath = sessionManager.getSessionFile();
1856
1942
  if (candidatePath) {
1857
1943
  sessionFileName = safePiSessionFileName(path7.basename(candidatePath));
1858
1944
  }
1859
1945
  translatorState = createPiTranslatorState({
1860
1946
  builtinToolNames: builtinNames,
1947
+ hostToolNames: userTools.map((tool2) => tool2.name),
1861
1948
  nativeToCommon: NATIVE_TO_COMMON
1862
1949
  });
1863
1950
  unsubscribe = piSession.subscribe((rawEvent) => {
@@ -1872,6 +1959,7 @@ async function createPiSession(input) {
1872
1959
  }
1873
1960
  }
1874
1961
  });
1962
+ return resourcesReloaded;
1875
1963
  }
1876
1964
  async function runTurn(turnOpts) {
1877
1965
  if (stopped) {
@@ -1880,11 +1968,14 @@ async function createPiSession(input) {
1880
1968
  const userTools = turnOpts.tools;
1881
1969
  const signature = JSON.stringify(userTools.map((t) => t.name).sort());
1882
1970
  const needsRebuild = piSession == null || signature !== lastToolsSignature;
1971
+ let resourcesReloaded = false;
1883
1972
  if (needsRebuild) {
1884
- await rebuildPiSession(userTools, piSession == null);
1973
+ resourcesReloaded = await rebuildPiSession(userTools, piSession == null);
1885
1974
  lastToolsSignature = signature;
1886
1975
  }
1887
- await resourceLoader.reload();
1976
+ if (!resourcesReloaded) {
1977
+ await reloadResourcesOnly();
1978
+ }
1888
1979
  await syncHostWorkspaceFromSandbox({
1889
1980
  sandbox,
1890
1981
  sandboxWorkDir: input.sessionWorkDir,
@@ -1893,6 +1984,7 @@ async function createPiSession(input) {
1893
1984
  currentEmit = turnOpts.emit;
1894
1985
  translatorState = createPiTranslatorState({
1895
1986
  builtinToolNames: [...PI_NATIVE_BUILTIN_NAMES],
1987
+ hostToolNames: userTools.map((tool2) => tool2.name),
1896
1988
  nativeToCommon: NATIVE_TO_COMMON
1897
1989
  });
1898
1990
  turnOpts.emit({ type: "stream-start" });
@@ -1974,10 +2066,7 @@ async function createPiSession(input) {
1974
2066
  } catch {
1975
2067
  }
1976
2068
  }
1977
- unsubscribe?.();
1978
- unsubscribe = void 0;
1979
- piSession?.dispose();
1980
- piSession = void 0;
2069
+ await disposePiSession();
1981
2070
  workspaceVfs.unmount();
1982
2071
  await rm2(hostRoot, { recursive: true, force: true });
1983
2072
  return {
@@ -2036,10 +2125,7 @@ async function createPiSession(input) {
2036
2125
  parkedPiSessions.delete(input.sessionId);
2037
2126
  settlePendingToolResults("Pi session stopped");
2038
2127
  settlePendingToolApprovals("Pi session stopped");
2039
- unsubscribe?.();
2040
- unsubscribe = void 0;
2041
- piSession?.dispose();
2042
- piSession = void 0;
2128
+ await disposePiSession();
2043
2129
  workspaceVfs.unmount();
2044
2130
  await rm2(hostRoot, { recursive: true, force: true });
2045
2131
  },
@@ -2094,10 +2180,7 @@ async function createPiSession(input) {
2094
2180
  parkedPiSessions.delete(input.sessionId);
2095
2181
  settlePendingToolResults("Pi session suspended");
2096
2182
  settlePendingToolApprovals("Pi session suspended");
2097
- unsubscribe?.();
2098
- unsubscribe = void 0;
2099
- piSession?.dispose();
2100
- piSession = void 0;
2183
+ await disposePiSession();
2101
2184
  workspaceVfs.unmount();
2102
2185
  await rm2(hostRoot, { recursive: true, force: true });
2103
2186
  return {
@@ -2110,6 +2193,19 @@ async function createPiSession(input) {
2110
2193
  };
2111
2194
  return sessionImpl;
2112
2195
  }
2196
+ function resolvePiMcpServers({
2197
+ mcpServers
2198
+ }) {
2199
+ if (mcpServers == null) return {};
2200
+ for (const [name, value] of Object.entries(mcpServers)) {
2201
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
2202
+ throw new Error(
2203
+ `Pi MCP server ${JSON.stringify(name)} must be configured with an object value.`
2204
+ );
2205
+ }
2206
+ }
2207
+ return mcpServers;
2208
+ }
2113
2209
  function isAbortError(value) {
2114
2210
  if (value == null) return false;
2115
2211
  if (typeof value === "object" && value.name === "AbortError") {
@@ -2428,7 +2524,9 @@ function createPi(settings = {}) {
2428
2524
  settings: {
2429
2525
  ...settings.auth ? { auth: settings.auth } : {},
2430
2526
  ...settings.model ? { model: settings.model } : {},
2431
- ...settings.thinkingLevel ? { thinkingLevel: settings.thinkingLevel } : {}
2527
+ ...settings.thinkingLevel ? { thinkingLevel: settings.thinkingLevel } : {},
2528
+ ...settings.mcpServers ? { mcpServers: settings.mcpServers } : {},
2529
+ ...settings.extensionFactories ? { extensionFactories: settings.extensionFactories } : {}
2432
2530
  },
2433
2531
  clientApp: PI_CLIENT_APP,
2434
2532
  isResume: lifecycleState != null,