@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/Terminal.ts CHANGED
@@ -5,18 +5,18 @@ import * as Effect from "effect/Effect"
5
5
  import * as Ref from "effect/Ref"
6
6
  import * as Semaphore from "effect/Semaphore"
7
7
  import type * as Scope from "effect/Scope"
8
- import * as Schema from "effect/Schema"
9
- import * as Stream from "effect/Stream"
10
8
  import {
11
- awaitConsumed,
9
+ eventLanes,
12
10
  type FlowEvent,
13
11
  type FlowEventHub,
12
+ laneOf,
13
+ type LaneRef,
14
14
  rosterEventMessage,
15
15
  type ReviewFindings
16
16
  } from "@llm4ts/flow/FlowEvents"
17
+ import { atLeast, presentationOf, Verbosity } from "./EventPresentation.ts"
17
18
 
18
- export const Verbosity = Schema.Literals(["Quiet", "Normal", "Verbose", "Debug"])
19
- export type Verbosity = typeof Verbosity.Type
19
+ export { Verbosity }
20
20
 
21
21
  export const parseVerbosity = (value: string | undefined): Verbosity => {
22
22
  switch (value?.trim().toLowerCase()) {
@@ -32,45 +32,10 @@ export const parseVerbosity = (value: string | undefined): Verbosity => {
32
32
  }
33
33
 
34
34
  export const rendersEvent = (verbosity: Verbosity, event: FlowEvent): boolean => {
35
- switch (event._tag) {
36
- // Advise mode publishes a separate Info event for the operator; progress
37
- // feeds the status rows; a blackboard run is for the trace file; a lease
38
- // ending is routine; the story's outcome already says whether the judge
39
- // cleared it. None is a line of its own.
40
- case "JudgmentObserved":
41
- case "BlackboardRun":
42
- case "UsageProgress":
43
- case "ExecutorReleased":
44
- case "StoryJudged":
45
- case "RoundtableTurn":
46
- case "BriefReported":
47
- case "Timed":
48
- case "TasksPlanned":
49
- case "TaskStarted":
50
- case "TaskCompleted":
51
- case "StoryStatusChanged":
52
- return false
53
- // A gate or a setup may run for many minutes: its start is a line, so a
54
- // long one reads as running, not stuck. Other work starts too often.
55
- case "Began":
56
- return event.kind === "gate" && verbosity !== "Quiet"
57
- case "StageStarted":
58
- case "StageCompleted":
59
- case "StageFailed":
60
- case "Aborted":
61
- case "CapabilityDenied":
62
- case "CapabilityUnenforceable":
63
- case "EvidenceChecked":
64
- case "ReviewFindingDemoted":
65
- return true
66
- // Every git read and gate command is a capability event: useful in a
67
- // trace, noise on screen once several stories run.
68
- case "TokensUsed":
69
- case "CapabilityUsed":
70
- return verbosity === "Verbose" || verbosity === "Debug"
71
- default:
72
- return verbosity !== "Quiet"
73
- }
35
+ const { line } = presentationOf(event)
36
+ return typeof line === "function"
37
+ ? line(event, verbosity)
38
+ : line !== "never" && atLeast(verbosity, line)
74
39
  }
75
40
 
76
41
  export const terminalSafe = (value: string): string => {
@@ -548,7 +513,6 @@ export interface TerminalRunStats {
548
513
  }
549
514
 
550
515
  export interface TerminalConsumer {
551
- readonly consumed: Ref.Ref<number>
552
516
  readonly stats: Effect.Effect<TerminalRunStats>
553
517
  readonly awaitDrained: (timeout?: Duration.Input) => Effect.Effect<void>
554
518
  }
@@ -562,33 +526,20 @@ interface OpenStage {
562
526
  readonly startedAt: number
563
527
  }
564
528
 
565
- /** The lane an event belongs to (a concurrent story), if it carries one. */
566
- export const laneOfEvent = (
567
- event: FlowEvent
568
- ): { readonly lane: string; readonly executor?: string } | undefined => {
569
- switch (event._tag) {
570
- case "StageStarted":
571
- case "StageCompleted":
572
- case "StageFailed":
573
- case "Info":
574
- case "ToolUse":
575
- case "AssistantMessage":
576
- case "TokensUsed":
577
- case "UsageProgress":
578
- case "ReviewFindings":
579
- case "TasksPlanned":
580
- case "TaskStarted":
581
- case "TaskCompleted":
582
- return event.lane === undefined
583
- ? undefined
584
- : {
585
- lane: event.lane,
586
- ...(event.executor === undefined ? {} : { executor: event.executor })
587
- }
588
- default:
589
- return undefined
590
- }
591
- }
529
+ /**
530
+ * The lane an event belongs to on screen (a concurrent story), if it carries
531
+ * one. This is `laneOf` with two exclusions, both kept as they were:
532
+ * - an event whose producer names its lane (a verdict, a roundtable turn, an
533
+ * evidence check, a parked item) prints, when it prints, as a line of the
534
+ * run: only the events `withLane` stamps drive a lane's row;
535
+ * - `Timed` and `Began`: one arriving after its lane's last stage closed
536
+ * would reopen a row that never closes (`UsageProgress` has the same
537
+ * hazard and is guarded where the consumer reads it).
538
+ */
539
+ export const laneOfEvent = (event: FlowEvent): LaneRef | undefined =>
540
+ eventLanes[event._tag].lane !== "stamped" || event._tag === "Timed" || event._tag === "Began"
541
+ ? undefined
542
+ : laneOf(event)
592
543
 
593
544
  /** `bonifici-list · lemonade-deepseek` — how a lane is named on screen. */
594
545
  export const laneLabel = (lane: string, executor: string | undefined): string =>
@@ -744,9 +695,7 @@ export const consumeTerminalEvents = Effect.fn("@llm4ts/runner/Terminal.consume"
744
695
  // Tokens of events outside any lane: the whole run, for a flow with one coder.
745
696
  const runTokens = emptyTally()
746
697
  const runInFlight = new Map<string, TokenTally>()
747
- const consumed = yield* Ref.make(0)
748
698
  const statsRef = yield* Ref.make<TerminalRunStats>({ stagesCompleted: 0, stagesFailed: 0 })
749
- const subscription = yield* events.subscribe
750
699
  const palette = surface.palette
751
700
  const timestampPrefix =
752
701
  options.timestamps === true
@@ -800,99 +749,93 @@ export const consumeTerminalEvents = Effect.fn("@llm4ts/runner/Terminal.consume"
800
749
  return `${line} ${palette.dim(`(${formatDurationMs(now - closed.startedAt)})`)}`
801
750
  })
802
751
 
803
- yield* Stream.fromSubscription(subscription).pipe(
804
- Stream.runForEach((event) =>
805
- Effect.gen(function* () {
806
- const tagged = laneOfEvent(event)
807
- if (tagged !== undefined && event._tag === "UsageProgress" && !lanes.has(tagged.lane)) {
808
- // A call that ends after its story's row closed: nothing to show.
809
- yield* Ref.update(consumed, (count) => count + 1)
810
- return
811
- }
812
- if (tagged !== undefined) {
813
- const now = yield* Clock.currentTimeMillis
814
- const state =
815
- lanes.get(tagged.lane) ??
816
- ({
817
- index: lanes.size,
818
- depth: 0,
819
- stages: [],
820
- tools: 0,
821
- executor: undefined,
822
- startedAt: now,
823
- tokens: emptyTally(),
824
- inFlight: new Map(),
825
- lastTool: undefined
826
- } satisfies LaneState)
827
- lanes.set(tagged.lane, state)
828
- if (tagged.executor !== undefined) {
829
- state.executor = tagged.executor
830
- }
831
- let closed: OpenStage | undefined
832
- if (closesChild(event)) {
833
- state.depth = Math.max(0, state.depth - 1)
834
- }
835
- const lineDepth = state.depth
836
- if (event._tag === "StageStarted") {
837
- state.depth += 1
838
- state.stages = [...state.stages, { name: terminalSafe(event.stage), startedAt: now }]
839
- } else if (closesChild(event)) {
840
- closed = state.stages.at(-1)
841
- state.stages = state.stages.slice(0, -1)
842
- yield* countStage(event)
843
- } else if (event._tag === "ToolUse") {
844
- state.tools += 1
845
- state.lastTool = { tool: terminalSafe(event.tool), at: now }
846
- }
847
- addTokens(state.tokens, event)
848
- trackProgress(state.inFlight, event)
849
- if (state.stages.length === 0 && closesChild(event)) {
850
- lanes.delete(tagged.lane)
851
- }
852
- if (rendersLanedEvent(verbosity, event)) {
853
- const tag = palette.lane(state.index, `[${laneLabel(tagged.lane, state.executor)}]`)
854
- const line = yield* withDuration(event, terminalLine(event, palette, verbosity), closed)
855
- const prefix = yield* timestampPrefix
856
- yield* surface.log(`${prefix}${tag} ${indentBlock(lineDepth, line)}`)
857
- }
858
- yield* refreshStatus
859
- yield* Ref.update(consumed, (count) => count + 1)
860
- return
752
+ const consumer = yield* events.consume("terminal", (event) =>
753
+ Effect.gen(function* () {
754
+ const tagged = laneOfEvent(event)
755
+ if (tagged !== undefined && event._tag === "UsageProgress" && !lanes.has(tagged.lane)) {
756
+ // A call that ends after its story's row closed: nothing to show.
757
+ return
758
+ }
759
+ if (tagged !== undefined) {
760
+ const now = yield* Clock.currentTimeMillis
761
+ const state =
762
+ lanes.get(tagged.lane) ??
763
+ ({
764
+ index: lanes.size,
765
+ depth: 0,
766
+ stages: [],
767
+ tools: 0,
768
+ executor: undefined,
769
+ startedAt: now,
770
+ tokens: emptyTally(),
771
+ inFlight: new Map(),
772
+ lastTool: undefined
773
+ } satisfies LaneState)
774
+ lanes.set(tagged.lane, state)
775
+ if (tagged.executor !== undefined) {
776
+ state.executor = tagged.executor
861
777
  }
862
- if (event._tag === "TokensUsed" || event._tag === "UsageProgress") {
863
- addTokens(runTokens, event)
864
- trackProgress(runInFlight, event)
865
- yield* refreshStatus
778
+ let closed: OpenStage | undefined
779
+ if (closesChild(event)) {
780
+ state.depth = Math.max(0, state.depth - 1)
866
781
  }
867
- const currentDepth = closesChild(event)
868
- ? yield* Ref.updateAndGet(depth, (value) => Math.max(0, value - 1))
869
- : yield* Ref.get(depth)
870
- let closedStage: OpenStage | undefined
782
+ const lineDepth = state.depth
871
783
  if (event._tag === "StageStarted") {
872
- yield* Ref.update(depth, (value) => value + 1)
873
- const active = terminalSafe(event.stage)
874
- const startedAt = yield* Clock.currentTimeMillis
875
- yield* Ref.update(stages, (current) => [...current, { name: active, startedAt }])
876
- yield* refreshStatus
784
+ state.depth += 1
785
+ state.stages = [...state.stages, { name: terminalSafe(event.stage), startedAt: now }]
877
786
  } else if (closesChild(event)) {
878
- closedStage = (yield* Ref.get(stages)).at(-1)
879
- yield* Ref.update(stages, (current) => current.slice(0, -1))
880
- yield* refreshStatus
787
+ closed = state.stages.at(-1)
788
+ state.stages = state.stages.slice(0, -1)
881
789
  yield* countStage(event)
790
+ } else if (event._tag === "ToolUse") {
791
+ state.tools += 1
792
+ state.lastTool = { tool: terminalSafe(event.tool), at: now }
882
793
  }
883
- if (rendersEvent(verbosity, event)) {
884
- const line = yield* withDuration(
885
- event,
886
- terminalLine(event, palette, verbosity),
887
- closedStage
888
- )
794
+ addTokens(state.tokens, event)
795
+ trackProgress(state.inFlight, event)
796
+ if (state.stages.length === 0 && closesChild(event)) {
797
+ lanes.delete(tagged.lane)
798
+ }
799
+ if (rendersLanedEvent(verbosity, event)) {
800
+ const tag = palette.lane(state.index, `[${laneLabel(tagged.lane, state.executor)}]`)
801
+ const line = yield* withDuration(event, terminalLine(event, palette, verbosity), closed)
889
802
  const prefix = yield* timestampPrefix
890
- yield* surface.log(`${prefix}${indentBlock(currentDepth, line)}`)
803
+ yield* surface.log(`${prefix}${tag} ${indentBlock(lineDepth, line)}`)
891
804
  }
892
- yield* Ref.update(consumed, (count) => count + 1)
893
- })
894
- ),
895
- Effect.forkScoped
805
+ yield* refreshStatus
806
+ return
807
+ }
808
+ if (event._tag === "TokensUsed" || event._tag === "UsageProgress") {
809
+ addTokens(runTokens, event)
810
+ trackProgress(runInFlight, event)
811
+ yield* refreshStatus
812
+ }
813
+ const currentDepth = closesChild(event)
814
+ ? yield* Ref.updateAndGet(depth, (value) => Math.max(0, value - 1))
815
+ : yield* Ref.get(depth)
816
+ let closedStage: OpenStage | undefined
817
+ if (event._tag === "StageStarted") {
818
+ yield* Ref.update(depth, (value) => value + 1)
819
+ const active = terminalSafe(event.stage)
820
+ const startedAt = yield* Clock.currentTimeMillis
821
+ yield* Ref.update(stages, (current) => [...current, { name: active, startedAt }])
822
+ yield* refreshStatus
823
+ } else if (closesChild(event)) {
824
+ closedStage = (yield* Ref.get(stages)).at(-1)
825
+ yield* Ref.update(stages, (current) => current.slice(0, -1))
826
+ yield* refreshStatus
827
+ yield* countStage(event)
828
+ }
829
+ if (rendersEvent(verbosity, event)) {
830
+ const line = yield* withDuration(
831
+ event,
832
+ terminalLine(event, palette, verbosity),
833
+ closedStage
834
+ )
835
+ const prefix = yield* timestampPrefix
836
+ yield* surface.log(`${prefix}${indentBlock(currentDepth, line)}`)
837
+ }
838
+ })
896
839
  )
897
840
  // Elapsed time and "Running <tool>…" age between events too.
898
841
  yield* Effect.sleep("1 second").pipe(
@@ -901,8 +844,8 @@ export const consumeTerminalEvents = Effect.fn("@llm4ts/runner/Terminal.consume"
901
844
  Effect.forkScoped
902
845
  )
903
846
  // Display only, so a drain that gives up just means a trailing line was
904
- // never printed; the shared helper already bounds the wait.
847
+ // never printed; the hub already bounds the wait.
905
848
  const awaitDrained = (timeout?: Duration.Input): Effect.Effect<void> =>
906
- Effect.asVoid(awaitConsumed(events, consumed, timeout))
907
- return { consumed, stats: Ref.get(statsRef), awaitDrained }
849
+ Effect.asVoid(consumer.caughtUp(timeout))
850
+ return { stats: Ref.get(statsRef), awaitDrained }
908
851
  })
package/src/Tools.ts CHANGED
@@ -7,6 +7,7 @@ import { builtinTools } from "@llm4ts/flow/tools/Builtin"
7
7
  import type { ToolReport, ToolSpec } from "@llm4ts/flow/tools/Tool"
8
8
  import { isToolName, renderUsage, specOf, type ToolShape } from "@llm4ts/flow/tools/Tool"
9
9
  import type { ToolPreset } from "@llm4ts/flow/Pack"
10
+ import { settingValue } from "@llm4ts/flow/Settings"
10
11
  import type { DiscoveredKit } from "./Kits.ts"
11
12
 
12
13
  /**
@@ -177,7 +178,7 @@ export const toolBrief = (
177
178
  specs: ReadonlyMap<string, ToolSpec>,
178
179
  environment: Readonly<Record<string, string | undefined>> = process.env
179
180
  ): string | undefined => {
180
- if (environment.LLM4TS_TOOL_BRIEF?.trim().toLowerCase() === "off") {
181
+ if (!settingValue("LLM4TS_TOOL_BRIEF", environment)) {
181
182
  return undefined
182
183
  }
183
184
  const lines = [
@@ -4,6 +4,7 @@ import * as Effect from "effect/Effect"
4
4
  import * as Schema from "effect/Schema"
5
5
  import { summariseToolArgs } from "@llm4ts/flow/Activity"
6
6
  import { TranscriptEntry, type TranscriptSink } from "@llm4ts/flow/Transcript"
7
+ import { settingValue } from "@llm4ts/flow/Settings"
7
8
  import { duration } from "./AgentTree.ts"
8
9
 
9
10
  /**
@@ -14,10 +15,10 @@ import { duration } from "./AgentTree.ts"
14
15
  * never fails the run.
15
16
  */
16
17
 
17
- /** `LLM4TS_TRANSCRIPT=on` (what `--transcript` sets); also `1`, `true`, `yes`. */
18
+ /** `LLM4TS_TRANSCRIPT=on` (what `--transcript` sets). */
18
19
  export const transcriptsWanted = (
19
20
  environment: Readonly<Record<string, string | undefined>>
20
- ): boolean => /^(on|1|true|yes)$/iu.test(environment.LLM4TS_TRANSCRIPT?.trim() ?? "")
21
+ ): boolean => settingValue("LLM4TS_TRANSCRIPT", environment) === true
21
22
 
22
23
  export const transcriptFileOf = (lane: string | undefined): string =>
23
24
  `${(lane ?? "run").replace(/[^A-Za-z0-9._-]/gu, "_")}.jsonl`
package/src/index.ts CHANGED
@@ -16,6 +16,9 @@ export {
16
16
  coderFor,
17
17
  coderFromEnv,
18
18
  mock,
19
+ coderFromEnvironment,
20
+ reasonerFromEnvironment,
21
+ seatsFromEnvironment,
19
22
  withModel,
20
23
  withTurnLimit
21
24
  } from "./Connectors.ts"