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