@sixb/core 0.1.2 → 0.1.3

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 (264) hide show
  1. package/dist/actions/index.js +16 -13
  2. package/dist/actions/index.js.map +1 -1
  3. package/dist/agents/adapters.d.ts +11 -1
  4. package/dist/agents/adapters.d.ts.map +1 -1
  5. package/dist/agents/context.js +1 -1
  6. package/dist/agents/errors.d.ts +1 -1
  7. package/dist/agents/errors.d.ts.map +1 -1
  8. package/dist/agents/execution.d.ts +3 -0
  9. package/dist/agents/execution.d.ts.map +1 -1
  10. package/dist/agents/index.d.ts +6 -3
  11. package/dist/agents/index.d.ts.map +1 -1
  12. package/dist/agents/index.js +33 -22
  13. package/dist/agents/index.js.map +1 -1
  14. package/dist/agents/prompt.d.ts +8 -2
  15. package/dist/agents/prompt.d.ts.map +1 -1
  16. package/dist/agents/request.d.ts.map +1 -1
  17. package/dist/agents/streams.js +2 -2
  18. package/dist/agents/thread-context-projection.d.ts +19 -0
  19. package/dist/agents/thread-context-projection.d.ts.map +1 -0
  20. package/dist/agents/tool-definition.d.ts.map +1 -1
  21. package/dist/agents/tool-result.d.ts +6 -0
  22. package/dist/agents/tool-result.d.ts.map +1 -0
  23. package/dist/agents/types.d.ts +37 -1
  24. package/dist/agents/types.d.ts.map +1 -1
  25. package/dist/agents/validation.d.ts +1 -1
  26. package/dist/agents/validation.d.ts.map +1 -1
  27. package/dist/auth/index.js +7 -5
  28. package/dist/auth/index.js.map +1 -1
  29. package/dist/authorization/index.js +1 -1
  30. package/dist/bootstrap/index.js +27 -24
  31. package/dist/bootstrap/index.js.map +1 -1
  32. package/dist/{chunk-0mbx247x.js → chunk-177ed2mp.js} +241 -45
  33. package/dist/chunk-177ed2mp.js.map +46 -0
  34. package/dist/{chunk-sa4340y1.js → chunk-1ywq4hzk.js} +7 -7
  35. package/dist/{chunk-zg9yryhz.js → chunk-261ag4t4.js} +3 -3
  36. package/dist/{chunk-k9pts48t.js → chunk-3rsf30p4.js} +5 -5
  37. package/dist/{chunk-8ryen3p3.js → chunk-4aw0jwk0.js} +1 -1
  38. package/dist/{chunk-bh6k3nc6.js → chunk-4p9rh7jj.js} +1 -1
  39. package/dist/{chunk-34bqh420.js → chunk-4z3n1dzt.js} +59 -21
  40. package/dist/{chunk-34bqh420.js.map → chunk-4z3n1dzt.js.map} +4 -4
  41. package/dist/chunk-96swp2wd.js +47 -0
  42. package/dist/chunk-96swp2wd.js.map +10 -0
  43. package/dist/{chunk-n8herkry.js → chunk-bv41fmjn.js} +11 -11
  44. package/dist/{chunk-7pc8b818.js → chunk-c6cfqydk.js} +1 -1
  45. package/dist/{chunk-0k4avqr2.js → chunk-c8bw2b6c.js} +2 -2
  46. package/dist/{chunk-jaj13078.js → chunk-d2adj15j.js} +3 -33
  47. package/dist/{chunk-jaj13078.js.map → chunk-d2adj15j.js.map} +3 -4
  48. package/dist/{chunk-c6qd89g5.js → chunk-d6y2wsfg.js} +3 -3
  49. package/dist/chunk-dysw7nsm.js.map +1 -1
  50. package/dist/{chunk-st4xgxey.js → chunk-ettb0cy4.js} +150 -23
  51. package/dist/chunk-ettb0cy4.js.map +45 -0
  52. package/dist/{chunk-nnyp53zt.js → chunk-f1cyvnrk.js} +1 -1
  53. package/dist/chunk-fmx1sz2h.js.map +2 -2
  54. package/dist/{chunk-bct7hmfc.js → chunk-km4rkj4t.js} +3 -3
  55. package/dist/chunk-kqxgf6kv.js +898 -0
  56. package/dist/chunk-kqxgf6kv.js.map +15 -0
  57. package/dist/{chunk-qvybnnpe.js → chunk-n5x5y80a.js} +2 -2
  58. package/dist/{chunk-4x43bkbp.js → chunk-pxtrxm3p.js} +1 -1
  59. package/dist/chunk-qer6am8w.js +133 -0
  60. package/dist/chunk-qer6am8w.js.map +11 -0
  61. package/dist/{chunk-85j5ybfj.js → chunk-rdeagz6b.js} +2 -2
  62. package/dist/{chunk-85j5ybfj.js.map → chunk-rdeagz6b.js.map} +3 -3
  63. package/dist/chunk-rhrbz7d4.js +36 -0
  64. package/dist/chunk-rhrbz7d4.js.map +10 -0
  65. package/dist/{chunk-663csy96.js → chunk-t0jjkep3.js} +2 -1
  66. package/dist/chunk-t0jjkep3.js.map +10 -0
  67. package/dist/{chunk-5egfka8j.js → chunk-ttba8x6z.js} +2 -2
  68. package/dist/{chunk-nten49ta.js → chunk-wb4051cx.js} +3 -3
  69. package/dist/{chunk-nten49ta.js.map → chunk-wb4051cx.js.map} +1 -1
  70. package/dist/{chunk-57r1cpxx.js → chunk-xc7hq65b.js} +3 -2
  71. package/dist/{chunk-57r1cpxx.js.map → chunk-xc7hq65b.js.map} +3 -3
  72. package/dist/{chunk-ys1nse2v.js → chunk-xmggaxg4.js} +256 -231
  73. package/dist/{chunk-ys1nse2v.js.map → chunk-xmggaxg4.js.map} +8 -10
  74. package/dist/{chunk-g9t3kdtx.js → chunk-z8j83gzd.js} +1 -1
  75. package/dist/{chunk-57zpb99z.js → chunk-zpw39qhj.js} +1 -1
  76. package/dist/execution/agent.js +23 -20
  77. package/dist/execution/agent.js.map +1 -1
  78. package/dist/execution/primitive.js +23 -20
  79. package/dist/execution/primitive.js.map +2 -2
  80. package/dist/execution/request.js +24 -21
  81. package/dist/execution/request.js.map +2 -2
  82. package/dist/index.d.ts +3 -3
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +32 -29
  85. package/dist/index.js.map +1 -1
  86. package/dist/materialization/model.d.ts +4 -0
  87. package/dist/materialization/model.d.ts.map +1 -1
  88. package/dist/materializer/edits/apply.d.ts +6 -1
  89. package/dist/materializer/edits/apply.d.ts.map +1 -1
  90. package/dist/materializer/edits/working-state.d.ts +2 -0
  91. package/dist/materializer/edits/working-state.d.ts.map +1 -1
  92. package/dist/materializer/effective/resolve.d.ts +3 -1
  93. package/dist/materializer/effective/resolve.d.ts.map +1 -1
  94. package/dist/materializer/execution/work-records.d.ts.map +1 -1
  95. package/dist/materializer/index.js +14 -11
  96. package/dist/materializer/index.js.map +1 -1
  97. package/dist/materializer/projections/replacement-plan.d.ts.map +1 -1
  98. package/dist/materializer/shared/normalize.d.ts.map +1 -1
  99. package/dist/objects/index.js +17 -14
  100. package/dist/objects/index.js.map +1 -1
  101. package/dist/objects/query/index.js +2 -2
  102. package/dist/pipelines/index.js +14 -12
  103. package/dist/pipelines/index.js.map +1 -1
  104. package/dist/projections/builders.d.ts +19 -6
  105. package/dist/projections/builders.d.ts.map +1 -1
  106. package/dist/projections/index.d.ts +2 -1
  107. package/dist/projections/index.d.ts.map +1 -1
  108. package/dist/projections/internal.d.ts +4 -1
  109. package/dist/projections/internal.d.ts.map +1 -1
  110. package/dist/projections/internal.js +17 -11
  111. package/dist/projections/internal.js.map +1 -1
  112. package/dist/projections/timestamp.d.ts +3 -0
  113. package/dist/projections/timestamp.d.ts.map +1 -0
  114. package/dist/projections/types.d.ts +9 -0
  115. package/dist/projections/types.d.ts.map +1 -1
  116. package/dist/projections/validation.d.ts.map +1 -1
  117. package/dist/queues/index.d.ts +1 -1
  118. package/dist/queues/index.d.ts.map +1 -1
  119. package/dist/queues/types.d.ts +7 -0
  120. package/dist/queues/types.d.ts.map +1 -1
  121. package/dist/runtime/ai-cost.d.ts +8 -0
  122. package/dist/runtime/ai-cost.d.ts.map +1 -0
  123. package/dist/storage/agents/in-memory.d.ts +18 -4
  124. package/dist/storage/agents/in-memory.d.ts.map +1 -1
  125. package/dist/storage/agents/index.d.ts +1 -1
  126. package/dist/storage/agents/index.d.ts.map +1 -1
  127. package/dist/storage/agents/provider.d.ts +31 -0
  128. package/dist/storage/agents/provider.d.ts.map +1 -1
  129. package/dist/storage/agents/provider.js +18 -7
  130. package/dist/storage/agents/provider.js.map +1 -1
  131. package/dist/storage/agents/types.d.ts +70 -4
  132. package/dist/storage/agents/types.d.ts.map +1 -1
  133. package/dist/storage/ai-cost/analytics.d.ts +61 -0
  134. package/dist/storage/ai-cost/analytics.d.ts.map +1 -0
  135. package/dist/storage/ai-cost/codec.d.ts +14 -0
  136. package/dist/storage/ai-cost/codec.d.ts.map +1 -0
  137. package/dist/storage/ai-cost/errors.d.ts +7 -0
  138. package/dist/storage/ai-cost/errors.d.ts.map +1 -0
  139. package/dist/storage/ai-cost/in-memory.d.ts +32 -0
  140. package/dist/storage/ai-cost/in-memory.d.ts.map +1 -0
  141. package/dist/storage/ai-cost/index.d.ts +6 -0
  142. package/dist/storage/ai-cost/index.d.ts.map +1 -0
  143. package/dist/storage/ai-cost/provider.d.ts +7 -0
  144. package/dist/storage/ai-cost/provider.d.ts.map +1 -0
  145. package/dist/storage/ai-cost/provider.js +132 -0
  146. package/dist/storage/ai-cost/provider.js.map +10 -0
  147. package/dist/storage/ai-cost/types.d.ts +154 -0
  148. package/dist/storage/ai-cost/types.d.ts.map +1 -0
  149. package/dist/storage/ai-cost/validation.d.ts +7 -0
  150. package/dist/storage/ai-cost/validation.d.ts.map +1 -0
  151. package/dist/storage/in-memory/index.d.ts +5 -0
  152. package/dist/storage/in-memory/index.d.ts.map +1 -1
  153. package/dist/storage/index.d.ts +3 -1
  154. package/dist/storage/index.d.ts.map +1 -1
  155. package/dist/storage/index.js +22 -15
  156. package/dist/storage/index.js.map +1 -1
  157. package/dist/storage/ontology/in-memory/materializations.d.ts.map +1 -1
  158. package/dist/storage/ontology/materializations.d.ts +3 -0
  159. package/dist/storage/ontology/materializations.d.ts.map +1 -1
  160. package/dist/storage/operation-scope.d.ts.map +1 -1
  161. package/dist/storage/operation-scope.js +1 -1
  162. package/dist/storage/types.d.ts +2 -0
  163. package/dist/storage/types.d.ts.map +1 -1
  164. package/dist/storage/workflow-runs/provider.js +6 -5
  165. package/dist/storage/workflow-runs/provider.js.map +1 -1
  166. package/dist/syncs/index.js +14 -12
  167. package/dist/syncs/index.js.map +1 -1
  168. package/dist/testing/agent-storage-contract.d.ts +3 -2
  169. package/dist/testing/agent-storage-contract.d.ts.map +1 -1
  170. package/dist/testing/ai-cost-storage-contract.d.ts +12 -0
  171. package/dist/testing/ai-cost-storage-contract.d.ts.map +1 -0
  172. package/dist/testing/index.d.ts +1 -0
  173. package/dist/testing/index.d.ts.map +1 -1
  174. package/dist/testing/index.js +2098 -1468
  175. package/dist/testing/index.js.map +6 -5
  176. package/dist/testing/materializer-storage-contract.d.ts.map +1 -1
  177. package/dist/webhooks/internal.js +12 -10
  178. package/dist/webhooks/internal.js.map +2 -2
  179. package/dist/workers/index.js +12 -2
  180. package/dist/workers/index.js.map +3 -3
  181. package/dist/workers/queue-worker.d.ts +2 -0
  182. package/dist/workers/queue-worker.d.ts.map +1 -1
  183. package/dist/workflows/execution.d.ts +3 -0
  184. package/dist/workflows/execution.d.ts.map +1 -1
  185. package/dist/workflows/index.js +24 -21
  186. package/dist/workflows/index.js.map +1 -1
  187. package/package.json +7 -1
  188. package/src/agents/adapters.ts +64 -23
  189. package/src/agents/errors.ts +2 -2
  190. package/src/agents/execution.ts +14 -2
  191. package/src/agents/index.ts +18 -1
  192. package/src/agents/prompt.ts +33 -5
  193. package/src/agents/request.ts +5 -0
  194. package/src/agents/thread-context-projection.ts +93 -0
  195. package/src/agents/tool-definition.ts +17 -2
  196. package/src/agents/tool-result.ts +61 -0
  197. package/src/agents/types.ts +47 -1
  198. package/src/agents/validation.ts +1 -1
  199. package/src/index.ts +13 -0
  200. package/src/materialization/model.ts +4 -0
  201. package/src/materializer/edits/apply.ts +55 -2
  202. package/src/materializer/edits/operations.ts +8 -1
  203. package/src/materializer/edits/working-state.ts +7 -0
  204. package/src/materializer/effective/resolve.ts +78 -9
  205. package/src/materializer/execution/work-records.ts +3 -1
  206. package/src/materializer/projections/entry-validator.ts +23 -1
  207. package/src/materializer/projections/replacement-plan.ts +2 -0
  208. package/src/materializer/shared/normalize.ts +16 -0
  209. package/src/projections/builders.ts +100 -35
  210. package/src/projections/index.ts +6 -0
  211. package/src/projections/internal.ts +4 -1
  212. package/src/projections/registry.ts +7 -0
  213. package/src/projections/revision.ts +4 -0
  214. package/src/projections/timestamp.ts +66 -0
  215. package/src/projections/types.ts +7 -0
  216. package/src/projections/validation.ts +24 -0
  217. package/src/queues/index.ts +1 -0
  218. package/src/queues/types.ts +8 -0
  219. package/src/runtime/ai-cost.ts +15 -0
  220. package/src/storage/agents/in-memory.ts +171 -4
  221. package/src/storage/agents/index.ts +5 -0
  222. package/src/storage/agents/provider.ts +225 -0
  223. package/src/storage/agents/types.ts +77 -3
  224. package/src/storage/ai-cost/analytics.ts +602 -0
  225. package/src/storage/ai-cost/codec.ts +136 -0
  226. package/src/storage/ai-cost/errors.ts +12 -0
  227. package/src/storage/ai-cost/in-memory.ts +164 -0
  228. package/src/storage/ai-cost/index.ts +35 -0
  229. package/src/storage/ai-cost/provider.ts +20 -0
  230. package/src/storage/ai-cost/types.ts +193 -0
  231. package/src/storage/ai-cost/validation.ts +337 -0
  232. package/src/storage/in-memory/index.ts +50 -0
  233. package/src/storage/index.ts +37 -0
  234. package/src/storage/ontology/in-memory/materializations.ts +1 -0
  235. package/src/storage/ontology/materializations.ts +3 -0
  236. package/src/storage/operation-scope.ts +1 -0
  237. package/src/storage/types.ts +2 -0
  238. package/src/testing/agent-storage-contract.ts +341 -2
  239. package/src/testing/ai-cost-storage-contract.ts +320 -0
  240. package/src/testing/index.ts +5 -0
  241. package/src/testing/materializer-storage-contract.ts +120 -2
  242. package/src/workers/queue-worker.ts +14 -1
  243. package/src/workflows/execution.ts +31 -10
  244. package/dist/chunk-0mbx247x.js.map +0 -43
  245. package/dist/chunk-663csy96.js.map +0 -10
  246. package/dist/chunk-st4xgxey.js.map +0 -45
  247. package/dist/chunk-vp1vc6ee.js +0 -35
  248. package/dist/chunk-vp1vc6ee.js.map +0 -11
  249. /package/dist/{chunk-sa4340y1.js.map → chunk-1ywq4hzk.js.map} +0 -0
  250. /package/dist/{chunk-zg9yryhz.js.map → chunk-261ag4t4.js.map} +0 -0
  251. /package/dist/{chunk-k9pts48t.js.map → chunk-3rsf30p4.js.map} +0 -0
  252. /package/dist/{chunk-8ryen3p3.js.map → chunk-4aw0jwk0.js.map} +0 -0
  253. /package/dist/{chunk-bh6k3nc6.js.map → chunk-4p9rh7jj.js.map} +0 -0
  254. /package/dist/{chunk-n8herkry.js.map → chunk-bv41fmjn.js.map} +0 -0
  255. /package/dist/{chunk-7pc8b818.js.map → chunk-c6cfqydk.js.map} +0 -0
  256. /package/dist/{chunk-0k4avqr2.js.map → chunk-c8bw2b6c.js.map} +0 -0
  257. /package/dist/{chunk-c6qd89g5.js.map → chunk-d6y2wsfg.js.map} +0 -0
  258. /package/dist/{chunk-nnyp53zt.js.map → chunk-f1cyvnrk.js.map} +0 -0
  259. /package/dist/{chunk-bct7hmfc.js.map → chunk-km4rkj4t.js.map} +0 -0
  260. /package/dist/{chunk-qvybnnpe.js.map → chunk-n5x5y80a.js.map} +0 -0
  261. /package/dist/{chunk-4x43bkbp.js.map → chunk-pxtrxm3p.js.map} +0 -0
  262. /package/dist/{chunk-5egfka8j.js.map → chunk-ttba8x6z.js.map} +0 -0
  263. /package/dist/{chunk-g9t3kdtx.js.map → chunk-z8j83gzd.js.map} +0 -0
  264. /package/dist/{chunk-57zpb99z.js.map → chunk-zpw39qhj.js.map} +0 -0
@@ -0,0 +1,320 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import type { AiCostStorage, AiModelCallCostRecord } from "../storage/ai-cost"
3
+ import { AiCostStorageError } from "../storage/ai-cost"
4
+ import type { AiUsageStorage } from "../storage/ai-usage"
5
+ import type { ExecutionStorage } from "../storage/executions"
6
+
7
+ export interface AiStorageContractSuiteOptions<TStorage> {
8
+ readonly createStorage: () => TStorage | Promise<TStorage>
9
+ readonly setup?: (storage: TStorage) => void | Promise<void>
10
+ readonly cleanup?: (storage: TStorage) => void | Promise<void>
11
+ }
12
+
13
+ const projectId = "cost-contract-project"
14
+ const executionId = "exec_cost_1"
15
+ const otherExecutionId = "exec_cost_2"
16
+
17
+ function priceSource(entry: string) {
18
+ return {
19
+ sourceId: "test-catalog",
20
+ sourceEntryId: entry,
21
+ sourceVersion: "test-catalog-v1",
22
+ sourceUrl: "https://example.test/ai-pricing.json",
23
+ observedAt: new Date("2026-08-01T00:00:00.000Z"),
24
+ }
25
+ }
26
+
27
+ function ratedCost(
28
+ usageRecordId = "usage_1",
29
+ overrides: Partial<Extract<AiModelCallCostRecord, { status: "rated" }>> = {}
30
+ ): Extract<AiModelCallCostRecord, { status: "rated" }> {
31
+ return {
32
+ projectId,
33
+ usageRecordId,
34
+ status: "rated",
35
+ billingIdentity: { providerId: "vercel", modelId: "openai/gpt-5" },
36
+ pricingContext: {},
37
+ priceSource: priceSource("vercel/openai/gpt-5"),
38
+ money: { currency: "USD", amountNanos: "95000" },
39
+ components: [
40
+ {
41
+ meter: "tokens.input.uncached",
42
+ quantity: "12",
43
+ rateAmountNanosPerMillion: "1250000000",
44
+ chargeAmountNanos: "15000",
45
+ },
46
+ {
47
+ meter: "tokens.input.cacheRead",
48
+ quantity: "0",
49
+ rateAmountNanosPerMillion: "125000000",
50
+ chargeAmountNanos: "0",
51
+ },
52
+ {
53
+ meter: "tokens.output.total",
54
+ quantity: "8",
55
+ rateAmountNanosPerMillion: "10000000000",
56
+ chargeAmountNanos: "80000",
57
+ },
58
+ ],
59
+ ratedAt: new Date("2026-08-01T12:00:00.200Z"),
60
+ ...overrides,
61
+ }
62
+ }
63
+
64
+ function unpriceableCost(): Extract<AiModelCallCostRecord, { status: "unpriceable" }> {
65
+ return {
66
+ projectId,
67
+ usageRecordId: "usage_2",
68
+ status: "unpriceable",
69
+ billingIdentity: { providerId: "vercel", modelId: "unpriced/model" },
70
+ pricingContext: {},
71
+ priceSource: priceSource("vercel/unpriced/model"),
72
+ reason: "missingCatalogEntry",
73
+ ratedAt: new Date("2026-08-01T12:00:00.200Z"),
74
+ }
75
+ }
76
+
77
+ export function runAiCostStorageContractSuite<TStorage extends AiCostStorage>(
78
+ name: string,
79
+ options: AiStorageContractSuiteOptions<TStorage>
80
+ ): void {
81
+ describe(name, () => {
82
+ async function fixture(): Promise<TStorage> {
83
+ const storage = await options.createStorage()
84
+ await options.setup?.(storage)
85
+ return storage
86
+ }
87
+
88
+ test("records model-call costs idempotently", async () => {
89
+ const storage = await fixture()
90
+ try {
91
+ await expect(storage.recordModelCallCost(ratedCost())).resolves.toBeUndefined()
92
+ await expect(storage.recordModelCallCost(ratedCost())).resolves.toBeUndefined()
93
+ await expect(
94
+ storage.listModelCalls({
95
+ projectId,
96
+ from: new Date("2026-08-01T00:00:00.000Z"),
97
+ to: new Date("2026-08-02T00:00:00.000Z"),
98
+ valuationStatus: "rated",
99
+ })
100
+ ).resolves.toMatchObject({
101
+ total: 1,
102
+ items: [{ usage: { id: "usage_1" }, cost: ratedCost(), valuationStatus: "rated" }],
103
+ })
104
+ } finally {
105
+ await options.cleanup?.(storage)
106
+ }
107
+ })
108
+
109
+ test("stores explicit unpriceable results and pricing context", async () => {
110
+ const storage = await fixture()
111
+ try {
112
+ const deploymentCost: AiModelCallCostRecord = {
113
+ projectId,
114
+ usageRecordId: "usage_deployment",
115
+ status: "unpriceable",
116
+ billingIdentity: { providerId: "openai", modelId: "gpt-5" },
117
+ pricingContext: { deploymentId: "production" },
118
+ priceSource: priceSource("openai/gpt-5"),
119
+ reason: "unsupportedPricingDimension",
120
+ ratedAt: new Date("2026-08-03T12:00:00.200Z"),
121
+ }
122
+ await storage.recordModelCallCost(unpriceableCost())
123
+ await storage.recordModelCallCost(deploymentCost)
124
+ await expect(
125
+ storage.listModelCalls({
126
+ projectId,
127
+ from: new Date("2026-08-03T00:00:00.000Z"),
128
+ to: new Date("2026-08-04T00:00:00.000Z"),
129
+ valuationStatus: "unpriceable",
130
+ })
131
+ ).resolves.toMatchObject({
132
+ total: 1,
133
+ items: [{ cost: deploymentCost, valuationStatus: "unpriceable" }],
134
+ })
135
+ } finally {
136
+ await options.cleanup?.(storage)
137
+ }
138
+ })
139
+
140
+ test("summarizes and queries accounting without inventing missing costs", async () => {
141
+ const storage = await fixture()
142
+ try {
143
+ await storage.recordModelCallCost(ratedCost())
144
+ await storage.recordModelCallCost(unpriceableCost())
145
+ await expect(
146
+ storage.summarizeExecutions({ projectId, executionIds: [executionId] })
147
+ ).resolves.toEqual([
148
+ {
149
+ amounts: [{ currency: "USD", amountNanos: "95000" }],
150
+ ratedCallCount: 1,
151
+ unpriceableCallCount: 1,
152
+ unvaluedCallCount: 1,
153
+ },
154
+ ])
155
+
156
+ const overview = await storage.queryProjectOverview({
157
+ projectId,
158
+ from: new Date("2026-08-01T00:00:00.000Z"),
159
+ to: new Date("2026-08-02T00:00:00.000Z"),
160
+ bucket: "day",
161
+ })
162
+ expect(overview.totals).toEqual({
163
+ modelCallCount: 3,
164
+ usage: {
165
+ inputTokens: 15,
166
+ outputTokens: 12,
167
+ totalTokens: 27,
168
+ reportingStatus: "complete",
169
+ },
170
+ costs: {
171
+ amounts: [{ currency: "USD", amountNanos: "95000" }],
172
+ ratedCallCount: 1,
173
+ unpriceableCallCount: 1,
174
+ unvaluedCallCount: 1,
175
+ },
176
+ })
177
+ expect(overview.series).toMatchObject([{ modelCallCount: 3 }])
178
+ expect(overview.models).toMatchObject([
179
+ { providerId: "gateway", modelId: "openai/gpt-5", modelCallCount: 2 },
180
+ { providerId: "gateway", modelId: "unpriced/model", modelCallCount: 1 },
181
+ ])
182
+ await expect(
183
+ storage.queryProjectOverview({
184
+ projectId,
185
+ from: new Date("2026-08-01T00:00:00.000Z"),
186
+ to: new Date("2026-08-02T00:00:00.000Z"),
187
+ bucket: "day",
188
+ providerId: "gateway",
189
+ modelId: "unpriced/model",
190
+ })
191
+ ).resolves.toMatchObject({ totals: { modelCallCount: 1 } })
192
+ await expect(
193
+ storage.listModelCalls({
194
+ projectId,
195
+ from: new Date("2026-08-01T00:00:00.000Z"),
196
+ to: new Date("2026-08-02T00:00:00.000Z"),
197
+ limit: 1,
198
+ offset: 1,
199
+ })
200
+ ).resolves.toMatchObject({
201
+ total: 3,
202
+ hasMore: true,
203
+ items: [{ usage: { id: "usage_2" } }],
204
+ })
205
+ await expect(
206
+ storage.listModelCalls({
207
+ projectId,
208
+ from: new Date("2026-08-01T00:00:00.000Z"),
209
+ to: new Date("2026-08-02T00:00:00.000Z"),
210
+ valuationStatus: "unpriceable",
211
+ })
212
+ ).resolves.toMatchObject({
213
+ total: 1,
214
+ hasMore: false,
215
+ items: [{ usage: { id: "usage_2" }, valuationStatus: "unpriceable" }],
216
+ })
217
+ } finally {
218
+ await options.cleanup?.(storage)
219
+ }
220
+ })
221
+
222
+ test("rejects missing usage and mismatched billed quantities", async () => {
223
+ const storage = await fixture()
224
+ try {
225
+ const missing = ratedCost("missing")
226
+ const missingError = await storage.recordModelCallCost(missing).catch((value) => value)
227
+ expect(missingError).toBeInstanceOf(AiCostStorageError)
228
+ expect((missingError as AiCostStorageError).code).toBe("missing_usage")
229
+
230
+ const base = ratedCost()
231
+ const mismatch = ratedCost("usage_1", {
232
+ money: { currency: "USD", amountNanos: "96250" },
233
+ components: [
234
+ { ...base.components[0]!, quantity: "13", chargeAmountNanos: "16250" },
235
+ ...base.components.slice(1),
236
+ ],
237
+ })
238
+ const mismatchError = await storage.recordModelCallCost(mismatch).catch((value) => value)
239
+ expect(mismatchError).toBeInstanceOf(AiCostStorageError)
240
+ expect((mismatchError as AiCostStorageError).code).toBe("cost_mismatch")
241
+ } finally {
242
+ await options.cleanup?.(storage)
243
+ }
244
+ })
245
+ })
246
+ }
247
+
248
+ /** Seed immutable usage used by every cost-storage provider contract. */
249
+ export async function seedAiCostStorageContractUsage(
250
+ executions: ExecutionStorage,
251
+ usage: AiUsageStorage
252
+ ): Promise<void> {
253
+ for (const fixture of [
254
+ { projectId, executionId },
255
+ { projectId, executionId: otherExecutionId },
256
+ ]) {
257
+ if (await executions.getById({ projectId: fixture.projectId, id: fixture.executionId }))
258
+ continue
259
+ const primitive = {
260
+ kind: "workflow" as const,
261
+ id: "ai-cost-contract",
262
+ runId: fixture.executionId,
263
+ }
264
+ await executions.create({
265
+ id: fixture.executionId,
266
+ projectId: fixture.projectId,
267
+ executor: { type: "primitive", kind: primitive.kind, runId: primitive.runId },
268
+ source: { type: "event", eventId: `test_event:${fixture.executionId}` },
269
+ correlationId: `test_correlation:${fixture.executionId}`,
270
+ authorizationRef: { type: "trustedPrimitive", primitive },
271
+ })
272
+ }
273
+
274
+ for (const fixture of [
275
+ { id: "usage_1", model: "openai/gpt-5", input: 12, output: 8 },
276
+ { id: "usage_2", model: "unpriced/model", input: 1, output: 1 },
277
+ { id: "usage_3", model: "openai/gpt-5", input: 2, output: 3 },
278
+ ]) {
279
+ await usage.recordModelCall({
280
+ id: fixture.id,
281
+ projectId,
282
+ executionId,
283
+ attempt: 1,
284
+ callId: `call_${fixture.id}`,
285
+ requesterGroupIds: [],
286
+ providerId: "gateway",
287
+ requestedModelId: fixture.model,
288
+ responseId: `response_${projectId}_${fixture.id}`,
289
+ usage: {
290
+ inputTokens: fixture.input,
291
+ outputTokens: fixture.output,
292
+ ...(fixture.id === "usage_1"
293
+ ? { uncachedInputTokens: fixture.input, cacheReadInputTokens: 0 }
294
+ : {}),
295
+ },
296
+ occurredAt: new Date("2026-08-01T12:00:00.000Z"),
297
+ recordedAt: new Date("2026-08-01T12:00:00.100Z"),
298
+ })
299
+ }
300
+
301
+ await usage.recordModelCall({
302
+ id: "usage_deployment",
303
+ projectId,
304
+ executionId: otherExecutionId,
305
+ attempt: 1,
306
+ callId: "call_usage_deployment",
307
+ requesterGroupIds: [],
308
+ providerId: "openai.responses",
309
+ requestedModelId: "gpt-5",
310
+ responseId: "response_usage_deployment",
311
+ usage: {
312
+ inputTokens: 2,
313
+ outputTokens: 3,
314
+ uncachedInputTokens: 2,
315
+ cacheReadInputTokens: 0,
316
+ },
317
+ occurredAt: new Date("2026-08-03T12:00:00.000Z"),
318
+ recordedAt: new Date("2026-08-03T12:00:00.100Z"),
319
+ })
320
+ }
@@ -7,6 +7,11 @@ export {
7
7
  type AgentStorageContractSuiteOptions,
8
8
  runAgentStorageContractSuite,
9
9
  } from "./agent-storage-contract"
10
+ export {
11
+ type AiStorageContractSuiteOptions,
12
+ runAiCostStorageContractSuite,
13
+ seedAiCostStorageContractUsage,
14
+ } from "./ai-cost-storage-contract"
10
15
  export {
11
16
  type AiUsageStorageContractSuiteOptions,
12
17
  runAiUsageStorageContractSuite,
@@ -55,6 +55,23 @@ const devices = defineDataset("storage_contract_devices", {
55
55
  const readings = defineDataset("storage_contract_readings", {
56
56
  schema: [col("device_id", "string"), col("at", "timestamp"), col("value", "float64")],
57
57
  })
58
+ const ConflictDevice = defineObjectType({
59
+ id: "StorageContractConflictDevice",
60
+ name: "Conflict device",
61
+ properties: [
62
+ prop("id", "string", { primary: true, required: true }),
63
+ prop("name", "string", { required: true }),
64
+ prop("state", "string", { required: true }),
65
+ ],
66
+ })
67
+ const conflictDevices = defineDataset("storage_contract_conflict_devices", {
68
+ schema: [
69
+ col("id", "string"),
70
+ col("name", "string"),
71
+ col("state", "string"),
72
+ col("updated_at", "timestamp"),
73
+ ],
74
+ })
58
75
  const deviceProjection = defineProjection("storage_contract_devices", Device)
59
76
  .fromDataset(devices)
60
77
  .properties({ id: "id", name: "name" })
@@ -71,14 +88,22 @@ const temperatureProjection = defineProjection(
71
88
  )
72
89
  .fromDataset(readings)
73
90
  .points({ objectId: "device_id", at: "at", value: "value" })
91
+ const conflictDeviceProjection = defineProjection(
92
+ "storage_contract_conflict_devices",
93
+ ConflictDevice
94
+ )
95
+ .fromDataset(conflictDevices)
96
+ .properties({ id: "id", name: "name", state: "state" })
97
+ .resolveConflicts({ strategy: "mostRecent", sourceTimestamp: "updated_at" })
74
98
 
75
- const ontology = new OntologyRegistry({ sources: [Device] })
99
+ const ontology = new OntologyRegistry({ sources: [Device, ConflictDevice] })
76
100
  const projections = new ProjectionRegistry({
77
- projections: [deviceProjection, temperatureProjection],
101
+ projections: [deviceProjection, temperatureProjection, conflictDeviceProjection],
78
102
  ontology,
79
103
  datasetsById: new Map<string, DatasetDefinition>([
80
104
  [devices.id, devices],
81
105
  [readings.id, readings],
106
+ [conflictDevices.id, conflictDevices],
82
107
  ]),
83
108
  })
84
109
 
@@ -339,6 +364,99 @@ export function runMaterializerStorageContractSuite<TStorage extends Storage>(
339
364
  }
340
365
  })
341
366
 
367
+ test(`${name} persists per-property Action edit times`, async () => {
368
+ const createdStorage = await provider.createStorage()
369
+ const storage = requireContractStorage(createdStorage)
370
+ let materializationOrdinal = 0
371
+ let now = new Date("2026-02-01T00:00:10.000Z")
372
+ const materializer = createOntologyMaterializer({
373
+ projectId: "materializer-storage-contract",
374
+ ontology,
375
+ projections,
376
+ storage,
377
+ dependencies: {
378
+ batching: { sourceStageRows: 1, statePageRows: 1, planChunkRows: 1 },
379
+ clock: () => now,
380
+ materializationId: () => `conflict-contract-candidate-${++materializationOrdinal}`,
381
+ },
382
+ })
383
+ const runtimeMaterializer = materializer.withScope(runtimeScope())
384
+ const ref = { objectTypeId: ConflictDevice.id, primaryId: "one" }
385
+ const replace = async (
386
+ versionId: string,
387
+ sourceUpdatedAt: string,
388
+ name: string,
389
+ state: string
390
+ ) => {
391
+ const datasetVersion = {
392
+ datasetId: conflictDevices.id,
393
+ versionId,
394
+ createdAt: sourceUpdatedAt,
395
+ }
396
+ const execution = await claim(storage, {
397
+ runId: `conflict-${versionId}`,
398
+ projectionId: conflictDeviceProjection.id,
399
+ protocol: "replacement",
400
+ datasetVersion,
401
+ })
402
+ const projectionMaterializerForRun = await projectionMaterializer(
403
+ materializer,
404
+ storage,
405
+ conflictDeviceProjection.id,
406
+ `conflict-${versionId}`
407
+ )
408
+ await projectionMaterializerForRun.projections.replace({
409
+ source: { projectionId: conflictDeviceProjection.id },
410
+ datasetVersion,
411
+ execution,
412
+ entries: entries([
413
+ {
414
+ root: { kind: "object", ref },
415
+ assertions: [{ kind: "object", ref, properties: { name, state }, sourceUpdatedAt }],
416
+ },
417
+ ]),
418
+ })
419
+ }
420
+ const edit = (requestId: string, propertyId: "name" | "state", value: string) =>
421
+ runtimeMaterializer.edits.commit({
422
+ mode: "atomic",
423
+ source: { kind: "runtime", requestId },
424
+ operations: [
425
+ {
426
+ id: requestId,
427
+ kind: "object.patch",
428
+ ref,
429
+ set: { [propertyId]: value },
430
+ unset: [],
431
+ reset: [],
432
+ },
433
+ ],
434
+ expectedObjects: [],
435
+ expectedLinks: [],
436
+ expectedLinkScopes: [],
437
+ })
438
+
439
+ try {
440
+ await replace("v1", "2026-02-01T00:00:10.000Z", "Source A", "source-open")
441
+
442
+ now = new Date("2026-02-01T00:00:11.000Z")
443
+ await edit("conflict-name", "name", "Action B")
444
+ now = new Date("2026-02-01T00:00:13.000Z")
445
+ await edit("conflict-state", "state", "action-closed")
446
+
447
+ await replace("v2", "2026-02-01T00:00:12.000Z", "Source C", "source-changed")
448
+ expect(
449
+ await storage.objects.getByPrimaryId({
450
+ projectId: "materializer-storage-contract",
451
+ objectTypeId: ConflictDevice.id,
452
+ primaryId: "one",
453
+ })
454
+ ).toMatchObject({ properties: { id: "one", name: "Source C", state: "action-closed" } })
455
+ } finally {
456
+ await provider.cleanup?.(createdStorage)
457
+ }
458
+ })
459
+
342
460
  test(`${name} persists cardinality-one edit authority by scope`, async () => {
343
461
  const createdStorage = await provider.createStorage()
344
462
  const storage = requireContractStorage(createdStorage)
@@ -66,11 +66,16 @@ export abstract class QueueWorker<
66
66
  failureCodes: config.failureCodes,
67
67
  workerId: config.workerId,
68
68
  leaseMs: config.leaseMs ?? DEFAULT_LEASE_MS,
69
- claimLimit: config.claimLimit ?? DEFAULT_CLAIM_LIMIT,
69
+ claimLimit: normalizeClaimLimit(config.claimLimit),
70
70
  idlePollMs: config.idlePollMs ?? DEFAULT_IDLE_POLL_MS,
71
71
  }
72
72
  }
73
73
 
74
+ /** Maximum jobs this worker process claims and executes at once. */
75
+ get concurrency(): number {
76
+ return this.config.claimLimit
77
+ }
78
+
74
79
  protected async run(signal: AbortSignal): Promise<void> {
75
80
  const inFlight = new Set<Promise<void>>()
76
81
  let runError: unknown = null
@@ -243,6 +248,14 @@ export abstract class QueueWorker<
243
248
  }
244
249
  }
245
250
 
251
+ function normalizeClaimLimit(value: number | undefined): number {
252
+ const claimLimit = value ?? DEFAULT_CLAIM_LIMIT
253
+ if (!Number.isSafeInteger(claimLimit) || claimLimit < 1) {
254
+ throw new Error("[SixbQueueWorker] Worker concurrency must be a positive safe integer.")
255
+ }
256
+ return claimLimit
257
+ }
258
+
246
259
  async function settleOrLog<TJob extends QueueJob, TFailureCode extends SixbErrorCode>(
247
260
  delivery: QueueDelivery<TJob, TFailureCode>,
248
261
  operation: string,
@@ -1,7 +1,9 @@
1
1
  import { canViewWorkflowIntervention, canViewWorkflowRun, isAllowed } from "../authorization"
2
2
  import type { AuthorizablePrincipal, ExecutionContext } from "../execution"
3
+ import { resolveExecutionCosts } from "../runtime/ai-cost"
3
4
  import { resolveExecutionUsage } from "../runtime/ai-usage"
4
5
  import type { SixbRuntimeContext } from "../runtime/types"
6
+ import type { AiCostSummary } from "../storage/ai-cost"
5
7
  import type { AiModelCallUsage } from "../storage/ai-usage"
6
8
  import type {
7
9
  ListWorkflowInterventionsInput,
@@ -61,6 +63,8 @@ export type ListWorkflowRunNodesInput = Omit<
61
63
  /** Execution-facing Agent node view derived from its run row and model-call ledger. */
62
64
  export interface WorkflowAgentNodeRunView extends Omit<WorkflowAgentNodeRunRecord, "execution"> {
63
65
  readonly usage?: AiModelCallUsage
66
+ /** Preferred valuation; omitted when there are no calls or cost storage is unavailable. */
67
+ readonly cost?: AiCostSummary
64
68
  }
65
69
 
66
70
  /** Execution-facing workflow node view with optional Agent execution details. */
@@ -226,29 +230,45 @@ async function attachWorkflowNodeViews(
226
230
  )
227
231
  if (agentExecutions.length === 0) return nodes
228
232
 
229
- const usages = await resolveExecutionUsage({
230
- storage: runtime.storage.aiUsage,
231
- projectId: runtime.projectId,
232
- executionIds: agentExecutions.map((execution) => execution.executionId),
233
- })
234
- const usageByExecutionId = new Map(
235
- agentExecutions.map((execution, index) => [execution.executionId, usages[index]] as const)
233
+ const executionIds = agentExecutions.map((execution) => execution.executionId)
234
+ const [usages, costs] = await Promise.all([
235
+ resolveExecutionUsage({
236
+ storage: runtime.storage.aiUsage,
237
+ projectId: runtime.projectId,
238
+ executionIds,
239
+ }),
240
+ resolveExecutionCosts({
241
+ storage: runtime.storage.aiCosts,
242
+ projectId: runtime.projectId,
243
+ executionIds,
244
+ }),
245
+ ])
246
+ const accountingByExecutionId = new Map(
247
+ agentExecutions.map((execution, index) => [
248
+ execution.executionId,
249
+ { usage: usages[index], cost: costs[index] },
250
+ ])
236
251
  )
237
252
 
238
253
  return nodes.map((node, index) => {
239
254
  const execution = executions[index]
240
255
  if (!execution) return node
241
- const usage = usageByExecutionId.get(execution.executionId)
256
+ const accounting = accountingByExecutionId.get(execution.executionId)
242
257
  return {
243
258
  ...node,
244
- agentExecution: toWorkflowAgentNodeRunView(execution, usage),
259
+ agentExecution: toWorkflowAgentNodeRunView(
260
+ execution,
261
+ accounting?.usage,
262
+ accounting?.usage === undefined ? undefined : accounting.cost
263
+ ),
245
264
  }
246
265
  })
247
266
  }
248
267
 
249
268
  function toWorkflowAgentNodeRunView(
250
269
  execution: WorkflowAgentNodeRunRecord,
251
- usage: AiModelCallUsage | undefined
270
+ usage: AiModelCallUsage | undefined,
271
+ cost: AiCostSummary | undefined
252
272
  ): WorkflowAgentNodeRunView {
253
273
  return {
254
274
  projectId: execution.projectId,
@@ -267,6 +287,7 @@ function toWorkflowAgentNodeRunView(
267
287
  startedAt: execution.startedAt,
268
288
  completedAt: execution.completedAt,
269
289
  ...(usage === undefined ? {} : { usage }),
290
+ ...(usage === undefined || cost === undefined ? {} : { cost }),
270
291
  }
271
292
  }
272
293