@ai-sdk/harness-pi 1.0.73 → 1.0.75

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/dist/index.js CHANGED
@@ -116,13 +116,16 @@ import { mkdir as mkdir3, rm as rm2 } from "fs/promises";
116
116
  import { tmpdir } from "os";
117
117
  import path7 from "path";
118
118
  import { Type as Type2 } from "typebox";
119
+ import {
120
+ HarnessCapabilityUnsupportedError as HarnessCapabilityUnsupportedError2
121
+ } from "@ai-sdk/harness";
119
122
  import { resolveSandboxHomeDir } from "@ai-sdk/harness/utils";
120
123
 
121
124
  // src/pi-auth.ts
122
125
  import { getAiGatewayAuthFromEnv } from "@ai-sdk/harness/utils";
123
126
 
124
127
  // src/version.ts
125
- var VERSION = true ? "1.0.73" : "0.0.0-test";
128
+ var VERSION = true ? "1.0.75" : "0.0.0-test";
126
129
 
127
130
  // src/pi-auth.ts
128
131
  var DEFAULT_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";
@@ -1557,34 +1560,113 @@ function normalizeRelativePath(inputPath) {
1557
1560
  }
1558
1561
  async function readCommandOutput(sandbox, command) {
1559
1562
  const result = await sandbox.run({ command });
1560
- const output = result.stdout || result.stderr;
1561
1563
  if (result.exitCode != null && result.exitCode !== 0) {
1562
1564
  throw new Error(
1563
- output || `Sandbox command failed with exit code ${result.exitCode}`
1565
+ result.stderr || result.stdout || `Sandbox command failed with exit code ${result.exitCode}`
1564
1566
  );
1565
1567
  }
1566
- return output;
1568
+ return result.stdout || result.stderr;
1567
1569
  }
1568
1570
  async function listRemoteWorkspaceEntries(sandbox, sandboxWorkDir) {
1569
- const contextPredicate = PI_CONTEXT_FILENAMES.map(
1570
- (name) => `-name ${shellQuote2(name)}`
1571
- ).join(" -o ");
1572
- const configFinds = PI_CONFIG_DIRS.map(
1573
- (dir) => ` if [ -d ./${dir} ]; then find -L ./${dir} \\( -type d -o -type f \\) -print0; fi;`
1574
- );
1571
+ const scopedPaths = [
1572
+ ...PI_CONFIG_DIRS.map((dir) => `./${dir}`),
1573
+ ...PI_CONTEXT_FILENAMES.map((name) => `./${name}`)
1574
+ ];
1575
1575
  const listCommand = [
1576
- "{",
1577
- ...configFinds,
1578
- ` find . -maxdepth 1 -type f \\( ${contextPredicate} \\) -print0;`,
1579
- "} |",
1580
- "while IFS= read -r -d '' entry; do",
1581
- " rel=${entry#./}",
1582
- ' if [ -d "$entry" ]; then',
1583
- ` printf 'd\\t%s\\n' "$rel"`,
1584
- ' elif [ -f "$entry" ]; then',
1585
- ` printf 'f\\t%s\\n' "$rel"`,
1576
+ `pi_config_sources=(${scopedPaths.map(
1577
+ (scopedPath) => shellQuote2(path6.posix.join(sandboxWorkDir, scopedPath.slice(2)))
1578
+ ).join(" ")})`,
1579
+ `pi_config_relatives=(${scopedPaths.map((scopedPath) => shellQuote2(scopedPath.slice(2))).join(" ")})`,
1580
+ `pi_config_ancestors=(${scopedPaths.map(() => "''").join(" ")})`,
1581
+ `pi_config_resolve_ancestors=(${scopedPaths.map(() => "1").join(" ")})`,
1582
+ "pi_config_index=0",
1583
+ 'while [ "$pi_config_index" -lt "${#pi_config_sources[@]}" ]; do',
1584
+ " source=${pi_config_sources[$pi_config_index]}",
1585
+ " relative=${pi_config_relatives[$pi_config_index]}",
1586
+ " ancestors=${pi_config_ancestors[$pi_config_index]}",
1587
+ " resolve_ancestors=${pi_config_resolve_ancestors[$pi_config_index]}",
1588
+ " pi_config_index=$((pi_config_index + 1))",
1589
+ ' if [ "$resolve_ancestors" = 1 ] || [ -L "$source" ]; then',
1590
+ " pending=$source",
1591
+ " resolved=",
1592
+ " seen_links=",
1593
+ ' case "$pending" in',
1594
+ " /*) ;;",
1595
+ " *) pending=$PWD/$pending ;;",
1596
+ " esac",
1597
+ ' while [ -n "$pending" ]; do',
1598
+ " pending=${pending#/}",
1599
+ ' [ -n "$pending" ] || break',
1600
+ " component=${pending%%/*}",
1601
+ ' if [ "$pending" = "$component" ]; then',
1602
+ " pending=",
1603
+ " else",
1604
+ " pending=${pending#*/}",
1605
+ " fi",
1606
+ ' case "$component" in',
1607
+ ' ""|.) continue ;;',
1608
+ " ..)",
1609
+ " resolved=${resolved%/*}",
1610
+ " continue",
1611
+ " ;;",
1612
+ " esac",
1613
+ " candidate=$resolved/$component",
1614
+ ' if [ -L "$candidate" ]; then',
1615
+ ' case "$seen_links" in',
1616
+ ' *"',
1617
+ '"$candidate"',
1618
+ '"*)',
1619
+ ` printf 'Pi config traversal encountered a symlink cycle at %s\\n' "$candidate" >&2`,
1620
+ " exit 1",
1621
+ " ;;",
1622
+ " esac",
1623
+ ' seen_links=$seen_links"',
1624
+ '"$candidate"',
1625
+ '"',
1626
+ ' target=$(readlink "$candidate") || exit $?',
1627
+ ' case "$target" in',
1628
+ " /*) pending=$target${pending:+/$pending} ;;",
1629
+ " *) pending=${candidate%/*}/$target${pending:+/$pending} ;;",
1630
+ " esac",
1631
+ " resolved=",
1632
+ " else",
1633
+ " resolved=$candidate",
1634
+ " fi",
1635
+ " done",
1636
+ ' if [ -z "$resolved" ]; then',
1637
+ ` printf 'Pi config traversal resolved %s to the filesystem root; skipping\\n' "$relative" >&2`,
1638
+ " continue",
1639
+ " fi",
1640
+ " source=$resolved",
1586
1641
  " fi",
1587
- "done | LC_ALL=C sort"
1642
+ ' if [ -d "$source" ]; then',
1643
+ ' case "$ancestors" in',
1644
+ ' *"',
1645
+ '"$source"',
1646
+ '"*)',
1647
+ ` printf 'Pi config traversal encountered a symlink cycle at %s\\n' "$relative" >&2`,
1648
+ " exit 1",
1649
+ " ;;",
1650
+ " esac",
1651
+ ' ancestors=$ancestors"',
1652
+ '"$source"',
1653
+ '"',
1654
+ ` printf 'd\\t%s\\n' "$relative"`,
1655
+ ' for child in "$source"/* "$source"/.[!.]* "$source"/..?*; do',
1656
+ ' if [ -L "$child" ] || [ -d "$child" ] || [ -f "$child" ]; then',
1657
+ " child_name=${child##*/}",
1658
+ ' pi_config_sources+=("$child")',
1659
+ ' pi_config_relatives+=("$relative/$child_name")',
1660
+ ' pi_config_ancestors+=("$ancestors")',
1661
+ " pi_config_resolve_ancestors+=(0)",
1662
+ " fi",
1663
+ " done",
1664
+ ' elif [ -f "$source" ]; then',
1665
+ ` printf 'f\\t%s\\t%s\\n' "$relative" "$source"`,
1666
+ " fi",
1667
+ " true",
1668
+ "done",
1669
+ "true"
1588
1670
  ].join("\n");
1589
1671
  const output = await readCommandOutput(
1590
1672
  sandbox,
@@ -1593,11 +1675,13 @@ async function listRemoteWorkspaceEntries(sandbox, sandboxWorkDir) {
1593
1675
  const directories = [];
1594
1676
  const files = [];
1595
1677
  for (const line of output.split("\n").filter(Boolean)) {
1596
- const [kind, rawPath] = line.split(" ", 2);
1678
+ const [kind, rawPath, sandboxPath] = line.split(" ", 3);
1597
1679
  if (!rawPath) continue;
1598
1680
  const relativePath = normalizeRelativePath(rawPath);
1599
1681
  if (kind === "d") directories.push(relativePath);
1600
- else if (kind === "f") files.push(relativePath);
1682
+ else if (kind === "f" && sandboxPath) {
1683
+ files.push({ relativePath, sandboxPath });
1684
+ }
1601
1685
  }
1602
1686
  return { directories, files };
1603
1687
  }
@@ -1648,7 +1732,7 @@ function buildRequiredDirectories(remoteDirectories, remoteFiles) {
1648
1732
  directories.add(normalizeRelativePath(directory));
1649
1733
  }
1650
1734
  for (const file of remoteFiles) {
1651
- let current = path6.dirname(normalizeRelativePath(file));
1735
+ let current = path6.dirname(normalizeRelativePath(file.relativePath));
1652
1736
  while (current !== "." && current !== path6.sep && current.length > 0) {
1653
1737
  directories.add(current);
1654
1738
  current = path6.dirname(current);
@@ -1663,7 +1747,9 @@ async function syncHostWorkspaceFromSandbox(args) {
1663
1747
  sandboxWorkDir
1664
1748
  );
1665
1749
  const hostEntries = await collectHostScopedEntries(hostWorkDir);
1666
- const remoteFiles = new Set(remoteEntries.files);
1750
+ const remoteFiles = new Set(
1751
+ remoteEntries.files.map((file) => file.relativePath)
1752
+ );
1667
1753
  const requiredDirectories = buildRequiredDirectories(
1668
1754
  remoteEntries.directories,
1669
1755
  remoteEntries.files
@@ -1685,15 +1771,11 @@ async function syncHostWorkspaceFromSandbox(args) {
1685
1771
  )) {
1686
1772
  await mkdir2(path6.join(hostWorkDir, relativePath), { recursive: true });
1687
1773
  }
1688
- for (const relativePath of remoteEntries.files) {
1689
- const remotePath = path6.posix.join(
1690
- sandboxWorkDir,
1691
- relativePath.split(path6.sep).join("/")
1692
- );
1693
- const bytes = await sandbox.readBinaryFile({ path: remotePath });
1774
+ for (const { relativePath, sandboxPath } of remoteEntries.files) {
1775
+ const bytes = await sandbox.readBinaryFile({ path: sandboxPath });
1694
1776
  if (!bytes) {
1695
1777
  throw new Error(
1696
- `Sandbox workspace file disappeared during mirror sync: ${remotePath}`
1778
+ `Sandbox workspace file disappeared during mirror sync: ${sandboxPath}`
1697
1779
  );
1698
1780
  }
1699
1781
  const content = Buffer.from(bytes);
@@ -1973,6 +2055,9 @@ async function createPiSession(input) {
1973
2055
  let suspending = false;
1974
2056
  const pendingToolResults = /* @__PURE__ */ new Map();
1975
2057
  const pendingToolApprovals = /* @__PURE__ */ new Map();
2058
+ const deliveredDanglingResults = /* @__PURE__ */ new Map();
2059
+ let restoredSessionManager;
2060
+ let deferredRerun;
1976
2061
  let currentEmit;
1977
2062
  let translatorState;
1978
2063
  let activeTurn;
@@ -2011,15 +2096,161 @@ async function createPiSession(input) {
2011
2096
  sessionFileName
2012
2097
  });
2013
2098
  }
2014
- function createPromptControl(input2) {
2015
- const abortHandler = () => {
2016
- piSession?.abort().catch(() => {
2099
+ function getRestoredSessionManager() {
2100
+ if (resumeSessionFilePath == null) return void 0;
2101
+ restoredSessionManager ??= SessionManager.open(
2102
+ resumeSessionFilePath,
2103
+ hostSessionDir,
2104
+ sessionWorkDir
2105
+ );
2106
+ return restoredSessionManager;
2107
+ }
2108
+ function findDanglingHostToolCalls(userTools) {
2109
+ if (piSession != null || resumeSessionFilePath == null) return [];
2110
+ const hostToolNames = new Set(userTools.map((tool2) => tool2.name));
2111
+ if (hostToolNames.size === 0) return [];
2112
+ const journal = getRestoredSessionManager();
2113
+ if (journal == null) return [];
2114
+ const messages = journal.buildSessionContext().messages;
2115
+ const resolvedToolCallIds = new Set(
2116
+ deliveredDanglingResults.keys()
2117
+ );
2118
+ for (const message of messages) {
2119
+ if (message.role === "toolResult") {
2120
+ resolvedToolCallIds.add(message.toolCallId);
2121
+ }
2122
+ }
2123
+ const dangling = [];
2124
+ for (const message of messages) {
2125
+ if (message.role !== "assistant") continue;
2126
+ if (message.stopReason === "error" || message.stopReason === "aborted") {
2127
+ continue;
2128
+ }
2129
+ for (const block of message.content) {
2130
+ if (block.type === "toolCall" && hostToolNames.has(block.name) && !resolvedToolCallIds.has(block.id)) {
2131
+ dangling.push({ toolCallId: block.id, toolName: block.name });
2132
+ }
2133
+ }
2134
+ }
2135
+ return dangling;
2136
+ }
2137
+ function acceptDanglingHostToolResult(args) {
2138
+ const barrier = deferredRerun;
2139
+ const toolName = barrier?.awaiting.get(args.toolCallId);
2140
+ if (barrier == null || toolName == null) return;
2141
+ barrier.awaiting.delete(args.toolCallId);
2142
+ deliveredDanglingResults.set(args.toolCallId, {
2143
+ toolName,
2144
+ output: args.output,
2145
+ isError: args.isError ?? false
2146
+ });
2147
+ if (barrier.awaiting.size === 0) {
2148
+ barrier.startRerun();
2149
+ }
2150
+ }
2151
+ function appendDeliveredHostToolResults() {
2152
+ if (deliveredDanglingResults.size === 0 || resumeSessionFilePath == null) {
2153
+ return false;
2154
+ }
2155
+ const journal = getRestoredSessionManager();
2156
+ if (journal == null) return false;
2157
+ for (const [toolCallId, delivered] of deliveredDanglingResults) {
2158
+ journal.appendMessage({
2159
+ role: "toolResult",
2160
+ toolCallId,
2161
+ toolName: delivered.toolName,
2162
+ content: [
2163
+ { type: "text", text: serializeToolOutput(delivered.output) }
2164
+ ],
2165
+ isError: delivered.isError,
2166
+ timestamp: Date.now()
2017
2167
  });
2168
+ }
2169
+ deliveredDanglingResults.clear();
2170
+ if (!sessionFileName) {
2171
+ sessionFileName = safePiSessionFileName(
2172
+ path7.basename(resumeSessionFilePath)
2173
+ );
2174
+ }
2175
+ return true;
2176
+ }
2177
+ function deferRerunUntilHostToolResults(danglingCalls, continueOpts) {
2178
+ deferredRerun?.cancel();
2179
+ let resolveDone;
2180
+ let rejectDone;
2181
+ const done = new Promise((resolve, reject) => {
2182
+ resolveDone = resolve;
2183
+ rejectDone = reject;
2184
+ });
2185
+ let settled = false;
2186
+ const startRerun = () => {
2187
+ if (settled) return;
2188
+ settled = true;
2189
+ deferredRerun = void 0;
2190
+ void (async () => {
2191
+ try {
2192
+ const control = await runTurn({
2193
+ text: "",
2194
+ tools: continueOpts.tools ?? [],
2195
+ instructions: continueOpts.instructions,
2196
+ emit: continueOpts.emit,
2197
+ abortSignal: continueOpts.abortSignal
2198
+ });
2199
+ await control.done;
2200
+ resolveDone();
2201
+ } catch (error) {
2202
+ rejectDone(error);
2203
+ }
2204
+ })();
2018
2205
  };
2019
- if (input2.abortSignal) {
2020
- input2.abortSignal.addEventListener("abort", abortHandler, {
2206
+ const cancel = (reason) => {
2207
+ if (settled) return;
2208
+ settled = true;
2209
+ deferredRerun = void 0;
2210
+ if (reason == null) {
2211
+ resolveDone();
2212
+ } else {
2213
+ rejectDone(reason);
2214
+ }
2215
+ };
2216
+ deferredRerun = {
2217
+ awaiting: new Map(
2218
+ danglingCalls.map((call) => [call.toolCallId, call.toolName])
2219
+ ),
2220
+ startRerun,
2221
+ cancel
2222
+ };
2223
+ const abortBarrier = () => {
2224
+ cancel(
2225
+ continueOpts.abortSignal?.reason ?? new Error(
2226
+ "Pi turn was aborted before its host tool results were delivered."
2227
+ )
2228
+ );
2229
+ };
2230
+ if (continueOpts.abortSignal?.aborted) {
2231
+ abortBarrier();
2232
+ } else {
2233
+ continueOpts.abortSignal?.addEventListener("abort", abortBarrier, {
2021
2234
  once: true
2022
2235
  });
2236
+ }
2237
+ return createPromptControl({
2238
+ done,
2239
+ abortSignal: continueOpts.abortSignal
2240
+ });
2241
+ }
2242
+ function createPromptControl(input2) {
2243
+ const abortHandler = () => {
2244
+ void input2.abort?.(input2.abortSignal?.reason);
2245
+ };
2246
+ if (input2.abortSignal) {
2247
+ if (input2.abortSignal.aborted) {
2248
+ abortHandler();
2249
+ } else {
2250
+ input2.abortSignal.addEventListener("abort", abortHandler, {
2251
+ once: true
2252
+ });
2253
+ }
2023
2254
  void input2.done.then(
2024
2255
  () => {
2025
2256
  input2.abortSignal?.removeEventListener("abort", abortHandler);
@@ -2032,7 +2263,10 @@ async function createPiSession(input) {
2032
2263
  return {
2033
2264
  async submitToolResult(args) {
2034
2265
  const pending = pendingToolResults.get(args.toolCallId);
2035
- if (!pending) return;
2266
+ if (!pending) {
2267
+ acceptDanglingHostToolResult(args);
2268
+ return;
2269
+ }
2036
2270
  pendingToolResults.delete(args.toolCallId);
2037
2271
  translatorState?.hostToolResults.set(args.toolCallId, args.output);
2038
2272
  pending.resolve(args.output);
@@ -2121,11 +2355,7 @@ async function createPiSession(input) {
2121
2355
  }
2122
2356
  const { customTools, builtinNames } = buildToolDefinitions(userTools);
2123
2357
  const toolNames = customTools.map((t) => t.name);
2124
- const sessionManager = isFirstBuild && resumeSessionFilePath ? SessionManager.open(
2125
- resumeSessionFilePath,
2126
- hostSessionDir,
2127
- sessionWorkDir
2128
- ) : SessionManager.create(sessionWorkDir, hostSessionDir);
2358
+ const sessionManager = isFirstBuild && resumeSessionFilePath ? getRestoredSessionManager() : SessionManager.create(sessionWorkDir, hostSessionDir);
2129
2359
  const { session } = await createAgentSession({
2130
2360
  cwd: sessionWorkDir,
2131
2361
  agentDir: hostAgentDir,
@@ -2170,90 +2400,137 @@ async function createPiSession(input) {
2170
2400
  throw new Error("Pi session has been stopped.");
2171
2401
  }
2172
2402
  const userTools = turnOpts.tools;
2173
- const signature = JSON.stringify(userTools.map((t) => t.name).sort());
2174
- const needsRebuild = piSession == null || signature !== lastToolsSignature;
2175
- let resourcesReloaded = false;
2176
- if (needsRebuild) {
2177
- resourcesReloaded = await rebuildPiSession(userTools, piSession == null);
2178
- lastToolsSignature = signature;
2179
- }
2180
- if (!resourcesReloaded) {
2181
- await reloadResourcesOnly();
2182
- }
2183
- await syncHostWorkspaceFromSandbox({
2184
- sandbox,
2185
- sandboxWorkDir: input.sessionWorkDir,
2186
- hostWorkDir
2187
- });
2188
2403
  currentEmit = turnOpts.emit;
2189
- translatorState = createPiTranslatorState({
2190
- builtinToolNames: [...PI_NATIVE_BUILTIN_NAMES],
2191
- hostToolNames: userTools.map((tool2) => tool2.name),
2192
- nativeToCommon: NATIVE_TO_COMMON
2193
- });
2194
- turnOpts.emit({ type: "stream-start" });
2195
- const turnPromise = (async () => {
2196
- let terminalError;
2197
- const session = piSession;
2198
- const unsubErr = session.subscribe((raw) => {
2199
- const ev = parseNativeEvent(raw);
2200
- if (!ev) return;
2201
- const err = getPiTerminalError(ev);
2202
- if (err && !terminalError) {
2203
- terminalError = err;
2204
- }
2404
+ const turnAbortController = new AbortController();
2405
+ const abort = async (reason) => {
2406
+ if (turnAbortController.signal.aborted) return;
2407
+ if (reason === void 0) {
2408
+ turnAbortController.abort();
2409
+ } else {
2410
+ turnAbortController.abort(reason);
2411
+ }
2412
+ await Promise.resolve(piSession?.abort()).catch(() => {
2205
2413
  });
2414
+ };
2415
+ const turnPromise = (async () => {
2206
2416
  try {
2207
- await session.prompt(turnOpts.text);
2208
- if (terminalError) {
2209
- if (suspending && isAbortError(terminalError)) return;
2210
- currentEmit?.({ type: "error", error: new Error(terminalError) });
2211
- return;
2417
+ await applySessionInstructions(turnOpts.instructions);
2418
+ turnAbortController.signal.throwIfAborted();
2419
+ const didAppendDeliveredHostToolResults = appendDeliveredHostToolResults();
2420
+ const signature = JSON.stringify(userTools.map((t) => t.name).sort());
2421
+ const needsRebuild = piSession == null || signature !== lastToolsSignature;
2422
+ let resourcesReloaded = false;
2423
+ if (needsRebuild) {
2424
+ resourcesReloaded = await rebuildPiSession(
2425
+ userTools,
2426
+ piSession == null
2427
+ );
2428
+ turnAbortController.signal.throwIfAborted();
2429
+ lastToolsSignature = signature;
2212
2430
  }
2213
- const stats = session.getSessionStats();
2214
- const finishReason = {
2215
- unified: "stop",
2216
- raw: void 0
2217
- };
2218
- const usage = {
2219
- inputTokens: {
2220
- total: stats.tokens.input,
2221
- noCache: void 0,
2222
- cacheRead: stats.tokens.cacheRead,
2223
- cacheWrite: stats.tokens.cacheWrite
2224
- },
2225
- outputTokens: {
2226
- total: stats.tokens.output,
2227
- text: void 0,
2228
- reasoning: void 0
2431
+ if (!resourcesReloaded) {
2432
+ await reloadResourcesOnly();
2433
+ turnAbortController.signal.throwIfAborted();
2434
+ }
2435
+ await syncHostWorkspaceFromSandbox({
2436
+ sandbox,
2437
+ sandboxWorkDir: input.sessionWorkDir,
2438
+ hostWorkDir
2439
+ });
2440
+ turnAbortController.signal.throwIfAborted();
2441
+ translatorState = createPiTranslatorState({
2442
+ builtinToolNames: [...PI_NATIVE_BUILTIN_NAMES],
2443
+ hostToolNames: userTools.map((tool2) => tool2.name),
2444
+ nativeToCommon: NATIVE_TO_COMMON
2445
+ });
2446
+ currentEmit?.({ type: "stream-start" });
2447
+ if (didAppendDeliveredHostToolResults) {
2448
+ currentEmit?.({
2449
+ type: "finish-step",
2450
+ finishReason: { unified: "tool-calls", raw: void 0 },
2451
+ usage: {
2452
+ inputTokens: {
2453
+ total: 0,
2454
+ noCache: 0,
2455
+ cacheRead: 0,
2456
+ cacheWrite: 0
2457
+ },
2458
+ outputTokens: {
2459
+ total: 0,
2460
+ text: 0,
2461
+ reasoning: 0
2462
+ }
2463
+ },
2464
+ harnessMetadata: { pi: { inferredStep: true } }
2465
+ });
2466
+ }
2467
+ let terminalError;
2468
+ const session = piSession;
2469
+ const unsubErr = session.subscribe((raw) => {
2470
+ const ev = parseNativeEvent(raw);
2471
+ if (!ev) return;
2472
+ const err = getPiTerminalError(ev);
2473
+ if (err && !terminalError) {
2474
+ terminalError = err;
2229
2475
  }
2230
- };
2231
- currentEmit?.({
2232
- type: "finish",
2233
- finishReason,
2234
- totalUsage: usage
2235
2476
  });
2477
+ try {
2478
+ await session.prompt(turnOpts.text);
2479
+ if (terminalError) {
2480
+ if (suspending && isAbortError(terminalError)) return;
2481
+ currentEmit?.({ type: "error", error: new Error(terminalError) });
2482
+ return;
2483
+ }
2484
+ const stats = session.getSessionStats();
2485
+ const finishReason = {
2486
+ unified: "stop",
2487
+ raw: void 0
2488
+ };
2489
+ const usage = {
2490
+ inputTokens: {
2491
+ total: stats.tokens.input,
2492
+ noCache: void 0,
2493
+ cacheRead: stats.tokens.cacheRead,
2494
+ cacheWrite: stats.tokens.cacheWrite
2495
+ },
2496
+ outputTokens: {
2497
+ total: stats.tokens.output,
2498
+ text: void 0,
2499
+ reasoning: void 0
2500
+ }
2501
+ };
2502
+ currentEmit?.({
2503
+ type: "finish",
2504
+ finishReason,
2505
+ totalUsage: usage
2506
+ });
2507
+ } catch (err) {
2508
+ if (suspending && isAbortError(err)) return;
2509
+ currentEmit?.({ type: "error", error: err });
2510
+ } finally {
2511
+ unsubErr();
2512
+ }
2236
2513
  } catch (err) {
2237
2514
  if (suspending && isAbortError(err)) return;
2238
- currentEmit?.({ type: "error", error: err });
2239
- } finally {
2240
- unsubErr();
2515
+ throw err;
2241
2516
  }
2242
2517
  })();
2243
2518
  const activeTurnToken = {};
2244
2519
  const done = turnPromise.finally(() => {
2245
2520
  if (activeTurn?.token === activeTurnToken) {
2246
2521
  activeTurn = void 0;
2522
+ currentEmit = void 0;
2247
2523
  }
2248
- currentEmit = void 0;
2249
2524
  });
2250
2525
  activeTurn = {
2251
2526
  token: activeTurnToken,
2252
- done
2527
+ done,
2528
+ abort
2253
2529
  };
2254
2530
  return createPromptControl({
2255
2531
  done,
2256
- abortSignal: turnOpts.abortSignal
2532
+ abortSignal: turnOpts.abortSignal,
2533
+ abort
2257
2534
  });
2258
2535
  }
2259
2536
  const doStop = async () => {
@@ -2262,8 +2539,18 @@ async function createPiSession(input) {
2262
2539
  }
2263
2540
  stopped = true;
2264
2541
  parkedPiSessions.delete(input.sessionId);
2542
+ deferredRerun?.cancel();
2543
+ const turnToStop = activeTurn;
2544
+ const abortingTurn = turnToStop?.abort();
2265
2545
  settlePendingToolResults("Pi session stopped");
2266
2546
  settlePendingToolApprovals("Pi session stopped");
2547
+ await abortingTurn;
2548
+ await turnToStop?.done.catch(() => {
2549
+ });
2550
+ try {
2551
+ appendDeliveredHostToolResults();
2552
+ } catch {
2553
+ }
2267
2554
  if (sessionFileName) {
2268
2555
  try {
2269
2556
  await persistSessionFile();
@@ -2290,26 +2577,51 @@ async function createPiSession(input) {
2290
2577
  // only resume path is restoring the session file on a fresh/snapshotted
2291
2578
  // sandbox, i.e. `rerun`.
2292
2579
  doPromptTurn: async (promptOpts) => {
2293
- await applySessionInstructions(promptOpts.instructions);
2580
+ if (promptOpts.responseFormat?.type === "json") {
2581
+ throw new HarnessCapabilityUnsupportedError2({
2582
+ message: "Harness 'pi' does not support structured output.",
2583
+ harnessId: HARNESS_ID2
2584
+ });
2585
+ }
2294
2586
  return runTurn({
2295
2587
  text: extractUserText(promptOpts.prompt),
2296
2588
  tools: promptOpts.tools ?? [],
2589
+ instructions: promptOpts.instructions,
2297
2590
  emit: promptOpts.emit,
2298
2591
  abortSignal: promptOpts.abortSignal
2299
2592
  });
2300
2593
  },
2301
2594
  doContinueTurn: async (continueOpts) => {
2595
+ if (continueOpts.responseFormat?.type === "json") {
2596
+ throw new HarnessCapabilityUnsupportedError2({
2597
+ message: "Harness 'pi' does not support structured output.",
2598
+ harnessId: HARNESS_ID2
2599
+ });
2600
+ }
2302
2601
  if (activeTurn != null) {
2303
2602
  currentEmit = continueOpts.emit;
2304
2603
  return createPromptControl({
2305
2604
  done: activeTurn.done,
2306
- abortSignal: continueOpts.abortSignal
2605
+ abortSignal: continueOpts.abortSignal,
2606
+ abort: activeTurn.abort
2307
2607
  });
2308
2608
  }
2309
- await applySessionInstructions(continueOpts.instructions);
2609
+ if (stopped) {
2610
+ throw new Error("Pi session has been stopped.");
2611
+ }
2612
+ const danglingHostToolCalls = findDanglingHostToolCalls(
2613
+ continueOpts.tools ?? []
2614
+ );
2615
+ if (danglingHostToolCalls.length > 0) {
2616
+ return deferRerunUntilHostToolResults(
2617
+ danglingHostToolCalls,
2618
+ continueOpts
2619
+ );
2620
+ }
2310
2621
  return runTurn({
2311
2622
  text: "",
2312
2623
  tools: continueOpts.tools ?? [],
2624
+ instructions: continueOpts.instructions,
2313
2625
  emit: continueOpts.emit,
2314
2626
  abortSignal: continueOpts.abortSignal
2315
2627
  });
@@ -2324,8 +2636,14 @@ async function createPiSession(input) {
2324
2636
  if (stopped) return;
2325
2637
  stopped = true;
2326
2638
  parkedPiSessions.delete(input.sessionId);
2639
+ deferredRerun?.cancel();
2640
+ const turnToDestroy = activeTurn;
2641
+ const abortingTurn = turnToDestroy?.abort();
2327
2642
  settlePendingToolResults("Pi session stopped");
2328
2643
  settlePendingToolApprovals("Pi session stopped");
2644
+ await abortingTurn;
2645
+ await turnToDestroy?.done.catch(() => {
2646
+ });
2329
2647
  await disposePiSession();
2330
2648
  workspaceVfs.unmount();
2331
2649
  await rm2(hostRoot, { recursive: true, force: true });
@@ -2369,8 +2687,15 @@ async function createPiSession(input) {
2369
2687
  };
2370
2688
  }
2371
2689
  suspending = true;
2372
- await Promise.resolve(piSession?.abort()).catch(() => {
2690
+ const turnToSuspend = activeTurn;
2691
+ await turnToSuspend?.abort();
2692
+ deferredRerun?.cancel();
2693
+ await turnToSuspend?.done.catch(() => {
2373
2694
  });
2695
+ try {
2696
+ appendDeliveredHostToolResults();
2697
+ } catch {
2698
+ }
2374
2699
  if (sessionFileName) {
2375
2700
  try {
2376
2701
  await persistSessionFile();