@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/src/FlowRunner.ts CHANGED
@@ -27,6 +27,8 @@ import { createConnectorRegistry } from "@llm4ts/core/providers/ConnectorFactori
27
27
  import { ConnectorIds } from "@llm4ts/core/Models"
28
28
  import { ProviderError } from "@llm4ts/core/Errors"
29
29
  import { makeCostTracker, type CostTracker } from "@llm4ts/flow/CostTracker"
30
+ import { settingValue, unreadableEnvironment, type Environment } from "@llm4ts/flow/Settings"
31
+ import { ScriptUsage } from "./FlowArgs.ts"
30
32
  import { runGeminiAcpBridge } from "./NodeGeminiAcpBridge.ts"
31
33
  import { runGeminiModelBridge } from "./NodeGeminiModelBridge.ts"
32
34
  import {
@@ -51,7 +53,7 @@ import {
51
53
  type FlowError
52
54
  } from "@llm4ts/flow/FlowError"
53
55
  import {
54
- defaultDrainTimeout,
56
+ type DrainReport,
55
57
  FlowEvents,
56
58
  FlowEventsValues,
57
59
  Info,
@@ -119,9 +121,9 @@ import {
119
121
  formatDurationMs,
120
122
  makeTerminalSurface,
121
123
  parseVerbosity,
122
- type TerminalSurface,
123
- type Verbosity
124
+ type TerminalSurface
124
125
  } from "./Terminal.ts"
126
+ import type { Verbosity } from "./EventPresentation.ts"
125
127
 
126
128
  const describeSeat = (config: ConnectorConfig): string => {
127
129
  const model = config.model === undefined ? "" : ` (${config.model})`
@@ -264,12 +266,6 @@ export interface FlowRunnerBundle {
264
266
  readonly announceStartupNotices: Effect.Effect<void>
265
267
  }
266
268
 
267
- const truthy = (value: string | undefined): boolean =>
268
- value !== undefined && ["1", "true", "on", "yes"].includes(value.trim().toLowerCase())
269
-
270
- const switchedOff = (value: string | undefined): boolean =>
271
- value !== undefined && ["0", "false", "no", "off"].includes(value.trim().toLowerCase())
272
-
273
269
  const isPiSeat = (config: ConnectorConfig | undefined): config is CliConnectorConfig =>
274
270
  config instanceof CliConnectorConfig && config.connectorId.value === ConnectorIds.Pi.value
275
271
 
@@ -289,7 +285,7 @@ const geminiBridgeSeats = Effect.fn("@llm4ts/runner/FlowRunner.geminiBridgeSeats
289
285
  environment: Readonly<Record<string, string | undefined>>
290
286
  ): Effect.fn.Return<(config: ConnectorConfig) => ConnectorConfig, FlowError, Scope.Scope> {
291
287
  const identity = (config: ConnectorConfig): ConnectorConfig => config
292
- if (!truthy(environment.LLM4TS_GEMINI_BRIDGE)) {
288
+ if (!settingValue("LLM4TS_GEMINI_BRIDGE", environment)) {
293
289
  return identity
294
290
  }
295
291
  // The reasoning seat defaults to the coder, so a pi coder makes this a pi
@@ -304,7 +300,7 @@ const geminiBridgeSeats = Effect.fn("@llm4ts/runner/FlowRunner.geminiBridgeSeats
304
300
  const resolution = resolveBridgeModel(
305
301
  readPiModelsJson(),
306
302
  port,
307
- environment.LLM4TS_GEMINI_BRIDGE_MODEL
303
+ settingValue("LLM4TS_GEMINI_BRIDGE_MODEL", environment)
308
304
  )
309
305
  const problem = bridgeModelProblem(resolution, port)
310
306
  if (problem !== undefined || resolution._tag !== "Resolved") {
@@ -322,8 +318,9 @@ const geminiBridgeSeats = Effect.fn("@llm4ts/runner/FlowRunner.geminiBridgeSeats
322
318
  })
323
319
  )
324
320
  }
325
- const geminiModel =
326
- environment.LLM4TS_GEMINI_MODEL === undefined ? {} : { model: environment.LLM4TS_GEMINI_MODEL }
321
+ const requestModel = settingValue("LLM4TS_GEMINI_MODEL", environment)
322
+ const geminiModel = requestModel === undefined ? {} : { model: requestModel }
323
+ const token = settingValue("LLM4TS_GEMINI_BRIDGE_TOKEN", environment)
327
324
  if (mode.mode === "model") {
328
325
  // ADR 0037: one locked-down `gemini` call per request, in an empty
329
326
  // directory so no project GEMINI.md reaches the model.
@@ -337,10 +334,7 @@ const geminiBridgeSeats = Effect.fn("@llm4ts/runner/FlowRunner.geminiBridgeSeats
337
334
  executor: dependencies.process,
338
335
  temporaryFiles: dependencies.temporaryFiles ?? nodeTemporaryFiles,
339
336
  ...geminiModel,
340
- ...(environment.LLM4TS_GEMINI_BRIDGE_TOKEN === undefined ||
341
- environment.LLM4TS_GEMINI_BRIDGE_TOKEN.length === 0
342
- ? {}
343
- : { token: environment.LLM4TS_GEMINI_BRIDGE_TOKEN })
337
+ ...(token === undefined ? {} : { token })
344
338
  }).pipe(Effect.mapError(FlowLlmError.from))
345
339
  } else {
346
340
  yield* runGeminiAcpBridge({
@@ -368,8 +362,8 @@ export const runIdentityFromEnvironment = (
368
362
  options: Pick<FlowRunnerOptions, "runId" | "tracePath">,
369
363
  environment: Readonly<Record<string, string | undefined>>
370
364
  ): { readonly runId?: string; readonly tracePath?: string } => {
371
- const runId = nonBlank(options.runId) ?? nonBlank(environment["LLM4TS_RUN_ID"])
372
- const tracePath = nonBlank(options.tracePath) ?? nonBlank(environment["LLM4TS_TRACE_PATH"])
365
+ const runId = nonBlank(options.runId) ?? settingValue("LLM4TS_RUN_ID", environment)
366
+ const tracePath = nonBlank(options.tracePath) ?? settingValue("LLM4TS_TRACE_PATH", environment)
373
367
  return {
374
368
  ...(runId === undefined ? {} : { runId }),
375
369
  ...(tracePath === undefined ? {} : { tracePath })
@@ -521,7 +515,7 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
521
515
  // notably the judgment seat and the worktree seats `contextFor` rebinds.
522
516
  const runEnvironment = options.environment ?? process.env
523
517
  const estimateOptions = estimatedUsageOptionsFromEnv(runEnvironment)
524
- const meterUsage = options.estimateUsage ?? !switchedOff(runEnvironment.LLM4TS_ESTIMATE_USAGE)
518
+ const meterUsage = options.estimateUsage ?? settingValue("LLM4TS_ESTIMATE_USAGE", runEnvironment)
525
519
  // Every seat's spans (ADR 0026): content only when asked, an estimate on a
526
520
  // call the backend reports no usage for, and who served it when known.
527
521
  const seatOptions = (executor?: Effect.Effect<string | undefined>): TimedSeatOptions => ({
@@ -578,11 +572,7 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
578
572
  seatConfig: ConnectorConfig,
579
573
  environment: Readonly<Record<string, string | undefined>>
580
574
  ): Effect.Effect<JudgmentShape, FlowError> => {
581
- const backend =
582
- options.judgmentBackend ??
583
- (environment.LLM4TS_JUDGMENT_BACKEND?.trim().toLowerCase() === "typesafe"
584
- ? "typesafe"
585
- : "llm")
575
+ const backend = options.judgmentBackend ?? settingValue("LLM4TS_JUDGMENT_BACKEND", environment)
586
576
  if (backend !== "typesafe") {
587
577
  return Effect.succeed(
588
578
  makeLlmJudgment(
@@ -592,7 +582,7 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
592
582
  ...(seatConfig.model === undefined ? {} : { model: seatConfig.model }),
593
583
  // LLM4TS_JUDGMENT_BATCHING=shared-prefix sends each request's state
594
584
  // once with every question; anything else keeps them independent.
595
- ...(environment.LLM4TS_JUDGMENT_BATCHING?.trim().toLowerCase() === "shared-prefix"
585
+ ...(settingValue("LLM4TS_JUDGMENT_BATCHING", environment) === "shared-prefix"
596
586
  ? { batching: "shared-prefix" as const }
597
587
  : {})
598
588
  }),
@@ -987,6 +977,21 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
987
977
  }
988
978
  })
989
979
 
980
+ /** What the run prints when `drained` reports a consumer short. */
981
+ export const drainShortfall = (report: DrainReport): string => {
982
+ const behind = report.behind.map(
983
+ (consumer) => `${consumer.name} was ${consumer.missing.toString()} event(s) behind`
984
+ )
985
+ const late = report.late.map(
986
+ (consumer) =>
987
+ `${consumer.name} started listening after ${consumer.missed.toString()} event(s) were published`
988
+ )
989
+ return (
990
+ `some run events were still unconsumed: ${[...late, ...behind].join("; ")}. ` +
991
+ "The totals below, the ledger, and the trace may be short"
992
+ )
993
+ }
994
+
990
995
  /** `claude (coder·reviewer·judge ×3)` — how the run header names an executor. */
991
996
  const describeExecutor = (spec: ExecutorSpec): string =>
992
997
  `${spec.id} (${rolesOf(spec).join("·")} ×${spec.slots ?? 1})`
@@ -1004,7 +1009,8 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
1004
1009
  const dependencies = withRunStateIgnore(given)
1005
1010
  const startedAt = yield* Clock.currentTimeMillis
1006
1011
  const environment = options.environment ?? process.env
1007
- const verbosity = options.verbosity ?? parseVerbosity(environment.LLM4TS_VERBOSITY)
1012
+ const verbosity =
1013
+ options.verbosity ?? parseVerbosity(settingValue("LLM4TS_VERBOSITY", environment))
1008
1014
  const persistRun = options.persistRun ?? true
1009
1015
  // OpenTelemetry export (ADR 0026): decided from the environment, built once
1010
1016
  // per run, flushed when the run's scope closes.
@@ -1043,7 +1049,7 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
1043
1049
  const surface = tree?.surface ?? classic
1044
1050
  const palette = surface.palette
1045
1051
  const terminal = yield* consumeTerminalEvents(bundle.events, surface, verbosity, {
1046
- timestamps: environment.LLM4TS_TIMESTAMPS === "1" || environment.LLM4TS_TIMESTAMPS === "true"
1052
+ timestamps: settingValue("LLM4TS_TIMESTAMPS", environment)
1047
1053
  })
1048
1054
  if (verbosity !== "Quiet") {
1049
1055
  const reviewers = options.reviewers ?? []
@@ -1130,10 +1136,11 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
1130
1136
  tracePath === undefined
1131
1137
  ? bundle.context
1132
1138
  : { ...bundle.context, trace: { runId: bundle.runId, path: tracePath } }
1139
+ const flow = settingValue("LLM4TS_FLOW", environment)
1133
1140
  const runAttributes: Record<string, unknown> = {
1134
1141
  [attr.run]: bundle.runId,
1135
1142
  [attr.session]: bundle.runId,
1136
- ...(environment.LLM4TS_FLOW === undefined ? {} : { [attr.flow]: environment.LLM4TS_FLOW })
1143
+ ...(flow === undefined ? {} : { [attr.flow]: flow })
1137
1144
  }
1138
1145
  // The run is a CHAIN span every other span descends from or links to; its
1139
1146
  // attributes ride on all of them. Effect's own function spans stay
@@ -1159,33 +1166,13 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
1159
1166
  Effect.provideService(FlowContext, context),
1160
1167
  Effect.andThen((value) => Effect.as(enforceBudget, value)),
1161
1168
  Effect.ensuring(
1162
- Effect.all(
1163
- [
1164
- terminal.awaitDrained(),
1165
- Effect.all(
1166
- [
1167
- tracker.awaitDrained(bundle.events),
1168
- recorder === undefined ? Effect.succeed(true) : recorder.awaitDrained(bundle.events),
1169
- bundle.judgmentLog === undefined
1170
- ? Effect.succeed(true)
1171
- : bundle.judgmentLog.awaitDrained(bundle.events)
1172
- ],
1173
- { concurrency: "unbounded" }
1174
- )
1175
- ],
1176
- { concurrency: "unbounded" }
1177
- ).pipe(
1178
- // A consumer that gave up before catching up leaves the totals below
1179
- // short. Say so rather than printing a quietly incomplete summary.
1180
- Effect.flatMap(([, accounted]) =>
1181
- accounted.every((drained) => drained)
1182
- ? Effect.void
1183
- : surface.log(
1184
- palette.fail(
1185
- `some run events were still unconsumed after ${defaultDrainTimeout} — ` +
1186
- "the totals below, the ledger, and the trace may be short"
1187
- )
1188
- )
1169
+ // One wait for every consumer of the run: the terminal, the totals,
1170
+ // the trace, the judgment log. A consumer that fell short leaves the
1171
+ // totals below short, so the run names it instead of printing a
1172
+ // quietly incomplete summary.
1173
+ bundle.events.drained().pipe(
1174
+ Effect.flatMap((report) =>
1175
+ report.complete ? Effect.void : surface.log(palette.fail(drainShortfall(report)))
1189
1176
  ),
1190
1177
  Effect.andThen(tree === undefined ? Effect.void : tree.close),
1191
1178
  Effect.andThen(appendLedger),
@@ -1254,7 +1241,7 @@ const wantsAgentTree = (
1254
1241
  output: { readonly isTTY?: boolean; readonly columns?: number },
1255
1242
  input: { readonly isTTY?: boolean }
1256
1243
  ): boolean =>
1257
- environment.LLM4TS_UI === "tree" &&
1244
+ settingValue("LLM4TS_UI", environment) === "tree" &&
1258
1245
  environment.NO_COLOR === undefined &&
1259
1246
  output.isTTY === true &&
1260
1247
  input.isTTY === true &&
@@ -1267,11 +1254,24 @@ const runOutcomeOf = <A, E>(exit: Exit.Exit<A, E>): RunOutcome =>
1267
1254
  ? "interrupted"
1268
1255
  : "failed"
1269
1256
 
1257
+ /**
1258
+ * The shell's launch check for a run that did not come through it: an
1259
+ * environment value the settings registry cannot read stops the run before
1260
+ * anything starts, instead of reading as unset.
1261
+ */
1262
+ const requireReadableEnvironment = (environment: Environment): Effect.Effect<void, ScriptUsage> => {
1263
+ const problems = unreadableEnvironment(environment)
1264
+ return problems.length === 0
1265
+ ? Effect.void
1266
+ : Effect.fail(ScriptUsage.make({ message: problems.join("\n") }))
1267
+ }
1268
+
1270
1269
  export const runEmbedded = Effect.fn("@llm4ts/runner/FlowRunner.runEmbedded")(function* <A, E, R>(
1271
1270
  options: FlowRunnerOptions,
1272
1271
  body: (context: FlowContextShape) => Effect.Effect<A, E, R>,
1273
1272
  dependencies: FlowRunnerDependencies = nodeFlowRunnerDependencies()
1274
- ): Effect.fn.Return<A, E | FlowError, R | Scope.Scope> {
1273
+ ): Effect.fn.Return<A, E | FlowError | ScriptUsage, R | Scope.Scope> {
1274
+ yield* requireReadableEnvironment(options.environment ?? process.env)
1275
1275
  const bundle = yield* makeFlowRunnerContext(options, dependencies)
1276
1276
  return yield* runWithBundle(bundle, options, body, dependencies)
1277
1277
  })
@@ -1279,7 +1279,7 @@ export const runEmbedded = Effect.fn("@llm4ts/runner/FlowRunner.runEmbedded")(fu
1279
1279
  export const runNode = <A, E, R>(
1280
1280
  options: FlowRunnerOptions,
1281
1281
  body: (context: FlowContextShape) => Effect.Effect<A, E, R>
1282
- ): Effect.Effect<A, E | FlowError, R> => Effect.scoped(runEmbedded(options, body))
1282
+ ): Effect.Effect<A, E | FlowError | ScriptUsage, R> => Effect.scoped(runEmbedded(options, body))
1283
1283
 
1284
1284
  const mainErrorMessage = (error: unknown): string =>
1285
1285
  error instanceof Error && error.message.length > 0 ? error.message : String(error)
@@ -1359,9 +1359,15 @@ const processSignals: SignalSource = {
1359
1359
  exit: (code) => process.exit(code)
1360
1360
  }
1361
1361
 
1362
+ /**
1363
+ * A flow script's entry point. An environment value the settings registry
1364
+ * cannot read stops it before `program` starts, as `llm4ts run` would have;
1365
+ * any failure is printed and exits 1.
1366
+ */
1362
1367
  export const runFlowMain = <E>(program: Effect.Effect<void, E>): void => {
1363
1368
  const fiber = Effect.runFork(
1364
- program.pipe(
1369
+ requireReadableEnvironment(process.env).pipe(
1370
+ Effect.andThen(program),
1365
1371
  Effect.match({
1366
1372
  onFailure: (error) =>
1367
1373
  Effect.sync(() => {
@@ -1,3 +1,4 @@
1
+ // @internal: Gemini CLI executor wired by FlowRunner
1
2
  import * as Effect from "effect/Effect"
2
3
  import * as Layer from "effect/Layer"
3
4
  import * as Stream from "effect/Stream"
@@ -19,6 +20,9 @@ import {
19
20
  } from "@llm4ts/core/providers/GeminiCliProvider"
20
21
  import { nodeProcessExecutor } from "./NodeProcessExecutor.ts"
21
22
  import { nodeTemporaryFiles } from "./NodeTemporaryFiles.ts"
23
+ import { cliExecutable } from "@llm4ts/core/Models"
24
+
25
+ const executable = cliExecutable("GeminiCli")
22
26
 
23
27
  const failure = (message: string): ProviderError => ProviderError.make({ message })
24
28
 
@@ -47,7 +51,7 @@ export const makeNodeGeminiCliExecutor = (
47
51
  const supportsAdminPolicy: Effect.Effect<boolean> = Effect.suspend(() =>
48
52
  adminPolicy !== undefined
49
53
  ? Effect.succeed(adminPolicy)
50
- : executor.run(["gemini", "--help"], process.cwd(), {}).pipe(
54
+ : executor.run([executable, "--help"], process.cwd(), {}).pipe(
51
55
  Effect.map((result) =>
52
56
  geminiSupportsAdminPolicy([...result.stdout, ...result.stderr].join("\n"))
53
57
  ),
@@ -75,7 +79,7 @@ export const makeNodeGeminiCliExecutor = (
75
79
  )
76
80
  return {
77
81
  checkGeminiInstalled: executor
78
- .run(["gemini", "--version"], process.cwd(), {})
82
+ .run([executable, "--version"], process.cwd(), {})
79
83
  .pipe(
80
84
  Effect.flatMap((result) =>
81
85
  result.exitCode === 0
@@ -89,7 +93,7 @@ export const makeNodeGeminiCliExecutor = (
89
93
  const environment = yield* environmentFor(context)
90
94
  const args = yield* argsFor(config, context, "json")
91
95
  const result = yield* executor.runWithStdin(
92
- ["gemini", ...args],
96
+ [executable, ...args],
93
97
  context.cwd ?? process.cwd(),
94
98
  environment,
95
99
  prompt
@@ -111,7 +115,7 @@ export const makeNodeGeminiCliExecutor = (
111
115
  ([environment, args]) =>
112
116
  executor
113
117
  .runStreamingWithStdin(
114
- ["gemini", ...args],
118
+ [executable, ...args],
115
119
  context.cwd ?? process.cwd(),
116
120
  environment,
117
121
  prompt
@@ -1,3 +1,4 @@
1
+ // @internal: Gemini bridge wiring for FlowRunner, Doctor and the bridge server
1
2
  import { existsSync, readdirSync } from "node:fs"
2
3
  import type { IncomingMessage, ServerResponse } from "node:http"
3
4
  import { join } from "node:path"
@@ -34,6 +35,9 @@ import {
34
35
  requestPath,
35
36
  writeJson
36
37
  } from "./NodeGeminiAcpBridge.ts"
38
+ import { cliExecutable } from "@llm4ts/core/Models"
39
+
40
+ const executable = cliExecutable("GeminiCli")
37
41
 
38
42
  /**
39
43
  * The bridge's model route (ADR 0037): `/v1/chat/completions` in OpenAI's
@@ -131,7 +135,7 @@ const geminiTurn = (
131
135
  config.executor
132
136
  .runStreamingWithStdin(
133
137
  [
134
- "gemini",
138
+ executable,
135
139
  // Bound to its flag, so it can never read as a flag of its own.
136
140
  `--model=${model}`,
137
141
  "--output-format",
@@ -1,3 +1,4 @@
1
+ // @internal: the Node HTTP client FlowRunner provides
1
2
  import * as Duration from "effect/Duration"
2
3
  import * as Effect from "effect/Effect"
3
4
  import * as Layer from "effect/Layer"
package/src/Otel.ts CHANGED
@@ -1,3 +1,4 @@
1
+ // @internal: OpenTelemetry export wired by FlowRunner and Doctor
1
2
  // OpenTelemetry export (ADR 0026): Effect's own OTLP tracer and metrics
2
3
  // exporters over core's fetch client, so a run can show up in Phoenix,
3
4
  // Langfuse, SigNoz or any collector with no SDK and no new dependency.
@@ -12,6 +13,7 @@ import * as OtlpExporter from "effect/observability/OtlpExporter"
12
13
  import * as OtlpMetrics from "effect/observability/OtlpMetrics"
13
14
  import * as OtlpSerialization from "effect/observability/OtlpSerialization"
14
15
  import * as OtlpTracer from "effect/observability/OtlpTracer"
16
+ import { settingValue } from "@llm4ts/flow/Settings"
15
17
  import { packageVersion } from "./Package.ts"
16
18
 
17
19
  export type OtelConfig =
@@ -23,8 +25,6 @@ export const phoenixTracesUrl = "http://localhost:6006/v1/traces"
23
25
 
24
26
  const truthy = (value: string | undefined): boolean =>
25
27
  /^(on|1|true|yes)$/iu.test(value?.trim() ?? "")
26
- const falsy = (value: string | undefined): boolean =>
27
- /^(off|0|false|no)$/iu.test(value?.trim() ?? "")
28
28
 
29
29
  /**
30
30
  * Off unless asked. An OTLP endpoint in the environment wins and is used as
@@ -34,7 +34,8 @@ const falsy = (value: string | undefined): boolean =>
34
34
  export const otelConfig = (
35
35
  environment: Readonly<Record<string, string | undefined>>
36
36
  ): OtelConfig => {
37
- if (truthy(environment.OTEL_SDK_DISABLED) || falsy(environment.LLM4TS_OTEL)) {
37
+ const asked = settingValue("LLM4TS_OTEL", environment)
38
+ if (truthy(environment.OTEL_SDK_DISABLED) || asked === false) {
38
39
  return { mode: "off" }
39
40
  }
40
41
  // Under the test runner the exporter's sleeps run on a TestClock nobody
@@ -49,9 +50,7 @@ export const otelConfig = (
49
50
  if (endpoint !== undefined && endpoint.length > 0) {
50
51
  return { mode: "env", endpoint }
51
52
  }
52
- return truthy(environment.LLM4TS_OTEL)
53
- ? { mode: "phoenix", tracesUrl: phoenixTracesUrl }
54
- : { mode: "off" }
53
+ return asked === true ? { mode: "phoenix", tracesUrl: phoenixTracesUrl } : { mode: "off" }
55
54
  }
56
55
 
57
56
  export interface OtelResource {
package/src/Package.ts CHANGED
@@ -1,3 +1,4 @@
1
+ // @internal: the runner's own version stamp
1
2
  import { createRequire } from "node:module"
2
3
  import * as Schema from "effect/Schema"
3
4
 
package/src/Packs.ts CHANGED
@@ -7,6 +7,7 @@ import type { PlanParseError } from "@llm4ts/flow/FlowError"
7
7
  import { loadPack, type Pack } from "@llm4ts/flow/Pack"
8
8
  import { loadPatternCards, type PatternCard } from "@llm4ts/flow/Patterns"
9
9
  import type { WorkspaceError, WorkspaceShape } from "@llm4ts/flow/Workspace"
10
+ import { settingValue } from "@llm4ts/flow/Settings"
10
11
  import {
11
12
  builtinKitsDir,
12
13
  discoverKits,
@@ -108,7 +109,7 @@ export const openPack = Effect.fn("@llm4ts/runner/Packs.openPack")(function* (op
108
109
  /** Kit tiers to search; defaults to the standard tiers around `launchDir` and `flowDir`. */
109
110
  readonly kits?: KitTierPaths
110
111
  }): Effect.fn.Return<OpenedPack, PackNotFound | WorkspaceError | PlanParseError> {
111
- const reference = options.environment.LLM4TS_PACK?.trim() || defaultPackName
112
+ const reference = settingValue("LLM4TS_PACK", options.environment) ?? defaultPackName
112
113
  const builtinDir = builtinKitsDir(options.flowDir)
113
114
  const tiers =
114
115
  options.kits ??
package/src/PiModels.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs"
2
2
  import { homedir } from "node:os"
3
3
  import { join } from "node:path"
4
+ import { settingGiven, settingText } from "@llm4ts/flow/Settings"
4
5
 
5
6
  /**
6
7
  * Reading `pi`'s own model config (ADR 0016). Separate from `Doctor.ts` so
@@ -29,10 +30,7 @@ export const defaultReadPiModelsJson = (): string | undefined => {
29
30
 
30
31
  export const geminiBridgePort = (
31
32
  environment: Readonly<Record<string, string | undefined>>
32
- ): string =>
33
- isSet(environment.LLM4TS_GEMINI_BRIDGE_PORT)
34
- ? environment.LLM4TS_GEMINI_BRIDGE_PORT.trim()
35
- : defaultGeminiBridgePort
33
+ ): string => settingText("LLM4TS_GEMINI_BRIDGE_PORT", environment) ?? defaultGeminiBridgePort
36
34
 
37
35
  /**
38
36
  * Which route the bridge serves (ADR 0037): `acp` (ADR 0016's Anthropic
@@ -46,12 +44,10 @@ export type GeminiBridgeMode =
46
44
  export const geminiBridgeMode = (
47
45
  environment: Readonly<Record<string, string | undefined>>
48
46
  ): GeminiBridgeMode => {
49
- const value = (environment.LLM4TS_GEMINI_BRIDGE_MODE ?? "").trim().toLowerCase()
50
- return value === "" || value === "acp"
51
- ? { _tag: "Mode", mode: "acp" }
52
- : value === "model"
53
- ? { _tag: "Mode", mode: "model" }
54
- : { _tag: "Unknown", value }
47
+ const given = settingGiven("LLM4TS_GEMINI_BRIDGE_MODE", environment)
48
+ return given._tag === "Unreadable"
49
+ ? { _tag: "Unknown", value: given.text.toLowerCase() }
50
+ : { _tag: "Mode", mode: given._tag === "Read" ? given.value : "acp" }
55
51
  }
56
52
 
57
53
  /**