@adecore/agents 0.0.1 → 0.17.0-beta.1

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 +33 -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,51 @@
1
+ import { type ChatApprovalItem, type ChatItem, type ChatQuestionItem, type Task } from '@adecore/agent-contracts';
2
+ import type { ChatCore } from '../chat/chat-core.ts';
3
+ import type { AgentEvent } from '../events.ts';
4
+ import type { OutboxOutcome } from '../outbox/outbox-worker.ts';
5
+ import type { DeliverWaitingEntry, TaskWork } from './task-work.ts';
6
+ export type WaitingRequest = ChatQuestionItem | ChatApprovalItem;
7
+ export declare function isPendingRequest(item: ChatItem | null | undefined): item is WaitingRequest;
8
+ export interface ChatRequests {
9
+ request(nodeId: string, requestId: string): WaitingRequest | null;
10
+ waiting(nodeId: string): WaitingRequest[];
11
+ answer(nodeId: string, requestId: string, answers: Record<string, string>): boolean;
12
+ }
13
+ export declare function chatRequests(chats: Pick<ChatCore, 'get' | 'answer'>): ChatRequests;
14
+ export declare function waitingNoteId(childId: string, requestId: string): string;
15
+ export declare const WAITING_GRACE_MS = 15000;
16
+ export interface WaitingObserverDeps {
17
+ openTask(childId: string): Task | undefined;
18
+ enqueue(projectId: string, target: string, work: TaskWork, notBefore: number): Promise<void>;
19
+ now(): number;
20
+ log?(message: string): void;
21
+ }
22
+ export declare class WaitingObserver {
23
+ private readonly deps;
24
+ private readonly owed;
25
+ private stopped;
26
+ constructor(deps: WaitingObserverDeps);
27
+ stop(): void;
28
+ chatEvent(event: AgentEvent): void;
29
+ }
30
+ export interface WaitingWords {
31
+ app: string;
32
+ cli: string;
33
+ }
34
+ export declare function waitingTexts(child: {
35
+ id: string;
36
+ title: string;
37
+ }, item: WaitingRequest, words: WaitingWords): {
38
+ note: string;
39
+ preamble: string;
40
+ };
41
+ export interface DeliverWaitingDeps {
42
+ request: ChatRequests['request'];
43
+ titleFor(nodeId: string): string | null;
44
+ deliver(chatId: string, delivery: {
45
+ noteId: string;
46
+ note: string;
47
+ preamble: string;
48
+ }): Promise<boolean>;
49
+ words: WaitingWords;
50
+ }
51
+ export declare function deliverWaitingHandler(deps: DeliverWaitingDeps): (entry: DeliverWaitingEntry) => Promise<OutboxOutcome>;
@@ -0,0 +1,143 @@
1
+ import { clipText } from '@adecore/agent-contracts';
2
+ import { errorText } from '../error-text.js';
3
+ export function isPendingRequest(item) {
4
+ return (item?.kind === 'question' && item.state === 'pending') || (item?.kind === 'approval' && item.decision === 'pending');
5
+ }
6
+ /* Read off the chats the host has loaded: a chat nobody loaded since a restart waits on nothing. */
7
+ export function chatRequests(chats) {
8
+ return {
9
+ request: (nodeId, requestId) => {
10
+ // The projector writes each under the kind it is.
11
+ const thread = chats.get(nodeId)?.thread;
12
+ const item = thread?.get(`question-${requestId}`) ?? thread?.get(`approval-${requestId}`);
13
+ return item?.kind === 'question' || item?.kind === 'approval' ? item : null;
14
+ },
15
+ waiting: (nodeId) => (chats.get(nodeId)?.thread.list() ?? []).filter(isPendingRequest),
16
+ answer: (nodeId, requestId, answers) => {
17
+ try {
18
+ chats.answer(nodeId, requestId, answers);
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ };
26
+ }
27
+ /* The id of the note one request of one child leaves in its parent, so delivering it twice leaves one. */
28
+ export function waitingNoteId(childId, requestId) {
29
+ return `waiting-${childId}-${requestId}`;
30
+ }
31
+ /*
32
+ * How long a request may wait before its parent hears of it. A person watching the child settles one
33
+ * within seconds (1.5 to 7 seconds in a run of Claude Code 2.1.282), and a note about that is stale
34
+ * before the parent reads it.
35
+ */
36
+ export const WAITING_GRACE_MS = 15_000;
37
+ /*
38
+ * Notes that a child with an open task asked something, and owes its parent one note in the outbox,
39
+ * due once the request waited out `WAITING_GRACE_MS`. It never writes the note itself, since it runs
40
+ * inside the broadcast of the child it heard, and it never wakes the parent: a finished task and a
41
+ * message are the only things that wake a chat.
42
+ */
43
+ export class WaitingObserver {
44
+ deps;
45
+ // A pending item is upserted again as it changes; one entry per request is enough, and the handler is idempotent after a restart.
46
+ owed = new Set();
47
+ stopped = false;
48
+ constructor(deps) {
49
+ this.deps = deps;
50
+ }
51
+ stop() {
52
+ this.stopped = true;
53
+ }
54
+ /* For `chats.observe()`. */
55
+ chatEvent(event) {
56
+ if (this.stopped || event.event !== 'chat.event') {
57
+ return;
58
+ }
59
+ const { chatId, event: chatEvent } = event.payload;
60
+ if (chatEvent.type !== 'item' || !isPendingRequest(chatEvent.item)) {
61
+ return;
62
+ }
63
+ const task = this.deps.openTask(chatId);
64
+ const key = `${chatId}\n${chatEvent.item.requestId}`;
65
+ if (task === undefined || this.owed.has(key)) {
66
+ return;
67
+ }
68
+ this.owed.add(key);
69
+ void this.deps
70
+ .enqueue(task.projectId, task.parentId, { kind: 'deliver-waiting', payload: { childId: chatId, requestId: chatEvent.item.requestId } }, this.deps.now() + WAITING_GRACE_MS)
71
+ .catch((e) => (this.deps.log ?? console.error)(`Owing a note about ${chatId} failed: ${errorText(e)}`));
72
+ }
73
+ }
74
+ function quoted(text) {
75
+ return JSON.stringify(text.replace(/\s+/g, ' ').trim());
76
+ }
77
+ /* The one line a note shows before "Show more": a question longer than that is read in the node. */
78
+ const NOTE_QUESTION_LENGTH = 200;
79
+ function quotedShort(text) {
80
+ const line = text.replace(/\s+/g, ' ').trim();
81
+ return JSON.stringify(line.length > NOTE_QUESTION_LENGTH ? `${clipText(line, NOTE_QUESTION_LENGTH - 1)}…` : line);
82
+ }
83
+ /* One question as the parent's CLI reads it, with the choices it may pick from. */
84
+ function questionLine(question) {
85
+ const choices = question.choices.map((choice) => (choice.description === '' ? choice.label : `${choice.label} (${choice.description})`));
86
+ const how = choices.length === 0
87
+ ? 'answer in your own words'
88
+ : `choices: ${choices.join(' | ')}; ${question.multiSelect ? 'pick one or more' : 'pick one'}, or answer in your own words`;
89
+ return `- question ${question.id}: ${quoted(question.question)} (${how})`;
90
+ }
91
+ /* The call that answers it, in the shape the verb takes for this many questions. */
92
+ function answerCall(cli, childId, item) {
93
+ if (item.questions.length === 1) {
94
+ const multi = item.questions[0].multiSelect ? ', several labels in one --answer separated by a comma' : '';
95
+ return `${cli} answer ${childId} ${item.requestId} --answer '<a choice label as written, or your own words>'${multi}`;
96
+ }
97
+ const shape = Object.fromEntries(item.questions.map((question) => [question.id, '...']));
98
+ return `${cli} answer ${childId} ${item.requestId} --answers '${JSON.stringify(shape)}', one answer per question id`;
99
+ }
100
+ /*
101
+ * What the parent's thread shows and what its CLI hears, from one request of one child. The note's
102
+ * first line is what a person sees at a glance; under it, what the CLI hears in front of its next turn.
103
+ */
104
+ export function waitingTexts(child, item, words) {
105
+ const who = `${child.title} (node ${child.id})`;
106
+ if (item.kind === 'approval') {
107
+ const what = item.description === null ? item.toolName : `${item.toolName}: ${item.description}`;
108
+ return {
109
+ note: `${who} waits for a person to approve ${what}`,
110
+ preamble: [
111
+ `${words.app}: ${who}, an agent you gave a task, waits for a person to approve ${what} (request ${item.requestId}). This did not wake you.`,
112
+ 'Only a person answers an approval; there is no verb for it. If it holds up your plan, tell the person.'
113
+ ].join('\n')
114
+ };
115
+ }
116
+ const asked = item.questions.length === 1 ? quotedShort(item.questions[0].question) : `${item.questions.length} questions`;
117
+ const how = [
118
+ ...item.questions.map(questionLine),
119
+ `A person can answer it in that node, and this chat with ${answerCall(words.cli, child.id, item)}. Whoever answers first wins; the verb refuses a question that was answered already.`
120
+ ];
121
+ return {
122
+ note: [`${who} waits for an answer to ${asked}`, '', ...how].join('\n'),
123
+ preamble: [
124
+ `${words.app}: ${who}, an agent you gave a task, asked a question and waits for an answer (request ${item.requestId}). This did not wake you.`,
125
+ ...how
126
+ ].join('\n')
127
+ };
128
+ }
129
+ /*
130
+ * Leaves the note in the parent, once. A request that no longer waits (answered, allowed, taken back
131
+ * by its CLI, or cancelled by a restart) is news about nothing, so it says nothing.
132
+ */
133
+ export function deliverWaitingHandler(deps) {
134
+ return async (entry) => {
135
+ const { childId, requestId } = entry.payload;
136
+ const item = deps.request(childId, requestId);
137
+ if (!isPendingRequest(item)) {
138
+ return;
139
+ }
140
+ const texts = waitingTexts({ id: childId, title: deps.titleFor(childId) ?? childId }, item, deps.words);
141
+ await deps.deliver(entry.target, { noteId: waitingNoteId(childId, requestId), ...texts });
142
+ };
143
+ }
@@ -0,0 +1,19 @@
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 { TaskWork, WakeParentEntry } from './task-work.ts';
6
+ export declare const RESULT_PREVIEW_BYTES: number;
7
+ export declare function wakePrompt(tasks: readonly Task[], teamsOut?: number, restOf?: (childId: string) => string): string;
8
+ export declare function wakeLabel(tasks: readonly Task[]): string;
9
+ export declare function wakeNote(tasks: readonly Task[]): string;
10
+ export interface WakeParentDeps {
11
+ tasks: Pick<TaskStore, 'pendingWake' | 'readyWake' | 'openBatches' | 'markWoken' | 'dropWake'>;
12
+ chat(chatId: string): Promise<WakeChat | null>;
13
+ restOf?: (childId: string) => string;
14
+ }
15
+ export declare function wakeParentHandler(deps: WakeParentDeps): (entry: WakeParentEntry) => Promise<OutboxOutcome>;
16
+ export interface OweWakeDeps {
17
+ enqueue(projectId: string, target: string, work: TaskWork): Promise<void>;
18
+ }
19
+ export declare function oweWake(deps: OweWakeDeps, task: Task): Promise<void>;
@@ -0,0 +1,109 @@
1
+ /* What the parent reads of one result; the rest stays with the child, whose id is right beside it. */
2
+ export const RESULT_PREVIEW_BYTES = 8 * 1024;
3
+ const encoder = new TextEncoder();
4
+ const decoder = new TextDecoder();
5
+ /* The first `bytes` of a text without cutting a character in half, and whether anything was left off. */
6
+ function cutAt(text, bytes) {
7
+ const encoded = encoder.encode(text);
8
+ if (encoded.length <= bytes) {
9
+ return { text, cut: false };
10
+ }
11
+ return { text: decoder.decode(encoded.slice(0, bytes)).replace(/\uFFFD$/, ''), cut: true };
12
+ }
13
+ /*
14
+ * The prompt a woken chat's CLI gets. Every task is named with its child's id, so the agent can go
15
+ * on with that node (read it, notify it) without asking which one it was. `restOf` says where the rest
16
+ * of a result that was cut is to be read.
17
+ */
18
+ export function wakePrompt(tasks, teamsOut = 0, restOf) {
19
+ const head = tasks.length === 1 ? 'A task you gave has settled. Its result:' : `${tasks.length} tasks you gave have settled. Their results:`;
20
+ const sections = tasks.map((task) => {
21
+ const { text, cut } = cutAt(task.result?.text ?? '', RESULT_PREVIEW_BYTES);
22
+ const lines = [`## ${task.title} (node ${task.childId}, task ${task.id}): ${task.status}`, '', text === '' ? '(no result text)' : text];
23
+ if (cut) {
24
+ const rest = restOf === undefined ? '' : ` ${restOf(task.childId)}`;
25
+ lines.push('', `[The result was cut at ${RESULT_PREVIEW_BYTES / 1024} KiB.${rest}]`);
26
+ }
27
+ return lines.join('\n');
28
+ });
29
+ const together = tasks.some((task) => task.batchId !== undefined)
30
+ ? ['The tasks of one team call come in together, once every one of them has settled.']
31
+ : [];
32
+ const out = teamsOut === 0
33
+ ? []
34
+ : [
35
+ `${teamsOut === 1 ? 'A team you gave is' : `${teamsOut} teams you gave are`} still out: you are woken with all of a team's results once its last task settles.`
36
+ ];
37
+ return [head, ...sections, ...together, ...out].join('\n\n');
38
+ }
39
+ /* The label of the turn a wake opens: what a person reads above it in the thread. */
40
+ export function wakeLabel(tasks) {
41
+ return tasks.map((task) => task.title).join(', ');
42
+ }
43
+ export function wakeNote(tasks) {
44
+ return `Woken by ${tasks.length} finished ${tasks.length === 1 ? 'task' : 'tasks'}`;
45
+ }
46
+ /*
47
+ * CLI acceptance lives in the saved turn before the task store consumes its result, so a restart
48
+ * between those writes can finish delivery without opening another turn.
49
+ */
50
+ export function wakeParentHandler(deps) {
51
+ return async (entry) => {
52
+ const parentId = entry.target;
53
+ const pending = deps.tasks.pendingWake(parentId);
54
+ if (pending.length === 0) {
55
+ return;
56
+ }
57
+ const held = () => (deps.tasks.readyWake(parentId).length < deps.tasks.pendingWake(parentId).length ? 'wait' : undefined);
58
+ // Nothing to say yet, so the chat is not loaded from disk for it.
59
+ if (deps.tasks.readyWake(parentId).length === 0) {
60
+ return held();
61
+ }
62
+ const chat = await deps.chat(parentId);
63
+ if (chat === null) {
64
+ await deps.tasks.dropWake(parentId);
65
+ return;
66
+ }
67
+ const items = chat.items();
68
+ const named = new Set(items.flatMap((item) => (item.kind === 'turn' && !item.deliveryPending ? (item.taskIds ?? []) : [])));
69
+ const already = deps.tasks.pendingWake(parentId).filter((task) => named.has(task.id));
70
+ if (already.length > 0) {
71
+ await chat.persist?.();
72
+ await deps.tasks.markWoken(already.map((task) => task.id));
73
+ }
74
+ const tasks = deps.tasks.readyWake(parentId).filter((task) => !named.has(task.id));
75
+ if (tasks.length === 0) {
76
+ return held();
77
+ }
78
+ // An unacknowledged attempt waits for a person's next turn, rather than retrying on every chat event.
79
+ const blocked = items.some((item, index) => item.kind === 'turn' &&
80
+ item.deliveryPending &&
81
+ item.taskIds?.some((id) => tasks.some((task) => task.id === id)) &&
82
+ (item.state === 'running' || !items.slice(index + 1).some((later) => later.kind === 'turn' && later.origin !== 'agent')));
83
+ if (blocked) {
84
+ return 'wait';
85
+ }
86
+ const teamsOut = deps.tasks.openBatches(parentId).length;
87
+ if (!chat.wake({
88
+ text: wakePrompt(tasks, teamsOut, deps.restOf),
89
+ label: wakeLabel(tasks),
90
+ note: wakeNote(tasks),
91
+ taskIds: tasks.map((task) => task.id)
92
+ })) {
93
+ return 'wait';
94
+ }
95
+ await chat.persist?.();
96
+ if (tasks.some((task) => {
97
+ const turn = chat.items().findLast((item) => item.kind === 'turn' && item.taskIds?.includes(task.id));
98
+ return turn?.kind === 'turn' && turn.deliveryPending;
99
+ })) {
100
+ return 'wait';
101
+ }
102
+ await deps.tasks.markWoken(tasks.map((task) => task.id));
103
+ return held();
104
+ };
105
+ }
106
+ /* A settled task owes its parent a wake; one entry per task, and the first to run takes every task there is. */
107
+ export function oweWake(deps, task) {
108
+ return deps.enqueue(task.projectId, task.parentId, { kind: 'wake-parent', payload: { taskId: task.id } });
109
+ }
@@ -0,0 +1,51 @@
1
+ import type { Task } from '@adecore/agent-contracts';
2
+ import type { ChatCore } from '../chat/chat-core.ts';
3
+ import type { OutboxHandlers } from '../outbox/outbox-worker.ts';
4
+ import { TaskCoordinator, type TaskCoordinatorDeps } from './task-coordinator.ts';
5
+ import type { TaskStore } from './task-store.ts';
6
+ import { type AnyOutboxEntry, type TaskOutbox, type TaskWork } from './task-work.ts';
7
+ import { WaitingObserver, type ChatRequests, type WaitingWords } from './waiting-child.ts';
8
+ export type TaskChats = Pick<ChatCore, 'get' | 'observe' | 'hasStored' | 'create' | 'answer' | 'deliverNote' | 'isLoading' | 'storedBackground' | 'save'>;
9
+ export interface TaskRecord {
10
+ projectId: string;
11
+ parentId: string;
12
+ childId: string;
13
+ title: string;
14
+ prompt: string;
15
+ }
16
+ export interface TaskVerbs {
17
+ open(record: TaskRecord & {
18
+ batchId?: string;
19
+ }): Promise<Task>;
20
+ give(record: TaskRecord): Promise<Task>;
21
+ chatState(nodeId: string): Promise<'none' | 'idle' | 'running'>;
22
+ done(childId: string, text: string): Promise<Task | null>;
23
+ involving(nodeId: string): Task[];
24
+ }
25
+ export interface TaskWords extends WaitingWords {
26
+ assignment?: (task: Task) => string;
27
+ restOf?: (childId: string) => string;
28
+ }
29
+ export interface TaskWiringDeps {
30
+ tasks: TaskStore;
31
+ chats: TaskChats;
32
+ outbox: TaskOutbox;
33
+ placed(nodeId: string): boolean;
34
+ titleFor(nodeId: string): string | null;
35
+ alert(nodeId: string, title: string, body: string): void;
36
+ changed?: (task: Task) => void;
37
+ retryAt?: TaskCoordinatorDeps['retryAt'];
38
+ words: TaskWords;
39
+ now?: () => number;
40
+ }
41
+ export interface TaskWiring {
42
+ coordinator: TaskCoordinator;
43
+ recover(): Promise<void>;
44
+ waiting: WaitingObserver;
45
+ verbs: TaskVerbs;
46
+ requests: ChatRequests;
47
+ handlers: OutboxHandlers<TaskWork>;
48
+ onParked(entry: AnyOutboxEntry, error: unknown): void;
49
+ prune(projectId: string, ids: ReadonlySet<string>): Promise<void>;
50
+ }
51
+ export declare function wireTasks(deps: TaskWiringDeps): TaskWiring;
@@ -0,0 +1,151 @@
1
+ import { reportsOnBackgroundWork } from '../chat/background-work.js';
2
+ import { chatOpener, loadChat } from '../chat/wake-chat.js';
3
+ import { errorText } from '../error-text.js';
4
+ import { backgroundLimitHandler, backgroundLimits } from './background-limit.js';
5
+ import { giveTaskHandler } from './give-task.js';
6
+ import { TaskCoordinator } from './task-coordinator.js';
7
+ import { isGiveTask } from './task-work.js';
8
+ import { chatRequests, deliverWaitingHandler, WaitingObserver } from './waiting-child.js';
9
+ import { oweWake, wakeParentHandler } from './wake-parent.js';
10
+ /*
11
+ * The tasks of a host in one place, so the host only hands in what it has: the coordinator that
12
+ * settles, the work the outbox runs, the notes in a parent's thread and what the verbs reach.
13
+ */
14
+ export function wireTasks(deps) {
15
+ const now = deps.now ?? Date.now;
16
+ const { chats, outbox } = deps;
17
+ const enqueue = (projectId, target, work, notBefore) => outbox.enqueue(projectId, target, work, notBefore);
18
+ const limit = backgroundLimits(outbox);
19
+ const coordinator = new TaskCoordinator({
20
+ tasks: deps.tasks,
21
+ now,
22
+ chatItems: (chatId) => chats.get(chatId)?.thread.list() ?? null,
23
+ placed: (nodeId) => deps.placed(nodeId),
24
+ loading: (chatId) => chats.isLoading(chatId),
25
+ // Until the turn that carries a task ran, no turn of the child is its answer.
26
+ owedTurn: (taskId) => outbox.list().some((entry) => isGiveTask(entry) && entry.payload.taskId === taskId),
27
+ oweWake: (task) => oweWake({ enqueue }, task),
28
+ alert: (nodeId, title, body) => deps.alert(nodeId, title, body),
29
+ ...(deps.retryAt ? { retryAt: deps.retryAt } : {}),
30
+ afterBackgroundWork: (chatId) => {
31
+ const chat = chats.get(chatId);
32
+ if (!chat?.running) {
33
+ return 'gone';
34
+ }
35
+ return reportsOnBackgroundWork(chat.info.provider) ? 'reports' : 'silent';
36
+ },
37
+ commandsOf: (chatId) => chats.get(chatId)?.info.background ?? [],
38
+ limit
39
+ });
40
+ deps.tasks.onChange((task) => {
41
+ deps.changed?.(task);
42
+ // A cancelled task owes no wake of its own, but it may be the last of a team whose held results now go out.
43
+ if (task.status === 'cancelled' && task.batchId !== undefined) {
44
+ outbox.wake(task.parentId);
45
+ }
46
+ // However a task ended, the limit on its child's background commands has nothing left to settle.
47
+ if (task.status !== 'open' && limit.owed(task.id)) {
48
+ void limit.lapse(task.id).catch((e) => console.error(`Dropping the limit of task ${task.id} failed:`, errorText(e)));
49
+ }
50
+ });
51
+ const requests = chatRequests(chats);
52
+ const waiting = new WaitingObserver({ openTask: (childId) => deps.tasks.openFor(childId), enqueue, now });
53
+ chats.observe((event) => {
54
+ coordinator.chatEvent(event);
55
+ waiting.chatEvent(event);
56
+ if (event.event === 'chat.event' && chats.get(event.payload.chatId)?.info.activeTurnId === null) {
57
+ outbox.wake(event.payload.chatId);
58
+ }
59
+ });
60
+ const chatFor = chatOpener({ chats, placed: deps.placed });
61
+ return {
62
+ coordinator,
63
+ recover: async () => {
64
+ for (const task of deps.tasks.all()) {
65
+ if (task.status === 'open' && task.requiresTaskTurn === true && deps.placed(task.childId)) {
66
+ if (!outbox.list().some((entry) => isGiveTask(entry) && entry.payload.taskId === task.id)) {
67
+ await enqueue(task.projectId, task.childId, { kind: 'give-task', payload: { taskId: task.id } });
68
+ }
69
+ }
70
+ const storedBackground = task.status === 'open' ? await chats.storedBackground(task.childId) : { turnId: null, items: [], commands: [] };
71
+ if (task.status === 'open' &&
72
+ deps.placed(task.childId) &&
73
+ (task.background !== undefined ||
74
+ task.requiresTaskTurn === true ||
75
+ storedBackground.items.length > 0 ||
76
+ storedBackground.commands.length > 0)) {
77
+ if (task.background === undefined &&
78
+ storedBackground.turnId !== null &&
79
+ (storedBackground.items.length > 0 || storedBackground.commands.length > 0)) {
80
+ await deps.tasks.holdBackground(task.id, {
81
+ turnId: storedBackground.turnId,
82
+ itemIds: storedBackground.items.map((item) => item.id),
83
+ commands: storedBackground.commands
84
+ });
85
+ }
86
+ const chat = await loadChat({ chats, placed: deps.placed }, task.childId);
87
+ if (chat && chat.info.activeTurnId === null) {
88
+ const last = chat.thread.list().findLast((item) => item.kind === 'turn');
89
+ if (last?.kind === 'turn') {
90
+ coordinator.chatEvent({ event: 'chat.event', payload: { chatId: task.childId, event: { type: 'item', item: last } } });
91
+ }
92
+ }
93
+ }
94
+ }
95
+ await coordinator.settled();
96
+ for (const task of deps.tasks.all()) {
97
+ if (task.status !== 'open' &&
98
+ task.wake === 'pending' &&
99
+ !outbox.list().some((entry) => entry.kind === 'wake-parent' && entry.payload.taskId === task.id)) {
100
+ await oweWake({ enqueue }, task);
101
+ }
102
+ }
103
+ },
104
+ waiting,
105
+ verbs: {
106
+ open: (record) => deps.tasks.open(record, now()),
107
+ give: async (record) => {
108
+ // The record first: the handler reads what the task asks from it, and a restart in between still owes the turn.
109
+ const task = await deps.tasks.open({ ...record, requiresTaskTurn: true }, now());
110
+ await enqueue(task.projectId, task.childId, { kind: 'give-task', payload: { taskId: task.id } });
111
+ return task;
112
+ },
113
+ chatState: async (nodeId) => {
114
+ const session = chats.get(nodeId);
115
+ if (session) {
116
+ return session.info.activeTurnId === null ? 'idle' : 'running';
117
+ }
118
+ // A chat nobody has loaded is idle: the turn opens on the thread the host reads back from disk.
119
+ return (await chats.hasStored(nodeId)) ? 'idle' : 'none';
120
+ },
121
+ done: (childId, text) => coordinator.done(childId, text),
122
+ involving: (nodeId) => deps.tasks.involving(nodeId)
123
+ },
124
+ requests,
125
+ handlers: {
126
+ 'wake-parent': wakeParentHandler({ tasks: deps.tasks, chat: chatFor, ...(deps.words.restOf ? { restOf: deps.words.restOf } : {}) }),
127
+ 'background-limit': backgroundLimitHandler({ tasks: deps.tasks, coordinator, chats, placed: deps.placed }),
128
+ 'give-task': giveTaskHandler({
129
+ tasks: deps.tasks,
130
+ chat: chatFor,
131
+ titleFor: deps.titleFor,
132
+ placed: deps.placed,
133
+ ...(deps.words.assignment ? { assignment: deps.words.assignment } : {})
134
+ }),
135
+ 'deliver-waiting': deliverWaitingHandler({
136
+ request: requests.request,
137
+ titleFor: deps.titleFor,
138
+ deliver: (chatId, delivery) => chats.deliverNote(chatId, delivery),
139
+ words: { app: deps.words.app, cli: deps.words.cli }
140
+ })
141
+ },
142
+ onParked: (entry, error) => {
143
+ if (isGiveTask(entry)) {
144
+ coordinator.giveFailed(entry.target, error);
145
+ }
146
+ },
147
+ prune: async (projectId, ids) => {
148
+ coordinator.cancelled(await deps.tasks.prune(projectId, ids, now()));
149
+ }
150
+ };
151
+ }
@@ -0,0 +1,2 @@
1
+ export declare function cleanTitle(raw: unknown): string | null;
2
+ export declare function readLines(path: string, from: number, size: number, chunkBytes: number, onLine: (line: string) => void): Promise<number>;
@@ -0,0 +1,55 @@
1
+ import { open } from 'node:fs/promises';
2
+ import { clipText, SUGGESTED_TITLE_LIMIT } from '@adecore/agent-contracts';
3
+ // oxlint-disable-next-line no-control-regex
4
+ const CONTROL = /[\u0000-\u001f\u007f]+/g;
5
+ /*
6
+ * A model wrote this, so it is text and nothing else: no control characters, no line breaks, one
7
+ * space between words and a length a header can hold.
8
+ */
9
+ export function cleanTitle(raw) {
10
+ if (typeof raw !== 'string') {
11
+ return null;
12
+ }
13
+ const text = raw.replace(CONTROL, ' ').replace(/\s+/g, ' ').trim();
14
+ if (text === '') {
15
+ return null;
16
+ }
17
+ return text.length <= SUGGESTED_TITLE_LIMIT ? text : `${clipText(text, SUGGESTED_TITLE_LIMIT - 1).trimEnd()}…`;
18
+ }
19
+ /*
20
+ * Hands every whole line between `from` and `size` of a file that only grows to `onLine`, and
21
+ * answers where the next read starts: just past the last whole line, so a line still being written
22
+ * is read once it is done. In pieces, so a file of hundreds of megabytes is never one buffer, and cut
23
+ * on bytes, since a piece may end halfway into a character.
24
+ */
25
+ export async function readLines(path, from, size, chunkBytes, onLine) {
26
+ const handle = await open(path, 'r');
27
+ let offset = from;
28
+ try {
29
+ let carry = Buffer.alloc(0);
30
+ let position = from;
31
+ while (position < size) {
32
+ const piece = Buffer.alloc(Math.min(chunkBytes, size - position));
33
+ const { bytesRead } = await handle.read(piece, 0, piece.length, position);
34
+ if (bytesRead === 0) {
35
+ break;
36
+ }
37
+ position += bytesRead;
38
+ const bytes = Buffer.concat([carry, piece.subarray(0, bytesRead)]);
39
+ const end = bytes.lastIndexOf(0x0a);
40
+ if (end < 0) {
41
+ carry = bytes;
42
+ continue;
43
+ }
44
+ for (const line of bytes.subarray(0, end).toString('utf8').split('\n')) {
45
+ onLine(line);
46
+ }
47
+ carry = bytes.subarray(end + 1);
48
+ offset = position - carry.length;
49
+ }
50
+ }
51
+ finally {
52
+ await handle.close();
53
+ }
54
+ return offset;
55
+ }
@@ -0,0 +1,15 @@
1
+ import { type ChatInfo, type ProviderAccounts, type UsageAccount, type UsageProvider } from '@adecore/agent-contracts';
2
+ import type { ProviderAccountsService } from '../providers/accounts/service.ts';
3
+ import type { LimitAccounts } from './limits/monitor.ts';
4
+ import { type UsageRootPath } from './roots.ts';
5
+ interface MachineAccount extends UsageAccount {
6
+ isDefault: boolean;
7
+ enabled: boolean;
8
+ signedIn: boolean;
9
+ }
10
+ export declare function machineAccounts(snapshot: ProviderAccounts, nameOf: (kind: UsageProvider) => string): MachineAccount[];
11
+ export declare function usageAccountsOf(service: ProviderAccountsService, nameOf: (kind: UsageProvider) => string): UsageAccount[];
12
+ export declare function usageRootsOf(service: ProviderAccountsService): UsageRootPath[];
13
+ export declare function limitAccountsOf(service: ProviderAccountsService, nameOf: (kind: UsageProvider) => string, baseEnv: Record<string, string | undefined>): LimitAccounts;
14
+ export declare function chatSessionAccounts(chats: ReadonlyArray<Pick<ChatInfo, 'provider' | 'agentSessionId' | 'account'>>): Map<string, string>;
15
+ export {};
@@ -0,0 +1,64 @@
1
+ import { USAGE_PROVIDERS } from '@adecore/agent-contracts';
2
+ import { definedEnv } from '../providers/accounts/launch.js';
3
+ import { accountRoots } from './roots.js';
4
+ function isUsageProvider(kind) {
5
+ return USAGE_PROVIDERS.includes(kind);
6
+ }
7
+ /* The accounts of the CLIs whose usage the host reads; a default account nobody named is called after its CLI. */
8
+ export function machineAccounts(snapshot, nameOf) {
9
+ return Object.entries(snapshot.accounts).flatMap(([id, account]) => {
10
+ if (!isUsageProvider(account.kind)) {
11
+ return [];
12
+ }
13
+ const isDefault = id === account.kind;
14
+ const state = snapshot.statuses.find((status) => status.id === id)?.state;
15
+ return [
16
+ {
17
+ id,
18
+ kind: account.kind,
19
+ label: account.label ?? (isDefault ? nameOf(account.kind) : id),
20
+ ...(account.color === undefined ? {} : { color: account.color }),
21
+ isDefault,
22
+ enabled: account.enabled !== false,
23
+ signedIn: state === 'ready'
24
+ }
25
+ ];
26
+ });
27
+ }
28
+ function usageAccount({ id, kind, label, color }) {
29
+ return { id, kind, label, ...(color === undefined ? {} : { color }) };
30
+ }
31
+ /* What the usage page may filter by: every account, whether it is on or signed in or not, since what it spent still counts. */
32
+ export function usageAccountsOf(service, nameOf) {
33
+ return machineAccounts(service.snapshot(), nameOf).map(usageAccount);
34
+ }
35
+ /* The transcripts of every account the machine has. */
36
+ export function usageRootsOf(service) {
37
+ return accountRoots(Object.entries(service.snapshot().accounts).flatMap(([id, { kind }]) => {
38
+ if (!isUsageProvider(kind)) {
39
+ return [];
40
+ }
41
+ const folder = service.transcriptFolder(kind, id);
42
+ return folder === null || folder === '' ? [] : [{ id, kind, folder }];
43
+ }));
44
+ }
45
+ /* The plans the limits monitor reads: the default account of every CLI, and every other one that is on and signed in. */
46
+ export function limitAccountsOf(service, nameOf, baseEnv) {
47
+ return {
48
+ list: () => machineAccounts(service.snapshot(), nameOf)
49
+ .filter((account) => account.isDefault || (account.enabled && account.signedIn))
50
+ .map((account) => ({ ...usageAccount(account), isDefault: account.isDefault })),
51
+ envFor: (kind, id) => definedEnv(service.envFor(kind, id, baseEnv)),
52
+ lastUsedAt: (id) => service.lastLaunchAt(id)
53
+ };
54
+ }
55
+ /* The account every CLI session a chat here ran was under, by `<provider>\0<sessionId>`, for the transcripts accounts share. */
56
+ export function chatSessionAccounts(chats) {
57
+ const known = new Map();
58
+ for (const chat of chats) {
59
+ if (chat.agentSessionId !== null) {
60
+ known.set(`${chat.provider}\0${chat.agentSessionId}`, chat.account ?? chat.provider);
61
+ }
62
+ }
63
+ return known;
64
+ }