@llm4ts/flow 0.1.0

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 (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -0
  3. package/dist/AzureDevOpsTool.d.ts +60 -0
  4. package/dist/AzureDevOpsTool.d.ts.map +1 -0
  5. package/dist/AzureDevOpsTool.js +163 -0
  6. package/dist/AzureDevOpsTool.js.map +1 -0
  7. package/dist/Bench.d.ts +133 -0
  8. package/dist/Bench.d.ts.map +1 -0
  9. package/dist/Bench.js +272 -0
  10. package/dist/Bench.js.map +1 -0
  11. package/dist/BenchReport.d.ts +9 -0
  12. package/dist/BenchReport.d.ts.map +1 -0
  13. package/dist/BenchReport.js +360 -0
  14. package/dist/BenchReport.js.map +1 -0
  15. package/dist/CapabilityGuard.d.ts +6 -0
  16. package/dist/CapabilityGuard.d.ts.map +1 -0
  17. package/dist/CapabilityGuard.js +25 -0
  18. package/dist/CapabilityGuard.js.map +1 -0
  19. package/dist/Chat.d.ts +15 -0
  20. package/dist/Chat.d.ts.map +1 -0
  21. package/dist/Chat.js +36 -0
  22. package/dist/Chat.js.map +1 -0
  23. package/dist/Classified.d.ts +14 -0
  24. package/dist/Classified.d.ts.map +1 -0
  25. package/dist/Classified.js +48 -0
  26. package/dist/Classified.js.map +1 -0
  27. package/dist/CostLedger.d.ts +55 -0
  28. package/dist/CostLedger.d.ts.map +1 -0
  29. package/dist/CostLedger.js +126 -0
  30. package/dist/CostLedger.js.map +1 -0
  31. package/dist/CostTracker.d.ts +13 -0
  32. package/dist/CostTracker.d.ts.map +1 -0
  33. package/dist/CostTracker.js +116 -0
  34. package/dist/CostTracker.js.map +1 -0
  35. package/dist/Equiv.d.ts +163 -0
  36. package/dist/Equiv.d.ts.map +1 -0
  37. package/dist/Equiv.js +256 -0
  38. package/dist/Equiv.js.map +1 -0
  39. package/dist/EquivReport.d.ts +16 -0
  40. package/dist/EquivReport.d.ts.map +1 -0
  41. package/dist/EquivReport.js +77 -0
  42. package/dist/EquivReport.js.map +1 -0
  43. package/dist/Flow.d.ts +25 -0
  44. package/dist/Flow.d.ts.map +1 -0
  45. package/dist/Flow.js +39 -0
  46. package/dist/Flow.js.map +1 -0
  47. package/dist/FlowContext.d.ts +23 -0
  48. package/dist/FlowContext.d.ts.map +1 -0
  49. package/dist/FlowContext.js +4 -0
  50. package/dist/FlowContext.js.map +1 -0
  51. package/dist/FlowError.d.ts +80 -0
  52. package/dist/FlowError.d.ts.map +1 -0
  53. package/dist/FlowError.js +92 -0
  54. package/dist/FlowError.js.map +1 -0
  55. package/dist/FlowEvents.d.ts +101 -0
  56. package/dist/FlowEvents.d.ts.map +1 -0
  57. package/dist/FlowEvents.js +103 -0
  58. package/dist/FlowEvents.js.map +1 -0
  59. package/dist/FlowRecorder.d.ts +26 -0
  60. package/dist/FlowRecorder.d.ts.map +1 -0
  61. package/dist/FlowRecorder.js +74 -0
  62. package/dist/FlowRecorder.js.map +1 -0
  63. package/dist/GitHubTool.d.ts +53 -0
  64. package/dist/GitHubTool.d.ts.map +1 -0
  65. package/dist/GitHubTool.js +191 -0
  66. package/dist/GitHubTool.js.map +1 -0
  67. package/dist/GitTool.d.ts +46 -0
  68. package/dist/GitTool.d.ts.map +1 -0
  69. package/dist/GitTool.js +97 -0
  70. package/dist/GitTool.js.map +1 -0
  71. package/dist/Interaction.d.ts +21 -0
  72. package/dist/Interaction.d.ts.map +1 -0
  73. package/dist/Interaction.js +9 -0
  74. package/dist/Interaction.js.map +1 -0
  75. package/dist/McpServer.d.ts +18 -0
  76. package/dist/McpServer.d.ts.map +1 -0
  77. package/dist/McpServer.js +122 -0
  78. package/dist/McpServer.js.map +1 -0
  79. package/dist/Mermaid.d.ts +7 -0
  80. package/dist/Mermaid.d.ts.map +1 -0
  81. package/dist/Mermaid.js +98 -0
  82. package/dist/Mermaid.js.map +1 -0
  83. package/dist/Pack.d.ts +40 -0
  84. package/dist/Pack.d.ts.map +1 -0
  85. package/dist/Pack.js +141 -0
  86. package/dist/Pack.js.map +1 -0
  87. package/dist/Package.d.ts +2 -0
  88. package/dist/Package.d.ts.map +1 -0
  89. package/dist/Package.js +2 -0
  90. package/dist/Package.js.map +1 -0
  91. package/dist/Persistence.d.ts +32 -0
  92. package/dist/Persistence.d.ts.map +1 -0
  93. package/dist/Persistence.js +85 -0
  94. package/dist/Persistence.js.map +1 -0
  95. package/dist/Plan.d.ts +26 -0
  96. package/dist/Plan.d.ts.map +1 -0
  97. package/dist/Plan.js +101 -0
  98. package/dist/Plan.js.map +1 -0
  99. package/dist/PlanExecution.d.ts +7 -0
  100. package/dist/PlanExecution.d.ts.map +1 -0
  101. package/dist/PlanExecution.js +19 -0
  102. package/dist/PlanExecution.js.map +1 -0
  103. package/dist/Planner.d.ts +32 -0
  104. package/dist/Planner.d.ts.map +1 -0
  105. package/dist/Planner.js +97 -0
  106. package/dist/Planner.js.map +1 -0
  107. package/dist/PrSummary.d.ts +15 -0
  108. package/dist/PrSummary.d.ts.map +1 -0
  109. package/dist/PrSummary.js +29 -0
  110. package/dist/PrSummary.js.map +1 -0
  111. package/dist/PriceList.d.ts +11 -0
  112. package/dist/PriceList.d.ts.map +1 -0
  113. package/dist/PriceList.js +79 -0
  114. package/dist/PriceList.js.map +1 -0
  115. package/dist/Provenance.d.ts +27 -0
  116. package/dist/Provenance.d.ts.map +1 -0
  117. package/dist/Provenance.js +38 -0
  118. package/dist/Provenance.js.map +1 -0
  119. package/dist/Replay.d.ts +29 -0
  120. package/dist/Replay.d.ts.map +1 -0
  121. package/dist/Replay.js +181 -0
  122. package/dist/Replay.js.map +1 -0
  123. package/dist/Review.d.ts +71 -0
  124. package/dist/Review.d.ts.map +1 -0
  125. package/dist/Review.js +204 -0
  126. package/dist/Review.js.map +1 -0
  127. package/dist/ReviewCache.d.ts +6 -0
  128. package/dist/ReviewCache.d.ts.map +1 -0
  129. package/dist/ReviewCache.js +28 -0
  130. package/dist/ReviewCache.js.map +1 -0
  131. package/dist/Reviewer.d.ts +12 -0
  132. package/dist/Reviewer.d.ts.map +1 -0
  133. package/dist/Reviewer.js +30 -0
  134. package/dist/Reviewer.js.map +1 -0
  135. package/dist/SpecChecks.d.ts +17 -0
  136. package/dist/SpecChecks.d.ts.map +1 -0
  137. package/dist/SpecChecks.js +107 -0
  138. package/dist/SpecChecks.js.map +1 -0
  139. package/dist/Survey.d.ts +32 -0
  140. package/dist/Survey.d.ts.map +1 -0
  141. package/dist/Survey.js +119 -0
  142. package/dist/Survey.js.map +1 -0
  143. package/dist/TransientRetry.d.ts +16 -0
  144. package/dist/TransientRetry.d.ts.map +1 -0
  145. package/dist/TransientRetry.js +102 -0
  146. package/dist/TransientRetry.js.map +1 -0
  147. package/dist/Workspace.d.ts +39 -0
  148. package/dist/Workspace.d.ts.map +1 -0
  149. package/dist/Workspace.js +192 -0
  150. package/dist/Workspace.js.map +1 -0
  151. package/dist/WorkspaceLayout.d.ts +3 -0
  152. package/dist/WorkspaceLayout.d.ts.map +1 -0
  153. package/dist/WorkspaceLayout.js +19 -0
  154. package/dist/WorkspaceLayout.js.map +1 -0
  155. package/dist/WorkspaceTools.d.ts +4 -0
  156. package/dist/WorkspaceTools.d.ts.map +1 -0
  157. package/dist/WorkspaceTools.js +130 -0
  158. package/dist/WorkspaceTools.js.map +1 -0
  159. package/package.json +91 -0
  160. package/src/AzureDevOpsTool.ts +291 -0
  161. package/src/Bench.ts +340 -0
  162. package/src/BenchReport.ts +447 -0
  163. package/src/CapabilityGuard.ts +43 -0
  164. package/src/Chat.ts +60 -0
  165. package/src/Classified.ts +67 -0
  166. package/src/CostLedger.ts +171 -0
  167. package/src/CostTracker.ts +170 -0
  168. package/src/Equiv.ts +378 -0
  169. package/src/EquivReport.ts +92 -0
  170. package/src/Flow.ts +72 -0
  171. package/src/FlowContext.ts +23 -0
  172. package/src/FlowError.ts +112 -0
  173. package/src/FlowEvents.ts +147 -0
  174. package/src/FlowRecorder.ts +116 -0
  175. package/src/GitHubTool.ts +315 -0
  176. package/src/GitTool.ts +214 -0
  177. package/src/Interaction.ts +23 -0
  178. package/src/McpServer.ts +168 -0
  179. package/src/Mermaid.ts +112 -0
  180. package/src/Pack.ts +213 -0
  181. package/src/Package.ts +1 -0
  182. package/src/Persistence.ts +198 -0
  183. package/src/Plan.ts +119 -0
  184. package/src/PlanExecution.ts +46 -0
  185. package/src/Planner.ts +134 -0
  186. package/src/PrSummary.ts +37 -0
  187. package/src/PriceList.ts +91 -0
  188. package/src/Provenance.ts +85 -0
  189. package/src/Replay.ts +265 -0
  190. package/src/Review.ts +337 -0
  191. package/src/ReviewCache.ts +45 -0
  192. package/src/Reviewer.ts +31 -0
  193. package/src/SpecChecks.ts +143 -0
  194. package/src/Survey.ts +144 -0
  195. package/src/TransientRetry.ts +238 -0
  196. package/src/Workspace.ts +255 -0
  197. package/src/WorkspaceLayout.ts +22 -0
  198. package/src/WorkspaceTools.ts +188 -0
@@ -0,0 +1,171 @@
1
+ import * as Effect from "effect/Effect"
2
+ import * as Schema from "effect/Schema"
3
+ import { BudgetExceeded, PlanParseError, type FlowError } from "./FlowError.ts"
4
+ import type { PlainFileStoreShape } from "./Persistence.ts"
5
+ import { stableHash } from "./Plan.ts"
6
+ import { PricesAsOf } from "./PriceList.ts"
7
+
8
+ export class CostCell extends Schema.Class<CostCell>("CostCell")({
9
+ stage: Schema.String,
10
+ agent: Schema.String,
11
+ model: Schema.String,
12
+ prompt: Schema.Int,
13
+ completion: Schema.Int,
14
+ cached: Schema.optionalKey(Schema.Int),
15
+ total: Schema.Int,
16
+ costUsd: Schema.optionalKey(Schema.Number)
17
+ }) {}
18
+
19
+ export class CostRecord extends Schema.Class<CostRecord>("CostRecord")({
20
+ schemaVersion: Schema.Int,
21
+ runId: Schema.String,
22
+ at: Schema.String,
23
+ repo: Schema.String,
24
+ label: Schema.optionalKey(Schema.String),
25
+ promptHead: Schema.String,
26
+ promptHash: Schema.String,
27
+ pricesAsOf: Schema.String,
28
+ cells: Schema.Array(CostCell),
29
+ totalPrompt: Schema.Int,
30
+ totalCompletion: Schema.Int,
31
+ totalCostUsd: Schema.optionalKey(Schema.Number)
32
+ }) {}
33
+
34
+ export const CurrentCostRecordSchema = 1
35
+ export const CostLedgerFileName = "costs.jsonl"
36
+
37
+ export interface CostRecordInput {
38
+ readonly runId: string
39
+ readonly at: string
40
+ readonly repo: string
41
+ readonly label?: string
42
+ readonly prompt: string
43
+ readonly cells: ReadonlyArray<CostCell>
44
+ }
45
+
46
+ export const makeCostRecord = (input: CostRecordInput): CostRecord => {
47
+ const costs = input.cells.flatMap((cell) => (cell.costUsd === undefined ? [] : [cell.costUsd]))
48
+ return CostRecord.make({
49
+ schemaVersion: CurrentCostRecordSchema,
50
+ runId: input.runId,
51
+ at: input.at,
52
+ repo: input.repo,
53
+ ...(input.label === undefined ? {} : { label: input.label }),
54
+ promptHead: input.prompt.trim().replaceAll(/\s+/gu, " ").slice(0, 120),
55
+ promptHash: stableHash(input.prompt),
56
+ pricesAsOf: PricesAsOf,
57
+ cells: input.cells,
58
+ totalPrompt: input.cells.reduce((sum, cell) => sum + cell.prompt, 0),
59
+ totalCompletion: input.cells.reduce((sum, cell) => sum + cell.completion, 0),
60
+ ...(costs.length === 0 ? {} : { totalCostUsd: costs.reduce((sum, cost) => sum + cost, 0) })
61
+ })
62
+ }
63
+
64
+ export class CostBudget extends Schema.Class<CostBudget>("CostBudget")({
65
+ maximumTokens: Schema.optionalKey(Schema.Int),
66
+ maximumCostUsd: Schema.optionalKey(Schema.Number)
67
+ }) {}
68
+
69
+ export const checkCostBudget = (
70
+ record: CostRecord,
71
+ budget: CostBudget
72
+ ): Effect.Effect<void, BudgetExceeded> => {
73
+ const totalTokens = record.totalPrompt + record.totalCompletion
74
+ if (budget.maximumTokens !== undefined && totalTokens > budget.maximumTokens) {
75
+ return Effect.fail(
76
+ BudgetExceeded.make({
77
+ metric: "tokens",
78
+ limit: budget.maximumTokens,
79
+ actual: totalTokens
80
+ })
81
+ )
82
+ }
83
+ if (budget.maximumCostUsd !== undefined && (record.totalCostUsd ?? 0) > budget.maximumCostUsd) {
84
+ return Effect.fail(
85
+ BudgetExceeded.make({
86
+ metric: "costUsd",
87
+ limit: budget.maximumCostUsd,
88
+ actual: record.totalCostUsd ?? 0
89
+ })
90
+ )
91
+ }
92
+ return Effect.void
93
+ }
94
+
95
+ const costRecordCodec = Schema.fromJsonString(CostRecord)
96
+
97
+ export const appendCostRecord = Effect.fn("@llm4ts/flow/CostLedger.append")(function* (
98
+ files: PlainFileStoreShape,
99
+ path: string,
100
+ record: CostRecord
101
+ ): Effect.fn.Return<void, FlowError> {
102
+ const encoded = yield* Schema.encodeEffect(costRecordCodec)(record).pipe(
103
+ Effect.mapError((error) =>
104
+ PlanParseError.make({
105
+ message: `failed to encode cost record: ${String(error)}`
106
+ })
107
+ )
108
+ )
109
+ yield* files.append(path, `${encoded}\n`)
110
+ })
111
+
112
+ export const loadCostLedger = Effect.fn("@llm4ts/flow/CostLedger.load")(function* (
113
+ files: PlainFileStoreShape,
114
+ path: string
115
+ ): Effect.fn.Return<ReadonlyArray<CostRecord>, FlowError> {
116
+ const contents = yield* files.read(path)
117
+ if (contents === undefined) {
118
+ return []
119
+ }
120
+ const parsed = yield* Effect.forEach(
121
+ contents
122
+ .split(/\r?\n/u)
123
+ .map((line) => line.trim())
124
+ .filter((line) => line.length > 0),
125
+ (line) =>
126
+ Schema.decodeUnknownEffect(costRecordCodec)(line).pipe(
127
+ Effect.option,
128
+ Effect.map((option) => (option._tag === "Some" ? [option.value] : []))
129
+ ),
130
+ { concurrency: 1 }
131
+ )
132
+ return parsed.flat()
133
+ })
134
+
135
+ const sumOptional = (values: ReadonlyArray<number | undefined>): number | undefined => {
136
+ const present = values.flatMap((value) => (value === undefined ? [] : [value]))
137
+ return present.length === 0 ? undefined : present.reduce((sum, value) => sum + value, 0)
138
+ }
139
+
140
+ export const mergeCostCells = (records: ReadonlyArray<CostRecord>): ReadonlyArray<CostCell> => {
141
+ const groups = new Map<string, Array<CostCell>>()
142
+ for (const cell of records.flatMap((record) => record.cells)) {
143
+ const key = JSON.stringify([cell.stage, cell.agent, cell.model])
144
+ groups.set(key, [...(groups.get(key) ?? []), cell])
145
+ }
146
+ return [...groups.values()]
147
+ .map((cells) => {
148
+ const first = cells[0]
149
+ if (first === undefined) {
150
+ return undefined
151
+ }
152
+ const cached = sumOptional(cells.map((cell) => cell.cached))
153
+ const costUsd = sumOptional(cells.map((cell) => cell.costUsd))
154
+ return CostCell.make({
155
+ stage: first.stage,
156
+ agent: first.agent,
157
+ model: first.model,
158
+ prompt: cells.reduce((sum, cell) => sum + cell.prompt, 0),
159
+ completion: cells.reduce((sum, cell) => sum + cell.completion, 0),
160
+ total: cells.reduce((sum, cell) => sum + cell.total, 0),
161
+ ...(cached === undefined ? {} : { cached }),
162
+ ...(costUsd === undefined ? {} : { costUsd })
163
+ })
164
+ })
165
+ .filter((cell): cell is CostCell => cell !== undefined)
166
+ .sort((left, right) =>
167
+ [left.stage, left.agent, left.model]
168
+ .join("\0")
169
+ .localeCompare([right.stage, right.agent, right.model].join("\0"))
170
+ )
171
+ }
@@ -0,0 +1,170 @@
1
+ import * as Effect from "effect/Effect"
2
+ import * as Ref from "effect/Ref"
3
+ import type * as Scope from "effect/Scope"
4
+ import * as Stream from "effect/Stream"
5
+ import { TokenUsage } from "@llm4ts/core/Models"
6
+ import { CostCell } from "./CostLedger.ts"
7
+ import type { FlowEvent, FlowEventHub } from "./FlowEvents.ts"
8
+ import { estimateCostUsd, PricesAsOf } from "./PriceList.ts"
9
+
10
+ interface CostState {
11
+ readonly byAgent: ReadonlyMap<string, TokenUsage>
12
+ readonly byModel: ReadonlyMap<string, TokenUsage>
13
+ readonly byCell: ReadonlyMap<string, TokenUsage>
14
+ readonly cellKeys: ReadonlyMap<string, readonly [stage: string, agent: string, model: string]>
15
+ readonly openStages: ReadonlyArray<string>
16
+ }
17
+
18
+ export interface CostTracker {
19
+ readonly record: (event: FlowEvent) => Effect.Effect<void>
20
+ readonly cells: Effect.Effect<ReadonlyArray<CostCell>>
21
+ readonly summary: Effect.Effect<string>
22
+ readonly consume: (hub: FlowEventHub) => Effect.Effect<void, never, Scope.Scope>
23
+ readonly awaitDrained: (hub: FlowEventHub) => Effect.Effect<void>
24
+ }
25
+
26
+ const emptyState: CostState = {
27
+ byAgent: new Map(),
28
+ byModel: new Map(),
29
+ byCell: new Map(),
30
+ cellKeys: new Map(),
31
+ openStages: []
32
+ }
33
+
34
+ const mergeUsage = (previous: TokenUsage | undefined, usage: TokenUsage): TokenUsage => {
35
+ const cachedValues = [previous?.cached, usage.cached].flatMap((value) =>
36
+ value === undefined ? [] : [value]
37
+ )
38
+ return TokenUsage.make({
39
+ prompt: (previous?.prompt ?? 0) + usage.prompt,
40
+ completion: (previous?.completion ?? 0) + usage.completion,
41
+ total: (previous?.total ?? 0) + usage.total,
42
+ ...(cachedValues.length === 0
43
+ ? {}
44
+ : {
45
+ cached: cachedValues.reduce((sum, value) => sum + value, 0)
46
+ })
47
+ })
48
+ }
49
+
50
+ const addUsage = (
51
+ values: ReadonlyMap<string, TokenUsage>,
52
+ key: string,
53
+ usage: TokenUsage
54
+ ): ReadonlyMap<string, TokenUsage> => new Map(values).set(key, mergeUsage(values.get(key), usage))
55
+
56
+ const updateCostState = (state: CostState, event: FlowEvent): CostState => {
57
+ switch (event._tag) {
58
+ case "TokensUsed": {
59
+ const model = event.model ?? "(unknown)"
60
+ const stage = state.openStages[0] ?? "(no stage)"
61
+ const cellKey = JSON.stringify([stage, event.agent, model])
62
+ return {
63
+ ...state,
64
+ byAgent: addUsage(state.byAgent, event.agent, event.usage),
65
+ byModel: addUsage(state.byModel, model, event.usage),
66
+ byCell: addUsage(state.byCell, cellKey, event.usage),
67
+ cellKeys: new Map(state.cellKeys).set(cellKey, [stage, event.agent, model])
68
+ }
69
+ }
70
+ case "StageStarted":
71
+ return { ...state, openStages: [event.stage, ...state.openStages] }
72
+ case "StageCompleted":
73
+ case "StageFailed":
74
+ return { ...state, openStages: state.openStages.slice(1) }
75
+ default:
76
+ return state
77
+ }
78
+ }
79
+
80
+ const tokenLine = (label: string, usage: TokenUsage, cost: number | undefined): string => {
81
+ const cached = usage.cached === undefined || usage.cached <= 0 ? "" : ` (${usage.cached} cached)`
82
+ const money = cost === undefined ? "" : ` ($${cost.toFixed(4)}*)`
83
+ return ` ${label}: ${usage.prompt} in${cached}, ${usage.completion} out${money}`
84
+ }
85
+
86
+ const renderSummary = (state: CostState): string => {
87
+ const agents = [...state.byAgent.entries()]
88
+ .sort(([left], [right]) => left.localeCompare(right))
89
+ .map(([agent, usage]) => tokenLine(agent, usage, undefined))
90
+ const models = [...state.byModel.entries()]
91
+ .sort(([left], [right]) => left.localeCompare(right))
92
+ .map(([model, usage]) => {
93
+ const cost = model === "(unknown)" ? undefined : estimateCostUsd(model, usage)
94
+ return { line: tokenLine(model, usage, cost), cost }
95
+ })
96
+ const total = models.reduce((sum, entry) => sum + (entry.cost ?? 0), 0)
97
+ const footnote =
98
+ total <= 0
99
+ ? ""
100
+ : `\n\n* estimated from the pricing table (rates as of ${PricesAsOf} — may be stale)`
101
+ return `By agent:\n${agents.join("\n")}\n\nBy model:\n${models
102
+ .map(({ line }) => line)
103
+ .join("\n")}\n\nTotal: ${total <= 0 ? "$0.00" : `$${total.toFixed(4)}`}${footnote}`
104
+ }
105
+
106
+ export const makeCostTracker = Effect.fn("@llm4ts/flow/CostTracker.make")(
107
+ function* (): Effect.fn.Return<CostTracker> {
108
+ const state = yield* Ref.make<CostState>(emptyState)
109
+ const consumed = yield* Ref.make(0)
110
+ const record = (event: FlowEvent): Effect.Effect<void> =>
111
+ Ref.update(state, (current) => updateCostState(current, event))
112
+ return {
113
+ record,
114
+ cells: Ref.get(state).pipe(
115
+ Effect.map((current) =>
116
+ [...current.byCell.entries()]
117
+ .flatMap(([key, usage]) => {
118
+ const parts = current.cellKeys.get(key)
119
+ if (parts === undefined) {
120
+ return []
121
+ }
122
+ const [stage, agent, model] = parts
123
+ const costUsd = model === "(unknown)" ? undefined : estimateCostUsd(model, usage)
124
+ return [
125
+ CostCell.make({
126
+ stage,
127
+ agent,
128
+ model,
129
+ prompt: usage.prompt,
130
+ completion: usage.completion,
131
+ total: usage.total,
132
+ ...(usage.cached === undefined ? {} : { cached: usage.cached }),
133
+ ...(costUsd === undefined ? {} : { costUsd })
134
+ })
135
+ ]
136
+ })
137
+ .sort((left, right) =>
138
+ [left.stage, left.agent, left.model]
139
+ .join("\0")
140
+ .localeCompare([right.stage, right.agent, right.model].join("\0"))
141
+ )
142
+ )
143
+ ),
144
+ summary: Ref.get(state).pipe(Effect.map(renderSummary)),
145
+ consume: (hub) =>
146
+ Effect.gen(function* () {
147
+ const subscription = yield* hub.subscribe
148
+ yield* Stream.fromSubscription(subscription).pipe(
149
+ Stream.runForEach((event) =>
150
+ record(event).pipe(Effect.andThen(Ref.update(consumed, (count) => count + 1)))
151
+ ),
152
+ Effect.forkScoped,
153
+ Effect.asVoid
154
+ )
155
+ }),
156
+ awaitDrained: (hub) =>
157
+ Effect.gen(function* () {
158
+ const target = yield* hub.publishedCount
159
+ const drain: Effect.Effect<void> = Effect.suspend(() =>
160
+ Ref.get(consumed).pipe(
161
+ Effect.flatMap((count) =>
162
+ count >= target ? Effect.void : Effect.yieldNow.pipe(Effect.andThen(drain))
163
+ )
164
+ )
165
+ )
166
+ yield* drain
167
+ })
168
+ }
169
+ }
170
+ )