@llm4ts/runner 2.29.0 → 2.30.1
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/Doctor.d.ts +4 -1
- package/dist/Doctor.d.ts.map +1 -1
- package/dist/Doctor.js +51 -44
- package/dist/Doctor.js.map +1 -1
- package/dist/FlowArgs.d.ts +1 -1
- package/dist/FlowArgs.d.ts.map +1 -1
- package/dist/FlowArgs.js +2 -0
- package/dist/FlowArgs.js.map +1 -1
- package/dist/FlowRunner.d.ts.map +1 -1
- package/dist/FlowRunner.js +40 -7
- package/dist/FlowRunner.js.map +1 -1
- package/dist/Otel.d.ts +70 -0
- package/dist/Otel.d.ts.map +1 -0
- package/dist/Otel.js +154 -0
- package/dist/Otel.js.map +1 -0
- package/dist/Package.d.ts +1 -0
- package/dist/Package.d.ts.map +1 -1
- package/dist/Package.js +5 -0
- package/dist/Package.js.map +1 -1
- package/package.json +4 -3
- package/src/Doctor.ts +67 -56
- package/src/FlowArgs.ts +2 -0
- package/src/FlowRunner.ts +73 -8
- package/src/Otel.ts +221 -0
- package/src/Package.ts +10 -0
package/dist/Otel.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// OpenTelemetry export (ADR 0026): Effect's own OTLP tracer and metrics
|
|
2
|
+
// exporters over core's fetch client, so a run can show up in Phoenix,
|
|
3
|
+
// Langfuse, SigNoz or any collector with no SDK and no new dependency.
|
|
4
|
+
// The standard OTEL_* variables are the truth; `--otel` is the laptop case.
|
|
5
|
+
import * as ConfigProvider from "effect/ConfigProvider";
|
|
6
|
+
import * as Duration from "effect/Duration";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import * as Layer from "effect/Layer";
|
|
9
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
10
|
+
import * as OtlpExporter from "effect/observability/OtlpExporter";
|
|
11
|
+
import * as OtlpMetrics from "effect/observability/OtlpMetrics";
|
|
12
|
+
import * as OtlpSerialization from "effect/observability/OtlpSerialization";
|
|
13
|
+
import * as OtlpTracer from "effect/observability/OtlpTracer";
|
|
14
|
+
import { packageVersion } from "./Package.js";
|
|
15
|
+
export const phoenixTracesUrl = "http://localhost:6006/v1/traces";
|
|
16
|
+
const truthy = (value) => /^(on|1|true|yes)$/iu.test(value?.trim() ?? "");
|
|
17
|
+
const falsy = (value) => /^(off|0|false|no)$/iu.test(value?.trim() ?? "");
|
|
18
|
+
/**
|
|
19
|
+
* Off unless asked. An OTLP endpoint in the environment wins and is used as
|
|
20
|
+
* the standard variables say; `LLM4TS_OTEL=on` alone means a local Phoenix,
|
|
21
|
+
* traces only. `OTEL_SDK_DISABLED` or `LLM4TS_OTEL=off` switch everything off.
|
|
22
|
+
*/
|
|
23
|
+
export const otelConfig = (environment) => {
|
|
24
|
+
if (truthy(environment.OTEL_SDK_DISABLED) || falsy(environment.LLM4TS_OTEL)) {
|
|
25
|
+
return { mode: "off" };
|
|
26
|
+
}
|
|
27
|
+
// Under the test runner the exporter's sleeps run on a TestClock nobody
|
|
28
|
+
// advances, so its final flush would wait forever: a developer with
|
|
29
|
+
// LLM4TS_OTEL=on in the shell must still get a green suite.
|
|
30
|
+
if (environment.VITEST !== undefined) {
|
|
31
|
+
return { mode: "off" };
|
|
32
|
+
}
|
|
33
|
+
const endpoint = environment.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim() ||
|
|
34
|
+
environment.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
|
|
35
|
+
if (endpoint !== undefined && endpoint.length > 0) {
|
|
36
|
+
return { mode: "env", endpoint };
|
|
37
|
+
}
|
|
38
|
+
return truthy(environment.LLM4TS_OTEL)
|
|
39
|
+
? { mode: "phoenix", tracesUrl: phoenixTracesUrl }
|
|
40
|
+
: { mode: "off" };
|
|
41
|
+
};
|
|
42
|
+
/** The run end may wait this long for the last batch; the event drain waits the same. */
|
|
43
|
+
export const flushTimeout = Duration.seconds(3);
|
|
44
|
+
/** This runner's version, for `service.version`. */
|
|
45
|
+
export const runnerVersion = packageVersion;
|
|
46
|
+
const resourceOf = (resource) => ({
|
|
47
|
+
serviceName: "llm4ts",
|
|
48
|
+
serviceVersion: resource.serviceVersion,
|
|
49
|
+
attributes: { "openinference.project.name": resource.project }
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* The exporter for `config`: nothing when off; traces only for Phoenix;
|
|
53
|
+
* traces and metrics from the standard variables for a generic endpoint.
|
|
54
|
+
* Logs are never exported. Export errors are the exporter's to swallow; the
|
|
55
|
+
* run never fails on them.
|
|
56
|
+
*
|
|
57
|
+
* Bodies are protobuf, OTLP/HTTP's default encoding and the only one Phoenix
|
|
58
|
+
* accepts: its traces endpoint answers 415 to `application/json`, and the
|
|
59
|
+
* exporter then drops every batch for a minute, at debug level.
|
|
60
|
+
*/
|
|
61
|
+
export const otelLayer = (config, resource,
|
|
62
|
+
/** What the standard variables are read from in env mode (the run's environment). */
|
|
63
|
+
environment = process.env,
|
|
64
|
+
/** The HTTP client the exporter posts with; fetch by default, a fake in tests. */
|
|
65
|
+
http = FetchHttpClient.layer) => {
|
|
66
|
+
switch (config.mode) {
|
|
67
|
+
case "off":
|
|
68
|
+
// The exporter's no-op flusher: nothing to export, nothing to flush.
|
|
69
|
+
return OtlpExporter.layerFlusher;
|
|
70
|
+
case "phoenix":
|
|
71
|
+
return OtlpTracer.layer({
|
|
72
|
+
url: config.tracesUrl,
|
|
73
|
+
resource: resourceOf(resource),
|
|
74
|
+
shutdownTimeout: flushTimeout
|
|
75
|
+
}).pipe(Layer.provide(OtlpSerialization.layerProtobuf), Layer.provide(http));
|
|
76
|
+
case "env":
|
|
77
|
+
// The standard variables are read from the run's own environment, with
|
|
78
|
+
// the two exporter defaults an endpoint implies; the process env is
|
|
79
|
+
// never written.
|
|
80
|
+
return Layer.mergeAll(OtlpTracer.layerFromConfig({ resource: resourceOf(resource) }), OtlpMetrics.layerFromConfig({ resource: resourceOf(resource) })).pipe(Layer.provide(OtlpSerialization.layerProtobuf), Layer.provide(http), Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnvRecord({
|
|
81
|
+
...environment,
|
|
82
|
+
...otelEnvironmentDefaults(config, environment)
|
|
83
|
+
}))));
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* `layerFromConfig` exports nothing unless `OTEL_TRACES_EXPORTER` names
|
|
88
|
+
* `otlp`; an endpoint in the environment is the operator's intent, so the
|
|
89
|
+
* layer reads its variables with that default (and the metrics one) added
|
|
90
|
+
* when they are unset.
|
|
91
|
+
*/
|
|
92
|
+
export const otelEnvironmentDefaults = (config, environment) => config.mode !== "env"
|
|
93
|
+
? {}
|
|
94
|
+
: {
|
|
95
|
+
...(environment.OTEL_TRACES_EXPORTER === undefined ? { OTEL_TRACES_EXPORTER: "otlp" } : {}),
|
|
96
|
+
...(environment.OTEL_METRICS_EXPORTER === undefined
|
|
97
|
+
? { OTEL_METRICS_EXPORTER: "otlp" }
|
|
98
|
+
: {})
|
|
99
|
+
};
|
|
100
|
+
export const otelSummary = (config) => {
|
|
101
|
+
switch (config.mode) {
|
|
102
|
+
case "off":
|
|
103
|
+
return undefined;
|
|
104
|
+
case "phoenix":
|
|
105
|
+
return `otel → Phoenix at ${config.tracesUrl} (traces)`;
|
|
106
|
+
case "env":
|
|
107
|
+
return `otel → ${config.endpoint} (traces + metrics, OTEL_* variables)`;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* The one warning a run prints when export is on and nothing answers at the
|
|
112
|
+
* endpoint (spec §9): the exporter itself only logs at debug level and drops
|
|
113
|
+
* the batch. Nothing when off or when the endpoint answered.
|
|
114
|
+
*/
|
|
115
|
+
export const otelWarning = (config, answers) => {
|
|
116
|
+
if (config.mode === "off" || answers !== false) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
const where = config.mode === "phoenix" ? config.tracesUrl : config.endpoint;
|
|
120
|
+
return (`⚠ otel: nothing answers at ${where} — spans will be dropped (the run is unaffected)` +
|
|
121
|
+
(config.mode === "phoenix"
|
|
122
|
+
? "; start Phoenix with `docker run -p 6006:6006 arizephoenix/phoenix:latest`"
|
|
123
|
+
: ""));
|
|
124
|
+
};
|
|
125
|
+
/** The doctor's line: `answers` is the probe's result, undefined when export is off. */
|
|
126
|
+
export const otelDoctorLine = (config, answers) => {
|
|
127
|
+
switch (config.mode) {
|
|
128
|
+
case "off":
|
|
129
|
+
return "otel: off (LLM4TS_OTEL=on for a local Phoenix, or OTEL_EXPORTER_OTLP_ENDPOINT)";
|
|
130
|
+
case "phoenix":
|
|
131
|
+
case "env": {
|
|
132
|
+
const where = config.mode === "phoenix" ? `Phoenix at ${config.tracesUrl}` : config.endpoint;
|
|
133
|
+
return `otel: ${where} — ${answers === true
|
|
134
|
+
? "endpoint answers"
|
|
135
|
+
: "endpoint does not answer (spans will be dropped, the run is unaffected)"}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Whether the endpoint takes what the exporter will send: an empty protobuf
|
|
141
|
+
* traces batch, answered 2xx. A refused connection, a 404 or a 415 (the
|
|
142
|
+
* answer Phoenix gives JSON) all count as "does not answer".
|
|
143
|
+
*/
|
|
144
|
+
export const probeOtelEndpoint = (config, post = (url, init) => fetch(url, init)) => config.mode === "off"
|
|
145
|
+
? Effect.succeed(undefined)
|
|
146
|
+
: Effect.tryPromise(() => post(config.mode === "phoenix" ? config.tracesUrl : tracesUrlOf(config.endpoint), {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: { "content-type": "application/x-protobuf" },
|
|
149
|
+
body: new Uint8Array(0),
|
|
150
|
+
signal: AbortSignal.timeout(1_500)
|
|
151
|
+
})).pipe(Effect.map((response) => response.ok), Effect.orElseSucceed(() => false));
|
|
152
|
+
/** The traces URL the standard variables imply: a bare endpoint gets `/v1/traces`. */
|
|
153
|
+
const tracesUrlOf = (endpoint) => /\/v1\/traces\/?$/u.test(endpoint) ? endpoint : `${endpoint.replace(/\/+$/u, "")}/v1/traces`;
|
|
154
|
+
//# sourceMappingURL=Otel.js.map
|
package/dist/Otel.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Otel.js","sourceRoot":"","sources":["../src/Otel.ts"],"names":[],"mappings":"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"}
|
package/dist/Package.d.ts
CHANGED
package/dist/Package.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Package.d.ts","sourceRoot":"","sources":["../src/Package.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Package.d.ts","sourceRoot":"","sources":["../src/Package.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,WAAW,mBAAmB,CAAA;AAC3C,eAAO,MAAM,cAAc,EAAE,MAAyB,CAAA"}
|
package/dist/Package.js
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
const Manifest = Schema.Struct({ version: Schema.String });
|
|
4
|
+
const manifest = Schema.decodeUnknownSync(Manifest)(createRequire(import.meta.url)("../package.json"));
|
|
1
5
|
export const packageName = "@llm4ts/runner";
|
|
6
|
+
export const packageVersion = manifest.version;
|
|
2
7
|
//# sourceMappingURL=Package.js.map
|
package/dist/Package.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Package.js","sourceRoot":"","sources":["../src/Package.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,gBAAgB,CAAA"}
|
|
1
|
+
{"version":3,"file":"Package.js","sourceRoot":"","sources":["../src/Package.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAA;AAE1D,MAAM,QAAQ,GAAG,MAAM,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CACjD,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAAC,CAClD,CAAA;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,gBAAgB,CAAA;AAC3C,MAAM,CAAC,MAAM,cAAc,GAAW,QAAQ,CAAC,OAAO,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llm4ts/runner",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.30.1",
|
|
4
4
|
"description": "Node runner, CLI, terminal, and MCP composition for llm4ts",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"./NodeProcessExecutor": "./dist/NodeProcessExecutor.js",
|
|
34
34
|
"./NodeTemporaryFiles": "./dist/NodeTemporaryFiles.js",
|
|
35
35
|
"./NodeWorkspace": "./dist/NodeWorkspace.js",
|
|
36
|
+
"./Otel": "./dist/Otel.js",
|
|
36
37
|
"./NodeRuntime": "./dist/NodeRuntime.js",
|
|
37
38
|
"./Packs": "./dist/Packs.js",
|
|
38
39
|
"./Profile": "./dist/Profile.js",
|
|
@@ -68,8 +69,8 @@
|
|
|
68
69
|
"typescript"
|
|
69
70
|
],
|
|
70
71
|
"dependencies": {
|
|
71
|
-
"@llm4ts/
|
|
72
|
-
"@llm4ts/
|
|
72
|
+
"@llm4ts/flow": "2.30.1",
|
|
73
|
+
"@llm4ts/core": "2.30.1"
|
|
73
74
|
},
|
|
74
75
|
"peerDependencies": {
|
|
75
76
|
"effect": "4.0.0"
|
package/src/Doctor.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
geminiBridgePort as bridgePort,
|
|
12
12
|
piModelsConfig as parsePiModels
|
|
13
13
|
} from "./PiModels.ts"
|
|
14
|
+
import { otelConfig, otelDoctorLine, probeOtelEndpoint, type OtelConfig } from "./Otel.ts"
|
|
14
15
|
|
|
15
16
|
const credentialKeys = [
|
|
16
17
|
"OPENAI_API_KEY",
|
|
@@ -220,65 +221,75 @@ export const makeDoctorProgram = (
|
|
|
220
221
|
nodePlainFileStore,
|
|
221
222
|
process.cwd(),
|
|
222
223
|
environment
|
|
223
|
-
)
|
|
224
|
-
)
|
|
225
|
-
Effect.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
lines.push(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const value = environment[key]
|
|
238
|
-
lines.push(` ${value === undefined || value.length === 0 ? "✖" : "✔"} ${key}`)
|
|
239
|
-
}
|
|
240
|
-
lines.push("")
|
|
241
|
-
lines.push("gates:")
|
|
242
|
-
lines.push(
|
|
243
|
-
...wrap(
|
|
244
|
-
`${node._tag === "Mismatch" || node._tag === "NoNode" ? "✖" : "✔"} ${node.summary}`,
|
|
245
|
-
78,
|
|
246
|
-
" "
|
|
247
|
-
)
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
// Connector prerequisites the environment must carry BEFORE a run: shown
|
|
251
|
-
// when the connector is selected or the machine has its CLI, because that
|
|
252
|
-
// is exactly when a missing one turns into a confusing mid-run failure.
|
|
253
|
-
const coder = selectedCoder(environment)
|
|
254
|
-
const geminiRelevant =
|
|
255
|
-
coder.startsWith("gemini") ||
|
|
256
|
-
Array.from(statuses.keys()).some(
|
|
257
|
-
(id) =>
|
|
258
|
-
id.value === ConnectorIds.GeminiCli.value || id.value === ConnectorIds.GeminiApi.value
|
|
224
|
+
),
|
|
225
|
+
/** Whether the OTLP endpoint answers (ADR 0026); the network probe by default. */
|
|
226
|
+
probeOtel: (config: OtelConfig) => Effect.Effect<boolean | undefined> = probeOtelEndpoint
|
|
227
|
+
): Effect.Effect<string> => {
|
|
228
|
+
const otel = otelConfig(environment)
|
|
229
|
+
return Effect.map(
|
|
230
|
+
Effect.all([registry.healthCheckAll, nodeCheck, probeOtel(otel)]),
|
|
231
|
+
([statuses, node, otelAnswers]) => {
|
|
232
|
+
const lines: Array<string> = []
|
|
233
|
+
lines.push("llm4ts doctor")
|
|
234
|
+
lines.push("")
|
|
235
|
+
lines.push("connectors:")
|
|
236
|
+
const sorted = Array.from(statuses.entries()).sort(([a], [b]) =>
|
|
237
|
+
a.value.localeCompare(b.value)
|
|
259
238
|
)
|
|
260
|
-
|
|
261
|
-
|
|
239
|
+
for (const [id, status] of sorted) {
|
|
240
|
+
lines.push(statusLine(id.value, status))
|
|
241
|
+
}
|
|
262
242
|
lines.push("")
|
|
263
|
-
lines.push("
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
243
|
+
lines.push("credentials:")
|
|
244
|
+
for (const key of credentialKeys) {
|
|
245
|
+
const value = environment[key]
|
|
246
|
+
lines.push(` ${value === undefined || value.length === 0 ? "✖" : "✔"} ${key}`)
|
|
247
|
+
}
|
|
248
|
+
lines.push("")
|
|
249
|
+
lines.push("gates:")
|
|
250
|
+
lines.push(
|
|
251
|
+
...wrap(
|
|
252
|
+
`${node._tag === "Mismatch" || node._tag === "NoNode" ? "✖" : "✔"} ${node.summary}`,
|
|
253
|
+
78,
|
|
254
|
+
" "
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
// Connector prerequisites the environment must carry BEFORE a run: shown
|
|
259
|
+
// when the connector is selected or the machine has its CLI, because that
|
|
260
|
+
// is exactly when a missing one turns into a confusing mid-run failure.
|
|
261
|
+
const coder = selectedCoder(environment)
|
|
262
|
+
const geminiRelevant =
|
|
263
|
+
coder.startsWith("gemini") ||
|
|
264
|
+
Array.from(statuses.keys()).some(
|
|
265
|
+
(id) =>
|
|
266
|
+
id.value === ConnectorIds.GeminiCli.value || id.value === ConnectorIds.GeminiApi.value
|
|
267
|
+
)
|
|
268
|
+
const bridge = geminiBridgePrerequisites(environment, readPiModelsJson())
|
|
269
|
+
if (geminiRelevant || bridge !== undefined) {
|
|
270
|
+
lines.push("")
|
|
271
|
+
lines.push("prerequisites:")
|
|
272
|
+
const pushReport = (label: string, report: PrerequisiteReport): void => {
|
|
273
|
+
lines.push(` ${report.satisfied ? "✔" : "?"} ${label}: ${report.summary}`)
|
|
274
|
+
if (report.hint !== undefined) {
|
|
275
|
+
lines.push(...wrap(report.hint, 78, " "))
|
|
276
|
+
}
|
|
277
|
+
for (const line of report.detail ?? []) {
|
|
278
|
+
lines.push(` ${line}`)
|
|
279
|
+
}
|
|
268
280
|
}
|
|
269
|
-
|
|
270
|
-
|
|
281
|
+
if (geminiRelevant) {
|
|
282
|
+
pushReport("gemini", geminiPrerequisites(environment))
|
|
283
|
+
}
|
|
284
|
+
if (bridge !== undefined) {
|
|
285
|
+
pushReport("pi-gemini-bridge", bridge)
|
|
271
286
|
}
|
|
272
287
|
}
|
|
273
|
-
if (geminiRelevant) {
|
|
274
|
-
pushReport("gemini", geminiPrerequisites(environment))
|
|
275
|
-
}
|
|
276
|
-
if (bridge !== undefined) {
|
|
277
|
-
pushReport("pi-gemini-bridge", bridge)
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
288
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
289
|
+
lines.push("")
|
|
290
|
+
lines.push(otelDoctorLine(otel, otelAnswers))
|
|
291
|
+
lines.push(`coder: ${coder}`)
|
|
292
|
+
return `${lines.join("\n")}\n`
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
}
|
package/src/FlowArgs.ts
CHANGED
|
@@ -34,6 +34,8 @@ environment:
|
|
|
34
34
|
LLM4TS_VERBOSITY terminal verbosity: quiet|normal|verbose
|
|
35
35
|
LLM4TS_UI tree: the full-screen agent tree (ADR 0022; q returns to the classic view)
|
|
36
36
|
LLM4TS_TRANSCRIPT on: record each seat's input and output under .llm4ts/transcripts/
|
|
37
|
+
LLM4TS_OTEL on: export spans over OTLP (a local Phoenix at localhost:6006 unless OTEL_* says otherwise)
|
|
38
|
+
LLM4TS_OTEL_CONTENT on|full: prompts, replies and tool I/O travel too (off by default)
|
|
37
39
|
OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY
|
|
38
40
|
credentials for API providers
|
|
39
41
|
|
package/src/FlowRunner.ts
CHANGED
|
@@ -58,7 +58,18 @@ import {
|
|
|
58
58
|
} from "@llm4ts/flow/FlowEvents"
|
|
59
59
|
import { FlowContext, type ContextOptions, type FlowContextShape } from "@llm4ts/flow/FlowContext"
|
|
60
60
|
import { makeFlowRecorder, type RunOutcome } from "@llm4ts/flow/FlowRecorder"
|
|
61
|
-
import { timedJudgment, timedSeat, withTimedRole } from "@llm4ts/flow/Timing"
|
|
61
|
+
import { timedJudgment, timedSeat, withTimedRole, type TimedSeatOptions } from "@llm4ts/flow/Timing"
|
|
62
|
+
import { attr, otelContent, withKindSpan } from "@llm4ts/flow/Spans"
|
|
63
|
+
import * as Tracer from "effect/Tracer"
|
|
64
|
+
import { basename } from "node:path"
|
|
65
|
+
import {
|
|
66
|
+
otelConfig,
|
|
67
|
+
otelWarning,
|
|
68
|
+
probeOtelEndpoint,
|
|
69
|
+
otelLayer,
|
|
70
|
+
otelSummary,
|
|
71
|
+
runnerVersion
|
|
72
|
+
} from "./Otel.ts"
|
|
62
73
|
import { transcriptSeat, type TranscriptSink } from "@llm4ts/flow/Transcript"
|
|
63
74
|
import { nodeTranscriptFiles, nodeTranscriptSink, transcriptsWanted } from "./Transcripts.ts"
|
|
64
75
|
import { idleAfterFrom } from "./AgentTree.ts"
|
|
@@ -362,6 +373,13 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
362
373
|
const runEnvironment = options.environment ?? process.env
|
|
363
374
|
const estimateOptions = estimatedUsageOptionsFromEnv(runEnvironment)
|
|
364
375
|
const meterUsage = options.estimateUsage ?? !switchedOff(runEnvironment.LLM4TS_ESTIMATE_USAGE)
|
|
376
|
+
// Every seat's spans (ADR 0026): content only when asked, an estimate on a
|
|
377
|
+
// call the backend reports no usage for, and who served it when known.
|
|
378
|
+
const seatOptions = (executor?: Effect.Effect<string | undefined>): TimedSeatOptions => ({
|
|
379
|
+
content: otelContent(runEnvironment),
|
|
380
|
+
...(meterUsage ? { estimate: estimateOptions } : {}),
|
|
381
|
+
...(executor === undefined ? {} : { executor })
|
|
382
|
+
})
|
|
365
383
|
const metered = (service: LlmServiceShape): Effect.Effect<LlmServiceShape> =>
|
|
366
384
|
meterUsage
|
|
367
385
|
? Effect.map(makeEstimatedUsageMeter(service, estimateOptions), (meter) => meter.service)
|
|
@@ -490,18 +508,26 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
490
508
|
): FlowContextShape =>
|
|
491
509
|
FlowContext.of({
|
|
492
510
|
// Every call is timed on the lane's events, under its role.
|
|
493
|
-
reasoning: recorded(
|
|
511
|
+
reasoning: recorded(
|
|
512
|
+
timedSeat(seats.reasoning, laneEvents, "reasoning", seatOptions()),
|
|
513
|
+
lane,
|
|
514
|
+
"reasoning"
|
|
515
|
+
),
|
|
494
516
|
coder: {
|
|
495
517
|
...seats.coder,
|
|
496
|
-
...recorded(timedSeat(seats.coder, laneEvents, "coder"), lane, "coder")
|
|
518
|
+
...recorded(timedSeat(seats.coder, laneEvents, "coder", seatOptions()), lane, "coder")
|
|
497
519
|
},
|
|
498
520
|
judgment: timedJudgment(seats.judgment, laneEvents),
|
|
499
|
-
judge: recorded(
|
|
521
|
+
judge: recorded(
|
|
522
|
+
timedSeat(seats.judge, laneEvents, "judgment", seatOptions()),
|
|
523
|
+
lane,
|
|
524
|
+
"judgment"
|
|
525
|
+
),
|
|
500
526
|
git: makeGitTool(dependencies.process, workDir, laneEvents),
|
|
501
527
|
hosting: makeGitHubTool(dependencies.process, workDir, laneEvents),
|
|
502
528
|
events: laneEvents,
|
|
503
529
|
reviewers: seats.reviewers.map((reviewer) =>
|
|
504
|
-
recorded(timedSeat(reviewer, laneEvents, "reviewer"), lane, "reviewer")
|
|
530
|
+
recorded(timedSeat(reviewer, laneEvents, "reviewer", seatOptions()), lane, "reviewer")
|
|
505
531
|
),
|
|
506
532
|
coderCapabilities: seats.coder.capabilities,
|
|
507
533
|
userPrompt: options.userPrompt,
|
|
@@ -671,7 +697,8 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
671
697
|
label
|
|
672
698
|
}),
|
|
673
699
|
laneEvents,
|
|
674
|
-
role
|
|
700
|
+
role,
|
|
701
|
+
seatOptions()
|
|
675
702
|
),
|
|
676
703
|
lane,
|
|
677
704
|
role
|
|
@@ -709,7 +736,7 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
709
736
|
return FlowContext.of({
|
|
710
737
|
reasoning: view.forRole(rebind ? "planner" : "reviewer"),
|
|
711
738
|
coder: recorded(
|
|
712
|
-
timedSeat(held.service, laneEvents, "coder"),
|
|
739
|
+
timedSeat(held.service, laneEvents, "coder", seatOptions(held.executor)),
|
|
713
740
|
lane,
|
|
714
741
|
"coder",
|
|
715
742
|
held.executor
|
|
@@ -811,6 +838,9 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
811
838
|
const environment = options.environment ?? process.env
|
|
812
839
|
const verbosity = options.verbosity ?? "Normal"
|
|
813
840
|
const persistRun = options.persistRun ?? true
|
|
841
|
+
// OpenTelemetry export (ADR 0026): decided from the environment, built once
|
|
842
|
+
// per run, flushed when the run's scope closes.
|
|
843
|
+
const otel = otelConfig(environment)
|
|
814
844
|
const tracePath =
|
|
815
845
|
options.tracePath ??
|
|
816
846
|
(persistRun ? join(options.workDir, ".llm4ts", `trace-${startedAt}.jsonl`) : undefined)
|
|
@@ -867,6 +897,16 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
867
897
|
)
|
|
868
898
|
)
|
|
869
899
|
}
|
|
900
|
+
const where = otelSummary(otel)
|
|
901
|
+
if (where !== undefined) {
|
|
902
|
+
yield* surface.log(palette.info(where))
|
|
903
|
+
// One warning, at the start, when nothing listens: the exporter itself
|
|
904
|
+
// drops batches quietly. The probe is bounded (1.5s) and runs once.
|
|
905
|
+
const warning = otelWarning(otel, yield* probeOtelEndpoint(otel))
|
|
906
|
+
if (warning !== undefined) {
|
|
907
|
+
yield* surface.log(palette.warn(warning))
|
|
908
|
+
}
|
|
909
|
+
}
|
|
870
910
|
}
|
|
871
911
|
const recorder =
|
|
872
912
|
tracePath === undefined
|
|
@@ -925,7 +965,32 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
925
965
|
tracePath === undefined
|
|
926
966
|
? bundle.context
|
|
927
967
|
: { ...bundle.context, trace: { runId: bundle.runId, path: tracePath } }
|
|
928
|
-
|
|
968
|
+
const runAttributes: Record<string, unknown> = {
|
|
969
|
+
[attr.run]: bundle.runId,
|
|
970
|
+
[attr.session]: bundle.runId,
|
|
971
|
+
...(environment.LLM4TS_FLOW === undefined ? {} : { [attr.flow]: environment.LLM4TS_FLOW })
|
|
972
|
+
}
|
|
973
|
+
// The run is a CHAIN span every other span descends from or links to; its
|
|
974
|
+
// attributes ride on all of them. Effect's own function spans stay
|
|
975
|
+
// unsampled so an exported trace shows the flow, not the call stack.
|
|
976
|
+
const traced = withKindSpan(
|
|
977
|
+
"run",
|
|
978
|
+
{ kind: "CHAIN", attributes: runAttributes },
|
|
979
|
+
Object.entries(runAttributes).reduce(
|
|
980
|
+
(acc, [key, value]) => Effect.annotateSpans(key, value)(acc),
|
|
981
|
+
body(context)
|
|
982
|
+
)
|
|
983
|
+
).pipe(
|
|
984
|
+
Effect.provideService(Tracer.MinimumTraceLevel, otel.mode === "off" ? "All" : "None"),
|
|
985
|
+
Effect.provide(
|
|
986
|
+
otelLayer(
|
|
987
|
+
otel,
|
|
988
|
+
{ serviceVersion: runnerVersion, project: basename(options.workDir) },
|
|
989
|
+
environment
|
|
990
|
+
)
|
|
991
|
+
)
|
|
992
|
+
)
|
|
993
|
+
return yield* traced.pipe(
|
|
929
994
|
Effect.provideService(FlowContext, context),
|
|
930
995
|
Effect.andThen((value) => Effect.as(enforceBudget, value)),
|
|
931
996
|
Effect.ensuring(
|