@llm4ts/runner 3.3.0 → 3.4.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 (93) 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 +33 -2
  14. package/dist/Connectors.d.ts.map +1 -1
  15. package/dist/Connectors.js +85 -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 +133 -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 +3 -1
  32. package/dist/FlowRunner.d.ts.map +1 -1
  33. package/dist/FlowRunner.js +31 -38
  34. package/dist/FlowRunner.js.map +1 -1
  35. package/dist/NodeGeminiCliExecutor.d.ts.map +1 -1
  36. package/dist/NodeGeminiCliExecutor.js +6 -4
  37. package/dist/NodeGeminiCliExecutor.js.map +1 -1
  38. package/dist/NodeGeminiModelBridge.d.ts.map +1 -1
  39. package/dist/NodeGeminiModelBridge.js +3 -1
  40. package/dist/NodeGeminiModelBridge.js.map +1 -1
  41. package/dist/Otel.d.ts.map +1 -1
  42. package/dist/Otel.js +4 -5
  43. package/dist/Otel.js.map +1 -1
  44. package/dist/Packs.d.ts.map +1 -1
  45. package/dist/Packs.js +2 -1
  46. package/dist/Packs.js.map +1 -1
  47. package/dist/PiModels.d.ts.map +1 -1
  48. package/dist/PiModels.js +6 -9
  49. package/dist/PiModels.js.map +1 -1
  50. package/dist/Profile.d.ts +4 -0
  51. package/dist/Profile.d.ts.map +1 -1
  52. package/dist/Profile.js +45 -75
  53. package/dist/Profile.js.map +1 -1
  54. package/dist/RunReaders.d.ts +1 -0
  55. package/dist/RunReaders.d.ts.map +1 -1
  56. package/dist/RunReaders.js +4 -51
  57. package/dist/RunReaders.js.map +1 -1
  58. package/dist/Terminal.d.ts +14 -11
  59. package/dist/Terminal.d.ts.map +1 -1
  60. package/dist/Terminal.js +25 -78
  61. package/dist/Terminal.js.map +1 -1
  62. package/dist/Tools.d.ts.map +1 -1
  63. package/dist/Tools.js +2 -1
  64. package/dist/Tools.js.map +1 -1
  65. package/dist/Transcripts.d.ts +1 -1
  66. package/dist/Transcripts.d.ts.map +1 -1
  67. package/dist/Transcripts.js +3 -2
  68. package/dist/Transcripts.js.map +1 -1
  69. package/dist/index.d.ts +1 -1
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +1 -1
  72. package/dist/index.js.map +1 -1
  73. package/package.json +4 -3
  74. package/src/AgentTree.ts +16 -25
  75. package/src/AgentTreeSurface.ts +6 -11
  76. package/src/Cli.ts +2 -1
  77. package/src/Connectors.ts +141 -107
  78. package/src/Doctor.ts +10 -12
  79. package/src/EventPresentation.ts +168 -0
  80. package/src/ExecutorRoster.ts +4 -6
  81. package/src/FlowArgs.ts +7 -2
  82. package/src/FlowRunner.ts +41 -55
  83. package/src/NodeGeminiCliExecutor.ts +7 -4
  84. package/src/NodeGeminiModelBridge.ts +4 -1
  85. package/src/Otel.ts +4 -6
  86. package/src/Packs.ts +2 -1
  87. package/src/PiModels.ts +6 -10
  88. package/src/Profile.ts +64 -81
  89. package/src/RunReaders.ts +4 -53
  90. package/src/Terminal.ts +105 -162
  91. package/src/Tools.ts +2 -1
  92. package/src/Transcripts.ts +3 -2
  93. package/src/index.ts +3 -0
package/src/Profile.ts CHANGED
@@ -1,11 +1,19 @@
1
1
  import * as Effect from "effect/Effect"
2
2
  import * as Schema from "effect/Schema"
3
3
  import type { FlowError } from "@llm4ts/flow/FlowError"
4
- import type { FlowEvent, Timed } from "@llm4ts/flow/FlowEvents"
4
+ import {
5
+ LaneKind,
6
+ laneClosed,
7
+ laneOf,
8
+ laneOpened,
9
+ type FlowEvent,
10
+ type Timed
11
+ } from "@llm4ts/flow/FlowEvents"
5
12
  import { readTrace } from "@llm4ts/flow/Replay"
6
13
  import { duration, treeInputsOfTrace, type TreeInput } from "./AgentTree.ts"
7
14
 
8
15
  export { duration }
16
+ import { presentationOf } from "./EventPresentation.ts"
9
17
  import { formatCount } from "./Terminal.ts"
10
18
  import {
11
19
  resolveTraceTarget,
@@ -81,6 +89,8 @@ export type ToolCalls = typeof ToolCalls.Type
81
89
 
82
90
  export const StoryProfile = Schema.Struct({
83
91
  id: Schema.String,
92
+ /** What the lane stands for when it is not a story: a roundtable attendee, a taskforce specialist. */
93
+ kind: Schema.optionalKey(LaneKind),
84
94
  executor: Schema.optionalKey(Schema.String),
85
95
  status: Schema.Literals(["done", "failed", "running"]),
86
96
  wallMs: Ms,
@@ -169,6 +179,7 @@ interface TimedAt {
169
179
 
170
180
  interface Lane {
171
181
  readonly id: string
182
+ readonly kind: LaneKind
172
183
  start: number
173
184
  end: number | undefined
174
185
  status: "done" | "failed" | "running"
@@ -204,8 +215,6 @@ interface Lease {
204
215
  readonly label: string | undefined
205
216
  }
206
217
 
207
- const storyStage = /^story (\S+)$/u
208
-
209
218
  /** Events a coder turn emits while it runs: not where a model call starts. */
210
219
  const withinTurn: ReadonlySet<FlowEvent["_tag"]> = new Set([
211
220
  "ToolUse",
@@ -230,66 +239,21 @@ const groupBy = <A>(items: ReadonlyArray<A>, key: (item: A) => string): Map<stri
230
239
  return groups
231
240
  }
232
241
 
233
- const laneOf = (event: FlowEvent): string | undefined =>
234
- "lane" in event && typeof event.lane === "string" ? event.lane : undefined
235
-
236
242
  /** An event as the gaps name it: its kind, never its content. */
237
243
  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
- }
244
+ const { kind } = presentationOf(event)
245
+ return typeof kind === "function" ? kind(event) : kind
291
246
  }
292
247
 
248
+ const bareLaneStage = /^(\S+) (\S+)$/u
249
+
250
+ /**
251
+ * A stage that is the lane's own and not one of its coder's tasks: its steps
252
+ * (`story home: setup`) and a bare `<kind> <id>` that names another lane.
253
+ */
254
+ const ownStage = (lane: Lane, stage: string): boolean =>
255
+ bareLaneStage.exec(stage)?.[1] === lane.kind || stage.startsWith(`${lane.kind} ${lane.id}:`)
256
+
293
257
  const gapThresholdMs = 30_000
294
258
 
295
259
  /** The parts of each step between two lane events that no timed interval covers, largest first. */
@@ -346,14 +310,15 @@ export const profileOf = (inputs: ReadonlyArray<TreeInput>): ProfileReport => {
346
310
  continue
347
311
  }
348
312
  const { event, at } = input
349
- const laneId = laneOf(event)
350
- const lane = laneId === undefined ? undefined : lanes.get(laneId)
313
+ const tagged = laneOf(event)
314
+ const lane = tagged === undefined ? undefined : lanes.get(tagged.lane)
351
315
  switch (event._tag) {
352
316
  case "StageStarted": {
353
- const story = storyStage.exec(event.stage)?.[1]
354
- if (story !== undefined && event.lane === story) {
355
- lanes.set(story, {
356
- id: story,
317
+ const opened = laneOpened(event)
318
+ if (opened !== undefined) {
319
+ lanes.set(opened.id, {
320
+ id: opened.id,
321
+ kind: opened.kind,
357
322
  start: at,
358
323
  end: undefined,
359
324
  status: "running",
@@ -377,11 +342,7 @@ export const profileOf = (inputs: ReadonlyArray<TreeInput>): ProfileReport => {
377
342
  })
378
343
  continue
379
344
  }
380
- if (
381
- lane !== undefined &&
382
- !storyStage.test(event.stage) &&
383
- !event.stage.startsWith(`story ${lane.id}:`)
384
- ) {
345
+ if (lane !== undefined && !ownStage(lane, event.stage)) {
385
346
  lane.tasks += 1
386
347
  lane.firstTask = lane.firstTask ?? at
387
348
  }
@@ -392,7 +353,7 @@ export const profileOf = (inputs: ReadonlyArray<TreeInput>): ProfileReport => {
392
353
  if (event.stage === "story plan" && event.lane === undefined) {
393
354
  planReady = at
394
355
  }
395
- if (lane !== undefined && event.stage === `story ${lane.id}`) {
356
+ if (lane !== undefined && laneClosed(event)?.kind === lane.kind) {
396
357
  lane.end = at
397
358
  lane.status = event._tag === "StageFailed" ? "failed" : "done"
398
359
  }
@@ -462,7 +423,7 @@ export const profileOf = (inputs: ReadonlyArray<TreeInput>): ProfileReport => {
462
423
  }
463
424
  if (lane !== undefined) {
464
425
  lane.moments.push({ at, what: describe(event) })
465
- lane.executor = lane.executor ?? ("executor" in event ? event.executor : undefined)
426
+ lane.executor = lane.executor ?? tagged?.executor
466
427
  if (!withinTurn.has(event._tag)) {
467
428
  lane.last = at
468
429
  }
@@ -673,6 +634,7 @@ const storyProfile = (
673
634
  lowConfidenceTasks: lane.lowConfidenceTasks,
674
635
  ...(lane.reviewDiffLines === undefined ? {} : { reviewDiffLines: lane.reviewDiffLines }),
675
636
  id: lane.id,
637
+ ...(lane.kind === "story" ? {} : { kind: lane.kind }),
676
638
  ...(lane.executor === undefined ? {} : { executor: lane.executor }),
677
639
  status: lane.status,
678
640
  wallMs,
@@ -711,6 +673,24 @@ const share = (ms: number, of: number): string => (of <= 0 ? "" : `${Math.round(
711
673
 
712
674
  const plural = (count: number, word: string): string => `${count} ${word}${count === 1 ? "" : "s"}`
713
675
 
676
+ interface LaneWord {
677
+ readonly one: string
678
+ readonly many: string
679
+ }
680
+
681
+ /** What a report calls its lanes: their kind when they share one, stories when it has none. */
682
+ const laneWord = (stories: ReadonlyArray<StoryProfile>): LaneWord => {
683
+ const kinds = new Set(stories.map((story) => story.kind ?? "story"))
684
+ const [kind = "story"] = kinds
685
+ return kinds.size > 1
686
+ ? { one: "lane", many: "lanes" }
687
+ : kind === "story"
688
+ ? { one: "story", many: "stories" }
689
+ : { one: kind, many: `${kind}s` }
690
+ }
691
+
692
+ const capitalized = (word: string): string => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`
693
+
714
694
  const findingsOf = (facts: {
715
695
  readonly steps: { readonly avgSteps: number; readonly avgStepMs: number }
716
696
  readonly toolCategories: ReadonlyArray<CountRow>
@@ -723,7 +703,9 @@ const findingsOf = (facts: {
723
703
  readonly storyTime: number
724
704
  readonly estimated: boolean
725
705
  }): ReadonlyArray<Finding> => {
726
- const ofStories = (ms: number) => `${duration(ms)} (${share(ms, facts.storyTime)} of story time)`
706
+ const word = laneWord(facts.stories)
707
+ const ofStories = (ms: number) =>
708
+ `${duration(ms)} (${share(ms, facts.storyTime)} of ${word.one} time)`
727
709
  const byRole = [...groupBy(facts.models, (row) => row.role).entries()].map(([role, rows]) => {
728
710
  const calls = sum(rows.map((row) => row.calls))
729
711
  const ms = sum(rows.map((row) => row.ms))
@@ -853,6 +835,7 @@ const table = (
853
835
 
854
836
  export const renderProfile = (report: ProfileReport): string => {
855
837
  const storyTime = sum(report.stories.map((story) => story.wallMs))
838
+ const word = laneWord(report.stories)
856
839
  const categories: ReadonlyArray<readonly [string, number]> = [
857
840
  [report.estimated ? "model + tools + gates (estimated)" : "model", report.totals.model],
858
841
  ["coder tools", report.totals.tools],
@@ -862,7 +845,7 @@ export const renderProfile = (report: ProfileReport): string => {
862
845
  ["unaccounted", report.totals.unaccounted]
863
846
  ]
864
847
  return [
865
- `llm4ts profile · ${duration(report.wallMs)} wall · ${report.stories.length} ${report.stories.length === 1 ? "story" : "stories"}${
848
+ `llm4ts profile · ${duration(report.wallMs)} wall · ${report.stories.length} ${report.stories.length === 1 ? word.one : word.many}${
866
849
  report.estimated
867
850
  ? " · estimated: this trace has no timings (before 2.22), model time is read from gaps"
868
851
  : ""
@@ -873,16 +856,16 @@ export const renderProfile = (report: ProfileReport): string => {
873
856
  ? [" nothing timed yet"]
874
857
  : report.findings.map((finding, index) => ` ${index + 1}. ${finding.text}`)),
875
858
  "",
876
- "Story time",
859
+ `${capitalized(word.one)} time`,
877
860
  ...table(
878
861
  ["category", "time", "share"],
879
862
  categories.map(([name, ms]) => [name, duration(ms), share(ms, storyTime)])
880
863
  ),
881
864
  "",
882
- "Stories",
865
+ capitalized(word.many),
883
866
  ...table(
884
867
  [
885
- "story",
868
+ word.one,
886
869
  "status",
887
870
  "wall",
888
871
  "queued",
@@ -932,10 +915,10 @@ export const renderProfile = (report: ProfileReport): string => {
932
915
  ? []
933
916
  : [
934
917
  "",
935
- "Coder work per story",
918
+ `Coder work per ${word.one}`,
936
919
  ...table(
937
920
  [
938
- "story",
921
+ word.one,
939
922
  "tasks",
940
923
  "explore",
941
924
  "edit",
@@ -962,7 +945,7 @@ export const renderProfile = (report: ProfileReport): string => {
962
945
  ? []
963
946
  : [
964
947
  "",
965
- "Largest review diff per story",
948
+ `Largest review diff per ${word.one}`,
966
949
  ...report.stories.flatMap((story) =>
967
950
  story.reviewDiffLines === undefined
968
951
  ? []
@@ -987,7 +970,7 @@ export const renderProfile = (report: ProfileReport): string => {
987
970
  ? []
988
971
  : [
989
972
  "",
990
- "Timed outside any story (the run's own planning, or a seat no story owns)",
973
+ `Timed outside any ${word.one} (the run's own planning, or a seat no ${word.one} owns)`,
991
974
  ...table(
992
975
  ["label", "count", "total"],
993
976
  report.outside.map((row) => [row.label, String(row.count), duration(row.ms)])
package/src/RunReaders.ts CHANGED
@@ -24,6 +24,7 @@ import { loadVersioned, type PlainFileStoreShape } from "@llm4ts/flow/Persistenc
24
24
  import { parsePlan } from "@llm4ts/flow/Plan"
25
25
  import { readTrace } from "@llm4ts/flow/Replay"
26
26
  import { emptyTree, reduceTree, treeInputsOfTrace, type TreeState } from "./AgentTree.ts"
27
+ import { digestedTags, presentationOf, type DigestLine } from "./EventPresentation.ts"
27
28
  import { summarizeTree } from "./RunSummary.ts"
28
29
  import { loadTranscript, tailLinesOf, type TranscriptFiles } from "./Transcripts.ts"
29
30
 
@@ -146,60 +147,10 @@ export const runStatusOf = (
146
147
  })
147
148
  }
148
149
 
149
- export const meaningfulKinds: ReadonlySet<string> = new Set([
150
- "StoryStatusChanged",
151
- "StageFailed",
152
- "Aborted",
153
- "CapabilityUnenforceable",
154
- "ExecutorExcluded",
155
- "ExecutorHandedOver",
156
- "StoryJudged",
157
- "Began",
158
- "RunEnded",
159
- "CriterionChecked",
160
- "Parked"
161
- ])
150
+ /** Trace kinds a digest shows: the events with a digest line, and the run's end (a trace line, not an event). */
151
+ export const meaningfulKinds: ReadonlySet<string> = new Set([...digestedTags, "RunEnded"])
162
152
 
163
- const textOf = (
164
- event: FlowEvent
165
- ): { readonly text: string; readonly lane?: string } | undefined => {
166
- switch (event._tag) {
167
- case "StoryStatusChanged":
168
- return { text: `story ${event.id} ${event.status}`, lane: event.id }
169
- case "CriterionChecked":
170
- return { text: `criterion ${event.tag} ${event.status}`, lane: event.story }
171
- case "Parked":
172
- return { text: `parked ${event.item}: ${event.reason}`, lane: event.story }
173
- case "StageFailed":
174
- return {
175
- text: `${event.stage} failed: ${event.message}`,
176
- ...(event.lane === undefined ? {} : { lane: event.lane })
177
- }
178
- case "Aborted":
179
- return { text: `aborted: ${event.message}` }
180
- case "CapabilityUnenforceable":
181
- return { text: `capability unenforceable: ${event.detail}` }
182
- case "ExecutorExcluded":
183
- return { text: `${event.executor} excluded: ${event.reason}` }
184
- case "ExecutorHandedOver":
185
- return {
186
- text: `${event.from} handed over ${event.role} (${event.scope}): ${event.reason}`,
187
- ...(event.label === undefined ? {} : { lane: event.label })
188
- }
189
- case "StoryJudged":
190
- return {
191
- text: `${event.lane} judged round ${event.round}: ${event.cleared ? "cleared" : `${event.issues} issues`}`,
192
- lane: event.lane
193
- }
194
- case "Began":
195
- return {
196
- text: `began ${event.kind} ${event.label}`,
197
- ...(event.lane === undefined ? {} : { lane: event.lane })
198
- }
199
- default:
200
- return undefined
201
- }
202
- }
153
+ const textOf = (event: FlowEvent): DigestLine | undefined => presentationOf(event).digest?.(event)
203
154
 
204
155
  export const meaningfulEventsAfter = (
205
156
  lines: ReadonlyArray<TraceLine>,