@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,231 @@
1
+ import type { Task } from '@adecore/agent-contracts';
2
+ import { reportsOnBackgroundWork } from '../chat/background-work.ts';
3
+ import type { ChatCore } from '../chat/chat-core.ts';
4
+ import { chatOpener, loadChat } from '../chat/wake-chat.ts';
5
+ import { errorText } from '../error-text.ts';
6
+ import type { OutboxHandlers } from '../outbox/outbox-worker.ts';
7
+ import { backgroundLimitHandler, backgroundLimits } from './background-limit.ts';
8
+ import { giveTaskHandler } from './give-task.ts';
9
+ import { TaskCoordinator, type TaskCoordinatorDeps } from './task-coordinator.ts';
10
+ import type { TaskStore } from './task-store.ts';
11
+ import { isGiveTask, type AnyOutboxEntry, type TaskOutbox, type TaskWork } from './task-work.ts';
12
+ import { chatRequests, deliverWaitingHandler, WaitingObserver, type ChatRequests, type WaitingWords } from './waiting-child.ts';
13
+ import { oweWake, wakeParentHandler } from './wake-parent.ts';
14
+
15
+ /* What tasks need of the chats of a host; a `ChatCore` has all of it. */
16
+ export type TaskChats = Pick<ChatCore, 'get' | 'observe' | 'hasStored' | 'create' | 'answer' | 'deliverNote' | 'isLoading' | 'storedBackground' | 'save'>;
17
+
18
+ export interface TaskRecord {
19
+ projectId: string;
20
+ parentId: string;
21
+ childId: string;
22
+ title: string;
23
+ prompt: string;
24
+ }
25
+
26
+ /* What the verbs of a context CLI reach of the tasks. */
27
+ export interface TaskVerbs {
28
+ /* `batchId` ties the tasks of one `team --task` call together, which wake the parent as one. */
29
+ open(record: TaskRecord & { batchId?: string }): Promise<Task>;
30
+ /* Opens a task for an agent that is already running, and owes the turn that carries it. */
31
+ give(record: TaskRecord): Promise<Task>;
32
+ /* Whether the host has a chat for this node at all, and whether a turn of it is in the way. */
33
+ chatState(nodeId: string): Promise<'none' | 'idle' | 'running'>;
34
+ /* Ends the open task of this child with the result it reported; null when it has none open. */
35
+ done(childId: string, text: string): Promise<Task | null>;
36
+ /* Every task the node gave or was given, oldest first. */
37
+ involving(nodeId: string): Task[];
38
+ }
39
+
40
+ export interface TaskWords extends WaitingWords {
41
+ /* What a running agent's CLI is sent with a task given to it; the prompt alone when absent. */
42
+ assignment?: (task: Task) => string;
43
+ /* Where the rest of a result that was cut is read, after the line in the wake that says so. */
44
+ restOf?: (childId: string) => string;
45
+ }
46
+
47
+ export interface TaskWiringDeps {
48
+ tasks: TaskStore;
49
+ chats: TaskChats;
50
+ outbox: TaskOutbox;
51
+ /* Whether a project still places the node; a child that went is cancelled by `prune`, never failed. */
52
+ placed(nodeId: string): boolean;
53
+ /* What a person calls a node, for the notes a task leaves. */
54
+ titleFor(nodeId: string): string | null;
55
+ /* Raises attention on a node whose task failed, for the person who has to look at why. */
56
+ alert(nodeId: string, title: string, body: string): void;
57
+ /* Told of every task written, inside the broadcast of the child that settled it, so it only notes. */
58
+ changed?: (task: Task) => void;
59
+ /* When the host tries a turn that stopped on an overload again; null when it does not. */
60
+ retryAt?: TaskCoordinatorDeps['retryAt'];
61
+ words: TaskWords;
62
+ now?: () => number;
63
+ }
64
+
65
+ export interface TaskWiring {
66
+ coordinator: TaskCoordinator;
67
+ recover(): Promise<void>;
68
+ waiting: WaitingObserver;
69
+ verbs: TaskVerbs;
70
+ requests: ChatRequests;
71
+ handlers: OutboxHandlers<TaskWork>;
72
+ /* For the worker's `onParked`: a task whose turn never opened is one nobody is working on, and fails. */
73
+ onParked(entry: AnyOutboxEntry, error: unknown): void;
74
+ /* For a project that no longer places these ids: cancel what went, then look at the parents again. */
75
+ prune(projectId: string, ids: ReadonlySet<string>): Promise<void>;
76
+ }
77
+
78
+ /*
79
+ * The tasks of a host in one place, so the host only hands in what it has: the coordinator that
80
+ * settles, the work the outbox runs, the notes in a parent's thread and what the verbs reach.
81
+ */
82
+ export function wireTasks(deps: TaskWiringDeps): TaskWiring {
83
+ const now = deps.now ?? Date.now;
84
+ const { chats, outbox } = deps;
85
+ const enqueue = (projectId: string, target: string, work: TaskWork, notBefore?: number): Promise<void> =>
86
+ outbox.enqueue(projectId, target, work, notBefore);
87
+ const limit = backgroundLimits(outbox);
88
+ const coordinator = new TaskCoordinator({
89
+ tasks: deps.tasks,
90
+ now,
91
+ chatItems: (chatId) => chats.get(chatId)?.thread.list() ?? null,
92
+ placed: (nodeId) => deps.placed(nodeId),
93
+ loading: (chatId) => chats.isLoading(chatId),
94
+ // Until the turn that carries a task ran, no turn of the child is its answer.
95
+ owedTurn: (taskId) => outbox.list().some((entry) => isGiveTask(entry) && entry.payload.taskId === taskId),
96
+ oweWake: (task) => oweWake({ enqueue }, task),
97
+ alert: (nodeId, title, body) => deps.alert(nodeId, title, body),
98
+ ...(deps.retryAt ? { retryAt: deps.retryAt } : {}),
99
+ afterBackgroundWork: (chatId) => {
100
+ const chat = chats.get(chatId);
101
+ if (!chat?.running) {
102
+ return 'gone';
103
+ }
104
+ return reportsOnBackgroundWork(chat.info.provider) ? 'reports' : 'silent';
105
+ },
106
+ commandsOf: (chatId) => chats.get(chatId)?.info.background ?? [],
107
+ limit
108
+ });
109
+
110
+ deps.tasks.onChange((task: Task) => {
111
+ deps.changed?.(task);
112
+ // A cancelled task owes no wake of its own, but it may be the last of a team whose held results now go out.
113
+ if (task.status === 'cancelled' && task.batchId !== undefined) {
114
+ outbox.wake(task.parentId);
115
+ }
116
+ // However a task ended, the limit on its child's background commands has nothing left to settle.
117
+ if (task.status !== 'open' && limit.owed(task.id)) {
118
+ void limit.lapse(task.id).catch((e: unknown) => console.error(`Dropping the limit of task ${task.id} failed:`, errorText(e)));
119
+ }
120
+ });
121
+
122
+ const requests = chatRequests(chats);
123
+ const waiting = new WaitingObserver({ openTask: (childId) => deps.tasks.openFor(childId), enqueue, now });
124
+
125
+ chats.observe((event) => {
126
+ coordinator.chatEvent(event);
127
+ waiting.chatEvent(event);
128
+ if (event.event === 'chat.event' && chats.get(event.payload.chatId)?.info.activeTurnId === null) {
129
+ outbox.wake(event.payload.chatId);
130
+ }
131
+ });
132
+
133
+ const chatFor = chatOpener({ chats, placed: deps.placed });
134
+
135
+ return {
136
+ coordinator,
137
+ recover: async () => {
138
+ for (const task of deps.tasks.all()) {
139
+ if (task.status === 'open' && task.requiresTaskTurn === true && deps.placed(task.childId)) {
140
+ if (!outbox.list().some((entry) => isGiveTask(entry) && entry.payload.taskId === task.id)) {
141
+ await enqueue(task.projectId, task.childId, { kind: 'give-task', payload: { taskId: task.id } });
142
+ }
143
+ }
144
+ const storedBackground = task.status === 'open' ? await chats.storedBackground(task.childId) : { turnId: null, items: [], commands: [] };
145
+ if (
146
+ task.status === 'open' &&
147
+ deps.placed(task.childId) &&
148
+ (task.background !== undefined ||
149
+ task.requiresTaskTurn === true ||
150
+ storedBackground.items.length > 0 ||
151
+ storedBackground.commands.length > 0)
152
+ ) {
153
+ if (
154
+ task.background === undefined &&
155
+ storedBackground.turnId !== null &&
156
+ (storedBackground.items.length > 0 || storedBackground.commands.length > 0)
157
+ ) {
158
+ await deps.tasks.holdBackground(task.id, {
159
+ turnId: storedBackground.turnId,
160
+ itemIds: storedBackground.items.map((item) => item.id),
161
+ commands: storedBackground.commands
162
+ });
163
+ }
164
+ const chat = await loadChat({ chats, placed: deps.placed }, task.childId);
165
+ if (chat && chat.info.activeTurnId === null) {
166
+ const last = chat.thread.list().findLast((item) => item.kind === 'turn');
167
+ if (last?.kind === 'turn') {
168
+ coordinator.chatEvent({ event: 'chat.event', payload: { chatId: task.childId, event: { type: 'item', item: last } } });
169
+ }
170
+ }
171
+ }
172
+ }
173
+ await coordinator.settled();
174
+ for (const task of deps.tasks.all()) {
175
+ if (
176
+ task.status !== 'open' &&
177
+ task.wake === 'pending' &&
178
+ !outbox.list().some((entry) => entry.kind === 'wake-parent' && (entry.payload as { taskId?: string }).taskId === task.id)
179
+ ) {
180
+ await oweWake({ enqueue }, task);
181
+ }
182
+ }
183
+ },
184
+ waiting,
185
+ verbs: {
186
+ open: (record) => deps.tasks.open(record, now()),
187
+ give: async (record) => {
188
+ // The record first: the handler reads what the task asks from it, and a restart in between still owes the turn.
189
+ const task = await deps.tasks.open({ ...record, requiresTaskTurn: true }, now());
190
+ await enqueue(task.projectId, task.childId, { kind: 'give-task', payload: { taskId: task.id } });
191
+ return task;
192
+ },
193
+ chatState: async (nodeId) => {
194
+ const session = chats.get(nodeId);
195
+ if (session) {
196
+ return session.info.activeTurnId === null ? 'idle' : 'running';
197
+ }
198
+ // A chat nobody has loaded is idle: the turn opens on the thread the host reads back from disk.
199
+ return (await chats.hasStored(nodeId)) ? 'idle' : 'none';
200
+ },
201
+ done: (childId, text) => coordinator.done(childId, text),
202
+ involving: (nodeId) => deps.tasks.involving(nodeId)
203
+ },
204
+ requests,
205
+ handlers: {
206
+ 'wake-parent': wakeParentHandler({ tasks: deps.tasks, chat: chatFor, ...(deps.words.restOf ? { restOf: deps.words.restOf } : {}) }),
207
+ 'background-limit': backgroundLimitHandler({ tasks: deps.tasks, coordinator, chats, placed: deps.placed }),
208
+ 'give-task': giveTaskHandler({
209
+ tasks: deps.tasks,
210
+ chat: chatFor,
211
+ titleFor: deps.titleFor,
212
+ placed: deps.placed,
213
+ ...(deps.words.assignment ? { assignment: deps.words.assignment } : {})
214
+ }),
215
+ 'deliver-waiting': deliverWaitingHandler({
216
+ request: requests.request,
217
+ titleFor: deps.titleFor,
218
+ deliver: (chatId, delivery) => chats.deliverNote(chatId, delivery),
219
+ words: { app: deps.words.app, cli: deps.words.cli }
220
+ })
221
+ },
222
+ onParked: (entry, error) => {
223
+ if (isGiveTask(entry)) {
224
+ coordinator.giveFailed(entry.target, error);
225
+ }
226
+ },
227
+ prune: async (projectId, ids) => {
228
+ coordinator.cancelled(await deps.tasks.prune(projectId, ids, now()));
229
+ }
230
+ };
231
+ }
@@ -0,0 +1,57 @@
1
+ import { open } from 'node:fs/promises';
2
+ import { clipText, SUGGESTED_TITLE_LIMIT } from '@adecore/agent-contracts';
3
+
4
+ // oxlint-disable-next-line no-control-regex
5
+ const CONTROL = /[\u0000-\u001f\u007f]+/g;
6
+
7
+ /*
8
+ * A model wrote this, so it is text and nothing else: no control characters, no line breaks, one
9
+ * space between words and a length a header can hold.
10
+ */
11
+ export function cleanTitle(raw: unknown): string | null {
12
+ if (typeof raw !== 'string') {
13
+ return null;
14
+ }
15
+ const text = raw.replace(CONTROL, ' ').replace(/\s+/g, ' ').trim();
16
+ if (text === '') {
17
+ return null;
18
+ }
19
+ return text.length <= SUGGESTED_TITLE_LIMIT ? text : `${clipText(text, SUGGESTED_TITLE_LIMIT - 1).trimEnd()}…`;
20
+ }
21
+
22
+ /*
23
+ * Hands every whole line between `from` and `size` of a file that only grows to `onLine`, and
24
+ * answers where the next read starts: just past the last whole line, so a line still being written
25
+ * is read once it is done. In pieces, so a file of hundreds of megabytes is never one buffer, and cut
26
+ * on bytes, since a piece may end halfway into a character.
27
+ */
28
+ export async function readLines(path: string, from: number, size: number, chunkBytes: number, onLine: (line: string) => void): Promise<number> {
29
+ const handle = await open(path, 'r');
30
+ let offset = from;
31
+ try {
32
+ let carry = Buffer.alloc(0);
33
+ let position = from;
34
+ while (position < size) {
35
+ const piece = Buffer.alloc(Math.min(chunkBytes, size - position));
36
+ const { bytesRead } = await handle.read(piece, 0, piece.length, position);
37
+ if (bytesRead === 0) {
38
+ break;
39
+ }
40
+ position += bytesRead;
41
+ const bytes = Buffer.concat([carry, piece.subarray(0, bytesRead)]);
42
+ const end = bytes.lastIndexOf(0x0a);
43
+ if (end < 0) {
44
+ carry = bytes;
45
+ continue;
46
+ }
47
+ for (const line of bytes.subarray(0, end).toString('utf8').split('\n')) {
48
+ onLine(line);
49
+ }
50
+ carry = bytes.subarray(end + 1);
51
+ offset = position - carry.length;
52
+ }
53
+ } finally {
54
+ await handle.close();
55
+ }
56
+ return offset;
57
+ }
@@ -0,0 +1,86 @@
1
+ import { USAGE_PROVIDERS, type ChatInfo, type ProviderAccounts, type UsageAccount, type UsageProvider } from '@adecore/agent-contracts';
2
+ import { definedEnv } from '../providers/accounts/launch.ts';
3
+ import type { ProviderAccountsService } from '../providers/accounts/service.ts';
4
+ import type { LimitAccount, LimitAccounts } from './limits/monitor.ts';
5
+ import { accountRoots, type UsageRootPath } from './roots.ts';
6
+
7
+ function isUsageProvider(kind: string): kind is UsageProvider {
8
+ return (USAGE_PROVIDERS as readonly string[]).includes(kind);
9
+ }
10
+
11
+ interface MachineAccount extends UsageAccount {
12
+ isDefault: boolean;
13
+ enabled: boolean;
14
+ signedIn: boolean;
15
+ }
16
+
17
+ /* The accounts of the CLIs whose usage the host reads; a default account nobody named is called after its CLI. */
18
+ export function machineAccounts(snapshot: ProviderAccounts, nameOf: (kind: UsageProvider) => string): MachineAccount[] {
19
+ return Object.entries(snapshot.accounts).flatMap(([id, account]) => {
20
+ if (!isUsageProvider(account.kind)) {
21
+ return [];
22
+ }
23
+ const isDefault = id === account.kind;
24
+ const state = snapshot.statuses.find((status) => status.id === id)?.state;
25
+ return [
26
+ {
27
+ id,
28
+ kind: account.kind,
29
+ label: account.label ?? (isDefault ? nameOf(account.kind) : id),
30
+ ...(account.color === undefined ? {} : { color: account.color }),
31
+ isDefault,
32
+ enabled: account.enabled !== false,
33
+ signedIn: state === 'ready'
34
+ }
35
+ ];
36
+ });
37
+ }
38
+
39
+ function usageAccount({ id, kind, label, color }: MachineAccount): UsageAccount {
40
+ return { id, kind, label, ...(color === undefined ? {} : { color }) };
41
+ }
42
+
43
+ /* What the usage page may filter by: every account, whether it is on or signed in or not, since what it spent still counts. */
44
+ export function usageAccountsOf(service: ProviderAccountsService, nameOf: (kind: UsageProvider) => string): UsageAccount[] {
45
+ return machineAccounts(service.snapshot(), nameOf).map(usageAccount);
46
+ }
47
+
48
+ /* The transcripts of every account the machine has. */
49
+ export function usageRootsOf(service: ProviderAccountsService): UsageRootPath[] {
50
+ return accountRoots(
51
+ Object.entries(service.snapshot().accounts).flatMap(([id, { kind }]) => {
52
+ if (!isUsageProvider(kind)) {
53
+ return [];
54
+ }
55
+ const folder = service.transcriptFolder(kind, id);
56
+ return folder === null || folder === '' ? [] : [{ id, kind, folder }];
57
+ })
58
+ );
59
+ }
60
+
61
+ /* The plans the limits monitor reads: the default account of every CLI, and every other one that is on and signed in. */
62
+ export function limitAccountsOf(
63
+ service: ProviderAccountsService,
64
+ nameOf: (kind: UsageProvider) => string,
65
+ baseEnv: Record<string, string | undefined>
66
+ ): LimitAccounts {
67
+ return {
68
+ list: () =>
69
+ machineAccounts(service.snapshot(), nameOf)
70
+ .filter((account) => account.isDefault || (account.enabled && account.signedIn))
71
+ .map((account): LimitAccount => ({ ...usageAccount(account), isDefault: account.isDefault })),
72
+ envFor: (kind, id) => definedEnv(service.envFor(kind, id, baseEnv)),
73
+ lastUsedAt: (id) => service.lastLaunchAt(id)
74
+ };
75
+ }
76
+
77
+ /* The account every CLI session a chat here ran was under, by `<provider>\0<sessionId>`, for the transcripts accounts share. */
78
+ export function chatSessionAccounts(chats: ReadonlyArray<Pick<ChatInfo, 'provider' | 'agentSessionId' | 'account'>>): Map<string, string> {
79
+ const known = new Map<string, string>();
80
+ for (const chat of chats) {
81
+ if (chat.agentSessionId !== null) {
82
+ known.set(`${chat.provider}\0${chat.agentSessionId}`, chat.account ?? chat.provider);
83
+ }
84
+ }
85
+ return known;
86
+ }
@@ -0,0 +1,220 @@
1
+ import { addTotals, EMPTY_TOTALS, totalTokensOf } from '@adecore/agent-contracts';
2
+ import type {
3
+ UsageBucket,
4
+ UsageModel,
5
+ UsagePriceBasis,
6
+ UsageProject,
7
+ UsageProvider,
8
+ UsageResolution,
9
+ UsageSummaryPayload,
10
+ UsageTotals
11
+ } from '@adecore/agent-contracts';
12
+ import { cacheSavingsOf, costOf, type PriceBook } from './pricing.ts';
13
+ import { ProjectResolver, type KnownProject } from './projects.ts';
14
+ import type { UsageRecord } from './record.ts';
15
+
16
+ export interface Aggregation {
17
+ buckets: UsageBucket[];
18
+ models: UsageModel[];
19
+ projects: UsageProject[];
20
+ sessions: number;
21
+ }
22
+
23
+ /*
24
+ * The calendar day a moment falls in, as the viewer's clock reads it. The host may stand in
25
+ * another zone than the person looking, so the zone travels with the request and every boundary
26
+ * here is local wall clock, never UTC.
27
+ */
28
+ function slotFormatter(timeZone: string, resolution: UsageResolution): Intl.DateTimeFormat {
29
+ const options: Intl.DateTimeFormatOptions =
30
+ resolution === 'hour'
31
+ ? { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', hourCycle: 'h23' }
32
+ : { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' };
33
+ try {
34
+ return new Intl.DateTimeFormat('en-CA', options);
35
+ } catch {
36
+ // A zone this machine does not know degrades to UTC rather than failing the whole request.
37
+ return new Intl.DateTimeFormat('en-CA', { ...options, timeZone: 'UTC' });
38
+ }
39
+ }
40
+
41
+ /* `2026-09-10` for a day, `2026-09-10T14` for an hour: the day is always the first ten characters. */
42
+ function slotOf(format: Intl.DateTimeFormat, timestampMs: number, resolution: UsageResolution): string {
43
+ const parts = format.formatToParts(new Date(timestampMs));
44
+ const at = (type: Intl.DateTimeFormatPartTypes): string => parts.find((part) => part.type === type)?.value ?? '';
45
+ const day = `${at('year')}-${at('month')}-${at('day')}`;
46
+ return resolution === 'hour' ? `${day}T${at('hour')}` : day;
47
+ }
48
+
49
+ function emptyTotals(): UsageTotals {
50
+ return { ...EMPTY_TOTALS };
51
+ }
52
+
53
+ interface BucketAccumulator {
54
+ slot: string;
55
+ provider: UsageProvider;
56
+ model: string;
57
+ account: string | null;
58
+ totals: UsageTotals;
59
+ costUsd: number;
60
+ priced: boolean;
61
+ cacheSavingsUsd: number;
62
+ sessions: Set<string>;
63
+ }
64
+
65
+ interface ModelAccumulator {
66
+ provider: UsageProvider;
67
+ model: string;
68
+ account: string | null;
69
+ totals: UsageTotals;
70
+ costUsd: number;
71
+ priced: boolean;
72
+ }
73
+
74
+ interface ProjectAccumulator {
75
+ folder: string;
76
+ name: string;
77
+ projectId: string | null;
78
+ byProvider: Map<UsageProvider, { costUsd: number; tokens: number }>;
79
+ totals: UsageTotals;
80
+ costUsd: number;
81
+ }
82
+
83
+ /* The account a record was made under, the default one under its CLI's kind. */
84
+ export function accountOfRecord(record: UsageRecord): string {
85
+ return record.account ?? record.provider;
86
+ }
87
+
88
+ /*
89
+ * Every record of the period folded three ways at once: per slot for the chart, per model for the
90
+ * breakdown and its price basis, and per folder for the projects. The three cannot be derived from
91
+ * one another without a bucket per slot times model times project, which is a table nobody reads.
92
+ */
93
+ export async function aggregate(
94
+ records: readonly UsageRecord[],
95
+ payload: UsageSummaryPayload,
96
+ prices: PriceBook,
97
+ known: readonly KnownProject[]
98
+ ): Promise<Aggregation> {
99
+ const format = slotFormatter(payload.timeZone, payload.resolution);
100
+ const buckets = new Map<string, BucketAccumulator>();
101
+ const models = new Map<string, ModelAccumulator>();
102
+ /* The basis of the first record of a model prices the whole row: a model has one price. */
103
+ const modelBasis = new Map<string, { basis: UsagePriceBasis; pricedAs: string | null }>();
104
+ const projects = new Map<string, ProjectAccumulator>();
105
+ const sessions = new Set<string>();
106
+ const resolver = new ProjectResolver(known);
107
+ const folders = new Map<string, string>();
108
+ // A filter splits buckets and models per account; without one they fold the way they did before accounts.
109
+ const only = payload.accounts === undefined ? null : new Set(payload.accounts);
110
+
111
+ for (const record of records) {
112
+ const account = only === null ? null : accountOfRecord(record);
113
+ if (only !== null && !only.has(account!)) {
114
+ continue;
115
+ }
116
+ const slot = slotOf(format, record.timestampMs, payload.resolution);
117
+ const day = slot.slice(0, 10);
118
+ if (day < payload.from || day > payload.to) {
119
+ continue;
120
+ }
121
+ const { price, basis, pricedAs } = prices.look(record.model);
122
+ const cost = price === null ? 0 : costOf(record.totals, price);
123
+
124
+ const bucketKey = `${slot}\0${record.provider}\0${record.model}\0${account ?? ''}`;
125
+ let bucket = buckets.get(bucketKey);
126
+ if (bucket === undefined) {
127
+ bucket = {
128
+ slot,
129
+ provider: record.provider,
130
+ model: record.model,
131
+ account,
132
+ totals: emptyTotals(),
133
+ costUsd: 0,
134
+ priced: false,
135
+ cacheSavingsUsd: 0,
136
+ sessions: new Set()
137
+ };
138
+ buckets.set(bucketKey, bucket);
139
+ }
140
+ bucket.totals = addTotals(bucket.totals, record.totals);
141
+ bucket.costUsd += cost;
142
+ bucket.priced ||= price !== null;
143
+ bucket.cacheSavingsUsd += price === null ? 0 : cacheSavingsOf(record.totals, price);
144
+ if (record.sessionId !== '') {
145
+ bucket.sessions.add(record.sessionId);
146
+ sessions.add(`${record.provider}\0${record.sessionId}`);
147
+ }
148
+
149
+ const modelKey = `${record.provider}\0${record.model}\0${account ?? ''}`;
150
+ let model = models.get(modelKey);
151
+ if (model === undefined) {
152
+ model = { provider: record.provider, model: record.model, account, totals: emptyTotals(), costUsd: 0, priced: false };
153
+ models.set(modelKey, model);
154
+ modelBasis.set(modelKey, { basis, pricedAs });
155
+ }
156
+ model.totals = addTotals(model.totals, record.totals);
157
+ model.costUsd += cost;
158
+ model.priced ||= price !== null;
159
+
160
+ let folder = folders.get(record.cwd);
161
+ if (folder === undefined) {
162
+ const resolved = await resolver.resolve(record.cwd);
163
+ folder = resolved.folder;
164
+ folders.set(record.cwd, folder);
165
+ if (!projects.has(folder)) {
166
+ projects.set(folder, { folder, name: resolved.name, projectId: resolved.projectId, byProvider: new Map(), totals: emptyTotals(), costUsd: 0 });
167
+ }
168
+ }
169
+ const project = projects.get(folder)!;
170
+ project.totals = addTotals(project.totals, record.totals);
171
+ project.costUsd += cost;
172
+ const share = project.byProvider.get(record.provider) ?? { costUsd: 0, tokens: 0 };
173
+ share.costUsd += cost;
174
+ share.tokens += totalTokensOf(record.totals);
175
+ project.byProvider.set(record.provider, share);
176
+ }
177
+
178
+ return {
179
+ buckets: [...buckets.values()]
180
+ .map((bucket) => ({
181
+ slot: bucket.slot,
182
+ provider: bucket.provider,
183
+ model: bucket.model,
184
+ ...(bucket.account === null ? {} : { account: bucket.account }),
185
+ totals: bucket.totals,
186
+ costUsd: bucket.priced ? bucket.costUsd : null,
187
+ cacheSavingsUsd: bucket.cacheSavingsUsd,
188
+ sessions: bucket.sessions.size
189
+ }))
190
+ .sort(
191
+ (a, b) =>
192
+ a.slot.localeCompare(b.slot) ||
193
+ a.provider.localeCompare(b.provider) ||
194
+ a.model.localeCompare(b.model) ||
195
+ (a.account ?? '').localeCompare(b.account ?? '')
196
+ ),
197
+ models: [...models.entries()]
198
+ .map(([key, model]) => ({
199
+ provider: model.provider,
200
+ model: model.model,
201
+ ...(model.account === null ? {} : { account: model.account }),
202
+ totals: model.totals,
203
+ costUsd: model.priced ? model.costUsd : null,
204
+ priceBasis: modelBasis.get(key)?.basis ?? 'unknown',
205
+ pricedAs: modelBasis.get(key)?.pricedAs ?? null
206
+ }))
207
+ .sort((a, b) => (b.costUsd ?? -1) - (a.costUsd ?? -1)),
208
+ projects: [...projects.values()]
209
+ .map((project) => ({
210
+ folder: project.folder,
211
+ name: project.name,
212
+ projectId: project.projectId,
213
+ byProvider: Object.fromEntries(project.byProvider),
214
+ totals: project.totals,
215
+ costUsd: project.costUsd
216
+ }))
217
+ .sort((a, b) => b.costUsd - a.costUsd),
218
+ sessions: sessions.size
219
+ };
220
+ }