@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.
- package/README.md +1 -1
- package/dist/AgentTree.d.ts +2 -3
- package/dist/AgentTree.d.ts.map +1 -1
- package/dist/AgentTree.js +8 -20
- package/dist/AgentTree.js.map +1 -1
- package/dist/AgentTreeSurface.d.ts +1 -1
- package/dist/AgentTreeSurface.d.ts.map +1 -1
- package/dist/AgentTreeSurface.js +2 -5
- package/dist/AgentTreeSurface.js.map +1 -1
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +2 -1
- package/dist/Cli.js.map +1 -1
- package/dist/Connectors.d.ts +27 -2
- package/dist/Connectors.d.ts.map +1 -1
- package/dist/Connectors.js +76 -89
- package/dist/Connectors.js.map +1 -1
- package/dist/Doctor.d.ts.map +1 -1
- package/dist/Doctor.js +8 -11
- package/dist/Doctor.js.map +1 -1
- package/dist/EventPresentation.d.ts +35 -0
- package/dist/EventPresentation.d.ts.map +1 -0
- package/dist/EventPresentation.js +132 -0
- package/dist/EventPresentation.js.map +1 -0
- package/dist/ExecutorRoster.d.ts.map +1 -1
- package/dist/ExecutorRoster.js +4 -6
- package/dist/ExecutorRoster.js.map +1 -1
- package/dist/FlowArgs.d.ts +1 -1
- package/dist/FlowArgs.d.ts.map +1 -1
- package/dist/FlowArgs.js +4 -2
- package/dist/FlowArgs.js.map +1 -1
- package/dist/FlowRunner.d.ts +13 -4
- package/dist/FlowRunner.d.ts.map +1 -1
- package/dist/FlowRunner.js +50 -39
- package/dist/FlowRunner.js.map +1 -1
- package/dist/NodeGeminiCliExecutor.d.ts.map +1 -1
- package/dist/NodeGeminiCliExecutor.js +7 -4
- package/dist/NodeGeminiCliExecutor.js.map +1 -1
- package/dist/NodeGeminiModelBridge.d.ts.map +1 -1
- package/dist/NodeGeminiModelBridge.js +4 -1
- package/dist/NodeGeminiModelBridge.js.map +1 -1
- package/dist/NodeHttpClient.d.ts.map +1 -1
- package/dist/NodeHttpClient.js +1 -0
- package/dist/NodeHttpClient.js.map +1 -1
- package/dist/Otel.d.ts.map +1 -1
- package/dist/Otel.js +5 -5
- package/dist/Otel.js.map +1 -1
- package/dist/Package.d.ts.map +1 -1
- package/dist/Package.js +1 -0
- package/dist/Package.js.map +1 -1
- package/dist/Packs.d.ts.map +1 -1
- package/dist/Packs.js +2 -1
- package/dist/Packs.js.map +1 -1
- package/dist/PiModels.d.ts.map +1 -1
- package/dist/PiModels.js +6 -9
- package/dist/PiModels.js.map +1 -1
- package/dist/Terminal.d.ts +13 -11
- package/dist/Terminal.d.ts.map +1 -1
- package/dist/Terminal.js +24 -80
- package/dist/Terminal.js.map +1 -1
- package/dist/Tools.d.ts.map +1 -1
- package/dist/Tools.js +2 -1
- package/dist/Tools.js.map +1 -1
- package/dist/Transcripts.d.ts +1 -1
- package/dist/Transcripts.d.ts.map +1 -1
- package/dist/Transcripts.js +3 -2
- package/dist/Transcripts.js.map +1 -1
- package/dist/WindowsCommand.d.ts.map +1 -1
- package/dist/WindowsCommand.js +1 -0
- package/dist/WindowsCommand.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/package.json +9 -21
- package/src/AgentTree.ts +16 -25
- package/src/AgentTreeSurface.ts +6 -11
- package/src/Cli.ts +2 -1
- package/src/Connectors.ts +131 -107
- package/src/Doctor.ts +10 -12
- package/src/EventPresentation.ts +167 -0
- package/src/ExecutorRoster.ts +4 -6
- package/src/FlowArgs.ts +7 -2
- package/src/FlowRunner.ts +66 -60
- package/src/NodeGeminiCliExecutor.ts +8 -4
- package/src/NodeGeminiModelBridge.ts +5 -1
- package/src/NodeHttpClient.ts +1 -0
- package/src/Otel.ts +5 -6
- package/src/Package.ts +1 -0
- package/src/Packs.ts +2 -1
- package/src/PiModels.ts +6 -10
- package/src/Terminal.ts +104 -165
- package/src/Tools.ts +2 -1
- package/src/Transcripts.ts +3 -2
- package/src/WindowsCommand.ts +1 -0
- package/src/index.ts +3 -2
- package/dist/Costs.d.ts +0 -52
- package/dist/Costs.d.ts.map +0 -1
- package/dist/Costs.js +0 -95
- package/dist/Costs.js.map +0 -1
- package/dist/McpStdio.d.ts +0 -7
- package/dist/McpStdio.d.ts.map +0 -1
- package/dist/McpStdio.js +0 -19
- package/dist/McpStdio.js.map +0 -1
- package/dist/Profile.d.ts +0 -309
- package/dist/Profile.d.ts.map +0 -1
- package/dist/Profile.js +0 -967
- package/dist/Profile.js.map +0 -1
- package/dist/ReviewFingerprint.d.ts +0 -2
- package/dist/ReviewFingerprint.d.ts.map +0 -1
- package/dist/ReviewFingerprint.js +0 -13
- package/dist/ReviewFingerprint.js.map +0 -1
- package/dist/RunReaders.d.ts +0 -69
- package/dist/RunReaders.d.ts.map +0 -1
- package/dist/RunReaders.js +0 -283
- package/dist/RunReaders.js.map +0 -1
- package/dist/StoryReaders.d.ts +0 -16
- package/dist/StoryReaders.d.ts.map +0 -1
- package/dist/StoryReaders.js +0 -107
- package/dist/StoryReaders.js.map +0 -1
- package/dist/Watch.d.ts +0 -68
- package/dist/Watch.d.ts.map +0 -1
- package/dist/Watch.js +0 -261
- package/dist/Watch.js.map +0 -1
- package/src/Costs.ts +0 -148
- package/src/McpStdio.ts +0 -41
- package/src/Profile.ts +0 -1213
- package/src/ReviewFingerprint.ts +0 -13
- package/src/RunReaders.ts +0 -419
- package/src/StoryReaders.ts +0 -157
- package/src/Watch.ts +0 -446
package/src/Connectors.ts
CHANGED
|
@@ -4,10 +4,22 @@ import * as Redacted from "effect/Redacted"
|
|
|
4
4
|
import {
|
|
5
5
|
ApiConnectorConfig,
|
|
6
6
|
CliConnectorConfig,
|
|
7
|
-
type ConnectorConfig
|
|
7
|
+
type ConnectorConfig,
|
|
8
|
+
type ConnectorConfigOf
|
|
8
9
|
} from "@llm4ts/core/ConnectorConfig"
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
ConnectorIds,
|
|
12
|
+
apiProviderConnector,
|
|
13
|
+
coderConnector,
|
|
14
|
+
coderNames,
|
|
15
|
+
connectorDefaultBaseUrl,
|
|
16
|
+
connectorRow,
|
|
17
|
+
connectorTable,
|
|
18
|
+
offeredApiProviderNames,
|
|
19
|
+
type ConnectorName
|
|
20
|
+
} from "@llm4ts/core/Models"
|
|
10
21
|
import { runStateDeniedPaths } from "@llm4ts/flow/RunState"
|
|
22
|
+
import { settingText, settingValue } from "@llm4ts/flow/Settings"
|
|
11
23
|
import { ScriptUsage } from "./FlowArgs.ts"
|
|
12
24
|
|
|
13
25
|
export const claude = CliConnectorConfig.make({
|
|
@@ -53,12 +65,12 @@ export const opencode = CliConnectorConfig.make({
|
|
|
53
65
|
|
|
54
66
|
export const lmStudio = ApiConnectorConfig.make({
|
|
55
67
|
connectorId: ConnectorIds.LmStudio,
|
|
56
|
-
baseUrl:
|
|
68
|
+
baseUrl: connectorTable.LmStudio.baseUrl
|
|
57
69
|
})
|
|
58
70
|
|
|
59
71
|
export const mlxLm = ApiConnectorConfig.make({
|
|
60
72
|
connectorId: ConnectorIds.MlxLm,
|
|
61
|
-
baseUrl:
|
|
73
|
+
baseUrl: connectorTable.MlxLm.baseUrl
|
|
62
74
|
})
|
|
63
75
|
|
|
64
76
|
export const openAI = ApiConnectorConfig.make({
|
|
@@ -82,6 +94,26 @@ export const mock = ApiConnectorConfig.make({
|
|
|
82
94
|
model: "mock"
|
|
83
95
|
})
|
|
84
96
|
|
|
97
|
+
// A connector without a preset here does not compile.
|
|
98
|
+
const presets: { readonly [K in ConnectorName]: ConnectorConfigOf<K> } = {
|
|
99
|
+
OpenAI: openAI,
|
|
100
|
+
Anthropic: anthropic,
|
|
101
|
+
GeminiApi: geminiApi,
|
|
102
|
+
LmStudio: lmStudio,
|
|
103
|
+
Ollama: ollama,
|
|
104
|
+
MlxLm: mlxLm,
|
|
105
|
+
ClaudeCli: claude,
|
|
106
|
+
GeminiCli: gemini,
|
|
107
|
+
OpenCode: opencode,
|
|
108
|
+
Codex: codex,
|
|
109
|
+
Copilot: CliConnectorConfig.make({ connectorId: ConnectorIds.Copilot }),
|
|
110
|
+
Pi: pi,
|
|
111
|
+
AntigravityCli: antigravity,
|
|
112
|
+
Grok: grok,
|
|
113
|
+
Cursor: cursor,
|
|
114
|
+
Mock: mock
|
|
115
|
+
}
|
|
116
|
+
|
|
85
117
|
export function withModel(config: CliConnectorConfig, model: string): CliConnectorConfig
|
|
86
118
|
export function withModel(config: ApiConnectorConfig, model: string): ApiConnectorConfig
|
|
87
119
|
export function withModel(
|
|
@@ -134,16 +166,12 @@ const environmentApiKey = (
|
|
|
134
166
|
config: ApiConnectorConfig,
|
|
135
167
|
environment: Readonly<Record<string, string | undefined>>
|
|
136
168
|
): string | undefined => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return environment.GEMINI_API_KEY ?? environment.GOOGLE_API_KEY
|
|
144
|
-
default:
|
|
145
|
-
return undefined
|
|
146
|
-
}
|
|
169
|
+
const row = connectorRow(config.connectorId)
|
|
170
|
+
return row?.kind === "Api"
|
|
171
|
+
? row.credentialVariables
|
|
172
|
+
.map((variable) => environment[variable])
|
|
173
|
+
.find((value) => value !== undefined)
|
|
174
|
+
: undefined
|
|
147
175
|
}
|
|
148
176
|
|
|
149
177
|
export const enrichApiConnector = (
|
|
@@ -163,9 +191,6 @@ export const enrichApiConnector = (
|
|
|
163
191
|
})
|
|
164
192
|
}
|
|
165
193
|
|
|
166
|
-
const switchedOn = (value: string | undefined): boolean =>
|
|
167
|
-
value !== undefined && ["1", "true", "on", "yes"].includes(value.trim().toLowerCase())
|
|
168
|
-
|
|
169
194
|
export const prepareConnector = (
|
|
170
195
|
config: ConnectorConfig,
|
|
171
196
|
workDir: string,
|
|
@@ -179,7 +204,7 @@ export const prepareConnector = (
|
|
|
179
204
|
workingDir: workDir,
|
|
180
205
|
// `LLM4TS_ISOLATED=1` asks every CLI seat of the run to leave the
|
|
181
206
|
// repository's own harness material unloaded (ADR 0029).
|
|
182
|
-
...(
|
|
207
|
+
...(settingValue("LLM4TS_ISOLATED", environment) ? { isolated: true } : {}),
|
|
183
208
|
// Every CLI seat keeps out of the run's own state unless the seat
|
|
184
209
|
// (or the run) says otherwise; the connector maps it to a deny
|
|
185
210
|
// rule where its harness has one (`capabilities.pathDeny`).
|
|
@@ -188,110 +213,114 @@ export const prepareConnector = (
|
|
|
188
213
|
|
|
189
214
|
/** The API provider a name stands for (`LLM4TS_PROVIDER`, a roster executor's `harness`). */
|
|
190
215
|
export const apiPresetFor = (name: string): ApiConnectorConfig | undefined => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return mock
|
|
194
|
-
case "openai":
|
|
195
|
-
return openAI
|
|
196
|
-
case "anthropic":
|
|
197
|
-
return anthropic
|
|
198
|
-
case "gemini":
|
|
199
|
-
case "gemini-api":
|
|
200
|
-
return geminiApi
|
|
201
|
-
case "lm-studio":
|
|
202
|
-
case "lmstudio":
|
|
203
|
-
return lmStudio
|
|
204
|
-
case "ollama":
|
|
205
|
-
return ollama
|
|
206
|
-
case "mlx-lm":
|
|
207
|
-
case "mlxlm":
|
|
208
|
-
case "mlx":
|
|
209
|
-
return mlxLm
|
|
210
|
-
default:
|
|
211
|
-
return undefined
|
|
212
|
-
}
|
|
216
|
+
const connector = apiProviderConnector(name.trim().toLowerCase())
|
|
217
|
+
return connector === undefined ? undefined : presets[connector]
|
|
213
218
|
}
|
|
214
219
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
environment: Readonly<Record<string, string | undefined>> = process.env
|
|
220
|
+
const apiConnectorNamed = Effect.fnUntraced(function* (
|
|
221
|
+
named: string,
|
|
222
|
+
model: string | undefined
|
|
219
223
|
): Effect.fn.Return<ApiConnectorConfig, ScriptUsage> {
|
|
220
|
-
const provider =
|
|
224
|
+
const provider = named.toLowerCase()
|
|
221
225
|
const preset = apiPresetFor(provider)
|
|
222
226
|
if (preset === undefined) {
|
|
223
227
|
return yield* ScriptUsage.make({
|
|
224
|
-
message:
|
|
225
|
-
`unknown LLM4TS_PROVIDER '${provider}'; expected ` +
|
|
226
|
-
"mock|openai|anthropic|gemini|lm-studio|ollama|mlx-lm"
|
|
228
|
+
message: `unknown LLM4TS_PROVIDER '${provider}'; expected ${offeredApiProviderNames.join("|")}`
|
|
227
229
|
})
|
|
228
230
|
}
|
|
229
|
-
|
|
230
|
-
if (provider !== "mock" && (model === undefined || model.length === 0)) {
|
|
231
|
+
if (provider !== "mock" && model === undefined) {
|
|
231
232
|
return yield* ScriptUsage.make({
|
|
232
233
|
message: `LLM4TS_MODEL is required for provider '${provider}' (e.g. LLM4TS_MODEL=gpt-4o-mini)`
|
|
233
234
|
})
|
|
234
235
|
}
|
|
235
|
-
return model === undefined
|
|
236
|
+
return model === undefined ? preset : withModel(preset, model)
|
|
236
237
|
})
|
|
237
238
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
[gemini, ["gemini"]],
|
|
247
|
-
[pi, ["pi"]],
|
|
248
|
-
[antigravity, ["agy", "antigravity"]],
|
|
249
|
-
[grok, ["grok"]],
|
|
250
|
-
[cursor, ["cursor"]],
|
|
251
|
-
[opencode, ["opencode"]]
|
|
252
|
-
]
|
|
253
|
-
|
|
254
|
-
const coders = new Map<string, CliConnectorConfig>(
|
|
255
|
-
coderAliases.flatMap(([preset, aliases]) =>
|
|
256
|
-
[...aliases, preset.connectorId.value].map((name) => [name, preset] as const)
|
|
239
|
+
export const apiConnectorFromEnvironment = Effect.fn(
|
|
240
|
+
"@llm4ts/runner/Connectors.apiConnectorFromEnvironment"
|
|
241
|
+
)(function* (
|
|
242
|
+
environment: Readonly<Record<string, string | undefined>> = process.env
|
|
243
|
+
): Effect.fn.Return<ApiConnectorConfig, ScriptUsage> {
|
|
244
|
+
return yield* apiConnectorNamed(
|
|
245
|
+
settingText("LLM4TS_PROVIDER", environment) ?? "mock",
|
|
246
|
+
settingValue("LLM4TS_MODEL", environment)
|
|
257
247
|
)
|
|
258
|
-
)
|
|
248
|
+
})
|
|
259
249
|
|
|
260
|
-
export const coderIds: ReadonlyArray<string> = Object.freeze([...
|
|
250
|
+
export const coderIds: ReadonlyArray<string> = Object.freeze([...coderNames].sort())
|
|
261
251
|
|
|
262
|
-
export const coderFor = (name: string): CliConnectorConfig | undefined =>
|
|
263
|
-
|
|
252
|
+
export const coderFor = (name: string): CliConnectorConfig | undefined => {
|
|
253
|
+
const connector = coderConnector(name.trim().toLowerCase())
|
|
254
|
+
return connector === undefined ? undefined : presets[connector]
|
|
255
|
+
}
|
|
264
256
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
): CliConnectorConfig => {
|
|
271
|
-
const requested = (env.LLM4TS_CODER ?? "").trim()
|
|
272
|
-
return (requested.length === 0 ? claude : coderFor(requested)) ?? claude
|
|
257
|
+
export interface SeatDefaults {
|
|
258
|
+
/** The coder when `LLM4TS_CODER` is unset; claude when omitted. */
|
|
259
|
+
readonly coder?: CliConnectorConfig
|
|
260
|
+
/** The reasoner when `LLM4TS_REASONER` is unset; the coder seat when omitted. */
|
|
261
|
+
readonly reasoner?: CliConnectorConfig
|
|
273
262
|
}
|
|
274
263
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
264
|
+
export interface Seats {
|
|
265
|
+
readonly coder: CliConnectorConfig
|
|
266
|
+
readonly reasoning: CliConnectorConfig
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
type Environment = Readonly<Record<string, string | undefined>>
|
|
270
|
+
|
|
271
|
+
const namedSeat = (
|
|
272
|
+
variable: "LLM4TS_CODER" | "LLM4TS_REASONER",
|
|
273
|
+
environment: Environment
|
|
274
|
+
): Effect.Effect<CliConnectorConfig | undefined, ScriptUsage> => {
|
|
275
|
+
const requested = settingText(variable, environment)
|
|
276
|
+
if (requested === undefined) {
|
|
277
|
+
return Effect.succeed(undefined)
|
|
278
|
+
}
|
|
279
|
+
const preset = coderFor(requested)
|
|
280
|
+
return preset === undefined
|
|
281
|
+
? Effect.fail(
|
|
282
|
+
ScriptUsage.make({
|
|
283
|
+
message: `unknown ${variable} '${requested}'; expected ${coderIds.join("|")}`
|
|
290
284
|
})
|
|
291
|
-
|
|
285
|
+
)
|
|
286
|
+
: Effect.succeed(preset)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The coder and reasoning seats of a flow that runs both, from `LLM4TS_CODER`
|
|
291
|
+
* and `LLM4TS_REASONER`. An unknown name is a usage error naming the accepted
|
|
292
|
+
* names, never another agent. A flow with one seat reads that one alone
|
|
293
|
+
* (`coderFromEnvironment`, `reasonerFromEnvironment`).
|
|
294
|
+
*/
|
|
295
|
+
export const seatsFromEnvironment = Effect.fn("@llm4ts/runner/Connectors.seatsFromEnvironment")(
|
|
296
|
+
function* (
|
|
297
|
+
defaults: SeatDefaults = {},
|
|
298
|
+
environment: Environment = process.env
|
|
299
|
+
): Effect.fn.Return<Seats, ScriptUsage> {
|
|
300
|
+
const coder = (yield* namedSeat("LLM4TS_CODER", environment)) ?? defaults.coder ?? claude
|
|
301
|
+
const reasoner = yield* namedSeat("LLM4TS_REASONER", environment)
|
|
302
|
+
return { coder, reasoning: reasoner ?? defaults.reasoner ?? coder }
|
|
292
303
|
}
|
|
293
304
|
)
|
|
294
305
|
|
|
306
|
+
/**
|
|
307
|
+
* `LLM4TS_CODER` alone, for a flow that runs no separate reasoning seat. A
|
|
308
|
+
* flow answers only for the seats it uses: a bad `LLM4TS_REASONER` is not
|
|
309
|
+
* this flow's to refuse.
|
|
310
|
+
*/
|
|
311
|
+
export const coderFromEnvironment = (
|
|
312
|
+
environment: Environment = process.env,
|
|
313
|
+
fallback: CliConnectorConfig = claude
|
|
314
|
+
): Effect.Effect<CliConnectorConfig, ScriptUsage> =>
|
|
315
|
+
Effect.map(namedSeat("LLM4TS_CODER", environment), (coder) => coder ?? fallback)
|
|
316
|
+
|
|
317
|
+
/** `LLM4TS_REASONER` alone, for a flow that edits nothing. */
|
|
318
|
+
export const reasonerFromEnvironment = (
|
|
319
|
+
environment: Environment = process.env,
|
|
320
|
+
fallback: CliConnectorConfig = claude
|
|
321
|
+
): Effect.Effect<CliConnectorConfig, ScriptUsage> =>
|
|
322
|
+
Effect.map(namedSeat("LLM4TS_REASONER", environment), (reasoner) => reasoner ?? fallback)
|
|
323
|
+
|
|
295
324
|
/**
|
|
296
325
|
* The judgment seat from the environment (ADR 0017): `LLM4TS_JUDGMENT_PROVIDER`
|
|
297
326
|
* names an API provider the way `LLM4TS_PROVIDER` does, `LLM4TS_JUDGMENT_MODEL`
|
|
@@ -302,13 +331,8 @@ export const judgmentConnectorFromEnvironment = Effect.fn(
|
|
|
302
331
|
)(function* (
|
|
303
332
|
environment: Readonly<Record<string, string | undefined>> = process.env
|
|
304
333
|
): Effect.fn.Return<ApiConnectorConfig | undefined, ScriptUsage> {
|
|
305
|
-
const provider = environment
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
return yield* apiConnectorFromEnvironment({
|
|
310
|
-
...environment,
|
|
311
|
-
LLM4TS_PROVIDER: provider,
|
|
312
|
-
LLM4TS_MODEL: environment.LLM4TS_JUDGMENT_MODEL
|
|
313
|
-
})
|
|
334
|
+
const provider = settingText("LLM4TS_JUDGMENT_PROVIDER", environment)
|
|
335
|
+
return provider === undefined
|
|
336
|
+
? undefined
|
|
337
|
+
: yield* apiConnectorNamed(provider, settingValue("LLM4TS_JUDGMENT_MODEL", environment))
|
|
314
338
|
})
|
package/src/Doctor.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect"
|
|
2
2
|
import type { ConnectorKind, HealthStatus } from "@llm4ts/core/Models"
|
|
3
|
-
import { ConnectorIds } from "@llm4ts/core/Models"
|
|
3
|
+
import { ConnectorIds, credentialVariables } from "@llm4ts/core/Models"
|
|
4
4
|
import type { ConnectorRegistryShape } from "@llm4ts/core/ConnectorRegistry"
|
|
5
5
|
import { nodePreflightReport, type NodePreflightReport } from "@llm4ts/flow/NodePreflight"
|
|
6
|
+
import { settingText, settingValue } from "@llm4ts/flow/Settings"
|
|
6
7
|
import { nodeFlowRunnerDependencies } from "./FlowRunner.ts"
|
|
7
8
|
import { nodePlainFileStore } from "./NodePlainFileStore.ts"
|
|
8
9
|
import { nodeProcessExecutor } from "./NodeProcessExecutor.ts"
|
|
@@ -15,13 +16,6 @@ import {
|
|
|
15
16
|
import { systemAdminPolicies, systemPolicyProblem } from "./NodeGeminiModelBridge.ts"
|
|
16
17
|
import { otelConfig, otelDoctorLine, probeOtelEndpoint, type OtelConfig } from "./Otel.ts"
|
|
17
18
|
|
|
18
|
-
const credentialKeys = [
|
|
19
|
-
"OPENAI_API_KEY",
|
|
20
|
-
"ANTHROPIC_API_KEY",
|
|
21
|
-
"GEMINI_API_KEY",
|
|
22
|
-
"GOOGLE_API_KEY"
|
|
23
|
-
] as const
|
|
24
|
-
|
|
25
19
|
/**
|
|
26
20
|
* How far a connector's health was verified: `"auth"` means the CLI's own
|
|
27
21
|
* login check answered (`authStatus` is its word); `"version"` means only
|
|
@@ -133,7 +127,7 @@ const wrap = (text: string, width: number, indent: string): ReadonlyArray<string
|
|
|
133
127
|
}
|
|
134
128
|
|
|
135
129
|
const selectedCoder = (environment: Readonly<Record<string, string | undefined>>): string =>
|
|
136
|
-
|
|
130
|
+
settingText("LLM4TS_CODER", environment) ?? "claude (default)"
|
|
137
131
|
|
|
138
132
|
// Re-exported so `@llm4ts/runner/Doctor` stays the one place callers look
|
|
139
133
|
// for pi-config questions, while the parsing itself has no dependency on
|
|
@@ -161,7 +155,7 @@ export const geminiBridgePrerequisites = (
|
|
|
161
155
|
environment: Readonly<Record<string, string | undefined>>,
|
|
162
156
|
modelsJson: string | undefined
|
|
163
157
|
): PrerequisiteReport | undefined => {
|
|
164
|
-
if (!
|
|
158
|
+
if (!settingValue("LLM4TS_GEMINI_BRIDGE", environment)) {
|
|
165
159
|
return undefined
|
|
166
160
|
}
|
|
167
161
|
const port = bridgePort(environment)
|
|
@@ -254,7 +248,11 @@ export const geminiModelModePrerequisites = (
|
|
|
254
248
|
systemPolicies: ReadonlyArray<string>
|
|
255
249
|
): PrerequisiteReport | undefined => {
|
|
256
250
|
const mode = geminiBridgeMode(environment)
|
|
257
|
-
if (
|
|
251
|
+
if (
|
|
252
|
+
!settingValue("LLM4TS_GEMINI_BRIDGE", environment) ||
|
|
253
|
+
mode._tag !== "Mode" ||
|
|
254
|
+
mode.mode !== "model"
|
|
255
|
+
) {
|
|
258
256
|
return undefined
|
|
259
257
|
}
|
|
260
258
|
const detail = [
|
|
@@ -324,7 +322,7 @@ export const collectDoctorReport = (
|
|
|
324
322
|
const lines: Array<string> = []
|
|
325
323
|
lines.push("")
|
|
326
324
|
lines.push("credentials:")
|
|
327
|
-
for (const key of
|
|
325
|
+
for (const key of credentialVariables) {
|
|
328
326
|
const value = environment[key]
|
|
329
327
|
lines.push(` ${value === undefined || value.length === 0 ? "✖" : "✔"} ${key}`)
|
|
330
328
|
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema"
|
|
2
|
+
import type { FlowEvent, FlowEventByTag, FlowEventTag } from "@llm4ts/flow/FlowEvents"
|
|
3
|
+
|
|
4
|
+
export const Verbosity = Schema.Literals(["Quiet", "Normal", "Verbose", "Debug"])
|
|
5
|
+
export type Verbosity = typeof Verbosity.Type
|
|
6
|
+
|
|
7
|
+
const rank: Readonly<Record<Verbosity, number>> = { Quiet: 0, Normal: 1, Verbose: 2, Debug: 3 }
|
|
8
|
+
|
|
9
|
+
/** Whether `verbosity` is `least` or louder. */
|
|
10
|
+
export const atLeast = (verbosity: Verbosity, least: Verbosity): boolean =>
|
|
11
|
+
rank[verbosity] >= rank[least]
|
|
12
|
+
|
|
13
|
+
/** A line of `llm4ts watch` and of the MCP run digests, and the lane it is filed under. */
|
|
14
|
+
export interface DigestLine {
|
|
15
|
+
readonly text: string
|
|
16
|
+
readonly lane?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** How one event shows: on the terminal, in a profile's gaps, in a digest. */
|
|
20
|
+
export interface Presentation<E> {
|
|
21
|
+
/**
|
|
22
|
+
* Whether the terminal prints it as a line of its own: `never`, from a
|
|
23
|
+
* verbosity up (`Quiet`: always), or by a rule of its own.
|
|
24
|
+
*/
|
|
25
|
+
readonly line: "never" | Verbosity | ((event: E, verbosity: Verbosity) => boolean)
|
|
26
|
+
/** The event by kind, never its content: how `llm4ts profile` names it around a gap. */
|
|
27
|
+
readonly kind: string | ((event: E) => string)
|
|
28
|
+
/** Its line in a digest. Absent: digests leave the event out. */
|
|
29
|
+
readonly digest?: (event: E) => DigestLine
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const laned = (lane: string | undefined): { readonly lane?: string } =>
|
|
33
|
+
lane === undefined ? {} : { lane }
|
|
34
|
+
|
|
35
|
+
const capability = "a capability check"
|
|
36
|
+
const roster = "a roster change"
|
|
37
|
+
const other = "an event"
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Screen and digest policy for every event. Exhaustive by type: adding an
|
|
41
|
+
* event class without an entry here fails `pnpm typecheck`.
|
|
42
|
+
*/
|
|
43
|
+
export const eventPresentation: {
|
|
44
|
+
readonly [K in FlowEventTag]: Presentation<FlowEventByTag[K]>
|
|
45
|
+
} = {
|
|
46
|
+
// Advise mode publishes a separate Info event for the operator.
|
|
47
|
+
JudgmentObserved: { line: "never", kind: other },
|
|
48
|
+
// For the trace file.
|
|
49
|
+
BlackboardRun: { line: "never", kind: other },
|
|
50
|
+
StageStarted: { line: "Quiet", kind: "a stage start" },
|
|
51
|
+
StageCompleted: { line: "Quiet", kind: "a stage end" },
|
|
52
|
+
StageFailed: {
|
|
53
|
+
line: "Quiet",
|
|
54
|
+
kind: "a stage failure",
|
|
55
|
+
digest: (event) => ({ text: `${event.stage} failed: ${event.message}`, ...laned(event.lane) })
|
|
56
|
+
},
|
|
57
|
+
Aborted: {
|
|
58
|
+
line: "Quiet",
|
|
59
|
+
kind: other,
|
|
60
|
+
digest: (event) => ({ text: `aborted: ${event.message}` })
|
|
61
|
+
},
|
|
62
|
+
Info: { line: "Normal", kind: "a log note" },
|
|
63
|
+
ToolUse: { line: "Normal", kind: "a coder tool call" },
|
|
64
|
+
AssistantMessage: { line: "Normal", kind: "an assistant message" },
|
|
65
|
+
TokensUsed: { line: "Verbose", kind: "a token report" },
|
|
66
|
+
// Progress feeds the status rows.
|
|
67
|
+
UsageProgress: { line: "never", kind: other },
|
|
68
|
+
ReviewFindings: { line: "Normal", kind: "a review round" },
|
|
69
|
+
// Every git read and gate command is a capability event: useful in a
|
|
70
|
+
// trace, noise on screen once several stories run.
|
|
71
|
+
CapabilityUsed: { line: "Verbose", kind: capability },
|
|
72
|
+
CapabilityDenied: { line: "Quiet", kind: capability },
|
|
73
|
+
CapabilityUnenforceable: {
|
|
74
|
+
line: "Quiet",
|
|
75
|
+
kind: capability,
|
|
76
|
+
digest: (event) => ({ text: `capability unenforceable: ${event.detail}` })
|
|
77
|
+
},
|
|
78
|
+
EvidenceChecked: { line: "Quiet", kind: "an evidence check" },
|
|
79
|
+
ReviewFindingDemoted: { line: "Quiet", kind: "a review finding demoted" },
|
|
80
|
+
ExecutorLeased: { line: "Normal", kind: roster },
|
|
81
|
+
// A lease ending is routine.
|
|
82
|
+
ExecutorReleased: { line: "never", kind: roster },
|
|
83
|
+
ExecutorExcluded: {
|
|
84
|
+
line: "Normal",
|
|
85
|
+
kind: roster,
|
|
86
|
+
digest: (event) => ({ text: `${event.executor} excluded: ${event.reason}` })
|
|
87
|
+
},
|
|
88
|
+
ExecutorResumed: { line: "Normal", kind: roster },
|
|
89
|
+
ExecutorHandedOver: {
|
|
90
|
+
line: "Normal",
|
|
91
|
+
kind: roster,
|
|
92
|
+
digest: (event) => ({
|
|
93
|
+
text: `${event.from} handed over ${event.role} (${event.scope}): ${event.reason}`,
|
|
94
|
+
...laned(event.label)
|
|
95
|
+
})
|
|
96
|
+
},
|
|
97
|
+
// The story's outcome already says whether the judge cleared it.
|
|
98
|
+
StoryJudged: {
|
|
99
|
+
line: "never",
|
|
100
|
+
kind: "a judge verdict",
|
|
101
|
+
digest: (event) => ({
|
|
102
|
+
text: `${event.lane} judged round ${event.round}: ${event.cleared ? "cleared" : `${event.issues} issues`}`,
|
|
103
|
+
lane: event.lane
|
|
104
|
+
})
|
|
105
|
+
},
|
|
106
|
+
RoundtableTurn: { line: "never", kind: "a roundtable turn" },
|
|
107
|
+
BriefReported: { line: "never", kind: "a brief report" },
|
|
108
|
+
Timed: {
|
|
109
|
+
line: "never",
|
|
110
|
+
kind: (event) => {
|
|
111
|
+
switch (event.kind) {
|
|
112
|
+
case "model":
|
|
113
|
+
return `the end of a ${event.label} call`
|
|
114
|
+
case "tool":
|
|
115
|
+
return `the end of tool ${event.label}`
|
|
116
|
+
case "gate":
|
|
117
|
+
return `the end of gate ${event.label}`
|
|
118
|
+
case "wait":
|
|
119
|
+
return `the end of a wait for the ${event.label}`
|
|
120
|
+
default:
|
|
121
|
+
return `the end of ${event.label}`
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
Began: {
|
|
126
|
+
// A gate or a setup may run for many minutes: its start is a line, so a
|
|
127
|
+
// long one reads as running, not stuck. Other work starts too often.
|
|
128
|
+
line: (event, verbosity) => event.kind === "gate" && verbosity !== "Quiet",
|
|
129
|
+
kind: (event) =>
|
|
130
|
+
event.kind === "model"
|
|
131
|
+
? `the start of a ${event.label} call`
|
|
132
|
+
: `the start of ${event.kind} ${event.label}`,
|
|
133
|
+
digest: (event) => ({ text: `began ${event.kind} ${event.label}`, ...laned(event.lane) })
|
|
134
|
+
},
|
|
135
|
+
TasksPlanned: { line: "never", kind: other },
|
|
136
|
+
TaskStarted: { line: "never", kind: other },
|
|
137
|
+
TaskCompleted: { line: "never", kind: other },
|
|
138
|
+
StoryStatusChanged: {
|
|
139
|
+
line: "never",
|
|
140
|
+
kind: other,
|
|
141
|
+
digest: (event) => ({ text: `story ${event.id} ${event.status}`, lane: event.id })
|
|
142
|
+
},
|
|
143
|
+
CriterionChecked: {
|
|
144
|
+
line: "Normal",
|
|
145
|
+
kind: other,
|
|
146
|
+
digest: (event) => ({ text: `criterion ${event.tag} ${event.status}`, lane: event.story })
|
|
147
|
+
},
|
|
148
|
+
Parked: {
|
|
149
|
+
line: "Normal",
|
|
150
|
+
kind: other,
|
|
151
|
+
digest: (event) => ({ text: `parked ${event.item}: ${event.reason}`, lane: event.story })
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// A tag and its event are one value's, so the entry found by the tag takes
|
|
156
|
+
// the event. TypeScript does not correlate the two across the union; reading
|
|
157
|
+
// the entry through a generic key is the step that lets it.
|
|
158
|
+
const entryOf = <K extends FlowEventTag>(tag: K): Presentation<FlowEventByTag[K]> =>
|
|
159
|
+
eventPresentation[tag]
|
|
160
|
+
|
|
161
|
+
/** The entry of an event, to apply to that event. */
|
|
162
|
+
export const presentationOf = (event: FlowEvent): Presentation<FlowEvent> => entryOf(event._tag)
|
|
163
|
+
|
|
164
|
+
/** The tags a digest shows: the entries with a `digest`. */
|
|
165
|
+
export const digestedTags: ReadonlyArray<string> = Object.entries(eventPresentation)
|
|
166
|
+
.filter(([, entry]) => entry.digest !== undefined)
|
|
167
|
+
.map(([tag]) => tag)
|
package/src/ExecutorRoster.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
effortOf,
|
|
34
34
|
type Role
|
|
35
35
|
} from "@llm4ts/flow/Roster"
|
|
36
|
+
import { settingValue } from "@llm4ts/flow/Settings"
|
|
36
37
|
import { apiPresetFor, asReadOnly, coderFor, withEnvironment, withModel } from "./Connectors.ts"
|
|
37
38
|
|
|
38
39
|
type Environment = Readonly<Record<string, string | undefined>>
|
|
@@ -188,11 +189,11 @@ export interface RosterSource {
|
|
|
188
189
|
export const loadRosterDocument = Effect.fn("@llm4ts/runner/ExecutorRoster.load")(function* (
|
|
189
190
|
source: RosterSource
|
|
190
191
|
): Effect.fn.Return<RosterDocument | undefined, FlowError> {
|
|
191
|
-
const chosen = source.environment
|
|
192
|
+
const chosen = settingValue("LLM4TS_ROSTER", source.environment)
|
|
192
193
|
if (chosen === "none") {
|
|
193
194
|
return undefined
|
|
194
195
|
}
|
|
195
|
-
const loaded = yield* chosen !== undefined
|
|
196
|
+
const loaded = yield* chosen !== undefined
|
|
196
197
|
? Effect.flatMap(readRoster(source.files, chosen), (document) =>
|
|
197
198
|
document === undefined
|
|
198
199
|
? Effect.fail(RosterInvalid.make({ path: chosen, violations: ["no roster file there"] }))
|
|
@@ -218,10 +219,7 @@ export const loadRosterDocument = Effect.fn("@llm4ts/runner/ExecutorRoster.load"
|
|
|
218
219
|
return undefined
|
|
219
220
|
}
|
|
220
221
|
const { document, where } = loaded
|
|
221
|
-
const ids = (source.environment
|
|
222
|
-
.split(",")
|
|
223
|
-
.map((id) => id.trim())
|
|
224
|
-
.filter((id) => id.length > 0)
|
|
222
|
+
const ids = settingValue("LLM4TS_EXECUTORS", source.environment) ?? []
|
|
225
223
|
const unknown = ids.filter((id) => !document.executors.some((spec) => spec.id === id))
|
|
226
224
|
const narrowed = narrowRoster(document, ids)
|
|
227
225
|
const violations = [
|
package/src/FlowArgs.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile, stat } from "node:fs/promises"
|
|
|
2
2
|
import { resolve } from "node:path"
|
|
3
3
|
import * as Effect from "effect/Effect"
|
|
4
4
|
import * as Schema from "effect/Schema"
|
|
5
|
+
import { connectorTable, offeredCoderNames } from "@llm4ts/core/Models"
|
|
5
6
|
|
|
6
7
|
export interface FlowArgs {
|
|
7
8
|
readonly promptText?: string
|
|
@@ -11,6 +12,10 @@ export interface FlowArgs {
|
|
|
11
12
|
readonly version?: boolean
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
const firstCredentialVariables = Object.values(connectorTable).flatMap((row) =>
|
|
16
|
+
row.kind === "Api" ? row.credentialVariables.slice(0, 1) : []
|
|
17
|
+
)
|
|
18
|
+
|
|
14
19
|
export const FlowUsage =
|
|
15
20
|
'usage: llm4ts ["<prompt>" | --prompt-file <path> | @<path>] [--repo <path>] | llm4ts doctor'
|
|
16
21
|
|
|
@@ -30,13 +35,13 @@ commands:
|
|
|
30
35
|
doctor check connector availability and credentials
|
|
31
36
|
|
|
32
37
|
environment:
|
|
33
|
-
LLM4TS_CODER coding agent:
|
|
38
|
+
LLM4TS_CODER coding agent: ${offeredCoderNames.join("|")} (default: claude)
|
|
34
39
|
LLM4TS_VERBOSITY terminal verbosity: quiet|normal|verbose
|
|
35
40
|
LLM4TS_UI tree: the full-screen agent tree (ADR 0022; q returns to the classic view)
|
|
36
41
|
LLM4TS_TRANSCRIPT on: record each seat's input and output under .llm4ts/transcripts/
|
|
37
42
|
LLM4TS_OTEL on: export spans over OTLP (a local Phoenix at localhost:6006 unless OTEL_* says otherwise)
|
|
38
43
|
LLM4TS_OTEL_CONTENT on|full: prompts, replies and tool I/O travel too (off by default)
|
|
39
|
-
|
|
44
|
+
${firstCredentialVariables.join(", ")}
|
|
40
45
|
credentials for API providers
|
|
41
46
|
|
|
42
47
|
A trace of each run is written under .llm4ts/ in the target repository.
|