@nghyane/arcane 0.1.13 → 0.1.15

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 (303) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/package.json +21 -70
  3. package/scripts/format-prompts.ts +1 -3
  4. package/src/cli/args.ts +2 -7
  5. package/src/cli/config-cli.ts +1 -1
  6. package/src/cli/plugin-cli.ts +1 -1
  7. package/src/cli/setup-cli.ts +1 -1
  8. package/src/cli/update-cli.ts +1 -1
  9. package/src/cli/web-search-cli.ts +1 -1
  10. package/src/cli.ts +0 -1
  11. package/src/commands/config.ts +1 -1
  12. package/src/commands/grep.ts +1 -1
  13. package/src/commands/jupyter.ts +1 -1
  14. package/src/commands/plugin.ts +1 -1
  15. package/src/commands/setup.ts +1 -1
  16. package/src/commands/shell.ts +1 -1
  17. package/src/commands/ssh.ts +1 -1
  18. package/src/commands/stats.ts +1 -1
  19. package/src/commands/update.ts +1 -1
  20. package/src/config/model-registry.ts +3 -4
  21. package/src/config/model-resolver.ts +36 -9
  22. package/src/config/prompt-templates.ts +1 -9
  23. package/src/config/settings-schema.ts +32 -88
  24. package/src/config/settings.ts +3 -4
  25. package/src/debug/index.ts +1 -1
  26. package/src/debug/log-formatting.ts +1 -1
  27. package/src/debug/log-viewer.ts +2 -2
  28. package/src/discovery/helpers.ts +13 -3
  29. package/src/exa/index.ts +1 -35
  30. package/src/exa/render.ts +30 -190
  31. package/src/export/html/index.ts +1 -1
  32. package/src/extensibility/custom-tools/loader.ts +1 -1
  33. package/src/extensibility/custom-tools/types.ts +5 -1
  34. package/src/extensibility/custom-tools/wrapper.ts +1 -1
  35. package/src/extensibility/extensions/runner.ts +1 -1
  36. package/src/extensibility/extensions/types.ts +1 -1
  37. package/src/extensibility/extensions/wrapper.ts +7 -15
  38. package/src/extensibility/hooks/runner.ts +1 -1
  39. package/src/extensibility/hooks/types.ts +1 -1
  40. package/src/extensibility/plugins/doctor.ts +1 -1
  41. package/src/index.ts +13 -13
  42. package/src/lsp/index.ts +77 -24
  43. package/src/lsp/render.ts +34 -583
  44. package/src/lsp/types.ts +3 -3
  45. package/src/lsp/utils.ts +1 -1
  46. package/src/main.ts +1 -1
  47. package/src/mcp/tool-bridge.ts +1 -24
  48. package/src/modes/components/assistant-message.ts +7 -7
  49. package/src/modes/components/bash-execution.ts +50 -112
  50. package/src/modes/components/bordered-loader.ts +1 -1
  51. package/src/modes/components/branch-summary-message.ts +16 -10
  52. package/src/modes/components/compaction-summary-message.ts +20 -12
  53. package/src/modes/components/context-group.ts +106 -0
  54. package/src/modes/components/custom-message.ts +4 -5
  55. package/src/modes/components/diff.ts +2 -2
  56. package/src/modes/components/dynamic-border.ts +1 -1
  57. package/src/modes/components/extensions/extension-dashboard.ts +1 -1
  58. package/src/modes/components/extensions/extension-list.ts +1 -1
  59. package/src/modes/components/extensions/inspector-panel.ts +1 -1
  60. package/src/modes/components/footer.ts +2 -2
  61. package/src/modes/components/history-search.ts +1 -1
  62. package/src/modes/components/hook-editor.ts +1 -1
  63. package/src/modes/components/hook-input.ts +1 -1
  64. package/src/modes/components/hook-message.ts +4 -5
  65. package/src/modes/components/hook-selector.ts +1 -1
  66. package/src/modes/components/index.ts +0 -2
  67. package/src/modes/components/keybinding-hints.ts +1 -1
  68. package/src/modes/components/login-dialog.ts +1 -1
  69. package/src/modes/components/mcp-add-wizard.ts +1 -1
  70. package/src/modes/components/model-selector.ts +1 -1
  71. package/src/modes/components/oauth-selector.ts +1 -1
  72. package/src/modes/components/plugin-settings.ts +1 -1
  73. package/src/modes/components/python-execution.ts +51 -91
  74. package/src/modes/components/queue-mode-selector.ts +1 -1
  75. package/src/modes/components/session-selector.ts +1 -1
  76. package/src/modes/components/settings-defs.ts +5 -10
  77. package/src/modes/components/settings-selector.ts +1 -1
  78. package/src/modes/components/show-images-selector.ts +1 -1
  79. package/src/modes/components/skill-message.ts +4 -4
  80. package/src/modes/components/status-line/segments.ts +2 -2
  81. package/src/modes/components/status-line/separators.ts +1 -1
  82. package/src/modes/components/status-line-segment-editor.ts +1 -1
  83. package/src/modes/components/status-line.ts +1 -1
  84. package/src/modes/components/theme-selector.ts +1 -1
  85. package/src/modes/components/thinking-selector.ts +1 -1
  86. package/src/modes/components/todo-display.ts +2 -4
  87. package/src/modes/components/todo-reminder.ts +4 -4
  88. package/src/modes/components/tool-execution.ts +118 -440
  89. package/src/modes/components/tool-image-display.ts +107 -0
  90. package/src/modes/components/tree-selector.ts +2 -2
  91. package/src/modes/components/ttsr-notification.ts +4 -17
  92. package/src/modes/components/user-message-selector.ts +1 -1
  93. package/src/modes/components/user-message.ts +9 -10
  94. package/src/modes/components/welcome.ts +1 -1
  95. package/src/modes/controllers/command-controller.ts +1 -1
  96. package/src/modes/controllers/event-controller.ts +58 -187
  97. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  98. package/src/modes/controllers/input-controller.ts +3 -1
  99. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  100. package/src/modes/controllers/selector-controller.ts +3 -26
  101. package/src/modes/controllers/ssh-command-controller.ts +1 -1
  102. package/src/modes/interactive-mode.ts +3 -7
  103. package/src/modes/print-mode.ts +5 -5
  104. package/src/modes/rpc/rpc-mode.ts +1 -1
  105. package/src/modes/types.ts +1 -2
  106. package/src/modes/utils/ui-helpers.ts +34 -32
  107. package/src/patch/edit-tool.ts +742 -0
  108. package/src/patch/index.ts +32 -898
  109. package/src/patch/schemas.ts +208 -0
  110. package/src/patch/shared.ts +83 -151
  111. package/src/prompts/agents/explore.md +22 -37
  112. package/src/prompts/agents/init.md +1 -1
  113. package/src/prompts/agents/librarian.md +29 -20
  114. package/src/prompts/agents/oracle.md +9 -2
  115. package/src/prompts/agents/reviewer.md +14 -48
  116. package/src/prompts/agents/task.md +16 -8
  117. package/src/prompts/compaction/branch-summary.md +4 -1
  118. package/src/prompts/compaction/compaction-summary.md +4 -1
  119. package/src/prompts/system/subagent-system-prompt.md +1 -1
  120. package/src/prompts/system/system-prompt.md +162 -178
  121. package/src/prompts/system/verification-reminder.md +6 -0
  122. package/src/sdk.ts +0 -9
  123. package/src/session/agent-session.ts +244 -1459
  124. package/src/session/model-controller.ts +406 -0
  125. package/src/session/retry-utils.ts +71 -0
  126. package/src/session/session-manager.ts +22 -186
  127. package/src/session/session-types.ts +312 -0
  128. package/src/session/stats.ts +387 -0
  129. package/src/session/streaming-edit.ts +258 -0
  130. package/src/session/ttsr.ts +213 -0
  131. package/src/slash-commands/builtin-registry.ts +0 -8
  132. package/src/stt/recorder.ts +2 -2
  133. package/src/system-prompt.ts +1 -14
  134. package/src/task/agents.ts +7 -33
  135. package/src/task/executor.ts +50 -438
  136. package/src/task/index.ts +104 -71
  137. package/src/task/progress-tracker.ts +390 -0
  138. package/src/task/render.ts +371 -187
  139. package/src/task/subprocess-tool-registry.ts +1 -1
  140. package/src/task/types.ts +14 -47
  141. package/src/tools/ask.ts +31 -42
  142. package/src/tools/bash-interactive.ts +2 -2
  143. package/src/tools/bash-interceptor.ts +2 -2
  144. package/src/tools/bash-normalize.ts +1 -1
  145. package/src/tools/bash-skill-urls.ts +2 -2
  146. package/src/tools/bash.ts +87 -136
  147. package/src/tools/browser.ts +54 -84
  148. package/src/tools/create-tools.ts +186 -0
  149. package/src/tools/default-renderer.ts +104 -0
  150. package/src/tools/explore.ts +11 -10
  151. package/src/tools/fetch.ts +24 -114
  152. package/src/tools/find.ts +48 -132
  153. package/src/tools/gemini-image.ts +5 -15
  154. package/src/tools/github.ts +450 -0
  155. package/src/tools/grep.ts +43 -179
  156. package/src/tools/index.ts +35 -198
  157. package/src/tools/json-tree.ts +3 -3
  158. package/src/tools/librarian.ts +18 -18
  159. package/src/tools/list-limit.ts +2 -2
  160. package/src/tools/notebook.ts +35 -87
  161. package/src/tools/oracle.ts +25 -25
  162. package/src/tools/output-meta.ts +89 -4
  163. package/src/tools/output-utils.ts +2 -2
  164. package/src/tools/python.ts +86 -637
  165. package/src/tools/read.ts +36 -119
  166. package/src/tools/reviewer-tool.ts +19 -21
  167. package/src/tools/search-code.ts +128 -0
  168. package/src/tools/ssh.ts +67 -126
  169. package/src/tools/subagent-tool.ts +197 -123
  170. package/src/tools/todo-write.ts +15 -31
  171. package/src/tools/tool-errors.ts +0 -30
  172. package/src/tools/undo-edit.ts +30 -67
  173. package/src/tools/write.ts +78 -127
  174. package/src/tui/code-cell.ts +4 -4
  175. package/src/tui/file-list.ts +2 -2
  176. package/src/tui/output-block.ts +1 -1
  177. package/src/tui/status-line.ts +1 -1
  178. package/src/tui/tree-list.ts +2 -2
  179. package/src/tui/types.ts +1 -1
  180. package/src/tui/utils.ts +1 -1
  181. package/src/{tools → ui}/render-utils.ts +87 -126
  182. package/src/utils/external-editor.ts +4 -4
  183. package/src/utils/file-mentions.ts +1 -1
  184. package/src/utils/index.ts +30 -0
  185. package/src/utils/tools-manager.ts +9 -19
  186. package/src/web/github-client.ts +290 -0
  187. package/src/web/scrapers/github.ts +11 -62
  188. package/src/web/search/auth.ts +1 -3
  189. package/src/web/search/index.ts +82 -46
  190. package/src/web/search/provider.ts +11 -16
  191. package/src/web/search/providers/grep.ts +160 -0
  192. package/src/web/search/render.ts +48 -235
  193. package/src/web/search/types.ts +1 -1
  194. package/src/commands/commit.ts +0 -36
  195. package/src/commit/agentic/agent.ts +0 -311
  196. package/src/commit/agentic/fallback.ts +0 -96
  197. package/src/commit/agentic/index.ts +0 -359
  198. package/src/commit/agentic/prompts/analyze-file.md +0 -22
  199. package/src/commit/agentic/prompts/session-user.md +0 -25
  200. package/src/commit/agentic/prompts/split-confirm.md +0 -1
  201. package/src/commit/agentic/prompts/system.md +0 -38
  202. package/src/commit/agentic/state.ts +0 -69
  203. package/src/commit/agentic/tools/analyze-file.ts +0 -118
  204. package/src/commit/agentic/tools/git-file-diff.ts +0 -194
  205. package/src/commit/agentic/tools/git-hunk.ts +0 -50
  206. package/src/commit/agentic/tools/git-overview.ts +0 -84
  207. package/src/commit/agentic/tools/index.ts +0 -56
  208. package/src/commit/agentic/tools/propose-changelog.ts +0 -128
  209. package/src/commit/agentic/tools/propose-commit.ts +0 -154
  210. package/src/commit/agentic/tools/recent-commits.ts +0 -81
  211. package/src/commit/agentic/tools/split-commit.ts +0 -280
  212. package/src/commit/agentic/topo-sort.ts +0 -44
  213. package/src/commit/agentic/trivial.ts +0 -51
  214. package/src/commit/agentic/validation.ts +0 -200
  215. package/src/commit/analysis/conventional.ts +0 -165
  216. package/src/commit/analysis/index.ts +0 -4
  217. package/src/commit/analysis/scope.ts +0 -242
  218. package/src/commit/analysis/summary.ts +0 -112
  219. package/src/commit/analysis/validation.ts +0 -66
  220. package/src/commit/changelog/detect.ts +0 -37
  221. package/src/commit/changelog/generate.ts +0 -110
  222. package/src/commit/changelog/index.ts +0 -234
  223. package/src/commit/changelog/parse.ts +0 -44
  224. package/src/commit/cli.ts +0 -93
  225. package/src/commit/git/diff.ts +0 -148
  226. package/src/commit/git/errors.ts +0 -9
  227. package/src/commit/git/index.ts +0 -211
  228. package/src/commit/git/operations.ts +0 -54
  229. package/src/commit/index.ts +0 -5
  230. package/src/commit/map-reduce/index.ts +0 -64
  231. package/src/commit/map-reduce/map-phase.ts +0 -178
  232. package/src/commit/map-reduce/reduce-phase.ts +0 -145
  233. package/src/commit/map-reduce/utils.ts +0 -9
  234. package/src/commit/message.ts +0 -11
  235. package/src/commit/model-selection.ts +0 -69
  236. package/src/commit/pipeline.ts +0 -243
  237. package/src/commit/prompts/analysis-system.md +0 -148
  238. package/src/commit/prompts/analysis-user.md +0 -38
  239. package/src/commit/prompts/changelog-system.md +0 -50
  240. package/src/commit/prompts/changelog-user.md +0 -18
  241. package/src/commit/prompts/file-observer-system.md +0 -24
  242. package/src/commit/prompts/file-observer-user.md +0 -8
  243. package/src/commit/prompts/reduce-system.md +0 -50
  244. package/src/commit/prompts/reduce-user.md +0 -17
  245. package/src/commit/prompts/summary-retry.md +0 -3
  246. package/src/commit/prompts/summary-system.md +0 -38
  247. package/src/commit/prompts/summary-user.md +0 -13
  248. package/src/commit/prompts/types-description.md +0 -2
  249. package/src/commit/types.ts +0 -109
  250. package/src/commit/utils/exclusions.ts +0 -42
  251. package/src/mcp/render.ts +0 -123
  252. package/src/modes/components/agent-dashboard.ts +0 -1130
  253. package/src/modes/components/codemode-group.ts +0 -369
  254. package/src/modes/components/read-tool-group.ts +0 -119
  255. package/src/modes/components/visual-truncate.ts +0 -63
  256. package/src/prompts/system/subagent-user-prompt.md +0 -8
  257. package/src/prompts/tools/ask.md +0 -44
  258. package/src/prompts/tools/bash.md +0 -24
  259. package/src/prompts/tools/browser.md +0 -33
  260. package/src/prompts/tools/calculator.md +0 -12
  261. package/src/prompts/tools/explore.md +0 -29
  262. package/src/prompts/tools/fetch.md +0 -16
  263. package/src/prompts/tools/find.md +0 -18
  264. package/src/prompts/tools/gemini-image.md +0 -23
  265. package/src/prompts/tools/grep.md +0 -28
  266. package/src/prompts/tools/hashline.md +0 -232
  267. package/src/prompts/tools/librarian.md +0 -24
  268. package/src/prompts/tools/lsp.md +0 -28
  269. package/src/prompts/tools/oracle.md +0 -26
  270. package/src/prompts/tools/patch.md +0 -74
  271. package/src/prompts/tools/python.md +0 -66
  272. package/src/prompts/tools/read.md +0 -36
  273. package/src/prompts/tools/replace.md +0 -38
  274. package/src/prompts/tools/reviewer.md +0 -41
  275. package/src/prompts/tools/ssh.md +0 -51
  276. package/src/prompts/tools/task-summary.md +0 -28
  277. package/src/prompts/tools/task.md +0 -146
  278. package/src/prompts/tools/todo-write.md +0 -65
  279. package/src/prompts/tools/undo-edit.md +0 -7
  280. package/src/prompts/tools/web-search.md +0 -19
  281. package/src/prompts/tools/write.md +0 -18
  282. package/src/task/batch.ts +0 -102
  283. package/src/task/discovery.ts +0 -126
  284. package/src/task/parallel.ts +0 -84
  285. package/src/task/template.ts +0 -32
  286. package/src/tools/calculator.ts +0 -537
  287. package/src/tools/jtd-to-typescript.ts +0 -198
  288. package/src/tools/renderers.ts +0 -60
  289. package/src/tools/tool-result.ts +0 -86
  290. /package/src/{modes/theme → theme}/dark.json +0 -0
  291. /package/src/{modes/theme → theme}/defaults/dark-catppuccin.json +0 -0
  292. /package/src/{modes/theme → theme}/defaults/dark-dracula.json +0 -0
  293. /package/src/{modes/theme → theme}/defaults/dark-gruvbox.json +0 -0
  294. /package/src/{modes/theme → theme}/defaults/dark-solarized.json +0 -0
  295. /package/src/{modes/theme → theme}/defaults/dark-tokyo-night.json +0 -0
  296. /package/src/{modes/theme → theme}/defaults/index.ts +0 -0
  297. /package/src/{modes/theme → theme}/defaults/light-catppuccin.json +0 -0
  298. /package/src/{modes/theme → theme}/defaults/light-github.json +0 -0
  299. /package/src/{modes/theme → theme}/defaults/light-solarized.json +0 -0
  300. /package/src/{modes/theme → theme}/light.json +0 -0
  301. /package/src/{modes/theme → theme}/mermaid-cache.ts +0 -0
  302. /package/src/{modes/theme → theme}/theme-schema.json +0 -0
  303. /package/src/{modes/theme → theme}/theme.ts +0 -0
@@ -1,7 +1,10 @@
1
1
  /**
2
- * In-process execution for subagents.
2
+ * In-process subagent executor.
3
3
  *
4
- * Runs each subagent on the main thread and forwards AgentEvents for progress tracking.
4
+ * Manages agent lifecycle (session creation, abort handling, cleanup) and
5
+ * forwards AgentEvents to an EventBus. All observation (progress tracking,
6
+ * output extraction, usage accumulation) is the caller's responsibility
7
+ * via EventBus subscriptions.
5
8
  */
6
9
  import * as path from "node:path";
7
10
  import type { AgentEvent, ThinkingLevel } from "@nghyane/arcane-agent";
@@ -20,23 +23,14 @@ import { createAgentSession, discoverAuthStorage } from "../sdk";
20
23
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
21
24
  import type { AuthStorage } from "../session/auth-storage";
22
25
  import { SessionManager } from "../session/session-manager";
23
- import { type ContextFileEntry, truncateTail } from "../tools";
26
+ import type { ContextFileEntry } from "../tools";
24
27
  import { ToolAbortError } from "../tools/tool-errors";
25
28
  import type { EventBus } from "../utils/event-bus";
26
- import { subprocessToolRegistry } from "./subprocess-tool-registry";
27
- import {
28
- type AgentDefinition,
29
- type AgentProgress,
30
- MAX_OUTPUT_BYTES,
31
- MAX_OUTPUT_LINES,
32
- type SingleResult,
33
- TASK_SUBAGENT_EVENT_CHANNEL,
34
- TASK_SUBAGENT_PROGRESS_CHANNEL,
35
- } from "./types";
29
+ import { type AgentDefinition, type SingleResult, TASK_SUBAGENT_EVENT_CHANNEL } from "./types";
36
30
 
37
31
  const MCP_CALL_TIMEOUT_MS = 60_000;
38
32
 
39
- /** Agent event types to forward for progress tracking. */
33
+ /** Agent event types worth forwarding. */
40
34
  const agentEventTypes = new Set<AgentEvent["type"]>([
41
35
  "agent_start",
42
36
  "agent_end",
@@ -110,13 +104,13 @@ export interface ExecutorOptions {
110
104
  isSubagent?: boolean;
111
105
  enableLsp?: boolean;
112
106
  signal?: AbortSignal;
113
- onProgress?: (progress: AgentProgress) => void;
114
107
  sessionFile?: string | null;
115
108
  persistArtifacts?: boolean;
116
109
  artifactsDir?: string;
117
110
  /** Path to parent conversation context file */
118
111
  contextFile?: string;
119
- eventBus?: EventBus;
112
+ /** EventBus for forwarding agent events. Required — all observation happens here. */
113
+ eventBus: EventBus;
120
114
  contextFiles?: ContextFileEntry[];
121
115
  skills?: Skill[];
122
116
  preloadedSkills?: Skill[];
@@ -127,55 +121,6 @@ export interface ExecutorOptions {
127
121
  settings?: Settings;
128
122
  }
129
123
 
130
- /**
131
- * Extract a short preview from tool args for display.
132
- */
133
- function extractToolArgsPreview(args: Record<string, unknown>): string {
134
- // Priority order for preview
135
- const previewKeys = ["command", "file_path", "path", "pattern", "query", "url", "task", "prompt"];
136
-
137
- for (const key of previewKeys) {
138
- if (args[key] && typeof args[key] === "string") {
139
- const value = args[key] as string;
140
- return value.length > 60 ? `${value.slice(0, 59)}…` : value;
141
- }
142
- }
143
-
144
- return "";
145
- }
146
-
147
- function getNumberField(record: Record<string, unknown>, key: string): number | undefined {
148
- if (!Object.hasOwn(record, key)) return undefined;
149
- const value = record[key];
150
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
151
- }
152
-
153
- function firstNumberField(record: Record<string, unknown>, keys: string[]): number | undefined {
154
- for (const key of keys) {
155
- const value = getNumberField(record, key);
156
- if (value !== undefined) return value;
157
- }
158
- return undefined;
159
- }
160
-
161
- /**
162
- * Normalize usage objects from different event formats.
163
- */
164
- function getUsageTokens(usage: unknown): number {
165
- if (!usage || typeof usage !== "object") return 0;
166
- const record = usage as Record<string, unknown>;
167
-
168
- const totalTokens = firstNumberField(record, ["totalTokens", "total_tokens"]);
169
- if (totalTokens !== undefined && totalTokens > 0) return totalTokens;
170
-
171
- const input = firstNumberField(record, ["input", "input_tokens", "inputTokens"]) ?? 0;
172
- const output = firstNumberField(record, ["output", "output_tokens", "outputTokens"]) ?? 0;
173
- const cacheRead = firstNumberField(record, ["cacheRead", "cache_read", "cacheReadTokens"]) ?? 0;
174
- const cacheWrite = firstNumberField(record, ["cacheWrite", "cache_write", "cacheWriteTokens"]) ?? 0;
175
-
176
- return input + output + cacheRead + cacheWrite;
177
- }
178
-
179
124
  /**
180
125
  * Create proxy tools that reuse the parent's MCP connections.
181
126
  */
@@ -188,9 +133,7 @@ function createMCPProxyTools(mcpManager: MCPManager): CustomTool<TSchema>[] {
188
133
  description: tool.description ?? "",
189
134
  parameters: tool.parameters as TSchema,
190
135
  execute: async (_toolCallId, params, _onUpdate, _ctx, signal) => {
191
- if (signal?.aborted) {
192
- throw new ToolAbortError();
193
- }
136
+ if (signal?.aborted) throw new ToolAbortError();
194
137
  const serverName = mcpTool.mcpServerName ?? "";
195
138
  const mcpToolName = mcpTool.mcpToolName ?? "";
196
139
  try {
@@ -231,28 +174,14 @@ function createMCPProxyTools(mcpManager: MCPManager): CustomTool<TSchema>[] {
231
174
 
232
175
  /**
233
176
  * Run a single agent in-process.
177
+ *
178
+ * Forwards all AgentEvents to options.eventBus. Callers observe progress,
179
+ * usage, and output by subscribing to the bus before calling this function.
234
180
  */
235
181
  export async function runAgent(options: ExecutorOptions): Promise<SingleResult> {
236
- const { cwd, agent, task, index, id, modelOverride, thinkingLevel, enableLsp, signal, onProgress } = options;
182
+ const { cwd, agent, task, index, id, modelOverride, thinkingLevel, enableLsp, signal, eventBus } = options;
237
183
  const startTime = Date.now();
238
184
 
239
- // Initialize progress
240
- const progress: AgentProgress = {
241
- index,
242
- id,
243
- agent: agent.name,
244
- status: "running",
245
- task,
246
- description: options.description,
247
- lastIntent: undefined,
248
- recentTools: [],
249
- recentOutput: [],
250
- toolCount: 0,
251
- tokens: 0,
252
- durationMs: 0,
253
- toolHistory: [],
254
- };
255
-
256
185
  // Check if already aborted
257
186
  if (signal?.aborted) {
258
187
  return {
@@ -262,16 +191,14 @@ export async function runAgent(options: ExecutorOptions): Promise<SingleResult>
262
191
  task,
263
192
  description: options.description,
264
193
  exitCode: 1,
265
- output: "",
266
194
  stderr: "Aborted before start",
267
- truncated: false,
268
195
  durationMs: 0,
269
196
  tokens: 0,
270
197
  error: "Aborted",
271
198
  };
272
199
  }
273
200
 
274
- // Set up artifact paths and write input file upfront if artifacts dir provided
201
+ // Set up artifact paths
275
202
  let subtaskSessionFile: string | undefined;
276
203
  if (options.artifactsDir) {
277
204
  subtaskSessionFile = path.join(options.artifactsDir, `${id}.jsonl`);
@@ -279,12 +206,9 @@ export async function runAgent(options: ExecutorOptions): Promise<SingleResult>
279
206
 
280
207
  const settings = options.settings ?? Settings.isolated();
281
208
  // Sub-agents never get the task tool — no recursive nesting
282
- let toolNames: string[] | undefined;
283
- if (agent.tools && agent.tools.length > 0) {
284
- toolNames = agent.tools.filter(name => name !== "task");
285
- }
209
+ let toolNames = agent.tools.filter(name => name !== "task");
286
210
  const pythonToolMode = settings.get("python.toolMode") ?? "both";
287
- if (toolNames?.includes("exec")) {
211
+ if (toolNames.includes("exec")) {
288
212
  const expanded = toolNames.filter(name => name !== "exec");
289
213
  if (pythonToolMode === "bash-only") {
290
214
  expanded.push("bash");
@@ -298,14 +222,9 @@ export async function runAgent(options: ExecutorOptions): Promise<SingleResult>
298
222
 
299
223
  const modelPatterns = normalizeModelPatterns(modelOverride ?? agent.model);
300
224
  const sessionFile = subtaskSessionFile ?? null;
301
-
302
225
  const lspEnabled = enableLsp ?? true;
303
226
  const skipPythonPreflight = Array.isArray(toolNames) && !toolNames.includes("python");
304
227
 
305
- const outputChunks: string[] = [];
306
- const finalOutputChunks: string[] = [];
307
- const RECENT_OUTPUT_TAIL_BYTES = 8 * 1024;
308
- let recentOutputTail = "";
309
228
  let stderr = "";
310
229
  let resolved = false;
311
230
  type AbortReason = "signal" | "terminate";
@@ -318,17 +237,6 @@ export async function runAgent(options: ExecutorOptions): Promise<SingleResult>
318
237
  let activeSession: AgentSession | null = null;
319
238
  let unsubscribe: (() => void) | null = null;
320
239
 
321
- // Accumulate usage incrementally from message_end events (no memory for streaming events)
322
- const accumulatedUsage = {
323
- input: 0,
324
- output: 0,
325
- cacheRead: 0,
326
- cacheWrite: 0,
327
- totalTokens: 0,
328
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
329
- };
330
- let hasUsage = false;
331
-
332
240
  const requestAbort = (reason: AbortReason) => {
333
241
  if (abortSent) {
334
242
  if (reason === "signal" && abortReason !== "signal") {
@@ -345,6 +253,11 @@ export async function runAgent(options: ExecutorOptions): Promise<SingleResult>
345
253
  }
346
254
  };
347
255
 
256
+ /** Allow external observers (e.g. ProgressTracker) to request termination. */
257
+ const terminateListener = eventBus.on("executor:terminate", () => {
258
+ requestAbort("terminate");
259
+ });
260
+
348
261
  // Handle abort signal
349
262
  const onAbort = () => {
350
263
  if (!resolved) requestAbort("signal");
@@ -353,273 +266,15 @@ export async function runAgent(options: ExecutorOptions): Promise<SingleResult>
353
266
  signal.addEventListener("abort", onAbort, { once: true, signal: listenerSignal });
354
267
  }
355
268
 
356
- const PROGRESS_COALESCE_MS = 150;
357
- let lastProgressEmitMs = 0;
358
- let progressTimeoutId: NodeJS.Timeout | null = null;
359
-
360
- const emitProgressNow = () => {
361
- progress.durationMs = Date.now() - startTime;
362
- onProgress?.({ ...progress });
363
- if (options.eventBus) {
364
- options.eventBus.emit(TASK_SUBAGENT_PROGRESS_CHANNEL, {
365
- index,
366
- agent: agent.name,
367
- task,
368
- progress: { ...progress },
369
- });
370
- }
371
- lastProgressEmitMs = Date.now();
372
- };
373
-
374
- const scheduleProgress = (flush = false) => {
375
- if (flush) {
376
- if (progressTimeoutId) {
377
- clearTimeout(progressTimeoutId);
378
- progressTimeoutId = null;
379
- }
380
- emitProgressNow();
381
- return;
382
- }
383
- const now = Date.now();
384
- const elapsed = now - lastProgressEmitMs;
385
- if (lastProgressEmitMs === 0 || elapsed >= PROGRESS_COALESCE_MS) {
386
- if (progressTimeoutId) {
387
- clearTimeout(progressTimeoutId);
388
- progressTimeoutId = null;
389
- }
390
- emitProgressNow();
391
- return;
392
- }
393
- if (progressTimeoutId) return;
394
- progressTimeoutId = setTimeout(() => {
395
- progressTimeoutId = null;
396
- emitProgressNow();
397
- }, PROGRESS_COALESCE_MS - elapsed);
398
- };
399
-
400
- const getMessageContent = (message: unknown): unknown => {
401
- if (message && typeof message === "object" && "content" in message) {
402
- return (message as { content?: unknown }).content;
403
- }
404
- return undefined;
405
- };
406
-
407
- const getMessageUsage = (message: unknown): unknown => {
408
- if (message && typeof message === "object" && "usage" in message) {
409
- return (message as { usage?: unknown }).usage;
410
- }
411
- return undefined;
412
- };
413
-
414
- const updateRecentOutputLines = () => {
415
- const lines = recentOutputTail.split("\n").filter(line => line.trim());
416
- progress.recentOutput = lines.slice(-8).reverse();
417
- };
418
-
419
- const appendRecentOutputTail = (text: string) => {
420
- if (!text) return;
421
- recentOutputTail += text;
422
- if (recentOutputTail.length > RECENT_OUTPUT_TAIL_BYTES) {
423
- recentOutputTail = recentOutputTail.slice(-RECENT_OUTPUT_TAIL_BYTES);
424
- }
425
- updateRecentOutputLines();
426
- };
427
-
428
- const replaceRecentOutputFromContent = (content: unknown[]) => {
429
- recentOutputTail = "";
430
- for (const block of content) {
431
- if (!block || typeof block !== "object") continue;
432
- const record = block as { type?: unknown; text?: unknown };
433
- if (record.type !== "text" || typeof record.text !== "string") continue;
434
- if (!record.text) continue;
435
- recentOutputTail += record.text;
436
- if (recentOutputTail.length > RECENT_OUTPUT_TAIL_BYTES) {
437
- recentOutputTail = recentOutputTail.slice(-RECENT_OUTPUT_TAIL_BYTES);
438
- }
439
- }
440
- updateRecentOutputLines();
441
- };
442
-
443
- const resetRecentOutput = () => {
444
- recentOutputTail = "";
445
- progress.recentOutput = [];
446
- };
447
-
269
+ // Forward agent events to EventBus — the only thing processEvent does
448
270
  const processEvent = (event: AgentEvent) => {
449
271
  if (resolved) return;
450
-
451
- if (options.eventBus) {
452
- options.eventBus.emit(TASK_SUBAGENT_EVENT_CHANNEL, {
453
- index,
454
- agent: agent.name,
455
- task,
456
- event,
457
- });
458
- }
459
-
460
- const now = Date.now();
461
- let flushProgress = false;
462
-
463
- switch (event.type) {
464
- case "message_start":
465
- if (event.message?.role === "assistant") {
466
- resetRecentOutput();
467
- }
468
- break;
469
-
470
- case "tool_execution_start": {
471
- // Skip Code Mode sub-tool events — they inflate toolCount and corrupt currentTool tracking
472
- if (event.parentToolCallId) break;
473
- progress.toolCount++;
474
- progress.currentTool = event.toolName;
475
- progress.currentToolArgs = extractToolArgsPreview(
476
- (event as { toolArgs?: Record<string, unknown> }).toolArgs || event.args || {},
477
- );
478
- progress.currentToolStartMs = now;
479
- const intent = event.intent?.trim();
480
- if (intent) {
481
- progress.lastIntent = intent;
482
- }
483
- // Add running entry to history (capped at 50)
484
- if (progress.toolHistory.length < 50) {
485
- progress.toolHistory.push({
486
- tool: event.toolName,
487
- args: progress.currentToolArgs,
488
- status: "running",
489
- });
490
- }
491
- break;
492
- }
493
-
494
- case "tool_execution_end": {
495
- // Skip Code Mode sub-tool events
496
- if (event.parentToolCallId) break;
497
- const isError = !!(event as { isError?: boolean }).isError;
498
- if (progress.currentTool) {
499
- progress.recentTools.unshift({
500
- tool: progress.currentTool,
501
- args: progress.currentToolArgs || "",
502
- endMs: now,
503
- });
504
- if (progress.recentTools.length > 5) {
505
- progress.recentTools.pop();
506
- }
507
- // Update the last running entry in history to final status
508
- for (let i = progress.toolHistory.length - 1; i >= 0; i--) {
509
- if (progress.toolHistory[i].status === "running") {
510
- progress.toolHistory[i].status = isError ? "error" : "success";
511
- break;
512
- }
513
- }
514
- }
515
- progress.currentTool = undefined;
516
- progress.currentToolArgs = undefined;
517
- progress.currentToolStartMs = undefined;
518
-
519
- // Check for registered subagent tool handler
520
- const handler = subprocessToolRegistry.getHandler(event.toolName);
521
- const eventArgs = (event as { args?: Record<string, unknown> }).args ?? {};
522
- if (handler) {
523
- // Check if handler wants to terminate the session
524
- if (
525
- handler.shouldTerminate?.({
526
- toolName: event.toolName,
527
- toolCallId: event.toolCallId,
528
- args: eventArgs,
529
- result: event.result,
530
- isError: event.isError,
531
- })
532
- ) {
533
- requestAbort("terminate");
534
- }
535
- }
536
- flushProgress = true;
537
- break;
538
- }
539
-
540
- case "message_update": {
541
- if (event.message?.role !== "assistant") break;
542
- const assistantEvent = (
543
- event as AgentEvent & {
544
- assistantMessageEvent?: { type?: string; delta?: string };
545
- }
546
- ).assistantMessageEvent;
547
- if (assistantEvent?.type === "text_delta" && typeof assistantEvent.delta === "string") {
548
- appendRecentOutputTail(assistantEvent.delta);
549
- break;
550
- }
551
- if (assistantEvent && assistantEvent.type !== "text_delta") {
552
- break;
553
- }
554
- const updateContent =
555
- getMessageContent(event.message) || (event as AgentEvent & { content?: unknown }).content;
556
- if (updateContent && Array.isArray(updateContent)) {
557
- replaceRecentOutputFromContent(updateContent);
558
- }
559
- break;
560
- }
561
-
562
- case "message_end": {
563
- // Extract text from assistant and toolResult messages (not user prompts)
564
- const role = event.message?.role;
565
- if (role === "assistant") {
566
- const messageContent =
567
- getMessageContent(event.message) || (event as AgentEvent & { content?: unknown }).content;
568
- if (messageContent && Array.isArray(messageContent)) {
569
- for (const block of messageContent) {
570
- if (block.type === "text" && block.text) {
571
- outputChunks.push(block.text);
572
- }
573
- }
574
- }
575
- }
576
- // Extract and accumulate usage (prefer message.usage, fallback to event.usage)
577
- const messageUsage = getMessageUsage(event.message) || (event as AgentEvent & { usage?: unknown }).usage;
578
- if (messageUsage && typeof messageUsage === "object") {
579
- // Only count assistant messages (not tool results, etc.)
580
- if (role === "assistant") {
581
- const usageRecord = messageUsage as Record<string, unknown>;
582
- const costRecord = (messageUsage as { cost?: Record<string, unknown> }).cost;
583
- hasUsage = true;
584
- accumulatedUsage.input += getNumberField(usageRecord, "input") ?? 0;
585
- accumulatedUsage.output += getNumberField(usageRecord, "output") ?? 0;
586
- accumulatedUsage.cacheRead += getNumberField(usageRecord, "cacheRead") ?? 0;
587
- accumulatedUsage.cacheWrite += getNumberField(usageRecord, "cacheWrite") ?? 0;
588
- accumulatedUsage.totalTokens += getNumberField(usageRecord, "totalTokens") ?? 0;
589
- if (costRecord) {
590
- accumulatedUsage.cost.input += getNumberField(costRecord, "input") ?? 0;
591
- accumulatedUsage.cost.output += getNumberField(costRecord, "output") ?? 0;
592
- accumulatedUsage.cost.cacheRead += getNumberField(costRecord, "cacheRead") ?? 0;
593
- accumulatedUsage.cost.cacheWrite += getNumberField(costRecord, "cacheWrite") ?? 0;
594
- accumulatedUsage.cost.total += getNumberField(costRecord, "total") ?? 0;
595
- }
596
- }
597
- // Accumulate tokens for progress display
598
- progress.tokens += getUsageTokens(messageUsage);
599
- }
600
- break;
601
- }
602
-
603
- case "agent_end":
604
- // Extract final content from assistant messages only (not user prompts)
605
- if (event.messages && Array.isArray(event.messages)) {
606
- for (const msg of event.messages) {
607
- if ((msg as { role?: string })?.role !== "assistant") continue;
608
- const messageContent = getMessageContent(msg);
609
- if (messageContent && Array.isArray(messageContent)) {
610
- for (const block of messageContent) {
611
- if (block.type === "text" && block.text) {
612
- finalOutputChunks.push(block.text);
613
- }
614
- }
615
- }
616
- }
617
- }
618
- flushProgress = true;
619
- break;
620
- }
621
-
622
- scheduleProgress(flushProgress);
272
+ eventBus.emit(TASK_SUBAGENT_EVENT_CHANNEL, {
273
+ index,
274
+ agent: agent.name,
275
+ task,
276
+ event,
277
+ });
623
278
  };
624
279
 
625
280
  const runSubagent = async (): Promise<{
@@ -646,7 +301,10 @@ export async function runAgent(options: ExecutorOptions): Promise<SingleResult>
646
301
  const authStorage = options.authStorage ?? (await discoverAuthStorage());
647
302
  checkAbort();
648
303
  const modelRegistry = options.modelRegistry ?? new ModelRegistry(authStorage);
649
- await modelRegistry.refresh();
304
+ // Skip refresh when reusing parent's registry — models are already discovered
305
+ if (!options.modelRegistry) {
306
+ await modelRegistry.refresh();
307
+ }
650
308
  checkAbort();
651
309
 
652
310
  const { model, thinkingLevel: resolvedThinkingLevel } = resolveModelOverride(
@@ -681,13 +339,13 @@ export async function runAgent(options: ExecutorOptions): Promise<SingleResult>
681
339
  }),
682
340
  sessionManager,
683
341
  hasUI: false,
684
- spawns: "",
685
342
  isSubagent: true,
686
343
  parentTaskPrefix: id,
687
344
  enableLsp: lspEnabled,
688
345
  skipPythonPreflight,
689
346
  enableMCP,
690
347
  customTools: mcpProxyTools.length > 0 ? mcpProxyTools : undefined,
348
+ disableExtensionDiscovery: true,
691
349
  });
692
350
 
693
351
  activeSession = session;
@@ -717,15 +375,15 @@ export async function runAgent(options: ExecutorOptions): Promise<SingleResult>
717
375
  if (extensionRunner) {
718
376
  extensionRunner.initialize(
719
377
  {
720
- sendMessage: (message, options) => {
721
- session.sendCustomMessage(message, options).catch(e => {
378
+ sendMessage: (message, msgOptions) => {
379
+ session.sendCustomMessage(message, msgOptions).catch(e => {
722
380
  logger.error("Extension sendMessage failed", {
723
381
  error: e instanceof Error ? e.message : String(e),
724
382
  });
725
383
  });
726
384
  },
727
- sendUserMessage: (content, options) => {
728
- session.sendUserMessage(content, options).catch(e => {
385
+ sendUserMessage: (content, msgOptions) => {
386
+ session.sendUserMessage(content, msgOptions).catch(e => {
729
387
  logger.error("Extension sendUserMessage failed", {
730
388
  error: e instanceof Error ? e.message : String(e),
731
389
  });
@@ -739,13 +397,13 @@ export async function runAgent(options: ExecutorOptions): Promise<SingleResult>
739
397
  },
740
398
  getActiveTools: () => session.getActiveToolNames(),
741
399
  getAllTools: () => session.getAllToolNames(),
742
- setActiveTools: (toolNames: string[]) =>
743
- session.setActiveToolsByName(toolNames.filter(name => !parentOwnedToolNames.has(name))),
400
+ setActiveTools: (names: string[]) =>
401
+ session.setActiveToolsByName(names.filter(name => !parentOwnedToolNames.has(name))),
744
402
  getCommands: () => [],
745
- setModel: async model => {
746
- const key = await session.modelRegistry.getApiKey(model);
403
+ setModel: async modelStr => {
404
+ const key = await session.modelRegistry.getApiKey(modelStr);
747
405
  if (!key) return false;
748
- await session.setModel(model);
406
+ await session.setModel(modelStr);
749
407
  return true;
750
408
  },
751
409
  getThinkingLevel: () => session.thinkingLevel,
@@ -761,17 +419,14 @@ export async function runAgent(options: ExecutorOptions): Promise<SingleResult>
761
419
  getSystemPrompt: () => session.systemPrompt,
762
420
  compact: async instructionsOrOptions => {
763
421
  const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
764
- const options =
422
+ const compactOptions =
765
423
  instructionsOrOptions && typeof instructionsOrOptions === "object"
766
424
  ? instructionsOrOptions
767
425
  : undefined;
768
- await session.compact(instructions, options);
426
+ await session.compact(instructions, compactOptions);
769
427
  },
770
428
  },
771
429
  );
772
- extensionRunner.onError(err => {
773
- logger.error("Extension error", { path: err.extensionPath, error: err.error });
774
- });
775
430
  await extensionRunner.emit({ type: "session_start" });
776
431
  }
777
432
 
@@ -841,45 +496,13 @@ export async function runAgent(options: ExecutorOptions): Promise<SingleResult>
841
496
  const done = await runSubagent();
842
497
  resolved = true;
843
498
  listenerController.abort();
499
+ terminateListener();
844
500
 
845
- if (progressTimeoutId) {
846
- clearTimeout(progressTimeoutId);
847
- progressTimeoutId = null;
848
- }
849
-
850
- const exitCode = done.exitCode;
851
501
  if (done.error) {
852
502
  stderr = done.error;
853
503
  }
854
504
 
855
- // Use final output if available, otherwise accumulated output
856
- const rawOutput = finalOutputChunks.length > 0 ? finalOutputChunks.join("") : outputChunks.join("");
857
- const { content: truncatedOutput, truncated } = truncateTail(rawOutput, {
858
- maxBytes: MAX_OUTPUT_BYTES,
859
- maxLines: MAX_OUTPUT_LINES,
860
- });
861
-
862
- // Write output artifact (input and jsonl already written in real-time)
863
- // Compute output metadata for agent:// URL integration
864
- let outputMeta: { lineCount: number; charCount: number } | undefined;
865
- let outputPath: string | undefined;
866
- if (options.artifactsDir) {
867
- outputPath = path.join(options.artifactsDir, `${id}.md`);
868
- try {
869
- await Bun.write(outputPath, rawOutput);
870
- outputMeta = {
871
- lineCount: rawOutput.split("\n").length,
872
- charCount: rawOutput.length,
873
- };
874
- } catch {
875
- // Non-fatal
876
- }
877
- }
878
-
879
- // Update final progress
880
505
  const wasAborted = done.aborted || signal?.aborted || false;
881
- progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
882
- scheduleProgress(true);
883
506
 
884
507
  return {
885
508
  index,
@@ -887,22 +510,11 @@ export async function runAgent(options: ExecutorOptions): Promise<SingleResult>
887
510
  agent: agent.name,
888
511
  task,
889
512
  description: options.description,
890
- lastIntent: progress.lastIntent,
891
- exitCode,
892
- output: truncatedOutput,
513
+ exitCode: done.exitCode,
893
514
  stderr,
894
- truncated,
895
515
  durationMs: Date.now() - startTime,
896
- tokens: progress.tokens,
897
- error: exitCode !== 0 && stderr ? stderr : undefined,
516
+ tokens: 0,
517
+ error: done.exitCode !== 0 && stderr ? stderr : undefined,
898
518
  aborted: wasAborted,
899
- usage: hasUsage ? accumulatedUsage : undefined,
900
- outputPath,
901
- outputMeta,
902
- toolHistory: progress.toolHistory.map(t => ({
903
- tool: t.tool,
904
- args: t.args,
905
- status: t.status === "running" ? ("error" as const) : t.status,
906
- })),
907
519
  };
908
520
  }