@moxxy/core 0.3.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 (332) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/dist/events/factory.d.ts +6 -0
  4. package/dist/events/factory.d.ts.map +1 -0
  5. package/dist/events/factory.js +14 -0
  6. package/dist/events/factory.js.map +1 -0
  7. package/dist/events/log.d.ts +92 -0
  8. package/dist/events/log.d.ts.map +1 -0
  9. package/dist/events/log.js +244 -0
  10. package/dist/events/log.js.map +1 -0
  11. package/dist/index.d.ts +40 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +41 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/logger.d.ts +16 -0
  16. package/dist/logger.d.ts.map +1 -0
  17. package/dist/logger.js +27 -0
  18. package/dist/logger.js.map +1 -0
  19. package/dist/permissions/engine.d.ts +94 -0
  20. package/dist/permissions/engine.d.ts.map +1 -0
  21. package/dist/permissions/engine.js +203 -0
  22. package/dist/permissions/engine.js.map +1 -0
  23. package/dist/permissions/resolvers.d.ts +8 -0
  24. package/dist/permissions/resolvers.d.ts.map +1 -0
  25. package/dist/permissions/resolvers.js +8 -0
  26. package/dist/permissions/resolvers.js.map +1 -0
  27. package/dist/plugins/discovery.d.ts +9 -0
  28. package/dist/plugins/discovery.d.ts.map +1 -0
  29. package/dist/plugins/discovery.js +112 -0
  30. package/dist/plugins/discovery.js.map +1 -0
  31. package/dist/plugins/host-options.d.ts +72 -0
  32. package/dist/plugins/host-options.d.ts.map +1 -0
  33. package/dist/plugins/host-options.js +2 -0
  34. package/dist/plugins/host-options.js.map +1 -0
  35. package/dist/plugins/host.d.ts +53 -0
  36. package/dist/plugins/host.d.ts.map +1 -0
  37. package/dist/plugins/host.js +269 -0
  38. package/dist/plugins/host.js.map +1 -0
  39. package/dist/plugins/lifecycle.d.ts +31 -0
  40. package/dist/plugins/lifecycle.d.ts.map +1 -0
  41. package/dist/plugins/lifecycle.js +100 -0
  42. package/dist/plugins/lifecycle.js.map +1 -0
  43. package/dist/plugins/loader.d.ts +7 -0
  44. package/dist/plugins/loader.d.ts.map +1 -0
  45. package/dist/plugins/loader.js +68 -0
  46. package/dist/plugins/loader.js.map +1 -0
  47. package/dist/plugins/package-requirements.d.ts +16 -0
  48. package/dist/plugins/package-requirements.d.ts.map +1 -0
  49. package/dist/plugins/package-requirements.js +52 -0
  50. package/dist/plugins/package-requirements.js.map +1 -0
  51. package/dist/plugins/registry-kinds.d.ts +58 -0
  52. package/dist/plugins/registry-kinds.d.ts.map +1 -0
  53. package/dist/plugins/registry-kinds.js +139 -0
  54. package/dist/plugins/registry-kinds.js.map +1 -0
  55. package/dist/plugins/toposort.d.ts +24 -0
  56. package/dist/plugins/toposort.d.ts.map +1 -0
  57. package/dist/plugins/toposort.js +67 -0
  58. package/dist/plugins/toposort.js.map +1 -0
  59. package/dist/preferences.d.ts +40 -0
  60. package/dist/preferences.d.ts.map +1 -0
  61. package/dist/preferences.js +58 -0
  62. package/dist/preferences.js.map +1 -0
  63. package/dist/registries/active-backend-registry.d.ts +65 -0
  64. package/dist/registries/active-backend-registry.d.ts.map +1 -0
  65. package/dist/registries/active-backend-registry.js +108 -0
  66. package/dist/registries/active-backend-registry.js.map +1 -0
  67. package/dist/registries/active-def-registry.d.ts +59 -0
  68. package/dist/registries/active-def-registry.d.ts.map +1 -0
  69. package/dist/registries/active-def-registry.js +63 -0
  70. package/dist/registries/active-def-registry.js.map +1 -0
  71. package/dist/registries/agents.d.ts +16 -0
  72. package/dist/registries/agents.d.ts.map +1 -0
  73. package/dist/registries/agents.js +17 -0
  74. package/dist/registries/agents.js.map +1 -0
  75. package/dist/registries/cache-strategies.d.ts +12 -0
  76. package/dist/registries/cache-strategies.d.ts.map +1 -0
  77. package/dist/registries/cache-strategies.js +13 -0
  78. package/dist/registries/cache-strategies.js.map +1 -0
  79. package/dist/registries/channels.d.ts +16 -0
  80. package/dist/registries/channels.d.ts.map +1 -0
  81. package/dist/registries/channels.js +35 -0
  82. package/dist/registries/channels.js.map +1 -0
  83. package/dist/registries/commands.d.ts +29 -0
  84. package/dist/registries/commands.d.ts.map +1 -0
  85. package/dist/registries/commands.js +72 -0
  86. package/dist/registries/commands.js.map +1 -0
  87. package/dist/registries/compactors.d.ts +12 -0
  88. package/dist/registries/compactors.d.ts.map +1 -0
  89. package/dist/registries/compactors.js +13 -0
  90. package/dist/registries/compactors.js.map +1 -0
  91. package/dist/registries/def-map-registry.d.ts +33 -0
  92. package/dist/registries/def-map-registry.d.ts.map +1 -0
  93. package/dist/registries/def-map-registry.js +36 -0
  94. package/dist/registries/def-map-registry.js.map +1 -0
  95. package/dist/registries/embedders.d.ts +16 -0
  96. package/dist/registries/embedders.d.ts.map +1 -0
  97. package/dist/registries/embedders.js +17 -0
  98. package/dist/registries/embedders.js.map +1 -0
  99. package/dist/registries/isolators.d.ts +21 -0
  100. package/dist/registries/isolators.d.ts.map +1 -0
  101. package/dist/registries/isolators.js +32 -0
  102. package/dist/registries/isolators.js.map +1 -0
  103. package/dist/registries/modes.d.ts +28 -0
  104. package/dist/registries/modes.d.ts.map +1 -0
  105. package/dist/registries/modes.js +81 -0
  106. package/dist/registries/modes.js.map +1 -0
  107. package/dist/registries/providers.d.ts +44 -0
  108. package/dist/registries/providers.d.ts.map +1 -0
  109. package/dist/registries/providers.js +95 -0
  110. package/dist/registries/providers.js.map +1 -0
  111. package/dist/registries/skills.d.ts +24 -0
  112. package/dist/registries/skills.d.ts.map +1 -0
  113. package/dist/registries/skills.js +81 -0
  114. package/dist/registries/skills.js.map +1 -0
  115. package/dist/registries/surfaces.d.ts +13 -0
  116. package/dist/registries/surfaces.d.ts.map +1 -0
  117. package/dist/registries/surfaces.js +14 -0
  118. package/dist/registries/surfaces.js.map +1 -0
  119. package/dist/registries/synthesizers.d.ts +26 -0
  120. package/dist/registries/synthesizers.d.ts.map +1 -0
  121. package/dist/registries/synthesizers.js +34 -0
  122. package/dist/registries/synthesizers.js.map +1 -0
  123. package/dist/registries/tools.d.ts +49 -0
  124. package/dist/registries/tools.d.ts.map +1 -0
  125. package/dist/registries/tools.js +98 -0
  126. package/dist/registries/tools.js.map +1 -0
  127. package/dist/registries/transcribers.d.ts +17 -0
  128. package/dist/registries/transcribers.d.ts.map +1 -0
  129. package/dist/registries/transcribers.js +18 -0
  130. package/dist/registries/transcribers.js.map +1 -0
  131. package/dist/registries/tunnel-providers.d.ts +12 -0
  132. package/dist/registries/tunnel-providers.d.ts.map +1 -0
  133. package/dist/registries/tunnel-providers.js +13 -0
  134. package/dist/registries/tunnel-providers.js.map +1 -0
  135. package/dist/registries/view-renderers.d.ts +13 -0
  136. package/dist/registries/view-renderers.d.ts.map +1 -0
  137. package/dist/registries/view-renderers.js +14 -0
  138. package/dist/registries/view-renderers.js.map +1 -0
  139. package/dist/registries/workflow-executors.d.ts +13 -0
  140. package/dist/registries/workflow-executors.d.ts.map +1 -0
  141. package/dist/registries/workflow-executors.js +14 -0
  142. package/dist/registries/workflow-executors.js.map +1 -0
  143. package/dist/requirements.d.ts +59 -0
  144. package/dist/requirements.d.ts.map +1 -0
  145. package/dist/requirements.js +149 -0
  146. package/dist/requirements.js.map +1 -0
  147. package/dist/run-turn.d.ts +6 -0
  148. package/dist/run-turn.d.ts.map +1 -0
  149. package/dist/run-turn.js +141 -0
  150. package/dist/run-turn.js.map +1 -0
  151. package/dist/session-runtime.d.ts +67 -0
  152. package/dist/session-runtime.d.ts.map +1 -0
  153. package/dist/session-runtime.js +2 -0
  154. package/dist/session-runtime.js.map +1 -0
  155. package/dist/session.d.ts +203 -0
  156. package/dist/session.d.ts.map +1 -0
  157. package/dist/session.js +399 -0
  158. package/dist/session.js.map +1 -0
  159. package/dist/sessions/persistence.d.ts +184 -0
  160. package/dist/sessions/persistence.d.ts.map +1 -0
  161. package/dist/sessions/persistence.js +442 -0
  162. package/dist/sessions/persistence.js.map +1 -0
  163. package/dist/setup-agent.d.ts +91 -0
  164. package/dist/setup-agent.d.ts.map +1 -0
  165. package/dist/setup-agent.js +138 -0
  166. package/dist/setup-agent.js.map +1 -0
  167. package/dist/skills/index.d.ts +4 -0
  168. package/dist/skills/index.d.ts.map +1 -0
  169. package/dist/skills/index.js +4 -0
  170. package/dist/skills/index.js.map +1 -0
  171. package/dist/skills/loader.d.ts +16 -0
  172. package/dist/skills/loader.d.ts.map +1 -0
  173. package/dist/skills/loader.js +99 -0
  174. package/dist/skills/loader.js.map +1 -0
  175. package/dist/skills/parse.d.ts +5 -0
  176. package/dist/skills/parse.d.ts.map +1 -0
  177. package/dist/skills/parse.js +8 -0
  178. package/dist/skills/parse.js.map +1 -0
  179. package/dist/skills/synthesize-draft.d.ts +8 -0
  180. package/dist/skills/synthesize-draft.d.ts.map +1 -0
  181. package/dist/skills/synthesize-draft.js +32 -0
  182. package/dist/skills/synthesize-draft.js.map +1 -0
  183. package/dist/skills/synthesize.d.ts +33 -0
  184. package/dist/skills/synthesize.d.ts.map +1 -0
  185. package/dist/skills/synthesize.js +241 -0
  186. package/dist/skills/synthesize.js.map +1 -0
  187. package/dist/subagents/events.d.ts +20 -0
  188. package/dist/subagents/events.d.ts.map +1 -0
  189. package/dist/subagents/events.js +146 -0
  190. package/dist/subagents/events.js.map +1 -0
  191. package/dist/subagents/registry.d.ts +40 -0
  192. package/dist/subagents/registry.d.ts.map +1 -0
  193. package/dist/subagents/registry.js +42 -0
  194. package/dist/subagents/registry.js.map +1 -0
  195. package/dist/subagents/run-child.d.ts +43 -0
  196. package/dist/subagents/run-child.d.ts.map +1 -0
  197. package/dist/subagents/run-child.js +276 -0
  198. package/dist/subagents/run-child.js.map +1 -0
  199. package/dist/subagents/spawn.d.ts +8 -0
  200. package/dist/subagents/spawn.d.ts.map +1 -0
  201. package/dist/subagents/spawn.js +8 -0
  202. package/dist/subagents/spawn.js.map +1 -0
  203. package/dist/subagents/tools.d.ts +4 -0
  204. package/dist/subagents/tools.d.ts.map +1 -0
  205. package/dist/subagents/tools.js +13 -0
  206. package/dist/subagents/tools.js.map +1 -0
  207. package/dist/subagents.d.ts +3 -0
  208. package/dist/subagents.d.ts.map +1 -0
  209. package/dist/subagents.js +3 -0
  210. package/dist/subagents.js.map +1 -0
  211. package/dist/surfaces/host.d.ts +53 -0
  212. package/dist/surfaces/host.d.ts.map +1 -0
  213. package/dist/surfaces/host.js +177 -0
  214. package/dist/surfaces/host.js.map +1 -0
  215. package/dist/tunnel/localhost.d.ts +7 -0
  216. package/dist/tunnel/localhost.d.ts.map +1 -0
  217. package/dist/tunnel/localhost.js +21 -0
  218. package/dist/tunnel/localhost.js.map +1 -0
  219. package/dist/usage-stats.d.ts +42 -0
  220. package/dist/usage-stats.d.ts.map +1 -0
  221. package/dist/usage-stats.js +83 -0
  222. package/dist/usage-stats.js.map +1 -0
  223. package/dist/view/default-renderer.d.ts +7 -0
  224. package/dist/view/default-renderer.d.ts.map +1 -0
  225. package/dist/view/default-renderer.js +14 -0
  226. package/dist/view/default-renderer.js.map +1 -0
  227. package/dist/view/parse.d.ts +7 -0
  228. package/dist/view/parse.d.ts.map +1 -0
  229. package/dist/view/parse.js +479 -0
  230. package/dist/view/parse.js.map +1 -0
  231. package/package.json +61 -0
  232. package/src/events/factory.ts +27 -0
  233. package/src/events/log.test.ts +470 -0
  234. package/src/events/log.ts +256 -0
  235. package/src/index.ts +102 -0
  236. package/src/logger.ts +46 -0
  237. package/src/permissions/engine.test.ts +206 -0
  238. package/src/permissions/engine.ts +247 -0
  239. package/src/permissions/resolvers.test.ts +40 -0
  240. package/src/permissions/resolvers.ts +17 -0
  241. package/src/plugins/discovery.test.ts +115 -0
  242. package/src/plugins/discovery.ts +124 -0
  243. package/src/plugins/host-options.ts +73 -0
  244. package/src/plugins/host-view-tunnel.test.ts +86 -0
  245. package/src/plugins/host.test.ts +434 -0
  246. package/src/plugins/host.ts +327 -0
  247. package/src/plugins/lifecycle.test.ts +187 -0
  248. package/src/plugins/lifecycle.ts +136 -0
  249. package/src/plugins/loader.ts +79 -0
  250. package/src/plugins/package-requirements.ts +50 -0
  251. package/src/plugins/registry-kinds.test.ts +162 -0
  252. package/src/plugins/registry-kinds.ts +210 -0
  253. package/src/plugins/toposort.test.ts +66 -0
  254. package/src/plugins/toposort.ts +69 -0
  255. package/src/preferences.test.ts +105 -0
  256. package/src/preferences.ts +84 -0
  257. package/src/registries/active-backend-registry.ts +142 -0
  258. package/src/registries/active-def-registry.test.ts +103 -0
  259. package/src/registries/active-def-registry.ts +97 -0
  260. package/src/registries/agents.ts +18 -0
  261. package/src/registries/cache-strategies.ts +14 -0
  262. package/src/registries/channels.ts +42 -0
  263. package/src/registries/commands.test.ts +71 -0
  264. package/src/registries/commands.ts +78 -0
  265. package/src/registries/compactors.ts +14 -0
  266. package/src/registries/def-map-registry.test.ts +67 -0
  267. package/src/registries/def-map-registry.ts +59 -0
  268. package/src/registries/embedders.test.ts +54 -0
  269. package/src/registries/embedders.ts +18 -0
  270. package/src/registries/isolators.test.ts +43 -0
  271. package/src/registries/isolators.ts +38 -0
  272. package/src/registries/modes.test.ts +66 -0
  273. package/src/registries/modes.ts +80 -0
  274. package/src/registries/providers.test.ts +50 -0
  275. package/src/registries/providers.ts +97 -0
  276. package/src/registries/semantics.test.ts +117 -0
  277. package/src/registries/skills.ts +87 -0
  278. package/src/registries/surfaces.ts +18 -0
  279. package/src/registries/synthesizers.test.ts +63 -0
  280. package/src/registries/synthesizers.ts +36 -0
  281. package/src/registries/tools.test.ts +142 -0
  282. package/src/registries/tools.ts +145 -0
  283. package/src/registries/transcribers.test.ts +70 -0
  284. package/src/registries/transcribers.ts +19 -0
  285. package/src/registries/tunnel-providers.test.ts +30 -0
  286. package/src/registries/tunnel-providers.ts +14 -0
  287. package/src/registries/view-renderers.test.ts +68 -0
  288. package/src/registries/view-renderers.ts +15 -0
  289. package/src/registries/workflow-executors.test.ts +53 -0
  290. package/src/registries/workflow-executors.ts +15 -0
  291. package/src/requirements.targetinfo.test.ts +70 -0
  292. package/src/requirements.test.ts +111 -0
  293. package/src/requirements.ts +262 -0
  294. package/src/run-turn.test.ts +178 -0
  295. package/src/run-turn.ts +156 -0
  296. package/src/session-runtime.ts +75 -0
  297. package/src/session.test.ts +216 -0
  298. package/src/session.ts +491 -0
  299. package/src/sessions/persistence.flush.test.ts +100 -0
  300. package/src/sessions/persistence.ready.test.ts +91 -0
  301. package/src/sessions/persistence.test.ts +274 -0
  302. package/src/sessions/persistence.ts +498 -0
  303. package/src/setup-agent.test.ts +72 -0
  304. package/src/setup-agent.ts +189 -0
  305. package/src/skills/index.ts +14 -0
  306. package/src/skills/loader.test.ts +102 -0
  307. package/src/skills/loader.ts +114 -0
  308. package/src/skills/parse.test.ts +59 -0
  309. package/src/skills/parse.ts +11 -0
  310. package/src/skills/synthesize-draft.test.ts +79 -0
  311. package/src/skills/synthesize-draft.ts +46 -0
  312. package/src/skills/synthesize.test.ts +170 -0
  313. package/src/skills/synthesize.ts +304 -0
  314. package/src/subagents/events.test.ts +56 -0
  315. package/src/subagents/events.ts +194 -0
  316. package/src/subagents/registry.ts +62 -0
  317. package/src/subagents/run-child.test.ts +185 -0
  318. package/src/subagents/run-child.ts +425 -0
  319. package/src/subagents/spawn.ts +12 -0
  320. package/src/subagents/tools.ts +19 -0
  321. package/src/subagents.ts +2 -0
  322. package/src/surfaces/host.test.ts +330 -0
  323. package/src/surfaces/host.ts +192 -0
  324. package/src/tunnel/localhost.test.ts +22 -0
  325. package/src/tunnel/localhost.ts +21 -0
  326. package/src/usage-stats.test.ts +77 -0
  327. package/src/usage-stats.ts +119 -0
  328. package/src/view/default-renderer.test.ts +29 -0
  329. package/src/view/default-renderer.ts +14 -0
  330. package/src/view/parse-extended.test.ts +255 -0
  331. package/src/view/parse.test.ts +232 -0
  332. package/src/view/parse.ts +520 -0
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Subagent <-> parent log bridge. Wraps child `MoxxyEvent`s into
3
+ * `plugin_event` envelopes on the parent log so the TUI / exporters can
4
+ * render live progress, and emits the start/completed bookend envelopes.
5
+ */
6
+
7
+ import type {
8
+ MoxxyEvent,
9
+ SessionId,
10
+ StopReason,
11
+ SubagentSpec,
12
+ TurnId,
13
+ } from '@moxxy/sdk';
14
+ import { asPluginId } from '@moxxy/sdk';
15
+ import type { SessionRuntime } from '../session-runtime.js';
16
+
17
+ export const SUBAGENT_PLUGIN_ID = asPluginId('@moxxy/subagents');
18
+
19
+ export async function emitSubagentStart(
20
+ parentSession: SessionRuntime,
21
+ parentTurnId: TurnId,
22
+ label: string,
23
+ childSessionId: SessionId,
24
+ spec: SubagentSpec,
25
+ mode: string,
26
+ ): Promise<void> {
27
+ await parentSession.log.append({
28
+ type: 'plugin_event',
29
+ sessionId: parentSession.id,
30
+ turnId: parentTurnId,
31
+ source: 'plugin',
32
+ pluginId: SUBAGENT_PLUGIN_ID,
33
+ subtype: 'subagent_started',
34
+ payload: {
35
+ label,
36
+ childSessionId: String(childSessionId),
37
+ prompt: spec.prompt,
38
+ mode,
39
+ agentType: spec.agentType ?? 'default',
40
+ ...(spec.model ? { model: spec.model } : {}),
41
+ },
42
+ });
43
+ }
44
+
45
+ export async function emitSubagentCompleted(
46
+ parentSession: SessionRuntime,
47
+ parentTurnId: TurnId,
48
+ label: string,
49
+ childSessionId: SessionId,
50
+ text: string,
51
+ stopReason: StopReason,
52
+ errorMessage: string | null,
53
+ agentType: string,
54
+ tokensUsed: number,
55
+ ): Promise<void> {
56
+ await parentSession.log.append({
57
+ type: 'plugin_event',
58
+ sessionId: parentSession.id,
59
+ turnId: parentTurnId,
60
+ source: 'plugin',
61
+ pluginId: SUBAGENT_PLUGIN_ID,
62
+ subtype: 'subagent_completed',
63
+ payload: {
64
+ label,
65
+ childSessionId: String(childSessionId),
66
+ text,
67
+ stopReason,
68
+ agentType,
69
+ tokensUsed,
70
+ ...(errorMessage ? { error: errorMessage } : {}),
71
+ },
72
+ });
73
+ }
74
+
75
+ export async function emitSubagentWarning(
76
+ parentSession: SessionRuntime,
77
+ parentTurnId: TurnId,
78
+ label: string,
79
+ childSessionId: SessionId,
80
+ message: string,
81
+ ): Promise<void> {
82
+ await parentSession.log.append({
83
+ type: 'plugin_event',
84
+ sessionId: parentSession.id,
85
+ turnId: parentTurnId,
86
+ source: 'plugin',
87
+ pluginId: SUBAGENT_PLUGIN_ID,
88
+ subtype: 'subagent_warning',
89
+ payload: {
90
+ label,
91
+ childSessionId: String(childSessionId),
92
+ message,
93
+ },
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Map each interesting child event to a parent `plugin_event` so the TUI
99
+ * can render the subagent's progress in real time. Noisy / book-keeping
100
+ * events (mode_iteration, provider_request, provider_response,
101
+ * assistant_message — covered by the explicit `subagent_completed`) are
102
+ * filtered out to keep the parent log lean.
103
+ */
104
+ export async function streamChildEventToParent(
105
+ parentSession: SessionRuntime,
106
+ parentTurnId: TurnId,
107
+ label: string,
108
+ childSessionId: SessionId,
109
+ childEvt: MoxxyEvent,
110
+ ): Promise<void> {
111
+ const mapped = mapChildEvent(label, childSessionId, childEvt);
112
+ if (!mapped) return;
113
+ try {
114
+ await parentSession.log.append({
115
+ type: 'plugin_event',
116
+ sessionId: parentSession.id,
117
+ turnId: parentTurnId,
118
+ source: 'plugin',
119
+ pluginId: SUBAGENT_PLUGIN_ID,
120
+ subtype: mapped.subtype,
121
+ payload: mapped.payload,
122
+ });
123
+ } catch (err) {
124
+ // Forwarding a child progress event is best-effort: a parent-log append
125
+ // failure must not abort the subagent run. EventLog.append already swallows
126
+ // listener errors, so this rejection would otherwise vanish with zero
127
+ // trace (the surface just stops updating); surface it so a chronic
128
+ // forwarding failure during a long subagent run is at least diagnosable.
129
+ process.stderr.write(
130
+ `moxxy: dropped subagent progress event (${mapped.subtype}) — parent log append failed: ` +
131
+ `${err instanceof Error ? err.message : String(err)}\n`,
132
+ );
133
+ }
134
+ }
135
+
136
+ function mapChildEvent(
137
+ label: string,
138
+ childSessionId: SessionId,
139
+ childEvt: MoxxyEvent,
140
+ ): { subtype: string; payload: Record<string, unknown> } | null {
141
+ const payload: Record<string, unknown> = {
142
+ label,
143
+ childSessionId: String(childSessionId),
144
+ };
145
+ switch (childEvt.type) {
146
+ case 'assistant_chunk':
147
+ payload.delta = childEvt.delta;
148
+ return { subtype: 'subagent_chunk', payload };
149
+ case 'tool_call_requested':
150
+ payload.name = childEvt.name;
151
+ payload.input = childEvt.input;
152
+ payload.callId = String(childEvt.callId);
153
+ return { subtype: 'subagent_tool_call', payload };
154
+ case 'tool_result':
155
+ payload.callId = String(childEvt.callId);
156
+ payload.ok = childEvt.ok;
157
+ if (childEvt.ok) payload.output = childEvt.output;
158
+ else payload.error = childEvt.error;
159
+ return { subtype: 'subagent_tool_result', payload };
160
+ case 'error':
161
+ payload.kind = childEvt.kind;
162
+ payload.message = childEvt.message;
163
+ return { subtype: 'subagent_error', payload };
164
+ case 'abort':
165
+ payload.reason = childEvt.reason;
166
+ return { subtype: 'subagent_abort', payload };
167
+ case 'plugin_event':
168
+ return mapNestedPluginEvent(label, payload, childEvt);
169
+ default:
170
+ return null;
171
+ }
172
+ }
173
+
174
+ function mapNestedPluginEvent(
175
+ label: string,
176
+ payload: Record<string, unknown>,
177
+ childEvt: Extract<MoxxyEvent, { type: 'plugin_event' }>,
178
+ ): { subtype: string; payload: Record<string, unknown> } | null {
179
+ // Bubble nested subagent events too, so a grand-child's progress
180
+ // surfaces all the way up. We strip the nested label-prefix to
181
+ // keep things compact; payload retains the chain via the embedded
182
+ // childSessionId.
183
+ const nestedSubtype = childEvt.subtype;
184
+ if (typeof nestedSubtype !== 'string' || !nestedSubtype.startsWith('subagent_')) return null;
185
+ const nestedPayload = childEvt.payload;
186
+ if (nestedPayload && typeof nestedPayload === 'object') {
187
+ for (const [k, v] of Object.entries(nestedPayload as Record<string, unknown>)) {
188
+ if (k !== 'label' && k !== 'childSessionId') payload[k] = v;
189
+ }
190
+ // Preserve the chain via a `via` field naming the immediate parent label.
191
+ payload.via = label;
192
+ }
193
+ return { subtype: nestedSubtype, payload };
194
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Retained-child session registry — backs the `awaitInput` pause/resume flow.
3
+ *
4
+ * A subagent spawned with `retainSession: true` keeps its `EventLog` and
5
+ * `ModeContext` alive after its first turn so a later `continue()` can append
6
+ * an operator reply and run it again. The registry is a process-local map
7
+ * keyed by child session id; entries are dropped on `continue` or `release`.
8
+ *
9
+ * Each entry records its `parentSession`, so {@link clearRetainedChildren}
10
+ * drops only the children belonging to the session that is closing. The map is
11
+ * process-wide (one per module), but a `Session.close()` must NOT wipe another
12
+ * live session's paused children — that would break their `continue()` in any
13
+ * host that runs >1 Session per process (the desktop in-process path, tests).
14
+ */
15
+
16
+ import type { ModeContext, SessionId, SubagentSpec, TurnId } from '@moxxy/sdk';
17
+ import type { EventLog } from '../events/log.js';
18
+ import type { SessionRuntime } from '../session-runtime.js';
19
+
20
+ export interface RetainedChildSession {
21
+ readonly label: string;
22
+ readonly childSessionId: SessionId;
23
+ readonly childTurnId: TurnId;
24
+ readonly childLog: EventLog;
25
+ readonly childCtx: ModeContext;
26
+ readonly spec: SubagentSpec;
27
+ readonly strategy: ReturnType<SessionRuntime['modes']['list']>[number];
28
+ readonly strategyName: string;
29
+ readonly parentSession: SessionRuntime;
30
+ readonly parentTurnId: TurnId;
31
+ }
32
+
33
+ const retained = new Map<string, RetainedChildSession>();
34
+
35
+ export function registerRetainedChild(session: RetainedChildSession): void {
36
+ retained.set(String(session.childSessionId), session);
37
+ }
38
+
39
+ export function getRetainedChild(childSessionId: SessionId): RetainedChildSession | undefined {
40
+ return retained.get(String(childSessionId));
41
+ }
42
+
43
+ export function releaseRetainedChild(childSessionId: SessionId): void {
44
+ retained.delete(String(childSessionId));
45
+ }
46
+
47
+ /**
48
+ * Drop retained children. With a `parentSessionId`, drop ONLY the children
49
+ * whose `parentSession` is that session — so one session's `close()` can't wipe
50
+ * another live session's paused `awaitInput` children. Without an id (legacy /
51
+ * test-teardown callers), clear everything.
52
+ */
53
+ export function clearRetainedChildren(parentSessionId?: SessionId): void {
54
+ if (parentSessionId === undefined) {
55
+ retained.clear();
56
+ return;
57
+ }
58
+ const target = String(parentSessionId);
59
+ for (const [childId, entry] of retained) {
60
+ if (String(entry.parentSession.id) === target) retained.delete(childId);
61
+ }
62
+ }
@@ -0,0 +1,185 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ModeContext, ModeDef, MoxxyEvent, ProviderDef } from '@moxxy/sdk';
3
+ import { defineMode, defineProvider, definePlugin } from '@moxxy/sdk';
4
+ import { Session } from '../session.js';
5
+ import { runChildTurn, type SubagentRuntime } from './run-child.js';
6
+
7
+ // A mode that reports the model it was handed — lets tests observe the
8
+ // child's EFFECTIVE model through the returned result text.
9
+ function makeEchoModelMode(): ModeDef {
10
+ return defineMode({
11
+ name: 'echo-model',
12
+ run: async function* (ctx: ModeContext): AsyncIterable<MoxxyEvent> {
13
+ await ctx.emit({
14
+ type: 'assistant_message',
15
+ sessionId: ctx.sessionId,
16
+ turnId: ctx.turnId,
17
+ source: 'assistant',
18
+ content: ctx.model,
19
+ stopReason: 'end_turn',
20
+ });
21
+ },
22
+ });
23
+ }
24
+
25
+ // A mode that spawns a grandchild (no model override) and reports what model
26
+ // the grandchild ran on — exercises nested-spawner model inheritance.
27
+ function makeNestedSpawnMode(): ModeDef {
28
+ return defineMode({
29
+ name: 'nested-spawn',
30
+ run: async function* (ctx: ModeContext): AsyncIterable<MoxxyEvent> {
31
+ const result = await ctx.subagents!.spawn({ prompt: 'echo', mode: 'echo-model' });
32
+ await ctx.emit({
33
+ type: 'assistant_message',
34
+ sessionId: ctx.sessionId,
35
+ turnId: ctx.turnId,
36
+ source: 'assistant',
37
+ content: result.text,
38
+ stopReason: 'end_turn',
39
+ });
40
+ },
41
+ });
42
+ }
43
+
44
+ function makeProvider(modelIds: ReadonlyArray<string>): ProviderDef {
45
+ const models = modelIds.map((id) => ({ id }));
46
+ return defineProvider({
47
+ name: 'listed',
48
+ models,
49
+ createClient: () => ({
50
+ name: 'listed',
51
+ models,
52
+ stream: async function* () {
53
+ // unused — the test modes don't call into the provider.
54
+ },
55
+ countTokens: async () => 0,
56
+ }),
57
+ });
58
+ }
59
+
60
+ function buildSession(modelIds: ReadonlyArray<string>): Session {
61
+ const session = new Session({ cwd: '/tmp', silent: true });
62
+ session.pluginHost.registerStatic(
63
+ definePlugin({
64
+ name: 'test-subagent-models',
65
+ version: '0.0.0',
66
+ providers: [makeProvider(modelIds)],
67
+ modes: [makeEchoModelMode(), makeNestedSpawnMode()],
68
+ }),
69
+ );
70
+ session.providers.setActive('listed');
71
+ session.modes.setActive('echo-model');
72
+ return session;
73
+ }
74
+
75
+ function buildRuntime(session: Session, parentModel: string): SubagentRuntime {
76
+ return {
77
+ parentSession: session,
78
+ parentTurnId: session.startTurn().turnId,
79
+ parentSignal: new AbortController().signal,
80
+ parentModel,
81
+ };
82
+ }
83
+
84
+ function collectParentEvents(session: Session): MoxxyEvent[] {
85
+ const events: MoxxyEvent[] = [];
86
+ session.log.subscribe((e) => void events.push(e));
87
+ return events;
88
+ }
89
+
90
+ function findWarning(events: ReadonlyArray<MoxxyEvent>): string | undefined {
91
+ const evt = events.find((e) => e.type === 'plugin_event' && e.subtype === 'subagent_warning');
92
+ return evt?.type === 'plugin_event' ? (evt.payload as { message: string }).message : undefined;
93
+ }
94
+
95
+ describe('runChildTurn model resolution', () => {
96
+ it('falls back to the parent model (with a warning) on an unknown model id', async () => {
97
+ const session = buildSession(['parent-model', 'cheap-model']);
98
+ const events = collectParentEvents(session);
99
+ const rt = buildRuntime(session, 'parent-model');
100
+
101
+ const result = await runChildTurn({
102
+ rt,
103
+ spec: { prompt: 'echo', mode: 'echo-model', model: 'claude-3-5-sonnet' },
104
+ retainSession: false,
105
+ });
106
+
107
+ expect(result.text).toBe('parent-model');
108
+ expect(findWarning(events)).toBe(
109
+ 'unknown model "claude-3-5-sonnet" — falling back to parent model "parent-model"',
110
+ );
111
+ });
112
+
113
+ it('honors a model id the active provider lists, without warning', async () => {
114
+ const session = buildSession(['parent-model', 'cheap-model']);
115
+ const events = collectParentEvents(session);
116
+ const rt = buildRuntime(session, 'parent-model');
117
+
118
+ const result = await runChildTurn({
119
+ rt,
120
+ spec: { prompt: 'echo', mode: 'echo-model', model: 'cheap-model' },
121
+ retainSession: false,
122
+ });
123
+
124
+ expect(result.text).toBe('cheap-model');
125
+ expect(findWarning(events)).toBeUndefined();
126
+ });
127
+
128
+ it('inherits the parent model when no override is given', async () => {
129
+ const session = buildSession(['parent-model']);
130
+ const rt = buildRuntime(session, 'parent-model');
131
+
132
+ const result = await runChildTurn({
133
+ rt,
134
+ spec: { prompt: 'echo', mode: 'echo-model' },
135
+ retainSession: false,
136
+ });
137
+
138
+ expect(result.text).toBe('parent-model');
139
+ });
140
+
141
+ it('skips validation when the provider publishes no models (sparse providers)', async () => {
142
+ const session = buildSession([]);
143
+ const events = collectParentEvents(session);
144
+ const rt = buildRuntime(session, 'parent-model');
145
+
146
+ const result = await runChildTurn({
147
+ rt,
148
+ spec: { prompt: 'echo', mode: 'echo-model', model: 'unlisted-but-real' },
149
+ retainSession: false,
150
+ });
151
+
152
+ expect(result.text).toBe('unlisted-but-real');
153
+ expect(findWarning(events)).toBeUndefined();
154
+ });
155
+
156
+ it('does not warn when the override IS the parent model, even if unlisted', async () => {
157
+ const session = buildSession(['some-other-model']);
158
+ const events = collectParentEvents(session);
159
+ const rt = buildRuntime(session, 'unlisted-parent');
160
+
161
+ const result = await runChildTurn({
162
+ rt,
163
+ spec: { prompt: 'echo', mode: 'echo-model', model: 'unlisted-parent' },
164
+ retainSession: false,
165
+ });
166
+
167
+ expect(result.text).toBe('unlisted-parent');
168
+ expect(findWarning(events)).toBeUndefined();
169
+ });
170
+
171
+ it('grandchildren inherit the child effective model, not the grandparent model', async () => {
172
+ const session = buildSession(['parent-model', 'cheap-model']);
173
+ const rt = buildRuntime(session, 'parent-model');
174
+
175
+ const result = await runChildTurn({
176
+ rt,
177
+ spec: { prompt: 'spawn nested', mode: 'nested-spawn', model: 'cheap-model' },
178
+ retainSession: false,
179
+ });
180
+
181
+ // The grandchild echoes its own ctx.model — it must see the child's
182
+ // resolved model ('cheap-model'), not the original parent's.
183
+ expect(result.text).toBe('cheap-model');
184
+ });
185
+ });