@adecore/agents 0.0.1 → 0.17.0-beta.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 (348) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +48 -0
  3. package/dist/async.d.ts +2 -0
  4. package/dist/async.js +31 -0
  5. package/dist/chat/attachment-store.d.ts +10 -0
  6. package/dist/chat/attachment-store.js +93 -0
  7. package/dist/chat/backend.d.ts +214 -0
  8. package/dist/chat/backend.js +1 -0
  9. package/dist/chat/background-work.d.ts +8 -0
  10. package/dist/chat/background-work.js +33 -0
  11. package/dist/chat/bookmark-store.d.ts +25 -0
  12. package/dist/chat/bookmark-store.js +141 -0
  13. package/dist/chat/chat-core.d.ts +311 -0
  14. package/dist/chat/chat-core.js +1025 -0
  15. package/dist/chat/chat-log.d.ts +33 -0
  16. package/dist/chat/chat-log.js +163 -0
  17. package/dist/chat/chat-process.d.ts +46 -0
  18. package/dist/chat/chat-process.js +190 -0
  19. package/dist/chat/chat-session.d.ts +209 -0
  20. package/dist/chat/chat-session.js +1511 -0
  21. package/dist/chat/chat-store.d.ts +37 -0
  22. package/dist/chat/chat-store.js +202 -0
  23. package/dist/chat/chat-title.d.ts +10 -0
  24. package/dist/chat/chat-title.js +74 -0
  25. package/dist/chat/claude-backend.d.ts +35 -0
  26. package/dist/chat/claude-backend.js +214 -0
  27. package/dist/chat/claude-protocol.d.ts +41 -0
  28. package/dist/chat/claude-protocol.js +642 -0
  29. package/dist/chat/claude-title.d.ts +11 -0
  30. package/dist/chat/claude-title.js +97 -0
  31. package/dist/chat/claude-transcript.d.ts +28 -0
  32. package/dist/chat/claude-transcript.js +211 -0
  33. package/dist/chat/codex-backend.d.ts +51 -0
  34. package/dist/chat/codex-backend.js +364 -0
  35. package/dist/chat/codex-protocol.d.ts +56 -0
  36. package/dist/chat/codex-protocol.js +767 -0
  37. package/dist/chat/codex-thread.d.ts +32 -0
  38. package/dist/chat/codex-thread.js +131 -0
  39. package/dist/chat/codex-transport.d.ts +42 -0
  40. package/dist/chat/codex-transport.js +188 -0
  41. package/dist/chat/composer-preferences.d.ts +15 -0
  42. package/dist/chat/composer-preferences.js +46 -0
  43. package/dist/chat/context-breakdown.d.ts +2 -0
  44. package/dist/chat/context-breakdown.js +74 -0
  45. package/dist/chat/delta-coalescer.d.ts +12 -0
  46. package/dist/chat/delta-coalescer.js +55 -0
  47. package/dist/chat/errors.d.ts +4 -0
  48. package/dist/chat/errors.js +4 -0
  49. package/dist/chat/fake-claude.d.ts +3 -0
  50. package/dist/chat/fake-claude.js +982 -0
  51. package/dist/chat/fake-cli.d.ts +25 -0
  52. package/dist/chat/fake-cli.js +157 -0
  53. package/dist/chat/fake-codex.d.ts +10 -0
  54. package/dist/chat/fake-codex.js +502 -0
  55. package/dist/chat/fixtures/claude-2.1.285-auto-fallback.json +24 -0
  56. package/dist/chat/fixtures/claude-2.1.285-native-children.json +6600 -0
  57. package/dist/chat/fixtures/claude-2.1.285-resume-direct-compact.json +202 -0
  58. package/dist/chat/fixtures/claude-2.1.285-resume-task-notification.json +57 -0
  59. package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.jsonl +10 -0
  60. package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.meta.json +1 -0
  61. package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.jsonl +2 -0
  62. package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.meta.json +1 -0
  63. package/dist/chat/fixtures/codex-0.159.2-native-approval.json +119 -0
  64. package/dist/chat/fixtures/codex-0.159.2-native-file-approval.json +136 -0
  65. package/dist/chat/fixtures/codex-0.159.2-native-interacted.json +220 -0
  66. package/dist/chat/fixtures/codex-0.159.2-usage-windows.json +74 -0
  67. package/dist/chat/input.d.ts +19 -0
  68. package/dist/chat/input.js +71 -0
  69. package/dist/chat/limit-resume.d.ts +11 -0
  70. package/dist/chat/limit-resume.js +68 -0
  71. package/dist/chat/projector.d.ts +60 -0
  72. package/dist/chat/projector.js +860 -0
  73. package/dist/chat/request-summary.d.ts +3 -0
  74. package/dist/chat/request-summary.js +143 -0
  75. package/dist/chat/subagent-projection.d.ts +8 -0
  76. package/dist/chat/subagent-projection.js +40 -0
  77. package/dist/chat/subagent-reader.d.ts +66 -0
  78. package/dist/chat/subagent-reader.js +396 -0
  79. package/dist/chat/subagent-settlement.d.ts +6 -0
  80. package/dist/chat/subagent-settlement.js +103 -0
  81. package/dist/chat/thread.d.ts +30 -0
  82. package/dist/chat/thread.js +147 -0
  83. package/dist/chat/wake-chat.d.ts +20 -0
  84. package/dist/chat/wake-chat.js +27 -0
  85. package/dist/client-sinks.d.ts +9 -0
  86. package/dist/client-sinks.js +34 -0
  87. package/dist/coded-error.d.ts +5 -0
  88. package/dist/coded-error.js +17 -0
  89. package/dist/context/argv.d.ts +11 -0
  90. package/dist/context/argv.js +58 -0
  91. package/dist/context/refusal.d.ts +9 -0
  92. package/dist/context/refusal.js +38 -0
  93. package/dist/context/verb.d.ts +85 -0
  94. package/dist/context/verb.js +233 -0
  95. package/dist/error-text.d.ts +5 -0
  96. package/dist/error-text.js +24 -0
  97. package/dist/events.d.ts +12 -0
  98. package/dist/events.js +1 -0
  99. package/dist/fs.d.ts +9 -0
  100. package/dist/fs.js +107 -0
  101. package/dist/host/agent-host.d.ts +28 -0
  102. package/dist/host/agent-host.js +105 -0
  103. package/dist/host/environment.d.ts +7 -0
  104. package/dist/host/environment.js +22 -0
  105. package/dist/host/handlers.d.ts +30 -0
  106. package/dist/host/handlers.js +111 -0
  107. package/dist/host/memory-port.d.ts +2 -0
  108. package/dist/host/memory-port.js +25 -0
  109. package/dist/host/wiring.d.ts +40 -0
  110. package/dist/host/wiring.js +120 -0
  111. package/dist/lineage.d.ts +44 -0
  112. package/dist/lineage.js +134 -0
  113. package/dist/messages/deliver-message.d.ts +21 -0
  114. package/dist/messages/deliver-message.js +42 -0
  115. package/dist/messages/deliver-notice.d.ts +25 -0
  116. package/dist/messages/deliver-notice.js +75 -0
  117. package/dist/messages/notice-notes.d.ts +15 -0
  118. package/dist/messages/notice-notes.js +26 -0
  119. package/dist/messages/notice-store.d.ts +30 -0
  120. package/dist/messages/notice-store.js +120 -0
  121. package/dist/modes.d.ts +11 -0
  122. package/dist/modes.js +28 -0
  123. package/dist/outbox/manual-clock.d.ts +10 -0
  124. package/dist/outbox/manual-clock.js +25 -0
  125. package/dist/outbox/outbox-worker.d.ts +44 -0
  126. package/dist/outbox/outbox-worker.js +173 -0
  127. package/dist/outbox/outbox.d.ts +37 -0
  128. package/dist/outbox/outbox.js +76 -0
  129. package/dist/providers/accounts/accounts.d.ts +20 -0
  130. package/dist/providers/accounts/accounts.js +158 -0
  131. package/dist/providers/accounts/launch.d.ts +17 -0
  132. package/dist/providers/accounts/launch.js +30 -0
  133. package/dist/providers/accounts/service.d.ts +87 -0
  134. package/dist/providers/accounts/service.js +538 -0
  135. package/dist/providers/accounts/shadow-home.d.ts +8 -0
  136. package/dist/providers/accounts/shadow-home.js +70 -0
  137. package/dist/providers/accounts/status.d.ts +12 -0
  138. package/dist/providers/accounts/status.js +85 -0
  139. package/dist/providers/accounts/store.d.ts +14 -0
  140. package/dist/providers/accounts/store.js +112 -0
  141. package/dist/providers/accounts/test-accounts.d.ts +12 -0
  142. package/dist/providers/accounts/test-accounts.js +38 -0
  143. package/dist/providers/accounts/variables.d.ts +27 -0
  144. package/dist/providers/accounts/variables.js +94 -0
  145. package/dist/providers/catalog.d.ts +17 -0
  146. package/dist/providers/catalog.js +76 -0
  147. package/dist/providers/claude-models.json +114 -0
  148. package/dist/providers/claude-provider.d.ts +4 -0
  149. package/dist/providers/claude-provider.js +29 -0
  150. package/dist/providers/claude.d.ts +15 -0
  151. package/dist/providers/claude.js +105 -0
  152. package/dist/providers/codex-models.json +135 -0
  153. package/dist/providers/codex-provider.d.ts +4 -0
  154. package/dist/providers/codex-provider.js +27 -0
  155. package/dist/providers/codex-rules.d.ts +9 -0
  156. package/dist/providers/codex-rules.js +35 -0
  157. package/dist/providers/codex.d.ts +14 -0
  158. package/dist/providers/codex.js +52 -0
  159. package/dist/providers/detect.d.ts +6 -0
  160. package/dist/providers/detect.js +30 -0
  161. package/dist/providers/provider.d.ts +25 -0
  162. package/dist/providers/provider.js +1 -0
  163. package/dist/providers/registry.d.ts +28 -0
  164. package/dist/providers/registry.js +118 -0
  165. package/dist/record-directory.d.ts +27 -0
  166. package/dist/record-directory.js +114 -0
  167. package/dist/run-process.d.ts +13 -0
  168. package/dist/run-process.js +42 -0
  169. package/dist/serializer.d.ts +9 -0
  170. package/dist/serializer.js +34 -0
  171. package/dist/skills.d.ts +21 -0
  172. package/dist/skills.js +174 -0
  173. package/dist/tasks/background-limit.d.ts +16 -0
  174. package/dist/tasks/background-limit.js +55 -0
  175. package/dist/tasks/end-children.d.ts +34 -0
  176. package/dist/tasks/end-children.js +53 -0
  177. package/dist/tasks/give-task.d.ts +14 -0
  178. package/dist/tasks/give-task.js +33 -0
  179. package/dist/tasks/task-coordinator.d.ts +52 -0
  180. package/dist/tasks/task-coordinator.js +323 -0
  181. package/dist/tasks/task-store.d.ts +49 -0
  182. package/dist/tasks/task-store.js +197 -0
  183. package/dist/tasks/task-work.d.ts +47 -0
  184. package/dist/tasks/task-work.js +28 -0
  185. package/dist/tasks/waiting-child.d.ts +51 -0
  186. package/dist/tasks/waiting-child.js +143 -0
  187. package/dist/tasks/wake-parent.d.ts +19 -0
  188. package/dist/tasks/wake-parent.js +109 -0
  189. package/dist/tasks/wiring.d.ts +51 -0
  190. package/dist/tasks/wiring.js +151 -0
  191. package/dist/title-file.d.ts +2 -0
  192. package/dist/title-file.js +55 -0
  193. package/dist/usage/accounts.d.ts +15 -0
  194. package/dist/usage/accounts.js +64 -0
  195. package/dist/usage/aggregate.d.ts +12 -0
  196. package/dist/usage/aggregate.js +154 -0
  197. package/dist/usage/exchange.d.ts +16 -0
  198. package/dist/usage/exchange.js +84 -0
  199. package/dist/usage/index-file.d.ts +15 -0
  200. package/dist/usage/index-file.js +123 -0
  201. package/dist/usage/limits/monitor.d.ts +50 -0
  202. package/dist/usage/limits/monitor.js +179 -0
  203. package/dist/usage/limits/normalize.d.ts +21 -0
  204. package/dist/usage/limits/normalize.js +160 -0
  205. package/dist/usage/limits/probe.d.ts +9 -0
  206. package/dist/usage/limits/probe.js +113 -0
  207. package/dist/usage/prices-snapshot.json +1071 -0
  208. package/dist/usage/pricing.d.ts +34 -0
  209. package/dist/usage/pricing.js +186 -0
  210. package/dist/usage/projects.d.ts +17 -0
  211. package/dist/usage/projects.js +49 -0
  212. package/dist/usage/readers/claude.d.ts +3 -0
  213. package/dist/usage/readers/claude.js +59 -0
  214. package/dist/usage/readers/codex.d.ts +23 -0
  215. package/dist/usage/readers/codex.js +155 -0
  216. package/dist/usage/record.d.ts +15 -0
  217. package/dist/usage/record.js +39 -0
  218. package/dist/usage/roots.d.ts +12 -0
  219. package/dist/usage/roots.js +29 -0
  220. package/dist/usage/scanner.d.ts +30 -0
  221. package/dist/usage/scanner.js +234 -0
  222. package/dist/usage/usage-service.d.ts +35 -0
  223. package/dist/usage/usage-service.js +130 -0
  224. package/dist/watch-seam.d.ts +29 -0
  225. package/dist/watch-seam.js +78 -0
  226. package/dist/watch-test-helpers.d.ts +28 -0
  227. package/dist/watch-test-helpers.js +86 -0
  228. package/package.json +42 -3
  229. package/src/async.ts +40 -0
  230. package/src/chat/attachment-store.ts +108 -0
  231. package/src/chat/backend.ts +199 -0
  232. package/src/chat/background-work.ts +42 -0
  233. package/src/chat/bookmark-store.ts +166 -0
  234. package/src/chat/chat-core.ts +1228 -0
  235. package/src/chat/chat-log.ts +187 -0
  236. package/src/chat/chat-process.ts +237 -0
  237. package/src/chat/chat-session.ts +1723 -0
  238. package/src/chat/chat-store.ts +261 -0
  239. package/src/chat/chat-title.ts +90 -0
  240. package/src/chat/claude-backend.ts +235 -0
  241. package/src/chat/claude-protocol.ts +684 -0
  242. package/src/chat/claude-title.ts +106 -0
  243. package/src/chat/claude-transcript.ts +233 -0
  244. package/src/chat/codex-backend.ts +397 -0
  245. package/src/chat/codex-protocol.ts +872 -0
  246. package/src/chat/codex-thread.ts +162 -0
  247. package/src/chat/codex-transport.ts +224 -0
  248. package/src/chat/composer-preferences.ts +67 -0
  249. package/src/chat/context-breakdown.ts +80 -0
  250. package/src/chat/delta-coalescer.ts +62 -0
  251. package/src/chat/errors.ts +25 -0
  252. package/src/chat/fake-claude.ts +1006 -0
  253. package/src/chat/fake-cli.ts +199 -0
  254. package/src/chat/fake-codex.ts +534 -0
  255. package/src/chat/fixtures/claude-2.1.285-auto-fallback.json +24 -0
  256. package/src/chat/fixtures/claude-2.1.285-native-children.json +6600 -0
  257. package/src/chat/fixtures/claude-2.1.285-resume-direct-compact.json +202 -0
  258. package/src/chat/fixtures/claude-2.1.285-resume-task-notification.json +57 -0
  259. package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.jsonl +10 -0
  260. package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.meta.json +1 -0
  261. package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.jsonl +2 -0
  262. package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.meta.json +1 -0
  263. package/src/chat/fixtures/codex-0.159.2-native-approval.json +119 -0
  264. package/src/chat/fixtures/codex-0.159.2-native-file-approval.json +136 -0
  265. package/src/chat/fixtures/codex-0.159.2-native-interacted.json +220 -0
  266. package/src/chat/fixtures/codex-0.159.2-usage-windows.json +74 -0
  267. package/src/chat/input.ts +89 -0
  268. package/src/chat/limit-resume.ts +74 -0
  269. package/src/chat/projector.ts +969 -0
  270. package/src/chat/request-summary.ts +168 -0
  271. package/src/chat/subagent-projection.ts +45 -0
  272. package/src/chat/subagent-reader.ts +469 -0
  273. package/src/chat/subagent-settlement.ts +111 -0
  274. package/src/chat/thread.ts +167 -0
  275. package/src/chat/wake-chat.ts +45 -0
  276. package/src/client-sinks.ts +39 -0
  277. package/src/coded-error.ts +18 -0
  278. package/src/context/argv.ts +61 -0
  279. package/src/context/refusal.ts +50 -0
  280. package/src/context/verb.ts +392 -0
  281. package/src/error-text.ts +28 -0
  282. package/src/events.ts +9 -0
  283. package/src/fs.ts +118 -0
  284. package/src/host/agent-host.ts +124 -0
  285. package/src/host/environment.ts +31 -0
  286. package/src/host/handlers.ts +142 -0
  287. package/src/host/memory-port.ts +27 -0
  288. package/src/host/wiring.ts +164 -0
  289. package/src/lineage.ts +159 -0
  290. package/src/messages/deliver-message.ts +61 -0
  291. package/src/messages/deliver-notice.ts +120 -0
  292. package/src/messages/notice-notes.ts +34 -0
  293. package/src/messages/notice-store.ts +142 -0
  294. package/src/modes.ts +38 -0
  295. package/src/outbox/manual-clock.ts +31 -0
  296. package/src/outbox/outbox-worker.ts +214 -0
  297. package/src/outbox/outbox.ts +106 -0
  298. package/src/providers/accounts/accounts.ts +192 -0
  299. package/src/providers/accounts/launch.ts +55 -0
  300. package/src/providers/accounts/service.ts +632 -0
  301. package/src/providers/accounts/shadow-home.ts +85 -0
  302. package/src/providers/accounts/status.ts +106 -0
  303. package/src/providers/accounts/store.ts +158 -0
  304. package/src/providers/accounts/test-accounts.ts +48 -0
  305. package/src/providers/accounts/variables.ts +132 -0
  306. package/src/providers/catalog.ts +90 -0
  307. package/src/providers/claude-models.json +114 -0
  308. package/src/providers/claude-provider.ts +32 -0
  309. package/src/providers/claude.ts +123 -0
  310. package/src/providers/codex-models.json +135 -0
  311. package/src/providers/codex-provider.ts +30 -0
  312. package/src/providers/codex-rules.ts +54 -0
  313. package/src/providers/codex.ts +69 -0
  314. package/src/providers/detect.ts +40 -0
  315. package/src/providers/provider.ts +46 -0
  316. package/src/providers/registry.ts +150 -0
  317. package/src/record-directory.ts +136 -0
  318. package/src/run-process.ts +61 -0
  319. package/src/serializer.ts +41 -0
  320. package/src/skills.ts +199 -0
  321. package/src/tasks/background-limit.ts +72 -0
  322. package/src/tasks/end-children.ts +100 -0
  323. package/src/tasks/give-task.ts +52 -0
  324. package/src/tasks/task-coordinator.ts +384 -0
  325. package/src/tasks/task-store.ts +236 -0
  326. package/src/tasks/task-work.ts +62 -0
  327. package/src/tasks/waiting-child.ts +203 -0
  328. package/src/tasks/wake-parent.ts +142 -0
  329. package/src/tasks/wiring.ts +231 -0
  330. package/src/title-file.ts +57 -0
  331. package/src/usage/accounts.ts +86 -0
  332. package/src/usage/aggregate.ts +220 -0
  333. package/src/usage/exchange.ts +93 -0
  334. package/src/usage/index-file.ts +178 -0
  335. package/src/usage/limits/monitor.ts +239 -0
  336. package/src/usage/limits/normalize.ts +188 -0
  337. package/src/usage/limits/probe.ts +120 -0
  338. package/src/usage/prices-snapshot.json +1071 -0
  339. package/src/usage/pricing.ts +236 -0
  340. package/src/usage/projects.ts +65 -0
  341. package/src/usage/readers/claude.ts +60 -0
  342. package/src/usage/readers/codex.ts +196 -0
  343. package/src/usage/record.ts +60 -0
  344. package/src/usage/roots.ts +39 -0
  345. package/src/usage/scanner.ts +265 -0
  346. package/src/usage/usage-service.ts +161 -0
  347. package/src/watch-seam.ts +111 -0
  348. package/src/watch-test-helpers.ts +102 -0
@@ -0,0 +1,100 @@
1
+ import { z } from 'zod';
2
+ import type { AgentLineageStore } from '../lineage.ts';
3
+ import type { OutboxEntryOf } from '../outbox/outbox.ts';
4
+ import type { TaskStore } from './task-store.ts';
5
+ import type { AnyOutboxEntry } from './task-work.ts';
6
+
7
+ export const EndChildrenWorkSchema = z.object({
8
+ kind: z.literal('end-children'),
9
+ // The target is the node that was stopped or deleted; these are the agents it had opened when that was owed.
10
+ payload: z.object({ nodeIds: z.array(z.string().min(1)) })
11
+ });
12
+
13
+ export type EndChildrenWork = z.infer<typeof EndChildrenWorkSchema>;
14
+ export type EndChildrenEntry = OutboxEntryOf<EndChildrenWork>;
15
+
16
+ /* The work of tasks that would start or wake an agent again; an agent that was ended has none of it left. */
17
+ const TASK_REVIVING: readonly string[] = ['wake-parent', 'give-task'];
18
+
19
+ /* The outbox of a host as ending uses it, whatever else the host owes in it. */
20
+ export interface EndChildrenOutbox {
21
+ list(): readonly AnyOutboxEntry[];
22
+ enqueue(projectId: string, target: string, work: EndChildrenWork): Promise<void>;
23
+ remove(id: string): Promise<void>;
24
+ }
25
+
26
+ export interface EndChildrenDeps {
27
+ lineage: Pick<AgentLineageStore, 'descendants' | 'projectOf' | 'markEnded'>;
28
+ tasks: Pick<TaskStore, 'cancelOpen' | 'dropWake'>;
29
+ outbox: EndChildrenOutbox;
30
+ /* The host's own kinds of work that would start or wake an agent again, beside the task kinds. */
31
+ reviving?: readonly string[];
32
+ /* What the thread of a chat an `end-children` entry ended and its cancelled task say about why. */
33
+ reason: string;
34
+ /* Ends whatever runs for the node and keeps what a person can still read. */
35
+ stop(nodeId: string, reason: string): Promise<void>;
36
+ /* The host's own clean-up once these nodes stopped; like every step, run again after a restart halfway. */
37
+ ended?: (nodeIds: readonly string[]) => Promise<void>;
38
+ now?: () => number;
39
+ }
40
+
41
+ export interface EndChildren {
42
+ /*
43
+ * Owes ending the agents a node opened, before the node itself is stopped or once it is deleted, and
44
+ * answers how many that is. Written to disk first, so a restart in between still ends them. A second
45
+ * call for a node whose entry is still owed adds nothing. `nodeIds` names them for a target that did
46
+ * not open them itself, such as a document whose chats go with it; its descendants when absent.
47
+ */
48
+ owe(target: string, nodeIds?: readonly string[]): Promise<number>;
49
+ handler(entry: EndChildrenEntry): Promise<void>;
50
+ /* Ends these nodes at once, given nearest first; the ones they opened are not among them unless named. */
51
+ end(nodeIds: readonly string[], reason: string): Promise<void>;
52
+ }
53
+
54
+ /*
55
+ * The ending of agents and the ones they opened. Marked first, so a restart halfway through finishes
56
+ * the same way; then the work that would bring one back goes, the open tasks are cancelled before any
57
+ * turn is aborted (so no parent is woken by an agent that was stopped), and the leaves stop before
58
+ * the nodes that opened them. Every step is safe to run twice.
59
+ */
60
+ export function endChildren(deps: EndChildrenDeps): EndChildren {
61
+ const { lineage, tasks, outbox } = deps;
62
+ const now = deps.now ?? Date.now;
63
+ const reviving = new Set([...TASK_REVIVING, ...(deps.reviving ?? [])]);
64
+
65
+ const end = async (nodeIds: readonly string[], reason: string): Promise<void> => {
66
+ await lineage.markEnded(nodeIds);
67
+ const ended = new Set(nodeIds);
68
+ for (const owed of outbox.list()) {
69
+ if (reviving.has(owed.kind) && ended.has(owed.target)) {
70
+ await outbox.remove(owed.id);
71
+ }
72
+ }
73
+ await tasks.cancelOpen(ended, reason, now());
74
+ // A stopped chat that gave tasks of its own is not woken about them either.
75
+ for (const nodeId of nodeIds) {
76
+ await tasks.dropWake(nodeId);
77
+ }
78
+ for (const nodeId of [...nodeIds].reverse()) {
79
+ await deps.stop(nodeId, reason);
80
+ }
81
+ await deps.ended?.(nodeIds);
82
+ };
83
+
84
+ return {
85
+ owe: async (target, named) => {
86
+ const nodeIds = named === undefined ? lineage.descendants(target) : [...named];
87
+ const projectId = nodeIds.map((id) => lineage.projectOf(id)).find((id) => id !== null) ?? null;
88
+ if (nodeIds.length === 0 || projectId === null) {
89
+ return 0;
90
+ }
91
+ const owed = outbox.list().some((entry) => entry.kind === 'end-children' && entry.target === target);
92
+ if (!owed) {
93
+ await outbox.enqueue(projectId, target, { kind: 'end-children', payload: { nodeIds } });
94
+ }
95
+ return nodeIds.length;
96
+ },
97
+ handler: (entry) => end([...new Set([...entry.payload.nodeIds, ...lineage.descendants(entry.target)])], deps.reason),
98
+ end
99
+ };
100
+ }
@@ -0,0 +1,52 @@
1
+ import type { Task } from '@adecore/agent-contracts';
2
+ import type { WakeChat } from '../chat/wake-chat.ts';
3
+ import type { OutboxOutcome } from '../outbox/outbox-worker.ts';
4
+ import type { TaskStore } from './task-store.ts';
5
+ import type { GiveTaskEntry } from './task-work.ts';
6
+
7
+ /* The note above the turn a task opens, so whoever reads the child sees who asked for it. */
8
+ export function taskNote(from: string): string {
9
+ return `Task from ${from}`;
10
+ }
11
+
12
+ export interface GiveTaskDeps {
13
+ tasks: Pick<TaskStore, 'get'>;
14
+ /* The chat as it is now, loaded from disk when nobody has it; null for a node with no thread. */
15
+ chat(chatId: string): Promise<WakeChat | null>;
16
+ /* What a person calls the node that gave the task, for the note above the turn. */
17
+ titleFor(nodeId: string): string | null;
18
+ /* Whether a project still places the node; one that is gone has its task cancelled by the prune. */
19
+ placed(nodeId: string): boolean;
20
+ /* What the child's CLI is sent: the assignment, and how the host wants to hear back. The prompt alone when absent. */
21
+ assignment?: (task: Task) => string;
22
+ }
23
+
24
+ /*
25
+ * Opens the turn that carries a task given to an agent already running. A turn in the way makes this
26
+ * `wait`, the same as a person's own message, so nothing the host owes steps on what the agent is
27
+ * doing. One agent holds one task at a time, so the task is never queued twice.
28
+ */
29
+ export function giveTaskHandler(deps: GiveTaskDeps) {
30
+ return async (entry: GiveTaskEntry): Promise<OutboxOutcome> => {
31
+ const task = deps.tasks.get(entry.payload.taskId);
32
+ // Settled or cancelled before the turn could open, or a node the project no longer places.
33
+ if (!task || task.status !== 'open' || !deps.placed(task.childId)) {
34
+ return;
35
+ }
36
+ const chat = await deps.chat(task.childId);
37
+ if (chat === null) {
38
+ throw new Error(`${task.childId} has no chat to open a turn in`);
39
+ }
40
+ // The turn names the task, so a restart between opening it and dropping this entry gives it once.
41
+ if (chat.items().some((item) => item.kind === 'turn' && (item.taskIds ?? []).includes(task.id))) {
42
+ await chat.persist?.();
43
+ return;
44
+ }
45
+ const from = deps.titleFor(task.parentId) ?? task.parentId;
46
+ const text = deps.assignment?.(task) ?? task.prompt;
47
+ if (!chat.wake({ text, label: task.title, note: taskNote(from), taskIds: [task.id] })) {
48
+ return 'wait';
49
+ }
50
+ await chat.persist?.();
51
+ };
52
+ }
@@ -0,0 +1,384 @@
1
+ import type { ChatBackgroundTask, ChatItem, ChatTurnItem, Task, TaskResult } from '@adecore/agent-contracts';
2
+ import { abortedByMachine, clipText } from '@adecore/agent-contracts';
3
+ import { BACKGROUND_COMMAND_LIMIT_MS, commandLabel, isBackgroundWork, runningInBackground, runsInBackground } from '../chat/background-work.ts';
4
+ import { errorText } from '../error-text.ts';
5
+ import type { AgentEvent } from '../events.ts';
6
+ import type { TaskStore } from './task-store.ts';
7
+
8
+ export interface TaskCoordinatorDeps {
9
+ tasks: TaskStore;
10
+ now(): number;
11
+ /* The thread of a chat, when this host has it loaded. */
12
+ chatItems(chatId: string): readonly ChatItem[] | null;
13
+ /* Whether a project still places the node; a child that went is cancelled by the prune, never failed. */
14
+ placed(nodeId: string): boolean;
15
+ loading?(chatId: string): boolean;
16
+ /* Whether the host still owes the turn that carries a task, which is a task nobody is working on yet. */
17
+ owedTurn(taskId: string): boolean;
18
+ /* Owes the parent a wake; only ever writes the outbox. */
19
+ oweWake(task: Task): Promise<void>;
20
+ /* Raises attention on a node whose task failed, for the person who has to look at why. */
21
+ alert(nodeId: string, title: string, body: string): void;
22
+ /* When the host takes up a turn that stopped on an overload again; null when it does not, which makes it an error like any other. */
23
+ retryAt?(chatId: string, turn: ChatTurnItem, items: readonly ChatItem[]): number | null;
24
+ /*
25
+ * What comes after the work a chat left running in the background ended: a turn its CLI opens about
26
+ * it (`reports`), nothing (`silent`), or nothing ever again, since its process went (`gone`).
27
+ */
28
+ afterBackgroundWork(chatId: string): 'reports' | 'silent' | 'gone';
29
+ /* The commands and monitors a chat's CLI runs in the background right now. */
30
+ commandsOf(chatId: string): readonly ChatBackgroundTask[];
31
+ /* The `background-limit` outbox entry of a task, which settles it once its commands ran too long. */
32
+ limit: {
33
+ owed(taskId: string): boolean;
34
+ owe(task: Task, commands: readonly string[], at: number): Promise<void>;
35
+ lapse(taskId: string): Promise<void>;
36
+ };
37
+ }
38
+
39
+ // A child's answer is its last word of the turn; the thinking and the tools before it are not the result.
40
+ function answerOf(items: readonly ChatItem[], turnId: string): string | null {
41
+ for (let i = items.length - 1; i >= 0; i--) {
42
+ const item = items[i]!;
43
+ if (item.kind === 'assistant' && item.turnId === turnId && !item.parentToolUseId && item.text.trim() !== '') {
44
+ return item.text.trim();
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+
50
+ function lastNote(items: readonly ChatItem[], turnId: string, levels: readonly string[]): string | null {
51
+ for (let i = items.length - 1; i >= 0; i--) {
52
+ const item = items[i]!;
53
+ if (item.kind === 'note' && item.turnId === turnId && levels.includes(item.level)) {
54
+ return item.text;
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function commandList(commands: readonly string[]): string {
61
+ return commands.map((command) => `\`${command}\``).join(', ');
62
+ }
63
+
64
+ function outlastedNote(commands: readonly string[]): string {
65
+ return `The task settled after ${BACKGROUND_COMMAND_LIMIT_MS / 60_000} minutes while this still ran in the background: ${commandList(commands)}.`;
66
+ }
67
+
68
+ function restartedNote(commands: readonly string[]): string {
69
+ return `The machine restarted while this still ran in the background: ${commandList(commands)}.`;
70
+ }
71
+
72
+ /* What a settled turn of a child makes of its task. */
73
+ export function resultOfTurn(turn: ChatTurnItem, items: readonly ChatItem[], at: number): { status: 'done' | 'failed'; result: TaskResult } {
74
+ if (turn.state === 'done') {
75
+ return { status: 'done', result: { text: answerOf(items, turn.id) ?? 'It ended its turn without an answer.', source: 'turn', at } };
76
+ }
77
+ if (turn.state === 'aborted') {
78
+ const text = abortedByMachine(turn, items)
79
+ ? (lastNote(items, turn.id, ['warning']) ?? 'The machine stopped it.')
80
+ : 'It was stopped before it finished.';
81
+ return { status: 'failed', result: { text, source: 'turn', at } };
82
+ }
83
+ return { status: 'failed', result: { text: lastNote(items, turn.id, ['error']) ?? 'Its turn ended in an error.', source: 'turn', at } };
84
+ }
85
+
86
+ /*
87
+ * Settles tasks from what children do and does nothing else: it writes the task store and owes wakes
88
+ * in the outbox, never starts a turn, since it runs inside the broadcast of the child it heard. A chat
89
+ * child settles when a turn ends with none of its own tasks still open or waiting to wake it and no work
90
+ * of its CLI's still running in the background, or once the commands it runs there outlast their limit;
91
+ * an agent that is not a chat, which the host tells it about, settles only on `done`, or fails when it
92
+ * ends without one.
93
+ */
94
+ export class TaskCoordinator {
95
+ private readonly deps: TaskCoordinatorDeps;
96
+ private stopped = false;
97
+ private readonly pending = new Set<Promise<unknown>>();
98
+
99
+ constructor(deps: TaskCoordinatorDeps) {
100
+ this.deps = deps;
101
+ }
102
+
103
+ /* The host is going down: what dies with it did not end its task. */
104
+ stop(): void {
105
+ this.stopped = true;
106
+ }
107
+
108
+ async settled(): Promise<void> {
109
+ while (this.pending.size > 0) {
110
+ await Promise.allSettled([...this.pending]);
111
+ }
112
+ await this.deps.tasks.settled();
113
+ }
114
+
115
+ /* For `chats.observe()`. */
116
+ chatEvent(event: AgentEvent): void {
117
+ if (this.stopped || event.event !== 'chat.event') {
118
+ return;
119
+ }
120
+ const { chatId, event: chatEvent } = event.payload;
121
+ if (this.deps.loading?.(chatId)) {
122
+ return;
123
+ }
124
+ if (chatEvent.type === 'item' && chatEvent.item.kind === 'turn' && chatEvent.item.state === 'running') {
125
+ this.turnStarted(chatId);
126
+ } else if (chatEvent.type === 'item' && chatEvent.item.kind === 'turn') {
127
+ this.turnEnded(chatId, chatEvent.item);
128
+ } else if (chatEvent.type === 'item' && isBackgroundWork(chatEvent.item) && !runsInBackground(chatEvent.item)) {
129
+ this.recheck(chatId);
130
+ } else if (chatEvent.type === 'info' && chatEvent.info.activeTurnId === null) {
131
+ // A child that waited on tasks of its own may be done now that its last wake turn went.
132
+ this.recheck(chatId);
133
+ }
134
+ }
135
+
136
+ /*
137
+ * An agent that is not a chat ended, so no turn of it will answer its task: it fails with `text`,
138
+ * which says how that kind of agent reports back.
139
+ */
140
+ agentEnded(childId: string, text: string): void {
141
+ if (this.stopped) {
142
+ return;
143
+ }
144
+ const task = this.deps.tasks.openFor(childId);
145
+ if (!task || !this.deps.placed(childId)) {
146
+ return;
147
+ }
148
+ this.settle(task, 'failed', { text, source: 'exit', at: this.deps.now() });
149
+ }
150
+
151
+ startFailed(childId: string, error: unknown): void {
152
+ this.failed(childId, `The agent could not be started: ${errorText(error)}`);
153
+ }
154
+
155
+ /* The turn a task given to a running agent was to open never opened, so nobody is working on it. */
156
+ giveFailed(childId: string, error: unknown): void {
157
+ this.failed(childId, `The task could not be given: ${errorText(error)}`);
158
+ }
159
+
160
+ /* `done` from the child itself, which wins over whatever else was about to settle it. */
161
+ async done(childId: string, text: string): Promise<Task | null> {
162
+ const task = this.deps.tasks.openFor(childId);
163
+ if (!task) {
164
+ return null;
165
+ }
166
+ return this.settleNow(task, 'done', { text, source: 'done', at: this.deps.now() });
167
+ }
168
+
169
+ /*
170
+ * The limit on the commands a child ran in the background passed, or a restart took them down with
171
+ * its CLI: its task settles on its last turn, saying what still ran, and fails after a restart as it
172
+ * does when the CLI goes within one run.
173
+ */
174
+ async outlasted(childId: string, commands: readonly string[], cause: 'limit' | 'restart' = 'limit'): Promise<Task | null> {
175
+ const task = this.deps.tasks.openFor(childId);
176
+ const items = this.deps.chatItems(childId);
177
+ if (this.stopped || !task || items === null || this.delegating(childId)) {
178
+ return null;
179
+ }
180
+ const last = items.findLast((item): item is ChatTurnItem => item.kind === 'turn');
181
+ if (!last || last.state === 'running' || !this.answers(items, last, task)) {
182
+ return null;
183
+ }
184
+ const { status, result } = resultOfTurn(last, items, this.deps.now());
185
+ if (cause === 'restart') {
186
+ return this.settleNow(task, 'failed', { ...result, text: `${result.text}\n\n${restartedNote(commands)}`, source: 'exit' });
187
+ }
188
+ return this.settleNow(task, status, { ...result, text: `${result.text}\n\n${outlastedNote(commands)}` });
189
+ }
190
+
191
+ /* Tasks of children a person removed; their parent may have been waiting on nothing else. */
192
+ cancelled(tasks: readonly Task[]): void {
193
+ for (const task of tasks) {
194
+ this.recheck(task.parentId);
195
+ }
196
+ }
197
+
198
+ /* An open task of this child that nothing can carry any more. */
199
+ private failed(childId: string, text: string): void {
200
+ const task = this.deps.tasks.openFor(childId);
201
+ if (!task || this.stopped) {
202
+ return;
203
+ }
204
+ this.settle(task, 'failed', { text, source: 'exit', at: this.deps.now() });
205
+ }
206
+
207
+ /*
208
+ * Whether this turn is the one the task's result comes from. A task given to an already-working agent
209
+ * gets a turn of its own that names it; the turn that was in its way answers the person who sent it,
210
+ * not the task, and nothing settles while that turn is still owed. A turn after it answers too, as the
211
+ * one the CLI opens about work that turn left running. A child opened with its task has no turn of that
212
+ * kind, so its first turn settles it, as it always did.
213
+ */
214
+ private answers(items: readonly ChatItem[], turn: ChatTurnItem, task: Task): boolean {
215
+ if ((turn.taskIds ?? []).includes(task.id)) {
216
+ return true;
217
+ }
218
+ if (this.deps.owedTurn(task.id)) {
219
+ return false;
220
+ }
221
+ const own = items.findIndex((item) => item.kind === 'turn' && (item.taskIds ?? []).includes(task.id));
222
+ return own === -1 ? task.requiresTaskTurn !== true && turn.createdAt >= task.createdAt : own < items.findIndex((item) => item.id === turn.id);
223
+ }
224
+
225
+ private turnEnded(chatId: string, turn: ChatTurnItem): void {
226
+ const task = this.deps.tasks.openFor(chatId);
227
+ const items = this.deps.chatItems(chatId);
228
+ if (!task || items === null || !this.answers(items, turn, task) || this.delegating(chatId)) {
229
+ return;
230
+ }
231
+ const paused = this.pauseFor(chatId, turn, items);
232
+ if (paused !== null) {
233
+ this.hold(task, paused);
234
+ return;
235
+ }
236
+ // A turn a person stopped or that failed ends the task as it always did, whatever still runs beside it.
237
+ if (turn.state === 'done' && this.heldInBackground(chatId, task, items)) {
238
+ this.track(
239
+ this.deps.tasks.holdBackground(task.id, {
240
+ turnId: turn.id,
241
+ itemIds: runningInBackground(items).map((item) => item.id),
242
+ commands: this.deps.commandsOf(chatId).map(commandLabel)
243
+ }),
244
+ `Saving the background work of task ${task.id} failed`
245
+ );
246
+ return;
247
+ }
248
+ if (task.background?.turnId === turn.id) {
249
+ const after = this.deps.afterBackgroundWork(chatId);
250
+ if (after === 'reports') {
251
+ return;
252
+ }
253
+ if (after === 'gone') {
254
+ if (task.background.itemIds.length === 0 && this.deps.limit.owed(task.id)) {
255
+ return;
256
+ }
257
+ const lost = task.background.itemIds.filter((id) => {
258
+ const item = items.find((item) => item.id === id);
259
+ return !item || (item.kind === 'subagent' ? item.status !== 'done' : item.kind !== 'tool' || item.state !== 'done');
260
+ });
261
+ if (lost.length > 0 || task.background.commands.length > 0) {
262
+ this.settle(task, 'failed', {
263
+ text: `The agent ended before its background work completed: ${[
264
+ ...lost.map((id) => {
265
+ const row = items.find((item) => item.id === id);
266
+ return row?.kind === 'subagent' ? row.description || row.toolUseId : row?.kind === 'tool' ? row.name : id;
267
+ }),
268
+ ...task.background.commands
269
+ ].join(', ')}. This work will not send a result.`,
270
+ source: 'exit',
271
+ at: this.deps.now()
272
+ });
273
+ } else {
274
+ const reports = task.background.itemIds.flatMap((id) => {
275
+ const row = items.find((item) => item.id === id);
276
+ const report = row?.kind === 'subagent' ? row.result : row?.kind === 'tool' ? row.output : null;
277
+ return report ? [report] : [];
278
+ });
279
+ this.settle(task, 'done', {
280
+ text: reports.join('\n\n') || resultOfTurn(turn, items, this.deps.now()).result.text,
281
+ source: 'turn',
282
+ at: this.deps.now()
283
+ });
284
+ }
285
+ return;
286
+ }
287
+ } else if (turn.state === 'done' && this.deps.limit.owed(task.id) && this.deps.afterBackgroundWork(chatId) === 'gone') {
288
+ // After a restart nothing here remembers the turn was held, but the limit it owes does, and that settles it.
289
+ return;
290
+ }
291
+ const { status, result } = resultOfTurn(turn, items, this.deps.now());
292
+ this.settle(task, status, result);
293
+ }
294
+
295
+ /*
296
+ * Whether work in the background keeps this task open past the turn. Commands and monitors alone hold
297
+ * it only until the limit, owed from the moment they are all that is left; a subagent or a workflow
298
+ * running beside them holds it with no limit at all.
299
+ */
300
+ private heldInBackground(chatId: string, task: Task, items: readonly ChatItem[]): boolean {
301
+ if (runningInBackground(items).length > 0) {
302
+ if (this.deps.limit.owed(task.id)) {
303
+ void this.deps.limit.lapse(task.id).catch((e: unknown) => console.error(`Dropping the limit of task ${task.id} failed:`, errorText(e)));
304
+ }
305
+ return true;
306
+ }
307
+ const commands = this.deps.commandsOf(chatId);
308
+ if (commands.length === 0) {
309
+ return false;
310
+ }
311
+ if (!this.deps.limit.owed(task.id)) {
312
+ void this.deps.limit
313
+ .owe(task, commands.map(commandLabel), this.deps.now() + BACKGROUND_COMMAND_LIMIT_MS)
314
+ .catch((e: unknown) => console.error(`Owing the limit of task ${task.id} failed:`, errorText(e)));
315
+ }
316
+ return true;
317
+ }
318
+
319
+ /*
320
+ * A limit is work waiting, not work that failed: the plan's limit always holds the task until the child
321
+ * finishes after it, an overload only while the host still owes the turn another try.
322
+ */
323
+ private pauseFor(chatId: string, turn: ChatTurnItem, items: readonly ChatItem[]): NonNullable<Task['paused']> | null {
324
+ const limit = turn.state === 'error' ? turn.limit : undefined;
325
+ if (limit?.kind === 'usage') {
326
+ return { kind: 'usage', ...(limit.resetsAt === undefined ? {} : { until: limit.resetsAt }) };
327
+ }
328
+ if (limit?.kind === 'overload') {
329
+ const at = this.deps.retryAt?.(chatId, turn, items) ?? null;
330
+ return at === null ? null : { kind: 'overload', until: at };
331
+ }
332
+ return null;
333
+ }
334
+
335
+ /* A child that took up work again is no longer waiting out a limit. */
336
+ private turnStarted(chatId: string): void {
337
+ const task = this.deps.tasks.openFor(chatId);
338
+ if (task?.paused !== undefined) {
339
+ this.hold(task, null);
340
+ }
341
+ }
342
+
343
+ private hold(task: Task, paused: NonNullable<Task['paused']> | null): void {
344
+ void this.deps.tasks.pause(task.id, paused).catch((e: unknown) => console.error(`Pausing task ${task.id} failed:`, errorText(e)));
345
+ }
346
+
347
+ private recheck(chatId: string): void {
348
+ const task = this.deps.tasks.openFor(chatId);
349
+ const items = this.deps.chatItems(chatId);
350
+ if (!task || items === null || this.delegating(chatId)) {
351
+ return;
352
+ }
353
+ const last = [...items].reverse().find((item): item is ChatTurnItem => item.kind === 'turn');
354
+ if (last && last.state !== 'running' && last.createdAt >= task.createdAt) {
355
+ this.turnEnded(chatId, last);
356
+ }
357
+ }
358
+
359
+ /* A child that gave tasks of its own is not finished while one is open or has yet to wake it. */
360
+ private delegating(chatId: string): boolean {
361
+ return this.deps.tasks.ofParent(chatId).some((task) => task.status === 'open' || task.wake === 'pending');
362
+ }
363
+
364
+ private settle(task: Task, status: 'done' | 'failed', result: TaskResult): void {
365
+ this.track(this.settleNow(task, status, result), `Settling task ${task.id} failed`);
366
+ }
367
+
368
+ private track(work: Promise<unknown>, message: string): void {
369
+ this.pending.add(work);
370
+ void work.catch((e: unknown) => console.error(`${message}:`, errorText(e))).finally(() => this.pending.delete(work));
371
+ }
372
+
373
+ private async settleNow(task: Task, status: 'done' | 'failed', result: TaskResult): Promise<Task | null> {
374
+ const settled = await this.deps.tasks.settle(task.id, status, result, this.deps.now());
375
+ if (!settled) {
376
+ return null;
377
+ }
378
+ await this.deps.oweWake(settled);
379
+ if (status === 'failed') {
380
+ this.deps.alert(settled.childId, `Task failed: ${settled.title}`, clipText(result.text, 500));
381
+ }
382
+ return settled;
383
+ }
384
+ }