@nghyane/arcane 0.1.13 → 0.1.14

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 (323) 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 +48 -113
  50. package/src/modes/components/bordered-loader.ts +1 -1
  51. package/src/modes/components/branch-summary-message.ts +13 -10
  52. package/src/modes/components/compaction-summary-message.ts +14 -13
  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 +49 -92
  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/frontmatter.md +1 -1
  113. package/src/prompts/agents/init.md +2 -2
  114. package/src/prompts/agents/librarian.md +30 -21
  115. package/src/prompts/agents/oracle.md +9 -2
  116. package/src/prompts/agents/reviewer.md +15 -49
  117. package/src/prompts/agents/task.md +17 -9
  118. package/src/prompts/compaction/branch-summary-context.md +1 -1
  119. package/src/prompts/compaction/branch-summary-preamble.md +1 -1
  120. package/src/prompts/compaction/branch-summary.md +4 -1
  121. package/src/prompts/compaction/compaction-short-summary.md +1 -1
  122. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  123. package/src/prompts/compaction/compaction-summary.md +4 -1
  124. package/src/prompts/compaction/compaction-turn-prefix.md +1 -1
  125. package/src/prompts/compaction/compaction-update-summary.md +1 -1
  126. package/src/prompts/memories/consolidation.md +1 -1
  127. package/src/prompts/memories/read_path.md +1 -1
  128. package/src/prompts/memories/stage_one_input.md +1 -1
  129. package/src/prompts/memories/stage_one_system.md +1 -1
  130. package/src/prompts/review-request.md +1 -1
  131. package/src/prompts/system/agent-creation-architect.md +1 -1
  132. package/src/prompts/system/agent-creation-user.md +1 -1
  133. package/src/prompts/system/custom-system-prompt.md +1 -1
  134. package/src/prompts/system/file-operations.md +1 -1
  135. package/src/prompts/system/subagent-system-prompt.md +2 -2
  136. package/src/prompts/system/summarization-system.md +1 -1
  137. package/src/prompts/system/system-prompt.md +163 -178
  138. package/src/prompts/system/title-system.md +1 -1
  139. package/src/prompts/system/ttsr-interrupt.md +1 -1
  140. package/src/prompts/system/verification-reminder.md +6 -0
  141. package/src/prompts/system/web-search.md +1 -1
  142. package/src/sdk.ts +0 -9
  143. package/src/session/agent-session.ts +244 -1459
  144. package/src/session/model-controller.ts +406 -0
  145. package/src/session/retry-utils.ts +71 -0
  146. package/src/session/session-manager.ts +22 -186
  147. package/src/session/session-types.ts +312 -0
  148. package/src/session/stats.ts +387 -0
  149. package/src/session/streaming-edit.ts +258 -0
  150. package/src/session/ttsr.ts +213 -0
  151. package/src/slash-commands/builtin-registry.ts +0 -8
  152. package/src/stt/recorder.ts +2 -2
  153. package/src/system-prompt.ts +1 -14
  154. package/src/task/agents.ts +7 -33
  155. package/src/task/executor.ts +50 -438
  156. package/src/task/index.ts +104 -71
  157. package/src/task/progress-tracker.ts +390 -0
  158. package/src/task/render.ts +371 -187
  159. package/src/task/subprocess-tool-registry.ts +1 -1
  160. package/src/task/types.ts +14 -47
  161. package/src/tools/ask.ts +31 -42
  162. package/src/tools/bash-interactive.ts +2 -2
  163. package/src/tools/bash-interceptor.ts +2 -2
  164. package/src/tools/bash-normalize.ts +1 -1
  165. package/src/tools/bash-skill-urls.ts +2 -2
  166. package/src/tools/bash.ts +87 -136
  167. package/src/tools/browser.ts +54 -84
  168. package/src/tools/create-tools.ts +186 -0
  169. package/src/tools/default-renderer.ts +104 -0
  170. package/src/tools/explore.ts +11 -10
  171. package/src/tools/fetch.ts +24 -114
  172. package/src/tools/find.ts +48 -132
  173. package/src/tools/gemini-image.ts +5 -15
  174. package/src/tools/github.ts +450 -0
  175. package/src/tools/grep.ts +43 -179
  176. package/src/tools/index.ts +35 -198
  177. package/src/tools/json-tree.ts +3 -3
  178. package/src/tools/librarian.ts +18 -18
  179. package/src/tools/list-limit.ts +2 -2
  180. package/src/tools/notebook.ts +35 -87
  181. package/src/tools/oracle.ts +25 -25
  182. package/src/tools/output-meta.ts +89 -4
  183. package/src/tools/output-utils.ts +2 -2
  184. package/src/tools/python.ts +86 -637
  185. package/src/tools/read.ts +36 -119
  186. package/src/tools/reviewer-tool.ts +19 -21
  187. package/src/tools/search-code.ts +128 -0
  188. package/src/tools/ssh.ts +67 -126
  189. package/src/tools/subagent-tool.ts +197 -123
  190. package/src/tools/todo-write.ts +15 -31
  191. package/src/tools/tool-errors.ts +0 -30
  192. package/src/tools/undo-edit.ts +30 -67
  193. package/src/tools/write.ts +78 -127
  194. package/src/tui/code-cell.ts +4 -4
  195. package/src/tui/file-list.ts +2 -2
  196. package/src/tui/output-block.ts +1 -1
  197. package/src/tui/status-line.ts +1 -1
  198. package/src/tui/tree-list.ts +2 -2
  199. package/src/tui/types.ts +1 -1
  200. package/src/tui/utils.ts +1 -1
  201. package/src/{tools → ui}/render-utils.ts +87 -126
  202. package/src/utils/external-editor.ts +4 -4
  203. package/src/utils/file-mentions.ts +1 -1
  204. package/src/utils/index.ts +30 -0
  205. package/src/utils/tools-manager.ts +9 -19
  206. package/src/web/github-client.ts +290 -0
  207. package/src/web/scrapers/github.ts +11 -62
  208. package/src/web/search/auth.ts +1 -3
  209. package/src/web/search/index.ts +82 -46
  210. package/src/web/search/provider.ts +11 -16
  211. package/src/web/search/providers/grep.ts +160 -0
  212. package/src/web/search/render.ts +48 -235
  213. package/src/web/search/types.ts +1 -1
  214. package/src/commands/commit.ts +0 -36
  215. package/src/commit/agentic/agent.ts +0 -311
  216. package/src/commit/agentic/fallback.ts +0 -96
  217. package/src/commit/agentic/index.ts +0 -359
  218. package/src/commit/agentic/prompts/analyze-file.md +0 -22
  219. package/src/commit/agentic/prompts/session-user.md +0 -25
  220. package/src/commit/agentic/prompts/split-confirm.md +0 -1
  221. package/src/commit/agentic/prompts/system.md +0 -38
  222. package/src/commit/agentic/state.ts +0 -69
  223. package/src/commit/agentic/tools/analyze-file.ts +0 -118
  224. package/src/commit/agentic/tools/git-file-diff.ts +0 -194
  225. package/src/commit/agentic/tools/git-hunk.ts +0 -50
  226. package/src/commit/agentic/tools/git-overview.ts +0 -84
  227. package/src/commit/agentic/tools/index.ts +0 -56
  228. package/src/commit/agentic/tools/propose-changelog.ts +0 -128
  229. package/src/commit/agentic/tools/propose-commit.ts +0 -154
  230. package/src/commit/agentic/tools/recent-commits.ts +0 -81
  231. package/src/commit/agentic/tools/split-commit.ts +0 -280
  232. package/src/commit/agentic/topo-sort.ts +0 -44
  233. package/src/commit/agentic/trivial.ts +0 -51
  234. package/src/commit/agentic/validation.ts +0 -200
  235. package/src/commit/analysis/conventional.ts +0 -165
  236. package/src/commit/analysis/index.ts +0 -4
  237. package/src/commit/analysis/scope.ts +0 -242
  238. package/src/commit/analysis/summary.ts +0 -112
  239. package/src/commit/analysis/validation.ts +0 -66
  240. package/src/commit/changelog/detect.ts +0 -37
  241. package/src/commit/changelog/generate.ts +0 -110
  242. package/src/commit/changelog/index.ts +0 -234
  243. package/src/commit/changelog/parse.ts +0 -44
  244. package/src/commit/cli.ts +0 -93
  245. package/src/commit/git/diff.ts +0 -148
  246. package/src/commit/git/errors.ts +0 -9
  247. package/src/commit/git/index.ts +0 -211
  248. package/src/commit/git/operations.ts +0 -54
  249. package/src/commit/index.ts +0 -5
  250. package/src/commit/map-reduce/index.ts +0 -64
  251. package/src/commit/map-reduce/map-phase.ts +0 -178
  252. package/src/commit/map-reduce/reduce-phase.ts +0 -145
  253. package/src/commit/map-reduce/utils.ts +0 -9
  254. package/src/commit/message.ts +0 -11
  255. package/src/commit/model-selection.ts +0 -69
  256. package/src/commit/pipeline.ts +0 -243
  257. package/src/commit/prompts/analysis-system.md +0 -148
  258. package/src/commit/prompts/analysis-user.md +0 -38
  259. package/src/commit/prompts/changelog-system.md +0 -50
  260. package/src/commit/prompts/changelog-user.md +0 -18
  261. package/src/commit/prompts/file-observer-system.md +0 -24
  262. package/src/commit/prompts/file-observer-user.md +0 -8
  263. package/src/commit/prompts/reduce-system.md +0 -50
  264. package/src/commit/prompts/reduce-user.md +0 -17
  265. package/src/commit/prompts/summary-retry.md +0 -3
  266. package/src/commit/prompts/summary-system.md +0 -38
  267. package/src/commit/prompts/summary-user.md +0 -13
  268. package/src/commit/prompts/types-description.md +0 -2
  269. package/src/commit/types.ts +0 -109
  270. package/src/commit/utils/exclusions.ts +0 -42
  271. package/src/mcp/render.ts +0 -123
  272. package/src/modes/components/agent-dashboard.ts +0 -1130
  273. package/src/modes/components/codemode-group.ts +0 -369
  274. package/src/modes/components/read-tool-group.ts +0 -119
  275. package/src/modes/components/visual-truncate.ts +0 -63
  276. package/src/prompts/system/subagent-user-prompt.md +0 -8
  277. package/src/prompts/tools/ask.md +0 -44
  278. package/src/prompts/tools/bash.md +0 -24
  279. package/src/prompts/tools/browser.md +0 -33
  280. package/src/prompts/tools/calculator.md +0 -12
  281. package/src/prompts/tools/explore.md +0 -29
  282. package/src/prompts/tools/fetch.md +0 -16
  283. package/src/prompts/tools/find.md +0 -18
  284. package/src/prompts/tools/gemini-image.md +0 -23
  285. package/src/prompts/tools/grep.md +0 -28
  286. package/src/prompts/tools/hashline.md +0 -232
  287. package/src/prompts/tools/librarian.md +0 -24
  288. package/src/prompts/tools/lsp.md +0 -28
  289. package/src/prompts/tools/oracle.md +0 -26
  290. package/src/prompts/tools/patch.md +0 -74
  291. package/src/prompts/tools/python.md +0 -66
  292. package/src/prompts/tools/read.md +0 -36
  293. package/src/prompts/tools/replace.md +0 -38
  294. package/src/prompts/tools/reviewer.md +0 -41
  295. package/src/prompts/tools/ssh.md +0 -51
  296. package/src/prompts/tools/task-summary.md +0 -28
  297. package/src/prompts/tools/task.md +0 -146
  298. package/src/prompts/tools/todo-write.md +0 -65
  299. package/src/prompts/tools/undo-edit.md +0 -7
  300. package/src/prompts/tools/web-search.md +0 -19
  301. package/src/prompts/tools/write.md +0 -18
  302. package/src/task/batch.ts +0 -102
  303. package/src/task/discovery.ts +0 -126
  304. package/src/task/parallel.ts +0 -84
  305. package/src/task/template.ts +0 -32
  306. package/src/tools/calculator.ts +0 -537
  307. package/src/tools/jtd-to-typescript.ts +0 -198
  308. package/src/tools/renderers.ts +0 -60
  309. package/src/tools/tool-result.ts +0 -86
  310. /package/src/{modes/theme → theme}/dark.json +0 -0
  311. /package/src/{modes/theme → theme}/defaults/dark-catppuccin.json +0 -0
  312. /package/src/{modes/theme → theme}/defaults/dark-dracula.json +0 -0
  313. /package/src/{modes/theme → theme}/defaults/dark-gruvbox.json +0 -0
  314. /package/src/{modes/theme → theme}/defaults/dark-solarized.json +0 -0
  315. /package/src/{modes/theme → theme}/defaults/dark-tokyo-night.json +0 -0
  316. /package/src/{modes/theme → theme}/defaults/index.ts +0 -0
  317. /package/src/{modes/theme → theme}/defaults/light-catppuccin.json +0 -0
  318. /package/src/{modes/theme → theme}/defaults/light-github.json +0 -0
  319. /package/src/{modes/theme → theme}/defaults/light-solarized.json +0 -0
  320. /package/src/{modes/theme → theme}/light.json +0 -0
  321. /package/src/{modes/theme → theme}/mermaid-cache.ts +0 -0
  322. /package/src/{modes/theme → theme}/theme-schema.json +0 -0
  323. /package/src/{modes/theme → theme}/theme.ts +0 -0
package/src/task/index.ts CHANGED
@@ -10,22 +10,51 @@ import * as path from "node:path";
10
10
  import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@nghyane/arcane-agent";
11
11
  import { Snowflake } from "@nghyane/arcane-utils";
12
12
  import type { ToolSession } from "..";
13
- import type { Theme } from "../modes/theme/theme";
13
+ import type { Theme } from "../theme/theme";
14
+ import { EventBus } from "../utils/event-bus";
14
15
  import { getBundledAgent } from "./agents";
15
16
  import { runAgent } from "./executor";
16
17
  import { AgentOutputManager } from "./output-manager";
18
+ import { extractAgentOutput, ProgressTracker } from "./progress-tracker";
17
19
  import { renderCall, renderResult } from "./render";
18
- import { renderTemplate } from "./template";
19
- import { type AgentProgress, type TaskParams, type TaskSchema, type TaskToolDetails, taskSchema } from "./types";
20
+ import {
21
+ type AgentProgress,
22
+ TASK_SUBAGENT_EVENT_CHANNEL,
23
+ type TaskParams,
24
+ type TaskSchema,
25
+ type TaskToolDetails,
26
+ taskSchema,
27
+ } from "./types";
20
28
 
21
29
  // Re-export types and utilities
22
30
  export { loadBundledAgents as BUNDLED_AGENTS } from "./agents";
23
- export { type BatchOptions, type BatchResult, type BatchTask, runTaskBatch } from "./batch";
24
- export { discoverAgents, getAgent } from "./discovery";
25
31
  export { AgentOutputManager } from "./output-manager";
26
- export type { AgentDefinition, AgentProgress, SingleResult, TaskParams, TaskToolDetails } from "./types";
32
+ export type {
33
+ AgentDefinition,
34
+ AgentProgress,
35
+ SingleResult,
36
+ TaskParams,
37
+ TaskToolDetails,
38
+ } from "./types";
27
39
  export { taskSchema } from "./types";
28
40
 
41
+ // ═══════════════════════════════════════════════════════════════════════════
42
+ // Helpers
43
+ // ═══════════════════════════════════════════════════════════════════════════
44
+
45
+ /** Derive a CamelCase ID from a short description for artifact naming. */
46
+ function deriveId(description: string): string {
47
+ return (
48
+ description
49
+ .replace(/[^a-zA-Z0-9\s]/g, "")
50
+ .split(/\s+/)
51
+ .filter(Boolean)
52
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
53
+ .join("")
54
+ .slice(0, 32) || "Task"
55
+ );
56
+ }
57
+
29
58
  // ═══════════════════════════════════════════════════════════════════════════
30
59
  // Tool Class
31
60
  // ═══════════════════════════════════════════════════════════════════════════
@@ -43,8 +72,12 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
43
72
  readonly renderCall = renderCall;
44
73
  readonly renderResult = renderResult;
45
74
 
46
- readonly description =
47
- "Launch a subagent to execute a well-scoped task. Use Promise.all() for parallel tasks. Returns structured {exitCode, output, durationMs, tokens}.";
75
+ readonly description = [
76
+ "Perform a task (a sub-task of the user's overall task) using a sub-agent that has access to: grep, find, read, bash, edit, write, explore, web_search, fetch, python, undo_edit, todo_write.",
77
+ "When to use: Complex multi-step tasks; operations producing lots of output tokens not needed after; changes across many layers after planning; when user asks to launch an 'agent'.",
78
+ "When NOT to use: Single logical task; reading a single file; performing text search; editing a single file; not sure what changes to make.",
79
+ "How to use: Run multiple sub-agents concurrently if tasks are independent; include all necessary context and a detailed plan; tell sub-agent how to verify work; show user concise summary of result.",
80
+ ].join(" ");
48
81
 
49
82
  private constructor(private readonly session: ToolSession) {
50
83
  this.parameters = taskSchema;
@@ -72,7 +105,10 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
72
105
  };
73
106
  }
74
107
 
75
- const modelOverride = this.session.getActiveModelString?.() ?? this.session.getModelString?.();
108
+ const isLowComplexity = params.complexity === "low";
109
+ const modelOverride = isLowComplexity
110
+ ? "arcane/fast"
111
+ : (this.session.getActiveModelString?.() ?? this.session.getModelString?.());
76
112
  const sessionFile = this.session.getSessionFile();
77
113
  const artifactsDir = sessionFile ? sessionFile.slice(0, -6) : null;
78
114
  const tempArtifactsDir = artifactsDir ? null : path.join(os.tmpdir(), `arcane-task-${Snowflake.next()}`);
@@ -82,7 +118,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
82
118
  const emitProgress = () => {
83
119
  const progress = Array.from(progressMap.values());
84
120
  onUpdate?.({
85
- content: [{ type: "text", text: `Running task ${params.id}...` }],
121
+ content: [{ type: "text", text: `Running task...` }],
86
122
  details: {
87
123
  results: [],
88
124
  totalDurationMs: Date.now() - startTime,
@@ -102,99 +138,96 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
102
138
 
103
139
  const outputManager =
104
140
  this.session.agentOutputManager ?? new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
105
- const [uniqueId] = await outputManager.allocateBatch([params.id]);
141
+ const derivedId = deriveId(params.description);
142
+ const [uniqueId] = await outputManager.allocateBatch([derivedId]);
106
143
 
107
- // Build task text from context + assignment
108
- const taskItem = {
109
- id: uniqueId,
110
- description: params.description,
111
- assignment: params.assignment,
112
- skills: params.skills,
113
- };
114
- const rendered = renderTemplate(params.context ?? "", taskItem);
115
-
116
- // Resolve skills
117
- const contextFiles = this.session.contextFiles;
118
- const availableSkills = this.session.skills;
119
- const promptTemplates = this.session.promptTemplates;
120
- let resolvedSkills = availableSkills;
121
- let preloadedSkills: typeof availableSkills | undefined;
122
-
123
- if (params.skills !== undefined && availableSkills) {
124
- const skillLookup = new Map(availableSkills.map(s => [s.name, s]));
125
- const resolved: typeof availableSkills = [];
126
- const missing: string[] = [];
127
- for (const name of params.skills) {
128
- const trimmed = name.trim();
129
- if (!trimmed) continue;
130
- const skill = skillLookup.get(trimmed);
131
- if (skill) resolved.push(skill);
132
- else missing.push(trimmed);
133
- }
134
- if (missing.length > 0) {
135
- const available = availableSkills.map(s => s.name).join(", ") || "none";
136
- return {
137
- content: [{ type: "text", text: `Unknown skills: ${missing.join(", ")}. Available: ${available}` }],
138
- details: { results: [], totalDurationMs: Date.now() - startTime },
139
- };
140
- }
141
- resolvedSkills = resolved;
142
- preloadedSkills = resolved;
143
- }
144
+ // Set up EventBus all observation flows through here
145
+ const eventBus = new EventBus();
144
146
 
145
- progressMap.set(0, {
147
+ const tracker = new ProgressTracker({
146
148
  index: 0,
147
149
  id: uniqueId,
148
150
  agent: agentName,
149
- status: "pending",
150
- task: rendered.task,
151
- recentTools: [],
152
- recentOutput: [],
153
- toolCount: 0,
154
- tokens: 0,
155
- durationMs: 0,
156
- toolHistory: [],
151
+ task: params.prompt,
152
+ description: params.description,
153
+ startTime,
154
+ onProgress: progress => {
155
+ progressMap.set(0, { ...structuredClone(progress) });
156
+ emitProgress();
157
+ },
158
+ onTerminateRequest: () => eventBus.emit("executor:terminate", {}),
159
+ });
160
+ tracker.subscribe(eventBus);
161
+
162
+ // Capture output from agent_end
163
+ let agentOutput = "";
164
+ const outputListener = eventBus.on(TASK_SUBAGENT_EVENT_CHANNEL, (data: unknown) => {
165
+ const payload = data as { event?: { type: string; messages?: unknown[] } };
166
+ if (payload.event?.type === "agent_end") {
167
+ agentOutput = extractAgentOutput(payload.event as Parameters<typeof extractAgentOutput>[0]);
168
+ }
157
169
  });
158
- emitProgress();
159
170
 
160
171
  const result = await runAgent({
161
172
  cwd: this.session.cwd,
162
173
  agent: effectiveAgent,
163
- task: rendered.task,
164
- description: rendered.description,
174
+ task: params.prompt,
175
+ description: params.description,
165
176
  index: 0,
166
177
  id: uniqueId,
167
178
  isSubagent: true,
168
179
  modelOverride,
180
+ thinkingLevel: isLowComplexity ? "minimal" : undefined,
169
181
  sessionFile,
170
182
  persistArtifacts: !!artifactsDir,
171
183
  artifactsDir: effectiveArtifactsDir,
172
184
  contextFile: contextFilePath,
173
185
  enableLsp: false,
174
186
  signal,
175
- eventBus: undefined,
176
- onProgress: progress => {
177
- progressMap.set(0, { ...structuredClone(progress) });
178
- emitProgress();
179
- },
187
+ eventBus,
180
188
  authStorage: this.session.subagentContext?.authStorage,
181
189
  modelRegistry: this.session.subagentContext?.modelRegistry,
182
190
  settings: this.session.settings,
183
191
  mcpManager: this.session.subagentContext?.mcpManager,
184
- contextFiles,
185
- skills: resolvedSkills,
186
- preloadedSkills,
187
- promptTemplates,
192
+ contextFiles: this.session.contextFiles,
193
+ skills: this.session.skills,
194
+ promptTemplates: this.session.promptTemplates,
188
195
  });
189
196
 
197
+ // Finalize tracker
198
+ const wasAborted = result.aborted ?? false;
199
+ tracker.finalize(wasAborted ? "aborted" : result.exitCode === 0 ? "completed" : "failed");
200
+ tracker.dispose();
201
+ outputListener();
202
+
203
+ // Enrich result with tracker data
204
+ result.tokens = tracker.progress.tokens;
205
+ result.lastIntent = tracker.progress.lastIntent;
206
+ result.usage = tracker.usage;
207
+ result.toolHistory = tracker.progress.toolHistory.map(t => ({
208
+ tool: t.tool,
209
+ args: t.args,
210
+ status: t.status === "running" ? ("error" as const) : t.status,
211
+ }));
212
+
213
+ // Write output artifact for agent:// URL integration
214
+ if (artifactsDir && agentOutput) {
215
+ const outputFile = path.join(effectiveArtifactsDir, `${uniqueId}.md`);
216
+ try {
217
+ await Bun.write(outputFile, agentOutput);
218
+ } catch {
219
+ // Non-fatal
220
+ }
221
+ }
222
+
190
223
  if (tempArtifactsDir) {
191
224
  await fs.rm(tempArtifactsDir, { recursive: true, force: true });
192
225
  }
193
226
 
194
227
  const totalDuration = Date.now() - startTime;
195
- const output = result.output.trim() || result.stderr.trim() || "(no output)";
228
+ const output = agentOutput.trim() || result.stderr.trim() || "(no output)";
196
229
 
197
- // Return structured result as JSON for codemode composability
230
+ // Return structured result as JSON for code tool composability
198
231
  const structured = {
199
232
  exitCode: result.exitCode,
200
233
  output,
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Progress observer for subagent execution.
3
+ *
4
+ * Subscribes to raw AgentEvents via EventBus and maintains:
5
+ * - TUI progress state (tool history, recent output, status)
6
+ * - Usage accumulation (tokens, cost)
7
+ *
8
+ * Pure observer — does not control execution lifecycle.
9
+ */
10
+ import type { AgentEvent } from "@nghyane/arcane-agent";
11
+ import type { Usage } from "@nghyane/arcane-ai";
12
+ import type { EventBus } from "../utils/event-bus";
13
+ import { subprocessToolRegistry } from "./subprocess-tool-registry";
14
+ import { type AgentProgress, TASK_SUBAGENT_EVENT_CHANNEL } from "./types";
15
+
16
+ const RECENT_OUTPUT_TAIL_BYTES = 8 * 1024;
17
+
18
+ function extractToolArgsPreview(args: Record<string, unknown>): string {
19
+ const previewKeys = ["command", "file_path", "path", "pattern", "query", "url", "task", "prompt"];
20
+ for (const key of previewKeys) {
21
+ if (args[key] && typeof args[key] === "string") {
22
+ const value = args[key] as string;
23
+ return value.length > 60 ? `${value.slice(0, 59)}…` : value;
24
+ }
25
+ }
26
+ return "";
27
+ }
28
+
29
+ function getNumberField(record: Record<string, unknown>, key: string): number | undefined {
30
+ if (!Object.hasOwn(record, key)) return undefined;
31
+ const value = record[key];
32
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
33
+ }
34
+
35
+ function firstNumberField(record: Record<string, unknown>, keys: string[]): number | undefined {
36
+ for (const key of keys) {
37
+ const value = getNumberField(record, key);
38
+ if (value !== undefined) return value;
39
+ }
40
+ return undefined;
41
+ }
42
+
43
+ function getUsageTokens(usage: unknown): number {
44
+ if (!usage || typeof usage !== "object") return 0;
45
+ const record = usage as Record<string, unknown>;
46
+ const totalTokens = firstNumberField(record, ["totalTokens", "total_tokens"]);
47
+ if (totalTokens !== undefined && totalTokens > 0) return totalTokens;
48
+ const input = firstNumberField(record, ["input", "input_tokens", "inputTokens"]) ?? 0;
49
+ const output = firstNumberField(record, ["output", "output_tokens", "outputTokens"]) ?? 0;
50
+ const cacheRead = firstNumberField(record, ["cacheRead", "cache_read", "cacheReadTokens"]) ?? 0;
51
+ const cacheWrite = firstNumberField(record, ["cacheWrite", "cache_write", "cacheWriteTokens"]) ?? 0;
52
+ return input + output + cacheRead + cacheWrite;
53
+ }
54
+
55
+ function getMessageField<K extends string>(message: unknown, key: K): unknown {
56
+ if (message && typeof message === "object" && key in message) {
57
+ return (message as Record<string, unknown>)[key];
58
+ }
59
+ return undefined;
60
+ }
61
+
62
+ export interface ProgressTrackerOptions {
63
+ index: number;
64
+ id: string;
65
+ agent: string;
66
+ task: string;
67
+ description?: string;
68
+ startTime: number;
69
+ /** Minimum ms between progress emissions */
70
+ coalesceMs?: number;
71
+ onProgress?: (progress: AgentProgress) => void;
72
+ /** Called when a tool handler signals termination (e.g. shouldTerminate). */
73
+ onTerminateRequest?: () => void;
74
+ }
75
+
76
+ export class ProgressTracker {
77
+ #progress: AgentProgress;
78
+ #options: ProgressTrackerOptions;
79
+ #recentOutputTail = "";
80
+ #lastProgressEmitMs = 0;
81
+ #progressTimeoutId?: NodeJS.Timeout;
82
+ #coalesceMs: number;
83
+ #unsubscribe?: () => void;
84
+
85
+ // Usage accumulation
86
+ #usage: Usage = {
87
+ input: 0,
88
+ output: 0,
89
+ cacheRead: 0,
90
+ cacheWrite: 0,
91
+ totalTokens: 0,
92
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
93
+ };
94
+ #hasUsage = false;
95
+
96
+ constructor(options: ProgressTrackerOptions) {
97
+ this.#options = options;
98
+ this.#coalesceMs = options.coalesceMs ?? 150;
99
+ this.#progress = {
100
+ index: options.index,
101
+ id: options.id,
102
+ agent: options.agent,
103
+ status: "running",
104
+ task: options.task,
105
+ description: options.description,
106
+ lastIntent: undefined,
107
+ recentTools: [],
108
+ recentOutput: [],
109
+ toolCount: 0,
110
+ tokens: 0,
111
+ durationMs: 0,
112
+ toolHistory: [],
113
+ };
114
+ }
115
+
116
+ /** Subscribe to raw agent events on an EventBus. */
117
+ subscribe(eventBus: EventBus): void {
118
+ this.#unsubscribe = eventBus.on(TASK_SUBAGENT_EVENT_CHANNEL, (data: unknown) => {
119
+ const payload = data as { event?: AgentEvent };
120
+ if (payload.event) {
121
+ this.#processEvent(payload.event);
122
+ }
123
+ });
124
+ }
125
+
126
+ get progress(): AgentProgress {
127
+ return this.#progress;
128
+ }
129
+
130
+ /** Accumulated usage, or undefined if no usage events received. */
131
+ get usage(): Usage | undefined {
132
+ return this.#hasUsage ? this.#usage : undefined;
133
+ }
134
+
135
+ /** Set terminal status and flush. */
136
+ finalize(status: "completed" | "failed" | "aborted"): void {
137
+ this.#progress.status = status;
138
+ this.#progress.currentTool = undefined;
139
+ this.#progress.currentToolArgs = undefined;
140
+ this.#progress.currentToolStartMs = undefined;
141
+ this.#flushProgress();
142
+ }
143
+
144
+ /** Clean up timers and subscription. */
145
+ dispose(): void {
146
+ if (this.#progressTimeoutId) {
147
+ clearTimeout(this.#progressTimeoutId);
148
+ this.#progressTimeoutId = undefined;
149
+ }
150
+ this.#unsubscribe?.();
151
+ }
152
+
153
+ #processEvent(event: AgentEvent): void {
154
+ const now = Date.now();
155
+ let flush = false;
156
+
157
+ switch (event.type) {
158
+ case "message_start":
159
+ if (event.message?.role === "assistant") {
160
+ this.#resetRecentOutput();
161
+ }
162
+ break;
163
+ case "tool_execution_start":
164
+ this.#handleToolStart(event, now);
165
+ flush = true;
166
+ break;
167
+ case "tool_execution_end":
168
+ this.#handleToolEnd(event, now);
169
+ flush = true;
170
+ break;
171
+ case "message_update":
172
+ this.#handleMessageUpdate(event);
173
+ break;
174
+ case "message_end":
175
+ this.#handleMessageEnd(event);
176
+ break;
177
+ case "agent_end":
178
+ flush = true;
179
+ break;
180
+ }
181
+
182
+ this.#scheduleProgress(flush);
183
+ }
184
+
185
+ // -- Tool events --
186
+
187
+ #handleToolStart(event: Extract<AgentEvent, { type: "tool_execution_start" }>, now: number): void {
188
+ const toolArgs = extractToolArgsPreview(event.args ?? {});
189
+
190
+ this.#progress.toolCount++;
191
+ this.#progress.currentTool = event.toolName;
192
+ this.#progress.currentToolArgs = toolArgs;
193
+ this.#progress.currentToolStartMs = now;
194
+ const intent = event.intent?.trim();
195
+ if (intent) {
196
+ this.#progress.lastIntent = intent;
197
+ }
198
+
199
+ if (this.#progress.toolHistory.length < 50) {
200
+ this.#progress.toolHistory.push({
201
+ tool: event.toolName,
202
+ args: toolArgs,
203
+ status: "running",
204
+ });
205
+ }
206
+ }
207
+
208
+ #handleToolEnd(event: Extract<AgentEvent, { type: "tool_execution_end" }>, now: number): void {
209
+ const isError = !!(event as { isError?: boolean }).isError;
210
+
211
+ for (let i = this.#progress.toolHistory.length - 1; i >= 0; i--) {
212
+ if (this.#progress.toolHistory[i].status === "running") {
213
+ this.#progress.toolHistory[i].status = isError ? "error" : "success";
214
+ break;
215
+ }
216
+ }
217
+
218
+ if (this.#progress.currentTool) {
219
+ this.#progress.recentTools.unshift({
220
+ tool: this.#progress.currentTool,
221
+ args: this.#progress.currentToolArgs || "",
222
+ endMs: now,
223
+ });
224
+ if (this.#progress.recentTools.length > 5) {
225
+ this.#progress.recentTools.pop();
226
+ }
227
+ }
228
+ this.#progress.currentTool = undefined;
229
+ this.#progress.currentToolArgs = undefined;
230
+ this.#progress.currentToolStartMs = undefined;
231
+
232
+ const handler = subprocessToolRegistry.getHandler(event.toolName);
233
+ if (handler) {
234
+ const eventArgs = (event as { args?: Record<string, unknown> }).args ?? {};
235
+ if (
236
+ handler.shouldTerminate?.({
237
+ toolName: event.toolName,
238
+ toolCallId: event.toolCallId,
239
+ args: eventArgs,
240
+ result: event.result,
241
+ isError: event.isError,
242
+ })
243
+ ) {
244
+ this.#options.onTerminateRequest?.();
245
+ }
246
+ }
247
+ }
248
+ // -- Message events --
249
+
250
+ #handleMessageUpdate(event: Extract<AgentEvent, { type: "message_update" }>): void {
251
+ if (event.message?.role !== "assistant") return;
252
+ const assistantEvent = (
253
+ event as AgentEvent & {
254
+ assistantMessageEvent?: { type?: string; delta?: string };
255
+ }
256
+ ).assistantMessageEvent;
257
+ if (assistantEvent?.type === "text_delta" && typeof assistantEvent.delta === "string") {
258
+ this.#appendRecentOutputTail(assistantEvent.delta);
259
+ return;
260
+ }
261
+ if (assistantEvent && assistantEvent.type !== "text_delta") {
262
+ return;
263
+ }
264
+ const content =
265
+ getMessageField(event.message, "content") ?? (event as AgentEvent & { content?: unknown }).content;
266
+ if (content && Array.isArray(content)) {
267
+ this.#replaceRecentOutputFromContent(content);
268
+ }
269
+ }
270
+
271
+ #handleMessageEnd(event: Extract<AgentEvent, { type: "message_end" }>): void {
272
+ const role = event.message?.role;
273
+ const messageUsage = getMessageField(event.message, "usage") ?? (event as AgentEvent & { usage?: unknown }).usage;
274
+ if (!messageUsage || typeof messageUsage !== "object") return;
275
+
276
+ this.#progress.tokens += getUsageTokens(messageUsage);
277
+
278
+ if (role === "assistant") {
279
+ this.#hasUsage = true;
280
+ const u = messageUsage as Record<string, unknown>;
281
+ this.#usage.input += getNumberField(u, "input") ?? 0;
282
+ this.#usage.output += getNumberField(u, "output") ?? 0;
283
+ this.#usage.cacheRead += getNumberField(u, "cacheRead") ?? 0;
284
+ this.#usage.cacheWrite += getNumberField(u, "cacheWrite") ?? 0;
285
+ this.#usage.totalTokens += getNumberField(u, "totalTokens") ?? 0;
286
+ const costRecord = (u as { cost?: Record<string, unknown> }).cost;
287
+ if (costRecord) {
288
+ this.#usage.cost.input += getNumberField(costRecord, "input") ?? 0;
289
+ this.#usage.cost.output += getNumberField(costRecord, "output") ?? 0;
290
+ this.#usage.cost.cacheRead += getNumberField(costRecord, "cacheRead") ?? 0;
291
+ this.#usage.cost.cacheWrite += getNumberField(costRecord, "cacheWrite") ?? 0;
292
+ this.#usage.cost.total += getNumberField(costRecord, "total") ?? 0;
293
+ }
294
+ }
295
+ }
296
+
297
+ // -- Recent output tracking --
298
+
299
+ #resetRecentOutput(): void {
300
+ this.#recentOutputTail = "";
301
+ this.#progress.recentOutput = [];
302
+ }
303
+
304
+ #appendRecentOutputTail(text: string): void {
305
+ if (!text) return;
306
+ this.#recentOutputTail += text;
307
+ if (this.#recentOutputTail.length > RECENT_OUTPUT_TAIL_BYTES) {
308
+ this.#recentOutputTail = this.#recentOutputTail.slice(-RECENT_OUTPUT_TAIL_BYTES);
309
+ }
310
+ this.#updateRecentOutputLines();
311
+ }
312
+
313
+ #replaceRecentOutputFromContent(content: unknown[]): void {
314
+ this.#recentOutputTail = "";
315
+ for (const block of content) {
316
+ if (!block || typeof block !== "object") continue;
317
+ const record = block as { type?: unknown; text?: unknown };
318
+ if (record.type !== "text" || typeof record.text !== "string") continue;
319
+ if (!record.text) continue;
320
+ this.#recentOutputTail += record.text;
321
+ if (this.#recentOutputTail.length > RECENT_OUTPUT_TAIL_BYTES) {
322
+ this.#recentOutputTail = this.#recentOutputTail.slice(-RECENT_OUTPUT_TAIL_BYTES);
323
+ }
324
+ }
325
+ this.#updateRecentOutputLines();
326
+ }
327
+
328
+ #updateRecentOutputLines(): void {
329
+ const lines = this.#recentOutputTail.split("\n").filter(line => line.trim());
330
+ this.#progress.recentOutput = lines.slice(-8).reverse();
331
+ }
332
+
333
+ // -- Progress coalescing --
334
+
335
+ #emitProgressNow(): void {
336
+ this.#progress.durationMs = Date.now() - this.#options.startTime;
337
+ this.#options.onProgress?.({ ...this.#progress });
338
+ this.#lastProgressEmitMs = Date.now();
339
+ }
340
+
341
+ #flushProgress(): void {
342
+ if (this.#progressTimeoutId) {
343
+ clearTimeout(this.#progressTimeoutId);
344
+ this.#progressTimeoutId = undefined;
345
+ }
346
+ this.#emitProgressNow();
347
+ }
348
+
349
+ #scheduleProgress(flush = false): void {
350
+ if (flush) {
351
+ this.#flushProgress();
352
+ return;
353
+ }
354
+ const now = Date.now();
355
+ const elapsed = now - this.#lastProgressEmitMs;
356
+ if (this.#lastProgressEmitMs === 0 || elapsed >= this.#coalesceMs) {
357
+ this.#flushProgress();
358
+ return;
359
+ }
360
+ if (this.#progressTimeoutId) return;
361
+ this.#progressTimeoutId = setTimeout(() => {
362
+ this.#progressTimeoutId = undefined;
363
+ this.#emitProgressNow();
364
+ }, this.#coalesceMs - elapsed);
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Extract the last assistant message text from an agent_end event.
370
+ * Callers use this to get subagent output from the EventBus.
371
+ */
372
+ export function extractAgentOutput(event: Extract<AgentEvent, { type: "agent_end" }>): string {
373
+ const messages = event.messages;
374
+ if (!messages || !Array.isArray(messages)) return "";
375
+ for (let i = messages.length - 1; i >= 0; i--) {
376
+ const msg = messages[i];
377
+ if ((msg as { role?: string })?.role !== "assistant") continue;
378
+ const content = (msg as { content?: unknown[] })?.content;
379
+ if (!content || !Array.isArray(content)) continue;
380
+ const chunks: string[] = [];
381
+ for (const block of content) {
382
+ if ((block as { type?: string })?.type === "text" && (block as { text?: string })?.text) {
383
+ chunks.push((block as { text: string }).text);
384
+ }
385
+ }
386
+ if (chunks.length > 0) return chunks.join("");
387
+ break;
388
+ }
389
+ return "";
390
+ }