@oh-my-pi/pi-coding-agent 14.7.0 → 14.7.2

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +12 -12
  3. package/src/cli/grep-cli.ts +1 -1
  4. package/src/config/model-equivalence.ts +1 -0
  5. package/src/config/model-registry.ts +108 -22
  6. package/src/config/settings-schema.ts +46 -1
  7. package/src/config/settings.ts +71 -1
  8. package/src/dap/client.ts +1 -0
  9. package/src/discovery/builtin.ts +34 -9
  10. package/src/discovery/helpers.ts +4 -3
  11. package/src/edit/index.ts +1 -0
  12. package/src/edit/modes/hashline.ts +212 -63
  13. package/src/eval/py/gateway-coordinator.ts +2 -3
  14. package/src/eval/py/runtime.ts +1 -0
  15. package/src/internal-urls/docs-index.generated.ts +2 -2
  16. package/src/lsp/index.ts +2 -0
  17. package/src/main.ts +10 -15
  18. package/src/mcp/discoverable-tool-metadata.ts +24 -202
  19. package/src/modes/components/extensions/extension-dashboard.ts +26 -2
  20. package/src/modes/components/extensions/state-manager.ts +41 -0
  21. package/src/modes/controllers/selector-controller.ts +3 -0
  22. package/src/modes/interactive-mode.ts +45 -13
  23. package/src/prompts/system/plan-mode-active.md +7 -3
  24. package/src/prompts/system/plan-mode-approved.md +5 -0
  25. package/src/prompts/tools/search-tool-bm25.md +14 -14
  26. package/src/prompts/tools/todo-write.md +1 -0
  27. package/src/sdk.ts +69 -8
  28. package/src/session/agent-session.ts +177 -1
  29. package/src/slash-commands/builtin-registry.ts +13 -2
  30. package/src/task/index.ts +2 -0
  31. package/src/task/isolation-backend.ts +22 -0
  32. package/src/tool-discovery/tool-index.ts +377 -0
  33. package/src/tools/ask.ts +2 -0
  34. package/src/tools/ast-edit.ts +2 -0
  35. package/src/tools/ast-grep.ts +2 -0
  36. package/src/tools/bash.ts +1 -0
  37. package/src/tools/browser.ts +2 -0
  38. package/src/tools/calculator.ts +2 -0
  39. package/src/tools/checkpoint.ts +4 -0
  40. package/src/tools/debug.ts +2 -0
  41. package/src/tools/eval.ts +2 -0
  42. package/src/tools/find.ts +2 -0
  43. package/src/tools/gh.ts +2 -0
  44. package/src/tools/hindsight-recall.ts +2 -0
  45. package/src/tools/hindsight-reflect.ts +2 -0
  46. package/src/tools/hindsight-retain.ts +2 -0
  47. package/src/tools/index.ts +74 -14
  48. package/src/tools/inspect-image.ts +2 -0
  49. package/src/tools/irc.ts +2 -1
  50. package/src/tools/job.ts +2 -1
  51. package/src/tools/notebook.ts +2 -0
  52. package/src/tools/read.ts +7 -1
  53. package/src/tools/recipe/index.ts +2 -0
  54. package/src/tools/render-mermaid.ts +2 -0
  55. package/src/tools/search-tool-bm25.ts +128 -42
  56. package/src/tools/search.ts +2 -0
  57. package/src/tools/ssh.ts +2 -0
  58. package/src/tools/todo-write.ts +2 -1
  59. package/src/tools/write.ts +2 -0
  60. package/src/web/search/index.ts +2 -0
  61. package/src/web/search/providers/searxng.ts +8 -0
package/src/sdk.ts CHANGED
@@ -81,7 +81,6 @@ import {
81
81
  collectDiscoverableMCPTools,
82
82
  formatDiscoverableMCPToolServerSummary,
83
83
  selectDiscoverableMCPToolNamesByServer,
84
- summarizeDiscoverableMCPTools,
85
84
  } from "./mcp/discoverable-tool-metadata";
86
85
  import { getMemoryRoot } from "./memories";
87
86
  import { resolveMemoryBackend } from "./memory-backend";
@@ -110,9 +109,15 @@ import {
110
109
  } from "./system-prompt";
111
110
  import { AgentOutputManager } from "./task/output-manager";
112
111
  import { parseThinkingLevel, resolveThinkingLevelForModel, toReasoningEffort } from "./thinking";
112
+ import {
113
+ collectDiscoverableTools,
114
+ type DiscoverableTool,
115
+ summarizeDiscoverableTools,
116
+ } from "./tool-discovery/tool-index";
113
117
  import {
114
118
  BashTool,
115
119
  BUILTIN_TOOLS,
120
+ computeEssentialBuiltinNames,
116
121
  createTools,
117
122
  discoverStartupLspServers,
118
123
  EditTool,
@@ -995,6 +1000,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
995
1000
  getDiscoverableMCPSearchIndex: () => session.getDiscoverableMCPSearchIndex(),
996
1001
  getSelectedMCPToolNames: () => session.getSelectedMCPToolNames(),
997
1002
  activateDiscoveredMCPTools: toolNames => session.activateDiscoveredMCPTools(toolNames),
1003
+ // Generic tool discovery (unified — covers built-in + MCP + extension)
1004
+ isToolDiscoveryEnabled: () => session.isToolDiscoveryEnabled(),
1005
+ getDiscoverableTools: filter => session.getDiscoverableTools(filter),
1006
+ getDiscoverableToolSearchIndex: () => session.getDiscoverableToolSearchIndex(),
1007
+ getSelectedDiscoveredToolNames: () => session.getSelectedDiscoveredToolNames(),
1008
+ activateDiscoveredTools: toolNames => session.activateDiscoveredTools(toolNames),
998
1009
  getCheckpointState: () => session.getCheckpointState(),
999
1010
  setCheckpointState: state => session.setCheckpointState(state ?? undefined),
1000
1011
  getToolChoiceQueue: () => session.toolChoiceQueue,
@@ -1344,11 +1355,33 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1344
1355
  ): Promise<BuildSystemPromptResult> => {
1345
1356
  toolContextStore.setToolNames(toolNames);
1346
1357
  const discoverableMCPTools = mcpDiscoveryEnabled ? collectDiscoverableMCPTools(tools.values()) : [];
1347
- const discoverableMCPSummary = summarizeDiscoverableMCPTools(discoverableMCPTools);
1348
- const hasDiscoverableMCPTools =
1349
- mcpDiscoveryEnabled && toolNames.includes("search_tool_bm25") && discoverableMCPTools.length > 0;
1358
+ const activeToolNames = new Set(toolNames);
1359
+ const discoverableBuiltinTools: DiscoverableTool[] =
1360
+ effectiveDiscoveryMode === "all"
1361
+ ? collectDiscoverableTools(
1362
+ Array.from(tools.values()).filter(
1363
+ tool => tool.loadMode === "discoverable" && !activeToolNames.has(tool.name),
1364
+ ),
1365
+ { source: "builtin" },
1366
+ )
1367
+ : [];
1368
+ const discoverableToolsForDesc: DiscoverableTool[] = [
1369
+ ...discoverableBuiltinTools,
1370
+ ...discoverableMCPTools.map(t => ({
1371
+ name: t.name,
1372
+ label: t.label,
1373
+ summary: t.description,
1374
+ source: "mcp" as const,
1375
+ serverName: t.serverName,
1376
+ mcpToolName: t.mcpToolName,
1377
+ schemaKeys: t.schemaKeys,
1378
+ })),
1379
+ ];
1380
+ const discoverableToolSummary = summarizeDiscoverableTools(discoverableToolsForDesc);
1381
+ const hasDiscoverableTools =
1382
+ mcpDiscoveryEnabled && toolNames.includes("search_tool_bm25") && discoverableToolsForDesc.length > 0;
1350
1383
  const promptTools = buildSystemPromptToolMetadata(tools, {
1351
- search_tool_bm25: { description: renderSearchToolBm25Description(discoverableMCPTools) },
1384
+ search_tool_bm25: { description: renderSearchToolBm25Description(discoverableToolsForDesc) },
1352
1385
  });
1353
1386
  const memoryInstructions = await resolveMemoryBackend(settings).buildDeveloperInstructions(
1354
1387
  agentDir,
@@ -1386,8 +1419,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1386
1419
  appendSystemPrompt: appendPrompt,
1387
1420
  repeatToolDescriptions,
1388
1421
  intentField,
1389
- mcpDiscoveryMode: hasDiscoverableMCPTools,
1390
- mcpDiscoveryServerSummaries: discoverableMCPSummary.servers.map(formatDiscoverableMCPToolServerSummary),
1422
+ mcpDiscoveryMode: hasDiscoverableTools,
1423
+ mcpDiscoveryServerSummaries: discoverableToolSummary.servers.map(formatDiscoverableMCPToolServerSummary),
1391
1424
  eagerTasks,
1392
1425
  secretsEnabled,
1393
1426
  agentsMdSearch: agentsMdSearchPromise,
@@ -1411,7 +1444,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1411
1444
  toolNamesFromRegistry;
1412
1445
  const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
1413
1446
  const includeExitPlanMode = requestedToolNames.includes("exit_plan_mode");
1414
- const mcpDiscoveryEnabled = settings.get("mcp.discoveryMode") ?? false;
1447
+ // Effective discovery mode: tools.discoveryMode takes precedence; mcp.discoveryMode is back-compat alias.
1448
+ const toolsDiscoveryModeSetting = settings.get("tools.discoveryMode");
1449
+ const effectiveDiscoveryMode: "off" | "mcp-only" | "all" =
1450
+ toolsDiscoveryModeSetting !== "off"
1451
+ ? (toolsDiscoveryModeSetting as "off" | "mcp-only" | "all")
1452
+ : settings.get("mcp.discoveryMode")
1453
+ ? "mcp-only"
1454
+ : "off";
1455
+ const mcpDiscoveryEnabled = effectiveDiscoveryMode !== "off"; // back-compat: true when any discovery active
1415
1456
  const defaultInactiveToolNames = new Set(
1416
1457
  registeredTools.filter(tool => tool.definition.defaultInactive).map(tool => tool.definition.name),
1417
1458
  );
@@ -1468,6 +1509,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1468
1509
  }
1469
1510
  }
1470
1511
 
1512
+ // When tools.discoveryMode === "all", hide non-essential built-in discoverable tools
1513
+ // from the initial set unless they were explicitly requested or restored from persistence.
1514
+ // The model finds them via search_tool_bm25 and activates them on demand.
1515
+ if (effectiveDiscoveryMode === "all") {
1516
+ const essentialBuiltinNames = new Set(computeEssentialBuiltinNames(settings));
1517
+ const explicitlyRequestedToolNames = new Set(options.toolNames?.map(name => name.toLowerCase()) ?? []);
1518
+ // Back-compat: persisted activations live under selectedMCPToolNames today (built-in
1519
+ // activation persistence is a follow-up). MCP names won't collide with built-in names.
1520
+ const restoredDiscoveredNames = new Set(existingSession.selectedMCPToolNames);
1521
+ initialToolNames = initialToolNames.filter(name => {
1522
+ const tool = toolRegistry.get(name);
1523
+ if (!tool?.loadMode) return true; // not a built-in — leave MCP/custom/extension to existing logic
1524
+ if (tool.loadMode === "essential") return true;
1525
+ if (essentialBuiltinNames.has(name)) return true;
1526
+ if (explicitlyRequestedToolNames.has(name)) return true;
1527
+ if (restoredDiscoveredNames.has(name)) return true;
1528
+ return false;
1529
+ });
1530
+ }
1531
+
1471
1532
  const { systemPrompt } = await logger.time(
1472
1533
  "buildSystemPrompt",
1473
1534
  rebuildSystemPrompt,
@@ -129,6 +129,12 @@ import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { t
129
129
  import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
130
130
  import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
131
131
  import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
132
+ import {
133
+ buildDiscoverableToolSearchIndex,
134
+ collectDiscoverableTools,
135
+ type DiscoverableTool,
136
+ type DiscoverableToolSearchIndex,
137
+ } from "../tool-discovery/tool-index";
132
138
  import { assertEditableFile } from "../tools/auto-generated-guard";
133
139
  import type { CheckpointState } from "../tools/checkpoint";
134
140
  import { outputMeta } from "../tools/output-meta";
@@ -536,6 +542,9 @@ export class AgentSession {
536
542
  #discoverableMCPTools = new Map<string, DiscoverableMCPTool>();
537
543
  #discoverableMCPSearchIndex: DiscoverableMCPSearchIndex | null = null;
538
544
  #selectedMCPToolNames = new Set<string>();
545
+ // Generic tool discovery (covers built-in + MCP + extension when tools.discoveryMode === "all")
546
+ #discoverableToolSearchIndex: DiscoverableToolSearchIndex | null = null;
547
+ #selectedDiscoveredToolNames = new Set<string>();
539
548
  #rpcHostToolNames = new Set<string>();
540
549
  #defaultSelectedMCPServerNames = new Set<string>();
541
550
  #defaultSelectedMCPToolNames = new Set<string>();
@@ -2101,7 +2110,15 @@ export class AgentSession {
2101
2110
 
2102
2111
  #setDiscoverableMCPTools(discoverableMCPTools: Map<string, DiscoverableMCPTool>): void {
2103
2112
  this.#discoverableMCPTools = discoverableMCPTools;
2113
+ this.#invalidateDiscoveryCaches();
2114
+ }
2115
+
2116
+ /** Single point for invalidating cached discovery indices. Call after any change that can
2117
+ * affect which tools should be discoverable: registry mutations (refreshMCPTools,
2118
+ * refreshRpcHostTools) or active-tool mutations (#applyActiveToolsByName). */
2119
+ #invalidateDiscoveryCaches(): void {
2104
2120
  this.#discoverableMCPSearchIndex = null;
2121
+ this.#discoverableToolSearchIndex = null;
2105
2122
  }
2106
2123
 
2107
2124
  #filterSelectableMCPToolNames(toolNames: Iterable<string>): string[] {
@@ -2204,10 +2221,21 @@ export class AgentSession {
2204
2221
  return this.#mcpDiscoveryEnabled;
2205
2222
  }
2206
2223
 
2224
+ /** @deprecated Use {@link getDiscoverableTools} with `{ source: "mcp" }` instead.
2225
+ * Preserves the legacy `description`-bearing MCP shape for back-compat callers. */
2207
2226
  getDiscoverableMCPTools(): DiscoverableMCPTool[] {
2208
- return Array.from(this.#discoverableMCPTools.values());
2227
+ return Array.from(this.#discoverableMCPTools.values()).map(t => ({
2228
+ name: t.name,
2229
+ label: t.label,
2230
+ description: t.description,
2231
+ serverName: t.serverName,
2232
+ mcpToolName: t.mcpToolName,
2233
+ schemaKeys: t.schemaKeys,
2234
+ }));
2209
2235
  }
2210
2236
 
2237
+ /** @deprecated Use {@link getDiscoverableToolSearchIndex} instead.
2238
+ * Returns the legacy MCP search index whose documents expose `tool.description`. */
2211
2239
  getDiscoverableMCPSearchIndex(): DiscoverableMCPSearchIndex {
2212
2240
  if (!this.#discoverableMCPSearchIndex) {
2213
2241
  this.#discoverableMCPSearchIndex = buildDiscoverableMCPSearchIndex(this.#discoverableMCPTools.values());
@@ -2243,6 +2271,113 @@ export class AgentSession {
2243
2271
  return [...new Set(activated)];
2244
2272
  }
2245
2273
 
2274
+ // ── Generic tool discovery (covers built-in + MCP + extension) ────────────
2275
+
2276
+ /** Resolve effective discovery mode: tools.discoveryMode wins; mcp.discoveryMode is back-compat alias. */
2277
+ #resolveEffectiveDiscoveryMode(): "off" | "mcp-only" | "all" {
2278
+ const toolsMode = this.settings.get("tools.discoveryMode");
2279
+ if (toolsMode !== "off") return toolsMode as "off" | "mcp-only" | "all";
2280
+ if (this.settings.get("mcp.discoveryMode")) return "mcp-only";
2281
+ return "off";
2282
+ }
2283
+
2284
+ isToolDiscoveryEnabled(): boolean {
2285
+ return this.#resolveEffectiveDiscoveryMode() !== "off";
2286
+ }
2287
+
2288
+ getDiscoverableTools(filter?: { source?: DiscoverableTool["source"] }): DiscoverableTool[] {
2289
+ // For "all" mode we combine built-in registry entries + MCP tools.
2290
+ // For "mcp-only" mode we only return MCP tools.
2291
+ const mode = this.#resolveEffectiveDiscoveryMode();
2292
+ const activeNames = new Set(this.getActiveToolNames());
2293
+ const mcpTools: DiscoverableTool[] = Array.from(this.#discoverableMCPTools.values())
2294
+ .filter(t => !activeNames.has(t.name))
2295
+ .map(t => ({
2296
+ name: t.name,
2297
+ label: t.label,
2298
+ summary: t.description,
2299
+ source: "mcp" as const,
2300
+ serverName: t.serverName,
2301
+ mcpToolName: t.mcpToolName,
2302
+ schemaKeys: t.schemaKeys,
2303
+ }));
2304
+ const builtinTools: DiscoverableTool[] = mode === "all" ? this.#collectDiscoverableBuiltinTools() : [];
2305
+ const allTools = [...builtinTools, ...mcpTools];
2306
+ return filter?.source ? allTools.filter(t => t.source === filter.source) : allTools;
2307
+ }
2308
+
2309
+ /** Collect built-in tools the model can discover via search_tool_bm25. Restricted to tool
2310
+ * definitions whose `loadMode === "discoverable"`. This keeps hidden/internal tools
2311
+ * (resolve, yield, exit_plan_mode, report_finding, report_tool_issue) out of the index
2312
+ * and avoids mislabeling extension/custom default-inactive tools as built-ins. */
2313
+ #collectDiscoverableBuiltinTools(): DiscoverableTool[] {
2314
+ const activeNames = new Set(this.getActiveToolNames());
2315
+ const result: DiscoverableTool[] = [];
2316
+ for (const tool of this.#toolRegistry.values()) {
2317
+ if (tool.loadMode !== "discoverable") continue;
2318
+ if (activeNames.has(tool.name)) continue;
2319
+ const collected = collectDiscoverableTools([tool], { source: "builtin" });
2320
+ result.push(...collected);
2321
+ }
2322
+ return result;
2323
+ }
2324
+
2325
+ getDiscoverableToolSearchIndex(): DiscoverableToolSearchIndex {
2326
+ if (!this.#discoverableToolSearchIndex) {
2327
+ this.#discoverableToolSearchIndex = buildDiscoverableToolSearchIndex(this.getDiscoverableTools());
2328
+ }
2329
+ return this.#discoverableToolSearchIndex;
2330
+ }
2331
+
2332
+ /** Invalidate the generic search index cache (call after tool set changes).
2333
+ * Delegates to {@link #invalidateDiscoveryCaches} so all discovery-related caches stay in sync. */
2334
+ #invalidateDiscoverableToolSearchIndex(): void {
2335
+ this.#invalidateDiscoveryCaches();
2336
+ }
2337
+
2338
+ getSelectedDiscoveredToolNames(): string[] {
2339
+ // Union of MCP-selected and generic non-MCP selected. Non-MCP selections are only
2340
+ // selected while they are still active; otherwise BM25 must be able to rediscover them.
2341
+ const activeNames = new Set(this.getActiveToolNames());
2342
+ const mcpSelected = this.getSelectedMCPToolNames();
2343
+ const nonMcpSelected = Array.from(this.#selectedDiscoveredToolNames).filter(
2344
+ name => activeNames.has(name) && this.#toolRegistry.has(name) && !isMCPToolName(name),
2345
+ );
2346
+ return [...new Set([...mcpSelected, ...nonMcpSelected])];
2347
+ }
2348
+
2349
+ async activateDiscoveredTools(toolNames: string[]): Promise<string[]> {
2350
+ const mcpNames = toolNames.filter(isMCPToolName);
2351
+ const nonMcpNames = toolNames.filter(name => !isMCPToolName(name));
2352
+ const activated: string[] = [];
2353
+
2354
+ // Activate MCP tools via existing path
2355
+ if (mcpNames.length > 0) {
2356
+ const activatedMcp = await this.activateDiscoveredMCPTools(mcpNames);
2357
+ activated.push(...activatedMcp);
2358
+ }
2359
+
2360
+ // Activate non-MCP tools (built-ins that are in the registry but not currently active)
2361
+ if (nonMcpNames.length > 0) {
2362
+ const currentActiveNames = new Set(this.getActiveToolNames());
2363
+ const newlyAdded: string[] = [];
2364
+ for (const name of nonMcpNames) {
2365
+ if (this.#toolRegistry.has(name) && !currentActiveNames.has(name)) {
2366
+ newlyAdded.push(name);
2367
+ this.#selectedDiscoveredToolNames.add(name);
2368
+ activated.push(name);
2369
+ }
2370
+ }
2371
+ if (newlyAdded.length > 0) {
2372
+ const nextActive = [...this.getActiveToolNames(), ...newlyAdded];
2373
+ await this.setActiveToolsByName(nextActive);
2374
+ this.#invalidateDiscoverableToolSearchIndex();
2375
+ }
2376
+ }
2377
+
2378
+ return [...new Set(activated)];
2379
+ }
2380
+
2246
2381
  async #applyActiveToolsByName(
2247
2382
  toolNames: string[],
2248
2383
  options?: { persistMCPSelection?: boolean; previousSelectedMCPToolNames?: string[] },
@@ -2273,8 +2408,18 @@ export class AgentSession {
2273
2408
  ),
2274
2409
  );
2275
2410
  }
2411
+ const activeNameSet = new Set(validToolNames);
2412
+ for (const name of Array.from(this.#selectedDiscoveredToolNames)) {
2413
+ if (!activeNameSet.has(name) || isMCPToolName(name) || !this.#toolRegistry.has(name)) {
2414
+ this.#selectedDiscoveredToolNames.delete(name);
2415
+ }
2416
+ }
2276
2417
  this.agent.setTools(tools);
2277
2418
 
2419
+ // Active tool set changed → discoverable tool list (which excludes already-active tools)
2420
+ // is now stale. Invalidate before any prompt-template hook reads the discovery list.
2421
+ this.#invalidateDiscoveryCaches();
2422
+
2278
2423
  // Rebuild base system prompt with new tool set, but only when the tool set
2279
2424
  // actually changed. MCP servers can reconnect at arbitrary times and call
2280
2425
  // `refreshMCPTools` -> `#applyActiveToolsByName` even though the resulting
@@ -2509,6 +2654,11 @@ export class AgentSession {
2509
2654
  this.#rpcHostToolNames.add(finalTool.name);
2510
2655
  }
2511
2656
 
2657
+ // Registry contents changed — invalidate discovery caches so the next BM25 lookup sees
2658
+ // the new RPC-host tool set. (#applyActiveToolsByName below also invalidates, but doing
2659
+ // it here too keeps the contract local to "registry mutated".)
2660
+ this.#invalidateDiscoveryCaches();
2661
+
2512
2662
  const activeNonRpcToolNames = previousActiveToolNames.filter(name => !previousRpcHostToolNames.has(name));
2513
2663
  const preservedRpcToolNames = previousActiveToolNames.filter(
2514
2664
  name => previousRpcHostToolNames.has(name) && this.#rpcHostToolNames.has(name),
@@ -5983,6 +6133,32 @@ export class AgentSession {
5983
6133
  setAutoRetryEnabled(enabled: boolean): void {
5984
6134
  this.settings.set("retry.enabled", enabled);
5985
6135
  }
6136
+ /**
6137
+ * Manually retry the last failed assistant turn.
6138
+ * Removes the error message from agent state and re-attempts with a fresh retry budget.
6139
+ * @returns true if retry was initiated, false if no failed turn to retry or agent is busy
6140
+ */
6141
+ async retry(): Promise<boolean> {
6142
+ if (this.isStreaming || this.isCompacting || this.isRetrying) return false;
6143
+
6144
+ const messages = this.agent.state.messages;
6145
+ const lastMsg = messages[messages.length - 1];
6146
+ if (lastMsg?.role !== "assistant") return false;
6147
+
6148
+ const assistantMsg = lastMsg as AssistantMessage;
6149
+ if (assistantMsg.stopReason !== "error" && assistantMsg.stopReason !== "aborted") return false;
6150
+
6151
+ // Remove the failed/aborted assistant message (same as auto-retry does before re-attempting)
6152
+ this.agent.replaceMessages(messages.slice(0, -1));
6153
+
6154
+ // Reset retry budget for a fresh attempt
6155
+ this.#retryAttempt = 0;
6156
+
6157
+ // Re-attempt the turn
6158
+ this.#scheduleAgentContinue({ delayMs: 1 });
6159
+
6160
+ return true;
6161
+ }
5986
6162
 
5987
6163
  // =========================================================================
5988
6164
  // Bash Execution
@@ -57,8 +57,8 @@ interface BuiltinSlashCommandSpec extends BuiltinSlashCommand {
57
57
  handle: (
58
58
  command: ParsedBuiltinSlashCommand,
59
59
  runtime: BuiltinSlashCommandRuntime,
60
- // biome-ignore lint/suspicious/noConfusingVoidType: void needed so handlers returning nothing are assignable
61
- ) => Promise<string | undefined> | string | void;
60
+ // biome-ignore lint/suspicious/noConfusingVoidType: void needed so async handlers returning nothing are assignable
61
+ ) => Promise<string | void> | string | void;
62
62
  }
63
63
 
64
64
  export interface BuiltinSlashCommandRuntime {
@@ -572,6 +572,17 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
572
572
  await runtime.ctx.handleBtwCommand(question);
573
573
  },
574
574
  },
575
+ {
576
+ name: "retry",
577
+ description: "Retry the last failed agent turn",
578
+ handle: async (_command, runtime) => {
579
+ const didRetry = await runtime.ctx.session.retry();
580
+ if (!didRetry) {
581
+ runtime.ctx.showStatus("Nothing to retry");
582
+ }
583
+ runtime.ctx.editor.setText("");
584
+ },
585
+ },
575
586
  {
576
587
  name: "background",
577
588
  aliases: ["bg"],
package/src/task/index.ts CHANGED
@@ -195,7 +195,9 @@ function validateTaskModeParams(simpleMode: TaskSimpleMode, params: TaskParams):
195
195
  export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
196
196
  readonly name = "task";
197
197
  readonly label = "Task";
198
+ readonly summary = "Spawn a subagent to complete a parallel task";
198
199
  readonly strict = true;
200
+ readonly loadMode = "discoverable";
199
201
  readonly renderResult = renderResult;
200
202
  readonly #discoveredAgents: AgentDefinition[];
201
203
  readonly #blockedAgent: string | undefined;
@@ -9,11 +9,26 @@ export interface IsolationBackendResolution {
9
9
  warning: string;
10
10
  }
11
11
 
12
+ type ProcessorEnv = Partial<Pick<NodeJS.ProcessEnv, "PROCESSOR_ARCHITECTURE" | "PROCESSOR_ARCHITEW6432">>;
13
+
14
+ function isWindowsArm64HostUnderX64Emulation(
15
+ platform: NodeJS.Platform,
16
+ arch: NodeJS.Architecture,
17
+ env: ProcessorEnv,
18
+ ): boolean {
19
+ if (platform !== "win32" || arch !== "x64") return false;
20
+ return (
21
+ env.PROCESSOR_ARCHITECTURE?.toUpperCase() === "ARM64" || env.PROCESSOR_ARCHITEW6432?.toUpperCase() === "ARM64"
22
+ );
23
+ }
24
+
12
25
  export async function resolveIsolationBackendForTaskExecution(
13
26
  requestedMode: TaskIsolationMode,
14
27
  isIsolated: boolean,
15
28
  repoRoot: string | null,
16
29
  platform: NodeJS.Platform = process.platform,
30
+ arch: NodeJS.Architecture = process.arch,
31
+ env: ProcessorEnv = process.env as ProcessorEnv,
17
32
  ): Promise<IsolationBackendResolution> {
18
33
  let effectiveIsolationMode = requestedMode;
19
34
  let warning = "";
@@ -39,6 +54,13 @@ export async function resolveIsolationBackendForTaskExecution(
39
54
  return { effectiveIsolationMode, warning };
40
55
  }
41
56
 
57
+ if (isWindowsArm64HostUnderX64Emulation(platform, arch, env)) {
58
+ effectiveIsolationMode = "worktree";
59
+ warning =
60
+ "<system-notification>ProjFS isolation is disabled on Windows ARM64 x64 emulation. Falling back to worktree isolation.</system-notification>";
61
+ return { effectiveIsolationMode, warning };
62
+ }
63
+
42
64
  const probe = projfsOverlayProbe();
43
65
  if (!probe.available) {
44
66
  effectiveIsolationMode = "worktree";