palaryn 0.1.0 → 0.3.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 (344) hide show
  1. package/README.md +243 -588
  2. package/dist/sdk/typescript/src/client.js +2 -2
  3. package/dist/sdk/typescript/src/client.js.map +1 -1
  4. package/dist/src/anomaly/detector.d.ts +7 -4
  5. package/dist/src/anomaly/detector.d.ts.map +1 -1
  6. package/dist/src/anomaly/detector.js +22 -12
  7. package/dist/src/anomaly/detector.js.map +1 -1
  8. package/dist/src/audit/logger.d.ts +10 -0
  9. package/dist/src/audit/logger.d.ts.map +1 -1
  10. package/dist/src/audit/logger.js +52 -38
  11. package/dist/src/audit/logger.js.map +1 -1
  12. package/dist/src/auth/routes.d.ts.map +1 -1
  13. package/dist/src/auth/routes.js +35 -0
  14. package/dist/src/auth/routes.js.map +1 -1
  15. package/dist/src/budget/manager.d.ts +5 -0
  16. package/dist/src/budget/manager.d.ts.map +1 -1
  17. package/dist/src/budget/manager.js +32 -0
  18. package/dist/src/budget/manager.js.map +1 -1
  19. package/dist/src/budget/model-pricing.d.ts +20 -0
  20. package/dist/src/budget/model-pricing.d.ts.map +1 -0
  21. package/dist/src/budget/model-pricing.js +107 -0
  22. package/dist/src/budget/model-pricing.js.map +1 -0
  23. package/dist/src/budget/usage-extractor.d.ts +3 -1
  24. package/dist/src/budget/usage-extractor.d.ts.map +1 -1
  25. package/dist/src/budget/usage-extractor.js +47 -3
  26. package/dist/src/budget/usage-extractor.js.map +1 -1
  27. package/dist/src/config/defaults.d.ts.map +1 -1
  28. package/dist/src/config/defaults.js +65 -13
  29. package/dist/src/config/defaults.js.map +1 -1
  30. package/dist/src/dlp/tool-patterns.d.ts +7 -0
  31. package/dist/src/dlp/tool-patterns.d.ts.map +1 -0
  32. package/dist/src/dlp/tool-patterns.js +34 -0
  33. package/dist/src/dlp/tool-patterns.js.map +1 -0
  34. package/dist/src/executor/filesystem-executor.d.ts +28 -0
  35. package/dist/src/executor/filesystem-executor.d.ts.map +1 -0
  36. package/dist/src/executor/filesystem-executor.js +192 -0
  37. package/dist/src/executor/filesystem-executor.js.map +1 -0
  38. package/dist/src/executor/http-executor.d.ts.map +1 -1
  39. package/dist/src/executor/http-executor.js +22 -2
  40. package/dist/src/executor/http-executor.js.map +1 -1
  41. package/dist/src/executor/index.d.ts +4 -0
  42. package/dist/src/executor/index.d.ts.map +1 -1
  43. package/dist/src/executor/index.js +9 -1
  44. package/dist/src/executor/index.js.map +1 -1
  45. package/dist/src/executor/shell-executor.d.ts +22 -0
  46. package/dist/src/executor/shell-executor.d.ts.map +1 -0
  47. package/dist/src/executor/shell-executor.js +119 -0
  48. package/dist/src/executor/shell-executor.js.map +1 -0
  49. package/dist/src/executor/sql-executor.d.ts +29 -0
  50. package/dist/src/executor/sql-executor.d.ts.map +1 -0
  51. package/dist/src/executor/sql-executor.js +114 -0
  52. package/dist/src/executor/sql-executor.js.map +1 -0
  53. package/dist/src/executor/websocket-executor.d.ts +26 -0
  54. package/dist/src/executor/websocket-executor.d.ts.map +1 -0
  55. package/dist/src/executor/websocket-executor.js +205 -0
  56. package/dist/src/executor/websocket-executor.js.map +1 -0
  57. package/dist/src/interceptor/index.d.ts +2 -0
  58. package/dist/src/interceptor/index.d.ts.map +1 -0
  59. package/dist/src/interceptor/index.js +6 -0
  60. package/dist/src/interceptor/index.js.map +1 -0
  61. package/dist/src/interceptor/provider-interceptor.d.ts +36 -0
  62. package/dist/src/interceptor/provider-interceptor.d.ts.map +1 -0
  63. package/dist/src/interceptor/provider-interceptor.js +302 -0
  64. package/dist/src/interceptor/provider-interceptor.js.map +1 -0
  65. package/dist/src/mcp/auth-verifier.d.ts.map +1 -1
  66. package/dist/src/mcp/auth-verifier.js +3 -2
  67. package/dist/src/mcp/auth-verifier.js.map +1 -1
  68. package/dist/src/mcp/bridge.d.ts +14 -10
  69. package/dist/src/mcp/bridge.d.ts.map +1 -1
  70. package/dist/src/mcp/bridge.js +51 -227
  71. package/dist/src/mcp/bridge.js.map +1 -1
  72. package/dist/src/mcp/http-transport.d.ts +2 -0
  73. package/dist/src/mcp/http-transport.d.ts.map +1 -1
  74. package/dist/src/mcp/http-transport.js +117 -66
  75. package/dist/src/mcp/http-transport.js.map +1 -1
  76. package/dist/src/mcp/internal-auth.d.ts +13 -0
  77. package/dist/src/mcp/internal-auth.d.ts.map +1 -0
  78. package/dist/src/mcp/internal-auth.js +12 -0
  79. package/dist/src/mcp/internal-auth.js.map +1 -0
  80. package/dist/src/mcp/tool-definitions.d.ts +41 -0
  81. package/dist/src/mcp/tool-definitions.d.ts.map +1 -0
  82. package/dist/src/mcp/tool-definitions.js +491 -0
  83. package/dist/src/mcp/tool-definitions.js.map +1 -0
  84. package/dist/src/middleware/auth.js.map +1 -1
  85. package/dist/src/middleware/session.js.map +1 -1
  86. package/dist/src/middleware/validate.d.ts +8 -0
  87. package/dist/src/middleware/validate.d.ts.map +1 -1
  88. package/dist/src/middleware/validate.js +45 -0
  89. package/dist/src/middleware/validate.js.map +1 -1
  90. package/dist/src/policy/engine.d.ts +4 -0
  91. package/dist/src/policy/engine.d.ts.map +1 -1
  92. package/dist/src/policy/engine.js +117 -0
  93. package/dist/src/policy/engine.js.map +1 -1
  94. package/dist/src/saas/routes.d.ts.map +1 -1
  95. package/dist/src/saas/routes.js +355 -22
  96. package/dist/src/saas/routes.js.map +1 -1
  97. package/dist/src/server/app.d.ts.map +1 -1
  98. package/dist/src/server/app.js +24 -3
  99. package/dist/src/server/app.js.map +1 -1
  100. package/dist/src/server/gateway.d.ts.map +1 -1
  101. package/dist/src/server/gateway.js +17 -0
  102. package/dist/src/server/gateway.js.map +1 -1
  103. package/dist/src/server/index.d.ts.map +1 -1
  104. package/dist/src/server/index.js +18 -0
  105. package/dist/src/server/index.js.map +1 -1
  106. package/dist/src/storage/interfaces.d.ts +14 -3
  107. package/dist/src/storage/interfaces.d.ts.map +1 -1
  108. package/dist/src/storage/memory.d.ts +2 -0
  109. package/dist/src/storage/memory.d.ts.map +1 -1
  110. package/dist/src/storage/memory.js +6 -0
  111. package/dist/src/storage/memory.js.map +1 -1
  112. package/dist/src/storage/postgres.d.ts +5 -0
  113. package/dist/src/storage/postgres.d.ts.map +1 -1
  114. package/dist/src/storage/postgres.js +16 -0
  115. package/dist/src/storage/postgres.js.map +1 -1
  116. package/dist/src/storage/redis.d.ts +10 -0
  117. package/dist/src/storage/redis.d.ts.map +1 -1
  118. package/dist/src/storage/redis.js +65 -0
  119. package/dist/src/storage/redis.js.map +1 -1
  120. package/dist/src/types/budget.d.ts +4 -0
  121. package/dist/src/types/budget.d.ts.map +1 -1
  122. package/dist/src/types/config.d.ts +58 -0
  123. package/dist/src/types/config.d.ts.map +1 -1
  124. package/dist/src/types/events.d.ts +1 -0
  125. package/dist/src/types/events.d.ts.map +1 -1
  126. package/dist/src/types/policy.d.ts +11 -1
  127. package/dist/src/types/policy.d.ts.map +1 -1
  128. package/dist/src/types/tool-result.d.ts +11 -0
  129. package/dist/src/types/tool-result.d.ts.map +1 -1
  130. package/dist/tests/unit/app-routes.test.d.ts +2 -0
  131. package/dist/tests/unit/app-routes.test.d.ts.map +1 -0
  132. package/dist/tests/unit/app-routes.test.js +715 -0
  133. package/dist/tests/unit/app-routes.test.js.map +1 -0
  134. package/dist/tests/unit/audit-logger.test.js +105 -0
  135. package/dist/tests/unit/audit-logger.test.js.map +1 -1
  136. package/dist/tests/unit/auth-providers.test.d.ts +2 -0
  137. package/dist/tests/unit/auth-providers.test.d.ts.map +1 -0
  138. package/dist/tests/unit/auth-providers.test.js +279 -0
  139. package/dist/tests/unit/auth-providers.test.js.map +1 -0
  140. package/dist/tests/unit/auth-routes-extended.test.d.ts +2 -0
  141. package/dist/tests/unit/auth-routes-extended.test.d.ts.map +1 -0
  142. package/dist/tests/unit/auth-routes-extended.test.js +993 -0
  143. package/dist/tests/unit/auth-routes-extended.test.js.map +1 -0
  144. package/dist/tests/unit/auth-verifier.test.d.ts +2 -0
  145. package/dist/tests/unit/auth-verifier.test.d.ts.map +1 -0
  146. package/dist/tests/unit/auth-verifier.test.js +505 -0
  147. package/dist/tests/unit/auth-verifier.test.js.map +1 -0
  148. package/dist/tests/unit/billing-routes.test.d.ts +2 -0
  149. package/dist/tests/unit/billing-routes.test.d.ts.map +1 -0
  150. package/dist/tests/unit/billing-routes.test.js +432 -0
  151. package/dist/tests/unit/billing-routes.test.js.map +1 -0
  152. package/dist/tests/unit/config-defaults.test.d.ts +2 -0
  153. package/dist/tests/unit/config-defaults.test.d.ts.map +1 -0
  154. package/dist/tests/unit/config-defaults.test.js +119 -0
  155. package/dist/tests/unit/config-defaults.test.js.map +1 -0
  156. package/dist/tests/unit/defaults.test.js +0 -10
  157. package/dist/tests/unit/defaults.test.js.map +1 -1
  158. package/dist/tests/unit/filesystem-executor.test.d.ts +2 -0
  159. package/dist/tests/unit/filesystem-executor.test.d.ts.map +1 -0
  160. package/dist/tests/unit/filesystem-executor.test.js +280 -0
  161. package/dist/tests/unit/filesystem-executor.test.js.map +1 -0
  162. package/dist/tests/unit/gateway-branches.test.d.ts +2 -0
  163. package/dist/tests/unit/gateway-branches.test.d.ts.map +1 -0
  164. package/dist/tests/unit/gateway-branches.test.js +1039 -0
  165. package/dist/tests/unit/gateway-branches.test.js.map +1 -0
  166. package/dist/tests/unit/http-executor-branches.test.d.ts +2 -0
  167. package/dist/tests/unit/http-executor-branches.test.d.ts.map +1 -0
  168. package/dist/tests/unit/http-executor-branches.test.js +495 -0
  169. package/dist/tests/unit/http-executor-branches.test.js.map +1 -0
  170. package/dist/tests/unit/logger.test.d.ts +2 -0
  171. package/dist/tests/unit/logger.test.d.ts.map +1 -0
  172. package/dist/tests/unit/logger.test.js +97 -0
  173. package/dist/tests/unit/logger.test.js.map +1 -0
  174. package/dist/tests/unit/mcp-internal-auth.test.d.ts +2 -0
  175. package/dist/tests/unit/mcp-internal-auth.test.d.ts.map +1 -0
  176. package/dist/tests/unit/mcp-internal-auth.test.js +445 -0
  177. package/dist/tests/unit/mcp-internal-auth.test.js.map +1 -0
  178. package/dist/tests/unit/metrics.test.js +102 -0
  179. package/dist/tests/unit/metrics.test.js.map +1 -1
  180. package/dist/tests/unit/model-pricing.test.d.ts +2 -0
  181. package/dist/tests/unit/model-pricing.test.d.ts.map +1 -0
  182. package/dist/tests/unit/model-pricing.test.js +87 -0
  183. package/dist/tests/unit/model-pricing.test.js.map +1 -0
  184. package/dist/tests/unit/oauth-stores.test.d.ts +2 -0
  185. package/dist/tests/unit/oauth-stores.test.d.ts.map +1 -0
  186. package/dist/tests/unit/oauth-stores.test.js +260 -0
  187. package/dist/tests/unit/oauth-stores.test.js.map +1 -0
  188. package/dist/tests/unit/policy-engine.test.js +466 -0
  189. package/dist/tests/unit/policy-engine.test.js.map +1 -1
  190. package/dist/tests/unit/provider-interceptor.test.d.ts +2 -0
  191. package/dist/tests/unit/provider-interceptor.test.d.ts.map +1 -0
  192. package/dist/tests/unit/provider-interceptor.test.js +472 -0
  193. package/dist/tests/unit/provider-interceptor.test.js.map +1 -0
  194. package/dist/tests/unit/saas-routes-branches.test.d.ts +2 -0
  195. package/dist/tests/unit/saas-routes-branches.test.d.ts.map +1 -0
  196. package/dist/tests/unit/saas-routes-branches.test.js +2165 -0
  197. package/dist/tests/unit/saas-routes-branches.test.js.map +1 -0
  198. package/dist/tests/unit/saas-routes-crud.test.d.ts +2 -0
  199. package/dist/tests/unit/saas-routes-crud.test.d.ts.map +1 -0
  200. package/dist/tests/unit/saas-routes-crud.test.js +332 -0
  201. package/dist/tests/unit/saas-routes-crud.test.js.map +1 -0
  202. package/dist/tests/unit/saas-routes-data.test.d.ts +2 -0
  203. package/dist/tests/unit/saas-routes-data.test.d.ts.map +1 -0
  204. package/dist/tests/unit/saas-routes-data.test.js +405 -0
  205. package/dist/tests/unit/saas-routes-data.test.js.map +1 -0
  206. package/dist/tests/unit/saas-routes.test.js +3 -3
  207. package/dist/tests/unit/saas-routes.test.js.map +1 -1
  208. package/dist/tests/unit/shell-executor.test.d.ts +2 -0
  209. package/dist/tests/unit/shell-executor.test.d.ts.map +1 -0
  210. package/dist/tests/unit/shell-executor.test.js +145 -0
  211. package/dist/tests/unit/shell-executor.test.js.map +1 -0
  212. package/dist/tests/unit/sql-executor.test.d.ts +2 -0
  213. package/dist/tests/unit/sql-executor.test.d.ts.map +1 -0
  214. package/dist/tests/unit/sql-executor.test.js +177 -0
  215. package/dist/tests/unit/sql-executor.test.js.map +1 -0
  216. package/dist/tests/unit/stream-proxy.test.d.ts +2 -0
  217. package/dist/tests/unit/stream-proxy.test.d.ts.map +1 -0
  218. package/dist/tests/unit/stream-proxy.test.js +147 -0
  219. package/dist/tests/unit/stream-proxy.test.js.map +1 -0
  220. package/dist/tests/unit/tool-definitions.test.d.ts +2 -0
  221. package/dist/tests/unit/tool-definitions.test.d.ts.map +1 -0
  222. package/dist/tests/unit/tool-definitions.test.js +184 -0
  223. package/dist/tests/unit/tool-definitions.test.js.map +1 -0
  224. package/dist/tests/unit/usage-extractor.test.js +140 -0
  225. package/dist/tests/unit/usage-extractor.test.js.map +1 -1
  226. package/dist/tests/unit/webhook-handler.test.d.ts +2 -0
  227. package/dist/tests/unit/webhook-handler.test.d.ts.map +1 -0
  228. package/dist/tests/unit/webhook-handler.test.js +453 -0
  229. package/dist/tests/unit/webhook-handler.test.js.map +1 -0
  230. package/dist/tests/unit/webhook-routes.test.d.ts +2 -0
  231. package/dist/tests/unit/webhook-routes.test.d.ts.map +1 -0
  232. package/dist/tests/unit/webhook-routes.test.js +69 -0
  233. package/dist/tests/unit/webhook-routes.test.js.map +1 -0
  234. package/dist/tests/unit/websocket-executor.test.d.ts +2 -0
  235. package/dist/tests/unit/websocket-executor.test.d.ts.map +1 -0
  236. package/dist/tests/unit/websocket-executor.test.js +121 -0
  237. package/dist/tests/unit/websocket-executor.test.js.map +1 -0
  238. package/package.json +8 -2
  239. package/policy-packs/demo_fail.yaml +41 -0
  240. package/policy-packs/full_tools.yaml +136 -0
  241. package/src/admin/index.ts +1 -0
  242. package/src/admin/routes.ts +509 -0
  243. package/src/admin/templates.ts +572 -0
  244. package/src/anomaly/detector.ts +730 -0
  245. package/src/anomaly/index.ts +1 -0
  246. package/src/approval/manager.ts +569 -0
  247. package/src/approval/webhook.ts +133 -0
  248. package/src/audit/logger.ts +490 -0
  249. package/src/auth/index.ts +5 -0
  250. package/src/auth/password.ts +21 -0
  251. package/src/auth/pkce.ts +22 -0
  252. package/src/auth/providers.ts +208 -0
  253. package/src/auth/routes.ts +561 -0
  254. package/src/auth/session.ts +84 -0
  255. package/src/billing/index.ts +6 -0
  256. package/src/billing/plan-enforcer.ts +135 -0
  257. package/src/billing/routes.ts +229 -0
  258. package/src/billing/stripe-client.ts +58 -0
  259. package/src/billing/webhook-handler.ts +182 -0
  260. package/src/billing/webhook-routes.ts +28 -0
  261. package/src/budget/manager.ts +679 -0
  262. package/src/budget/model-pricing.ts +119 -0
  263. package/src/budget/usage-extractor.ts +214 -0
  264. package/src/cli.ts +91 -0
  265. package/src/config/defaults.ts +261 -0
  266. package/src/config/validate.ts +88 -0
  267. package/src/dlp/composite-scanner.ts +213 -0
  268. package/src/dlp/index.ts +9 -0
  269. package/src/dlp/interfaces.ts +34 -0
  270. package/src/dlp/patterns.ts +30 -0
  271. package/src/dlp/prompt-injection-backend.ts +181 -0
  272. package/src/dlp/prompt-injection-patterns.ts +302 -0
  273. package/src/dlp/regex-backend.ts +181 -0
  274. package/src/dlp/scanner.ts +502 -0
  275. package/src/dlp/text-normalizer.ts +225 -0
  276. package/src/dlp/tool-patterns.ts +35 -0
  277. package/src/dlp/trufflehog-backend.ts +190 -0
  278. package/src/executor/filesystem-executor.ts +196 -0
  279. package/src/executor/http-executor.ts +349 -0
  280. package/src/executor/index.ts +9 -0
  281. package/src/executor/interfaces.ts +11 -0
  282. package/src/executor/noop-executor.ts +23 -0
  283. package/src/executor/registry.ts +64 -0
  284. package/src/executor/shell-executor.ts +148 -0
  285. package/src/executor/slack-executor.ts +176 -0
  286. package/src/executor/sql-executor.ts +146 -0
  287. package/src/executor/websocket-executor.ts +211 -0
  288. package/src/index.ts +24 -0
  289. package/src/interceptor/index.ts +1 -0
  290. package/src/interceptor/provider-interceptor.ts +315 -0
  291. package/src/mcp/auth-verifier.ts +152 -0
  292. package/src/mcp/bridge.ts +703 -0
  293. package/src/mcp/http-transport.ts +698 -0
  294. package/src/mcp/index.ts +9 -0
  295. package/src/mcp/internal-auth.ts +14 -0
  296. package/src/mcp/oauth-pages.ts +139 -0
  297. package/src/mcp/oauth-postgres-stores.ts +278 -0
  298. package/src/mcp/oauth-provider.ts +536 -0
  299. package/src/mcp/oauth-stores.ts +202 -0
  300. package/src/mcp/server.ts +55 -0
  301. package/src/mcp/tool-definitions.ts +562 -0
  302. package/src/metrics/collector.ts +357 -0
  303. package/src/metrics/index.ts +1 -0
  304. package/src/middleware/auth.ts +814 -0
  305. package/src/middleware/session.ts +85 -0
  306. package/src/middleware/validate.ts +130 -0
  307. package/src/policy/engine.ts +815 -0
  308. package/src/policy/index.ts +2 -0
  309. package/src/policy/opa-engine.ts +829 -0
  310. package/src/proxy/forward-proxy.ts +649 -0
  311. package/src/proxy/index.ts +1 -0
  312. package/src/ratelimit/limiter.ts +196 -0
  313. package/src/replay/engine.ts +142 -0
  314. package/src/replay/index.ts +1 -0
  315. package/src/saas/index.ts +1 -0
  316. package/src/saas/routes.ts +2178 -0
  317. package/src/server/app.ts +985 -0
  318. package/src/server/errors.ts +49 -0
  319. package/src/server/gateway.ts +1130 -0
  320. package/src/server/index.ts +307 -0
  321. package/src/server/logger.ts +255 -0
  322. package/src/server/stream-proxy.ts +202 -0
  323. package/src/storage/file-persistence.ts +315 -0
  324. package/src/storage/index.ts +4 -0
  325. package/src/storage/interfaces.ts +287 -0
  326. package/src/storage/memory.ts +686 -0
  327. package/src/storage/postgres.ts +1831 -0
  328. package/src/storage/redis.ts +835 -0
  329. package/src/tracing/index.ts +1 -0
  330. package/src/tracing/provider.ts +100 -0
  331. package/src/trust/calculator.ts +141 -0
  332. package/src/trust/index.ts +7 -0
  333. package/src/types/budget.ts +36 -0
  334. package/src/types/config.ts +278 -0
  335. package/src/types/events.ts +41 -0
  336. package/src/types/express.d.ts +14 -0
  337. package/src/types/index.ts +7 -0
  338. package/src/types/policy.ts +83 -0
  339. package/src/types/stripe-config.ts +11 -0
  340. package/src/types/subscription.ts +59 -0
  341. package/src/types/tool-call.ts +47 -0
  342. package/src/types/tool-result.ts +82 -0
  343. package/src/types/user.ts +125 -0
  344. package/tsconfig.json +24 -0
@@ -0,0 +1,679 @@
1
+ import { BudgetConfig, BudgetState, CostEstimate } from '../types/budget';
2
+ import { BudgetReport, UsageData } from '../types/tool-result';
3
+ import { BudgetStore, WorkspaceBudgetConfig } from '../storage/interfaces';
4
+ import { ToolCall } from '../types/tool-call';
5
+
6
+ /** Small epsilon for floating-point comparison tolerance in budget checks */
7
+ const BUDGET_EPSILON = 1e-10;
8
+
9
+ // Default cost table for different tool types (used when no config override provided)
10
+ const DEFAULT_COST_TABLE: Record<string, number> = {
11
+ 'http.request': 0.001,
12
+ 'http.request.write': 0.002,
13
+ 'slack.post': 0.003,
14
+ 'git.operation': 0.002,
15
+ 'db.query': 0.005,
16
+ 'browser.navigate': 0.01,
17
+ 'default': 0.001,
18
+ };
19
+
20
+ export interface CostRecord {
21
+ estimated_cost_usd: number;
22
+ actual_cost_usd?: number;
23
+ usage?: UsageData;
24
+ }
25
+
26
+ export class BudgetManager {
27
+ private config: BudgetConfig;
28
+ private costTable: Record<string, number>;
29
+ private externalStore?: BudgetStore;
30
+ // In-memory stores (fallback when no external store)
31
+ private taskStates: Map<string, BudgetState>;
32
+ private userDailySpend: Map<string, number>; // key: "actor_id:date"
33
+ private userMonthlySpend: Map<string, number>; // key: "actor_id:month"
34
+ private workspaceDailySpend: Map<string, number>; // key: "workspace_id:date"
35
+ private workspaceMonthlySpend: Map<string, number>; // key: "workspace_id:month"
36
+ private callRetryCounts: Map<string, number>; // key: tool_call_id
37
+ private costRecords: Map<string, CostRecord>; // key: tool_call_id
38
+ private reservations: Map<string, { amount: number; taskId: string; workspaceId: string; actorId: string; createdAt: number }>;
39
+ /** Promise-based mutex per budget key to serialize concurrent reservations */
40
+ private budgetLocks: Map<string, Promise<void>>;
41
+
42
+ constructor(config: BudgetConfig, store?: BudgetStore) {
43
+ this.config = {
44
+ task_budget_usd: config.task_budget_usd ?? 2.0,
45
+ user_daily_budget_usd: config.user_daily_budget_usd,
46
+ user_monthly_budget_usd: config.user_monthly_budget_usd,
47
+ workspace_daily_budget_usd: config.workspace_daily_budget_usd,
48
+ workspace_monthly_budget_usd: config.workspace_monthly_budget_usd,
49
+ max_steps_per_task: config.max_steps_per_task ?? 50,
50
+ max_retries_per_call: config.max_retries_per_call ?? 3,
51
+ max_wall_clock_ms: config.max_wall_clock_ms ?? 300000,
52
+ token_pricing: config.token_pricing,
53
+ };
54
+
55
+ // Merge config.cost_table over defaults
56
+ this.costTable = { ...DEFAULT_COST_TABLE, ...(config.cost_table || {}) };
57
+
58
+ this.externalStore = store;
59
+ this.taskStates = new Map();
60
+ this.userDailySpend = new Map();
61
+ this.userMonthlySpend = new Map();
62
+ this.workspaceDailySpend = new Map();
63
+ this.workspaceMonthlySpend = new Map();
64
+ this.callRetryCounts = new Map();
65
+ this.costRecords = new Map();
66
+ this.reservations = new Map();
67
+ this.budgetLocks = new Map();
68
+ }
69
+
70
+ /**
71
+ * Populate in-memory maps from the external store.
72
+ * Call once at startup after the store itself has been hydrated.
73
+ */
74
+ hydrateFromStore(): void {
75
+ if (!this.externalStore) return;
76
+
77
+ if (this.externalStore.getAllTaskStates) {
78
+ for (const [id, state] of this.externalStore.getAllTaskStates()) {
79
+ if (!this.taskStates.has(id)) this.taskStates.set(id, state);
80
+ }
81
+ }
82
+
83
+ if (this.externalStore.getAllCounters) {
84
+ for (const [key, value] of this.externalStore.getAllCounters()) {
85
+ if (!this.userDailySpend.has(key)) this.userDailySpend.set(key, value);
86
+ if (!this.userMonthlySpend.has(key)) this.userMonthlySpend.set(key, value);
87
+ if (!this.workspaceDailySpend.has(key)) this.workspaceDailySpend.set(key, value);
88
+ if (!this.workspaceMonthlySpend.has(key)) this.workspaceMonthlySpend.set(key, value);
89
+ }
90
+ }
91
+
92
+ if (this.externalStore.getAllRetryCounts) {
93
+ for (const [id, count] of this.externalStore.getAllRetryCounts()) {
94
+ if (!this.callRetryCounts.has(id)) this.callRetryCounts.set(id, count);
95
+ }
96
+ }
97
+ }
98
+
99
+ // Estimate cost for a tool call
100
+ estimateCost(toolCall: ToolCall): CostEstimate {
101
+ const toolName = toolCall.tool.name;
102
+ const capability = toolCall.tool.capability;
103
+
104
+ // First try an exact match with capability suffix for write/delete operations
105
+ let cost: number | undefined;
106
+
107
+ if (capability === 'write' || capability === 'delete' || capability === 'admin') {
108
+ cost = this.costTable[`${toolName}.${capability}`] ?? this.costTable[`${toolName}.write`];
109
+ }
110
+
111
+ // Fall back to base tool name, then default
112
+ if (cost === undefined) {
113
+ cost = this.costTable[toolName] ?? this.costTable['default'];
114
+ }
115
+
116
+ // Apply a multiplier for destructive or elevated operations
117
+ if (capability === 'delete') {
118
+ cost *= 1.5;
119
+ } else if (capability === 'admin') {
120
+ cost *= 2.0;
121
+ }
122
+
123
+ // Respect per-call max_cost_usd constraint as an upper bound on the estimate
124
+ if (toolCall.constraints?.max_cost_usd !== undefined && toolCall.constraints.max_cost_usd > 0) {
125
+ cost = Math.min(cost, toolCall.constraints.max_cost_usd);
126
+ }
127
+
128
+ return {
129
+ tool_name: toolName,
130
+ capability,
131
+ estimated_cost_usd: cost,
132
+ };
133
+ }
134
+
135
+ // Check if a tool call is within budget (returns allowed: false with reason if not)
136
+ // Optional overrides allow per-workspace budget configuration.
137
+ check(toolCall: ToolCall, overrides?: WorkspaceBudgetConfig): { allowed: boolean; reason?: string; report: BudgetReport } {
138
+ const cfg = overrides ? { ...this.config, ...overrides } : this.config;
139
+ const state = this.getTaskState(toolCall);
140
+ const estimate = this.estimateCost(toolCall);
141
+ const estimatedCost = estimate.estimated_cost_usd;
142
+
143
+ // Check task budget
144
+ const taskBudget = cfg.task_budget_usd!;
145
+ if (state.spent_usd + estimatedCost > taskBudget + BUDGET_EPSILON) {
146
+ return {
147
+ allowed: false,
148
+ reason: `Task budget exceeded: spent $${state.spent_usd.toFixed(4)} + estimated $${estimatedCost.toFixed(4)} > limit $${taskBudget.toFixed(4)}`,
149
+ report: this.getReport(toolCall, estimatedCost, cfg),
150
+ };
151
+ }
152
+
153
+ // Check step limit
154
+ const maxSteps = cfg.max_steps_per_task!;
155
+ if (state.steps + 1 > maxSteps) {
156
+ return {
157
+ allowed: false,
158
+ reason: `Step limit exceeded: ${state.steps} steps taken, max is ${maxSteps}`,
159
+ report: this.getReport(toolCall, estimatedCost, cfg),
160
+ };
161
+ }
162
+
163
+ // Check wall clock time
164
+ const maxWallClock = cfg.max_wall_clock_ms!;
165
+ const elapsed = Date.now() - new Date(state.started_at).getTime();
166
+ if (elapsed > maxWallClock) {
167
+ return {
168
+ allowed: false,
169
+ reason: `Wall clock time exceeded: ${elapsed}ms elapsed, max is ${maxWallClock}ms`,
170
+ report: this.getReport(toolCall, estimatedCost, cfg),
171
+ };
172
+ }
173
+
174
+ // Check user daily budget
175
+ const userDailyLimit = cfg.user_daily_budget_usd;
176
+ if (userDailyLimit !== undefined) {
177
+ const dailyKey = `${state.actor_id}:${this.getDateKey()}`;
178
+ const dailySpend = this.userDailySpend.get(dailyKey) ?? 0;
179
+ if (dailySpend + estimatedCost > userDailyLimit + BUDGET_EPSILON) {
180
+ return {
181
+ allowed: false,
182
+ reason: `User daily budget exceeded: spent $${dailySpend.toFixed(4)} + estimated $${estimatedCost.toFixed(4)} > limit $${userDailyLimit.toFixed(4)}`,
183
+ report: this.getReport(toolCall, estimatedCost, cfg),
184
+ };
185
+ }
186
+ }
187
+
188
+ // Check user monthly budget
189
+ const userMonthlyLimit = cfg.user_monthly_budget_usd;
190
+ if (userMonthlyLimit !== undefined) {
191
+ const monthlyKey = `${state.actor_id}:${this.getMonthKey()}`;
192
+ const monthlySpend = this.userMonthlySpend.get(monthlyKey) ?? 0;
193
+ if (monthlySpend + estimatedCost > userMonthlyLimit + BUDGET_EPSILON) {
194
+ return {
195
+ allowed: false,
196
+ reason: `User monthly budget exceeded: spent $${monthlySpend.toFixed(4)} + estimated $${estimatedCost.toFixed(4)} > limit $${userMonthlyLimit.toFixed(4)}`,
197
+ report: this.getReport(toolCall, estimatedCost, cfg),
198
+ };
199
+ }
200
+ }
201
+
202
+ // Check workspace daily budget
203
+ const wsDailyLimit = cfg.workspace_daily_budget_usd;
204
+ if (wsDailyLimit !== undefined) {
205
+ const dailyKey = `${state.workspace_id}:${this.getDateKey()}`;
206
+ const dailySpend = this.workspaceDailySpend.get(dailyKey) ?? 0;
207
+ if (dailySpend + estimatedCost > wsDailyLimit + BUDGET_EPSILON) {
208
+ return {
209
+ allowed: false,
210
+ reason: `Workspace daily budget exceeded: spent $${dailySpend.toFixed(4)} + estimated $${estimatedCost.toFixed(4)} > limit $${wsDailyLimit.toFixed(4)}`,
211
+ report: this.getReport(toolCall, estimatedCost, cfg),
212
+ };
213
+ }
214
+ }
215
+
216
+ // Check workspace monthly budget
217
+ const wsMonthlyLimit = cfg.workspace_monthly_budget_usd;
218
+ if (wsMonthlyLimit !== undefined) {
219
+ const monthlyKey = `${state.workspace_id}:${this.getMonthKey()}`;
220
+ const monthlySpend = this.workspaceMonthlySpend.get(monthlyKey) ?? 0;
221
+ if (monthlySpend + estimatedCost > wsMonthlyLimit + BUDGET_EPSILON) {
222
+ return {
223
+ allowed: false,
224
+ reason: `Workspace monthly budget exceeded: spent $${monthlySpend.toFixed(4)} + estimated $${estimatedCost.toFixed(4)} > limit $${wsMonthlyLimit.toFixed(4)}`,
225
+ report: this.getReport(toolCall, estimatedCost, cfg),
226
+ };
227
+ }
228
+ }
229
+
230
+ return {
231
+ allowed: true,
232
+ report: this.getReport(toolCall, estimatedCost, cfg),
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Atomically check budget and reserve the estimated cost.
238
+ * The reservation temporarily reduces available budget so concurrent
239
+ * requests cannot both pass the check.
240
+ * Uses a per-task lock to serialize concurrent reservations for the same budget key.
241
+ */
242
+ async reserveAndCheck(toolCall: ToolCall, overrides?: WorkspaceBudgetConfig): Promise<{ allowed: boolean; reason?: string; report: BudgetReport; reservationKey?: string }> {
243
+ // Serialize concurrent reservations for the same task to prevent interleaving
244
+ const lockKey = toolCall.task_id;
245
+ const existingLock = this.budgetLocks.get(lockKey) ?? Promise.resolve();
246
+
247
+ let releaseLock!: () => void;
248
+ const newLock = new Promise<void>(resolve => { releaseLock = resolve; });
249
+ this.budgetLocks.set(lockKey, existingLock.then(() => newLock));
250
+
251
+ // Wait for prior reservation with timeout to prevent deadlocks
252
+ const LOCK_TIMEOUT_MS = 5000;
253
+ let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
254
+ try {
255
+ await Promise.race([
256
+ existingLock,
257
+ new Promise<void>((_, reject) => {
258
+ timeoutHandle = setTimeout(() => reject(new Error('Budget lock timeout')), LOCK_TIMEOUT_MS);
259
+ }),
260
+ ]);
261
+ } catch {
262
+ // On timeout, proceed without lock (accept brief race risk vs permanent hang)
263
+ console.warn(`[budget] Lock timeout for task ${lockKey}, proceeding without lock`);
264
+ } finally {
265
+ if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);
266
+ }
267
+
268
+ try {
269
+ return this._reserveAndCheckSync(toolCall, overrides);
270
+ } finally {
271
+ releaseLock();
272
+ // Clean up lock if no one else is waiting
273
+ if (this.budgetLocks.get(lockKey) === existingLock.then(() => newLock)) {
274
+ this.budgetLocks.delete(lockKey);
275
+ }
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Synchronous check-and-reserve. Must be called while holding the per-task lock.
281
+ */
282
+ private _reserveAndCheckSync(toolCall: ToolCall, overrides?: WorkspaceBudgetConfig): { allowed: boolean; reason?: string; report: BudgetReport; reservationKey?: string } {
283
+ const result = this.check(toolCall, overrides);
284
+ if (!result.allowed) {
285
+ return result;
286
+ }
287
+
288
+ // Reserve the estimated cost
289
+ const estimate = this.estimateCost(toolCall);
290
+ const reservationKey = `reservation:${toolCall.tool_call_id}`;
291
+
292
+ // Temporarily record the estimated cost so concurrent requests see it
293
+ const state = this.getTaskState(toolCall);
294
+ state.spent_usd += estimate.estimated_cost_usd;
295
+ this.taskStates.set(toolCall.task_id, state);
296
+
297
+ // Also reserve against user/workspace counters
298
+ const dateKey = this.getDateKey();
299
+ const monthKey = this.getMonthKey();
300
+
301
+ const userDailyKey = `${state.actor_id}:${dateKey}`;
302
+ this.userDailySpend.set(userDailyKey, (this.userDailySpend.get(userDailyKey) ?? 0) + estimate.estimated_cost_usd);
303
+
304
+ const userMonthlyKey = `${state.actor_id}:${monthKey}`;
305
+ this.userMonthlySpend.set(userMonthlyKey, (this.userMonthlySpend.get(userMonthlyKey) ?? 0) + estimate.estimated_cost_usd);
306
+
307
+ const workspaceDailyKey = `${state.workspace_id}:${dateKey}`;
308
+ this.workspaceDailySpend.set(workspaceDailyKey, (this.workspaceDailySpend.get(workspaceDailyKey) ?? 0) + estimate.estimated_cost_usd);
309
+
310
+ const workspaceMonthlyKey = `${state.workspace_id}:${monthKey}`;
311
+ this.workspaceMonthlySpend.set(workspaceMonthlyKey, (this.workspaceMonthlySpend.get(workspaceMonthlyKey) ?? 0) + estimate.estimated_cost_usd);
312
+
313
+ // Track the reservation so we can commit or release later
314
+ this.reservations.set(reservationKey, {
315
+ amount: estimate.estimated_cost_usd,
316
+ taskId: toolCall.task_id,
317
+ workspaceId: state.workspace_id,
318
+ actorId: state.actor_id,
319
+ createdAt: Date.now(),
320
+ });
321
+
322
+ return { ...result, reservationKey };
323
+ }
324
+
325
+ /**
326
+ * Commit a reservation with the actual cost. Releases the difference
327
+ * between the reserved (estimated) amount and the actual cost.
328
+ */
329
+ commitReservation(reservationKey: string, actualCost: number): void {
330
+ const reservation = this.reservations.get(reservationKey);
331
+ if (!reservation) return;
332
+
333
+ const diff = reservation.amount - actualCost;
334
+ if (diff !== 0) {
335
+ // Adjust task state: remove reserved amount, add actual
336
+ const state = this.taskStates.get(reservation.taskId);
337
+ if (state) {
338
+ state.spent_usd -= diff;
339
+ this.taskStates.set(reservation.taskId, state);
340
+ }
341
+
342
+ // Adjust user/workspace counters
343
+ const dateKey = this.getDateKey();
344
+ const monthKey = this.getMonthKey();
345
+
346
+ const userDailyKey = `${reservation.actorId}:${dateKey}`;
347
+ const curUD = this.userDailySpend.get(userDailyKey);
348
+ if (curUD !== undefined) this.userDailySpend.set(userDailyKey, curUD - diff);
349
+
350
+ const userMonthlyKey = `${reservation.actorId}:${monthKey}`;
351
+ const curUM = this.userMonthlySpend.get(userMonthlyKey);
352
+ if (curUM !== undefined) this.userMonthlySpend.set(userMonthlyKey, curUM - diff);
353
+
354
+ const workspaceDailyKey = `${reservation.workspaceId}:${dateKey}`;
355
+ const curWD = this.workspaceDailySpend.get(workspaceDailyKey);
356
+ if (curWD !== undefined) this.workspaceDailySpend.set(workspaceDailyKey, curWD - diff);
357
+
358
+ const workspaceMonthlyKey = `${reservation.workspaceId}:${monthKey}`;
359
+ const curWM = this.workspaceMonthlySpend.get(workspaceMonthlyKey);
360
+ if (curWM !== undefined) this.workspaceMonthlySpend.set(workspaceMonthlyKey, curWM - diff);
361
+ }
362
+
363
+ this.reservations.delete(reservationKey);
364
+ }
365
+
366
+ /**
367
+ * Release a reservation entirely (e.g. on error before execution completes).
368
+ * Removes the reserved amount from all counters.
369
+ */
370
+ releaseReservation(reservationKey: string): void {
371
+ const reservation = this.reservations.get(reservationKey);
372
+ if (!reservation) return;
373
+
374
+ // Remove the reserved amount from task state
375
+ const state = this.taskStates.get(reservation.taskId);
376
+ if (state) {
377
+ state.spent_usd -= reservation.amount;
378
+ state.steps = Math.max(0, state.steps - 0); // steps not incremented during reserve
379
+ this.taskStates.set(reservation.taskId, state);
380
+ }
381
+
382
+ // Remove from user/workspace counters
383
+ const dateKey = this.getDateKey();
384
+ const monthKey = this.getMonthKey();
385
+
386
+ const userDailyKey = `${reservation.actorId}:${dateKey}`;
387
+ const curUD = this.userDailySpend.get(userDailyKey);
388
+ if (curUD !== undefined) this.userDailySpend.set(userDailyKey, curUD - reservation.amount);
389
+
390
+ const userMonthlyKey = `${reservation.actorId}:${monthKey}`;
391
+ const curUM = this.userMonthlySpend.get(userMonthlyKey);
392
+ if (curUM !== undefined) this.userMonthlySpend.set(userMonthlyKey, curUM - reservation.amount);
393
+
394
+ const workspaceDailyKey = `${reservation.workspaceId}:${dateKey}`;
395
+ const curWD = this.workspaceDailySpend.get(workspaceDailyKey);
396
+ if (curWD !== undefined) this.workspaceDailySpend.set(workspaceDailyKey, curWD - reservation.amount);
397
+
398
+ const workspaceMonthlyKey = `${reservation.workspaceId}:${monthKey}`;
399
+ const curWM = this.workspaceMonthlySpend.get(workspaceMonthlyKey);
400
+ if (curWM !== undefined) this.workspaceMonthlySpend.set(workspaceMonthlyKey, curWM - reservation.amount);
401
+
402
+ this.reservations.delete(reservationKey);
403
+ }
404
+
405
+ /**
406
+ * Record a completed tool call's cost.
407
+ * When skipCostIncrement is true (e.g. because a reservation already accounts
408
+ * for the cost), only step counting and cost record storage are performed.
409
+ */
410
+ record(toolCall: ToolCall, costOrRecord: number | CostRecord, skipCostIncrement = false): void {
411
+ const actualCostUsd = typeof costOrRecord === 'number' ? costOrRecord : (costOrRecord.actual_cost_usd ?? costOrRecord.estimated_cost_usd);
412
+
413
+ // Store cost record for later retrieval
414
+ if (typeof costOrRecord !== 'number') {
415
+ this.costRecords.set(toolCall.tool_call_id, costOrRecord);
416
+ }
417
+
418
+ const state = this.getTaskState(toolCall);
419
+
420
+ if (!skipCostIncrement) {
421
+ // Update task state cost
422
+ state.spent_usd += actualCostUsd;
423
+ }
424
+ // Always increment steps
425
+ state.steps += 1;
426
+ this.taskStates.set(toolCall.task_id, state);
427
+
428
+ // Persist to external store if available
429
+ if (this.externalStore) {
430
+ this.externalStore.setTaskState(toolCall.task_id, state);
431
+ }
432
+
433
+ if (!skipCostIncrement) {
434
+ // Update user daily spend
435
+ const dateKey = this.getDateKey();
436
+ const monthKey = this.getMonthKey();
437
+
438
+ const userDailyKey = `${state.actor_id}:${dateKey}`;
439
+ const currentUserDaily = this.userDailySpend.get(userDailyKey) ?? 0;
440
+ this.userDailySpend.set(userDailyKey, currentUserDaily + actualCostUsd);
441
+
442
+ // Update user monthly spend
443
+ const userMonthlyKey = `${state.actor_id}:${monthKey}`;
444
+ const currentUserMonthly = this.userMonthlySpend.get(userMonthlyKey) ?? 0;
445
+ this.userMonthlySpend.set(userMonthlyKey, currentUserMonthly + actualCostUsd);
446
+
447
+ // Update workspace daily spend
448
+ const workspaceDailyKey = `${state.workspace_id}:${dateKey}`;
449
+ const currentWorkspaceDaily = this.workspaceDailySpend.get(workspaceDailyKey) ?? 0;
450
+ this.workspaceDailySpend.set(workspaceDailyKey, currentWorkspaceDaily + actualCostUsd);
451
+
452
+ // Update workspace monthly spend
453
+ const workspaceMonthlyKey = `${state.workspace_id}:${monthKey}`;
454
+ const currentWorkspaceMonthly = this.workspaceMonthlySpend.get(workspaceMonthlyKey) ?? 0;
455
+ this.workspaceMonthlySpend.set(workspaceMonthlyKey, currentWorkspaceMonthly + actualCostUsd);
456
+
457
+ // Persist counter updates to external store
458
+ if (this.externalStore) {
459
+ this.externalStore.incrementCounter(userDailyKey, actualCostUsd);
460
+ this.externalStore.incrementCounter(userMonthlyKey, actualCostUsd);
461
+ this.externalStore.incrementCounter(workspaceDailyKey, actualCostUsd);
462
+ this.externalStore.incrementCounter(workspaceMonthlyKey, actualCostUsd);
463
+ }
464
+
465
+ // Prune stale counter keys (from prior days/months) to prevent unbounded growth
466
+ this.pruneStaleCounters(dateKey, monthKey);
467
+ }
468
+ }
469
+
470
+ /** Remove counter entries whose date/month suffix doesn't match today */
471
+ private pruneStaleCounters(currentDateKey: string, currentMonthKey: string): void {
472
+ // Only prune periodically (when maps get large)
473
+ if (this.userDailySpend.size <= 100) return;
474
+
475
+ for (const key of this.userDailySpend.keys()) {
476
+ if (!key.endsWith(`:${currentDateKey}`)) {
477
+ this.userDailySpend.delete(key);
478
+ }
479
+ }
480
+ for (const key of this.workspaceDailySpend.keys()) {
481
+ if (!key.endsWith(`:${currentDateKey}`)) {
482
+ this.workspaceDailySpend.delete(key);
483
+ }
484
+ }
485
+ for (const key of this.userMonthlySpend.keys()) {
486
+ if (!key.endsWith(`:${currentMonthKey}`)) {
487
+ this.userMonthlySpend.delete(key);
488
+ }
489
+ }
490
+ for (const key of this.workspaceMonthlySpend.keys()) {
491
+ if (!key.endsWith(`:${currentMonthKey}`)) {
492
+ this.workspaceMonthlySpend.delete(key);
493
+ }
494
+ }
495
+ }
496
+
497
+ // Record a retry for a tool call
498
+ recordRetry(toolCallId: string): { allowed: boolean; count: number } {
499
+ const currentCount = this.callRetryCounts.get(toolCallId) ?? 0;
500
+ const newCount = currentCount + 1;
501
+ this.callRetryCounts.set(toolCallId, newCount);
502
+
503
+ if (this.externalStore) {
504
+ this.externalStore.incrementRetryCount(toolCallId);
505
+ }
506
+
507
+ const maxRetries = this.config.max_retries_per_call!;
508
+
509
+ return {
510
+ allowed: newCount <= maxRetries,
511
+ count: newCount,
512
+ };
513
+ }
514
+
515
+ // Get budget report for a tool call
516
+ getReport(toolCall: ToolCall, estimatedCost: number, effectiveConfig?: BudgetConfig): BudgetReport {
517
+ const cfg = effectiveConfig ?? this.config;
518
+ const state = this.getTaskState(toolCall);
519
+ return {
520
+ estimated_cost_usd: estimatedCost,
521
+ spent_cost_usd_task: state.spent_usd,
522
+ remaining_cost_usd_task: Math.max(0, (cfg.task_budget_usd ?? 2.0) - state.spent_usd),
523
+ };
524
+ }
525
+
526
+ // Get budget report with actual cost and usage data
527
+ getReportWithActual(toolCall: ToolCall, estimatedCost: number, actualCostUsd?: number, usage?: UsageData): BudgetReport {
528
+ const base = this.getReport(toolCall, estimatedCost);
529
+ return {
530
+ ...base,
531
+ actual_cost_usd: actualCostUsd,
532
+ usage,
533
+ };
534
+ }
535
+
536
+ // Get the active cost table (for introspection)
537
+ getCostTable(): Record<string, number> {
538
+ return { ...this.costTable };
539
+ }
540
+
541
+ // Get the cost record for a specific tool call
542
+ getCostRecord(toolCallId: string): CostRecord | undefined {
543
+ return this.costRecords.get(toolCallId);
544
+ }
545
+
546
+ // Get or create task state
547
+ private getTaskState(toolCall: ToolCall): BudgetState {
548
+ const existing = this.taskStates.get(toolCall.task_id);
549
+ if (existing) {
550
+ return existing;
551
+ }
552
+
553
+ const newState: BudgetState = {
554
+ task_id: toolCall.task_id,
555
+ workspace_id: toolCall.workspace_id,
556
+ actor_id: toolCall.actor.id,
557
+ spent_usd: 0,
558
+ steps: 0,
559
+ started_at: toolCall.timestamp ?? new Date().toISOString(),
560
+ };
561
+
562
+ this.taskStates.set(toolCall.task_id, newState);
563
+ return newState;
564
+ }
565
+
566
+ // Helper to get date key (YYYY-MM-DD)
567
+ private getDateKey(): string {
568
+ return new Date().toISOString().split('T')[0];
569
+ }
570
+
571
+ // Helper to get month key (YYYY-MM)
572
+ private getMonthKey(): string {
573
+ return new Date().toISOString().slice(0, 7);
574
+ }
575
+
576
+ // Reset task state (for testing)
577
+ async flush(): Promise<void> {
578
+ if (this.externalStore?.flush) await this.externalStore.flush();
579
+ }
580
+
581
+ reset(): void {
582
+ this.taskStates.clear();
583
+ this.userDailySpend.clear();
584
+ this.userMonthlySpend.clear();
585
+ this.workspaceDailySpend.clear();
586
+ this.workspaceMonthlySpend.clear();
587
+ this.callRetryCounts.clear();
588
+ this.costRecords.clear();
589
+ this.reservations.clear();
590
+ this.budgetLocks.clear();
591
+ this.externalStore?.reset();
592
+ }
593
+
594
+ /** Get spending data for a specific actor */
595
+ getActorSpending(actorId: string, days?: number): {
596
+ daily_spend: number;
597
+ monthly_spend: number;
598
+ task_count: number;
599
+ total_spend: number;
600
+ } {
601
+ const dateKey = this.getDateKey();
602
+ const monthKey = this.getMonthKey();
603
+
604
+ // Daily spend for today
605
+ const dailyKey = `${actorId}:${dateKey}`;
606
+ const dailySpend = this.userDailySpend.get(dailyKey) ?? 0;
607
+
608
+ // Monthly spend for current month
609
+ const monthlyKey = `${actorId}:${monthKey}`;
610
+ const monthlySpend = this.userMonthlySpend.get(monthlyKey) ?? 0;
611
+
612
+ // Count tasks where this actor is involved
613
+ let taskCount = 0;
614
+ let totalSpend = 0;
615
+ for (const state of this.taskStates.values()) {
616
+ if (state.actor_id === actorId) {
617
+ taskCount++;
618
+ totalSpend += state.spent_usd;
619
+ }
620
+ }
621
+
622
+ return {
623
+ daily_spend: dailySpend,
624
+ monthly_spend: monthlySpend,
625
+ task_count: taskCount,
626
+ total_spend: totalSpend,
627
+ };
628
+ }
629
+
630
+ // Get current config
631
+ getConfig(): BudgetConfig {
632
+ return { ...this.config };
633
+ }
634
+
635
+ /** Return aggregate spending across all tracked entities */
636
+ getSpendingSummary(): {
637
+ task_total: number;
638
+ user_daily_total: number;
639
+ user_monthly_total: number;
640
+ workspace_daily_total: number;
641
+ workspace_monthly_total: number;
642
+ } {
643
+ let taskTotal = 0;
644
+ for (const state of this.taskStates.values()) {
645
+ taskTotal += state.spent_usd;
646
+ }
647
+
648
+ const dateKey = this.getDateKey();
649
+ const monthKey = this.getMonthKey();
650
+
651
+ let userDailyTotal = 0;
652
+ for (const [key, val] of this.userDailySpend) {
653
+ if (key.endsWith(`:${dateKey}`)) userDailyTotal += val;
654
+ }
655
+
656
+ let userMonthlyTotal = 0;
657
+ for (const [key, val] of this.userMonthlySpend) {
658
+ if (key.endsWith(`:${monthKey}`)) userMonthlyTotal += val;
659
+ }
660
+
661
+ let workspaceDailyTotal = 0;
662
+ for (const [key, val] of this.workspaceDailySpend) {
663
+ if (key.endsWith(`:${dateKey}`)) workspaceDailyTotal += val;
664
+ }
665
+
666
+ let workspaceMonthlyTotal = 0;
667
+ for (const [key, val] of this.workspaceMonthlySpend) {
668
+ if (key.endsWith(`:${monthKey}`)) workspaceMonthlyTotal += val;
669
+ }
670
+
671
+ return {
672
+ task_total: taskTotal,
673
+ user_daily_total: userDailyTotal,
674
+ user_monthly_total: userMonthlyTotal,
675
+ workspace_daily_total: workspaceDailyTotal,
676
+ workspace_monthly_total: workspaceMonthlyTotal,
677
+ };
678
+ }
679
+ }