@ai-sdk/harness-pi 1.0.72 → 1.0.74

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