opencode-dux 1.0.0

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 (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +452 -0
  3. package/dist/agents/descriptions.d.ts +6 -0
  4. package/dist/agents/designer.d.ts +2 -0
  5. package/dist/agents/explorer.d.ts +2 -0
  6. package/dist/agents/fixer.d.ts +2 -0
  7. package/dist/agents/index.d.ts +22 -0
  8. package/dist/agents/interpreter.d.ts +2 -0
  9. package/dist/agents/librarian.d.ts +2 -0
  10. package/dist/agents/oracle.d.ts +2 -0
  11. package/dist/agents/orchestrator.d.ts +27 -0
  12. package/dist/agents/overrides.d.ts +18 -0
  13. package/dist/agents/prompt-blocks.d.ts +97 -0
  14. package/dist/agents/steward.d.ts +3 -0
  15. package/dist/cli/config-io.d.ts +24 -0
  16. package/dist/cli/config-manager.d.ts +4 -0
  17. package/dist/cli/index.d.ts +2 -0
  18. package/dist/cli/index.js +1006 -0
  19. package/dist/cli/install.d.ts +2 -0
  20. package/dist/cli/mcps.d.ts +13 -0
  21. package/dist/cli/model-key-normalization.d.ts +1 -0
  22. package/dist/cli/paths.d.ts +35 -0
  23. package/dist/cli/providers.d.ts +137 -0
  24. package/dist/cli/skills.d.ts +22 -0
  25. package/dist/cli/system.d.ts +5 -0
  26. package/dist/cli/types.d.ts +38 -0
  27. package/dist/config/constants.d.ts +12 -0
  28. package/dist/config/index.d.ts +4 -0
  29. package/dist/config/loader.d.ts +40 -0
  30. package/dist/config/runtime-preset.d.ts +12 -0
  31. package/dist/config/schema.d.ts +281 -0
  32. package/dist/config/utils.d.ts +10 -0
  33. package/dist/discovery/local/types.d.ts +79 -0
  34. package/dist/discovery/local.d.ts +73 -0
  35. package/dist/discovery/mcp-servers.d.ts +88 -0
  36. package/dist/discovery/skills.d.ts +94 -0
  37. package/dist/hooks/apply-patch/codec.d.ts +7 -0
  38. package/dist/hooks/apply-patch/errors.d.ts +25 -0
  39. package/dist/hooks/apply-patch/execution-context.d.ts +27 -0
  40. package/dist/hooks/apply-patch/index.d.ts +15 -0
  41. package/dist/hooks/apply-patch/matching.d.ts +26 -0
  42. package/dist/hooks/apply-patch/operations.d.ts +3 -0
  43. package/dist/hooks/apply-patch/patch.d.ts +2 -0
  44. package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
  45. package/dist/hooks/apply-patch/resolution.d.ts +19 -0
  46. package/dist/hooks/apply-patch/rewrite.d.ts +7 -0
  47. package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
  48. package/dist/hooks/apply-patch/types.d.ts +80 -0
  49. package/dist/hooks/auto-update-checker/cache.d.ts +11 -0
  50. package/dist/hooks/auto-update-checker/checker.d.ts +32 -0
  51. package/dist/hooks/auto-update-checker/constants.d.ts +11 -0
  52. package/dist/hooks/auto-update-checker/index.d.ts +18 -0
  53. package/dist/hooks/auto-update-checker/types.d.ts +22 -0
  54. package/dist/hooks/chat-headers.d.ts +16 -0
  55. package/dist/hooks/context-pressure-reminder/index.d.ts +33 -0
  56. package/dist/hooks/delegate-task-retry/guidance.d.ts +2 -0
  57. package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
  58. package/dist/hooks/delegate-task-retry/index.d.ts +4 -0
  59. package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
  60. package/dist/hooks/filter-available-skills/index.d.ts +32 -0
  61. package/dist/hooks/foreground-fallback/index.d.ts +72 -0
  62. package/dist/hooks/image-hook.d.ts +5 -0
  63. package/dist/hooks/index.d.ts +14 -0
  64. package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
  65. package/dist/hooks/json-error-recovery/index.d.ts +1 -0
  66. package/dist/hooks/phase-reminder/index.d.ts +26 -0
  67. package/dist/hooks/post-file-tool-nudge/index.d.ts +19 -0
  68. package/dist/hooks/task-session-manager/index.d.ts +52 -0
  69. package/dist/hooks/todo-continuation/index.d.ts +53 -0
  70. package/dist/hooks/todo-continuation/todo-hygiene.d.ts +35 -0
  71. package/dist/index.d.ts +5 -0
  72. package/dist/index.js +31782 -0
  73. package/dist/mcp/context7.d.ts +6 -0
  74. package/dist/mcp/grep-app.d.ts +6 -0
  75. package/dist/mcp/index.d.ts +13 -0
  76. package/dist/mcp/types.d.ts +12 -0
  77. package/dist/mcp/websearch.d.ts +9 -0
  78. package/dist/skills/registry.d.ts +29 -0
  79. package/dist/subscriptions/accounts-store.d.ts +57 -0
  80. package/dist/subscriptions/index.d.ts +13 -0
  81. package/dist/subscriptions/neuralwatt-scraper.d.ts +14 -0
  82. package/dist/subscriptions/opencode-go-scraper.d.ts +27 -0
  83. package/dist/subscriptions/types.d.ts +115 -0
  84. package/dist/subscriptions/usage-service.d.ts +74 -0
  85. package/dist/tools/ast-grep/cli.d.ts +15 -0
  86. package/dist/tools/ast-grep/constants.d.ts +25 -0
  87. package/dist/tools/ast-grep/downloader.d.ts +5 -0
  88. package/dist/tools/ast-grep/index.d.ts +10 -0
  89. package/dist/tools/ast-grep/tools.d.ts +3 -0
  90. package/dist/tools/ast-grep/types.d.ts +30 -0
  91. package/dist/tools/ast-grep/utils.d.ts +4 -0
  92. package/dist/tools/delegate.d.ts +14 -0
  93. package/dist/tools/index.d.ts +5 -0
  94. package/dist/tools/preset-manager.d.ts +27 -0
  95. package/dist/tools/smartfetch/binary.d.ts +3 -0
  96. package/dist/tools/smartfetch/cache.d.ts +6 -0
  97. package/dist/tools/smartfetch/constants.d.ts +12 -0
  98. package/dist/tools/smartfetch/index.d.ts +3 -0
  99. package/dist/tools/smartfetch/network.d.ts +38 -0
  100. package/dist/tools/smartfetch/secondary-model.d.ts +28 -0
  101. package/dist/tools/smartfetch/tool.d.ts +3 -0
  102. package/dist/tools/smartfetch/types.d.ts +122 -0
  103. package/dist/tools/smartfetch/utils.d.ts +18 -0
  104. package/dist/tui-state.d.ts +168 -0
  105. package/dist/tui.d.ts +37 -0
  106. package/dist/tui.js +1896 -0
  107. package/dist/utils/agent-variant.d.ts +63 -0
  108. package/dist/utils/compat.d.ts +30 -0
  109. package/dist/utils/env.d.ts +1 -0
  110. package/dist/utils/index.d.ts +9 -0
  111. package/dist/utils/internal-initiator.d.ts +6 -0
  112. package/dist/utils/logger.d.ts +8 -0
  113. package/dist/utils/polling.d.ts +21 -0
  114. package/dist/utils/session-manager.d.ts +55 -0
  115. package/dist/utils/session.d.ts +90 -0
  116. package/dist/utils/subagent-depth.d.ts +35 -0
  117. package/dist/utils/system-collapse.d.ts +6 -0
  118. package/dist/utils/task.d.ts +4 -0
  119. package/dist/utils/zip-extractor.d.ts +1 -0
  120. package/index.ts +1 -0
  121. package/opencode-dux.schema.json +634 -0
  122. package/package.json +103 -0
  123. package/src/agents/descriptions.ts +55 -0
  124. package/src/agents/designer.test.ts +86 -0
  125. package/src/agents/designer.ts +154 -0
  126. package/src/agents/display-name.test.ts +186 -0
  127. package/src/agents/explorer.test.ts +79 -0
  128. package/src/agents/explorer.ts +144 -0
  129. package/src/agents/fixer.test.ts +79 -0
  130. package/src/agents/fixer.ts +145 -0
  131. package/src/agents/index.test.ts +472 -0
  132. package/src/agents/index.ts +248 -0
  133. package/src/agents/interpreter.ts +136 -0
  134. package/src/agents/librarian.test.ts +80 -0
  135. package/src/agents/librarian.ts +145 -0
  136. package/src/agents/oracle.test.ts +89 -0
  137. package/src/agents/oracle.ts +184 -0
  138. package/src/agents/orchestrator.test.ts +116 -0
  139. package/src/agents/orchestrator.ts +574 -0
  140. package/src/agents/overrides.ts +95 -0
  141. package/src/agents/prompt-blocks.test.ts +114 -0
  142. package/src/agents/prompt-blocks.ts +640 -0
  143. package/src/agents/steward.ts +146 -0
  144. package/src/cli/config-io.test.ts +536 -0
  145. package/src/cli/config-io.ts +473 -0
  146. package/src/cli/config-manager.test.ts +141 -0
  147. package/src/cli/config-manager.ts +4 -0
  148. package/src/cli/index.ts +88 -0
  149. package/src/cli/install.ts +282 -0
  150. package/src/cli/mcps.test.ts +62 -0
  151. package/src/cli/mcps.ts +39 -0
  152. package/src/cli/model-key-normalization.test.ts +21 -0
  153. package/src/cli/model-key-normalization.ts +60 -0
  154. package/src/cli/paths.test.ts +167 -0
  155. package/src/cli/paths.ts +144 -0
  156. package/src/cli/providers.test.ts +118 -0
  157. package/src/cli/providers.ts +141 -0
  158. package/src/cli/skills.test.ts +111 -0
  159. package/src/cli/skills.ts +103 -0
  160. package/src/cli/system.test.ts +91 -0
  161. package/src/cli/system.ts +180 -0
  162. package/src/cli/types.ts +43 -0
  163. package/src/config/constants.ts +58 -0
  164. package/src/config/index.ts +4 -0
  165. package/src/config/loader.test.ts +1194 -0
  166. package/src/config/loader.ts +269 -0
  167. package/src/config/model-resolution.test.ts +176 -0
  168. package/src/config/runtime-preset.test.ts +61 -0
  169. package/src/config/runtime-preset.ts +37 -0
  170. package/src/config/schema.ts +248 -0
  171. package/src/config/utils.test.ts +41 -0
  172. package/src/config/utils.ts +23 -0
  173. package/src/discovery/local/types.ts +85 -0
  174. package/src/discovery/local.ts +322 -0
  175. package/src/discovery/mcp-servers.ts +804 -0
  176. package/src/discovery/skills.ts +959 -0
  177. package/src/hooks/apply-patch/codec.test.ts +184 -0
  178. package/src/hooks/apply-patch/codec.ts +352 -0
  179. package/src/hooks/apply-patch/errors.ts +117 -0
  180. package/src/hooks/apply-patch/execution-context.ts +432 -0
  181. package/src/hooks/apply-patch/hook.test.ts +768 -0
  182. package/src/hooks/apply-patch/index.ts +126 -0
  183. package/src/hooks/apply-patch/matching.test.ts +215 -0
  184. package/src/hooks/apply-patch/matching.ts +586 -0
  185. package/src/hooks/apply-patch/operations.test.ts +1535 -0
  186. package/src/hooks/apply-patch/operations.ts +3 -0
  187. package/src/hooks/apply-patch/patch.ts +9 -0
  188. package/src/hooks/apply-patch/prepared-changes.ts +400 -0
  189. package/src/hooks/apply-patch/resolution.test.ts +420 -0
  190. package/src/hooks/apply-patch/resolution.ts +437 -0
  191. package/src/hooks/apply-patch/rewrite.ts +496 -0
  192. package/src/hooks/apply-patch/test-helpers.ts +52 -0
  193. package/src/hooks/apply-patch/types.ts +111 -0
  194. package/src/hooks/auto-update-checker/cache.test.ts +179 -0
  195. package/src/hooks/auto-update-checker/cache.ts +188 -0
  196. package/src/hooks/auto-update-checker/checker.test.ts +159 -0
  197. package/src/hooks/auto-update-checker/checker.ts +308 -0
  198. package/src/hooks/auto-update-checker/constants.ts +33 -0
  199. package/src/hooks/auto-update-checker/index.test.ts +282 -0
  200. package/src/hooks/auto-update-checker/index.ts +225 -0
  201. package/src/hooks/auto-update-checker/types.ts +26 -0
  202. package/src/hooks/chat-headers.test.ts +236 -0
  203. package/src/hooks/chat-headers.ts +97 -0
  204. package/src/hooks/context-pressure-reminder/index.test.ts +179 -0
  205. package/src/hooks/context-pressure-reminder/index.ts +137 -0
  206. package/src/hooks/delegate-task-retry/guidance.ts +41 -0
  207. package/src/hooks/delegate-task-retry/hook.ts +23 -0
  208. package/src/hooks/delegate-task-retry/index.test.ts +38 -0
  209. package/src/hooks/delegate-task-retry/index.ts +7 -0
  210. package/src/hooks/delegate-task-retry/patterns.ts +79 -0
  211. package/src/hooks/filter-available-skills/index.test.ts +297 -0
  212. package/src/hooks/filter-available-skills/index.ts +160 -0
  213. package/src/hooks/foreground-fallback/index.test.ts +624 -0
  214. package/src/hooks/foreground-fallback/index.ts +374 -0
  215. package/src/hooks/image-hook.ts +6 -0
  216. package/src/hooks/index.ts +17 -0
  217. package/src/hooks/json-error-recovery/hook.ts +73 -0
  218. package/src/hooks/json-error-recovery/index.test.ts +111 -0
  219. package/src/hooks/json-error-recovery/index.ts +6 -0
  220. package/src/hooks/phase-reminder/index.test.ts +74 -0
  221. package/src/hooks/phase-reminder/index.ts +85 -0
  222. package/src/hooks/post-file-tool-nudge/index.test.ts +94 -0
  223. package/src/hooks/post-file-tool-nudge/index.ts +63 -0
  224. package/src/hooks/task-session-manager/index.test.ts +833 -0
  225. package/src/hooks/task-session-manager/index.ts +434 -0
  226. package/src/hooks/todo-continuation/index.test.ts +3026 -0
  227. package/src/hooks/todo-continuation/index.ts +878 -0
  228. package/src/hooks/todo-continuation/todo-hygiene.test.ts +204 -0
  229. package/src/hooks/todo-continuation/todo-hygiene.ts +207 -0
  230. package/src/index.ts +1672 -0
  231. package/src/mcp/context7.ts +14 -0
  232. package/src/mcp/grep-app.ts +11 -0
  233. package/src/mcp/index.test.ts +96 -0
  234. package/src/mcp/index.ts +66 -0
  235. package/src/mcp/types.ts +16 -0
  236. package/src/mcp/websearch.ts +47 -0
  237. package/src/skills/codemap/README.md +60 -0
  238. package/src/skills/codemap/SKILL.md +174 -0
  239. package/src/skills/codemap/scripts/codemap.mjs +483 -0
  240. package/src/skills/codemap/scripts/codemap.test.ts +129 -0
  241. package/src/skills/registry.ts +218 -0
  242. package/src/skills/simplify/README.md +19 -0
  243. package/src/skills/simplify/SKILL.md +138 -0
  244. package/src/subscriptions/accounts-store.test.ts +236 -0
  245. package/src/subscriptions/accounts-store.ts +184 -0
  246. package/src/subscriptions/index.ts +30 -0
  247. package/src/subscriptions/neuralwatt-scraper.ts +108 -0
  248. package/src/subscriptions/opencode-go-scraper.ts +301 -0
  249. package/src/subscriptions/types.ts +145 -0
  250. package/src/subscriptions/usage-service.test.ts +202 -0
  251. package/src/subscriptions/usage-service.ts +651 -0
  252. package/src/tools/ast-grep/cli.ts +257 -0
  253. package/src/tools/ast-grep/constants.ts +214 -0
  254. package/src/tools/ast-grep/downloader.ts +131 -0
  255. package/src/tools/ast-grep/index.ts +24 -0
  256. package/src/tools/ast-grep/tools.ts +117 -0
  257. package/src/tools/ast-grep/types.ts +51 -0
  258. package/src/tools/ast-grep/utils.ts +126 -0
  259. package/src/tools/delegate-handoff.test.ts +18 -0
  260. package/src/tools/delegate.ts +508 -0
  261. package/src/tools/index.ts +8 -0
  262. package/src/tools/preset-manager.test.ts +795 -0
  263. package/src/tools/preset-manager.ts +332 -0
  264. package/src/tools/smartfetch/binary.ts +58 -0
  265. package/src/tools/smartfetch/cache.test.ts +34 -0
  266. package/src/tools/smartfetch/cache.ts +112 -0
  267. package/src/tools/smartfetch/constants.ts +29 -0
  268. package/src/tools/smartfetch/index.ts +8 -0
  269. package/src/tools/smartfetch/network.test.ts +178 -0
  270. package/src/tools/smartfetch/network.ts +614 -0
  271. package/src/tools/smartfetch/secondary-model.test.ts +85 -0
  272. package/src/tools/smartfetch/secondary-model.ts +276 -0
  273. package/src/tools/smartfetch/tool.test.ts +60 -0
  274. package/src/tools/smartfetch/tool.ts +832 -0
  275. package/src/tools/smartfetch/types.ts +135 -0
  276. package/src/tools/smartfetch/utils.test.ts +24 -0
  277. package/src/tools/smartfetch/utils.ts +456 -0
  278. package/src/tui-state.test.ts +867 -0
  279. package/src/tui-state.ts +1255 -0
  280. package/src/tui.test.ts +336 -0
  281. package/src/tui.ts +1539 -0
  282. package/src/utils/agent-variant.test.ts +244 -0
  283. package/src/utils/agent-variant.ts +187 -0
  284. package/src/utils/compat.ts +91 -0
  285. package/src/utils/env.ts +12 -0
  286. package/src/utils/index.ts +9 -0
  287. package/src/utils/internal-initiator.ts +28 -0
  288. package/src/utils/logger.test.ts +220 -0
  289. package/src/utils/logger.ts +136 -0
  290. package/src/utils/polling.test.ts +191 -0
  291. package/src/utils/polling.ts +67 -0
  292. package/src/utils/session-manager.test.ts +173 -0
  293. package/src/utils/session-manager.ts +356 -0
  294. package/src/utils/session.test.ts +110 -0
  295. package/src/utils/session.ts +389 -0
  296. package/src/utils/subagent-depth.test.ts +170 -0
  297. package/src/utils/subagent-depth.ts +75 -0
  298. package/src/utils/system-collapse.test.ts +86 -0
  299. package/src/utils/system-collapse.ts +24 -0
  300. package/src/utils/task.test.ts +24 -0
  301. package/src/utils/task.ts +20 -0
  302. package/src/utils/zip-extractor.ts +102 -0
@@ -0,0 +1,508 @@
1
+ import type { ToolDefinition } from '@opencode-ai/plugin';
2
+ import { tool } from '@opencode-ai/plugin';
3
+ import { normalizeSkillConfig } from '../cli/skills';
4
+ import type { PluginConfig } from '../config';
5
+ import { ALL_AGENT_NAMES } from '../config/constants';
6
+ import { getAgentOverride } from '../config/utils';
7
+ import {
8
+ recordDelegatedSubagentSession,
9
+ recordSessionDone,
10
+ } from '../tui-state';
11
+ import {
12
+ extractAssistantTextAfterPrompt,
13
+ extractLatestUserImageParts,
14
+ extractSessionResult,
15
+ normalizeImagePartsForChildPrompt,
16
+ type PromptBody,
17
+ type PromptBodyPart,
18
+ parseModelReference,
19
+ promptWithTimeout,
20
+ } from '../utils/session';
21
+ import type { SubagentDepthTracker } from '../utils/subagent-depth';
22
+
23
+ /**
24
+ * Mutex for serializing blocking delegate_subagent calls.
25
+ * OpenCode runs multiple tool calls from the same LLM turn in parallel.
26
+ * This ensures steward->oracle->fixer ordering at the runtime level.
27
+ * Fire-forget calls bypass the mutex and run in parallel as intended.
28
+ */
29
+ class BlockingMutex {
30
+ private current: Promise<void> | null = null;
31
+ private resolveCurrent: (() => void) | null = null;
32
+
33
+ async acquire(): Promise<void> {
34
+ while (this.current) {
35
+ await this.current;
36
+ }
37
+ this.current = new Promise<void>((resolve) => {
38
+ this.resolveCurrent = resolve;
39
+ });
40
+ }
41
+
42
+ release(): void {
43
+ const resolve = this.resolveCurrent;
44
+ this.current = null;
45
+ this.resolveCurrent = null;
46
+ resolve?.();
47
+ }
48
+ }
49
+
50
+ const blockingMutex = new BlockingMutex();
51
+
52
+ /**
53
+ * When true, blocking `delegate_subagent` keeps the child session open and
54
+ * appends `<delegate_session_continue .../>` for `continue_session_id` flows.
55
+ */
56
+ export function subagentOutputRequestsUserHandoff(text: string): boolean {
57
+ return text.includes('<needs_user>');
58
+ }
59
+
60
+ type OpencodeClient = import('@opencode-ai/plugin').PluginInput['client'];
61
+
62
+ const VARIANT_OPTIONS = ['low', 'medium', 'high', 'max'] as const;
63
+ const MODE_OPTIONS = ['blocking', 'fire_forget'] as const;
64
+
65
+ export function createDelegateTools(
66
+ ctx: { client: OpencodeClient; directory: string },
67
+ config: PluginConfig | undefined,
68
+ depthTracker: SubagentDepthTracker | undefined,
69
+ ): Record<string, ToolDefinition> {
70
+ const directory = ctx.directory;
71
+
72
+ const subagentOptions: readonly string[] = [
73
+ ...ALL_AGENT_NAMES.filter((name) => name !== 'orchestrator'),
74
+ ];
75
+
76
+ function recordSessionTree(
77
+ sessionId: string,
78
+ parentSessionId: string,
79
+ agent: string,
80
+ variant?: string,
81
+ mode?: 'blocking' | 'fire_forget',
82
+ ): void {
83
+ recordDelegatedSubagentSession({
84
+ sessionID: sessionId,
85
+ parentSessionId,
86
+ agent,
87
+ variant,
88
+ mode,
89
+ });
90
+ }
91
+
92
+ function buildAgentToolsHint(agentName: string, _taskPrompt: string): string {
93
+ const override = getAgentOverride(config, agentName);
94
+ if (!override?.skills && !override?.mcps) return '';
95
+
96
+ const lines: string[] = [];
97
+
98
+ // Skills hint
99
+ if (override.skills) {
100
+ const normalized = normalizeSkillConfig(override.skills);
101
+ if (normalized.alwaysLoad.length > 0) {
102
+ lines.push(
103
+ `When relevant to the task, please use these skills first: ${normalized.alwaysLoad.join(', ')}.`,
104
+ );
105
+ }
106
+ }
107
+
108
+ // MCPs hint
109
+ if (override.mcps) {
110
+ const normalized = normalizeSkillConfig(override.mcps);
111
+ if (normalized.alwaysLoad.length > 0) {
112
+ lines.push(
113
+ `When relevant to the task, please use these MCP tools first: ${normalized.alwaysLoad.join(', ')}.`,
114
+ );
115
+ }
116
+ }
117
+
118
+ if (lines.length === 0) return '';
119
+
120
+ return `<skill_requirements>\n${lines.join('\n')}\n</skill_requirements>\n\n`;
121
+ }
122
+
123
+ async function runAgentSession(options: {
124
+ parentSessionId: string;
125
+ title: string;
126
+ agent: string;
127
+ model: string;
128
+ variant: string | undefined;
129
+ promptText: string;
130
+ timeout: number;
131
+ promptParts?: PromptBodyPart[];
132
+ /** Resume an open child session after a needs_user handoff; skips create */
133
+ continueSessionId?: string;
134
+ }): Promise<{ text: string; openSessionId?: string }> {
135
+ const modelRef = parseModelReference(options.model);
136
+ if (!modelRef) {
137
+ throw new Error(`Invalid model format: ${options.model}`);
138
+ }
139
+
140
+ let sessionId: string | undefined;
141
+ let keepChildSessionOpen = false;
142
+ const isContinuation = Boolean(options.continueSessionId?.trim());
143
+
144
+ try {
145
+ if (isContinuation) {
146
+ sessionId = options.continueSessionId?.trim();
147
+ if (!sessionId) {
148
+ throw new Error('continue_session_id was empty');
149
+ }
150
+ } else {
151
+ const session = await ctx.client.session.create({
152
+ body: {
153
+ parentID: options.parentSessionId,
154
+ title: options.title,
155
+ },
156
+ query: { directory },
157
+ });
158
+
159
+ if (!session.data?.id) {
160
+ throw new Error('Failed to create session');
161
+ }
162
+
163
+ sessionId = session.data.id;
164
+
165
+ recordSessionTree(
166
+ sessionId,
167
+ options.parentSessionId,
168
+ options.agent,
169
+ options.variant,
170
+ 'blocking',
171
+ );
172
+
173
+ if (depthTracker) {
174
+ const registered = depthTracker.registerChild(
175
+ options.parentSessionId,
176
+ sessionId,
177
+ );
178
+ if (!registered) {
179
+ throw new Error('Subagent depth exceeded');
180
+ }
181
+ }
182
+
183
+ }
184
+
185
+ if (!sessionId) {
186
+ throw new Error('Failed to obtain subagent session id');
187
+ }
188
+
189
+ const hint = buildAgentToolsHint(options.agent, options.promptText);
190
+ const effectivePrompt = hint
191
+ ? `${hint}${options.promptText}`
192
+ : options.promptText;
193
+
194
+ const parts: PromptBodyPart[] = options.promptParts?.length
195
+ ? [...options.promptParts, { type: 'text', text: effectivePrompt }]
196
+ : [{ type: 'text', text: effectivePrompt }];
197
+
198
+ const body: PromptBody = {
199
+ agent: options.agent,
200
+ model: modelRef,
201
+ tools: { task: false },
202
+ parts,
203
+ };
204
+
205
+ if (options.variant) {
206
+ body.variant = options.variant;
207
+ }
208
+
209
+ await promptWithTimeout(
210
+ ctx.client,
211
+ {
212
+ path: { id: sessionId },
213
+ body,
214
+ query: { directory },
215
+ },
216
+ options.timeout,
217
+ );
218
+
219
+ const extraction = await extractAssistantTextAfterPrompt(
220
+ ctx.client,
221
+ sessionId,
222
+ directory,
223
+ );
224
+
225
+ if (extraction.empty) {
226
+ throw new Error('Empty response from provider');
227
+ }
228
+
229
+ const text = extraction.text;
230
+ if (subagentOutputRequestsUserHandoff(text)) {
231
+ keepChildSessionOpen = true;
232
+ return { text, openSessionId: sessionId };
233
+ }
234
+
235
+ recordSessionDone(sessionId);
236
+ return { text };
237
+ } finally {
238
+ if (sessionId && !keepChildSessionOpen) {
239
+ try {
240
+ await Promise.race([
241
+ ctx.client.session.abort({ path: { id: sessionId } }),
242
+ new Promise((r) => setTimeout(r, 2000)),
243
+ ]);
244
+ } catch {
245
+ /* abort may fail if session already disposed */
246
+ }
247
+ if (depthTracker) {
248
+ depthTracker.cleanup(sessionId);
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ const delegateSubagent: ToolDefinition = tool({
255
+ description:
256
+ 'Delegate a task to a specialist subagent with explicit variant control. ' +
257
+ 'Always specify variant based on task complexity. ' +
258
+ 'Blocking mode waits for the result; fire_forget returns a session_id to collect later. ' +
259
+ 'If the result includes <delegate_session_continue/>, the child session stayed open for ' +
260
+ 'continue_session_id (same transcript after user clarification).',
261
+ args: {
262
+ agent: tool.schema
263
+ .enum(subagentOptions)
264
+ .describe('Target specialist subagent'),
265
+ prompt: tool.schema
266
+ .string()
267
+ .describe('Detailed task description for the subagent'),
268
+ variant: tool.schema
269
+ .enum(VARIANT_OPTIONS)
270
+ .describe(
271
+ 'Reasoning depth: low (simple), medium (typical), high (complex), max (critical)',
272
+ ),
273
+ mode: tool.schema
274
+ .enum(MODE_OPTIONS)
275
+ .optional()
276
+ .describe(
277
+ 'blocking (default) waits for result; fire_forget returns session_id immediately',
278
+ ),
279
+ model: tool.schema
280
+ .string()
281
+ .optional()
282
+ .describe(
283
+ 'Override the subagent model. Pass for @oracle when you selected a specific model (flash vs pro).',
284
+ ),
285
+ continue_session_id: tool.schema
286
+ .string()
287
+ .optional()
288
+ .describe(
289
+ 'Blocking only: resume the same child session after <needs_user>. Use session_id from <delegate_session_continue> in the prior delegate_subagent result (same agent, model, variant).',
290
+ ),
291
+ },
292
+ execute: async (args, context) => {
293
+ const parentSessionId = context.sessionID;
294
+ const agentName = args.agent;
295
+ const variant = args.variant;
296
+ const mode = args.mode ?? 'blocking';
297
+ const continueSessionId = args.continue_session_id?.trim() || undefined;
298
+
299
+ if (continueSessionId && mode === 'fire_forget') {
300
+ return 'Error: continue_session_id is only valid for blocking delegate_subagent (omit mode or mode: blocking).';
301
+ }
302
+
303
+ // Enforce steward blocking invariant
304
+ if (agentName === 'steward' && mode === 'fire_forget') {
305
+ return 'Error: @steward must always run in blocking mode. Its repo rule citations are required input for all downstream agents (@oracle, @fixer, @designer). Use mode: "blocking" (or omit mode).';
306
+ }
307
+
308
+ const agentOverride = getAgentOverride(config, agentName);
309
+ const effectiveVariant = agentOverride?.variant ?? variant;
310
+
311
+ let model = args.model;
312
+ if (!model && config?.agents?.[agentName]?.model) {
313
+ const rawModel = config.agents[agentName].model;
314
+ model = typeof rawModel === 'string' ? rawModel : undefined;
315
+ }
316
+
317
+ if (!model) {
318
+ return `Error: No model configured for agent "${agentName}"`;
319
+ }
320
+
321
+ let frameImageParts: PromptBodyPart[] = [];
322
+ if (agentName === 'interpreter' && !continueSessionId) {
323
+ const rawFrameParts = await extractLatestUserImageParts(
324
+ ctx.client,
325
+ parentSessionId,
326
+ directory,
327
+ );
328
+ frameImageParts = normalizeImagePartsForChildPrompt(
329
+ rawFrameParts,
330
+ directory,
331
+ );
332
+
333
+ if (rawFrameParts.length > 0 && frameImageParts.length === 0) {
334
+ return (
335
+ 'Error: delegate_subagent(agent: "interpreter") saw image-related parts on the latest user message but could not build a child prompt ' +
336
+ '(no usable `url` and no resolvable `source.path` for a file attachment). ' +
337
+ 'Try saving the image into the workspace and attaching it as a file, or check OpenCode attachment storage.'
338
+ );
339
+ }
340
+
341
+ if (frameImageParts.length === 0) {
342
+ return (
343
+ 'Error: delegate_subagent(agent: "interpreter") found no image attachment parts on the latest user message. ' +
344
+ 'OpenCode stores screenshots as parts with type `file` and mime `image/*` (not type `image`). ' +
345
+ 'If the UI shows placeholders like [Image N] or “img clipboard” in text but this error appears, the session API did not receive file parts - try attaching through the image control, or check OpenCode/provider issues for clipboard vs file attachment.'
346
+ );
347
+ }
348
+ }
349
+
350
+ function partsForPrompt(promptText: string): PromptBodyPart[] {
351
+ return frameImageParts.length > 0
352
+ ? [...frameImageParts, { type: 'text', text: promptText }]
353
+ : [{ type: 'text', text: promptText }];
354
+ }
355
+
356
+ if (mode === 'fire_forget') {
357
+ const modelRef = parseModelReference(model);
358
+ if (!modelRef) {
359
+ return `Error: Invalid model format: ${model}`;
360
+ }
361
+
362
+ try {
363
+ const session = await ctx.client.session.create({
364
+ body: {
365
+ parentID: parentSessionId,
366
+ title: `${agentName} (${effectiveVariant ?? 'default'})`,
367
+ },
368
+ query: { directory },
369
+ });
370
+
371
+ if (!session.data?.id) {
372
+ return 'Error: Failed to create session';
373
+ }
374
+
375
+ const sessionId = session.data.id;
376
+
377
+ // Record in session tree directly
378
+ recordSessionTree(
379
+ sessionId,
380
+ parentSessionId,
381
+ agentName,
382
+ effectiveVariant,
383
+ 'fire_forget',
384
+ );
385
+
386
+ if (depthTracker) {
387
+ depthTracker.registerChild(parentSessionId, sessionId);
388
+ }
389
+
390
+ const hint = buildAgentToolsHint(agentName, args.prompt);
391
+ const effectiveFirePrompt = hint
392
+ ? `${hint}${args.prompt}`
393
+ : args.prompt;
394
+
395
+ const promptBody: PromptBody = {
396
+ agent: agentName,
397
+ model: modelRef,
398
+ tools: { task: false },
399
+ parts: partsForPrompt(effectiveFirePrompt),
400
+ };
401
+
402
+ if (effectiveVariant) {
403
+ promptBody.variant = effectiveVariant;
404
+ }
405
+
406
+ ctx.client.session
407
+ .prompt({
408
+ path: { id: sessionId },
409
+ body: promptBody,
410
+ query: { directory },
411
+ })
412
+ .catch(() => {});
413
+
414
+ return `Launched ${agentName} (variant: ${effectiveVariant ?? 'default'}, mode: fire_forget).\nSession ID: ${sessionId}\nCollect with delegate_collect(session_id: "${sessionId}")`;
415
+ } catch (err) {
416
+ return `Error launching ${agentName}: ${err instanceof Error ? err.message : String(err)}`;
417
+ }
418
+ }
419
+
420
+ // Blocking mode - acquire serialization lock
421
+ await blockingMutex.acquire();
422
+ try {
423
+ const runResult = await runAgentSession({
424
+ parentSessionId,
425
+ title: `${agentName} (${effectiveVariant ?? 'default'})`,
426
+ agent: agentName,
427
+ model,
428
+ variant: effectiveVariant,
429
+ promptText: args.prompt,
430
+ timeout: 0, // no timeout - let subagents run freely
431
+ promptParts: frameImageParts.length > 0 ? frameImageParts : undefined,
432
+ continueSessionId,
433
+ });
434
+
435
+ let output = `**${agentName}** (variant: ${effectiveVariant ?? 'default'}):\n\n`;
436
+ output += runResult.text;
437
+ if (runResult.openSessionId) {
438
+ output += `\n\n<delegate_session_continue session_id="${runResult.openSessionId}" agent="${agentName}" />`;
439
+ }
440
+ return output;
441
+ } catch (err) {
442
+ return `Error running ${agentName} (variant: ${effectiveVariant ?? 'default'}): ${
443
+ err instanceof Error ? err.message : String(err)
444
+ }`;
445
+ } finally {
446
+ blockingMutex.release();
447
+ }
448
+ },
449
+ });
450
+
451
+ const delegateCollect: ToolDefinition = tool({
452
+ description:
453
+ 'Collect results from a fire_forget delegation. ' +
454
+ 'Pass the session_id returned by delegate_subagent in fire_forget mode.',
455
+ args: {
456
+ session_id: tool.schema
457
+ .string()
458
+ .describe('Session ID from delegate_subagent fire_forget'),
459
+ },
460
+ execute: async (args) => {
461
+ try {
462
+ const sid = args.session_id;
463
+ const statusResult = await (
464
+ ctx.client.session.status as (
465
+ args: Record<string, unknown>,
466
+ ) => Promise<{ data?: Record<string, unknown> }>
467
+ )({ path: { id: sid }, query: { directory } });
468
+
469
+ const status = (statusResult.data as Record<string, unknown>)?.type as
470
+ | string
471
+ | undefined;
472
+
473
+ if (status === 'idle' || status === 'completed' || status === 'error') {
474
+ recordSessionDone(args.session_id);
475
+
476
+ const extraction = await extractSessionResult(
477
+ ctx.client,
478
+ args.session_id,
479
+ { includeReasoning: false, directory },
480
+ );
481
+
482
+ ctx.client.session
483
+ .abort({ path: { id: args.session_id } })
484
+ .catch(() => {});
485
+
486
+ if (depthTracker) {
487
+ depthTracker.cleanup(args.session_id);
488
+ }
489
+
490
+ if (extraction.empty) {
491
+ return 'Session completed but produced no output.';
492
+ }
493
+
494
+ return extraction.text;
495
+ }
496
+
497
+ return `Session still running (status: ${status ?? 'unknown'}). Try again shortly. Use original delegate_subagent session_id.`;
498
+ } catch (err) {
499
+ return `Error collecting result: ${err instanceof Error ? err.message : String(err)}`;
500
+ }
501
+ },
502
+ });
503
+
504
+ return {
505
+ delegate_subagent: delegateSubagent,
506
+ delegate_collect: delegateCollect,
507
+ };
508
+ }
@@ -0,0 +1,8 @@
1
+ export { ast_grep_replace, ast_grep_search } from './ast-grep';
2
+ export {
3
+ createDelegateTools,
4
+ subagentOutputRequestsUserHandoff,
5
+ } from './delegate';
6
+ export type { PresetManager } from './preset-manager';
7
+ export { createPresetManager } from './preset-manager';
8
+ export { createWebfetchTool } from './smartfetch';