@llm4ts/runner 2.5.0 → 2.6.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 +35 -0
- package/dist/FlowRunner.d.ts.map +1 -1
- package/dist/FlowRunner.js +87 -11
- 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 +170 -14
- 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,25 @@ 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"
|
|
25
37
|
import { FlowLlmError, describeFlowError, type FlowError } from "@llm4ts/flow/FlowError"
|
|
26
38
|
import {
|
|
27
39
|
FlowEvents,
|
|
28
40
|
FlowEventsValues,
|
|
41
|
+
Info,
|
|
42
|
+
TokensUsed,
|
|
29
43
|
makeFlowEventHub,
|
|
30
44
|
type FlowEventHub
|
|
31
45
|
} from "@llm4ts/flow/FlowEvents"
|
|
32
46
|
import { FlowContext, type FlowContextShape } from "@llm4ts/flow/FlowContext"
|
|
33
47
|
import { makeFlowRecorder } from "@llm4ts/flow/FlowRecorder"
|
|
48
|
+
import { makeJudgmentLog, type JudgmentLogShape } from "@llm4ts/flow/JudgmentLog"
|
|
34
49
|
import { makeGitHubTool } from "@llm4ts/flow/GitHubTool"
|
|
35
50
|
import { makeGitTool } from "@llm4ts/flow/GitTool"
|
|
36
51
|
import type { PlainFileStoreShape } from "@llm4ts/flow/Persistence"
|
|
@@ -61,6 +76,8 @@ export interface FlowRunnerDependencies {
|
|
|
61
76
|
readonly files: PlainFileStoreShape
|
|
62
77
|
/** pi's own model config (ADR 0016), injectable so a test never reads $HOME. */
|
|
63
78
|
readonly readPiModelsJson?: () => string | undefined
|
|
79
|
+
/** For the hosted TypeSafe judgment backend; defaults to the Node client. */
|
|
80
|
+
readonly http?: HttpClientShape
|
|
64
81
|
}
|
|
65
82
|
|
|
66
83
|
export interface NodeConnectorDependencies {
|
|
@@ -73,6 +90,7 @@ export interface NodeConnectorDependencies {
|
|
|
73
90
|
export const nodeFlowRunnerDependencies = (): FlowRunnerDependencies => ({
|
|
74
91
|
process: nodeProcessExecutor,
|
|
75
92
|
files: nodePlainFileStore,
|
|
93
|
+
http: nodeHttpClient,
|
|
76
94
|
registry: createConnectorRegistry({
|
|
77
95
|
http: nodeHttpClient,
|
|
78
96
|
process: nodeProcessExecutor,
|
|
@@ -88,7 +106,36 @@ export interface FlowRunnerOptions {
|
|
|
88
106
|
readonly coder: ConnectorConfig
|
|
89
107
|
readonly reasoning?: ConnectorConfig
|
|
90
108
|
readonly reviewers?: ReadonlyArray<ConnectorConfig>
|
|
109
|
+
/**
|
|
110
|
+
* The seat typed judgments run on (ADR 0017). Defaults to the reasoning
|
|
111
|
+
* seat; point it at a small non-thinking model for one-token label scoring.
|
|
112
|
+
*/
|
|
113
|
+
readonly judgment?: ConnectorConfig
|
|
114
|
+
/**
|
|
115
|
+
* `llm` (default) answers on the judgment seat; `typesafe` uses the hosted
|
|
116
|
+
* Jev model with `TYPESAFE_API_KEY` from the environment. Unset, the
|
|
117
|
+
* environment's `LLM4TS_JUDGMENT_BACKEND` decides, then `llm`.
|
|
118
|
+
*/
|
|
119
|
+
readonly judgmentBackend?: JudgmentBackend
|
|
120
|
+
/** Persist judgment observations under workDir/.llm4ts/judgments; off by default. */
|
|
121
|
+
readonly judgmentLog?: boolean
|
|
122
|
+
/**
|
|
123
|
+
* Where the run's trace goes. Default `workDir/.llm4ts/trace-<start ms>.jsonl`
|
|
124
|
+
* while `persistRun` holds.
|
|
125
|
+
*/
|
|
91
126
|
readonly tracePath?: string
|
|
127
|
+
/**
|
|
128
|
+
* Append-only ledger (`CostLedger`) that receives one `CostRecord` per run
|
|
129
|
+
* that accrued usage, so `llm4ts costs` and budgets can look across runs.
|
|
130
|
+
* Default `workDir/.llm4ts/costs.jsonl` while `persistRun` holds.
|
|
131
|
+
*/
|
|
132
|
+
readonly costLedgerPath?: string
|
|
133
|
+
/**
|
|
134
|
+
* `false` writes neither the trace nor the ledger unless their paths are
|
|
135
|
+
* given explicitly. Default `true`: every run leaves its record under
|
|
136
|
+
* `workDir/.llm4ts/`, which `llm4ts costs` and replay read back.
|
|
137
|
+
*/
|
|
138
|
+
readonly persistRun?: boolean
|
|
92
139
|
readonly runId?: string
|
|
93
140
|
readonly verbosity?: Verbosity
|
|
94
141
|
readonly surface?: TerminalSurface
|
|
@@ -108,6 +155,8 @@ export interface FlowRunnerBundle {
|
|
|
108
155
|
readonly events: FlowEventHub
|
|
109
156
|
/** The run's cost tracker, already subscribed to `events`. */
|
|
110
157
|
readonly tracker: CostTracker
|
|
158
|
+
readonly runId: string
|
|
159
|
+
readonly judgmentLog?: JudgmentLogShape
|
|
111
160
|
}
|
|
112
161
|
|
|
113
162
|
const truthy = (value: string | undefined): boolean =>
|
|
@@ -177,6 +226,12 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
177
226
|
dependencies: FlowRunnerDependencies
|
|
178
227
|
): Effect.fn.Return<FlowRunnerBundle, FlowError, Scope.Scope> {
|
|
179
228
|
const events = yield* makeFlowEventHub()
|
|
229
|
+
const runId = options.runId ?? `run-${(yield* Clock.currentTimeMillis).toString()}`
|
|
230
|
+
const judgmentLog =
|
|
231
|
+
options.judgmentLog === true
|
|
232
|
+
? yield* makeJudgmentLog({ files: dependencies.files, root: options.workDir, runId })
|
|
233
|
+
: undefined
|
|
234
|
+
if (judgmentLog !== undefined) yield* judgmentLog.consume(events)
|
|
180
235
|
const tracker = yield* makeCostTracker()
|
|
181
236
|
yield* tracker.consume(events)
|
|
182
237
|
const reasoning = defaultReasoningConfig(options.coder, options.reasoning)
|
|
@@ -225,6 +280,62 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
225
280
|
)
|
|
226
281
|
)
|
|
227
282
|
)
|
|
283
|
+
// Judgment usage is metered like any chat (ADR 0005 amendment) and every
|
|
284
|
+
// fallback from label scoring to the verbalized path is announced.
|
|
285
|
+
const judgmentHooks = {
|
|
286
|
+
onUsage: (usage: TokenUsage, model: string | undefined) =>
|
|
287
|
+
events.publish(
|
|
288
|
+
new TokensUsed({ agent: "judgment", usage, ...(model === undefined ? {} : { model }) })
|
|
289
|
+
),
|
|
290
|
+
onFallback: (key: string, reason: string) =>
|
|
291
|
+
events.publish(
|
|
292
|
+
new Info({ message: `judgment '${key}' fell back to verbalized scoring: ${reason}` })
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
const judgmentFor = (
|
|
296
|
+
seat: LlmServiceShape,
|
|
297
|
+
seatConfig: ConnectorConfig,
|
|
298
|
+
environment: Readonly<Record<string, string | undefined>>
|
|
299
|
+
): Effect.Effect<JudgmentShape, FlowError> => {
|
|
300
|
+
const backend =
|
|
301
|
+
options.judgmentBackend ??
|
|
302
|
+
(environment.LLM4TS_JUDGMENT_BACKEND?.trim().toLowerCase() === "typesafe"
|
|
303
|
+
? "typesafe"
|
|
304
|
+
: "llm")
|
|
305
|
+
if (backend !== "typesafe") {
|
|
306
|
+
return Effect.succeed(
|
|
307
|
+
makeLlmJudgment(
|
|
308
|
+
seat,
|
|
309
|
+
LlmJudgmentConfig.make({
|
|
310
|
+
connector: seatConfig.connectorId.value,
|
|
311
|
+
...(seatConfig.model === undefined ? {} : { model: seatConfig.model }),
|
|
312
|
+
// LLM4TS_JUDGMENT_BATCHING=shared-prefix sends each request's state
|
|
313
|
+
// once with every question; anything else keeps them independent.
|
|
314
|
+
...(environment.LLM4TS_JUDGMENT_BATCHING?.trim().toLowerCase() === "shared-prefix"
|
|
315
|
+
? { batching: "shared-prefix" as const }
|
|
316
|
+
: {})
|
|
317
|
+
}),
|
|
318
|
+
judgmentHooks
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
const key = environment.TYPESAFE_API_KEY?.trim()
|
|
323
|
+
if (key === undefined || key.length === 0) {
|
|
324
|
+
return Effect.fail(
|
|
325
|
+
FlowLlmError.from(
|
|
326
|
+
ProviderError.make({
|
|
327
|
+
message: "judgment backend 'typesafe' needs TYPESAFE_API_KEY in the environment"
|
|
328
|
+
})
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
return Effect.succeed(
|
|
333
|
+
makeTypeSafeJudgment(
|
|
334
|
+
{ apiKey: Redacted.make(key), onUsage: judgmentHooks.onUsage },
|
|
335
|
+
dependencies.http ?? nodeHttpClient
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
}
|
|
228
339
|
const seatsFor = Effect.fn("@llm4ts/runner/FlowRunner.seatsFor")(function* (
|
|
229
340
|
workDir: string
|
|
230
341
|
): Effect.fn.Return<
|
|
@@ -232,6 +343,7 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
232
343
|
readonly coder: LlmServiceShape & { readonly capabilities: ConnectorCapabilities }
|
|
233
344
|
readonly reasoning: LlmServiceShape
|
|
234
345
|
readonly reviewers: ReadonlyArray<LlmServiceShape>
|
|
346
|
+
readonly judgment: JudgmentShape
|
|
235
347
|
},
|
|
236
348
|
FlowError,
|
|
237
349
|
Scope.Scope
|
|
@@ -244,7 +356,14 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
244
356
|
const reviewers = yield* Effect.forEach(options.reviewers ?? [], (configuration) =>
|
|
245
357
|
resolveSeat(prepareConnector(bridged(configuration), workDir, environment))
|
|
246
358
|
)
|
|
247
|
-
|
|
359
|
+
// The judgment seat defaults to the reasoning seat's service itself, so
|
|
360
|
+
// an unconfigured run resolves nothing extra.
|
|
361
|
+
const judgmentSeat =
|
|
362
|
+
options.judgment === undefined
|
|
363
|
+
? reasoningService
|
|
364
|
+
: yield* resolveSeat(prepareConnector(bridged(options.judgment), workDir, environment))
|
|
365
|
+
const judgment = yield* judgmentFor(judgmentSeat, options.judgment ?? reasoning, environment)
|
|
366
|
+
return { coder, reasoning: reasoningService, reviewers, judgment }
|
|
248
367
|
})
|
|
249
368
|
const contextAt = (
|
|
250
369
|
workDir: string,
|
|
@@ -252,12 +371,14 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
252
371
|
readonly coder: LlmServiceShape & { readonly capabilities: ConnectorCapabilities }
|
|
253
372
|
readonly reasoning: LlmServiceShape
|
|
254
373
|
readonly reviewers: ReadonlyArray<LlmServiceShape>
|
|
374
|
+
readonly judgment: JudgmentShape
|
|
255
375
|
},
|
|
256
376
|
rebind: boolean
|
|
257
377
|
): FlowContextShape =>
|
|
258
378
|
FlowContext.of({
|
|
259
379
|
reasoning: seats.reasoning,
|
|
260
380
|
coder: seats.coder,
|
|
381
|
+
judgment: seats.judgment,
|
|
261
382
|
git: makeGitTool(dependencies.process, workDir, events),
|
|
262
383
|
hosting: makeGitHubTool(dependencies.process, workDir, events),
|
|
263
384
|
events,
|
|
@@ -279,6 +400,8 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
279
400
|
return {
|
|
280
401
|
events,
|
|
281
402
|
tracker,
|
|
403
|
+
runId,
|
|
404
|
+
...(judgmentLog === undefined ? {} : { judgmentLog }),
|
|
282
405
|
context: contextAt(options.workDir, seats, true)
|
|
283
406
|
}
|
|
284
407
|
})
|
|
@@ -296,6 +419,13 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
296
419
|
const startedAt = yield* Clock.currentTimeMillis
|
|
297
420
|
const environment = options.environment ?? process.env
|
|
298
421
|
const verbosity = options.verbosity ?? "Normal"
|
|
422
|
+
const persistRun = options.persistRun ?? true
|
|
423
|
+
const tracePath =
|
|
424
|
+
options.tracePath ??
|
|
425
|
+
(persistRun ? join(options.workDir, ".llm4ts", `trace-${startedAt}.jsonl`) : undefined)
|
|
426
|
+
const costLedgerPath =
|
|
427
|
+
options.costLedgerPath ??
|
|
428
|
+
(persistRun ? join(options.workDir, ".llm4ts", CostLedgerFileName) : undefined)
|
|
299
429
|
const tracker = bundle.tracker
|
|
300
430
|
const surface = options.surface ?? (yield* makeTerminalSurface(environment))
|
|
301
431
|
const palette = surface.palette
|
|
@@ -310,22 +440,18 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
310
440
|
...(reviewers.length === 0 ? [] : [`reviewers ${reviewers.map(describeSeat).join(", ")}`])
|
|
311
441
|
].join(" · ")
|
|
312
442
|
yield* surface.log(palette.info(`${seats} · ${options.workDir}`))
|
|
313
|
-
if (
|
|
443
|
+
if (tracePath !== undefined) {
|
|
314
444
|
yield* surface.log(
|
|
315
445
|
palette.info(
|
|
316
|
-
`trace ${
|
|
446
|
+
`trace ${tracePath}${options.runId === undefined ? "" : ` · run ${options.runId}`}`
|
|
317
447
|
)
|
|
318
448
|
)
|
|
319
449
|
}
|
|
320
450
|
}
|
|
321
451
|
const recorder =
|
|
322
|
-
|
|
452
|
+
tracePath === undefined
|
|
323
453
|
? undefined
|
|
324
|
-
: yield* makeFlowRecorder(
|
|
325
|
-
dependencies.files,
|
|
326
|
-
options.tracePath,
|
|
327
|
-
options.runId ?? `run-${(yield* Clock.currentTimeMillis).toString()}`
|
|
328
|
-
)
|
|
454
|
+
: yield* makeFlowRecorder(dependencies.files, tracePath, bundle.runId)
|
|
329
455
|
if (recorder !== undefined) {
|
|
330
456
|
yield* recorder.consume(bundle.events)
|
|
331
457
|
}
|
|
@@ -349,6 +475,32 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
349
475
|
budget
|
|
350
476
|
)
|
|
351
477
|
})
|
|
478
|
+
// Runs without usage leave no record: a ledger line of zeros would only
|
|
479
|
+
// inflate run counts for backends that report nothing.
|
|
480
|
+
const appendLedger =
|
|
481
|
+
costLedgerPath === undefined
|
|
482
|
+
? Effect.void
|
|
483
|
+
: Effect.gen(function* () {
|
|
484
|
+
const cells = yield* tracker.cells
|
|
485
|
+
if (cells.length === 0) {
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
yield* appendCostRecord(
|
|
489
|
+
dependencies.files,
|
|
490
|
+
costLedgerPath,
|
|
491
|
+
makeCostRecord({
|
|
492
|
+
runId: bundle.runId,
|
|
493
|
+
at: new Date(startedAt).toISOString(),
|
|
494
|
+
repo: options.workDir,
|
|
495
|
+
prompt: options.userPrompt,
|
|
496
|
+
cells
|
|
497
|
+
})
|
|
498
|
+
)
|
|
499
|
+
}).pipe(
|
|
500
|
+
Effect.catch((error) =>
|
|
501
|
+
surface.log(palette.fail(`cost ledger not written: ${describeFlowError(error)}`))
|
|
502
|
+
)
|
|
503
|
+
)
|
|
352
504
|
return yield* body(bundle.context).pipe(
|
|
353
505
|
Effect.provideService(FlowContext, bundle.context),
|
|
354
506
|
Effect.andThen((value) => Effect.as(enforceBudget, value)),
|
|
@@ -357,10 +509,14 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
357
509
|
[
|
|
358
510
|
terminal.awaitDrained(),
|
|
359
511
|
tracker.awaitDrained(bundle.events),
|
|
360
|
-
recorder === undefined ? Effect.void : recorder.awaitDrained(bundle.events)
|
|
512
|
+
recorder === undefined ? Effect.void : recorder.awaitDrained(bundle.events),
|
|
513
|
+
bundle.judgmentLog === undefined
|
|
514
|
+
? Effect.void
|
|
515
|
+
: bundle.judgmentLog.awaitDrained(bundle.events)
|
|
361
516
|
],
|
|
362
517
|
{ concurrency: "unbounded" }
|
|
363
518
|
).pipe(
|
|
519
|
+
Effect.andThen(appendLedger),
|
|
364
520
|
Effect.andThen(surface.setStatus(undefined)),
|
|
365
521
|
Effect.andThen(tracker.summary),
|
|
366
522
|
Effect.flatMap((summary) => surface.log(`\n${summary}`))
|
|
@@ -375,8 +531,8 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
375
531
|
`flow failed after ${formatDurationMs(finishedAt - startedAt)}: ${describeFlowError(error)}`
|
|
376
532
|
)}`
|
|
377
533
|
)
|
|
378
|
-
if (
|
|
379
|
-
yield* surface.log(palette.info(`trace ${
|
|
534
|
+
if (tracePath !== undefined) {
|
|
535
|
+
yield* surface.log(palette.info(`trace ${tracePath}`))
|
|
380
536
|
}
|
|
381
537
|
})
|
|
382
538
|
),
|
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":
|