@llm4ts/runner 3.3.0 → 4.0.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/README.md +1 -1
- package/dist/AgentTree.d.ts +2 -3
- package/dist/AgentTree.d.ts.map +1 -1
- package/dist/AgentTree.js +8 -20
- package/dist/AgentTree.js.map +1 -1
- package/dist/AgentTreeSurface.d.ts +1 -1
- package/dist/AgentTreeSurface.d.ts.map +1 -1
- package/dist/AgentTreeSurface.js +2 -5
- package/dist/AgentTreeSurface.js.map +1 -1
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +2 -1
- package/dist/Cli.js.map +1 -1
- package/dist/Connectors.d.ts +27 -2
- package/dist/Connectors.d.ts.map +1 -1
- package/dist/Connectors.js +76 -89
- package/dist/Connectors.js.map +1 -1
- package/dist/Doctor.d.ts.map +1 -1
- package/dist/Doctor.js +8 -11
- package/dist/Doctor.js.map +1 -1
- package/dist/EventPresentation.d.ts +35 -0
- package/dist/EventPresentation.d.ts.map +1 -0
- package/dist/EventPresentation.js +132 -0
- package/dist/EventPresentation.js.map +1 -0
- package/dist/ExecutorRoster.d.ts.map +1 -1
- package/dist/ExecutorRoster.js +4 -6
- package/dist/ExecutorRoster.js.map +1 -1
- package/dist/FlowArgs.d.ts +1 -1
- package/dist/FlowArgs.d.ts.map +1 -1
- package/dist/FlowArgs.js +4 -2
- package/dist/FlowArgs.js.map +1 -1
- package/dist/FlowRunner.d.ts +13 -4
- package/dist/FlowRunner.d.ts.map +1 -1
- package/dist/FlowRunner.js +50 -39
- package/dist/FlowRunner.js.map +1 -1
- package/dist/NodeGeminiCliExecutor.d.ts.map +1 -1
- package/dist/NodeGeminiCliExecutor.js +7 -4
- package/dist/NodeGeminiCliExecutor.js.map +1 -1
- package/dist/NodeGeminiModelBridge.d.ts.map +1 -1
- package/dist/NodeGeminiModelBridge.js +4 -1
- package/dist/NodeGeminiModelBridge.js.map +1 -1
- package/dist/NodeHttpClient.d.ts.map +1 -1
- package/dist/NodeHttpClient.js +1 -0
- package/dist/NodeHttpClient.js.map +1 -1
- package/dist/Otel.d.ts.map +1 -1
- package/dist/Otel.js +5 -5
- package/dist/Otel.js.map +1 -1
- package/dist/Package.d.ts.map +1 -1
- package/dist/Package.js +1 -0
- package/dist/Package.js.map +1 -1
- package/dist/Packs.d.ts.map +1 -1
- package/dist/Packs.js +2 -1
- package/dist/Packs.js.map +1 -1
- package/dist/PiModels.d.ts.map +1 -1
- package/dist/PiModels.js +6 -9
- package/dist/PiModels.js.map +1 -1
- package/dist/Terminal.d.ts +13 -11
- package/dist/Terminal.d.ts.map +1 -1
- package/dist/Terminal.js +24 -80
- package/dist/Terminal.js.map +1 -1
- package/dist/Tools.d.ts.map +1 -1
- package/dist/Tools.js +2 -1
- package/dist/Tools.js.map +1 -1
- package/dist/Transcripts.d.ts +1 -1
- package/dist/Transcripts.d.ts.map +1 -1
- package/dist/Transcripts.js +3 -2
- package/dist/Transcripts.js.map +1 -1
- package/dist/WindowsCommand.d.ts.map +1 -1
- package/dist/WindowsCommand.js +1 -0
- package/dist/WindowsCommand.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/package.json +9 -21
- package/src/AgentTree.ts +16 -25
- package/src/AgentTreeSurface.ts +6 -11
- package/src/Cli.ts +2 -1
- package/src/Connectors.ts +131 -107
- package/src/Doctor.ts +10 -12
- package/src/EventPresentation.ts +167 -0
- package/src/ExecutorRoster.ts +4 -6
- package/src/FlowArgs.ts +7 -2
- package/src/FlowRunner.ts +66 -60
- package/src/NodeGeminiCliExecutor.ts +8 -4
- package/src/NodeGeminiModelBridge.ts +5 -1
- package/src/NodeHttpClient.ts +1 -0
- package/src/Otel.ts +5 -6
- package/src/Package.ts +1 -0
- package/src/Packs.ts +2 -1
- package/src/PiModels.ts +6 -10
- package/src/Terminal.ts +104 -165
- package/src/Tools.ts +2 -1
- package/src/Transcripts.ts +3 -2
- package/src/WindowsCommand.ts +1 -0
- package/src/index.ts +3 -2
- package/dist/Costs.d.ts +0 -52
- package/dist/Costs.d.ts.map +0 -1
- package/dist/Costs.js +0 -95
- package/dist/Costs.js.map +0 -1
- package/dist/McpStdio.d.ts +0 -7
- package/dist/McpStdio.d.ts.map +0 -1
- package/dist/McpStdio.js +0 -19
- package/dist/McpStdio.js.map +0 -1
- package/dist/Profile.d.ts +0 -309
- package/dist/Profile.d.ts.map +0 -1
- package/dist/Profile.js +0 -967
- package/dist/Profile.js.map +0 -1
- package/dist/ReviewFingerprint.d.ts +0 -2
- package/dist/ReviewFingerprint.d.ts.map +0 -1
- package/dist/ReviewFingerprint.js +0 -13
- package/dist/ReviewFingerprint.js.map +0 -1
- package/dist/RunReaders.d.ts +0 -69
- package/dist/RunReaders.d.ts.map +0 -1
- package/dist/RunReaders.js +0 -283
- package/dist/RunReaders.js.map +0 -1
- package/dist/StoryReaders.d.ts +0 -16
- package/dist/StoryReaders.d.ts.map +0 -1
- package/dist/StoryReaders.js +0 -107
- package/dist/StoryReaders.js.map +0 -1
- package/dist/Watch.d.ts +0 -68
- package/dist/Watch.d.ts.map +0 -1
- package/dist/Watch.js +0 -261
- package/dist/Watch.js.map +0 -1
- package/src/Costs.ts +0 -148
- package/src/McpStdio.ts +0 -41
- package/src/Profile.ts +0 -1213
- package/src/ReviewFingerprint.ts +0 -13
- package/src/RunReaders.ts +0 -419
- package/src/StoryReaders.ts +0 -157
- package/src/Watch.ts +0 -446
package/src/Profile.ts
DELETED
|
@@ -1,1213 +0,0 @@
|
|
|
1
|
-
import * as Effect from "effect/Effect"
|
|
2
|
-
import * as Schema from "effect/Schema"
|
|
3
|
-
import type { FlowError } from "@llm4ts/flow/FlowError"
|
|
4
|
-
import type { FlowEvent, Timed } from "@llm4ts/flow/FlowEvents"
|
|
5
|
-
import { readTrace } from "@llm4ts/flow/Replay"
|
|
6
|
-
import { duration, treeInputsOfTrace, type TreeInput } from "./AgentTree.ts"
|
|
7
|
-
|
|
8
|
-
export { duration }
|
|
9
|
-
import { formatCount } from "./Terminal.ts"
|
|
10
|
-
import {
|
|
11
|
-
resolveTraceTarget,
|
|
12
|
-
WatchTargetMissing,
|
|
13
|
-
type TraceChoice,
|
|
14
|
-
type TraceSources
|
|
15
|
-
} from "./Watch.ts"
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* `llm4ts profile`: where a run's time went. A pure fold over the
|
|
19
|
-
* same inputs as the agent tree: per story, its wall time split into model,
|
|
20
|
-
* the coder's tools, gates, merge and waiting, the rest left as unaccounted;
|
|
21
|
-
* per role and executor the model calls, per command the gates; and the
|
|
22
|
-
* biggest sinks ranked in plain words.
|
|
23
|
-
*
|
|
24
|
-
* Content-free by design, so a report can leave a customer's server: story
|
|
25
|
-
* ids, roles, executor ids, tool names, gate commands as configured, counts
|
|
26
|
-
* and durations — never a prompt, a reply, an argument or an output.
|
|
27
|
-
*
|
|
28
|
-
* A trace from before the `Timed` event (2.22) is estimated: a model call is
|
|
29
|
-
* the gap before its token report in the same lane, and the report says so.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
const Ms = Schema.Number
|
|
33
|
-
|
|
34
|
-
export const ProfileTime = Schema.Struct({
|
|
35
|
-
model: Ms,
|
|
36
|
-
tools: Ms,
|
|
37
|
-
gates: Ms,
|
|
38
|
-
merge: Ms,
|
|
39
|
-
waiting: Ms,
|
|
40
|
-
unaccounted: Ms
|
|
41
|
-
})
|
|
42
|
-
export type ProfileTime = typeof ProfileTime.Type
|
|
43
|
-
|
|
44
|
-
export const TurnStats = Schema.Struct({
|
|
45
|
-
count: Schema.Int,
|
|
46
|
-
avgMs: Ms,
|
|
47
|
-
maxMs: Ms,
|
|
48
|
-
firstPrompt: Schema.optionalKey(Schema.Int),
|
|
49
|
-
lastPrompt: Schema.optionalKey(Schema.Int),
|
|
50
|
-
maxPrompt: Schema.optionalKey(Schema.Int),
|
|
51
|
-
/** The last prompt is more than three times the first. */
|
|
52
|
-
promptGrowth: Schema.Boolean,
|
|
53
|
-
/** Tool calls per finished turn: each is a model round trip. */
|
|
54
|
-
avgSteps: Schema.Number,
|
|
55
|
-
/** The model's own time per round trip: a turn's time less its tools, over its steps + 1. */
|
|
56
|
-
avgStepMs: Ms
|
|
57
|
-
})
|
|
58
|
-
export type TurnStats = typeof TurnStats.Type
|
|
59
|
-
|
|
60
|
-
/** A stretch of a story's time no `Timed` event covers, named by the events around it. */
|
|
61
|
-
export const Gap = Schema.Struct({
|
|
62
|
-
ms: Ms,
|
|
63
|
-
after: Schema.String,
|
|
64
|
-
before: Schema.String
|
|
65
|
-
})
|
|
66
|
-
export type Gap = typeof Gap.Type
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* The coder's tool calls by kind of work (build, install and git count as
|
|
70
|
-
* other). `script` is the ad-hoc Python/Node one-liners, heredocs and scratch
|
|
71
|
-
* files it improvised; profiles saved before it existed decode it as 0.
|
|
72
|
-
*/
|
|
73
|
-
export const ToolCalls = Schema.Struct({
|
|
74
|
-
explore: Schema.Int,
|
|
75
|
-
edit: Schema.Int,
|
|
76
|
-
test: Schema.Int,
|
|
77
|
-
script: Schema.Int.pipe(Schema.withDecodingDefaultKey(Effect.succeed(0))),
|
|
78
|
-
other: Schema.Int
|
|
79
|
-
})
|
|
80
|
-
export type ToolCalls = typeof ToolCalls.Type
|
|
81
|
-
|
|
82
|
-
export const StoryProfile = Schema.Struct({
|
|
83
|
-
id: Schema.String,
|
|
84
|
-
executor: Schema.optionalKey(Schema.String),
|
|
85
|
-
status: Schema.Literals(["done", "failed", "running"]),
|
|
86
|
-
wallMs: Ms,
|
|
87
|
-
/** From the plan being ready to this story starting: dependencies and free coder slots. */
|
|
88
|
-
queuedMs: Schema.optionalKey(Ms),
|
|
89
|
-
time: ProfileTime,
|
|
90
|
-
turns: TurnStats,
|
|
91
|
-
/** Its largest untimed stretches, 30 seconds or more: where "unaccounted" sits. */
|
|
92
|
-
gaps: Schema.Array(Gap),
|
|
93
|
-
/**
|
|
94
|
-
* A coder turn the trace shows working (tool calls after its last finished
|
|
95
|
-
* call) but not ended: a run still going. Counted as model time so far.
|
|
96
|
-
*/
|
|
97
|
-
openTurnMs: Ms,
|
|
98
|
-
openTurnTools: Schema.Int,
|
|
99
|
-
toolCalls: ToolCalls,
|
|
100
|
-
/** Task stages the coder ran on this lane: the tasks it gave itself, plus any revision. */
|
|
101
|
-
tasks: Schema.Int,
|
|
102
|
-
/** From the coder's first task (after setup and planning) to its first edit; absent when it never edited. */
|
|
103
|
-
firstEditMs: Schema.optionalKey(Ms),
|
|
104
|
-
/** Explore calls (ls, find, grep, read) made before that first edit. */
|
|
105
|
-
exploreBeforeEdit: Schema.Int,
|
|
106
|
-
/** Commands the coder's Findings claimed to have run that no tool call carried (ADR 0027). */
|
|
107
|
-
unverifiedClaims: Schema.Int,
|
|
108
|
-
/** Tasks whose Findings said `confidence: low`. */
|
|
109
|
-
lowConfidenceTasks: Schema.Int,
|
|
110
|
-
/** The largest diff its reviewers read in one round, in lines (ADR 0044). */
|
|
111
|
-
reviewDiffLines: Schema.optionalKey(Schema.Int)
|
|
112
|
-
})
|
|
113
|
-
export type StoryProfile = typeof StoryProfile.Type
|
|
114
|
-
|
|
115
|
-
export const ModelRow = Schema.Struct({
|
|
116
|
-
role: Schema.String,
|
|
117
|
-
executor: Schema.optionalKey(Schema.String),
|
|
118
|
-
calls: Schema.Int,
|
|
119
|
-
ms: Ms,
|
|
120
|
-
avgMs: Ms,
|
|
121
|
-
avgFirstMs: Schema.optionalKey(Ms),
|
|
122
|
-
/** As the backend reported them. */
|
|
123
|
-
apiMs: Schema.optionalKey(Ms),
|
|
124
|
-
toolMs: Schema.optionalKey(Ms)
|
|
125
|
-
})
|
|
126
|
-
export type ModelRow = typeof ModelRow.Type
|
|
127
|
-
|
|
128
|
-
export const GateRow = Schema.Struct({
|
|
129
|
-
command: Schema.String,
|
|
130
|
-
runs: Schema.Int,
|
|
131
|
-
ms: Ms,
|
|
132
|
-
avgMs: Ms,
|
|
133
|
-
failed: Schema.Int
|
|
134
|
-
})
|
|
135
|
-
export type GateRow = typeof GateRow.Type
|
|
136
|
-
|
|
137
|
-
export const CountRow = Schema.Struct({ label: Schema.String, count: Schema.Int, ms: Ms })
|
|
138
|
-
export type CountRow = typeof CountRow.Type
|
|
139
|
-
|
|
140
|
-
export const Finding = Schema.Struct({ text: Schema.String, ms: Schema.optionalKey(Ms) })
|
|
141
|
-
export type Finding = typeof Finding.Type
|
|
142
|
-
|
|
143
|
-
export const ProfileReport = Schema.Struct({
|
|
144
|
-
/** No `Timed` events: model time is estimated from gaps, the rest is unknown. */
|
|
145
|
-
estimated: Schema.Boolean,
|
|
146
|
-
wallMs: Ms,
|
|
147
|
-
stories: Schema.Array(StoryProfile),
|
|
148
|
-
/** Stories merged on an earlier run: this run only skipped them. */
|
|
149
|
-
skipped: Schema.Array(Schema.String),
|
|
150
|
-
totals: ProfileTime,
|
|
151
|
-
models: Schema.Array(ModelRow),
|
|
152
|
-
gates: Schema.Array(GateRow),
|
|
153
|
-
tools: Schema.Array(CountRow),
|
|
154
|
-
/** Coder tool time by kind of work: explore, edit, test, script, build, install, git, other. */
|
|
155
|
-
toolCategories: Schema.Array(CountRow),
|
|
156
|
-
waits: Schema.Array(CountRow),
|
|
157
|
-
/** Timed on the run's events, in no story: the run's own planning, or a seat no story owns. */
|
|
158
|
-
outside: Schema.Array(CountRow),
|
|
159
|
-
findings: Schema.Array(Finding)
|
|
160
|
-
})
|
|
161
|
-
export type ProfileReport = typeof ProfileReport.Type
|
|
162
|
-
|
|
163
|
-
// ── fold ────────────────────────────────────────────────────────────────────
|
|
164
|
-
|
|
165
|
-
interface TimedAt {
|
|
166
|
-
readonly at: number
|
|
167
|
-
readonly event: Timed
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
interface Lane {
|
|
171
|
-
readonly id: string
|
|
172
|
-
start: number
|
|
173
|
-
end: number | undefined
|
|
174
|
-
status: "done" | "failed" | "running"
|
|
175
|
-
executor: string | undefined
|
|
176
|
-
readonly timed: Array<TimedAt>
|
|
177
|
-
/** Coder prompt sizes, in order. */
|
|
178
|
-
readonly prompts: Array<number>
|
|
179
|
-
/** Estimated mode: the gaps before each token report. */
|
|
180
|
-
readonly estimatedCalls: Array<number>
|
|
181
|
-
/** The last turn boundary: any lane event but a tool call inside a turn. */
|
|
182
|
-
last: number
|
|
183
|
-
skipped: boolean
|
|
184
|
-
/** Every event on the lane, described without its content, for the gaps. */
|
|
185
|
-
readonly moments: Array<{ readonly at: number; readonly what: string }>
|
|
186
|
-
/** Since when a coder turn has been working without ending, and its tool calls. */
|
|
187
|
-
openSince: number | undefined
|
|
188
|
-
openTools: number
|
|
189
|
-
tasks: number
|
|
190
|
-
/** When the coder's first task began: setup and task planning come before it. */
|
|
191
|
-
firstTask: number | undefined
|
|
192
|
-
toolCalls: { explore: number; edit: number; test: number; script: number; other: number }
|
|
193
|
-
firstEdit: number | undefined
|
|
194
|
-
exploreBeforeEdit: number
|
|
195
|
-
unverifiedClaims: number
|
|
196
|
-
lowConfidenceTasks: number
|
|
197
|
-
reviewDiffLines: number | undefined
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
interface Lease {
|
|
201
|
-
readonly at: number
|
|
202
|
-
readonly executor: string
|
|
203
|
-
readonly role: string
|
|
204
|
-
readonly label: string | undefined
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const storyStage = /^story (\S+)$/u
|
|
208
|
-
|
|
209
|
-
/** Events a coder turn emits while it runs: not where a model call starts. */
|
|
210
|
-
const withinTurn: ReadonlySet<FlowEvent["_tag"]> = new Set([
|
|
211
|
-
"ToolUse",
|
|
212
|
-
"UsageProgress",
|
|
213
|
-
"CapabilityUsed",
|
|
214
|
-
"AssistantMessage"
|
|
215
|
-
])
|
|
216
|
-
|
|
217
|
-
const sum = (values: ReadonlyArray<number>): number =>
|
|
218
|
-
values.reduce((total, value) => total + value, 0)
|
|
219
|
-
|
|
220
|
-
const average = (values: ReadonlyArray<number>): number | undefined =>
|
|
221
|
-
values.length === 0 ? undefined : sum(values) / values.length
|
|
222
|
-
|
|
223
|
-
const groupBy = <A>(items: ReadonlyArray<A>, key: (item: A) => string): Map<string, Array<A>> => {
|
|
224
|
-
const groups = new Map<string, Array<A>>()
|
|
225
|
-
for (const item of items) {
|
|
226
|
-
const group = groups.get(key(item)) ?? []
|
|
227
|
-
group.push(item)
|
|
228
|
-
groups.set(key(item), group)
|
|
229
|
-
}
|
|
230
|
-
return groups
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const laneOf = (event: FlowEvent): string | undefined =>
|
|
234
|
-
"lane" in event && typeof event.lane === "string" ? event.lane : undefined
|
|
235
|
-
|
|
236
|
-
/** An event as the gaps name it: its kind, never its content. */
|
|
237
|
-
const describe = (event: FlowEvent): string => {
|
|
238
|
-
switch (event._tag) {
|
|
239
|
-
case "StageStarted":
|
|
240
|
-
return "a stage start"
|
|
241
|
-
case "StageCompleted":
|
|
242
|
-
return "a stage end"
|
|
243
|
-
case "StageFailed":
|
|
244
|
-
return "a stage failure"
|
|
245
|
-
case "ToolUse":
|
|
246
|
-
return "a coder tool call"
|
|
247
|
-
case "TokensUsed":
|
|
248
|
-
return "a token report"
|
|
249
|
-
case "Info":
|
|
250
|
-
return "a log note"
|
|
251
|
-
case "ReviewFindings":
|
|
252
|
-
return "a review round"
|
|
253
|
-
case "StoryJudged":
|
|
254
|
-
return "a judge verdict"
|
|
255
|
-
case "AssistantMessage":
|
|
256
|
-
return "an assistant message"
|
|
257
|
-
case "CapabilityUsed":
|
|
258
|
-
case "CapabilityDenied":
|
|
259
|
-
case "CapabilityUnenforceable":
|
|
260
|
-
return "a capability check"
|
|
261
|
-
case "EvidenceChecked":
|
|
262
|
-
return "an evidence check"
|
|
263
|
-
case "ReviewFindingDemoted":
|
|
264
|
-
return "a review finding demoted"
|
|
265
|
-
case "ExecutorLeased":
|
|
266
|
-
case "ExecutorReleased":
|
|
267
|
-
case "ExecutorExcluded":
|
|
268
|
-
case "ExecutorResumed":
|
|
269
|
-
case "ExecutorHandedOver":
|
|
270
|
-
return "a roster change"
|
|
271
|
-
case "Timed":
|
|
272
|
-
switch (event.kind) {
|
|
273
|
-
case "model":
|
|
274
|
-
return `the end of a ${event.label} call`
|
|
275
|
-
case "tool":
|
|
276
|
-
return `the end of tool ${event.label}`
|
|
277
|
-
case "gate":
|
|
278
|
-
return `the end of gate ${event.label}`
|
|
279
|
-
case "wait":
|
|
280
|
-
return `the end of a wait for the ${event.label}`
|
|
281
|
-
default:
|
|
282
|
-
return `the end of ${event.label}`
|
|
283
|
-
}
|
|
284
|
-
case "Began":
|
|
285
|
-
return event.kind === "model"
|
|
286
|
-
? `the start of a ${event.label} call`
|
|
287
|
-
: `the start of ${event.kind} ${event.label}`
|
|
288
|
-
default:
|
|
289
|
-
return "an event"
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const gapThresholdMs = 30_000
|
|
294
|
-
|
|
295
|
-
/** The parts of each step between two lane events that no timed interval covers, largest first. */
|
|
296
|
-
const gapsOf = (lane: Lane): ReadonlyArray<Gap> => {
|
|
297
|
-
const covered = [
|
|
298
|
-
...lane.timed.map((item) => [item.at - item.event.ms, item.at] as const),
|
|
299
|
-
// A turn still open is model time, not a gap.
|
|
300
|
-
...(lane.openSince === undefined || lane.end !== undefined
|
|
301
|
-
? []
|
|
302
|
-
: [[lane.openSince, Number.POSITIVE_INFINITY] as const])
|
|
303
|
-
].sort((left, right) => left[0] - right[0])
|
|
304
|
-
const uncovered = (from: number, to: number): number => {
|
|
305
|
-
let free = 0
|
|
306
|
-
let cursor = from
|
|
307
|
-
for (const [start, end] of covered) {
|
|
308
|
-
if (end <= cursor || start >= to) {
|
|
309
|
-
continue
|
|
310
|
-
}
|
|
311
|
-
free += Math.max(0, start - cursor)
|
|
312
|
-
cursor = Math.max(cursor, end)
|
|
313
|
-
}
|
|
314
|
-
return free + Math.max(0, to - cursor)
|
|
315
|
-
}
|
|
316
|
-
const moments = [...lane.moments].sort((left, right) => left.at - right.at)
|
|
317
|
-
return moments
|
|
318
|
-
.slice(1)
|
|
319
|
-
.flatMap((next, index) => {
|
|
320
|
-
const previous = moments[index]
|
|
321
|
-
if (previous === undefined) {
|
|
322
|
-
return []
|
|
323
|
-
}
|
|
324
|
-
const ms = uncovered(previous.at, next.at)
|
|
325
|
-
return ms >= gapThresholdMs ? [{ ms, after: previous.what, before: next.what }] : []
|
|
326
|
-
})
|
|
327
|
-
.sort((left, right) => right.ms - left.ms)
|
|
328
|
-
.slice(0, 3)
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/** The roster role a seat label stands for, to find its lease. */
|
|
332
|
-
const leaseRole = (label: string): string => (label === "reasoning" ? "reviewer" : label)
|
|
333
|
-
|
|
334
|
-
export const profileOf = (inputs: ReadonlyArray<TreeInput>): ProfileReport => {
|
|
335
|
-
const lanes = new Map<string, Lane>()
|
|
336
|
-
const leases: Array<Lease> = []
|
|
337
|
-
const allTimed: Array<TimedAt> = []
|
|
338
|
-
let first: number | undefined
|
|
339
|
-
let lastAt = 0
|
|
340
|
-
let planReady: number | undefined
|
|
341
|
-
|
|
342
|
-
for (const input of inputs) {
|
|
343
|
-
first = first ?? input.at
|
|
344
|
-
lastAt = Math.max(lastAt, input.at)
|
|
345
|
-
if (input._tag !== "Event") {
|
|
346
|
-
continue
|
|
347
|
-
}
|
|
348
|
-
const { event, at } = input
|
|
349
|
-
const laneId = laneOf(event)
|
|
350
|
-
const lane = laneId === undefined ? undefined : lanes.get(laneId)
|
|
351
|
-
switch (event._tag) {
|
|
352
|
-
case "StageStarted": {
|
|
353
|
-
const story = storyStage.exec(event.stage)?.[1]
|
|
354
|
-
if (story !== undefined && event.lane === story) {
|
|
355
|
-
lanes.set(story, {
|
|
356
|
-
id: story,
|
|
357
|
-
start: at,
|
|
358
|
-
end: undefined,
|
|
359
|
-
status: "running",
|
|
360
|
-
executor: event.executor,
|
|
361
|
-
timed: [],
|
|
362
|
-
prompts: [],
|
|
363
|
-
estimatedCalls: [],
|
|
364
|
-
last: at,
|
|
365
|
-
skipped: false,
|
|
366
|
-
moments: [{ at, what: describe(event) }],
|
|
367
|
-
openSince: undefined,
|
|
368
|
-
openTools: 0,
|
|
369
|
-
tasks: 0,
|
|
370
|
-
firstTask: undefined,
|
|
371
|
-
toolCalls: { explore: 0, edit: 0, test: 0, script: 0, other: 0 },
|
|
372
|
-
firstEdit: undefined,
|
|
373
|
-
exploreBeforeEdit: 0,
|
|
374
|
-
unverifiedClaims: 0,
|
|
375
|
-
lowConfidenceTasks: 0,
|
|
376
|
-
reviewDiffLines: undefined
|
|
377
|
-
})
|
|
378
|
-
continue
|
|
379
|
-
}
|
|
380
|
-
if (
|
|
381
|
-
lane !== undefined &&
|
|
382
|
-
!storyStage.test(event.stage) &&
|
|
383
|
-
!event.stage.startsWith(`story ${lane.id}:`)
|
|
384
|
-
) {
|
|
385
|
-
lane.tasks += 1
|
|
386
|
-
lane.firstTask = lane.firstTask ?? at
|
|
387
|
-
}
|
|
388
|
-
break
|
|
389
|
-
}
|
|
390
|
-
case "StageCompleted":
|
|
391
|
-
case "StageFailed":
|
|
392
|
-
if (event.stage === "story plan" && event.lane === undefined) {
|
|
393
|
-
planReady = at
|
|
394
|
-
}
|
|
395
|
-
if (lane !== undefined && event.stage === `story ${lane.id}`) {
|
|
396
|
-
lane.end = at
|
|
397
|
-
lane.status = event._tag === "StageFailed" ? "failed" : "done"
|
|
398
|
-
}
|
|
399
|
-
break
|
|
400
|
-
case "Timed": {
|
|
401
|
-
allTimed.push({ at, event })
|
|
402
|
-
lane?.timed.push({ at, event })
|
|
403
|
-
if (lane !== undefined && event.kind === "tool" && event.category !== undefined) {
|
|
404
|
-
const kind =
|
|
405
|
-
event.category === "explore" ||
|
|
406
|
-
event.category === "edit" ||
|
|
407
|
-
event.category === "test" ||
|
|
408
|
-
event.category === "script"
|
|
409
|
-
? event.category
|
|
410
|
-
: "other"
|
|
411
|
-
lane.toolCalls[kind] += 1
|
|
412
|
-
if (kind === "edit" && lane.firstEdit === undefined) {
|
|
413
|
-
lane.firstEdit = at - (lane.firstTask ?? lane.start)
|
|
414
|
-
lane.exploreBeforeEdit = lane.toolCalls.explore
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
if (lane !== undefined && event.kind === "model" && event.label === "coder") {
|
|
418
|
-
lane.openSince = undefined
|
|
419
|
-
lane.openTools = 0
|
|
420
|
-
}
|
|
421
|
-
break
|
|
422
|
-
}
|
|
423
|
-
case "ToolUse":
|
|
424
|
-
if (lane !== undefined) {
|
|
425
|
-
// A tool call opens a turn until a coder call ends: it began at
|
|
426
|
-
// the last turn boundary.
|
|
427
|
-
lane.openSince = lane.openSince ?? lane.last
|
|
428
|
-
lane.openTools += 1
|
|
429
|
-
}
|
|
430
|
-
break
|
|
431
|
-
case "TokensUsed":
|
|
432
|
-
if (lane !== undefined) {
|
|
433
|
-
if (event.agent === "coder") {
|
|
434
|
-
lane.prompts.push(event.usage.prompt)
|
|
435
|
-
}
|
|
436
|
-
lane.estimatedCalls.push(at - lane.last)
|
|
437
|
-
}
|
|
438
|
-
break
|
|
439
|
-
case "ExecutorLeased":
|
|
440
|
-
leases.push({ at, executor: event.executor, role: event.role, label: event.label })
|
|
441
|
-
break
|
|
442
|
-
case "Info":
|
|
443
|
-
if (lane !== undefined && event.message.startsWith(`story ${lane.id}: already merged`)) {
|
|
444
|
-
lane.skipped = true
|
|
445
|
-
}
|
|
446
|
-
break
|
|
447
|
-
case "ReviewFindings":
|
|
448
|
-
if (lane !== undefined && event.diffLines !== undefined) {
|
|
449
|
-
lane.reviewDiffLines = Math.max(lane.reviewDiffLines ?? 0, event.diffLines)
|
|
450
|
-
}
|
|
451
|
-
break
|
|
452
|
-
case "EvidenceChecked":
|
|
453
|
-
if (lane !== undefined) {
|
|
454
|
-
lane.unverifiedClaims += event.unverified
|
|
455
|
-
if (event.confidence === "low") {
|
|
456
|
-
lane.lowConfidenceTasks += 1
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
break
|
|
460
|
-
default:
|
|
461
|
-
break
|
|
462
|
-
}
|
|
463
|
-
if (lane !== undefined) {
|
|
464
|
-
lane.moments.push({ at, what: describe(event) })
|
|
465
|
-
lane.executor = lane.executor ?? ("executor" in event ? event.executor : undefined)
|
|
466
|
-
if (!withinTurn.has(event._tag)) {
|
|
467
|
-
lane.last = at
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const estimated = allTimed.length === 0
|
|
473
|
-
const stories = [...lanes.values()]
|
|
474
|
-
.filter((lane) => !lane.skipped)
|
|
475
|
-
.map((lane) => storyProfile(lane, lastAt, planReady, estimated))
|
|
476
|
-
const skipped = [...lanes.values()].filter((lane) => lane.skipped).map((lane) => lane.id)
|
|
477
|
-
|
|
478
|
-
const executorOf = (item: TimedAt): string | undefined => {
|
|
479
|
-
if (item.event.executor !== undefined) {
|
|
480
|
-
return item.event.executor
|
|
481
|
-
}
|
|
482
|
-
const role = leaseRole(item.event.label)
|
|
483
|
-
const lease = [...leases]
|
|
484
|
-
.reverse()
|
|
485
|
-
.find(
|
|
486
|
-
(candidate) =>
|
|
487
|
-
candidate.at <= item.at && candidate.role === role && candidate.label === item.event.lane
|
|
488
|
-
)
|
|
489
|
-
return lease?.executor
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const modelCalls = allTimed.filter((item) => item.event.kind === "model")
|
|
493
|
-
const models = [
|
|
494
|
-
...groupBy(modelCalls, (item) => `${item.event.label}\u0000${executorOf(item) ?? ""}`).values()
|
|
495
|
-
].map((calls): ModelRow => {
|
|
496
|
-
const sample = calls[0]
|
|
497
|
-
const executor = sample === undefined ? undefined : executorOf(sample)
|
|
498
|
-
const ms = sum(calls.map((item) => item.event.ms))
|
|
499
|
-
const firsts = calls.flatMap((item) =>
|
|
500
|
-
item.event.firstMs === undefined ? [] : [item.event.firstMs]
|
|
501
|
-
)
|
|
502
|
-
const apis = calls.flatMap((item) => (item.event.apiMs === undefined ? [] : [item.event.apiMs]))
|
|
503
|
-
const toolsReported = calls.flatMap((item) =>
|
|
504
|
-
item.event.toolMs === undefined ? [] : [item.event.toolMs]
|
|
505
|
-
)
|
|
506
|
-
const avgFirstMs = average(firsts)
|
|
507
|
-
return {
|
|
508
|
-
role: sample?.event.label ?? "",
|
|
509
|
-
...(executor === undefined ? {} : { executor }),
|
|
510
|
-
calls: calls.length,
|
|
511
|
-
ms,
|
|
512
|
-
avgMs: ms / calls.length,
|
|
513
|
-
...(avgFirstMs === undefined ? {} : { avgFirstMs }),
|
|
514
|
-
...(apis.length === 0 ? {} : { apiMs: sum(apis) }),
|
|
515
|
-
...(toolsReported.length === 0 ? {} : { toolMs: sum(toolsReported) })
|
|
516
|
-
}
|
|
517
|
-
})
|
|
518
|
-
|
|
519
|
-
const gates = [
|
|
520
|
-
...groupBy(
|
|
521
|
-
allTimed.filter((item) => item.event.kind === "gate"),
|
|
522
|
-
(item) => item.event.label
|
|
523
|
-
).entries()
|
|
524
|
-
].map(([command, runs]): GateRow => {
|
|
525
|
-
const ms = sum(runs.map((item) => item.event.ms))
|
|
526
|
-
return {
|
|
527
|
-
command,
|
|
528
|
-
runs: runs.length,
|
|
529
|
-
ms,
|
|
530
|
-
avgMs: ms / runs.length,
|
|
531
|
-
failed: runs.filter((item) => (item.event.exitCode ?? 0) !== 0 || item.event.failed === true)
|
|
532
|
-
.length
|
|
533
|
-
}
|
|
534
|
-
})
|
|
535
|
-
|
|
536
|
-
const counted = (kind: Timed["kind"]): ReadonlyArray<CountRow> =>
|
|
537
|
-
[
|
|
538
|
-
...groupBy(
|
|
539
|
-
allTimed.filter((item) => item.event.kind === kind),
|
|
540
|
-
(item) => item.event.label
|
|
541
|
-
).entries()
|
|
542
|
-
].map(([label, items]) => ({
|
|
543
|
-
label,
|
|
544
|
-
count: items.length,
|
|
545
|
-
ms: sum(items.map((item) => item.event.ms))
|
|
546
|
-
}))
|
|
547
|
-
const tools = counted("tool")
|
|
548
|
-
const toolCategories = [
|
|
549
|
-
...groupBy(
|
|
550
|
-
allTimed.filter((item) => item.event.kind === "tool" && item.event.category !== undefined),
|
|
551
|
-
(item) => item.event.category ?? ""
|
|
552
|
-
).entries()
|
|
553
|
-
]
|
|
554
|
-
.map(([label, items]) => ({
|
|
555
|
-
label,
|
|
556
|
-
count: items.length,
|
|
557
|
-
ms: sum(items.map((item) => item.event.ms))
|
|
558
|
-
}))
|
|
559
|
-
.sort((left, right) => right.ms - left.ms)
|
|
560
|
-
const waits = counted("wait")
|
|
561
|
-
const steps = stepStats(estimated ? [] : [...lanes.values()].flatMap(turnSteps))
|
|
562
|
-
const outside = [
|
|
563
|
-
...groupBy(
|
|
564
|
-
allTimed.filter((item) => item.event.lane === undefined),
|
|
565
|
-
(item) => item.event.label
|
|
566
|
-
).entries()
|
|
567
|
-
].map(([label, items]) => ({
|
|
568
|
-
label,
|
|
569
|
-
count: items.length,
|
|
570
|
-
ms: sum(items.map((item) => item.event.ms))
|
|
571
|
-
}))
|
|
572
|
-
const merges = [...counted("merge"), ...counted("git")]
|
|
573
|
-
|
|
574
|
-
const totals = stories.reduce<ProfileTime>(
|
|
575
|
-
(total, story) => ({
|
|
576
|
-
model: total.model + story.time.model,
|
|
577
|
-
tools: total.tools + story.time.tools,
|
|
578
|
-
gates: total.gates + story.time.gates,
|
|
579
|
-
merge: total.merge + story.time.merge,
|
|
580
|
-
waiting: total.waiting + story.time.waiting,
|
|
581
|
-
unaccounted: total.unaccounted + story.time.unaccounted
|
|
582
|
-
}),
|
|
583
|
-
{ model: 0, tools: 0, gates: 0, merge: 0, waiting: 0, unaccounted: 0 }
|
|
584
|
-
)
|
|
585
|
-
const storyTime = sum(stories.map((story) => story.wallMs))
|
|
586
|
-
|
|
587
|
-
return {
|
|
588
|
-
estimated,
|
|
589
|
-
wallMs: first === undefined ? 0 : lastAt - first,
|
|
590
|
-
stories,
|
|
591
|
-
skipped,
|
|
592
|
-
totals,
|
|
593
|
-
models,
|
|
594
|
-
gates,
|
|
595
|
-
tools,
|
|
596
|
-
toolCategories,
|
|
597
|
-
waits,
|
|
598
|
-
outside,
|
|
599
|
-
findings: findingsOf({
|
|
600
|
-
steps,
|
|
601
|
-
toolCategories,
|
|
602
|
-
stories,
|
|
603
|
-
models,
|
|
604
|
-
gates,
|
|
605
|
-
tools,
|
|
606
|
-
waits,
|
|
607
|
-
merges,
|
|
608
|
-
storyTime,
|
|
609
|
-
estimated
|
|
610
|
-
})
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
const stepStats = (
|
|
615
|
-
turns: ReadonlyArray<{ readonly steps: number; readonly stepMs: number }>
|
|
616
|
-
): { readonly avgSteps: number; readonly avgStepMs: number } => ({
|
|
617
|
-
avgSteps: average(turns.map((turn) => turn.steps)) ?? 0,
|
|
618
|
-
avgStepMs: average(turns.map((turn) => turn.stepMs)) ?? 0
|
|
619
|
-
})
|
|
620
|
-
|
|
621
|
-
/** Each finished coder turn's steps (its tool calls) and model time per step. */
|
|
622
|
-
const turnSteps = (
|
|
623
|
-
lane: Lane
|
|
624
|
-
): ReadonlyArray<{ readonly steps: number; readonly stepMs: number }> => {
|
|
625
|
-
const tools = lane.timed.filter((item) => item.event.kind === "tool")
|
|
626
|
-
return lane.timed
|
|
627
|
-
.filter((item) => item.event.kind === "model" && item.event.label === "coder")
|
|
628
|
-
.map((turn) => {
|
|
629
|
-
const start = turn.at - turn.event.ms
|
|
630
|
-
const inside = tools.filter((tool) => tool.at > start && tool.at <= turn.at)
|
|
631
|
-
const toolMs = sum(inside.map((tool) => tool.event.ms))
|
|
632
|
-
return {
|
|
633
|
-
steps: inside.length,
|
|
634
|
-
stepMs: Math.max(0, turn.event.ms - toolMs) / (inside.length + 1)
|
|
635
|
-
}
|
|
636
|
-
})
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const storyProfile = (
|
|
640
|
-
lane: Lane,
|
|
641
|
-
lastAt: number,
|
|
642
|
-
planReady: number | undefined,
|
|
643
|
-
estimated: boolean
|
|
644
|
-
): StoryProfile => {
|
|
645
|
-
const wallMs = (lane.end ?? lastAt) - lane.start
|
|
646
|
-
const of = (kind: Timed["kind"]) =>
|
|
647
|
-
sum(lane.timed.filter((item) => item.event.kind === kind).map((item) => item.event.ms))
|
|
648
|
-
const coderCalls = lane.timed
|
|
649
|
-
.filter((item) => item.event.kind === "model" && item.event.label === "coder")
|
|
650
|
-
.map((item) => item.event.ms)
|
|
651
|
-
const tools = of("tool")
|
|
652
|
-
// Only a story still running has a turn in progress; in a finished one an
|
|
653
|
-
// unclosed turn is a timing that never came, left to the gaps.
|
|
654
|
-
const open = !estimated && lane.end === undefined && lane.openSince !== undefined
|
|
655
|
-
const openTurnMs = open && lane.openSince !== undefined ? lastAt - lane.openSince : 0
|
|
656
|
-
const model = estimated ? sum(lane.estimatedCalls) : Math.max(0, of("model") + openTurnMs - tools)
|
|
657
|
-
const gates = of("gate")
|
|
658
|
-
const merge = of("merge") + of("git")
|
|
659
|
-
const waiting = of("wait")
|
|
660
|
-
const turnTimes = estimated ? lane.estimatedCalls : coderCalls
|
|
661
|
-
const firstPrompt = lane.prompts[0]
|
|
662
|
-
const lastPrompt = lane.prompts.at(-1)
|
|
663
|
-
const maxPrompt = lane.prompts.length === 0 ? undefined : Math.max(...lane.prompts)
|
|
664
|
-
return {
|
|
665
|
-
gaps: estimated ? [] : gapsOf(lane),
|
|
666
|
-
openTurnMs,
|
|
667
|
-
openTurnTools: open ? lane.openTools : 0,
|
|
668
|
-
toolCalls: { ...lane.toolCalls },
|
|
669
|
-
tasks: lane.tasks,
|
|
670
|
-
...(lane.firstEdit === undefined ? {} : { firstEditMs: lane.firstEdit }),
|
|
671
|
-
exploreBeforeEdit: lane.exploreBeforeEdit,
|
|
672
|
-
unverifiedClaims: lane.unverifiedClaims,
|
|
673
|
-
lowConfidenceTasks: lane.lowConfidenceTasks,
|
|
674
|
-
...(lane.reviewDiffLines === undefined ? {} : { reviewDiffLines: lane.reviewDiffLines }),
|
|
675
|
-
id: lane.id,
|
|
676
|
-
...(lane.executor === undefined ? {} : { executor: lane.executor }),
|
|
677
|
-
status: lane.status,
|
|
678
|
-
wallMs,
|
|
679
|
-
...(planReady === undefined ? {} : { queuedMs: Math.max(0, lane.start - planReady) }),
|
|
680
|
-
time: {
|
|
681
|
-
model,
|
|
682
|
-
tools,
|
|
683
|
-
gates,
|
|
684
|
-
merge,
|
|
685
|
-
waiting,
|
|
686
|
-
unaccounted: Math.max(0, wallMs - model - tools - gates - merge - waiting)
|
|
687
|
-
},
|
|
688
|
-
turns: {
|
|
689
|
-
count: turnTimes.length,
|
|
690
|
-
avgMs: average(turnTimes) ?? 0,
|
|
691
|
-
maxMs: turnTimes.length === 0 ? 0 : Math.max(...turnTimes),
|
|
692
|
-
...(firstPrompt === undefined ? {} : { firstPrompt }),
|
|
693
|
-
...(lastPrompt === undefined ? {} : { lastPrompt }),
|
|
694
|
-
...(maxPrompt === undefined ? {} : { maxPrompt }),
|
|
695
|
-
...stepStats(estimated ? [] : turnSteps(lane)),
|
|
696
|
-
promptGrowth:
|
|
697
|
-
firstPrompt !== undefined &&
|
|
698
|
-
lastPrompt !== undefined &&
|
|
699
|
-
firstPrompt > 0 &&
|
|
700
|
-
lastPrompt > 3 * firstPrompt
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// ── words ───────────────────────────────────────────────────────────────────
|
|
706
|
-
|
|
707
|
-
const stepsText = (steps: number): string =>
|
|
708
|
-
`${Number.isInteger(steps) ? steps : steps.toFixed(1)} step${steps === 1 ? "" : "s"}`
|
|
709
|
-
|
|
710
|
-
const share = (ms: number, of: number): string => (of <= 0 ? "" : `${Math.round((ms / of) * 100)}%`)
|
|
711
|
-
|
|
712
|
-
const plural = (count: number, word: string): string => `${count} ${word}${count === 1 ? "" : "s"}`
|
|
713
|
-
|
|
714
|
-
const findingsOf = (facts: {
|
|
715
|
-
readonly steps: { readonly avgSteps: number; readonly avgStepMs: number }
|
|
716
|
-
readonly toolCategories: ReadonlyArray<CountRow>
|
|
717
|
-
readonly stories: ReadonlyArray<StoryProfile>
|
|
718
|
-
readonly models: ReadonlyArray<ModelRow>
|
|
719
|
-
readonly gates: ReadonlyArray<GateRow>
|
|
720
|
-
readonly tools: ReadonlyArray<CountRow>
|
|
721
|
-
readonly waits: ReadonlyArray<CountRow>
|
|
722
|
-
readonly merges: ReadonlyArray<CountRow>
|
|
723
|
-
readonly storyTime: number
|
|
724
|
-
readonly estimated: boolean
|
|
725
|
-
}): ReadonlyArray<Finding> => {
|
|
726
|
-
const ofStories = (ms: number) => `${duration(ms)} (${share(ms, facts.storyTime)} of story time)`
|
|
727
|
-
const byRole = [...groupBy(facts.models, (row) => row.role).entries()].map(([role, rows]) => {
|
|
728
|
-
const calls = sum(rows.map((row) => row.calls))
|
|
729
|
-
const ms = sum(rows.map((row) => row.ms))
|
|
730
|
-
const firsts = rows.flatMap((row) => (row.avgFirstMs === undefined ? [] : [row.avgFirstMs]))
|
|
731
|
-
const firstMs = average(firsts)
|
|
732
|
-
const first = firstMs === undefined ? "" : `, first output after ${duration(firstMs)}`
|
|
733
|
-
return {
|
|
734
|
-
ms,
|
|
735
|
-
text:
|
|
736
|
-
role === "coder"
|
|
737
|
-
? `coder turns: ${calls}, ${ofStories(ms)}, avg ${duration(ms / calls)}${first}${
|
|
738
|
-
facts.steps.avgSteps > 0
|
|
739
|
-
? `, ${stepsText(facts.steps.avgSteps)} per turn, ${duration(facts.steps.avgStepMs)} of model per step`
|
|
740
|
-
: ""
|
|
741
|
-
}`
|
|
742
|
-
: `${role} calls: ${calls}, ${ofStories(ms)}, avg ${duration(ms / calls)}${first}`
|
|
743
|
-
}
|
|
744
|
-
})
|
|
745
|
-
const candidates: Array<{ readonly ms: number; readonly text: string }> = [
|
|
746
|
-
...byRole,
|
|
747
|
-
...facts.gates.map((row) => ({
|
|
748
|
-
ms: row.ms,
|
|
749
|
-
text: `gate \`${row.command}\`: ${plural(row.runs, "run")}, ${ofStories(row.ms)}${
|
|
750
|
-
row.failed === 0 ? "" : `, ${row.failed} failed`
|
|
751
|
-
}`
|
|
752
|
-
})),
|
|
753
|
-
// By kind of work when the trace has it (2.23), else by tool name.
|
|
754
|
-
...(facts.toolCategories.length > 0
|
|
755
|
-
? facts.toolCategories.map((row) => ({
|
|
756
|
-
ms: row.ms,
|
|
757
|
-
text: `coder tools, ${row.label}: ${plural(row.count, "call")}, ${ofStories(row.ms)}`
|
|
758
|
-
}))
|
|
759
|
-
: facts.tools.map((row) => ({
|
|
760
|
-
ms: row.ms,
|
|
761
|
-
text: `coder tool \`${row.label}\`: ${plural(row.count, "call")}, ${ofStories(row.ms)}`
|
|
762
|
-
}))),
|
|
763
|
-
...facts.waits.map((row) => ({
|
|
764
|
-
ms: row.ms,
|
|
765
|
-
text: `waiting for the ${row.label}: ${row.count}×, ${ofStories(row.ms)}`
|
|
766
|
-
})),
|
|
767
|
-
...facts.merges.map((row) => ({
|
|
768
|
-
ms: row.ms,
|
|
769
|
-
text: `${row.label}: ${row.count}×, ${ofStories(row.ms)}`
|
|
770
|
-
}))
|
|
771
|
-
]
|
|
772
|
-
for (const story of facts.stories) {
|
|
773
|
-
if (story.openTurnMs >= 1_000) {
|
|
774
|
-
candidates.push({
|
|
775
|
-
ms: story.openTurnMs,
|
|
776
|
-
text: `${story.id}: a coder turn still running for ${duration(story.openTurnMs)} (${plural(story.openTurnTools, "tool call")} so far)`
|
|
777
|
-
})
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
for (const story of facts.stories) {
|
|
781
|
-
const wandered =
|
|
782
|
-
story.firstEditMs !== undefined &&
|
|
783
|
-
story.wallMs >= 60_000 &&
|
|
784
|
-
(story.exploreBeforeEdit >= 15 ||
|
|
785
|
-
(story.exploreBeforeEdit >= 5 && story.firstEditMs >= story.wallMs / 4))
|
|
786
|
-
if (wandered && story.firstEditMs !== undefined) {
|
|
787
|
-
candidates.push({
|
|
788
|
-
ms: story.firstEditMs,
|
|
789
|
-
text: `${story.id}: ${plural(story.exploreBeforeEdit, "explore call")} and ${duration(story.firstEditMs)} before the coder's first edit — it found its way instead of being told where to go`
|
|
790
|
-
})
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
const queued = sum(facts.stories.map((story) => story.queuedMs ?? 0))
|
|
794
|
-
if (queued >= 1_000) {
|
|
795
|
-
candidates.push({
|
|
796
|
-
ms: queued,
|
|
797
|
-
text: `stories queued before starting (dependencies, free coder slots): ${duration(queued)} in all`
|
|
798
|
-
})
|
|
799
|
-
}
|
|
800
|
-
const estimated = facts.estimated
|
|
801
|
-
? facts.stories.map((story) => ({
|
|
802
|
-
ms: story.time.model,
|
|
803
|
-
text: `${story.id}: model calls (estimated) ${duration(story.time.model)} of ${duration(story.wallMs)}`
|
|
804
|
-
}))
|
|
805
|
-
: []
|
|
806
|
-
const ranked = [...candidates, ...estimated]
|
|
807
|
-
.filter((candidate) => candidate.ms >= 1_000)
|
|
808
|
-
.sort((left, right) => right.ms - left.ms)
|
|
809
|
-
.slice(0, 3)
|
|
810
|
-
const fabricated = facts.stories.flatMap((story) =>
|
|
811
|
-
story.unverifiedClaims > 0
|
|
812
|
-
? [
|
|
813
|
-
{
|
|
814
|
-
text: `${story.id}: the coder claimed ${plural(story.unverifiedClaims, "verification command")} that never ran — its account of its own testing is not evidence`
|
|
815
|
-
}
|
|
816
|
-
]
|
|
817
|
-
: []
|
|
818
|
-
)
|
|
819
|
-
const improvised = facts.stories.flatMap((story) =>
|
|
820
|
-
story.toolCalls.script >= 3
|
|
821
|
-
? [
|
|
822
|
-
{
|
|
823
|
-
text: `${story.id}: ${story.toolCalls.script} ad-hoc scripts — a pack tool preset (llm4ts tool) would make this repeatable`
|
|
824
|
-
}
|
|
825
|
-
]
|
|
826
|
-
: []
|
|
827
|
-
)
|
|
828
|
-
const growth = facts.stories.flatMap((story) =>
|
|
829
|
-
story.turns.promptGrowth &&
|
|
830
|
-
story.turns.firstPrompt !== undefined &&
|
|
831
|
-
story.turns.lastPrompt !== undefined
|
|
832
|
-
? [
|
|
833
|
-
{
|
|
834
|
-
text: `${story.id}: coder prompts grew ${(story.turns.lastPrompt / story.turns.firstPrompt).toFixed(1)}× (${formatCount(story.turns.firstPrompt)} → ${formatCount(story.turns.lastPrompt)} tokens): every turn resends the history`
|
|
835
|
-
}
|
|
836
|
-
]
|
|
837
|
-
: []
|
|
838
|
-
)
|
|
839
|
-
return [...ranked, ...fabricated, ...improvised, ...growth]
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
const table = (
|
|
843
|
-
header: ReadonlyArray<string>,
|
|
844
|
-
rows: ReadonlyArray<ReadonlyArray<string>>
|
|
845
|
-
): Array<string> => {
|
|
846
|
-
const widths = header.map((title, column) =>
|
|
847
|
-
Math.max(title.length, ...rows.map((row) => (row[column] ?? "").length))
|
|
848
|
-
)
|
|
849
|
-
const line = (cells: ReadonlyArray<string>) =>
|
|
850
|
-
` ${cells.map((cell, column) => (column === 0 ? cell.padEnd(widths[column] ?? 0) : cell.padStart(widths[column] ?? 0))).join(" ")}`.trimEnd()
|
|
851
|
-
return [line(header), ...rows.map(line)]
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
export const renderProfile = (report: ProfileReport): string => {
|
|
855
|
-
const storyTime = sum(report.stories.map((story) => story.wallMs))
|
|
856
|
-
const categories: ReadonlyArray<readonly [string, number]> = [
|
|
857
|
-
[report.estimated ? "model + tools + gates (estimated)" : "model", report.totals.model],
|
|
858
|
-
["coder tools", report.totals.tools],
|
|
859
|
-
["gates", report.totals.gates],
|
|
860
|
-
["git + merge", report.totals.merge],
|
|
861
|
-
["waiting", report.totals.waiting],
|
|
862
|
-
["unaccounted", report.totals.unaccounted]
|
|
863
|
-
]
|
|
864
|
-
return [
|
|
865
|
-
`llm4ts profile · ${duration(report.wallMs)} wall · ${report.stories.length} ${report.stories.length === 1 ? "story" : "stories"}${
|
|
866
|
-
report.estimated
|
|
867
|
-
? " · estimated: this trace has no timings (before 2.22), model time is read from gaps"
|
|
868
|
-
: ""
|
|
869
|
-
}`,
|
|
870
|
-
"",
|
|
871
|
-
"Where the time goes",
|
|
872
|
-
...(report.findings.length === 0
|
|
873
|
-
? [" nothing timed yet"]
|
|
874
|
-
: report.findings.map((finding, index) => ` ${index + 1}. ${finding.text}`)),
|
|
875
|
-
"",
|
|
876
|
-
"Story time",
|
|
877
|
-
...table(
|
|
878
|
-
["category", "time", "share"],
|
|
879
|
-
categories.map(([name, ms]) => [name, duration(ms), share(ms, storyTime)])
|
|
880
|
-
),
|
|
881
|
-
"",
|
|
882
|
-
"Stories",
|
|
883
|
-
...table(
|
|
884
|
-
[
|
|
885
|
-
"story",
|
|
886
|
-
"status",
|
|
887
|
-
"wall",
|
|
888
|
-
"queued",
|
|
889
|
-
"model",
|
|
890
|
-
"tools",
|
|
891
|
-
"gates",
|
|
892
|
-
"git+merge",
|
|
893
|
-
"wait",
|
|
894
|
-
"other",
|
|
895
|
-
"turns",
|
|
896
|
-
"avg turn",
|
|
897
|
-
"steps/turn",
|
|
898
|
-
"model/step",
|
|
899
|
-
"prompt ×"
|
|
900
|
-
],
|
|
901
|
-
report.stories.map((story) => [
|
|
902
|
-
story.id,
|
|
903
|
-
story.status,
|
|
904
|
-
duration(story.wallMs),
|
|
905
|
-
story.queuedMs === undefined ? "" : duration(story.queuedMs),
|
|
906
|
-
duration(story.time.model),
|
|
907
|
-
duration(story.time.tools),
|
|
908
|
-
duration(story.time.gates),
|
|
909
|
-
duration(story.time.merge),
|
|
910
|
-
duration(story.time.waiting),
|
|
911
|
-
duration(story.time.unaccounted),
|
|
912
|
-
String(story.turns.count),
|
|
913
|
-
duration(story.turns.avgMs),
|
|
914
|
-
story.turns.avgSteps === 0 ? "" : story.turns.avgSteps.toFixed(1),
|
|
915
|
-
story.turns.avgSteps === 0 ? "" : duration(story.turns.avgStepMs),
|
|
916
|
-
story.turns.firstPrompt !== undefined &&
|
|
917
|
-
story.turns.lastPrompt !== undefined &&
|
|
918
|
-
story.turns.firstPrompt > 0
|
|
919
|
-
? `${(story.turns.lastPrompt / story.turns.firstPrompt).toFixed(1)}×`
|
|
920
|
-
: ""
|
|
921
|
-
])
|
|
922
|
-
),
|
|
923
|
-
...(report.stories.every(
|
|
924
|
-
(story) =>
|
|
925
|
-
story.toolCalls.explore +
|
|
926
|
-
story.toolCalls.edit +
|
|
927
|
-
story.toolCalls.test +
|
|
928
|
-
story.toolCalls.script +
|
|
929
|
-
story.toolCalls.other ===
|
|
930
|
-
0
|
|
931
|
-
)
|
|
932
|
-
? []
|
|
933
|
-
: [
|
|
934
|
-
"",
|
|
935
|
-
"Coder work per story",
|
|
936
|
-
...table(
|
|
937
|
-
[
|
|
938
|
-
"story",
|
|
939
|
-
"tasks",
|
|
940
|
-
"explore",
|
|
941
|
-
"edit",
|
|
942
|
-
"test",
|
|
943
|
-
"script",
|
|
944
|
-
"other",
|
|
945
|
-
"before 1st edit",
|
|
946
|
-
"1st edit at"
|
|
947
|
-
],
|
|
948
|
-
report.stories.map((story) => [
|
|
949
|
-
story.id,
|
|
950
|
-
String(story.tasks),
|
|
951
|
-
String(story.toolCalls.explore),
|
|
952
|
-
String(story.toolCalls.edit),
|
|
953
|
-
String(story.toolCalls.test),
|
|
954
|
-
String(story.toolCalls.script),
|
|
955
|
-
String(story.toolCalls.other),
|
|
956
|
-
story.firstEditMs === undefined ? "" : `${story.exploreBeforeEdit} explore`,
|
|
957
|
-
story.firstEditMs === undefined ? "never" : duration(story.firstEditMs)
|
|
958
|
-
])
|
|
959
|
-
)
|
|
960
|
-
]),
|
|
961
|
-
...(report.stories.every((story) => story.reviewDiffLines === undefined)
|
|
962
|
-
? []
|
|
963
|
-
: [
|
|
964
|
-
"",
|
|
965
|
-
"Largest review diff per story",
|
|
966
|
-
...report.stories.flatMap((story) =>
|
|
967
|
-
story.reviewDiffLines === undefined
|
|
968
|
-
? []
|
|
969
|
-
: [` ${story.id}: review diff ${story.reviewDiffLines} lines`]
|
|
970
|
-
)
|
|
971
|
-
]),
|
|
972
|
-
...(report.skipped.length === 0
|
|
973
|
-
? []
|
|
974
|
-
: [` skipped (merged on an earlier run): ${report.skipped.join(", ")}`]),
|
|
975
|
-
...(report.stories.every((story) => story.gaps.length === 0)
|
|
976
|
-
? []
|
|
977
|
-
: [
|
|
978
|
-
"",
|
|
979
|
-
"Unaccounted, largest gaps (no timing covers them)",
|
|
980
|
-
...report.stories.flatMap((story) =>
|
|
981
|
-
story.gaps.map(
|
|
982
|
-
(gap) => ` ${story.id}: ${duration(gap.ms)} between ${gap.after} and ${gap.before}`
|
|
983
|
-
)
|
|
984
|
-
)
|
|
985
|
-
]),
|
|
986
|
-
...(report.outside.length === 0
|
|
987
|
-
? []
|
|
988
|
-
: [
|
|
989
|
-
"",
|
|
990
|
-
"Timed outside any story (the run's own planning, or a seat no story owns)",
|
|
991
|
-
...table(
|
|
992
|
-
["label", "count", "total"],
|
|
993
|
-
report.outside.map((row) => [row.label, String(row.count), duration(row.ms)])
|
|
994
|
-
)
|
|
995
|
-
]),
|
|
996
|
-
...(report.models.length === 0
|
|
997
|
-
? []
|
|
998
|
-
: [
|
|
999
|
-
"",
|
|
1000
|
-
"Model calls",
|
|
1001
|
-
...table(
|
|
1002
|
-
[
|
|
1003
|
-
"role",
|
|
1004
|
-
"executor",
|
|
1005
|
-
"calls",
|
|
1006
|
-
"total",
|
|
1007
|
-
"avg",
|
|
1008
|
-
"first output",
|
|
1009
|
-
"api (reported)",
|
|
1010
|
-
"tools (reported)"
|
|
1011
|
-
],
|
|
1012
|
-
report.models.map((row) => [
|
|
1013
|
-
row.role,
|
|
1014
|
-
row.executor ?? "",
|
|
1015
|
-
String(row.calls),
|
|
1016
|
-
duration(row.ms),
|
|
1017
|
-
duration(row.avgMs),
|
|
1018
|
-
row.avgFirstMs === undefined ? "" : duration(row.avgFirstMs),
|
|
1019
|
-
row.apiMs === undefined ? "" : duration(row.apiMs),
|
|
1020
|
-
row.toolMs === undefined ? "" : duration(row.toolMs)
|
|
1021
|
-
])
|
|
1022
|
-
)
|
|
1023
|
-
]),
|
|
1024
|
-
...(report.gates.length === 0
|
|
1025
|
-
? []
|
|
1026
|
-
: [
|
|
1027
|
-
"",
|
|
1028
|
-
"Gates",
|
|
1029
|
-
...table(
|
|
1030
|
-
["command", "runs", "total", "avg", "failed"],
|
|
1031
|
-
report.gates.map((row) => [
|
|
1032
|
-
row.command,
|
|
1033
|
-
String(row.runs),
|
|
1034
|
-
duration(row.ms),
|
|
1035
|
-
duration(row.avgMs),
|
|
1036
|
-
String(row.failed)
|
|
1037
|
-
])
|
|
1038
|
-
)
|
|
1039
|
-
]),
|
|
1040
|
-
...(report.toolCategories.length === 0
|
|
1041
|
-
? []
|
|
1042
|
-
: [
|
|
1043
|
-
"",
|
|
1044
|
-
"Coder tools by kind",
|
|
1045
|
-
...table(
|
|
1046
|
-
["kind", "calls", "total", "avg"],
|
|
1047
|
-
report.toolCategories.map((row) => [
|
|
1048
|
-
row.label,
|
|
1049
|
-
String(row.count),
|
|
1050
|
-
duration(row.ms),
|
|
1051
|
-
duration(row.ms / row.count)
|
|
1052
|
-
])
|
|
1053
|
-
)
|
|
1054
|
-
])
|
|
1055
|
-
].join("\n")
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
// ── delta ───────────────────────────────────────────────────────────────────
|
|
1059
|
-
|
|
1060
|
-
/** One line of a before/after comparison (ADR 0029): a count or a duration. */
|
|
1061
|
-
export const DeltaRow = Schema.Struct({
|
|
1062
|
-
label: Schema.String,
|
|
1063
|
-
before: Schema.Number,
|
|
1064
|
-
after: Schema.Number,
|
|
1065
|
-
unit: Schema.Literals(["count", "ms"]),
|
|
1066
|
-
/** Which direction is better, for the arrow; `none` for neutral facts. */
|
|
1067
|
-
better: Schema.Literals(["lower", "higher", "none"])
|
|
1068
|
-
})
|
|
1069
|
-
export type DeltaRow = typeof DeltaRow.Type
|
|
1070
|
-
|
|
1071
|
-
export const ProfileDelta = Schema.Struct({
|
|
1072
|
-
rows: Schema.Array(DeltaRow)
|
|
1073
|
-
})
|
|
1074
|
-
export type ProfileDelta = typeof ProfileDelta.Type
|
|
1075
|
-
|
|
1076
|
-
const roleCalls = (report: ProfileReport, role: string): number =>
|
|
1077
|
-
sum(report.models.filter((row) => row.role === role).map((row) => row.calls))
|
|
1078
|
-
|
|
1079
|
-
/**
|
|
1080
|
-
* The harness-evals comparison (`docs/harness-evals/README.md`), in its
|
|
1081
|
-
* order: how much the coders explored before editing, how often review and
|
|
1082
|
-
* judgment were called, what the gates said, how honest the claims were,
|
|
1083
|
-
* then where the time went. Tokens and cost are `llm4ts costs`' business.
|
|
1084
|
-
*/
|
|
1085
|
-
export const profileDelta = (before: ProfileReport, after: ProfileReport): ProfileDelta => {
|
|
1086
|
-
const count = (
|
|
1087
|
-
label: string,
|
|
1088
|
-
pick: (report: ProfileReport) => number,
|
|
1089
|
-
better: DeltaRow["better"] = "lower"
|
|
1090
|
-
): DeltaRow => ({
|
|
1091
|
-
label,
|
|
1092
|
-
before: pick(before),
|
|
1093
|
-
after: pick(after),
|
|
1094
|
-
unit: "count",
|
|
1095
|
-
better
|
|
1096
|
-
})
|
|
1097
|
-
const time = (label: string, pick: (report: ProfileReport) => number): DeltaRow => ({
|
|
1098
|
-
label,
|
|
1099
|
-
before: pick(before),
|
|
1100
|
-
after: pick(after),
|
|
1101
|
-
unit: "ms",
|
|
1102
|
-
better: "lower"
|
|
1103
|
-
})
|
|
1104
|
-
return {
|
|
1105
|
-
rows: [
|
|
1106
|
-
count("stories done", (r) => r.stories.filter((s) => s.status === "done").length, "higher"),
|
|
1107
|
-
count("stories failed", (r) => r.stories.filter((s) => s.status === "failed").length),
|
|
1108
|
-
count("explore calls before the first edit", (r) =>
|
|
1109
|
-
sum(r.stories.map((s) => s.exploreBeforeEdit))
|
|
1110
|
-
),
|
|
1111
|
-
count("coder turns", (r) => sum(r.stories.map((s) => s.turns.count))),
|
|
1112
|
-
count("coder edits", (r) => sum(r.stories.map((s) => s.toolCalls.edit)), "none"),
|
|
1113
|
-
count("coder test runs", (r) => sum(r.stories.map((s) => s.toolCalls.test)), "none"),
|
|
1114
|
-
count("coder scripts", (r) => sum(r.stories.map((s) => s.toolCalls.script))),
|
|
1115
|
-
count("reviewer calls", (r) => roleCalls(r, "reviewer")),
|
|
1116
|
-
count("judge calls", (r) => roleCalls(r, "judge")),
|
|
1117
|
-
count("gate runs", (r) => sum(r.gates.map((g) => g.runs))),
|
|
1118
|
-
count("gate failures", (r) => sum(r.gates.map((g) => g.failed))),
|
|
1119
|
-
count("fabricated verification claims", (r) => sum(r.stories.map((s) => s.unverifiedClaims))),
|
|
1120
|
-
count("low-confidence tasks", (r) => sum(r.stories.map((s) => s.lowConfidenceTasks))),
|
|
1121
|
-
count(
|
|
1122
|
-
"stories whose prompt grew over 3×",
|
|
1123
|
-
(r) => r.stories.filter((s) => s.turns.promptGrowth).length
|
|
1124
|
-
),
|
|
1125
|
-
time("wall", (r) => r.wallMs),
|
|
1126
|
-
time("model time", (r) => r.totals.model),
|
|
1127
|
-
time("coder tool time", (r) => r.totals.tools),
|
|
1128
|
-
time("gate time", (r) => r.totals.gates),
|
|
1129
|
-
time("waiting", (r) => r.totals.waiting)
|
|
1130
|
-
]
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
const deltaCell = (row: DeltaRow): string => {
|
|
1135
|
-
const diff = row.after - row.before
|
|
1136
|
-
if (diff === 0) {
|
|
1137
|
-
return "="
|
|
1138
|
-
}
|
|
1139
|
-
const shown = row.unit === "ms" ? duration(Math.abs(diff)) : String(Math.abs(diff))
|
|
1140
|
-
const sign = diff > 0 ? "+" : "-"
|
|
1141
|
-
const verdict =
|
|
1142
|
-
row.better === "none" ? "" : diff < 0 === (row.better === "lower") ? " better" : " worse"
|
|
1143
|
-
const percent = row.before === 0 ? "" : ` (${Math.round((diff / row.before) * 100)}%)`
|
|
1144
|
-
return `${sign}${shown}${percent}${verdict}`
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
export const renderProfileDelta = (
|
|
1148
|
-
delta: ProfileDelta,
|
|
1149
|
-
labels: { readonly before: string; readonly after: string }
|
|
1150
|
-
): string =>
|
|
1151
|
-
[
|
|
1152
|
-
`llm4ts profile · ${labels.before} → ${labels.after}`,
|
|
1153
|
-
"",
|
|
1154
|
-
...table(
|
|
1155
|
-
["measure", "before", "after", "change"],
|
|
1156
|
-
delta.rows.map((row) => [
|
|
1157
|
-
row.label,
|
|
1158
|
-
row.unit === "ms" ? duration(row.before) : String(row.before),
|
|
1159
|
-
row.unit === "ms" ? duration(row.after) : String(row.after),
|
|
1160
|
-
deltaCell(row)
|
|
1161
|
-
])
|
|
1162
|
-
),
|
|
1163
|
-
"",
|
|
1164
|
-
"* tokens and cost per role: llm4ts costs --repo <repo>"
|
|
1165
|
-
].join("\n")
|
|
1166
|
-
|
|
1167
|
-
export interface ProfileOptions extends TraceChoice {
|
|
1168
|
-
readonly json?: boolean
|
|
1169
|
-
/** A trace, or a `--json` report saved earlier, to compare the profiled run against (ADR 0029). */
|
|
1170
|
-
readonly against?: string
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
const encodeReport = Schema.encodeSync(Schema.fromJsonString(ProfileReport))
|
|
1174
|
-
const decodeReport = Schema.decodeUnknownOption(Schema.fromJsonString(ProfileReport))
|
|
1175
|
-
const encodeDelta = Schema.encodeSync(Schema.fromJsonString(ProfileDelta))
|
|
1176
|
-
|
|
1177
|
-
const reportAt = Effect.fn("@llm4ts/runner/Profile.reportAt")(function* (
|
|
1178
|
-
files: TraceSources["files"],
|
|
1179
|
-
path: string
|
|
1180
|
-
): Effect.fn.Return<ProfileReport, FlowError | WatchTargetMissing> {
|
|
1181
|
-
if (path.endsWith(".json")) {
|
|
1182
|
-
const text = yield* files.read(path)
|
|
1183
|
-
const decoded = text === undefined ? undefined : decodeReport(text)
|
|
1184
|
-
if (decoded === undefined || decoded._tag === "None") {
|
|
1185
|
-
return yield* WatchTargetMissing.make({
|
|
1186
|
-
message: `${path} is not a profile report (write one with llm4ts profile --json)`
|
|
1187
|
-
})
|
|
1188
|
-
}
|
|
1189
|
-
return decoded.value
|
|
1190
|
-
}
|
|
1191
|
-
return profileOf(treeInputsOfTrace(yield* readTrace(files, path)))
|
|
1192
|
-
})
|
|
1193
|
-
|
|
1194
|
-
/**
|
|
1195
|
-
* The report for a trace, an epic's latest run, or the newest trace: text,
|
|
1196
|
-
* or JSON for comparing runs; with `against`, the delta from that earlier
|
|
1197
|
-
* run to this one.
|
|
1198
|
-
*/
|
|
1199
|
-
export const makeProfileProgram = Effect.fn("@llm4ts/runner/Profile.make")(function* (
|
|
1200
|
-
options: ProfileOptions,
|
|
1201
|
-
sources: TraceSources
|
|
1202
|
-
): Effect.fn.Return<string, FlowError | WatchTargetMissing> {
|
|
1203
|
-
const target = yield* resolveTraceTarget(options, sources)
|
|
1204
|
-
const report = profileOf(treeInputsOfTrace(yield* readTrace(sources.files, target.tracePath)))
|
|
1205
|
-
if (options.against !== undefined) {
|
|
1206
|
-
const before = yield* reportAt(sources.files, options.against)
|
|
1207
|
-
const delta = profileDelta(before, report)
|
|
1208
|
-
return options.json === true
|
|
1209
|
-
? encodeDelta(delta)
|
|
1210
|
-
: renderProfileDelta(delta, { before: options.against, after: target.tracePath })
|
|
1211
|
-
}
|
|
1212
|
-
return options.json === true ? encodeReport(report) : renderProfile(report)
|
|
1213
|
-
})
|