@adecore/agents 0.0.1 → 0.17.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +48 -0
  3. package/dist/async.d.ts +2 -0
  4. package/dist/async.js +31 -0
  5. package/dist/chat/attachment-store.d.ts +10 -0
  6. package/dist/chat/attachment-store.js +93 -0
  7. package/dist/chat/backend.d.ts +214 -0
  8. package/dist/chat/backend.js +1 -0
  9. package/dist/chat/background-work.d.ts +8 -0
  10. package/dist/chat/background-work.js +33 -0
  11. package/dist/chat/bookmark-store.d.ts +25 -0
  12. package/dist/chat/bookmark-store.js +141 -0
  13. package/dist/chat/chat-core.d.ts +311 -0
  14. package/dist/chat/chat-core.js +1025 -0
  15. package/dist/chat/chat-log.d.ts +33 -0
  16. package/dist/chat/chat-log.js +163 -0
  17. package/dist/chat/chat-process.d.ts +46 -0
  18. package/dist/chat/chat-process.js +190 -0
  19. package/dist/chat/chat-session.d.ts +209 -0
  20. package/dist/chat/chat-session.js +1511 -0
  21. package/dist/chat/chat-store.d.ts +37 -0
  22. package/dist/chat/chat-store.js +202 -0
  23. package/dist/chat/chat-title.d.ts +10 -0
  24. package/dist/chat/chat-title.js +74 -0
  25. package/dist/chat/claude-backend.d.ts +35 -0
  26. package/dist/chat/claude-backend.js +214 -0
  27. package/dist/chat/claude-protocol.d.ts +41 -0
  28. package/dist/chat/claude-protocol.js +642 -0
  29. package/dist/chat/claude-title.d.ts +11 -0
  30. package/dist/chat/claude-title.js +97 -0
  31. package/dist/chat/claude-transcript.d.ts +28 -0
  32. package/dist/chat/claude-transcript.js +211 -0
  33. package/dist/chat/codex-backend.d.ts +51 -0
  34. package/dist/chat/codex-backend.js +364 -0
  35. package/dist/chat/codex-protocol.d.ts +56 -0
  36. package/dist/chat/codex-protocol.js +767 -0
  37. package/dist/chat/codex-thread.d.ts +32 -0
  38. package/dist/chat/codex-thread.js +131 -0
  39. package/dist/chat/codex-transport.d.ts +42 -0
  40. package/dist/chat/codex-transport.js +188 -0
  41. package/dist/chat/composer-preferences.d.ts +15 -0
  42. package/dist/chat/composer-preferences.js +46 -0
  43. package/dist/chat/context-breakdown.d.ts +2 -0
  44. package/dist/chat/context-breakdown.js +74 -0
  45. package/dist/chat/delta-coalescer.d.ts +12 -0
  46. package/dist/chat/delta-coalescer.js +55 -0
  47. package/dist/chat/errors.d.ts +4 -0
  48. package/dist/chat/errors.js +4 -0
  49. package/dist/chat/fake-claude.d.ts +3 -0
  50. package/dist/chat/fake-claude.js +982 -0
  51. package/dist/chat/fake-cli.d.ts +25 -0
  52. package/dist/chat/fake-cli.js +157 -0
  53. package/dist/chat/fake-codex.d.ts +10 -0
  54. package/dist/chat/fake-codex.js +502 -0
  55. package/dist/chat/fixtures/claude-2.1.285-auto-fallback.json +24 -0
  56. package/dist/chat/fixtures/claude-2.1.285-native-children.json +6600 -0
  57. package/dist/chat/fixtures/claude-2.1.285-resume-direct-compact.json +202 -0
  58. package/dist/chat/fixtures/claude-2.1.285-resume-task-notification.json +57 -0
  59. package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.jsonl +10 -0
  60. package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.meta.json +1 -0
  61. package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.jsonl +2 -0
  62. package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.meta.json +1 -0
  63. package/dist/chat/fixtures/codex-0.159.2-native-approval.json +119 -0
  64. package/dist/chat/fixtures/codex-0.159.2-native-file-approval.json +136 -0
  65. package/dist/chat/fixtures/codex-0.159.2-native-interacted.json +220 -0
  66. package/dist/chat/fixtures/codex-0.159.2-usage-windows.json +74 -0
  67. package/dist/chat/input.d.ts +19 -0
  68. package/dist/chat/input.js +71 -0
  69. package/dist/chat/limit-resume.d.ts +11 -0
  70. package/dist/chat/limit-resume.js +68 -0
  71. package/dist/chat/projector.d.ts +60 -0
  72. package/dist/chat/projector.js +860 -0
  73. package/dist/chat/request-summary.d.ts +3 -0
  74. package/dist/chat/request-summary.js +143 -0
  75. package/dist/chat/subagent-projection.d.ts +8 -0
  76. package/dist/chat/subagent-projection.js +40 -0
  77. package/dist/chat/subagent-reader.d.ts +66 -0
  78. package/dist/chat/subagent-reader.js +396 -0
  79. package/dist/chat/subagent-settlement.d.ts +6 -0
  80. package/dist/chat/subagent-settlement.js +103 -0
  81. package/dist/chat/thread.d.ts +30 -0
  82. package/dist/chat/thread.js +147 -0
  83. package/dist/chat/wake-chat.d.ts +20 -0
  84. package/dist/chat/wake-chat.js +27 -0
  85. package/dist/client-sinks.d.ts +9 -0
  86. package/dist/client-sinks.js +34 -0
  87. package/dist/coded-error.d.ts +5 -0
  88. package/dist/coded-error.js +17 -0
  89. package/dist/context/argv.d.ts +11 -0
  90. package/dist/context/argv.js +58 -0
  91. package/dist/context/refusal.d.ts +9 -0
  92. package/dist/context/refusal.js +38 -0
  93. package/dist/context/verb.d.ts +85 -0
  94. package/dist/context/verb.js +233 -0
  95. package/dist/error-text.d.ts +5 -0
  96. package/dist/error-text.js +24 -0
  97. package/dist/events.d.ts +12 -0
  98. package/dist/events.js +1 -0
  99. package/dist/fs.d.ts +9 -0
  100. package/dist/fs.js +107 -0
  101. package/dist/host/agent-host.d.ts +28 -0
  102. package/dist/host/agent-host.js +105 -0
  103. package/dist/host/environment.d.ts +7 -0
  104. package/dist/host/environment.js +22 -0
  105. package/dist/host/handlers.d.ts +30 -0
  106. package/dist/host/handlers.js +111 -0
  107. package/dist/host/memory-port.d.ts +2 -0
  108. package/dist/host/memory-port.js +25 -0
  109. package/dist/host/wiring.d.ts +40 -0
  110. package/dist/host/wiring.js +120 -0
  111. package/dist/lineage.d.ts +44 -0
  112. package/dist/lineage.js +134 -0
  113. package/dist/messages/deliver-message.d.ts +21 -0
  114. package/dist/messages/deliver-message.js +42 -0
  115. package/dist/messages/deliver-notice.d.ts +25 -0
  116. package/dist/messages/deliver-notice.js +75 -0
  117. package/dist/messages/notice-notes.d.ts +15 -0
  118. package/dist/messages/notice-notes.js +26 -0
  119. package/dist/messages/notice-store.d.ts +30 -0
  120. package/dist/messages/notice-store.js +120 -0
  121. package/dist/modes.d.ts +11 -0
  122. package/dist/modes.js +28 -0
  123. package/dist/outbox/manual-clock.d.ts +10 -0
  124. package/dist/outbox/manual-clock.js +25 -0
  125. package/dist/outbox/outbox-worker.d.ts +44 -0
  126. package/dist/outbox/outbox-worker.js +173 -0
  127. package/dist/outbox/outbox.d.ts +37 -0
  128. package/dist/outbox/outbox.js +76 -0
  129. package/dist/providers/accounts/accounts.d.ts +20 -0
  130. package/dist/providers/accounts/accounts.js +158 -0
  131. package/dist/providers/accounts/launch.d.ts +17 -0
  132. package/dist/providers/accounts/launch.js +30 -0
  133. package/dist/providers/accounts/service.d.ts +87 -0
  134. package/dist/providers/accounts/service.js +538 -0
  135. package/dist/providers/accounts/shadow-home.d.ts +8 -0
  136. package/dist/providers/accounts/shadow-home.js +70 -0
  137. package/dist/providers/accounts/status.d.ts +12 -0
  138. package/dist/providers/accounts/status.js +85 -0
  139. package/dist/providers/accounts/store.d.ts +14 -0
  140. package/dist/providers/accounts/store.js +112 -0
  141. package/dist/providers/accounts/test-accounts.d.ts +12 -0
  142. package/dist/providers/accounts/test-accounts.js +38 -0
  143. package/dist/providers/accounts/variables.d.ts +27 -0
  144. package/dist/providers/accounts/variables.js +94 -0
  145. package/dist/providers/catalog.d.ts +17 -0
  146. package/dist/providers/catalog.js +76 -0
  147. package/dist/providers/claude-models.json +114 -0
  148. package/dist/providers/claude-provider.d.ts +4 -0
  149. package/dist/providers/claude-provider.js +29 -0
  150. package/dist/providers/claude.d.ts +15 -0
  151. package/dist/providers/claude.js +105 -0
  152. package/dist/providers/codex-models.json +135 -0
  153. package/dist/providers/codex-provider.d.ts +4 -0
  154. package/dist/providers/codex-provider.js +27 -0
  155. package/dist/providers/codex-rules.d.ts +9 -0
  156. package/dist/providers/codex-rules.js +35 -0
  157. package/dist/providers/codex.d.ts +14 -0
  158. package/dist/providers/codex.js +52 -0
  159. package/dist/providers/detect.d.ts +6 -0
  160. package/dist/providers/detect.js +30 -0
  161. package/dist/providers/provider.d.ts +25 -0
  162. package/dist/providers/provider.js +1 -0
  163. package/dist/providers/registry.d.ts +28 -0
  164. package/dist/providers/registry.js +118 -0
  165. package/dist/record-directory.d.ts +27 -0
  166. package/dist/record-directory.js +114 -0
  167. package/dist/run-process.d.ts +13 -0
  168. package/dist/run-process.js +42 -0
  169. package/dist/serializer.d.ts +9 -0
  170. package/dist/serializer.js +34 -0
  171. package/dist/skills.d.ts +21 -0
  172. package/dist/skills.js +174 -0
  173. package/dist/tasks/background-limit.d.ts +16 -0
  174. package/dist/tasks/background-limit.js +55 -0
  175. package/dist/tasks/end-children.d.ts +34 -0
  176. package/dist/tasks/end-children.js +53 -0
  177. package/dist/tasks/give-task.d.ts +14 -0
  178. package/dist/tasks/give-task.js +33 -0
  179. package/dist/tasks/task-coordinator.d.ts +52 -0
  180. package/dist/tasks/task-coordinator.js +323 -0
  181. package/dist/tasks/task-store.d.ts +49 -0
  182. package/dist/tasks/task-store.js +197 -0
  183. package/dist/tasks/task-work.d.ts +47 -0
  184. package/dist/tasks/task-work.js +28 -0
  185. package/dist/tasks/waiting-child.d.ts +51 -0
  186. package/dist/tasks/waiting-child.js +143 -0
  187. package/dist/tasks/wake-parent.d.ts +19 -0
  188. package/dist/tasks/wake-parent.js +109 -0
  189. package/dist/tasks/wiring.d.ts +51 -0
  190. package/dist/tasks/wiring.js +151 -0
  191. package/dist/title-file.d.ts +2 -0
  192. package/dist/title-file.js +55 -0
  193. package/dist/usage/accounts.d.ts +15 -0
  194. package/dist/usage/accounts.js +64 -0
  195. package/dist/usage/aggregate.d.ts +12 -0
  196. package/dist/usage/aggregate.js +154 -0
  197. package/dist/usage/exchange.d.ts +16 -0
  198. package/dist/usage/exchange.js +84 -0
  199. package/dist/usage/index-file.d.ts +15 -0
  200. package/dist/usage/index-file.js +123 -0
  201. package/dist/usage/limits/monitor.d.ts +50 -0
  202. package/dist/usage/limits/monitor.js +179 -0
  203. package/dist/usage/limits/normalize.d.ts +21 -0
  204. package/dist/usage/limits/normalize.js +160 -0
  205. package/dist/usage/limits/probe.d.ts +9 -0
  206. package/dist/usage/limits/probe.js +113 -0
  207. package/dist/usage/prices-snapshot.json +1071 -0
  208. package/dist/usage/pricing.d.ts +34 -0
  209. package/dist/usage/pricing.js +186 -0
  210. package/dist/usage/projects.d.ts +17 -0
  211. package/dist/usage/projects.js +49 -0
  212. package/dist/usage/readers/claude.d.ts +3 -0
  213. package/dist/usage/readers/claude.js +59 -0
  214. package/dist/usage/readers/codex.d.ts +23 -0
  215. package/dist/usage/readers/codex.js +155 -0
  216. package/dist/usage/record.d.ts +15 -0
  217. package/dist/usage/record.js +39 -0
  218. package/dist/usage/roots.d.ts +12 -0
  219. package/dist/usage/roots.js +29 -0
  220. package/dist/usage/scanner.d.ts +30 -0
  221. package/dist/usage/scanner.js +234 -0
  222. package/dist/usage/usage-service.d.ts +35 -0
  223. package/dist/usage/usage-service.js +130 -0
  224. package/dist/watch-seam.d.ts +29 -0
  225. package/dist/watch-seam.js +78 -0
  226. package/dist/watch-test-helpers.d.ts +28 -0
  227. package/dist/watch-test-helpers.js +86 -0
  228. package/package.json +42 -3
  229. package/src/async.ts +40 -0
  230. package/src/chat/attachment-store.ts +108 -0
  231. package/src/chat/backend.ts +199 -0
  232. package/src/chat/background-work.ts +42 -0
  233. package/src/chat/bookmark-store.ts +166 -0
  234. package/src/chat/chat-core.ts +1228 -0
  235. package/src/chat/chat-log.ts +187 -0
  236. package/src/chat/chat-process.ts +237 -0
  237. package/src/chat/chat-session.ts +1723 -0
  238. package/src/chat/chat-store.ts +261 -0
  239. package/src/chat/chat-title.ts +90 -0
  240. package/src/chat/claude-backend.ts +235 -0
  241. package/src/chat/claude-protocol.ts +684 -0
  242. package/src/chat/claude-title.ts +106 -0
  243. package/src/chat/claude-transcript.ts +233 -0
  244. package/src/chat/codex-backend.ts +397 -0
  245. package/src/chat/codex-protocol.ts +872 -0
  246. package/src/chat/codex-thread.ts +162 -0
  247. package/src/chat/codex-transport.ts +224 -0
  248. package/src/chat/composer-preferences.ts +67 -0
  249. package/src/chat/context-breakdown.ts +80 -0
  250. package/src/chat/delta-coalescer.ts +62 -0
  251. package/src/chat/errors.ts +25 -0
  252. package/src/chat/fake-claude.ts +1006 -0
  253. package/src/chat/fake-cli.ts +199 -0
  254. package/src/chat/fake-codex.ts +534 -0
  255. package/src/chat/fixtures/claude-2.1.285-auto-fallback.json +24 -0
  256. package/src/chat/fixtures/claude-2.1.285-native-children.json +6600 -0
  257. package/src/chat/fixtures/claude-2.1.285-resume-direct-compact.json +202 -0
  258. package/src/chat/fixtures/claude-2.1.285-resume-task-notification.json +57 -0
  259. package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.jsonl +10 -0
  260. package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.meta.json +1 -0
  261. package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.jsonl +2 -0
  262. package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.meta.json +1 -0
  263. package/src/chat/fixtures/codex-0.159.2-native-approval.json +119 -0
  264. package/src/chat/fixtures/codex-0.159.2-native-file-approval.json +136 -0
  265. package/src/chat/fixtures/codex-0.159.2-native-interacted.json +220 -0
  266. package/src/chat/fixtures/codex-0.159.2-usage-windows.json +74 -0
  267. package/src/chat/input.ts +89 -0
  268. package/src/chat/limit-resume.ts +74 -0
  269. package/src/chat/projector.ts +969 -0
  270. package/src/chat/request-summary.ts +168 -0
  271. package/src/chat/subagent-projection.ts +45 -0
  272. package/src/chat/subagent-reader.ts +469 -0
  273. package/src/chat/subagent-settlement.ts +111 -0
  274. package/src/chat/thread.ts +167 -0
  275. package/src/chat/wake-chat.ts +45 -0
  276. package/src/client-sinks.ts +39 -0
  277. package/src/coded-error.ts +18 -0
  278. package/src/context/argv.ts +61 -0
  279. package/src/context/refusal.ts +50 -0
  280. package/src/context/verb.ts +392 -0
  281. package/src/error-text.ts +28 -0
  282. package/src/events.ts +9 -0
  283. package/src/fs.ts +118 -0
  284. package/src/host/agent-host.ts +124 -0
  285. package/src/host/environment.ts +31 -0
  286. package/src/host/handlers.ts +142 -0
  287. package/src/host/memory-port.ts +27 -0
  288. package/src/host/wiring.ts +164 -0
  289. package/src/lineage.ts +159 -0
  290. package/src/messages/deliver-message.ts +61 -0
  291. package/src/messages/deliver-notice.ts +120 -0
  292. package/src/messages/notice-notes.ts +34 -0
  293. package/src/messages/notice-store.ts +142 -0
  294. package/src/modes.ts +38 -0
  295. package/src/outbox/manual-clock.ts +31 -0
  296. package/src/outbox/outbox-worker.ts +214 -0
  297. package/src/outbox/outbox.ts +106 -0
  298. package/src/providers/accounts/accounts.ts +192 -0
  299. package/src/providers/accounts/launch.ts +55 -0
  300. package/src/providers/accounts/service.ts +632 -0
  301. package/src/providers/accounts/shadow-home.ts +85 -0
  302. package/src/providers/accounts/status.ts +106 -0
  303. package/src/providers/accounts/store.ts +158 -0
  304. package/src/providers/accounts/test-accounts.ts +48 -0
  305. package/src/providers/accounts/variables.ts +132 -0
  306. package/src/providers/catalog.ts +90 -0
  307. package/src/providers/claude-models.json +114 -0
  308. package/src/providers/claude-provider.ts +32 -0
  309. package/src/providers/claude.ts +123 -0
  310. package/src/providers/codex-models.json +135 -0
  311. package/src/providers/codex-provider.ts +30 -0
  312. package/src/providers/codex-rules.ts +54 -0
  313. package/src/providers/codex.ts +69 -0
  314. package/src/providers/detect.ts +40 -0
  315. package/src/providers/provider.ts +46 -0
  316. package/src/providers/registry.ts +150 -0
  317. package/src/record-directory.ts +136 -0
  318. package/src/run-process.ts +61 -0
  319. package/src/serializer.ts +41 -0
  320. package/src/skills.ts +199 -0
  321. package/src/tasks/background-limit.ts +72 -0
  322. package/src/tasks/end-children.ts +100 -0
  323. package/src/tasks/give-task.ts +52 -0
  324. package/src/tasks/task-coordinator.ts +384 -0
  325. package/src/tasks/task-store.ts +236 -0
  326. package/src/tasks/task-work.ts +62 -0
  327. package/src/tasks/waiting-child.ts +203 -0
  328. package/src/tasks/wake-parent.ts +142 -0
  329. package/src/tasks/wiring.ts +231 -0
  330. package/src/title-file.ts +57 -0
  331. package/src/usage/accounts.ts +86 -0
  332. package/src/usage/aggregate.ts +220 -0
  333. package/src/usage/exchange.ts +93 -0
  334. package/src/usage/index-file.ts +178 -0
  335. package/src/usage/limits/monitor.ts +239 -0
  336. package/src/usage/limits/normalize.ts +188 -0
  337. package/src/usage/limits/probe.ts +120 -0
  338. package/src/usage/prices-snapshot.json +1071 -0
  339. package/src/usage/pricing.ts +236 -0
  340. package/src/usage/projects.ts +65 -0
  341. package/src/usage/readers/claude.ts +60 -0
  342. package/src/usage/readers/codex.ts +196 -0
  343. package/src/usage/record.ts +60 -0
  344. package/src/usage/roots.ts +39 -0
  345. package/src/usage/scanner.ts +265 -0
  346. package/src/usage/usage-service.ts +161 -0
  347. package/src/watch-seam.ts +111 -0
  348. package/src/watch-test-helpers.ts +102 -0
@@ -0,0 +1,234 @@
1
+ import { mkdir, open, readFile, readdir, stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { isNotFound, writeAtomic } from '../fs.js';
4
+ import { decodeIndex, encodeIndex } from './index-file.js';
5
+ import { foldByKey } from './record.js';
6
+ import { claudeMightCarryUsage, parseClaudeLine } from './readers/claude.js';
7
+ import { cloneCodexState, codexMightCarryUsage, createCodexState, parseCodexLine } from './readers/codex.js';
8
+ import { usageRoots } from './roots.js';
9
+ import { errorText } from '../error-text.js';
10
+ /* What a file holds from `from` to its end, as it is at the read. */
11
+ async function readFrom(path, from) {
12
+ const handle = await open(path, 'r');
13
+ try {
14
+ const { size } = await handle.stat();
15
+ const buffer = Buffer.alloc(Math.max(0, size - from));
16
+ let filled = 0;
17
+ while (filled < buffer.length) {
18
+ const { bytesRead } = await handle.read(buffer, filled, buffer.length - filled, from + filled);
19
+ if (bytesRead === 0) {
20
+ break;
21
+ }
22
+ filled += bytesRead;
23
+ }
24
+ return buffer.subarray(0, filled).toString('utf8');
25
+ }
26
+ finally {
27
+ await handle.close();
28
+ }
29
+ }
30
+ async function listJsonl(dir, out = []) {
31
+ let entries;
32
+ try {
33
+ entries = await readdir(dir, { withFileTypes: true });
34
+ }
35
+ catch {
36
+ // A directory that cannot be read is one the scan walks past; the root itself reports status.
37
+ return out;
38
+ }
39
+ for (const entry of entries) {
40
+ const path = join(dir, entry.name);
41
+ if (entry.isDirectory()) {
42
+ await listJsonl(path, out);
43
+ }
44
+ else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
45
+ out.push(path);
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+ /*
51
+ * Reads what a file added since the last scan. Only whole lines count towards the offset; the piece
52
+ * after the last newline is a line still being written, so it is parsed for what it is worth and
53
+ * read again next time. The Codex state is snapshotted before that tail for the same reason.
54
+ */
55
+ function parseChunk(text, provider, from, state) {
56
+ let records = [];
57
+ const end = text.lastIndexOf('\n');
58
+ const whole = end === -1 ? '' : text.slice(0, end);
59
+ const tail = text.slice(end + 1);
60
+ const read = (chunk, codex) => {
61
+ for (const raw of chunk.split('\n')) {
62
+ const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
63
+ if (line === '') {
64
+ continue;
65
+ }
66
+ if (provider === 'claude') {
67
+ if (claudeMightCarryUsage(line)) {
68
+ const record = parseClaudeLine(line);
69
+ if (record !== null) {
70
+ records.push(record);
71
+ }
72
+ }
73
+ continue;
74
+ }
75
+ if (codex !== null && codexMightCarryUsage(line)) {
76
+ const record = parseCodexLine(line, codex);
77
+ if (record !== null) {
78
+ records.push(record);
79
+ }
80
+ }
81
+ }
82
+ };
83
+ const codex = provider === 'codex' ? (state ?? createCodexState()) : null;
84
+ read(whole, codex);
85
+ const committed = codex === null ? null : cloneCodexState(codex);
86
+ const counted = records;
87
+ records = [];
88
+ read(tail, codex);
89
+ return { records: counted, tail: records, offset: from + (end === -1 ? 0 : Buffer.byteLength(whole, 'utf8') + 1), codex: committed };
90
+ }
91
+ /*
92
+ * The account a record of a root belongs to, as a record keeps it (absent for the default account):
93
+ * the one account that writes there, else the one the host knows ran the session, else the
94
+ * default account of the CLI, which is the only one a session nobody started through the host ran under.
95
+ */
96
+ export function accountResolver(root, sessionAccounts) {
97
+ const accounts = root.accounts ?? [root.provider];
98
+ const stored = (id) => (id === root.provider ? undefined : id);
99
+ if (accounts.length === 1) {
100
+ const only = stored(accounts[0]);
101
+ return () => only;
102
+ }
103
+ const fallback = accounts.includes(root.provider) ? root.provider : accounts[0];
104
+ return (record) => {
105
+ const ran = sessionAccounts().get(`${root.provider}\0${record.sessionId}`);
106
+ return stored(ran !== undefined && accounts.includes(ran) ? ran : fallback);
107
+ };
108
+ }
109
+ function withAccount(records, accountOf) {
110
+ return records.map((record) => {
111
+ const account = accountOf(record);
112
+ return account === undefined ? record : { ...record, account };
113
+ });
114
+ }
115
+ /*
116
+ * Read appended transcript bytes incrementally, but restart after rewrites or truncation. Preserve
117
+ * totals for deleted transcripts because their usage still happened.
118
+ */
119
+ export class UsageScanner {
120
+ home;
121
+ roots;
122
+ sessionAccounts;
123
+ index = new Map();
124
+ loaded = false;
125
+ dirty = false;
126
+ /* `roots` as a function is asked again on every scan, so an account added since is walked too. */
127
+ constructor(home, roots = usageRoots(), options = {}) {
128
+ this.home = home;
129
+ this.roots = typeof roots === 'function' ? roots : () => roots;
130
+ this.sessionAccounts = options.sessionAccounts ?? (() => new Map());
131
+ }
132
+ get file() {
133
+ return join(this.home, 'usage', 'index.json');
134
+ }
135
+ async load() {
136
+ if (this.loaded) {
137
+ return;
138
+ }
139
+ this.loaded = true;
140
+ try {
141
+ this.index = decodeIndex(await readFile(this.file, 'utf8'));
142
+ }
143
+ catch (e) {
144
+ if (!isNotFound(e)) {
145
+ console.warn('The usage index would not read; starting from a cold scan:', errorText(e));
146
+ }
147
+ }
148
+ }
149
+ async scan() {
150
+ const started = Date.now();
151
+ await this.load();
152
+ const roots = [];
153
+ let files = 0;
154
+ let changedFiles = 0;
155
+ let sessions = null;
156
+ const sessionAccounts = () => {
157
+ sessions ??= this.sessionAccounts();
158
+ return sessions;
159
+ };
160
+ for (const root of this.roots()) {
161
+ try {
162
+ const info = await stat(root.path);
163
+ if (!info.isDirectory()) {
164
+ roots.push({ provider: root.provider, path: root.path, status: 'missing', message: null });
165
+ continue;
166
+ }
167
+ }
168
+ catch (e) {
169
+ const missing = isNotFound(e);
170
+ roots.push({
171
+ provider: root.provider,
172
+ path: root.path,
173
+ status: missing ? 'missing' : 'failed',
174
+ message: missing ? null : errorText(e)
175
+ });
176
+ continue;
177
+ }
178
+ let failure = null;
179
+ const accountOf = accountResolver(root, sessionAccounts);
180
+ for (const path of await listJsonl(root.path)) {
181
+ files += 1;
182
+ try {
183
+ if (await this.readFileInto(path, root.provider, accountOf)) {
184
+ changedFiles += 1;
185
+ }
186
+ }
187
+ catch (e) {
188
+ // One unreadable transcript is not the root failing; the count simply misses it.
189
+ failure ??= errorText(e);
190
+ }
191
+ }
192
+ roots.push({ provider: root.provider, path: root.path, status: failure === null ? 'ok' : 'failed', message: failure });
193
+ }
194
+ if (this.dirty) {
195
+ this.dirty = false;
196
+ await mkdir(join(this.home, 'usage'), { recursive: true, mode: 0o700 });
197
+ await writeAtomic(this.file, encodeIndex(this.index));
198
+ }
199
+ return { at: Date.now(), files, changedFiles, durationMs: Date.now() - started, roots };
200
+ }
201
+ /* Every record the index holds, with the copies one message left in several transcripts folded. */
202
+ records() {
203
+ const all = [];
204
+ for (const file of this.index.values()) {
205
+ all.push(...file.records, ...file.tail);
206
+ }
207
+ return foldByKey(all);
208
+ }
209
+ async readFileInto(path, provider, accountOf) {
210
+ const info = await stat(path);
211
+ const known = this.index.get(path);
212
+ if (known !== undefined && known.provider === provider && known.size === info.size && known.mtimeMs === info.mtimeMs) {
213
+ return false;
214
+ }
215
+ const grown = known !== undefined && known.provider === provider && info.size > known.size;
216
+ const from = grown ? known.offset : 0;
217
+ const text = await readFrom(path, from);
218
+ const parsed = parseChunk(text, provider, from, grown ? known.codex : null);
219
+ const before = grown ? known.records : [];
220
+ const entry = {
221
+ provider,
222
+ size: info.size,
223
+ mtimeMs: info.mtimeMs,
224
+ offset: parsed.offset,
225
+ // Folding here keeps the streaming copies of one message out of the index entirely.
226
+ records: foldByKey([...before, ...withAccount(parsed.records, accountOf)]),
227
+ tail: withAccount(parsed.tail, accountOf),
228
+ codex: parsed.codex
229
+ };
230
+ this.index.set(path, entry);
231
+ this.dirty = true;
232
+ return true;
233
+ }
234
+ }
@@ -0,0 +1,35 @@
1
+ import type { UsageAccount, UsageSummaryPayload, UsageSummaryResult } from '@adecore/agent-contracts';
2
+ import type { AgentSink } from '../events.ts';
3
+ import type { KnownProject } from './projects.ts';
4
+ import { type UsageScannerOptions } from './scanner.ts';
5
+ import type { UsageRootPath } from './roots.ts';
6
+ export interface UsageServiceOptions {
7
+ home: string;
8
+ allowPriceFetch?: boolean;
9
+ knownProjects(): Promise<KnownProject[]>;
10
+ roots?: UsageRootPath[] | (() => UsageRootPath[]);
11
+ sessionAccounts?: UsageScannerOptions['sessionAccounts'];
12
+ accounts?: () => UsageAccount[];
13
+ }
14
+ export declare class UsageService {
15
+ private readonly scanner;
16
+ private readonly prices;
17
+ private readonly rates;
18
+ private readonly knownProjects;
19
+ private readonly accounts;
20
+ private readonly sinks;
21
+ private readonly followers;
22
+ private report;
23
+ private failed;
24
+ private inFlight;
25
+ private timer;
26
+ constructor(options: UsageServiceOptions);
27
+ subscribe(clientId: string, sink: AgentSink): () => void;
28
+ follow(clientId: string): void;
29
+ unfollow(clientId: string): void;
30
+ summary(payload: UsageSummaryPayload): Promise<UsageSummaryResult>;
31
+ stop(): void;
32
+ private rescan;
33
+ private runScan;
34
+ private emit;
35
+ }
@@ -0,0 +1,130 @@
1
+ import { accountOfRecord, aggregate } from './aggregate.js';
2
+ import { ExchangeRates } from './exchange.js';
3
+ import { PriceBook } from './pricing.js';
4
+ import { UsageScanner } from './scanner.js';
5
+ import { errorText } from '../error-text.js';
6
+ import { ClientSinks } from '../client-sinks.js';
7
+ /* A scan this fresh answers the question the page is asking, so nothing is opened for it. */
8
+ const SCAN_TTL_MS = 60_000;
9
+ /* The accounts of the machine, and after them any a record names that the machine no longer has, under its id. */
10
+ function withRecordAccounts(accounts, records) {
11
+ const listed = new Set(accounts.map((account) => account.id));
12
+ const gone = [];
13
+ for (const record of records) {
14
+ const id = accountOfRecord(record);
15
+ if (!listed.has(id)) {
16
+ listed.add(id);
17
+ gone.push({ id, kind: record.provider, label: id });
18
+ }
19
+ }
20
+ return [...accounts, ...gone];
21
+ }
22
+ const EMPTY_SCAN = { at: 0, files: 0, changedFiles: 0, durationMs: 0, roots: [] };
23
+ /*
24
+ * What the usage page asks the host. It owns one scanner and one price table for every client:
25
+ * the transcripts are machine-wide, so a second person looking costs a second aggregation and not a
26
+ * second scan. The scan runs when a request finds the last one stale and, while any client has the
27
+ * page open, once a minute, which is what `usage.changed` announces.
28
+ */
29
+ export class UsageService {
30
+ scanner;
31
+ prices;
32
+ rates;
33
+ knownProjects;
34
+ accounts;
35
+ sinks = new ClientSinks((clientId) => this.unfollow(clientId));
36
+ followers = new Set();
37
+ report = EMPTY_SCAN;
38
+ failed = false;
39
+ inFlight = null;
40
+ timer = null;
41
+ constructor(options) {
42
+ this.scanner = new UsageScanner(options.home, options.roots, options.sessionAccounts ? { sessionAccounts: options.sessionAccounts } : {});
43
+ this.accounts = options.accounts ?? null;
44
+ this.prices = new PriceBook(options.home, options.allowPriceFetch ?? true);
45
+ this.rates = new ExchangeRates(options.home, options.allowPriceFetch ?? true);
46
+ this.knownProjects = options.knownProjects;
47
+ }
48
+ subscribe(clientId, sink) {
49
+ return this.sinks.subscribe(clientId, sink);
50
+ }
51
+ /* The page is open here: keep the numbers moving until it closes or the socket does. */
52
+ follow(clientId) {
53
+ this.followers.add(clientId);
54
+ this.timer ??= setInterval(() => {
55
+ void this.rescan();
56
+ }, SCAN_TTL_MS);
57
+ }
58
+ unfollow(clientId) {
59
+ this.followers.delete(clientId);
60
+ if (this.followers.size === 0 && this.timer !== null) {
61
+ clearInterval(this.timer);
62
+ this.timer = null;
63
+ }
64
+ }
65
+ async summary(payload) {
66
+ if (Date.now() - this.report.at > SCAN_TTL_MS) {
67
+ await this.rescan();
68
+ }
69
+ const records = this.scanner.records();
70
+ const { buckets, models, projects, sessions } = await aggregate(records, payload, this.prices, await this.knownProjects());
71
+ return {
72
+ from: payload.from,
73
+ to: payload.to,
74
+ resolution: payload.resolution,
75
+ timeZone: payload.timeZone,
76
+ buckets,
77
+ models,
78
+ projects,
79
+ sessions,
80
+ scan: {
81
+ at: this.report.at,
82
+ files: this.report.files,
83
+ changedFiles: this.report.changedFiles,
84
+ durationMs: this.report.durationMs,
85
+ running: this.inFlight !== null,
86
+ failed: this.failed
87
+ },
88
+ pricing: this.prices.pricing,
89
+ rate: this.rates.current,
90
+ roots: this.report.roots,
91
+ ...(this.accounts === null ? {} : { accounts: withRecordAccounts(this.accounts(), records) })
92
+ };
93
+ }
94
+ stop() {
95
+ if (this.timer !== null) {
96
+ clearInterval(this.timer);
97
+ this.timer = null;
98
+ }
99
+ }
100
+ /* One pass at a time: a request that arrives while a pass runs waits for that one's answer. */
101
+ rescan() {
102
+ this.inFlight ??= this.runScan().finally(() => {
103
+ this.inFlight = null;
104
+ });
105
+ return this.inFlight;
106
+ }
107
+ async runScan() {
108
+ // The table and the rate are fetched beside the walk, so a slow answer never holds up the scan.
109
+ const [report] = await Promise.all([
110
+ this.scanner.scan().catch((e) => {
111
+ console.error('The usage scan failed:', errorText(e));
112
+ return null;
113
+ }),
114
+ this.prices.ensure(),
115
+ this.rates.ensure()
116
+ ]);
117
+ this.failed = report === null;
118
+ if (report === null) {
119
+ // The last good numbers stay on screen; only the moment of the scan is not moved on.
120
+ return;
121
+ }
122
+ this.report = report;
123
+ if (report.changedFiles > 0) {
124
+ this.emit({ event: 'usage.changed', payload: { scannedAt: report.at } });
125
+ }
126
+ }
127
+ emit(event) {
128
+ this.sinks.emit(event);
129
+ }
130
+ }
@@ -0,0 +1,29 @@
1
+ export interface DirectoryWatcher {
2
+ on(event: 'error', listener: (error: Error) => void): unknown;
3
+ close(): void;
4
+ }
5
+ export type WatchDirectory = (path: string, options: {
6
+ recursive: boolean;
7
+ }, listener: (event: string, filename: string | null) => void) => DirectoryWatcher;
8
+ export type Schedule = (callback: () => unknown, ms: number) => () => void;
9
+ export interface WatchSeams {
10
+ watch: WatchDirectory;
11
+ schedule: Schedule;
12
+ }
13
+ export declare function supportsRecursive(platform: NodeJS.Platform): boolean;
14
+ export declare const SYSTEM_WATCH: WatchSeams;
15
+ export interface Settled {
16
+ nudge(): void;
17
+ stop(): void;
18
+ }
19
+ export declare function settled(seams: WatchSeams, ms: number, run: () => unknown): Settled;
20
+ export declare class PerClientWatches<TState> {
21
+ private readonly byClient;
22
+ private readonly stop;
23
+ constructor(stop: (state: TState) => void);
24
+ get(clientId: string, key: string): TState | undefined;
25
+ put(clientId: string, key: string, state: TState): void;
26
+ all(clientId: string): Array<[string, TState]>;
27
+ remove(clientId: string, key: string): void;
28
+ detachAll(clientId: string): void;
29
+ }
@@ -0,0 +1,78 @@
1
+ import { watch } from 'node:fs';
2
+ /*
3
+ * Recursive watching is one call to the platform on macOS and Windows; elsewhere it costs a
4
+ * descriptor per directory, which a folder tree has too many of to be worth it.
5
+ */
6
+ export function supportsRecursive(platform) {
7
+ return platform === 'darwin' || platform === 'win32';
8
+ }
9
+ export const SYSTEM_WATCH = {
10
+ watch: (path, options, listener) => watch(path, options, listener),
11
+ schedule: (callback, ms) => {
12
+ const timer = setTimeout(callback, ms);
13
+ return () => clearTimeout(timer);
14
+ }
15
+ };
16
+ /*
17
+ * A save, a formatter and a build all touch a folder in a burst, and one answer per burst is enough.
18
+ * How long a burst is differs per watcher: reading one file again is cheap, a status run over a
19
+ * whole repository is not.
20
+ */
21
+ export function settled(seams, ms, run) {
22
+ let cancel = null;
23
+ return {
24
+ nudge: () => {
25
+ cancel?.();
26
+ cancel = seams.schedule(() => {
27
+ cancel = null;
28
+ return run();
29
+ }, ms);
30
+ },
31
+ stop: () => {
32
+ cancel?.();
33
+ cancel = null;
34
+ }
35
+ };
36
+ }
37
+ /*
38
+ * What each client watches, keyed on the path it asked for. A watch is per client, like a session
39
+ * attach: two windows on the same folder each get their own, and a socket that goes takes only its
40
+ * own with it.
41
+ */
42
+ export class PerClientWatches {
43
+ byClient = new Map();
44
+ stop;
45
+ constructor(stop) {
46
+ this.stop = stop;
47
+ }
48
+ get(clientId, key) {
49
+ return this.byClient.get(clientId)?.get(key);
50
+ }
51
+ put(clientId, key, state) {
52
+ const watches = this.byClient.get(clientId) ?? new Map();
53
+ watches.set(key, state);
54
+ this.byClient.set(clientId, watches);
55
+ }
56
+ /* Everything this client watches, as a copy, so a caller may close one while it walks them. */
57
+ all(clientId) {
58
+ return [...(this.byClient.get(clientId) ?? [])];
59
+ }
60
+ remove(clientId, key) {
61
+ const watches = this.byClient.get(clientId);
62
+ const state = watches?.get(key);
63
+ if (!watches || state === undefined) {
64
+ return;
65
+ }
66
+ this.stop(state);
67
+ watches.delete(key);
68
+ if (watches.size === 0) {
69
+ this.byClient.delete(clientId);
70
+ }
71
+ }
72
+ detachAll(clientId) {
73
+ for (const state of this.byClient.get(clientId)?.values() ?? []) {
74
+ this.stop(state);
75
+ }
76
+ this.byClient.delete(clientId);
77
+ }
78
+ }
@@ -0,0 +1,28 @@
1
+ import type { DirectoryWatcher, WatchDirectory, WatchSeams } from './watch-seam.ts';
2
+ export declare class FakeDirectoryWatcher implements DirectoryWatcher {
3
+ readonly path: string;
4
+ readonly recursive: boolean;
5
+ closed: boolean;
6
+ private readonly listener;
7
+ constructor(path: string, recursive: boolean, listener: (event: string, filename: string | null) => void);
8
+ emit(filename: string | null, event?: 'change' | 'rename'): void;
9
+ on(): this;
10
+ close(): void;
11
+ }
12
+ export declare class FakeWatch implements WatchSeams {
13
+ readonly watchers: FakeDirectoryWatcher[];
14
+ private readonly due;
15
+ private nextId;
16
+ readonly watch: WatchDirectory;
17
+ readonly schedule: (callback: () => unknown, _ms: number) => (() => void);
18
+ get pending(): number;
19
+ openOn(path: string): FakeDirectoryWatcher[];
20
+ on(path: string): FakeDirectoryWatcher;
21
+ settle(): Promise<void>;
22
+ }
23
+ export declare class EventLog<T> {
24
+ readonly events: T[];
25
+ private waiters;
26
+ readonly push: (event: T) => void;
27
+ until(check: (events: T[]) => boolean): Promise<void>;
28
+ }
@@ -0,0 +1,86 @@
1
+ export class FakeDirectoryWatcher {
2
+ path;
3
+ recursive;
4
+ closed = false;
5
+ listener;
6
+ constructor(path, recursive, listener) {
7
+ this.path = path;
8
+ this.recursive = recursive;
9
+ this.listener = listener;
10
+ }
11
+ /* What the platform would report for a write to `filename`, relative to the watched directory. */
12
+ emit(filename, event = 'change') {
13
+ if (this.closed) {
14
+ return;
15
+ }
16
+ this.listener(event, filename);
17
+ }
18
+ on() {
19
+ return this;
20
+ }
21
+ close() {
22
+ this.closed = true;
23
+ }
24
+ }
25
+ /*
26
+ * A watch function and a settle timer that only move when a test says so: `emit` on a watcher stands
27
+ * in for the platform, and `settle` runs every timer that is due and waits for the work it started.
28
+ */
29
+ export class FakeWatch {
30
+ watchers = [];
31
+ due = new Map();
32
+ nextId = 1;
33
+ watch = (path, options, listener) => {
34
+ const watcher = new FakeDirectoryWatcher(path, options.recursive, listener);
35
+ this.watchers.push(watcher);
36
+ return watcher;
37
+ };
38
+ schedule = (callback, _ms) => {
39
+ const id = this.nextId++;
40
+ this.due.set(id, callback);
41
+ return () => {
42
+ this.due.delete(id);
43
+ };
44
+ };
45
+ /* How many timers are waiting to run. */
46
+ get pending() {
47
+ return this.due.size;
48
+ }
49
+ openOn(path) {
50
+ return this.watchers.filter((watcher) => watcher.path === path && !watcher.closed);
51
+ }
52
+ /* The one open watcher on `path`, which a test that emits on it expects to exist. */
53
+ on(path) {
54
+ const [watcher, ...rest] = this.openOn(path);
55
+ if (!watcher || rest.length > 0) {
56
+ throw new Error(`Expected one open watcher on ${path}, found ${rest.length + (watcher ? 1 : 0)}`);
57
+ }
58
+ return watcher;
59
+ }
60
+ async settle() {
61
+ const callbacks = [...this.due.values()];
62
+ this.due.clear();
63
+ await Promise.all(callbacks.map((callback) => callback()));
64
+ }
65
+ }
66
+ /* A sink's events, with a promise for the moment one of them passes a check, so a test awaits the event itself rather than a deadline. */
67
+ export class EventLog {
68
+ events = [];
69
+ waiters = [];
70
+ push = (event) => {
71
+ this.events.push(event);
72
+ const ready = this.waiters.filter((waiter) => waiter.check(this.events));
73
+ this.waiters = this.waiters.filter((waiter) => !ready.includes(waiter));
74
+ for (const waiter of ready) {
75
+ waiter.resolve();
76
+ }
77
+ };
78
+ until(check) {
79
+ if (check(this.events)) {
80
+ return Promise.resolve();
81
+ }
82
+ return new Promise((resolve) => {
83
+ this.waiters.push({ check, resolve });
84
+ });
85
+ }
86
+ }
package/package.json CHANGED
@@ -1,11 +1,50 @@
1
1
  {
2
2
  "name": "@adecore/agents",
3
- "version": "0.0.1",
4
- "description": "Placeholder for the first release.",
3
+ "version": "0.17.0-beta.2",
4
+ "description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
5
+ "type": "module",
6
+ "exports": {
7
+ "./*.json": {
8
+ "source": "./src/*.json",
9
+ "default": "./dist/*.json"
10
+ },
11
+ "./*": {
12
+ "source": "./src/*.ts",
13
+ "types": "./dist/*.d.ts",
14
+ "default": "./dist/*.js"
15
+ }
16
+ },
17
+ "scripts": {
18
+ "typecheck": "tsc --noEmit",
19
+ "build": "bun scripts/build.ts",
20
+ "test": "bun --conditions=source test"
21
+ },
22
+ "dependencies": {
23
+ "@adecore/agent-contracts": "0.17.0-beta.2",
24
+ "zod": "^4.6.5"
25
+ },
26
+ "devDependencies": {
27
+ "@types/bun": "^1.4.2",
28
+ "typescript": "^7.0.2",
29
+ "@types/node": "^26.6.3"
30
+ },
5
31
  "license": "FSL-1.1-MIT",
32
+ "author": "Bas Milius",
6
33
  "repository": {
7
34
  "type": "git",
8
35
  "url": "git+https://github.com/basmilius/adecore.git",
9
36
  "directory": "packages/agents"
10
- }
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/basmilius/adecore/issues"
40
+ },
41
+ "homepage": "https://adecore.dev/agents/",
42
+ "files": [
43
+ "dist",
44
+ "src",
45
+ "!src/**/*.test.ts",
46
+ "!src/**/*.test.tsx",
47
+ "!src/**/__snapshots__"
48
+ ],
49
+ "sideEffects": false
11
50
  }