@llm4ts/runner 2.5.0 → 2.7.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/Cli.d.ts.map +1 -1
- package/dist/Cli.js +0 -2
- package/dist/Cli.js.map +1 -1
- package/dist/Connectors.d.ts +7 -0
- package/dist/Connectors.d.ts.map +1 -1
- package/dist/Connectors.js +25 -1
- package/dist/Connectors.js.map +1 -1
- package/dist/Costs.d.ts +48 -0
- package/dist/Costs.d.ts.map +1 -0
- package/dist/Costs.js +76 -0
- package/dist/Costs.js.map +1 -0
- package/dist/FlowRunner.d.ts +43 -0
- package/dist/FlowRunner.d.ts.map +1 -1
- package/dist/FlowRunner.js +111 -13
- package/dist/FlowRunner.js.map +1 -1
- package/dist/Terminal.d.ts.map +1 -1
- package/dist/Terminal.js +5 -0
- package/dist/Terminal.js.map +1 -1
- package/package.json +4 -3
- package/src/Cli.ts +0 -2
- package/src/Connectors.ts +31 -1
- package/src/Costs.ts +116 -0
- package/src/FlowRunner.ts +205 -16
- package/src/Terminal.ts +5 -0
package/src/FlowRunner.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
1
2
|
import * as Clock from "effect/Clock"
|
|
2
3
|
import * as Effect from "effect/Effect"
|
|
3
4
|
import type * as Scope from "effect/Scope"
|
|
@@ -5,8 +6,13 @@ import type { ConnectorConfig } from "@llm4ts/core/ConnectorConfig"
|
|
|
5
6
|
import { CliConnectorConfig, defaultReasoningConfig } from "@llm4ts/core/ConnectorConfig"
|
|
6
7
|
import type { ConnectorRegistryShape } from "@llm4ts/core/ConnectorRegistry"
|
|
7
8
|
import type { HttpClientShape } from "@llm4ts/core/HttpClient"
|
|
9
|
+
import type { JudgmentBackend } from "@llm4ts/core/judgment/Schemas"
|
|
10
|
+
import type { JudgmentShape } from "@llm4ts/core/judgment/Judgment"
|
|
11
|
+
import { LlmJudgmentConfig, makeLlmJudgment } from "@llm4ts/core/judgment/LlmJudgment"
|
|
12
|
+
import { makeTypeSafeJudgment } from "@llm4ts/core/judgment/TypeSafeJudgment"
|
|
13
|
+
import * as Redacted from "effect/Redacted"
|
|
8
14
|
import type { LlmServiceShape } from "@llm4ts/core/LlmService"
|
|
9
|
-
import type { ConnectorCapabilities } from "@llm4ts/core/Models"
|
|
15
|
+
import type { ConnectorCapabilities, TokenUsage } from "@llm4ts/core/Models"
|
|
10
16
|
import type { ProcessExecutorShape } from "@llm4ts/core/ProcessExecutor"
|
|
11
17
|
import type { TemporaryFilesShape } from "@llm4ts/core/TemporaryFiles"
|
|
12
18
|
import type { GeminiCliExecutorShape } from "@llm4ts/core/providers/GeminiCliProvider"
|
|
@@ -21,16 +27,26 @@ import {
|
|
|
21
27
|
geminiBridgePort,
|
|
22
28
|
resolveBridgeModel
|
|
23
29
|
} from "./PiModels.ts"
|
|
24
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
appendCostRecord,
|
|
32
|
+
checkCostBudget,
|
|
33
|
+
CostLedgerFileName,
|
|
34
|
+
makeCostRecord,
|
|
35
|
+
type CostBudget
|
|
36
|
+
} from "@llm4ts/flow/CostLedger"
|
|
37
|
+
import { estimatedUsageOptionsFromEnv, makeEstimatedUsageMeter } from "@llm4ts/flow/EstimatedUsage"
|
|
25
38
|
import { FlowLlmError, describeFlowError, type FlowError } from "@llm4ts/flow/FlowError"
|
|
26
39
|
import {
|
|
27
40
|
FlowEvents,
|
|
28
41
|
FlowEventsValues,
|
|
42
|
+
Info,
|
|
43
|
+
TokensUsed,
|
|
29
44
|
makeFlowEventHub,
|
|
30
45
|
type FlowEventHub
|
|
31
46
|
} from "@llm4ts/flow/FlowEvents"
|
|
32
47
|
import { FlowContext, type FlowContextShape } from "@llm4ts/flow/FlowContext"
|
|
33
48
|
import { makeFlowRecorder } from "@llm4ts/flow/FlowRecorder"
|
|
49
|
+
import { makeJudgmentLog, type JudgmentLogShape } from "@llm4ts/flow/JudgmentLog"
|
|
34
50
|
import { makeGitHubTool } from "@llm4ts/flow/GitHubTool"
|
|
35
51
|
import { makeGitTool } from "@llm4ts/flow/GitTool"
|
|
36
52
|
import type { PlainFileStoreShape } from "@llm4ts/flow/Persistence"
|
|
@@ -61,6 +77,8 @@ export interface FlowRunnerDependencies {
|
|
|
61
77
|
readonly files: PlainFileStoreShape
|
|
62
78
|
/** pi's own model config (ADR 0016), injectable so a test never reads $HOME. */
|
|
63
79
|
readonly readPiModelsJson?: () => string | undefined
|
|
80
|
+
/** For the hosted TypeSafe judgment backend; defaults to the Node client. */
|
|
81
|
+
readonly http?: HttpClientShape
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
export interface NodeConnectorDependencies {
|
|
@@ -73,6 +91,7 @@ export interface NodeConnectorDependencies {
|
|
|
73
91
|
export const nodeFlowRunnerDependencies = (): FlowRunnerDependencies => ({
|
|
74
92
|
process: nodeProcessExecutor,
|
|
75
93
|
files: nodePlainFileStore,
|
|
94
|
+
http: nodeHttpClient,
|
|
76
95
|
registry: createConnectorRegistry({
|
|
77
96
|
http: nodeHttpClient,
|
|
78
97
|
process: nodeProcessExecutor,
|
|
@@ -88,7 +107,44 @@ export interface FlowRunnerOptions {
|
|
|
88
107
|
readonly coder: ConnectorConfig
|
|
89
108
|
readonly reasoning?: ConnectorConfig
|
|
90
109
|
readonly reviewers?: ReadonlyArray<ConnectorConfig>
|
|
110
|
+
/**
|
|
111
|
+
* The seat typed judgments run on (ADR 0017). Defaults to the reasoning
|
|
112
|
+
* seat; point it at a small non-thinking model for one-token label scoring.
|
|
113
|
+
*/
|
|
114
|
+
readonly judgment?: ConnectorConfig
|
|
115
|
+
/**
|
|
116
|
+
* `llm` (default) answers on the judgment seat; `typesafe` uses the hosted
|
|
117
|
+
* Jev model with `TYPESAFE_API_KEY` from the environment. Unset, the
|
|
118
|
+
* environment's `LLM4TS_JUDGMENT_BACKEND` decides, then `llm`.
|
|
119
|
+
*/
|
|
120
|
+
readonly judgmentBackend?: JudgmentBackend
|
|
121
|
+
/** Persist judgment observations under workDir/.llm4ts/judgments; off by default. */
|
|
122
|
+
readonly judgmentLog?: boolean
|
|
123
|
+
/**
|
|
124
|
+
* Meter every seat with `EstimatedUsage`, so a backend that reports no
|
|
125
|
+
* token counts still yields usage — counted from characters and
|
|
126
|
+
* labelled `estimated:<model>`, never mixed with measured usage. Default
|
|
127
|
+
* `true`; `false`, or `LLM4TS_ESTIMATE_USAGE=0` in the environment, leaves
|
|
128
|
+
* the seats raw and those runs accrue nothing.
|
|
129
|
+
*/
|
|
130
|
+
readonly estimateUsage?: boolean
|
|
131
|
+
/**
|
|
132
|
+
* Where the run's trace goes. Default `workDir/.llm4ts/trace-<start ms>.jsonl`
|
|
133
|
+
* while `persistRun` holds.
|
|
134
|
+
*/
|
|
91
135
|
readonly tracePath?: string
|
|
136
|
+
/**
|
|
137
|
+
* Append-only ledger (`CostLedger`) that receives one `CostRecord` per run
|
|
138
|
+
* that accrued usage, so `llm4ts costs` and budgets can look across runs.
|
|
139
|
+
* Default `workDir/.llm4ts/costs.jsonl` while `persistRun` holds.
|
|
140
|
+
*/
|
|
141
|
+
readonly costLedgerPath?: string
|
|
142
|
+
/**
|
|
143
|
+
* `false` writes neither the trace nor the ledger unless their paths are
|
|
144
|
+
* given explicitly. Default `true`: every run leaves its record under
|
|
145
|
+
* `workDir/.llm4ts/`, which `llm4ts costs` and replay read back.
|
|
146
|
+
*/
|
|
147
|
+
readonly persistRun?: boolean
|
|
92
148
|
readonly runId?: string
|
|
93
149
|
readonly verbosity?: Verbosity
|
|
94
150
|
readonly surface?: TerminalSurface
|
|
@@ -108,11 +164,16 @@ export interface FlowRunnerBundle {
|
|
|
108
164
|
readonly events: FlowEventHub
|
|
109
165
|
/** The run's cost tracker, already subscribed to `events`. */
|
|
110
166
|
readonly tracker: CostTracker
|
|
167
|
+
readonly runId: string
|
|
168
|
+
readonly judgmentLog?: JudgmentLogShape
|
|
111
169
|
}
|
|
112
170
|
|
|
113
171
|
const truthy = (value: string | undefined): boolean =>
|
|
114
172
|
value !== undefined && ["1", "true", "yes"].includes(value.trim().toLowerCase())
|
|
115
173
|
|
|
174
|
+
const switchedOff = (value: string | undefined): boolean =>
|
|
175
|
+
value !== undefined && ["0", "false", "no", "off"].includes(value.trim().toLowerCase())
|
|
176
|
+
|
|
116
177
|
const isPiSeat = (config: ConnectorConfig | undefined): config is CliConnectorConfig =>
|
|
117
178
|
config instanceof CliConnectorConfig && config.connectorId.value === ConnectorIds.Pi.value
|
|
118
179
|
|
|
@@ -177,6 +238,12 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
177
238
|
dependencies: FlowRunnerDependencies
|
|
178
239
|
): Effect.fn.Return<FlowRunnerBundle, FlowError, Scope.Scope> {
|
|
179
240
|
const events = yield* makeFlowEventHub()
|
|
241
|
+
const runId = options.runId ?? `run-${(yield* Clock.currentTimeMillis).toString()}`
|
|
242
|
+
const judgmentLog =
|
|
243
|
+
options.judgmentLog === true
|
|
244
|
+
? yield* makeJudgmentLog({ files: dependencies.files, root: options.workDir, runId })
|
|
245
|
+
: undefined
|
|
246
|
+
if (judgmentLog !== undefined) yield* judgmentLog.consume(events)
|
|
180
247
|
const tracker = yield* makeCostTracker()
|
|
181
248
|
yield* tracker.consume(events)
|
|
182
249
|
const reasoning = defaultReasoningConfig(options.coder, options.reasoning)
|
|
@@ -212,19 +279,96 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
212
279
|
)
|
|
213
280
|
)
|
|
214
281
|
: Effect.void
|
|
282
|
+
// Every seat is metered, so no run is silently free: a backend that reports
|
|
283
|
+
// no token counts (`capabilities.usageReporting` false) is estimated from
|
|
284
|
+
// character counts and labelled `estimated:<model>`, which is what makes
|
|
285
|
+
// `TokensUsed` — and therefore cost summaries, budgets, the ledger, and
|
|
286
|
+
// `llm4ts costs` — answer for those seats at all. Measured usage always
|
|
287
|
+
// wins; the estimate only fills a gap the backend left. Metering here rather
|
|
288
|
+
// than in each flow also covers the seats a flow never touches directly,
|
|
289
|
+
// notably the judgment seat and the worktree seats `contextFor` rebinds.
|
|
290
|
+
const runEnvironment = options.environment ?? process.env
|
|
291
|
+
const estimateOptions = estimatedUsageOptionsFromEnv(runEnvironment)
|
|
292
|
+
const meterUsage = options.estimateUsage ?? !switchedOff(runEnvironment.LLM4TS_ESTIMATE_USAGE)
|
|
293
|
+
const metered = (service: LlmServiceShape): Effect.Effect<LlmServiceShape> =>
|
|
294
|
+
meterUsage
|
|
295
|
+
? Effect.map(makeEstimatedUsageMeter(service, estimateOptions), (meter) => meter.service)
|
|
296
|
+
: Effect.succeed(service)
|
|
215
297
|
// The spread keeps everything the connector carries beyond the service
|
|
216
|
-
// methods — notably `capabilities`, which the flow context exposes.
|
|
298
|
+
// methods — notably `capabilities`, which the flow context exposes. The
|
|
299
|
+
// meter decorates the retrying seat, so an estimate reflects the attempt
|
|
300
|
+
// that actually succeeded, and it sees `scoreLabelSequence` when the
|
|
301
|
+
// connector has one (the retry wrapper does not carry it).
|
|
217
302
|
const resolveSeat = (configuration: ConnectorConfig) =>
|
|
218
303
|
dependencies.registry.resolve(configuration).pipe(
|
|
219
304
|
Effect.mapError(FlowLlmError.from),
|
|
220
305
|
Effect.flatMap((connector) =>
|
|
221
306
|
announceReadOnlyGrade(configuration, connector).pipe(
|
|
222
307
|
Effect.andThen(
|
|
223
|
-
Effect.
|
|
308
|
+
Effect.flatMap(resilient(connector), (retrying) => {
|
|
309
|
+
const seat = { ...connector, ...retrying }
|
|
310
|
+
return Effect.map(metered(seat), (measured) => ({ ...seat, ...measured }))
|
|
311
|
+
})
|
|
224
312
|
)
|
|
225
313
|
)
|
|
226
314
|
)
|
|
227
315
|
)
|
|
316
|
+
// Judgment usage is metered like any chat (ADR 0005 amendment) and every
|
|
317
|
+
// fallback from label scoring to the verbalized path is announced.
|
|
318
|
+
const judgmentHooks = {
|
|
319
|
+
onUsage: (usage: TokenUsage, model: string | undefined) =>
|
|
320
|
+
events.publish(
|
|
321
|
+
new TokensUsed({ agent: "judgment", usage, ...(model === undefined ? {} : { model }) })
|
|
322
|
+
),
|
|
323
|
+
onFallback: (key: string, reason: string) =>
|
|
324
|
+
events.publish(
|
|
325
|
+
new Info({ message: `judgment '${key}' fell back to verbalized scoring: ${reason}` })
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
const judgmentFor = (
|
|
329
|
+
seat: LlmServiceShape,
|
|
330
|
+
seatConfig: ConnectorConfig,
|
|
331
|
+
environment: Readonly<Record<string, string | undefined>>
|
|
332
|
+
): Effect.Effect<JudgmentShape, FlowError> => {
|
|
333
|
+
const backend =
|
|
334
|
+
options.judgmentBackend ??
|
|
335
|
+
(environment.LLM4TS_JUDGMENT_BACKEND?.trim().toLowerCase() === "typesafe"
|
|
336
|
+
? "typesafe"
|
|
337
|
+
: "llm")
|
|
338
|
+
if (backend !== "typesafe") {
|
|
339
|
+
return Effect.succeed(
|
|
340
|
+
makeLlmJudgment(
|
|
341
|
+
seat,
|
|
342
|
+
LlmJudgmentConfig.make({
|
|
343
|
+
connector: seatConfig.connectorId.value,
|
|
344
|
+
...(seatConfig.model === undefined ? {} : { model: seatConfig.model }),
|
|
345
|
+
// LLM4TS_JUDGMENT_BATCHING=shared-prefix sends each request's state
|
|
346
|
+
// once with every question; anything else keeps them independent.
|
|
347
|
+
...(environment.LLM4TS_JUDGMENT_BATCHING?.trim().toLowerCase() === "shared-prefix"
|
|
348
|
+
? { batching: "shared-prefix" as const }
|
|
349
|
+
: {})
|
|
350
|
+
}),
|
|
351
|
+
judgmentHooks
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
const key = environment.TYPESAFE_API_KEY?.trim()
|
|
356
|
+
if (key === undefined || key.length === 0) {
|
|
357
|
+
return Effect.fail(
|
|
358
|
+
FlowLlmError.from(
|
|
359
|
+
ProviderError.make({
|
|
360
|
+
message: "judgment backend 'typesafe' needs TYPESAFE_API_KEY in the environment"
|
|
361
|
+
})
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
return Effect.succeed(
|
|
366
|
+
makeTypeSafeJudgment(
|
|
367
|
+
{ apiKey: Redacted.make(key), onUsage: judgmentHooks.onUsage },
|
|
368
|
+
dependencies.http ?? nodeHttpClient
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
}
|
|
228
372
|
const seatsFor = Effect.fn("@llm4ts/runner/FlowRunner.seatsFor")(function* (
|
|
229
373
|
workDir: string
|
|
230
374
|
): Effect.fn.Return<
|
|
@@ -232,6 +376,7 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
232
376
|
readonly coder: LlmServiceShape & { readonly capabilities: ConnectorCapabilities }
|
|
233
377
|
readonly reasoning: LlmServiceShape
|
|
234
378
|
readonly reviewers: ReadonlyArray<LlmServiceShape>
|
|
379
|
+
readonly judgment: JudgmentShape
|
|
235
380
|
},
|
|
236
381
|
FlowError,
|
|
237
382
|
Scope.Scope
|
|
@@ -244,7 +389,14 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
244
389
|
const reviewers = yield* Effect.forEach(options.reviewers ?? [], (configuration) =>
|
|
245
390
|
resolveSeat(prepareConnector(bridged(configuration), workDir, environment))
|
|
246
391
|
)
|
|
247
|
-
|
|
392
|
+
// The judgment seat defaults to the reasoning seat's service itself, so
|
|
393
|
+
// an unconfigured run resolves nothing extra.
|
|
394
|
+
const judgmentSeat =
|
|
395
|
+
options.judgment === undefined
|
|
396
|
+
? reasoningService
|
|
397
|
+
: yield* resolveSeat(prepareConnector(bridged(options.judgment), workDir, environment))
|
|
398
|
+
const judgment = yield* judgmentFor(judgmentSeat, options.judgment ?? reasoning, environment)
|
|
399
|
+
return { coder, reasoning: reasoningService, reviewers, judgment }
|
|
248
400
|
})
|
|
249
401
|
const contextAt = (
|
|
250
402
|
workDir: string,
|
|
@@ -252,12 +404,14 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
252
404
|
readonly coder: LlmServiceShape & { readonly capabilities: ConnectorCapabilities }
|
|
253
405
|
readonly reasoning: LlmServiceShape
|
|
254
406
|
readonly reviewers: ReadonlyArray<LlmServiceShape>
|
|
407
|
+
readonly judgment: JudgmentShape
|
|
255
408
|
},
|
|
256
409
|
rebind: boolean
|
|
257
410
|
): FlowContextShape =>
|
|
258
411
|
FlowContext.of({
|
|
259
412
|
reasoning: seats.reasoning,
|
|
260
413
|
coder: seats.coder,
|
|
414
|
+
judgment: seats.judgment,
|
|
261
415
|
git: makeGitTool(dependencies.process, workDir, events),
|
|
262
416
|
hosting: makeGitHubTool(dependencies.process, workDir, events),
|
|
263
417
|
events,
|
|
@@ -279,6 +433,8 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
279
433
|
return {
|
|
280
434
|
events,
|
|
281
435
|
tracker,
|
|
436
|
+
runId,
|
|
437
|
+
...(judgmentLog === undefined ? {} : { judgmentLog }),
|
|
282
438
|
context: contextAt(options.workDir, seats, true)
|
|
283
439
|
}
|
|
284
440
|
})
|
|
@@ -296,6 +452,13 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
296
452
|
const startedAt = yield* Clock.currentTimeMillis
|
|
297
453
|
const environment = options.environment ?? process.env
|
|
298
454
|
const verbosity = options.verbosity ?? "Normal"
|
|
455
|
+
const persistRun = options.persistRun ?? true
|
|
456
|
+
const tracePath =
|
|
457
|
+
options.tracePath ??
|
|
458
|
+
(persistRun ? join(options.workDir, ".llm4ts", `trace-${startedAt}.jsonl`) : undefined)
|
|
459
|
+
const costLedgerPath =
|
|
460
|
+
options.costLedgerPath ??
|
|
461
|
+
(persistRun ? join(options.workDir, ".llm4ts", CostLedgerFileName) : undefined)
|
|
299
462
|
const tracker = bundle.tracker
|
|
300
463
|
const surface = options.surface ?? (yield* makeTerminalSurface(environment))
|
|
301
464
|
const palette = surface.palette
|
|
@@ -310,22 +473,18 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
310
473
|
...(reviewers.length === 0 ? [] : [`reviewers ${reviewers.map(describeSeat).join(", ")}`])
|
|
311
474
|
].join(" · ")
|
|
312
475
|
yield* surface.log(palette.info(`${seats} · ${options.workDir}`))
|
|
313
|
-
if (
|
|
476
|
+
if (tracePath !== undefined) {
|
|
314
477
|
yield* surface.log(
|
|
315
478
|
palette.info(
|
|
316
|
-
`trace ${
|
|
479
|
+
`trace ${tracePath}${options.runId === undefined ? "" : ` · run ${options.runId}`}`
|
|
317
480
|
)
|
|
318
481
|
)
|
|
319
482
|
}
|
|
320
483
|
}
|
|
321
484
|
const recorder =
|
|
322
|
-
|
|
485
|
+
tracePath === undefined
|
|
323
486
|
? undefined
|
|
324
|
-
: yield* makeFlowRecorder(
|
|
325
|
-
dependencies.files,
|
|
326
|
-
options.tracePath,
|
|
327
|
-
options.runId ?? `run-${(yield* Clock.currentTimeMillis).toString()}`
|
|
328
|
-
)
|
|
487
|
+
: yield* makeFlowRecorder(dependencies.files, tracePath, bundle.runId)
|
|
329
488
|
if (recorder !== undefined) {
|
|
330
489
|
yield* recorder.consume(bundle.events)
|
|
331
490
|
}
|
|
@@ -349,6 +508,32 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
349
508
|
budget
|
|
350
509
|
)
|
|
351
510
|
})
|
|
511
|
+
// Runs without usage leave no record: a ledger line of zeros would only
|
|
512
|
+
// inflate run counts for backends that report nothing.
|
|
513
|
+
const appendLedger =
|
|
514
|
+
costLedgerPath === undefined
|
|
515
|
+
? Effect.void
|
|
516
|
+
: Effect.gen(function* () {
|
|
517
|
+
const cells = yield* tracker.cells
|
|
518
|
+
if (cells.length === 0) {
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
yield* appendCostRecord(
|
|
522
|
+
dependencies.files,
|
|
523
|
+
costLedgerPath,
|
|
524
|
+
makeCostRecord({
|
|
525
|
+
runId: bundle.runId,
|
|
526
|
+
at: new Date(startedAt).toISOString(),
|
|
527
|
+
repo: options.workDir,
|
|
528
|
+
prompt: options.userPrompt,
|
|
529
|
+
cells
|
|
530
|
+
})
|
|
531
|
+
)
|
|
532
|
+
}).pipe(
|
|
533
|
+
Effect.catch((error) =>
|
|
534
|
+
surface.log(palette.fail(`cost ledger not written: ${describeFlowError(error)}`))
|
|
535
|
+
)
|
|
536
|
+
)
|
|
352
537
|
return yield* body(bundle.context).pipe(
|
|
353
538
|
Effect.provideService(FlowContext, bundle.context),
|
|
354
539
|
Effect.andThen((value) => Effect.as(enforceBudget, value)),
|
|
@@ -357,10 +542,14 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
357
542
|
[
|
|
358
543
|
terminal.awaitDrained(),
|
|
359
544
|
tracker.awaitDrained(bundle.events),
|
|
360
|
-
recorder === undefined ? Effect.void : recorder.awaitDrained(bundle.events)
|
|
545
|
+
recorder === undefined ? Effect.void : recorder.awaitDrained(bundle.events),
|
|
546
|
+
bundle.judgmentLog === undefined
|
|
547
|
+
? Effect.void
|
|
548
|
+
: bundle.judgmentLog.awaitDrained(bundle.events)
|
|
361
549
|
],
|
|
362
550
|
{ concurrency: "unbounded" }
|
|
363
551
|
).pipe(
|
|
552
|
+
Effect.andThen(appendLedger),
|
|
364
553
|
Effect.andThen(surface.setStatus(undefined)),
|
|
365
554
|
Effect.andThen(tracker.summary),
|
|
366
555
|
Effect.flatMap((summary) => surface.log(`\n${summary}`))
|
|
@@ -375,8 +564,8 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
375
564
|
`flow failed after ${formatDurationMs(finishedAt - startedAt)}: ${describeFlowError(error)}`
|
|
376
565
|
)}`
|
|
377
566
|
)
|
|
378
|
-
if (
|
|
379
|
-
yield* surface.log(palette.info(`trace ${
|
|
567
|
+
if (tracePath !== undefined) {
|
|
568
|
+
yield* surface.log(palette.info(`trace ${tracePath}`))
|
|
380
569
|
}
|
|
381
570
|
})
|
|
382
571
|
),
|
package/src/Terminal.ts
CHANGED
|
@@ -26,6 +26,9 @@ export const parseVerbosity = (value: string | undefined): Verbosity => {
|
|
|
26
26
|
|
|
27
27
|
export const rendersEvent = (verbosity: Verbosity, event: FlowEvent): boolean => {
|
|
28
28
|
switch (event._tag) {
|
|
29
|
+
case "JudgmentObserved":
|
|
30
|
+
// Advise mode publishes a separate Info event for the operator.
|
|
31
|
+
return false
|
|
29
32
|
case "StageStarted":
|
|
30
33
|
case "StageCompleted":
|
|
31
34
|
case "StageFailed":
|
|
@@ -141,6 +144,8 @@ export const terminalLine = (
|
|
|
141
144
|
): string => {
|
|
142
145
|
const safe = terminalSafe
|
|
143
146
|
switch (event._tag) {
|
|
147
|
+
case "JudgmentObserved":
|
|
148
|
+
return ""
|
|
144
149
|
case "StageStarted":
|
|
145
150
|
return palette.stageStart(safe(event.stage))
|
|
146
151
|
case "StageCompleted":
|