@llm4ts/runner 2.35.3 → 2.36.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/Connectors.d.ts.map +1 -1
- package/dist/Connectors.js +8 -1
- package/dist/Connectors.js.map +1 -1
- package/dist/ExecutorRoster.d.ts +2 -2
- package/dist/ExecutorRoster.d.ts.map +1 -1
- package/dist/ExecutorRoster.js +16 -6
- package/dist/ExecutorRoster.js.map +1 -1
- package/dist/FlowRunner.d.ts.map +1 -1
- package/dist/FlowRunner.js +34 -11
- package/dist/FlowRunner.js.map +1 -1
- package/dist/NodeGeminiCliExecutor.d.ts.map +1 -1
- package/dist/NodeGeminiCliExecutor.js +19 -4
- package/dist/NodeGeminiCliExecutor.js.map +1 -1
- package/dist/NodeProcessExecutor.d.ts +7 -0
- package/dist/NodeProcessExecutor.d.ts.map +1 -1
- package/dist/NodeProcessExecutor.js +22 -4
- package/dist/NodeProcessExecutor.js.map +1 -1
- package/dist/Profile.d.ts +40 -2
- package/dist/Profile.d.ts.map +1 -1
- package/dist/Profile.js +109 -2
- package/dist/Profile.js.map +1 -1
- package/package.json +3 -3
- package/src/Connectors.ts +10 -1
- package/src/ExecutorRoster.ts +19 -6
- package/src/FlowRunner.ts +56 -17
- package/src/NodeGeminiCliExecutor.ts +49 -10
- package/src/NodeProcessExecutor.ts +27 -4
- package/src/Profile.ts +143 -3
package/src/FlowRunner.ts
CHANGED
|
@@ -88,7 +88,8 @@ import {
|
|
|
88
88
|
type ExecutorSpec,
|
|
89
89
|
type Role,
|
|
90
90
|
type RosterDocument,
|
|
91
|
-
type RosterShape
|
|
91
|
+
type RosterShape,
|
|
92
|
+
effortOf
|
|
92
93
|
} from "@llm4ts/flow/Roster"
|
|
93
94
|
import {
|
|
94
95
|
makeHeldCoder,
|
|
@@ -349,19 +350,54 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
349
350
|
// `capabilities.readOnlyEnforcement`.
|
|
350
351
|
const announceReadOnlyGrade = (
|
|
351
352
|
configuration: ConnectorConfig,
|
|
352
|
-
connector: {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
353
|
+
connector: {
|
|
354
|
+
readonly capabilities: {
|
|
355
|
+
readonly readOnlyEnforcement: string
|
|
356
|
+
readonly effort: string
|
|
357
|
+
readonly isolatedHeadless: string
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
): Effect.Effect<void> => {
|
|
361
|
+
const name = configuration.connectorId.value
|
|
362
|
+
const notes: Array<string> = []
|
|
363
|
+
if (
|
|
364
|
+
configuration instanceof CliConnectorConfig &&
|
|
365
|
+
configuration.readOnly &&
|
|
366
|
+
connector.capabilities.readOnlyEnforcement !== "enforced"
|
|
367
|
+
) {
|
|
368
|
+
notes.push(
|
|
369
|
+
`readOnly requested from '${name}', but its harness mapping ` +
|
|
370
|
+
`is ${connector.capabilities.readOnlyEnforcement} — the flag is a request, not a ` +
|
|
371
|
+
"capability removal"
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
// The same honesty for the ADR 0029 requests: an effort the harness
|
|
375
|
+
// has no flag for, an isolation the harness only partly provides.
|
|
376
|
+
if (configuration.effort !== undefined && connector.capabilities.effort !== "mapped") {
|
|
377
|
+
notes.push(
|
|
378
|
+
`effort '${configuration.effort}' requested from '${name}', but its harness has no ` +
|
|
379
|
+
"effort flag — the seat runs at the harness's own default"
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
if (
|
|
383
|
+
configuration instanceof CliConnectorConfig &&
|
|
384
|
+
configuration.isolated &&
|
|
385
|
+
connector.capabilities.isolatedHeadless !== "enforced"
|
|
386
|
+
) {
|
|
387
|
+
notes.push(
|
|
388
|
+
`isolated requested from '${name}', but its harness isolation is ` +
|
|
389
|
+
`${connector.capabilities.isolatedHeadless} — ` +
|
|
390
|
+
(connector.capabilities.isolatedHeadless === "partial"
|
|
391
|
+
? "the repository's own instruction file is still read"
|
|
392
|
+
: "nothing reaches the harness")
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
return Effect.forEach(
|
|
396
|
+
notes,
|
|
397
|
+
(detail) => events.publish(FlowEventsValues.CapabilityUnenforceable(detail)),
|
|
398
|
+
{ discard: true }
|
|
399
|
+
)
|
|
400
|
+
}
|
|
365
401
|
// Every seat is metered, so no run is silently free: a backend that reports
|
|
366
402
|
// no token counts (`capabilities.usageReporting` false) is estimated from
|
|
367
403
|
// character counts and labelled `estimated:<model>`, which is what makes
|
|
@@ -633,12 +669,15 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
633
669
|
cacheLock.withPermit(
|
|
634
670
|
Effect.gen(function* () {
|
|
635
671
|
const readOnly = role !== "coder"
|
|
636
|
-
|
|
672
|
+
// One seat per executor, mode, directory and effort: two roles
|
|
673
|
+
// asking the same effort share a process configuration.
|
|
674
|
+
const effort = effortOf(executor, role) ?? ""
|
|
675
|
+
const key = `${executor.id}\u0000${readOnly ? "ro" : "rw"}\u0000${workDir}\u0000${effort}`
|
|
637
676
|
const cached = seatCache.get(key)
|
|
638
677
|
if (cached !== undefined) {
|
|
639
678
|
return cached
|
|
640
679
|
}
|
|
641
|
-
const configuration = executorConfig(executor, readOnly, runEnvironment)
|
|
680
|
+
const configuration = executorConfig(executor, readOnly, runEnvironment, role)
|
|
642
681
|
if (configuration === undefined) {
|
|
643
682
|
return yield* RosterInvalid.make({
|
|
644
683
|
violations: [`executor '${executor.id}' has an unknown harness '${executor.harness}'`]
|
|
@@ -659,7 +698,7 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
659
698
|
document.executors
|
|
660
699
|
.filter((spec) => rolesOf(spec).includes("judge"))
|
|
661
700
|
.flatMap((spec) => {
|
|
662
|
-
const configuration = executorConfig(spec, true, runEnvironment)
|
|
701
|
+
const configuration = executorConfig(spec, true, runEnvironment, "judge")
|
|
663
702
|
return configuration === undefined ? [] : [configuration]
|
|
664
703
|
})[0] ??
|
|
665
704
|
reasoning
|
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
GeminiCliExecutor,
|
|
9
9
|
buildGeminiArgs,
|
|
10
10
|
extractGeminiCliResponse,
|
|
11
|
+
geminiHarnessPolicyToml,
|
|
11
12
|
geminiProcessEnv,
|
|
13
|
+
geminiSupportsAdminPolicy,
|
|
12
14
|
geminiTurnLimitSettingsJson,
|
|
13
15
|
parseGeminiCliStreamEvent,
|
|
14
16
|
validateGeminiExitCode,
|
|
@@ -37,6 +39,40 @@ export const makeNodeGeminiCliExecutor = (
|
|
|
37
39
|
),
|
|
38
40
|
(path) => geminiProcessEnv(context, path)
|
|
39
41
|
)
|
|
42
|
+
// The harness policy (`geminiHarnessPolicyToml`) goes in with
|
|
43
|
+
// `--admin-policy` on a CLI that lists the flag: `gemini --help` is asked
|
|
44
|
+
// once, and an older CLI simply runs without it — an unknown flag would
|
|
45
|
+
// fail every turn.
|
|
46
|
+
let adminPolicy: boolean | undefined
|
|
47
|
+
const supportsAdminPolicy: Effect.Effect<boolean> = Effect.suspend(() =>
|
|
48
|
+
adminPolicy !== undefined
|
|
49
|
+
? Effect.succeed(adminPolicy)
|
|
50
|
+
: executor.run(["gemini", "--help"], process.cwd(), {}).pipe(
|
|
51
|
+
Effect.map((result) =>
|
|
52
|
+
geminiSupportsAdminPolicy([...result.stdout, ...result.stderr].join("\n"))
|
|
53
|
+
),
|
|
54
|
+
Effect.catch(() => Effect.succeed(false)),
|
|
55
|
+
Effect.tap((supported) =>
|
|
56
|
+
Effect.sync(() => {
|
|
57
|
+
adminPolicy = supported
|
|
58
|
+
})
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
/** The turn's arguments: the policy file, written for the turn, when the CLI takes it. */
|
|
63
|
+
const argsFor = (
|
|
64
|
+
config: Parameters<typeof buildGeminiArgs>[0],
|
|
65
|
+
context: GeminiCliExecutionContext,
|
|
66
|
+
outputFormat: string
|
|
67
|
+
) =>
|
|
68
|
+
Effect.flatMap(supportsAdminPolicy, (supported) =>
|
|
69
|
+
supported
|
|
70
|
+
? Effect.map(
|
|
71
|
+
temporaryFiles.write("gemini-policy-", ".toml", geminiHarnessPolicyToml),
|
|
72
|
+
(path) => buildGeminiArgs(config, context, outputFormat, path)
|
|
73
|
+
)
|
|
74
|
+
: Effect.succeed(buildGeminiArgs(config, context, outputFormat))
|
|
75
|
+
)
|
|
40
76
|
return {
|
|
41
77
|
checkGeminiInstalled: executor
|
|
42
78
|
.run(["gemini", "--version"], process.cwd(), {})
|
|
@@ -51,8 +87,9 @@ export const makeNodeGeminiCliExecutor = (
|
|
|
51
87
|
Effect.scoped(
|
|
52
88
|
Effect.gen(function* () {
|
|
53
89
|
const environment = yield* environmentFor(context)
|
|
90
|
+
const args = yield* argsFor(config, context, "json")
|
|
54
91
|
const result = yield* executor.runWithStdin(
|
|
55
|
-
["gemini", ...
|
|
92
|
+
["gemini", ...args],
|
|
56
93
|
context.cwd ?? process.cwd(),
|
|
57
94
|
environment,
|
|
58
95
|
prompt
|
|
@@ -69,15 +106,17 @@ export const makeNodeGeminiCliExecutor = (
|
|
|
69
106
|
runGeminiProcessStream: (prompt, config, context) =>
|
|
70
107
|
Stream.scoped(
|
|
71
108
|
Stream.unwrap(
|
|
72
|
-
Effect.map(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
109
|
+
Effect.map(
|
|
110
|
+
Effect.all([environmentFor(context), argsFor(config, context, "stream-json")]),
|
|
111
|
+
([environment, args]) =>
|
|
112
|
+
executor
|
|
113
|
+
.runStreamingWithStdin(
|
|
114
|
+
["gemini", ...args],
|
|
115
|
+
context.cwd ?? process.cwd(),
|
|
116
|
+
environment,
|
|
117
|
+
prompt
|
|
118
|
+
)
|
|
119
|
+
.pipe(Stream.map(parseGeminiCliStreamEvent))
|
|
81
120
|
)
|
|
82
121
|
)
|
|
83
122
|
)
|
|
@@ -32,6 +32,32 @@ const processFailure = (argv: ReadonlyArray<string>, detail: string): ProviderEr
|
|
|
32
32
|
: `${commandName(argv)} failed: ${detail}`
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The orchestrator's own variables: its `LLM4TS_*` knobs and `_`, the path
|
|
37
|
+
* of the binary the shell started. The standard `OTEL_*` variables stay:
|
|
38
|
+
* other OpenTelemetry-aware tools (a CLI's own telemetry) read them too.
|
|
39
|
+
*/
|
|
40
|
+
const orchestratorOwn = (name: string): boolean => name === "_" || name.startsWith("LLM4TS_")
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* What a child process inherits: this process's environment without the
|
|
44
|
+
* orchestrator's own variables, then `envVars`. A coding agent, a gate or a
|
|
45
|
+
* setup command has no use for llm4ts's knobs, and an agent that lists its
|
|
46
|
+
* environment should not be pointed at the tool running it.
|
|
47
|
+
*/
|
|
48
|
+
export const childEnvironment = (
|
|
49
|
+
parent: Readonly<Record<string, string | undefined>>,
|
|
50
|
+
envVars: Readonly<Record<string, string>>
|
|
51
|
+
): Record<string, string> => {
|
|
52
|
+
const inherited: Record<string, string> = {}
|
|
53
|
+
for (const [name, value] of Object.entries(parent)) {
|
|
54
|
+
if (value !== undefined && !orchestratorOwn(name)) {
|
|
55
|
+
inherited[name] = value
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { ...inherited, ...envVars }
|
|
59
|
+
}
|
|
60
|
+
|
|
35
61
|
const spawnChild = (
|
|
36
62
|
argv: ReadonlyArray<string>,
|
|
37
63
|
cwd: string,
|
|
@@ -58,10 +84,7 @@ const spawnChild = (
|
|
|
58
84
|
try: () =>
|
|
59
85
|
spawn(file, spawnArgs, {
|
|
60
86
|
cwd,
|
|
61
|
-
env:
|
|
62
|
-
...process.env,
|
|
63
|
-
...envVars
|
|
64
|
-
},
|
|
87
|
+
env: childEnvironment(process.env, envVars),
|
|
65
88
|
stdio: "pipe",
|
|
66
89
|
...(windows === undefined ? {} : { windowsVerbatimArguments: windows.verbatim })
|
|
67
90
|
}),
|
package/src/Profile.ts
CHANGED
|
@@ -9,9 +9,9 @@ export { duration }
|
|
|
9
9
|
import { formatCount } from "./Terminal.ts"
|
|
10
10
|
import {
|
|
11
11
|
resolveTraceTarget,
|
|
12
|
+
WatchTargetMissing,
|
|
12
13
|
type TraceChoice,
|
|
13
|
-
type TraceSources
|
|
14
|
-
type WatchTargetMissing
|
|
14
|
+
type TraceSources
|
|
15
15
|
} from "./Watch.ts"
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -1014,18 +1014,158 @@ export const renderProfile = (report: ProfileReport): string => {
|
|
|
1014
1014
|
].join("\n")
|
|
1015
1015
|
}
|
|
1016
1016
|
|
|
1017
|
+
// ── delta ───────────────────────────────────────────────────────────────────
|
|
1018
|
+
|
|
1019
|
+
/** One line of a before/after comparison (ADR 0029): a count or a duration. */
|
|
1020
|
+
export const DeltaRow = Schema.Struct({
|
|
1021
|
+
label: Schema.String,
|
|
1022
|
+
before: Schema.Number,
|
|
1023
|
+
after: Schema.Number,
|
|
1024
|
+
unit: Schema.Literals(["count", "ms"]),
|
|
1025
|
+
/** Which direction is better, for the arrow; `none` for neutral facts. */
|
|
1026
|
+
better: Schema.Literals(["lower", "higher", "none"])
|
|
1027
|
+
})
|
|
1028
|
+
export type DeltaRow = typeof DeltaRow.Type
|
|
1029
|
+
|
|
1030
|
+
export const ProfileDelta = Schema.Struct({
|
|
1031
|
+
rows: Schema.Array(DeltaRow)
|
|
1032
|
+
})
|
|
1033
|
+
export type ProfileDelta = typeof ProfileDelta.Type
|
|
1034
|
+
|
|
1035
|
+
const roleCalls = (report: ProfileReport, role: string): number =>
|
|
1036
|
+
sum(report.models.filter((row) => row.role === role).map((row) => row.calls))
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* The harness-evals comparison (`docs/harness-evals/README.md`), in its
|
|
1040
|
+
* order: how much the coders explored before editing, how often review and
|
|
1041
|
+
* judgment were called, what the gates said, how honest the claims were,
|
|
1042
|
+
* then where the time went. Tokens and cost are `llm4ts costs`' business.
|
|
1043
|
+
*/
|
|
1044
|
+
export const profileDelta = (before: ProfileReport, after: ProfileReport): ProfileDelta => {
|
|
1045
|
+
const count = (
|
|
1046
|
+
label: string,
|
|
1047
|
+
pick: (report: ProfileReport) => number,
|
|
1048
|
+
better: DeltaRow["better"] = "lower"
|
|
1049
|
+
): DeltaRow => ({
|
|
1050
|
+
label,
|
|
1051
|
+
before: pick(before),
|
|
1052
|
+
after: pick(after),
|
|
1053
|
+
unit: "count",
|
|
1054
|
+
better
|
|
1055
|
+
})
|
|
1056
|
+
const time = (label: string, pick: (report: ProfileReport) => number): DeltaRow => ({
|
|
1057
|
+
label,
|
|
1058
|
+
before: pick(before),
|
|
1059
|
+
after: pick(after),
|
|
1060
|
+
unit: "ms",
|
|
1061
|
+
better: "lower"
|
|
1062
|
+
})
|
|
1063
|
+
return {
|
|
1064
|
+
rows: [
|
|
1065
|
+
count("stories done", (r) => r.stories.filter((s) => s.status === "done").length, "higher"),
|
|
1066
|
+
count("stories failed", (r) => r.stories.filter((s) => s.status === "failed").length),
|
|
1067
|
+
count("explore calls before the first edit", (r) =>
|
|
1068
|
+
sum(r.stories.map((s) => s.exploreBeforeEdit))
|
|
1069
|
+
),
|
|
1070
|
+
count("coder turns", (r) => sum(r.stories.map((s) => s.turns.count))),
|
|
1071
|
+
count("coder edits", (r) => sum(r.stories.map((s) => s.toolCalls.edit)), "none"),
|
|
1072
|
+
count("coder test runs", (r) => sum(r.stories.map((s) => s.toolCalls.test)), "none"),
|
|
1073
|
+
count("reviewer calls", (r) => roleCalls(r, "reviewer")),
|
|
1074
|
+
count("judge calls", (r) => roleCalls(r, "judge")),
|
|
1075
|
+
count("gate runs", (r) => sum(r.gates.map((g) => g.runs))),
|
|
1076
|
+
count("gate failures", (r) => sum(r.gates.map((g) => g.failed))),
|
|
1077
|
+
count("fabricated verification claims", (r) => sum(r.stories.map((s) => s.unverifiedClaims))),
|
|
1078
|
+
count("low-confidence tasks", (r) => sum(r.stories.map((s) => s.lowConfidenceTasks))),
|
|
1079
|
+
count(
|
|
1080
|
+
"stories whose prompt grew over 3×",
|
|
1081
|
+
(r) => r.stories.filter((s) => s.turns.promptGrowth).length
|
|
1082
|
+
),
|
|
1083
|
+
time("wall", (r) => r.wallMs),
|
|
1084
|
+
time("model time", (r) => r.totals.model),
|
|
1085
|
+
time("coder tool time", (r) => r.totals.tools),
|
|
1086
|
+
time("gate time", (r) => r.totals.gates),
|
|
1087
|
+
time("waiting", (r) => r.totals.waiting)
|
|
1088
|
+
]
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const deltaCell = (row: DeltaRow): string => {
|
|
1093
|
+
const diff = row.after - row.before
|
|
1094
|
+
if (diff === 0) {
|
|
1095
|
+
return "="
|
|
1096
|
+
}
|
|
1097
|
+
const shown = row.unit === "ms" ? duration(Math.abs(diff)) : String(Math.abs(diff))
|
|
1098
|
+
const sign = diff > 0 ? "+" : "-"
|
|
1099
|
+
const verdict =
|
|
1100
|
+
row.better === "none" ? "" : diff < 0 === (row.better === "lower") ? " better" : " worse"
|
|
1101
|
+
const percent = row.before === 0 ? "" : ` (${Math.round((diff / row.before) * 100)}%)`
|
|
1102
|
+
return `${sign}${shown}${percent}${verdict}`
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
export const renderProfileDelta = (
|
|
1106
|
+
delta: ProfileDelta,
|
|
1107
|
+
labels: { readonly before: string; readonly after: string }
|
|
1108
|
+
): string =>
|
|
1109
|
+
[
|
|
1110
|
+
`llm4ts profile · ${labels.before} → ${labels.after}`,
|
|
1111
|
+
"",
|
|
1112
|
+
...table(
|
|
1113
|
+
["measure", "before", "after", "change"],
|
|
1114
|
+
delta.rows.map((row) => [
|
|
1115
|
+
row.label,
|
|
1116
|
+
row.unit === "ms" ? duration(row.before) : String(row.before),
|
|
1117
|
+
row.unit === "ms" ? duration(row.after) : String(row.after),
|
|
1118
|
+
deltaCell(row)
|
|
1119
|
+
])
|
|
1120
|
+
),
|
|
1121
|
+
"",
|
|
1122
|
+
"* tokens and cost per role: llm4ts costs --repo <repo>"
|
|
1123
|
+
].join("\n")
|
|
1124
|
+
|
|
1017
1125
|
export interface ProfileOptions extends TraceChoice {
|
|
1018
1126
|
readonly json?: boolean
|
|
1127
|
+
/** A trace, or a `--json` report saved earlier, to compare the profiled run against (ADR 0029). */
|
|
1128
|
+
readonly against?: string
|
|
1019
1129
|
}
|
|
1020
1130
|
|
|
1021
1131
|
const encodeReport = Schema.encodeSync(Schema.fromJsonString(ProfileReport))
|
|
1132
|
+
const decodeReport = Schema.decodeUnknownOption(Schema.fromJsonString(ProfileReport))
|
|
1133
|
+
const encodeDelta = Schema.encodeSync(Schema.fromJsonString(ProfileDelta))
|
|
1022
1134
|
|
|
1023
|
-
|
|
1135
|
+
const reportAt = Effect.fn("@llm4ts/runner/Profile.reportAt")(function* (
|
|
1136
|
+
files: TraceSources["files"],
|
|
1137
|
+
path: string
|
|
1138
|
+
): Effect.fn.Return<ProfileReport, FlowError | WatchTargetMissing> {
|
|
1139
|
+
if (path.endsWith(".json")) {
|
|
1140
|
+
const text = yield* files.read(path)
|
|
1141
|
+
const decoded = text === undefined ? undefined : decodeReport(text)
|
|
1142
|
+
if (decoded === undefined || decoded._tag === "None") {
|
|
1143
|
+
return yield* WatchTargetMissing.make({
|
|
1144
|
+
message: `${path} is not a profile report (write one with llm4ts profile --json)`
|
|
1145
|
+
})
|
|
1146
|
+
}
|
|
1147
|
+
return decoded.value
|
|
1148
|
+
}
|
|
1149
|
+
return profileOf(treeInputsOfTrace(yield* readTrace(files, path)))
|
|
1150
|
+
})
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* The report for a trace, an epic's latest run, or the newest trace: text,
|
|
1154
|
+
* or JSON for comparing runs; with `against`, the delta from that earlier
|
|
1155
|
+
* run to this one.
|
|
1156
|
+
*/
|
|
1024
1157
|
export const makeProfileProgram = Effect.fn("@llm4ts/runner/Profile.make")(function* (
|
|
1025
1158
|
options: ProfileOptions,
|
|
1026
1159
|
sources: TraceSources
|
|
1027
1160
|
): Effect.fn.Return<string, FlowError | WatchTargetMissing> {
|
|
1028
1161
|
const target = yield* resolveTraceTarget(options, sources)
|
|
1029
1162
|
const report = profileOf(treeInputsOfTrace(yield* readTrace(sources.files, target.tracePath)))
|
|
1163
|
+
if (options.against !== undefined) {
|
|
1164
|
+
const before = yield* reportAt(sources.files, options.against)
|
|
1165
|
+
const delta = profileDelta(before, report)
|
|
1166
|
+
return options.json === true
|
|
1167
|
+
? encodeDelta(delta)
|
|
1168
|
+
: renderProfileDelta(delta, { before: options.against, after: target.tracePath })
|
|
1169
|
+
}
|
|
1030
1170
|
return options.json === true ? encodeReport(report) : renderProfile(report)
|
|
1031
1171
|
})
|