@cjhyy/code-shell-core 0.5.0-rc.1 → 0.5.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/dist/agent/agent-definition-registry.d.ts +18 -3
  2. package/dist/agent/agent-definition-registry.js +47 -18
  3. package/dist/agent/agent-definition.d.ts +17 -0
  4. package/dist/agent/agent-definition.js +22 -1
  5. package/dist/arena/arena.js +11 -12
  6. package/dist/arena/context/context-tools.js +2 -4
  7. package/dist/arena/context/within-root.d.ts +7 -0
  8. package/dist/arena/context/within-root.js +15 -0
  9. package/dist/arena/iterate/phases/argue.js +8 -2
  10. package/dist/arena/iterate/phases/revise.js +1 -1
  11. package/dist/arena/iterate/phases/tournament.js +3 -3
  12. package/dist/arena/phases/adjudication.js +1 -4
  13. package/dist/arena/phases/build-consensus.js +1 -4
  14. package/dist/arena/phases/cross-review.js +2 -8
  15. package/dist/arena/phases/debate-rounds.js +1 -4
  16. package/dist/arena/phases/participant-research.js +1 -4
  17. package/dist/arena/phases/planning-detail-expansion.js +8 -10
  18. package/dist/arena/planner.js +0 -1
  19. package/dist/arena/providers/docs.d.ts +1 -0
  20. package/dist/arena/providers/docs.js +5 -2
  21. package/dist/arena/providers/repo.d.ts +1 -0
  22. package/dist/arena/providers/repo.js +5 -2
  23. package/dist/arena/strategies/utils.js +36 -3
  24. package/dist/arena/transitions.d.ts +7 -2
  25. package/dist/arena/transitions.js +9 -4
  26. package/dist/arena/types.d.ts +8 -2
  27. package/dist/automation/cron-expr.d.ts +31 -0
  28. package/dist/automation/cron-expr.js +151 -0
  29. package/dist/automation/index.d.ts +41 -0
  30. package/dist/automation/index.js +39 -0
  31. package/dist/automation/runner.d.ts +67 -0
  32. package/dist/automation/runner.js +56 -0
  33. package/dist/automation/scheduler.d.ts +156 -0
  34. package/dist/automation/scheduler.js +546 -0
  35. package/dist/automation/store.d.ts +39 -0
  36. package/dist/automation/store.js +119 -0
  37. package/dist/automation/write-policy.d.ts +34 -0
  38. package/dist/automation/write-policy.js +113 -0
  39. package/dist/automation/write-run.d.ts +51 -0
  40. package/dist/automation/write-run.js +38 -0
  41. package/dist/capability-control/index.d.ts +5 -0
  42. package/dist/capability-control/index.js +3 -0
  43. package/dist/capability-control/overlay.d.ts +49 -0
  44. package/dist/capability-control/overlay.js +89 -0
  45. package/dist/capability-control/project.d.ts +65 -0
  46. package/dist/capability-control/project.js +147 -0
  47. package/dist/capability-control/service.d.ts +70 -0
  48. package/dist/capability-control/service.js +147 -0
  49. package/dist/capability-control/types.d.ts +62 -0
  50. package/dist/capability-control/types.js +17 -0
  51. package/dist/cli/agent-server-stdio.d.ts +12 -1
  52. package/dist/cli/agent-server-stdio.js +114 -19
  53. package/dist/cli/agent-server-tcp.d.ts +20 -0
  54. package/dist/cli/agent-server-tcp.js +120 -0
  55. package/dist/cli/graceful-shutdown.d.ts +27 -0
  56. package/dist/cli/graceful-shutdown.js +30 -0
  57. package/dist/context/compaction.js +23 -15
  58. package/dist/context/manager.d.ts +0 -20
  59. package/dist/context/manager.js +0 -37
  60. package/dist/cron/cron-runtime.d.ts +2 -0
  61. package/dist/cron/cron-runtime.js +2 -0
  62. package/dist/cron/cron-store.d.ts +2 -0
  63. package/dist/cron/cron-store.js +2 -0
  64. package/dist/cron/scheduler.d.ts +5 -32
  65. package/dist/cron/scheduler.js +5 -113
  66. package/dist/data/openrouter-sync.js +1 -1
  67. package/dist/engine/engine.d.ts +208 -14
  68. package/dist/engine/engine.js +754 -169
  69. package/dist/engine/goal.d.ts +42 -0
  70. package/dist/engine/goal.js +52 -0
  71. package/dist/engine/image-compression.d.ts +81 -0
  72. package/dist/engine/image-compression.js +186 -0
  73. package/dist/engine/image-policy.d.ts +152 -0
  74. package/dist/engine/image-policy.js +165 -0
  75. package/dist/engine/reactive-threshold.d.ts +13 -0
  76. package/dist/engine/reactive-threshold.js +17 -0
  77. package/dist/engine/runtime.js +7 -0
  78. package/dist/engine/session-title.d.ts +13 -0
  79. package/dist/engine/session-title.js +34 -0
  80. package/dist/engine/turn-loop.d.ts +29 -8
  81. package/dist/engine/turn-loop.js +498 -316
  82. package/dist/git/parse-log.d.ts +13 -0
  83. package/dist/git/parse-log.js +21 -0
  84. package/dist/git/utils.d.ts +8 -6
  85. package/dist/git/utils.js +57 -26
  86. package/dist/git/worktree.js +33 -18
  87. package/dist/hooks/events.d.ts +23 -1
  88. package/dist/hooks/goal-stop-hook.d.ts +30 -0
  89. package/dist/hooks/goal-stop-hook.js +102 -0
  90. package/dist/hooks/hook-output.d.ts +33 -0
  91. package/dist/hooks/hook-output.js +79 -0
  92. package/dist/hooks/registry.d.ts +7 -0
  93. package/dist/hooks/registry.js +38 -1
  94. package/dist/hooks/shell-runner.d.ts +2 -0
  95. package/dist/hooks/shell-runner.js +43 -2
  96. package/dist/index.d.ts +31 -6
  97. package/dist/index.js +30 -3
  98. package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
  99. package/dist/llm/capabilities/reasoning-control.js +38 -0
  100. package/dist/llm/capabilities/rules.js +33 -3
  101. package/dist/llm/capabilities/types.d.ts +22 -2
  102. package/dist/llm/clamp-max-tokens.d.ts +11 -0
  103. package/dist/llm/clamp-max-tokens.js +17 -0
  104. package/dist/llm/client-base.d.ts +41 -4
  105. package/dist/llm/client-base.js +67 -13
  106. package/dist/llm/client-factory.d.ts +3 -3
  107. package/dist/llm/client-factory.js +2 -2
  108. package/dist/llm/model-pool.d.ts +15 -11
  109. package/dist/llm/model-pool.js +23 -20
  110. package/dist/llm/provider-catalog.d.ts +2 -2
  111. package/dist/llm/providers/anthropic.d.ts +32 -2
  112. package/dist/llm/providers/anthropic.js +147 -8
  113. package/dist/llm/providers/openai.d.ts +11 -2
  114. package/dist/llm/providers/openai.js +253 -52
  115. package/dist/llm/reasoning-setting.d.ts +51 -0
  116. package/dist/llm/reasoning-setting.js +32 -0
  117. package/dist/llm/stop-reason.d.ts +13 -0
  118. package/dist/llm/stop-reason.js +21 -0
  119. package/dist/llm/strip-vision.d.ts +32 -0
  120. package/dist/llm/strip-vision.js +94 -0
  121. package/dist/llm/types.d.ts +5 -6
  122. package/dist/logging/logger.js +18 -4
  123. package/dist/logging/sanitize-messages.d.ts +10 -0
  124. package/dist/logging/sanitize-messages.js +90 -0
  125. package/dist/lsp/client.js +28 -8
  126. package/dist/lsp/manager.js +2 -1
  127. package/dist/lsp/root-path.d.ts +9 -0
  128. package/dist/lsp/root-path.js +12 -0
  129. package/dist/onboarding.js +18 -11
  130. package/dist/plugins/gitOps.d.ts +34 -0
  131. package/dist/plugins/gitOps.js +59 -3
  132. package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
  133. package/dist/plugins/installer/codex/convertAgents.js +55 -0
  134. package/dist/plugins/installer/codex/convertMcp.d.ts +8 -0
  135. package/dist/plugins/installer/codex/convertMcp.js +45 -0
  136. package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
  137. package/dist/plugins/installer/codex/convertSkills.js +33 -0
  138. package/dist/plugins/installer/detectFormat.d.ts +2 -0
  139. package/dist/plugins/installer/detectFormat.js +6 -0
  140. package/dist/plugins/installer/install.d.ts +7 -0
  141. package/dist/plugins/installer/install.js +99 -0
  142. package/dist/plugins/installer/installFromSource.d.ts +13 -0
  143. package/dist/plugins/installer/installFromSource.js +45 -0
  144. package/dist/plugins/installer/list.d.ts +9 -0
  145. package/dist/plugins/installer/list.js +30 -0
  146. package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
  147. package/dist/plugins/installer/loadPluginAgents.js +23 -0
  148. package/dist/plugins/installer/loadPluginMcp.d.ts +7 -0
  149. package/dist/plugins/installer/loadPluginMcp.js +74 -0
  150. package/dist/plugins/installer/parseSource.d.ts +20 -0
  151. package/dist/plugins/installer/parseSource.js +74 -0
  152. package/dist/plugins/installer/paths.d.ts +5 -0
  153. package/dist/plugins/installer/paths.js +25 -0
  154. package/dist/plugins/installer/types.d.ts +49 -0
  155. package/dist/plugins/installer/types.js +27 -0
  156. package/dist/plugins/installer/uninstall.d.ts +2 -0
  157. package/dist/plugins/installer/uninstall.js +14 -0
  158. package/dist/plugins/installer/update.d.ts +14 -0
  159. package/dist/plugins/installer/update.js +65 -0
  160. package/dist/plugins/loadPluginHooks.d.ts +8 -1
  161. package/dist/plugins/loadPluginHooks.js +11 -1
  162. package/dist/plugins/marketplaceManager.d.ts +8 -1
  163. package/dist/plugins/marketplaceManager.js +38 -4
  164. package/dist/plugins/pluginCommandHook.js +34 -0
  165. package/dist/plugins/pluginInstaller.d.ts +21 -0
  166. package/dist/plugins/pluginInstaller.js +128 -29
  167. package/dist/plugins/schemas.js +40 -6
  168. package/dist/plugins/types.d.ts +4 -0
  169. package/dist/preset/index.js +16 -0
  170. package/dist/prompt/composer.d.ts +4 -0
  171. package/dist/prompt/composer.js +28 -4
  172. package/dist/prompt/sections/orchestration.md +8 -0
  173. package/dist/protocol/chat-session-manager.d.ts +8 -1
  174. package/dist/protocol/chat-session-manager.js +10 -0
  175. package/dist/protocol/chat-session.d.ts +17 -0
  176. package/dist/protocol/chat-session.js +28 -0
  177. package/dist/protocol/redact.d.ts +50 -0
  178. package/dist/protocol/redact.js +71 -0
  179. package/dist/protocol/server.d.ts +41 -2
  180. package/dist/protocol/server.js +231 -33
  181. package/dist/protocol/tcp-transport.d.ts +45 -0
  182. package/dist/protocol/tcp-transport.js +74 -0
  183. package/dist/protocol/types.d.ts +23 -0
  184. package/dist/protocol/types.js +6 -0
  185. package/dist/remote/bridge.d.ts +11 -2
  186. package/dist/remote/bridge.js +61 -33
  187. package/dist/run/ArtifactTracker.js +5 -4
  188. package/dist/run/EngineRunner.d.ts +27 -1
  189. package/dist/run/EngineRunner.js +46 -8
  190. package/dist/run/FileRunStore.js +32 -8
  191. package/dist/run/RunLock.d.ts +28 -2
  192. package/dist/run/RunLock.js +49 -7
  193. package/dist/run/RunManager.js +51 -3
  194. package/dist/run/RunQueue.d.ts +1 -0
  195. package/dist/run/RunQueue.js +8 -2
  196. package/dist/run/factory.d.ts +7 -0
  197. package/dist/run/factory.js +1 -0
  198. package/dist/run/index.d.ts +2 -2
  199. package/dist/run/index.js +1 -1
  200. package/dist/run/redirect-target.d.ts +7 -0
  201. package/dist/run/redirect-target.js +13 -0
  202. package/dist/run/types.js +1 -1
  203. package/dist/services/browser-open.d.ts +13 -0
  204. package/dist/services/browser-open.js +16 -0
  205. package/dist/services/dream-consolidation.d.ts +57 -0
  206. package/dist/services/dream-consolidation.js +151 -0
  207. package/dist/services/memory-orchestrator.js +18 -0
  208. package/dist/services/notifier.d.ts +8 -0
  209. package/dist/services/notifier.js +39 -25
  210. package/dist/services/oauth.js +5 -7
  211. package/dist/services/session-memory-sort.d.ts +8 -0
  212. package/dist/services/session-memory-sort.js +9 -0
  213. package/dist/services/session-memory.js +8 -9
  214. package/dist/session/file-history.js +10 -3
  215. package/dist/session/session-manager.d.ts +23 -1
  216. package/dist/session/session-manager.js +73 -2
  217. package/dist/settings/disk-defaults.d.ts +35 -0
  218. package/dist/settings/disk-defaults.js +24 -0
  219. package/dist/settings/manager.d.ts +33 -0
  220. package/dist/settings/manager.js +94 -1
  221. package/dist/settings/personalization.d.ts +19 -0
  222. package/dist/settings/personalization.js +7 -0
  223. package/dist/settings/schema.d.ts +1142 -126
  224. package/dist/settings/schema.js +119 -10
  225. package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
  226. package/dist/tool-system/builtin/add-marketplace.js +76 -0
  227. package/dist/tool-system/builtin/agent-registry.d.ts +11 -0
  228. package/dist/tool-system/builtin/agent-registry.js +8 -4
  229. package/dist/tool-system/builtin/agent.d.ts +29 -10
  230. package/dist/tool-system/builtin/agent.js +108 -52
  231. package/dist/tool-system/builtin/apply-patch/index.js +22 -3
  232. package/dist/tool-system/builtin/arena.js +6 -7
  233. package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
  234. package/dist/tool-system/builtin/complete-goal.js +45 -0
  235. package/dist/tool-system/builtin/cron.js +53 -9
  236. package/dist/tool-system/builtin/edit.d.ts +2 -1
  237. package/dist/tool-system/builtin/edit.js +6 -1
  238. package/dist/tool-system/builtin/generate-image.d.ts +25 -0
  239. package/dist/tool-system/builtin/generate-image.js +146 -0
  240. package/dist/tool-system/builtin/glob.js +7 -0
  241. package/dist/tool-system/builtin/grep.js +6 -0
  242. package/dist/tool-system/builtin/index.d.ts +18 -1
  243. package/dist/tool-system/builtin/index.js +62 -1
  244. package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
  245. package/dist/tool-system/builtin/notebook-edit.js +8 -1
  246. package/dist/tool-system/builtin/read.d.ts +2 -1
  247. package/dist/tool-system/builtin/read.js +9 -1
  248. package/dist/tool-system/builtin/sleep.js +9 -3
  249. package/dist/tool-system/builtin/task.js +4 -1
  250. package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
  251. package/dist/tool-system/builtin/update-automation-memory.js +60 -0
  252. package/dist/tool-system/builtin/view-image.d.ts +18 -0
  253. package/dist/tool-system/builtin/view-image.js +96 -0
  254. package/dist/tool-system/builtin/web-search.d.ts +4 -1
  255. package/dist/tool-system/builtin/web-search.js +36 -3
  256. package/dist/tool-system/builtin/write.d.ts +2 -1
  257. package/dist/tool-system/builtin/write.js +7 -1
  258. package/dist/tool-system/context.d.ts +12 -0
  259. package/dist/tool-system/executor.d.ts +0 -1
  260. package/dist/tool-system/executor.js +34 -40
  261. package/dist/tool-system/investigation-guard.d.ts +4 -1
  262. package/dist/tool-system/investigation-guard.js +19 -3
  263. package/dist/tool-system/mcp-manager.d.ts +36 -12
  264. package/dist/tool-system/mcp-manager.js +173 -10
  265. package/dist/tool-system/path-policy.d.ts +94 -0
  266. package/dist/tool-system/path-policy.js +279 -0
  267. package/dist/tool-system/permission.js +43 -50
  268. package/dist/tool-system/plan-mode-allowlist.d.ts +21 -0
  269. package/dist/tool-system/plan-mode-allowlist.js +43 -0
  270. package/dist/tool-system/registry.js +12 -1
  271. package/dist/tool-system/validation.d.ts +10 -0
  272. package/dist/tool-system/validation.js +16 -3
  273. package/dist/types.d.ts +90 -15
  274. package/dist/utils/format.js +4 -2
  275. package/dist/utils/lockfile.js +9 -2
  276. package/dist/utils/theme.d.ts +6 -0
  277. package/dist/utils/theme.js +4 -2
  278. package/package.json +2 -1
@@ -2,6 +2,27 @@
2
2
  * Settings schema with Zod validation.
3
3
  */
4
4
  import { z } from "zod";
5
+ import { ReasoningSettingSchema } from "../llm/reasoning-setting.js";
6
+ /**
7
+ * Tri-state project capability overlay. Lives in PROJECT settings only and
8
+ * layers over the global baseline (disabledSkills / disabledPlugins /
9
+ * mcpServers.<name>.enabled / disabledAgents). NOT a second denylist:
10
+ * - "on" → force enabled (even if globally disabled)
11
+ * - "off" → force disabled (even if globally enabled)
12
+ * - "inherit" → take the global baseline. We don't persist "inherit"; an
13
+ * absent key means inherit. See the project-scoped
14
+ * capabilities design (spec §4).
15
+ */
16
+ export const CapabilityOverrideSchema = z.enum(["inherit", "on", "off"]);
17
+ export const CapabilityOverridesSchema = z
18
+ .object({
19
+ skills: z.record(CapabilityOverrideSchema).optional(),
20
+ plugins: z.record(CapabilityOverrideSchema).optional(),
21
+ agents: z.record(CapabilityOverrideSchema).optional(),
22
+ mcp: z.record(CapabilityOverrideSchema).optional(),
23
+ builtin: z.record(CapabilityOverrideSchema).optional(),
24
+ })
25
+ .optional();
5
26
  export const SettingsSchema = z
6
27
  .object({
7
28
  agent: z
@@ -12,6 +33,14 @@ export const SettingsSchema = z
12
33
  disabledBuiltinTools: z.array(z.string()).default([]),
13
34
  customSystemPrompt: z.string().optional(),
14
35
  appendSystemPrompt: z.string().optional(),
36
+ responseLanguage: z.string().optional(),
37
+ userProfile: z.string().optional(),
38
+ instructions: z
39
+ .object({
40
+ compatClaude: z.boolean().default(true),
41
+ compatCodex: z.boolean().default(true),
42
+ })
43
+ .optional(),
15
44
  })
16
45
  .default({}),
17
46
  /**
@@ -22,6 +51,15 @@ export const SettingsSchema = z
22
51
  * both — appendOnboardingResult does this in one shot.
23
52
  */
24
53
  activeKey: z.string().optional(),
54
+ /**
55
+ * Auxiliary-task model — points at a models[].key. Background, non-user-
56
+ * facing LLM calls (memory extraction, the auto-dream loop) use this model
57
+ * instead of the active one, so per-turn book-keeping doesn't burn the
58
+ * expensive primary model. Pick something fast/cheap (e.g. a Haiku or
59
+ * DeepSeek key). When unset, those calls fall back to the active model
60
+ * (legacy behavior). An invalid/missing key also falls back to active.
61
+ */
62
+ auxModelKey: z.string().optional(),
25
63
  /**
26
64
  * Toggle background auto-update. When true (default), code-shell checks
27
65
  * npm for newer versions and — if the npm global prefix is writable —
@@ -62,12 +100,11 @@ export const SettingsSchema = z
62
100
  protocol: z.enum(["openai-compat", "anthropic-style"]).optional(),
63
101
  modelsPath: z.string().optional(),
64
102
  /**
65
- * DeepSeek V4 thinking-mode default for this provider. "enabled"
66
- * matches endpoint default; "disabled" makes V4 calls run in
67
- * non-thinking mode (faster, cheaper). Ignored by every provider
68
- * other than DeepSeek V4. Per-call overrides still win.
103
+ * Default reasoning/thinking setting for this provider's models.
104
+ * Rich shape: {mode:"off"|"on"} | {mode:"effort",effort} |
105
+ * {mode:"budget",budgetTokens}. Per-model `reasoning` wins.
69
106
  */
70
- thinking: z.enum(["enabled", "disabled"]).optional(),
107
+ reasoning: ReasoningSettingSchema.optional(),
71
108
  }))
72
109
  .default([]),
73
110
  models: z
@@ -90,12 +127,12 @@ export const SettingsSchema = z
90
127
  baseUrl: z.string().optional(),
91
128
  apiKey: z.string().optional(),
92
129
  /**
93
- * Per-model thinking override. Wins over the provider-level
94
- * setting (settings.providers[].thinking). Use this when
95
- * different models under the same provider need different
96
- * defaults — e.g. DeepSeek V4 Pro off (faster) but V4 Flash on.
130
+ * Per-model reasoning override. Wins over the provider-level
131
+ * `reasoning` setting (settings.providers[].reasoning). Use this
132
+ * when different models under the same provider need different
133
+ * defaults — e.g. one model off (faster) and another on.
97
134
  */
98
- thinking: z.enum(["enabled", "disabled"]).optional(),
135
+ reasoning: ReasoningSettingSchema.optional(),
99
136
  })
100
137
  .transform((m) => {
101
138
  // Normalize: prefer `protocol`; fall back to legacy `provider`.
@@ -146,6 +183,9 @@ export const SettingsSchema = z
146
183
  url: z.string().optional(),
147
184
  transport: z.enum(["stdio", "sse", "streamable-http", "inprocess"]).optional(),
148
185
  headers: z.record(z.string()).optional(),
186
+ // Codex-style on/off switch. Absent or true → connected; only
187
+ // literal false disables. Filtered in MCPManager.connectAll.
188
+ enabled: z.boolean().optional(),
149
189
  }))
150
190
  .default({}),
151
191
  /**
@@ -162,6 +202,20 @@ export const SettingsSchema = z
162
202
  * names (no colon suffix). See scanSkills(opts.disabledPlugins).
163
203
  */
164
204
  disabledPlugins: z.array(z.string()).default([]),
205
+ /**
206
+ * Sub-agent role names (the `name` in .code-shell/agents/*.md) to
207
+ * hide from the registry. A disabled role is filtered out at load
208
+ * so it never appears in registry.list()/get() — the Agent tool's
209
+ * agent_type list won't show it and the LLM can't pick it. Mirrors
210
+ * disabledSkills / disabledPlugins.
211
+ */
212
+ disabledAgents: z.array(z.string()).default([]),
213
+ /**
214
+ * Project-scoped tri-state capability overlay. Only meaningful in
215
+ * ${cwd}/.code-shell/settings.json; layered over the global baseline at
216
+ * capability-assembly time. Optional — absence === everything inherits.
217
+ */
218
+ capabilityOverrides: CapabilityOverridesSchema,
165
219
  instructions: z
166
220
  .object({
167
221
  fileName: z.string().default("CODESHELL.md"),
@@ -204,6 +258,15 @@ export const SettingsSchema = z
204
258
  .record(z.enum(["serper", "tavily", "searxng"]), z.object({
205
259
  apiKey: z.string().optional(),
206
260
  baseUrl: z.string().optional(),
261
+ lastProbe: z
262
+ .object({
263
+ status: z.enum(["ok", "error", "unconfigured"]),
264
+ sampleTitles: z.array(z.string()).optional(),
265
+ errorMessage: z.string().optional(),
266
+ errorDetail: z.string().optional(),
267
+ lastProbedAt: z.string(),
268
+ })
269
+ .optional(),
207
270
  }))
208
271
  .optional(),
209
272
  })
@@ -213,6 +276,24 @@ export const SettingsSchema = z
213
276
  format: z.enum(["text", "json", "jsonl", "stream-json"]).default("text"),
214
277
  })
215
278
  .default({}),
279
+ /**
280
+ * Image attachment handling. Sub-fields:
281
+ * - detail: OpenAI-style fidelity hint applied to every user-
282
+ * attached image. "low" = 85 tokens/image fixed (cheap
283
+ * thumbnail), "high" = ~768 px tiles (default), "original" =
284
+ * keep client-side dimensions (most expensive). Anthropic
285
+ * ignores this field today; that's fine — it just gets
286
+ * dropped on the Claude path.
287
+ * - mcpImageTokenBudget: per-turn cap on token cost of images
288
+ * returned by MCP tools. Codex doesn't enforce this; CC uses
289
+ * 25 000. Set to 0 to disable.
290
+ */
291
+ images: z
292
+ .object({
293
+ detail: z.enum(["low", "high", "original"]).optional(),
294
+ mcpImageTokenBudget: z.number().int().nonnegative().optional(),
295
+ })
296
+ .optional(),
216
297
  /**
217
298
  * OS-level sandbox for shell-tool execution. "auto" picks per platform:
218
299
  * Seatbelt on macOS, bubblewrap on Linux when installed, otherwise off.
@@ -227,6 +308,34 @@ export const SettingsSchema = z
227
308
  network: z.enum(["allow", "deny"]).optional(),
228
309
  })
229
310
  .optional(),
311
+ /**
312
+ * Project-local environment profile. Mirrors Codex's local-environment
313
+ * shape at the settings layer: setup/cleanup scripts can be customized per
314
+ * platform, and env values are stored as KEY=VALUE pairs. Runtime wiring is
315
+ * intentionally separate from the MCP server env model above.
316
+ */
317
+ localEnvironment: z
318
+ .object({
319
+ name: z.string().optional(),
320
+ setupScripts: z
321
+ .object({
322
+ default: z.string().optional(),
323
+ macos: z.string().optional(),
324
+ linux: z.string().optional(),
325
+ windows: z.string().optional(),
326
+ })
327
+ .optional(),
328
+ cleanupScripts: z
329
+ .object({
330
+ default: z.string().optional(),
331
+ macos: z.string().optional(),
332
+ linux: z.string().optional(),
333
+ windows: z.string().optional(),
334
+ })
335
+ .optional(),
336
+ env: z.record(z.string()).optional(),
337
+ })
338
+ .optional(),
230
339
  /**
231
340
  * Shell-hook configuration. Each entry binds a HookEventName to a
232
341
  * shell command; the command receives ctx JSON on stdin and returns
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Built-in AddMarketplace tool — register a plugin marketplace source so the
3
+ * user can browse/install plugins from it in the UI. The tool only *adds the
4
+ * source* (git clone + validate marketplace.json via core's addMarketplace);
5
+ * which plugin to install is left to the user. Pairs with WebSearch/WebFetch:
6
+ * the model can discover a marketplace repo, then add it.
7
+ *
8
+ * Side effects (network + git clone + disk write) → permissionDefault "ask".
9
+ */
10
+ import type { ToolDefinition } from "../../types.js";
11
+ export declare const addMarketplaceToolDef: ToolDefinition;
12
+ export declare function addMarketplaceTool(args: Record<string, unknown>): Promise<string>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Built-in AddMarketplace tool — register a plugin marketplace source so the
3
+ * user can browse/install plugins from it in the UI. The tool only *adds the
4
+ * source* (git clone + validate marketplace.json via core's addMarketplace);
5
+ * which plugin to install is left to the user. Pairs with WebSearch/WebFetch:
6
+ * the model can discover a marketplace repo, then add it.
7
+ *
8
+ * Side effects (network + git clone + disk write) → permissionDefault "ask".
9
+ */
10
+ import { addMarketplace } from "../../plugins/marketplaceManager.js";
11
+ export const addMarketplaceToolDef = {
12
+ name: "AddMarketplace",
13
+ description: "Register a plugin marketplace source so the user can browse and install " +
14
+ "plugins from it. Provide a short name and a source: either a GitHub repo " +
15
+ "(owner/name) or a git URL. This only ADDS the source — it does not install " +
16
+ "any plugin. Use WebSearch/WebFetch first to find a marketplace repo if you " +
17
+ "don't already have one.",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: {
21
+ name: {
22
+ type: "string",
23
+ description: "Short local name for this marketplace (e.g. 'official').",
24
+ },
25
+ source_type: {
26
+ type: "string",
27
+ enum: ["github", "git"],
28
+ description: "'github' for an owner/name repo, 'git' for a clone URL.",
29
+ },
30
+ repo: {
31
+ type: "string",
32
+ description: "GitHub repo in owner/name form (required when source_type='github').",
33
+ },
34
+ url: {
35
+ type: "string",
36
+ description: "Git clone URL (required when source_type='git').",
37
+ },
38
+ },
39
+ required: ["name", "source_type"],
40
+ },
41
+ };
42
+ export async function addMarketplaceTool(args) {
43
+ const name = args.name;
44
+ if (typeof name !== "string" || !name.trim()) {
45
+ return "Error: name is required";
46
+ }
47
+ const sourceType = args.source_type;
48
+ let source;
49
+ if (sourceType === "github") {
50
+ const repo = args.repo;
51
+ if (typeof repo !== "string" || !repo.includes("/")) {
52
+ return "Error: github source requires repo in owner/name form";
53
+ }
54
+ source = { source: "github", repo };
55
+ }
56
+ else if (sourceType === "git") {
57
+ const url = args.url;
58
+ if (typeof url !== "string" || !url.trim()) {
59
+ return "Error: git source requires a url";
60
+ }
61
+ source = { source: "git", url };
62
+ }
63
+ else {
64
+ return "Error: source_type must be 'github' or 'git'";
65
+ }
66
+ try {
67
+ const result = await addMarketplace(name, source);
68
+ if (!result.ok) {
69
+ return `Error adding marketplace ${name}: ${result.error}`;
70
+ }
71
+ return `Marketplace '${name}' added. The user can now browse and install plugins from it in the Extensions → Market UI.`;
72
+ }
73
+ catch (err) {
74
+ return `Error adding marketplace ${name}: ${err.message}`;
75
+ }
76
+ }
@@ -29,7 +29,15 @@ export interface AsyncAgentEntry {
29
29
  agentId: string;
30
30
  /** Short kind label shown in the dock (e.g. "Explore", "Plan"). */
31
31
  name?: string;
32
+ /** Dispatched role (e.g. "general-purpose", "explorer"); shown in the dock
33
+ * so you can see what kind of agent is running. */
34
+ agentType?: string;
32
35
  description: string;
36
+ /** Session that spawned this background agent. Lets the spawning
37
+ * Engine.run wait only on ITS OWN background agents (hasRunningForSession),
38
+ * not every concurrent session's. Undefined only for legacy/ad-hoc callers
39
+ * outside Engine.run. */
40
+ sessionId?: string;
33
41
  status: AsyncAgentStatus;
34
42
  startedAt: number;
35
43
  finishedAt?: number;
@@ -46,6 +54,9 @@ declare class AsyncAgentRegistry {
46
54
  subscribe: (cb: () => void) => (() => void);
47
55
  getSnapshot: () => AsyncAgentEntry[];
48
56
  hasRunning: () => boolean;
57
+ /** True if any background agent spawned by `sessionId` is still running.
58
+ * Drives Engine.run's "wait for my background agents before resolving". */
59
+ hasRunningForSession: (sessionId: string) => boolean;
49
60
  /** Count of agents currently in the "running" state (cap enforcement). */
50
61
  runningCount(): number;
51
62
  private notify;
@@ -30,6 +30,11 @@ class AsyncAgentRegistry {
30
30
  hasRunning = () => {
31
31
  return this.snapshot.some((e) => e.status === "running");
32
32
  };
33
+ /** True if any background agent spawned by `sessionId` is still running.
34
+ * Drives Engine.run's "wait for my background agents before resolving". */
35
+ hasRunningForSession = (sessionId) => {
36
+ return this.snapshot.some((e) => e.status === "running" && e.sessionId === sessionId);
37
+ };
33
38
  /** Count of agents currently in the "running" state (cap enforcement). */
34
39
  runningCount() {
35
40
  return this.snapshot.filter((e) => e.status === "running").length;
@@ -108,10 +113,9 @@ class AsyncAgentRegistry {
108
113
  catch {
109
114
  // ignore abort errors — we still mark cancelled
110
115
  }
111
- e.status = "cancelled";
112
- e.finishedAt = Date.now();
113
- e.finishedFadeAt = e.finishedAt + 30_000;
114
- this.notify();
116
+ // Reuse markFinished for the status/timestamp/notify bookkeeping rather
117
+ // than duplicating it (the entry is still "running" after abort()).
118
+ this.markFinished(agentId, "cancelled");
115
119
  return true;
116
120
  }
117
121
  reset() {
@@ -11,6 +11,7 @@ import type { ToolContext } from "../context.js";
11
11
  import type { AgentDefinitionRegistry } from "../../agent/agent-definition-registry.js";
12
12
  import type { HookRegistry } from "../../hooks/registry.js";
13
13
  export interface AgentTypeOverrides {
14
+ resolvedType?: string;
14
15
  model?: string;
15
16
  maxTurns?: number;
16
17
  toolAllowlist?: string[];
@@ -18,10 +19,36 @@ export interface AgentTypeOverrides {
18
19
  }
19
20
  /**
20
21
  * Resolve an `agent_type` against the role registry into spawn overrides.
21
- * Omitted type → empty overrides (ephemeral mode). Unknown type → throw, so
22
- * the LLM gets a clear correction instead of silently running a generic agent.
22
+ *
23
+ * - Omitted type + non-empty registry fall back to a configured role
24
+ * ("general-purpose" if present, else the first available) instead of
25
+ * running a nameless ephemeral agent. Relaxed from the earlier "throw when
26
+ * agent_type omitted": the model habitually omits it, and a hard error just
27
+ * turned every spawn into a failure. Falling back to a real role keeps the
28
+ * tool-allowlist / system-prompt benefits without breaking the call.
29
+ * - Omitted type + empty registry → empty overrides (true ephemeral; nothing
30
+ * to fall back to).
31
+ * - Unknown explicit type → throw, so the LLM gets a clear correction rather
32
+ * than silently running a generic agent.
23
33
  */
24
34
  export declare function resolveAgentTypeOverrides(agentType: string | undefined, registry: AgentDefinitionRegistry | undefined): AgentTypeOverrides;
35
+ /**
36
+ * Render the "Available agent types" block injected into the Agent tool's
37
+ * description, listing the roles defined in .code-shell/agents/*.md so the
38
+ * model knows it can pass `agent_type` instead of hand-rolling an ad-hoc
39
+ * agent. Without this the model never sees the registry (it lives per-engine,
40
+ * not in the static tool def) and falls back to nameless ephemeral agents —
41
+ * see the Core A/B/C incident. Returns "" when no roles are defined, so the
42
+ * base description is left untouched.
43
+ */
44
+ export declare function buildAgentTypesBlock(registry: AgentDefinitionRegistry | undefined): string;
45
+ /**
46
+ * Produce an Agent tool definition whose description ends with the live
47
+ * available-agent-types listing. Pure: takes the registry, returns a new def
48
+ * (the base `agentToolDef` const is never mutated). When no roles exist the
49
+ * base def is returned unchanged.
50
+ */
51
+ export declare function agentToolDefWithTypes(registry: AgentDefinitionRegistry | undefined): ToolDefinition;
25
52
  type SubAgentLifecycle = "subagent_start" | "subagent_finish" | "subagent_error";
26
53
  /**
27
54
  * Emit a sub-agent lifecycle event via the existing `notification` hook,
@@ -35,14 +62,6 @@ export declare function emitSubAgentHook(hooks: HookRegistry | undefined, kind:
35
62
  text?: string;
36
63
  error?: string;
37
64
  }): void;
38
- /** Default per-sub-agent wall-clock timeout (5 minutes). */
39
- export declare const DEFAULT_SUBAGENT_TIMEOUT_MS: number;
40
- /**
41
- * Run `work()` with a timeout. On expiry, calls `onTimeout` (to abort the
42
- * child) and rejects with a timeout error. The child's own abort handling
43
- * unwinds its resources.
44
- */
45
- export declare function runWithTimeout<T>(work: () => Promise<T>, timeoutMs: number, onTimeout: () => void): Promise<T>;
46
65
  export declare const agentToolDef: ToolDefinition;
47
66
  export declare function agentTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
48
67
  export declare const agentStatusToolDef: ToolDefinition;
@@ -32,26 +32,79 @@ function safeEmit(sink, event) {
32
32
  });
33
33
  }
34
34
  }
35
+ /** Preferred default role when the caller omits agent_type but roles exist. */
36
+ const DEFAULT_AGENT_TYPE = "general-purpose";
35
37
  /**
36
38
  * Resolve an `agent_type` against the role registry into spawn overrides.
37
- * Omitted type → empty overrides (ephemeral mode). Unknown type → throw, so
38
- * the LLM gets a clear correction instead of silently running a generic agent.
39
+ *
40
+ * - Omitted type + non-empty registry fall back to a configured role
41
+ * ("general-purpose" if present, else the first available) instead of
42
+ * running a nameless ephemeral agent. Relaxed from the earlier "throw when
43
+ * agent_type omitted": the model habitually omits it, and a hard error just
44
+ * turned every spawn into a failure. Falling back to a real role keeps the
45
+ * tool-allowlist / system-prompt benefits without breaking the call.
46
+ * - Omitted type + empty registry → empty overrides (true ephemeral; nothing
47
+ * to fall back to).
48
+ * - Unknown explicit type → throw, so the LLM gets a clear correction rather
49
+ * than silently running a generic agent.
39
50
  */
40
51
  export function resolveAgentTypeOverrides(agentType, registry) {
41
- if (!agentType)
42
- return {};
43
- const def = registry?.get(agentType);
52
+ const available = registry?.list().map((d) => d.name) ?? [];
53
+ let resolvedType = agentType;
54
+ if (!resolvedType) {
55
+ if (available.length === 0)
56
+ return {};
57
+ resolvedType = available.includes(DEFAULT_AGENT_TYPE) ? DEFAULT_AGENT_TYPE : available[0];
58
+ }
59
+ const def = registry?.get(resolvedType);
44
60
  if (!def) {
45
- const available = registry?.list().map((d) => d.name).join(", ") || "(none defined)";
46
- throw new Error(`unknown agent_type '${agentType}'. Available: ${available}`);
61
+ const list = available.join(", ") || "(none defined)";
62
+ throw new Error(`unknown agent_type '${resolvedType}'. Available: ${list}`);
47
63
  }
48
64
  return {
65
+ resolvedType,
49
66
  model: def.model,
50
67
  maxTurns: def.maxTurns,
51
68
  toolAllowlist: def.tools,
52
69
  appendSystemPrompt: def.systemPrompt,
53
70
  };
54
71
  }
72
+ /**
73
+ * Render the "Available agent types" block injected into the Agent tool's
74
+ * description, listing the roles defined in .code-shell/agents/*.md so the
75
+ * model knows it can pass `agent_type` instead of hand-rolling an ad-hoc
76
+ * agent. Without this the model never sees the registry (it lives per-engine,
77
+ * not in the static tool def) and falls back to nameless ephemeral agents —
78
+ * see the Core A/B/C incident. Returns "" when no roles are defined, so the
79
+ * base description is left untouched.
80
+ */
81
+ export function buildAgentTypesBlock(registry) {
82
+ const defs = registry?.list() ?? [];
83
+ if (defs.length === 0)
84
+ return "";
85
+ const lines = defs.map((d) => {
86
+ const tools = d.tools && d.tools.length > 0 ? d.tools.join(", ") : "all parent tools";
87
+ return `- ${d.name}: ${d.description} (tools: ${tools})`;
88
+ });
89
+ return [
90
+ "",
91
+ "Available agent types (pass one as `agent_type` to reuse its role, tool allowlist, and turn cap):",
92
+ ...lines,
93
+ "Pass the closest matching agent_type (e.g. read-only investigation → researcher/explorer, planning → planner, full multi-step work → general-purpose). If you omit agent_type it defaults to general-purpose (or the first available role) — passing an explicit one is preferred.",
94
+ ].join("\n");
95
+ }
96
+ /**
97
+ * Produce an Agent tool definition whose description ends with the live
98
+ * available-agent-types listing. Pure: takes the registry, returns a new def
99
+ * (the base `agentToolDef` const is never mutated). When no roles exist the
100
+ * base def is returned unchanged.
101
+ */
102
+ export function agentToolDefWithTypes(registry) {
103
+ const block = buildAgentTypesBlock(registry);
104
+ if (!block)
105
+ return agentToolDef;
106
+ return { ...agentToolDef, description: agentToolDef.description + "\n" + block };
107
+ }
55
108
  /**
56
109
  * Emit a sub-agent lifecycle event via the existing `notification` hook,
57
110
  * tagged with a `kind`. No-op when hooks are absent. Fire-and-forget: emit is
@@ -61,41 +114,25 @@ export function resolveAgentTypeOverrides(agentType, registry) {
61
114
  export function emitSubAgentHook(hooks, kind, payload) {
62
115
  void hooks?.emit("notification", { kind, ...payload });
63
116
  }
64
- /** Default per-sub-agent wall-clock timeout (5 minutes). */
65
- export const DEFAULT_SUBAGENT_TIMEOUT_MS = 5 * 60_000;
66
- /**
67
- * Run `work()` with a timeout. On expiry, calls `onTimeout` (to abort the
68
- * child) and rejects with a timeout error. The child's own abort handling
69
- * unwinds its resources.
70
- */
71
- export async function runWithTimeout(work, timeoutMs, onTimeout) {
72
- let timer;
73
- const timeout = new Promise((_, reject) => {
74
- timer = setTimeout(() => {
75
- onTimeout();
76
- reject(new Error(`Sub-agent timed out after ${timeoutMs}ms`));
77
- }, timeoutMs);
78
- });
79
- try {
80
- return await Promise.race([work(), timeout]);
81
- }
82
- finally {
83
- if (timer)
84
- clearTimeout(timer);
85
- }
86
- }
87
117
  export const agentToolDef = {
88
118
  name: "Agent",
89
- description: "Launch a sub-agent to handle a complex task autonomously. " +
90
- "The sub-agent has access to the same tools and runs independently. " +
91
- "Use this for tasks that can be delegated, parallelized, or require deep exploration. " +
92
- "Provide a clear, complete description of what the agent should do.\n\n" +
93
- "When you launch multiple agents for independent work, send them in a single " +
94
- "message with multiple tool uses so they run concurrently.\n\n" +
95
- "You can optionally run agents in the background using the run_in_background " +
96
- "parameter. When an agent runs in the background, you will be automatically " +
97
- "notified when it completes do NOT sleep, poll, or proactively check on its " +
98
- "progress. Continue with other work or respond to the user instead.",
119
+ description: "Launch a sub-agent to handle a task in its own clean, isolated context. " +
120
+ "The sub-agent runs independently with access to the same tools, and its final " +
121
+ "report is the ONLY thing returned to you — it is stateless, so you cannot send " +
122
+ "follow-up messages. Write a complete, self-contained task description.\n\n" +
123
+ "PRIMARY USE context isolation: when a task needs to read many files or run a " +
124
+ "broad investigation but you only need the conclusion, delegate it. The sub-agent " +
125
+ "absorbs the noisy intermediate output in its own context; you keep the answer, not " +
126
+ "the file dumps. This protects your main context from being flooded.\n\n" +
127
+ "DON'T use this for a quick lookup where you know the file/symbol and expect a few " +
128
+ "matches use Read/Grep/Glob directly instead; spawning an agent wastes a turn.\n\n" +
129
+ "Parallel fan-out is the EXCEPTION: only launch several agents in one message when " +
130
+ "the work truly splits into independent pieces with no shared state. Prefer one " +
131
+ "well-scoped delegation over a swarm.\n\n" +
132
+ "You can optionally run an agent in the background using the run_in_background " +
133
+ "parameter. When it runs in the background, you will be automatically notified when " +
134
+ "it completes — do NOT sleep, poll, or proactively check on its progress. Continue " +
135
+ "with other work or respond to the user instead.",
99
136
  inputSchema: {
100
137
  type: "object",
101
138
  properties: {
@@ -109,7 +146,10 @@ export const agentToolDef = {
109
146
  type: "string",
110
147
  description: "Optional reusable role defined in .code-shell/agents/*.md (e.g. 'researcher'). " +
111
148
  "Loads that role's model, tool allowlist, turn cap, and system prompt. " +
112
- "Omit to run an ad-hoc agent described entirely by 'prompt'.",
149
+ "Disabled roles are not available. If you pass an unknown role you'll get " +
150
+ "an error listing the currently available roles. " +
151
+ "If omitted, defaults to a configured role (general-purpose or the first available); " +
152
+ "with no roles configured it runs an ephemeral agent. Passing an explicit role is preferred.",
113
153
  },
114
154
  description: {
115
155
  type: "string",
@@ -155,9 +195,9 @@ uiStream,
155
195
  * sub-agents.
156
196
  */
157
197
  streamOverride) {
158
- const { agentId, name, description } = opts;
198
+ const { agentId, name, description, agentType } = opts;
159
199
  const startEndSink = uiStream ?? spawner.parentStream;
160
- safeEmit(startEndSink, { type: "agent_start", agentId, name, description });
200
+ safeEmit(startEndSink, { type: "agent_start", agentId, name, description, agentType });
161
201
  emitSubAgentHook(opts.hooks, "subagent_start", { agentId, description });
162
202
  // `resetPlanMode` / `restorePlanMode` operated on a module-level singleton
163
203
  // that no longer exists. The child Engine is a fresh instance; plan-mode
@@ -165,7 +205,7 @@ streamOverride) {
165
205
  // instances (finalized in T6).
166
206
  const text = await spawner.spawn({ ...opts, streamOverride });
167
207
  const finalText = text || `Agent completed but produced no text output.`;
168
- safeEmit(startEndSink, { type: "agent_end", agentId, name, description, text: finalText });
208
+ safeEmit(startEndSink, { type: "agent_end", agentId, name, description, text: finalText, agentType });
169
209
  emitSubAgentHook(opts.hooks, "subagent_finish", { agentId, description, text: finalText });
170
210
  return finalText;
171
211
  }
@@ -218,7 +258,11 @@ export async function agentTool(args, ctx) {
218
258
  asyncAgentRegistry.register({
219
259
  agentId,
220
260
  name,
261
+ agentType: overrides.resolvedType,
221
262
  description,
263
+ // Tag with the spawning session so the parent Engine.run waits only on
264
+ // its own background agents (hasRunningForSession).
265
+ sessionId: ctx?.sessionId,
222
266
  status: "running",
223
267
  startedAt: Date.now(),
224
268
  abort: () => controller.abort(),
@@ -241,11 +285,13 @@ export async function agentTool(args, ctx) {
241
285
  agentId,
242
286
  name,
243
287
  description,
288
+ agentType: overrides.resolvedType,
244
289
  prompt,
245
290
  maxTurns,
246
291
  model: overrides.model,
247
292
  toolAllowlist: overrides.toolAllowlist,
248
293
  appendSystemPrompt: overrides.appendSystemPrompt,
294
+ readOnlySession: overrides.resolvedType === "researcher" || overrides.resolvedType === "explorer",
249
295
  hooks: ctx?.hooks,
250
296
  signal: controller.signal,
251
297
  }, parentStream, // uiStream: agent_start/end → main feed
@@ -338,31 +384,41 @@ export async function agentTool(args, ctx) {
338
384
  ].join("\n");
339
385
  }
340
386
  // ─── Synchronous path ──────────────────────────────────────────
341
- // A timeout-capable controller: the timeout callback aborts the child, and
342
- // a parent abort is forwarded to it too. Keeping the timeout OUTSIDE
343
- // runSubAgent (and off the background path) avoids the background path's
344
- // "abort means user-cancel drop silently" semantics; here a timeout is a
345
- // genuine error surfaced to the parent.
387
+ // No wall-clock timeout on the sub-agent lifecycle that matches Claude
388
+ // Code / Codex, where a sub-agent (Task) is bounded by maxTurns + per-tool
389
+ // timeouts (Bash etc. carry their own) + parent/user abort, NOT by a global
390
+ // countdown that kills legitimate heavy work mid-task. The old
391
+ // runWithTimeout(5min) both (a) murdered a "read 40 files" agent at 5:00 and
392
+ // (b) raced with the normal completion path: the timeout aborted the child,
393
+ // the catch emitted agent_end{error}, AND the aborted child still let
394
+ // runSubAgent reach its line-280 agent_end{text} — two agent_end events for
395
+ // one agent, the later text one overwriting the error so a timed-out agent
396
+ // rendered as "done". Dropping the timeout removes the race entirely: a
397
+ // genuine spawn error throws BEFORE the success emit, so only the catch
398
+ // fires; a clean run emits agent_end{text} exactly once. (fix:
399
+ // subagent-timeout-double-agent-end)
346
400
  const syncController = new AbortController();
347
401
  const onParentAbort = () => syncController.abort();
348
402
  parentSignal?.addEventListener("abort", onParentAbort, { once: true });
349
403
  try {
350
- return await runWithTimeout(() => runSubAgent(spawner, {
404
+ return await runSubAgent(spawner, {
351
405
  agentId,
352
406
  name,
353
407
  description,
408
+ agentType: overrides.resolvedType,
354
409
  prompt,
355
410
  maxTurns,
356
411
  model: overrides.model,
357
412
  toolAllowlist: overrides.toolAllowlist,
358
413
  appendSystemPrompt: overrides.appendSystemPrompt,
414
+ readOnlySession: overrides.resolvedType === "researcher" || overrides.resolvedType === "explorer",
359
415
  hooks: ctx?.hooks,
360
416
  signal: syncController.signal,
361
- }), DEFAULT_SUBAGENT_TIMEOUT_MS, () => syncController.abort());
417
+ });
362
418
  }
363
419
  catch (err) {
364
420
  emitSubAgentHook(ctx?.hooks, "subagent_error", { agentId, description, error: err.message });
365
- safeEmit(parentStream, { type: "agent_end", agentId, name, description, error: err.message });
421
+ safeEmit(parentStream, { type: "agent_end", agentId, name, description, error: err.message, agentType: overrides.resolvedType });
366
422
  if (parentSignal?.aborted)
367
423
  return "Agent was aborted.";
368
424
  return `Agent error: ${err.message}`;