@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.
- package/LICENSE +21 -0
- package/README.md +41 -0
- package/dist/AzureDevOpsTool.d.ts +60 -0
- package/dist/AzureDevOpsTool.d.ts.map +1 -0
- package/dist/AzureDevOpsTool.js +163 -0
- package/dist/AzureDevOpsTool.js.map +1 -0
- package/dist/Bench.d.ts +133 -0
- package/dist/Bench.d.ts.map +1 -0
- package/dist/Bench.js +272 -0
- package/dist/Bench.js.map +1 -0
- package/dist/BenchReport.d.ts +9 -0
- package/dist/BenchReport.d.ts.map +1 -0
- package/dist/BenchReport.js +360 -0
- package/dist/BenchReport.js.map +1 -0
- package/dist/CapabilityGuard.d.ts +6 -0
- package/dist/CapabilityGuard.d.ts.map +1 -0
- package/dist/CapabilityGuard.js +25 -0
- package/dist/CapabilityGuard.js.map +1 -0
- package/dist/Chat.d.ts +15 -0
- package/dist/Chat.d.ts.map +1 -0
- package/dist/Chat.js +36 -0
- package/dist/Chat.js.map +1 -0
- package/dist/Classified.d.ts +14 -0
- package/dist/Classified.d.ts.map +1 -0
- package/dist/Classified.js +48 -0
- package/dist/Classified.js.map +1 -0
- package/dist/CostLedger.d.ts +55 -0
- package/dist/CostLedger.d.ts.map +1 -0
- package/dist/CostLedger.js +126 -0
- package/dist/CostLedger.js.map +1 -0
- package/dist/CostTracker.d.ts +13 -0
- package/dist/CostTracker.d.ts.map +1 -0
- package/dist/CostTracker.js +116 -0
- package/dist/CostTracker.js.map +1 -0
- package/dist/Equiv.d.ts +163 -0
- package/dist/Equiv.d.ts.map +1 -0
- package/dist/Equiv.js +256 -0
- package/dist/Equiv.js.map +1 -0
- package/dist/EquivReport.d.ts +16 -0
- package/dist/EquivReport.d.ts.map +1 -0
- package/dist/EquivReport.js +77 -0
- package/dist/EquivReport.js.map +1 -0
- package/dist/Flow.d.ts +25 -0
- package/dist/Flow.d.ts.map +1 -0
- package/dist/Flow.js +39 -0
- package/dist/Flow.js.map +1 -0
- package/dist/FlowContext.d.ts +23 -0
- package/dist/FlowContext.d.ts.map +1 -0
- package/dist/FlowContext.js +4 -0
- package/dist/FlowContext.js.map +1 -0
- package/dist/FlowError.d.ts +80 -0
- package/dist/FlowError.d.ts.map +1 -0
- package/dist/FlowError.js +92 -0
- package/dist/FlowError.js.map +1 -0
- package/dist/FlowEvents.d.ts +101 -0
- package/dist/FlowEvents.d.ts.map +1 -0
- package/dist/FlowEvents.js +103 -0
- package/dist/FlowEvents.js.map +1 -0
- package/dist/FlowRecorder.d.ts +26 -0
- package/dist/FlowRecorder.d.ts.map +1 -0
- package/dist/FlowRecorder.js +74 -0
- package/dist/FlowRecorder.js.map +1 -0
- package/dist/GitHubTool.d.ts +53 -0
- package/dist/GitHubTool.d.ts.map +1 -0
- package/dist/GitHubTool.js +191 -0
- package/dist/GitHubTool.js.map +1 -0
- package/dist/GitTool.d.ts +46 -0
- package/dist/GitTool.d.ts.map +1 -0
- package/dist/GitTool.js +97 -0
- package/dist/GitTool.js.map +1 -0
- package/dist/Interaction.d.ts +21 -0
- package/dist/Interaction.d.ts.map +1 -0
- package/dist/Interaction.js +9 -0
- package/dist/Interaction.js.map +1 -0
- package/dist/McpServer.d.ts +18 -0
- package/dist/McpServer.d.ts.map +1 -0
- package/dist/McpServer.js +122 -0
- package/dist/McpServer.js.map +1 -0
- package/dist/Mermaid.d.ts +7 -0
- package/dist/Mermaid.d.ts.map +1 -0
- package/dist/Mermaid.js +98 -0
- package/dist/Mermaid.js.map +1 -0
- package/dist/Pack.d.ts +40 -0
- package/dist/Pack.d.ts.map +1 -0
- package/dist/Pack.js +141 -0
- package/dist/Pack.js.map +1 -0
- package/dist/Package.d.ts +2 -0
- package/dist/Package.d.ts.map +1 -0
- package/dist/Package.js +2 -0
- package/dist/Package.js.map +1 -0
- package/dist/Persistence.d.ts +32 -0
- package/dist/Persistence.d.ts.map +1 -0
- package/dist/Persistence.js +85 -0
- package/dist/Persistence.js.map +1 -0
- package/dist/Plan.d.ts +26 -0
- package/dist/Plan.d.ts.map +1 -0
- package/dist/Plan.js +101 -0
- package/dist/Plan.js.map +1 -0
- package/dist/PlanExecution.d.ts +7 -0
- package/dist/PlanExecution.d.ts.map +1 -0
- package/dist/PlanExecution.js +19 -0
- package/dist/PlanExecution.js.map +1 -0
- package/dist/Planner.d.ts +32 -0
- package/dist/Planner.d.ts.map +1 -0
- package/dist/Planner.js +97 -0
- package/dist/Planner.js.map +1 -0
- package/dist/PrSummary.d.ts +15 -0
- package/dist/PrSummary.d.ts.map +1 -0
- package/dist/PrSummary.js +29 -0
- package/dist/PrSummary.js.map +1 -0
- package/dist/PriceList.d.ts +11 -0
- package/dist/PriceList.d.ts.map +1 -0
- package/dist/PriceList.js +79 -0
- package/dist/PriceList.js.map +1 -0
- package/dist/Provenance.d.ts +27 -0
- package/dist/Provenance.d.ts.map +1 -0
- package/dist/Provenance.js +38 -0
- package/dist/Provenance.js.map +1 -0
- package/dist/Replay.d.ts +29 -0
- package/dist/Replay.d.ts.map +1 -0
- package/dist/Replay.js +181 -0
- package/dist/Replay.js.map +1 -0
- package/dist/Review.d.ts +71 -0
- package/dist/Review.d.ts.map +1 -0
- package/dist/Review.js +204 -0
- package/dist/Review.js.map +1 -0
- package/dist/ReviewCache.d.ts +6 -0
- package/dist/ReviewCache.d.ts.map +1 -0
- package/dist/ReviewCache.js +28 -0
- package/dist/ReviewCache.js.map +1 -0
- package/dist/Reviewer.d.ts +12 -0
- package/dist/Reviewer.d.ts.map +1 -0
- package/dist/Reviewer.js +30 -0
- package/dist/Reviewer.js.map +1 -0
- package/dist/SpecChecks.d.ts +17 -0
- package/dist/SpecChecks.d.ts.map +1 -0
- package/dist/SpecChecks.js +107 -0
- package/dist/SpecChecks.js.map +1 -0
- package/dist/Survey.d.ts +32 -0
- package/dist/Survey.d.ts.map +1 -0
- package/dist/Survey.js +119 -0
- package/dist/Survey.js.map +1 -0
- package/dist/TransientRetry.d.ts +16 -0
- package/dist/TransientRetry.d.ts.map +1 -0
- package/dist/TransientRetry.js +102 -0
- package/dist/TransientRetry.js.map +1 -0
- package/dist/Workspace.d.ts +39 -0
- package/dist/Workspace.d.ts.map +1 -0
- package/dist/Workspace.js +192 -0
- package/dist/Workspace.js.map +1 -0
- package/dist/WorkspaceLayout.d.ts +3 -0
- package/dist/WorkspaceLayout.d.ts.map +1 -0
- package/dist/WorkspaceLayout.js +19 -0
- package/dist/WorkspaceLayout.js.map +1 -0
- package/dist/WorkspaceTools.d.ts +4 -0
- package/dist/WorkspaceTools.d.ts.map +1 -0
- package/dist/WorkspaceTools.js +130 -0
- package/dist/WorkspaceTools.js.map +1 -0
- package/package.json +91 -0
- package/src/AzureDevOpsTool.ts +291 -0
- package/src/Bench.ts +340 -0
- package/src/BenchReport.ts +447 -0
- package/src/CapabilityGuard.ts +43 -0
- package/src/Chat.ts +60 -0
- package/src/Classified.ts +67 -0
- package/src/CostLedger.ts +171 -0
- package/src/CostTracker.ts +170 -0
- package/src/Equiv.ts +378 -0
- package/src/EquivReport.ts +92 -0
- package/src/Flow.ts +72 -0
- package/src/FlowContext.ts +23 -0
- package/src/FlowError.ts +112 -0
- package/src/FlowEvents.ts +147 -0
- package/src/FlowRecorder.ts +116 -0
- package/src/GitHubTool.ts +315 -0
- package/src/GitTool.ts +214 -0
- package/src/Interaction.ts +23 -0
- package/src/McpServer.ts +168 -0
- package/src/Mermaid.ts +112 -0
- package/src/Pack.ts +213 -0
- package/src/Package.ts +1 -0
- package/src/Persistence.ts +198 -0
- package/src/Plan.ts +119 -0
- package/src/PlanExecution.ts +46 -0
- package/src/Planner.ts +134 -0
- package/src/PrSummary.ts +37 -0
- package/src/PriceList.ts +91 -0
- package/src/Provenance.ts +85 -0
- package/src/Replay.ts +265 -0
- package/src/Review.ts +337 -0
- package/src/ReviewCache.ts +45 -0
- package/src/Reviewer.ts +31 -0
- package/src/SpecChecks.ts +143 -0
- package/src/Survey.ts +144 -0
- package/src/TransientRetry.ts +238 -0
- package/src/Workspace.ts +255 -0
- package/src/WorkspaceLayout.ts +22 -0
- 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
|
+
)
|