@llm4ts/runner 2.22.1 → 2.23.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/dist/AgentTree.js +1 -1
- package/dist/AgentTree.js.map +1 -1
- package/dist/FlowRunner.d.ts.map +1 -1
- package/dist/FlowRunner.js +10 -3
- package/dist/FlowRunner.js.map +1 -1
- package/dist/Profile.d.ts +55 -0
- package/dist/Profile.d.ts.map +1 -1
- package/dist/Profile.js +238 -12
- package/dist/Profile.js.map +1 -1
- package/package.json +3 -3
- package/src/AgentTree.ts +1 -1
- package/src/FlowRunner.ts +12 -4
- package/src/Profile.ts +276 -12
package/src/Profile.ts
CHANGED
|
@@ -47,10 +47,22 @@ export const TurnStats = Schema.Struct({
|
|
|
47
47
|
lastPrompt: Schema.optionalKey(Schema.Int),
|
|
48
48
|
maxPrompt: Schema.optionalKey(Schema.Int),
|
|
49
49
|
/** The last prompt is more than three times the first. */
|
|
50
|
-
promptGrowth: Schema.Boolean
|
|
50
|
+
promptGrowth: Schema.Boolean,
|
|
51
|
+
/** Tool calls per finished turn: each is a model round trip. */
|
|
52
|
+
avgSteps: Schema.Number,
|
|
53
|
+
/** The model's own time per round trip: a turn's time less its tools, over its steps + 1. */
|
|
54
|
+
avgStepMs: Ms
|
|
51
55
|
})
|
|
52
56
|
export type TurnStats = typeof TurnStats.Type
|
|
53
57
|
|
|
58
|
+
/** A stretch of a story's time no `Timed` event covers, named by the events around it. */
|
|
59
|
+
export const Gap = Schema.Struct({
|
|
60
|
+
ms: Ms,
|
|
61
|
+
after: Schema.String,
|
|
62
|
+
before: Schema.String
|
|
63
|
+
})
|
|
64
|
+
export type Gap = typeof Gap.Type
|
|
65
|
+
|
|
54
66
|
export const StoryProfile = Schema.Struct({
|
|
55
67
|
id: Schema.String,
|
|
56
68
|
executor: Schema.optionalKey(Schema.String),
|
|
@@ -59,7 +71,15 @@ export const StoryProfile = Schema.Struct({
|
|
|
59
71
|
/** From the plan being ready to this story starting: dependencies and free coder slots. */
|
|
60
72
|
queuedMs: Schema.optionalKey(Ms),
|
|
61
73
|
time: ProfileTime,
|
|
62
|
-
turns: TurnStats
|
|
74
|
+
turns: TurnStats,
|
|
75
|
+
/** Its largest untimed stretches, 30 seconds or more: where "unaccounted" sits. */
|
|
76
|
+
gaps: Schema.Array(Gap),
|
|
77
|
+
/**
|
|
78
|
+
* A coder turn the trace shows working (tool calls after its last finished
|
|
79
|
+
* call) but not ended: a run still going. Counted as model time so far.
|
|
80
|
+
*/
|
|
81
|
+
openTurnMs: Ms,
|
|
82
|
+
openTurnTools: Schema.Int
|
|
63
83
|
})
|
|
64
84
|
export type StoryProfile = typeof StoryProfile.Type
|
|
65
85
|
|
|
@@ -102,7 +122,11 @@ export const ProfileReport = Schema.Struct({
|
|
|
102
122
|
models: Schema.Array(ModelRow),
|
|
103
123
|
gates: Schema.Array(GateRow),
|
|
104
124
|
tools: Schema.Array(CountRow),
|
|
125
|
+
/** Coder tool time by kind of work: explore, edit, test, build, install, git, other. */
|
|
126
|
+
toolCategories: Schema.Array(CountRow),
|
|
105
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),
|
|
106
130
|
findings: Schema.Array(Finding)
|
|
107
131
|
})
|
|
108
132
|
export type ProfileReport = typeof ProfileReport.Type
|
|
@@ -128,6 +152,11 @@ interface Lane {
|
|
|
128
152
|
/** The last turn boundary: any lane event but a tool call inside a turn. */
|
|
129
153
|
last: number
|
|
130
154
|
skipped: boolean
|
|
155
|
+
/** Every event on the lane, described without its content, for the gaps. */
|
|
156
|
+
readonly moments: Array<{ readonly at: number; readonly what: string }>
|
|
157
|
+
/** Since when a coder turn has been working without ending, and its tool calls. */
|
|
158
|
+
openSince: number | undefined
|
|
159
|
+
openTools: number
|
|
131
160
|
}
|
|
132
161
|
|
|
133
162
|
interface Lease {
|
|
@@ -166,6 +195,93 @@ const groupBy = <A>(items: ReadonlyArray<A>, key: (item: A) => string): Map<stri
|
|
|
166
195
|
const laneOf = (event: FlowEvent): string | undefined =>
|
|
167
196
|
"lane" in event && typeof event.lane === "string" ? event.lane : undefined
|
|
168
197
|
|
|
198
|
+
/** An event as the gaps name it: its kind, never its content. */
|
|
199
|
+
const describe = (event: FlowEvent): string => {
|
|
200
|
+
switch (event._tag) {
|
|
201
|
+
case "StageStarted":
|
|
202
|
+
return "a stage start"
|
|
203
|
+
case "StageCompleted":
|
|
204
|
+
return "a stage end"
|
|
205
|
+
case "StageFailed":
|
|
206
|
+
return "a stage failure"
|
|
207
|
+
case "ToolUse":
|
|
208
|
+
return "a coder tool call"
|
|
209
|
+
case "TokensUsed":
|
|
210
|
+
return "a token report"
|
|
211
|
+
case "Info":
|
|
212
|
+
return "a log note"
|
|
213
|
+
case "ReviewFindings":
|
|
214
|
+
return "a review round"
|
|
215
|
+
case "StoryJudged":
|
|
216
|
+
return "a judge verdict"
|
|
217
|
+
case "AssistantMessage":
|
|
218
|
+
return "an assistant message"
|
|
219
|
+
case "CapabilityUsed":
|
|
220
|
+
case "CapabilityDenied":
|
|
221
|
+
case "CapabilityUnenforceable":
|
|
222
|
+
return "a capability check"
|
|
223
|
+
case "ExecutorLeased":
|
|
224
|
+
case "ExecutorReleased":
|
|
225
|
+
case "ExecutorExcluded":
|
|
226
|
+
case "ExecutorResumed":
|
|
227
|
+
case "ExecutorHandedOver":
|
|
228
|
+
return "a roster change"
|
|
229
|
+
case "Timed":
|
|
230
|
+
switch (event.kind) {
|
|
231
|
+
case "model":
|
|
232
|
+
return `the end of a ${event.label} call`
|
|
233
|
+
case "tool":
|
|
234
|
+
return `the end of tool ${event.label}`
|
|
235
|
+
case "gate":
|
|
236
|
+
return `the end of gate ${event.label}`
|
|
237
|
+
case "wait":
|
|
238
|
+
return `the end of a wait for the ${event.label}`
|
|
239
|
+
default:
|
|
240
|
+
return `the end of ${event.label}`
|
|
241
|
+
}
|
|
242
|
+
default:
|
|
243
|
+
return "an event"
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const gapThresholdMs = 30_000
|
|
248
|
+
|
|
249
|
+
/** The parts of each step between two lane events that no timed interval covers, largest first. */
|
|
250
|
+
const gapsOf = (lane: Lane): ReadonlyArray<Gap> => {
|
|
251
|
+
const covered = [
|
|
252
|
+
...lane.timed.map((item) => [item.at - item.event.ms, item.at] as const),
|
|
253
|
+
// A turn still open is model time, not a gap.
|
|
254
|
+
...(lane.openSince === undefined || lane.end !== undefined
|
|
255
|
+
? []
|
|
256
|
+
: [[lane.openSince, Number.POSITIVE_INFINITY] as const])
|
|
257
|
+
].sort((left, right) => left[0] - right[0])
|
|
258
|
+
const uncovered = (from: number, to: number): number => {
|
|
259
|
+
let free = 0
|
|
260
|
+
let cursor = from
|
|
261
|
+
for (const [start, end] of covered) {
|
|
262
|
+
if (end <= cursor || start >= to) {
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
free += Math.max(0, start - cursor)
|
|
266
|
+
cursor = Math.max(cursor, end)
|
|
267
|
+
}
|
|
268
|
+
return free + Math.max(0, to - cursor)
|
|
269
|
+
}
|
|
270
|
+
const moments = [...lane.moments].sort((left, right) => left.at - right.at)
|
|
271
|
+
return moments
|
|
272
|
+
.slice(1)
|
|
273
|
+
.flatMap((next, index) => {
|
|
274
|
+
const previous = moments[index]
|
|
275
|
+
if (previous === undefined) {
|
|
276
|
+
return []
|
|
277
|
+
}
|
|
278
|
+
const ms = uncovered(previous.at, next.at)
|
|
279
|
+
return ms >= gapThresholdMs ? [{ ms, after: previous.what, before: next.what }] : []
|
|
280
|
+
})
|
|
281
|
+
.sort((left, right) => right.ms - left.ms)
|
|
282
|
+
.slice(0, 3)
|
|
283
|
+
}
|
|
284
|
+
|
|
169
285
|
/** The roster role a seat label stands for, to find its lease. */
|
|
170
286
|
const leaseRole = (label: string): string => (label === "reasoning" ? "reviewer" : label)
|
|
171
287
|
|
|
@@ -200,7 +316,10 @@ export const profileOf = (inputs: ReadonlyArray<TreeInput>): ProfileReport => {
|
|
|
200
316
|
prompts: [],
|
|
201
317
|
estimatedCalls: [],
|
|
202
318
|
last: at,
|
|
203
|
-
skipped: false
|
|
319
|
+
skipped: false,
|
|
320
|
+
moments: [{ at, what: describe(event) }],
|
|
321
|
+
openSince: undefined,
|
|
322
|
+
openTools: 0
|
|
204
323
|
})
|
|
205
324
|
continue
|
|
206
325
|
}
|
|
@@ -219,8 +338,20 @@ export const profileOf = (inputs: ReadonlyArray<TreeInput>): ProfileReport => {
|
|
|
219
338
|
case "Timed": {
|
|
220
339
|
allTimed.push({ at, event })
|
|
221
340
|
lane?.timed.push({ at, event })
|
|
341
|
+
if (lane !== undefined && event.kind === "model" && event.label === "coder") {
|
|
342
|
+
lane.openSince = undefined
|
|
343
|
+
lane.openTools = 0
|
|
344
|
+
}
|
|
222
345
|
break
|
|
223
346
|
}
|
|
347
|
+
case "ToolUse":
|
|
348
|
+
if (lane !== undefined) {
|
|
349
|
+
// A tool call opens a turn until a coder call ends: it began at
|
|
350
|
+
// the last turn boundary.
|
|
351
|
+
lane.openSince = lane.openSince ?? lane.last
|
|
352
|
+
lane.openTools += 1
|
|
353
|
+
}
|
|
354
|
+
break
|
|
224
355
|
case "TokensUsed":
|
|
225
356
|
if (lane !== undefined) {
|
|
226
357
|
if (event.agent === "coder") {
|
|
@@ -241,6 +372,7 @@ export const profileOf = (inputs: ReadonlyArray<TreeInput>): ProfileReport => {
|
|
|
241
372
|
break
|
|
242
373
|
}
|
|
243
374
|
if (lane !== undefined) {
|
|
375
|
+
lane.moments.push({ at, what: describe(event) })
|
|
244
376
|
lane.executor = lane.executor ?? ("executor" in event ? event.executor : undefined)
|
|
245
377
|
if (!withinTurn.has(event._tag)) {
|
|
246
378
|
lane.last = at
|
|
@@ -324,7 +456,30 @@ export const profileOf = (inputs: ReadonlyArray<TreeInput>): ProfileReport => {
|
|
|
324
456
|
ms: sum(items.map((item) => item.event.ms))
|
|
325
457
|
}))
|
|
326
458
|
const tools = counted("tool")
|
|
459
|
+
const toolCategories = [
|
|
460
|
+
...groupBy(
|
|
461
|
+
allTimed.filter((item) => item.event.kind === "tool" && item.event.category !== undefined),
|
|
462
|
+
(item) => item.event.category ?? ""
|
|
463
|
+
).entries()
|
|
464
|
+
]
|
|
465
|
+
.map(([label, items]) => ({
|
|
466
|
+
label,
|
|
467
|
+
count: items.length,
|
|
468
|
+
ms: sum(items.map((item) => item.event.ms))
|
|
469
|
+
}))
|
|
470
|
+
.sort((left, right) => right.ms - left.ms)
|
|
327
471
|
const waits = counted("wait")
|
|
472
|
+
const steps = stepStats(estimated ? [] : [...lanes.values()].flatMap(turnSteps))
|
|
473
|
+
const outside = [
|
|
474
|
+
...groupBy(
|
|
475
|
+
allTimed.filter((item) => item.event.lane === undefined),
|
|
476
|
+
(item) => item.event.label
|
|
477
|
+
).entries()
|
|
478
|
+
].map(([label, items]) => ({
|
|
479
|
+
label,
|
|
480
|
+
count: items.length,
|
|
481
|
+
ms: sum(items.map((item) => item.event.ms))
|
|
482
|
+
}))
|
|
328
483
|
const merges = [...counted("merge"), ...counted("git")]
|
|
329
484
|
|
|
330
485
|
const totals = stories.reduce<ProfileTime>(
|
|
@@ -349,11 +504,49 @@ export const profileOf = (inputs: ReadonlyArray<TreeInput>): ProfileReport => {
|
|
|
349
504
|
models,
|
|
350
505
|
gates,
|
|
351
506
|
tools,
|
|
507
|
+
toolCategories,
|
|
352
508
|
waits,
|
|
353
|
-
|
|
509
|
+
outside,
|
|
510
|
+
findings: findingsOf({
|
|
511
|
+
steps,
|
|
512
|
+
toolCategories,
|
|
513
|
+
stories,
|
|
514
|
+
models,
|
|
515
|
+
gates,
|
|
516
|
+
tools,
|
|
517
|
+
waits,
|
|
518
|
+
merges,
|
|
519
|
+
storyTime,
|
|
520
|
+
estimated
|
|
521
|
+
})
|
|
354
522
|
}
|
|
355
523
|
}
|
|
356
524
|
|
|
525
|
+
const stepStats = (
|
|
526
|
+
turns: ReadonlyArray<{ readonly steps: number; readonly stepMs: number }>
|
|
527
|
+
): { readonly avgSteps: number; readonly avgStepMs: number } => ({
|
|
528
|
+
avgSteps: average(turns.map((turn) => turn.steps)) ?? 0,
|
|
529
|
+
avgStepMs: average(turns.map((turn) => turn.stepMs)) ?? 0
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
/** Each finished coder turn's steps (its tool calls) and model time per step. */
|
|
533
|
+
const turnSteps = (
|
|
534
|
+
lane: Lane
|
|
535
|
+
): ReadonlyArray<{ readonly steps: number; readonly stepMs: number }> => {
|
|
536
|
+
const tools = lane.timed.filter((item) => item.event.kind === "tool")
|
|
537
|
+
return lane.timed
|
|
538
|
+
.filter((item) => item.event.kind === "model" && item.event.label === "coder")
|
|
539
|
+
.map((turn) => {
|
|
540
|
+
const start = turn.at - turn.event.ms
|
|
541
|
+
const inside = tools.filter((tool) => tool.at > start && tool.at <= turn.at)
|
|
542
|
+
const toolMs = sum(inside.map((tool) => tool.event.ms))
|
|
543
|
+
return {
|
|
544
|
+
steps: inside.length,
|
|
545
|
+
stepMs: Math.max(0, turn.event.ms - toolMs) / (inside.length + 1)
|
|
546
|
+
}
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
|
|
357
550
|
const storyProfile = (
|
|
358
551
|
lane: Lane,
|
|
359
552
|
lastAt: number,
|
|
@@ -367,7 +560,11 @@ const storyProfile = (
|
|
|
367
560
|
.filter((item) => item.event.kind === "model" && item.event.label === "coder")
|
|
368
561
|
.map((item) => item.event.ms)
|
|
369
562
|
const tools = of("tool")
|
|
370
|
-
|
|
563
|
+
// Only a story still running has a turn in progress; in a finished one an
|
|
564
|
+
// unclosed turn is a timing that never came, left to the gaps.
|
|
565
|
+
const open = !estimated && lane.end === undefined && lane.openSince !== undefined
|
|
566
|
+
const openTurnMs = open && lane.openSince !== undefined ? lastAt - lane.openSince : 0
|
|
567
|
+
const model = estimated ? sum(lane.estimatedCalls) : Math.max(0, of("model") + openTurnMs - tools)
|
|
371
568
|
const gates = of("gate")
|
|
372
569
|
const merge = of("merge") + of("git")
|
|
373
570
|
const waiting = of("wait")
|
|
@@ -376,6 +573,9 @@ const storyProfile = (
|
|
|
376
573
|
const lastPrompt = lane.prompts.at(-1)
|
|
377
574
|
const maxPrompt = lane.prompts.length === 0 ? undefined : Math.max(...lane.prompts)
|
|
378
575
|
return {
|
|
576
|
+
gaps: estimated ? [] : gapsOf(lane),
|
|
577
|
+
openTurnMs,
|
|
578
|
+
openTurnTools: open ? lane.openTools : 0,
|
|
379
579
|
id: lane.id,
|
|
380
580
|
...(lane.executor === undefined ? {} : { executor: lane.executor }),
|
|
381
581
|
status: lane.status,
|
|
@@ -396,6 +596,7 @@ const storyProfile = (
|
|
|
396
596
|
...(firstPrompt === undefined ? {} : { firstPrompt }),
|
|
397
597
|
...(lastPrompt === undefined ? {} : { lastPrompt }),
|
|
398
598
|
...(maxPrompt === undefined ? {} : { maxPrompt }),
|
|
599
|
+
...stepStats(estimated ? [] : turnSteps(lane)),
|
|
399
600
|
promptGrowth:
|
|
400
601
|
firstPrompt !== undefined &&
|
|
401
602
|
lastPrompt !== undefined &&
|
|
@@ -420,11 +621,16 @@ export const duration = (ms: number): string => {
|
|
|
420
621
|
return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, "0")}m`
|
|
421
622
|
}
|
|
422
623
|
|
|
624
|
+
const stepsText = (steps: number): string =>
|
|
625
|
+
`${Number.isInteger(steps) ? steps : steps.toFixed(1)} step${steps === 1 ? "" : "s"}`
|
|
626
|
+
|
|
423
627
|
const share = (ms: number, of: number): string => (of <= 0 ? "" : `${Math.round((ms / of) * 100)}%`)
|
|
424
628
|
|
|
425
629
|
const plural = (count: number, word: string): string => `${count} ${word}${count === 1 ? "" : "s"}`
|
|
426
630
|
|
|
427
631
|
const findingsOf = (facts: {
|
|
632
|
+
readonly steps: { readonly avgSteps: number; readonly avgStepMs: number }
|
|
633
|
+
readonly toolCategories: ReadonlyArray<CountRow>
|
|
428
634
|
readonly stories: ReadonlyArray<StoryProfile>
|
|
429
635
|
readonly models: ReadonlyArray<ModelRow>
|
|
430
636
|
readonly gates: ReadonlyArray<GateRow>
|
|
@@ -445,7 +651,11 @@ const findingsOf = (facts: {
|
|
|
445
651
|
ms,
|
|
446
652
|
text:
|
|
447
653
|
role === "coder"
|
|
448
|
-
? `coder turns: ${calls}, ${ofStories(ms)}, avg ${duration(ms / calls)}${first}
|
|
654
|
+
? `coder turns: ${calls}, ${ofStories(ms)}, avg ${duration(ms / calls)}${first}${
|
|
655
|
+
facts.steps.avgSteps > 0
|
|
656
|
+
? `, ${stepsText(facts.steps.avgSteps)} per turn, ${duration(facts.steps.avgStepMs)} of model per step`
|
|
657
|
+
: ""
|
|
658
|
+
}`
|
|
449
659
|
: `${role} calls: ${calls}, ${ofStories(ms)}, avg ${duration(ms / calls)}${first}`
|
|
450
660
|
}
|
|
451
661
|
})
|
|
@@ -457,10 +667,16 @@ const findingsOf = (facts: {
|
|
|
457
667
|
row.failed === 0 ? "" : `, ${row.failed} failed`
|
|
458
668
|
}`
|
|
459
669
|
})),
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
670
|
+
// By kind of work when the trace has it (2.23), else by tool name.
|
|
671
|
+
...(facts.toolCategories.length > 0
|
|
672
|
+
? facts.toolCategories.map((row) => ({
|
|
673
|
+
ms: row.ms,
|
|
674
|
+
text: `coder tools, ${row.label}: ${plural(row.count, "call")}, ${ofStories(row.ms)}`
|
|
675
|
+
}))
|
|
676
|
+
: facts.tools.map((row) => ({
|
|
677
|
+
ms: row.ms,
|
|
678
|
+
text: `coder tool \`${row.label}\`: ${plural(row.count, "call")}, ${ofStories(row.ms)}`
|
|
679
|
+
}))),
|
|
464
680
|
...facts.waits.map((row) => ({
|
|
465
681
|
ms: row.ms,
|
|
466
682
|
text: `waiting for the ${row.label}: ${row.count}×, ${ofStories(row.ms)}`
|
|
@@ -470,6 +686,14 @@ const findingsOf = (facts: {
|
|
|
470
686
|
text: `${row.label}: ${row.count}×, ${ofStories(row.ms)}`
|
|
471
687
|
}))
|
|
472
688
|
]
|
|
689
|
+
for (const story of facts.stories) {
|
|
690
|
+
if (story.openTurnMs >= 1_000) {
|
|
691
|
+
candidates.push({
|
|
692
|
+
ms: story.openTurnMs,
|
|
693
|
+
text: `${story.id}: a coder turn still running for ${duration(story.openTurnMs)} (${plural(story.openTurnTools, "tool call")} so far)`
|
|
694
|
+
})
|
|
695
|
+
}
|
|
696
|
+
}
|
|
473
697
|
const queued = sum(facts.stories.map((story) => story.queuedMs ?? 0))
|
|
474
698
|
if (queued >= 1_000) {
|
|
475
699
|
candidates.push({
|
|
@@ -519,7 +743,7 @@ export const renderProfile = (report: ProfileReport): string => {
|
|
|
519
743
|
[report.estimated ? "model + tools + gates (estimated)" : "model", report.totals.model],
|
|
520
744
|
["coder tools", report.totals.tools],
|
|
521
745
|
["gates", report.totals.gates],
|
|
522
|
-
["merge", report.totals.merge],
|
|
746
|
+
["git + merge", report.totals.merge],
|
|
523
747
|
["waiting", report.totals.waiting],
|
|
524
748
|
["unaccounted", report.totals.unaccounted]
|
|
525
749
|
]
|
|
@@ -551,11 +775,13 @@ export const renderProfile = (report: ProfileReport): string => {
|
|
|
551
775
|
"model",
|
|
552
776
|
"tools",
|
|
553
777
|
"gates",
|
|
554
|
-
"merge",
|
|
778
|
+
"git+merge",
|
|
555
779
|
"wait",
|
|
556
780
|
"other",
|
|
557
781
|
"turns",
|
|
558
782
|
"avg turn",
|
|
783
|
+
"steps/turn",
|
|
784
|
+
"model/step",
|
|
559
785
|
"prompt ×"
|
|
560
786
|
],
|
|
561
787
|
report.stories.map((story) => [
|
|
@@ -571,6 +797,8 @@ export const renderProfile = (report: ProfileReport): string => {
|
|
|
571
797
|
duration(story.time.unaccounted),
|
|
572
798
|
String(story.turns.count),
|
|
573
799
|
duration(story.turns.avgMs),
|
|
800
|
+
story.turns.avgSteps === 0 ? "" : story.turns.avgSteps.toFixed(1),
|
|
801
|
+
story.turns.avgSteps === 0 ? "" : duration(story.turns.avgStepMs),
|
|
574
802
|
story.turns.firstPrompt !== undefined &&
|
|
575
803
|
story.turns.lastPrompt !== undefined &&
|
|
576
804
|
story.turns.firstPrompt > 0
|
|
@@ -581,6 +809,27 @@ export const renderProfile = (report: ProfileReport): string => {
|
|
|
581
809
|
...(report.skipped.length === 0
|
|
582
810
|
? []
|
|
583
811
|
: [` skipped (merged on an earlier run): ${report.skipped.join(", ")}`]),
|
|
812
|
+
...(report.stories.every((story) => story.gaps.length === 0)
|
|
813
|
+
? []
|
|
814
|
+
: [
|
|
815
|
+
"",
|
|
816
|
+
"Unaccounted, largest gaps (no timing covers them)",
|
|
817
|
+
...report.stories.flatMap((story) =>
|
|
818
|
+
story.gaps.map(
|
|
819
|
+
(gap) => ` ${story.id}: ${duration(gap.ms)} between ${gap.after} and ${gap.before}`
|
|
820
|
+
)
|
|
821
|
+
)
|
|
822
|
+
]),
|
|
823
|
+
...(report.outside.length === 0
|
|
824
|
+
? []
|
|
825
|
+
: [
|
|
826
|
+
"",
|
|
827
|
+
"Timed outside any story (the run's own planning, or a seat no story owns)",
|
|
828
|
+
...table(
|
|
829
|
+
["label", "count", "total"],
|
|
830
|
+
report.outside.map((row) => [row.label, String(row.count), duration(row.ms)])
|
|
831
|
+
)
|
|
832
|
+
]),
|
|
584
833
|
...(report.models.length === 0
|
|
585
834
|
? []
|
|
586
835
|
: [
|
|
@@ -624,6 +873,21 @@ export const renderProfile = (report: ProfileReport): string => {
|
|
|
624
873
|
String(row.failed)
|
|
625
874
|
])
|
|
626
875
|
)
|
|
876
|
+
]),
|
|
877
|
+
...(report.toolCategories.length === 0
|
|
878
|
+
? []
|
|
879
|
+
: [
|
|
880
|
+
"",
|
|
881
|
+
"Coder tools by kind",
|
|
882
|
+
...table(
|
|
883
|
+
["kind", "calls", "total", "avg"],
|
|
884
|
+
report.toolCategories.map((row) => [
|
|
885
|
+
row.label,
|
|
886
|
+
String(row.count),
|
|
887
|
+
duration(row.ms),
|
|
888
|
+
duration(row.ms / row.count)
|
|
889
|
+
])
|
|
890
|
+
)
|
|
627
891
|
])
|
|
628
892
|
].join("\n")
|
|
629
893
|
}
|