@llm4ts/runner 3.3.0 → 3.4.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 +33 -2
- package/dist/Connectors.d.ts.map +1 -1
- package/dist/Connectors.js +85 -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 +133 -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 +3 -1
- package/dist/FlowRunner.d.ts.map +1 -1
- package/dist/FlowRunner.js +31 -38
- package/dist/FlowRunner.js.map +1 -1
- package/dist/NodeGeminiCliExecutor.d.ts.map +1 -1
- package/dist/NodeGeminiCliExecutor.js +6 -4
- package/dist/NodeGeminiCliExecutor.js.map +1 -1
- package/dist/NodeGeminiModelBridge.d.ts.map +1 -1
- package/dist/NodeGeminiModelBridge.js +3 -1
- package/dist/NodeGeminiModelBridge.js.map +1 -1
- package/dist/Otel.d.ts.map +1 -1
- package/dist/Otel.js +4 -5
- package/dist/Otel.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/Profile.d.ts +4 -0
- package/dist/Profile.d.ts.map +1 -1
- package/dist/Profile.js +45 -75
- package/dist/Profile.js.map +1 -1
- package/dist/RunReaders.d.ts +1 -0
- package/dist/RunReaders.d.ts.map +1 -1
- package/dist/RunReaders.js +4 -51
- package/dist/RunReaders.js.map +1 -1
- package/dist/Terminal.d.ts +14 -11
- package/dist/Terminal.d.ts.map +1 -1
- package/dist/Terminal.js +25 -78
- 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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/AgentTree.ts +16 -25
- package/src/AgentTreeSurface.ts +6 -11
- package/src/Cli.ts +2 -1
- package/src/Connectors.ts +141 -107
- package/src/Doctor.ts +10 -12
- package/src/EventPresentation.ts +168 -0
- package/src/ExecutorRoster.ts +4 -6
- package/src/FlowArgs.ts +7 -2
- package/src/FlowRunner.ts +41 -55
- package/src/NodeGeminiCliExecutor.ts +7 -4
- package/src/NodeGeminiModelBridge.ts +4 -1
- package/src/Otel.ts +4 -6
- package/src/Packs.ts +2 -1
- package/src/PiModels.ts +6 -10
- package/src/Profile.ts +64 -81
- package/src/RunReaders.ts +4 -53
- package/src/Terminal.ts +105 -162
- package/src/Tools.ts +2 -1
- package/src/Transcripts.ts +3 -2
- package/src/index.ts +3 -0
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
Declassified: { line: "Normal", kind: other },
|
|
81
|
+
ExecutorLeased: { line: "Normal", kind: roster },
|
|
82
|
+
// A lease ending is routine.
|
|
83
|
+
ExecutorReleased: { line: "never", kind: roster },
|
|
84
|
+
ExecutorExcluded: {
|
|
85
|
+
line: "Normal",
|
|
86
|
+
kind: roster,
|
|
87
|
+
digest: (event) => ({ text: `${event.executor} excluded: ${event.reason}` })
|
|
88
|
+
},
|
|
89
|
+
ExecutorResumed: { line: "Normal", kind: roster },
|
|
90
|
+
ExecutorHandedOver: {
|
|
91
|
+
line: "Normal",
|
|
92
|
+
kind: roster,
|
|
93
|
+
digest: (event) => ({
|
|
94
|
+
text: `${event.from} handed over ${event.role} (${event.scope}): ${event.reason}`,
|
|
95
|
+
...laned(event.label)
|
|
96
|
+
})
|
|
97
|
+
},
|
|
98
|
+
// The story's outcome already says whether the judge cleared it.
|
|
99
|
+
StoryJudged: {
|
|
100
|
+
line: "never",
|
|
101
|
+
kind: "a judge verdict",
|
|
102
|
+
digest: (event) => ({
|
|
103
|
+
text: `${event.lane} judged round ${event.round}: ${event.cleared ? "cleared" : `${event.issues} issues`}`,
|
|
104
|
+
lane: event.lane
|
|
105
|
+
})
|
|
106
|
+
},
|
|
107
|
+
RoundtableTurn: { line: "never", kind: "a roundtable turn" },
|
|
108
|
+
BriefReported: { line: "never", kind: "a brief report" },
|
|
109
|
+
Timed: {
|
|
110
|
+
line: "never",
|
|
111
|
+
kind: (event) => {
|
|
112
|
+
switch (event.kind) {
|
|
113
|
+
case "model":
|
|
114
|
+
return `the end of a ${event.label} call`
|
|
115
|
+
case "tool":
|
|
116
|
+
return `the end of tool ${event.label}`
|
|
117
|
+
case "gate":
|
|
118
|
+
return `the end of gate ${event.label}`
|
|
119
|
+
case "wait":
|
|
120
|
+
return `the end of a wait for the ${event.label}`
|
|
121
|
+
default:
|
|
122
|
+
return `the end of ${event.label}`
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
Began: {
|
|
127
|
+
// A gate or a setup may run for many minutes: its start is a line, so a
|
|
128
|
+
// long one reads as running, not stuck. Other work starts too often.
|
|
129
|
+
line: (event, verbosity) => event.kind === "gate" && verbosity !== "Quiet",
|
|
130
|
+
kind: (event) =>
|
|
131
|
+
event.kind === "model"
|
|
132
|
+
? `the start of a ${event.label} call`
|
|
133
|
+
: `the start of ${event.kind} ${event.label}`,
|
|
134
|
+
digest: (event) => ({ text: `began ${event.kind} ${event.label}`, ...laned(event.lane) })
|
|
135
|
+
},
|
|
136
|
+
TasksPlanned: { line: "never", kind: other },
|
|
137
|
+
TaskStarted: { line: "never", kind: other },
|
|
138
|
+
TaskCompleted: { line: "never", kind: other },
|
|
139
|
+
StoryStatusChanged: {
|
|
140
|
+
line: "never",
|
|
141
|
+
kind: other,
|
|
142
|
+
digest: (event) => ({ text: `story ${event.id} ${event.status}`, lane: event.id })
|
|
143
|
+
},
|
|
144
|
+
CriterionChecked: {
|
|
145
|
+
line: "Normal",
|
|
146
|
+
kind: other,
|
|
147
|
+
digest: (event) => ({ text: `criterion ${event.tag} ${event.status}`, lane: event.story })
|
|
148
|
+
},
|
|
149
|
+
Parked: {
|
|
150
|
+
line: "Normal",
|
|
151
|
+
kind: other,
|
|
152
|
+
digest: (event) => ({ text: `parked ${event.item}: ${event.reason}`, lane: event.story })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// A tag and its event are one value's, so the entry found by the tag takes
|
|
157
|
+
// the event. TypeScript does not correlate the two across the union; reading
|
|
158
|
+
// the entry through a generic key is the step that lets it.
|
|
159
|
+
const entryOf = <K extends FlowEventTag>(tag: K): Presentation<FlowEventByTag[K]> =>
|
|
160
|
+
eventPresentation[tag]
|
|
161
|
+
|
|
162
|
+
/** The entry of an event, to apply to that event. */
|
|
163
|
+
export const presentationOf = (event: FlowEvent): Presentation<FlowEvent> => entryOf(event._tag)
|
|
164
|
+
|
|
165
|
+
/** The tags a digest shows: the entries with a `digest`. */
|
|
166
|
+
export const digestedTags: ReadonlyArray<string> = Object.entries(eventPresentation)
|
|
167
|
+
.filter(([, entry]) => entry.digest !== undefined)
|
|
168
|
+
.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.
|
package/src/FlowRunner.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { createConnectorRegistry } from "@llm4ts/core/providers/ConnectorFactori
|
|
|
27
27
|
import { ConnectorIds } from "@llm4ts/core/Models"
|
|
28
28
|
import { ProviderError } from "@llm4ts/core/Errors"
|
|
29
29
|
import { makeCostTracker, type CostTracker } from "@llm4ts/flow/CostTracker"
|
|
30
|
+
import { settingValue } from "@llm4ts/flow/Settings"
|
|
30
31
|
import { runGeminiAcpBridge } from "./NodeGeminiAcpBridge.ts"
|
|
31
32
|
import { runGeminiModelBridge } from "./NodeGeminiModelBridge.ts"
|
|
32
33
|
import {
|
|
@@ -51,7 +52,7 @@ import {
|
|
|
51
52
|
type FlowError
|
|
52
53
|
} from "@llm4ts/flow/FlowError"
|
|
53
54
|
import {
|
|
54
|
-
|
|
55
|
+
type DrainReport,
|
|
55
56
|
FlowEvents,
|
|
56
57
|
FlowEventsValues,
|
|
57
58
|
Info,
|
|
@@ -264,12 +265,6 @@ export interface FlowRunnerBundle {
|
|
|
264
265
|
readonly announceStartupNotices: Effect.Effect<void>
|
|
265
266
|
}
|
|
266
267
|
|
|
267
|
-
const truthy = (value: string | undefined): boolean =>
|
|
268
|
-
value !== undefined && ["1", "true", "on", "yes"].includes(value.trim().toLowerCase())
|
|
269
|
-
|
|
270
|
-
const switchedOff = (value: string | undefined): boolean =>
|
|
271
|
-
value !== undefined && ["0", "false", "no", "off"].includes(value.trim().toLowerCase())
|
|
272
|
-
|
|
273
268
|
const isPiSeat = (config: ConnectorConfig | undefined): config is CliConnectorConfig =>
|
|
274
269
|
config instanceof CliConnectorConfig && config.connectorId.value === ConnectorIds.Pi.value
|
|
275
270
|
|
|
@@ -289,7 +284,7 @@ const geminiBridgeSeats = Effect.fn("@llm4ts/runner/FlowRunner.geminiBridgeSeats
|
|
|
289
284
|
environment: Readonly<Record<string, string | undefined>>
|
|
290
285
|
): Effect.fn.Return<(config: ConnectorConfig) => ConnectorConfig, FlowError, Scope.Scope> {
|
|
291
286
|
const identity = (config: ConnectorConfig): ConnectorConfig => config
|
|
292
|
-
if (!
|
|
287
|
+
if (!settingValue("LLM4TS_GEMINI_BRIDGE", environment)) {
|
|
293
288
|
return identity
|
|
294
289
|
}
|
|
295
290
|
// The reasoning seat defaults to the coder, so a pi coder makes this a pi
|
|
@@ -304,7 +299,7 @@ const geminiBridgeSeats = Effect.fn("@llm4ts/runner/FlowRunner.geminiBridgeSeats
|
|
|
304
299
|
const resolution = resolveBridgeModel(
|
|
305
300
|
readPiModelsJson(),
|
|
306
301
|
port,
|
|
307
|
-
environment
|
|
302
|
+
settingValue("LLM4TS_GEMINI_BRIDGE_MODEL", environment)
|
|
308
303
|
)
|
|
309
304
|
const problem = bridgeModelProblem(resolution, port)
|
|
310
305
|
if (problem !== undefined || resolution._tag !== "Resolved") {
|
|
@@ -322,8 +317,9 @@ const geminiBridgeSeats = Effect.fn("@llm4ts/runner/FlowRunner.geminiBridgeSeats
|
|
|
322
317
|
})
|
|
323
318
|
)
|
|
324
319
|
}
|
|
325
|
-
const
|
|
326
|
-
|
|
320
|
+
const requestModel = settingValue("LLM4TS_GEMINI_MODEL", environment)
|
|
321
|
+
const geminiModel = requestModel === undefined ? {} : { model: requestModel }
|
|
322
|
+
const token = settingValue("LLM4TS_GEMINI_BRIDGE_TOKEN", environment)
|
|
327
323
|
if (mode.mode === "model") {
|
|
328
324
|
// ADR 0037: one locked-down `gemini` call per request, in an empty
|
|
329
325
|
// directory so no project GEMINI.md reaches the model.
|
|
@@ -337,10 +333,7 @@ const geminiBridgeSeats = Effect.fn("@llm4ts/runner/FlowRunner.geminiBridgeSeats
|
|
|
337
333
|
executor: dependencies.process,
|
|
338
334
|
temporaryFiles: dependencies.temporaryFiles ?? nodeTemporaryFiles,
|
|
339
335
|
...geminiModel,
|
|
340
|
-
...(
|
|
341
|
-
environment.LLM4TS_GEMINI_BRIDGE_TOKEN.length === 0
|
|
342
|
-
? {}
|
|
343
|
-
: { token: environment.LLM4TS_GEMINI_BRIDGE_TOKEN })
|
|
336
|
+
...(token === undefined ? {} : { token })
|
|
344
337
|
}).pipe(Effect.mapError(FlowLlmError.from))
|
|
345
338
|
} else {
|
|
346
339
|
yield* runGeminiAcpBridge({
|
|
@@ -368,8 +361,8 @@ export const runIdentityFromEnvironment = (
|
|
|
368
361
|
options: Pick<FlowRunnerOptions, "runId" | "tracePath">,
|
|
369
362
|
environment: Readonly<Record<string, string | undefined>>
|
|
370
363
|
): { readonly runId?: string; readonly tracePath?: string } => {
|
|
371
|
-
const runId = nonBlank(options.runId) ??
|
|
372
|
-
const tracePath = nonBlank(options.tracePath) ??
|
|
364
|
+
const runId = nonBlank(options.runId) ?? settingValue("LLM4TS_RUN_ID", environment)
|
|
365
|
+
const tracePath = nonBlank(options.tracePath) ?? settingValue("LLM4TS_TRACE_PATH", environment)
|
|
373
366
|
return {
|
|
374
367
|
...(runId === undefined ? {} : { runId }),
|
|
375
368
|
...(tracePath === undefined ? {} : { tracePath })
|
|
@@ -521,7 +514,7 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
521
514
|
// notably the judgment seat and the worktree seats `contextFor` rebinds.
|
|
522
515
|
const runEnvironment = options.environment ?? process.env
|
|
523
516
|
const estimateOptions = estimatedUsageOptionsFromEnv(runEnvironment)
|
|
524
|
-
const meterUsage = options.estimateUsage ??
|
|
517
|
+
const meterUsage = options.estimateUsage ?? settingValue("LLM4TS_ESTIMATE_USAGE", runEnvironment)
|
|
525
518
|
// Every seat's spans (ADR 0026): content only when asked, an estimate on a
|
|
526
519
|
// call the backend reports no usage for, and who served it when known.
|
|
527
520
|
const seatOptions = (executor?: Effect.Effect<string | undefined>): TimedSeatOptions => ({
|
|
@@ -578,11 +571,7 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
578
571
|
seatConfig: ConnectorConfig,
|
|
579
572
|
environment: Readonly<Record<string, string | undefined>>
|
|
580
573
|
): Effect.Effect<JudgmentShape, FlowError> => {
|
|
581
|
-
const backend =
|
|
582
|
-
options.judgmentBackend ??
|
|
583
|
-
(environment.LLM4TS_JUDGMENT_BACKEND?.trim().toLowerCase() === "typesafe"
|
|
584
|
-
? "typesafe"
|
|
585
|
-
: "llm")
|
|
574
|
+
const backend = options.judgmentBackend ?? settingValue("LLM4TS_JUDGMENT_BACKEND", environment)
|
|
586
575
|
if (backend !== "typesafe") {
|
|
587
576
|
return Effect.succeed(
|
|
588
577
|
makeLlmJudgment(
|
|
@@ -592,7 +581,7 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
592
581
|
...(seatConfig.model === undefined ? {} : { model: seatConfig.model }),
|
|
593
582
|
// LLM4TS_JUDGMENT_BATCHING=shared-prefix sends each request's state
|
|
594
583
|
// once with every question; anything else keeps them independent.
|
|
595
|
-
...(environment
|
|
584
|
+
...(settingValue("LLM4TS_JUDGMENT_BATCHING", environment) === "shared-prefix"
|
|
596
585
|
? { batching: "shared-prefix" as const }
|
|
597
586
|
: {})
|
|
598
587
|
}),
|
|
@@ -987,6 +976,21 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
987
976
|
}
|
|
988
977
|
})
|
|
989
978
|
|
|
979
|
+
/** What the run prints when `drained` reports a consumer short. */
|
|
980
|
+
export const drainShortfall = (report: DrainReport): string => {
|
|
981
|
+
const behind = report.behind.map(
|
|
982
|
+
(consumer) => `${consumer.name} was ${consumer.missing.toString()} event(s) behind`
|
|
983
|
+
)
|
|
984
|
+
const late = report.late.map(
|
|
985
|
+
(consumer) =>
|
|
986
|
+
`${consumer.name} started listening after ${consumer.missed.toString()} event(s) were published`
|
|
987
|
+
)
|
|
988
|
+
return (
|
|
989
|
+
`some run events were still unconsumed: ${[...late, ...behind].join("; ")}. ` +
|
|
990
|
+
"The totals below, the ledger, and the trace may be short"
|
|
991
|
+
)
|
|
992
|
+
}
|
|
993
|
+
|
|
990
994
|
/** `claude (coder·reviewer·judge ×3)` — how the run header names an executor. */
|
|
991
995
|
const describeExecutor = (spec: ExecutorSpec): string =>
|
|
992
996
|
`${spec.id} (${rolesOf(spec).join("·")} ×${spec.slots ?? 1})`
|
|
@@ -1004,7 +1008,8 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
1004
1008
|
const dependencies = withRunStateIgnore(given)
|
|
1005
1009
|
const startedAt = yield* Clock.currentTimeMillis
|
|
1006
1010
|
const environment = options.environment ?? process.env
|
|
1007
|
-
const verbosity =
|
|
1011
|
+
const verbosity =
|
|
1012
|
+
options.verbosity ?? parseVerbosity(settingValue("LLM4TS_VERBOSITY", environment))
|
|
1008
1013
|
const persistRun = options.persistRun ?? true
|
|
1009
1014
|
// OpenTelemetry export (ADR 0026): decided from the environment, built once
|
|
1010
1015
|
// per run, flushed when the run's scope closes.
|
|
@@ -1043,7 +1048,7 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
1043
1048
|
const surface = tree?.surface ?? classic
|
|
1044
1049
|
const palette = surface.palette
|
|
1045
1050
|
const terminal = yield* consumeTerminalEvents(bundle.events, surface, verbosity, {
|
|
1046
|
-
timestamps:
|
|
1051
|
+
timestamps: settingValue("LLM4TS_TIMESTAMPS", environment)
|
|
1047
1052
|
})
|
|
1048
1053
|
if (verbosity !== "Quiet") {
|
|
1049
1054
|
const reviewers = options.reviewers ?? []
|
|
@@ -1130,10 +1135,11 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
1130
1135
|
tracePath === undefined
|
|
1131
1136
|
? bundle.context
|
|
1132
1137
|
: { ...bundle.context, trace: { runId: bundle.runId, path: tracePath } }
|
|
1138
|
+
const flow = settingValue("LLM4TS_FLOW", environment)
|
|
1133
1139
|
const runAttributes: Record<string, unknown> = {
|
|
1134
1140
|
[attr.run]: bundle.runId,
|
|
1135
1141
|
[attr.session]: bundle.runId,
|
|
1136
|
-
...(
|
|
1142
|
+
...(flow === undefined ? {} : { [attr.flow]: flow })
|
|
1137
1143
|
}
|
|
1138
1144
|
// The run is a CHAIN span every other span descends from or links to; its
|
|
1139
1145
|
// attributes ride on all of them. Effect's own function spans stay
|
|
@@ -1159,33 +1165,13 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
1159
1165
|
Effect.provideService(FlowContext, context),
|
|
1160
1166
|
Effect.andThen((value) => Effect.as(enforceBudget, value)),
|
|
1161
1167
|
Effect.ensuring(
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
bundle.judgmentLog === undefined
|
|
1170
|
-
? Effect.succeed(true)
|
|
1171
|
-
: bundle.judgmentLog.awaitDrained(bundle.events)
|
|
1172
|
-
],
|
|
1173
|
-
{ concurrency: "unbounded" }
|
|
1174
|
-
)
|
|
1175
|
-
],
|
|
1176
|
-
{ concurrency: "unbounded" }
|
|
1177
|
-
).pipe(
|
|
1178
|
-
// A consumer that gave up before catching up leaves the totals below
|
|
1179
|
-
// short. Say so rather than printing a quietly incomplete summary.
|
|
1180
|
-
Effect.flatMap(([, accounted]) =>
|
|
1181
|
-
accounted.every((drained) => drained)
|
|
1182
|
-
? Effect.void
|
|
1183
|
-
: surface.log(
|
|
1184
|
-
palette.fail(
|
|
1185
|
-
`some run events were still unconsumed after ${defaultDrainTimeout} — ` +
|
|
1186
|
-
"the totals below, the ledger, and the trace may be short"
|
|
1187
|
-
)
|
|
1188
|
-
)
|
|
1168
|
+
// One wait for every consumer of the run: the terminal, the totals,
|
|
1169
|
+
// the trace, the judgment log. A consumer that fell short leaves the
|
|
1170
|
+
// totals below short, so the run names it instead of printing a
|
|
1171
|
+
// quietly incomplete summary.
|
|
1172
|
+
bundle.events.drained().pipe(
|
|
1173
|
+
Effect.flatMap((report) =>
|
|
1174
|
+
report.complete ? Effect.void : surface.log(palette.fail(drainShortfall(report)))
|
|
1189
1175
|
),
|
|
1190
1176
|
Effect.andThen(tree === undefined ? Effect.void : tree.close),
|
|
1191
1177
|
Effect.andThen(appendLedger),
|
|
@@ -1254,7 +1240,7 @@ const wantsAgentTree = (
|
|
|
1254
1240
|
output: { readonly isTTY?: boolean; readonly columns?: number },
|
|
1255
1241
|
input: { readonly isTTY?: boolean }
|
|
1256
1242
|
): boolean =>
|
|
1257
|
-
|
|
1243
|
+
settingValue("LLM4TS_UI", environment) === "tree" &&
|
|
1258
1244
|
environment.NO_COLOR === undefined &&
|
|
1259
1245
|
output.isTTY === true &&
|
|
1260
1246
|
input.isTTY === true &&
|
|
@@ -19,6 +19,9 @@ import {
|
|
|
19
19
|
} from "@llm4ts/core/providers/GeminiCliProvider"
|
|
20
20
|
import { nodeProcessExecutor } from "./NodeProcessExecutor.ts"
|
|
21
21
|
import { nodeTemporaryFiles } from "./NodeTemporaryFiles.ts"
|
|
22
|
+
import { cliExecutable } from "@llm4ts/core/Models"
|
|
23
|
+
|
|
24
|
+
const executable = cliExecutable("GeminiCli")
|
|
22
25
|
|
|
23
26
|
const failure = (message: string): ProviderError => ProviderError.make({ message })
|
|
24
27
|
|
|
@@ -47,7 +50,7 @@ export const makeNodeGeminiCliExecutor = (
|
|
|
47
50
|
const supportsAdminPolicy: Effect.Effect<boolean> = Effect.suspend(() =>
|
|
48
51
|
adminPolicy !== undefined
|
|
49
52
|
? Effect.succeed(adminPolicy)
|
|
50
|
-
: executor.run([
|
|
53
|
+
: executor.run([executable, "--help"], process.cwd(), {}).pipe(
|
|
51
54
|
Effect.map((result) =>
|
|
52
55
|
geminiSupportsAdminPolicy([...result.stdout, ...result.stderr].join("\n"))
|
|
53
56
|
),
|
|
@@ -75,7 +78,7 @@ export const makeNodeGeminiCliExecutor = (
|
|
|
75
78
|
)
|
|
76
79
|
return {
|
|
77
80
|
checkGeminiInstalled: executor
|
|
78
|
-
.run([
|
|
81
|
+
.run([executable, "--version"], process.cwd(), {})
|
|
79
82
|
.pipe(
|
|
80
83
|
Effect.flatMap((result) =>
|
|
81
84
|
result.exitCode === 0
|
|
@@ -89,7 +92,7 @@ export const makeNodeGeminiCliExecutor = (
|
|
|
89
92
|
const environment = yield* environmentFor(context)
|
|
90
93
|
const args = yield* argsFor(config, context, "json")
|
|
91
94
|
const result = yield* executor.runWithStdin(
|
|
92
|
-
[
|
|
95
|
+
[executable, ...args],
|
|
93
96
|
context.cwd ?? process.cwd(),
|
|
94
97
|
environment,
|
|
95
98
|
prompt
|
|
@@ -111,7 +114,7 @@ export const makeNodeGeminiCliExecutor = (
|
|
|
111
114
|
([environment, args]) =>
|
|
112
115
|
executor
|
|
113
116
|
.runStreamingWithStdin(
|
|
114
|
-
[
|
|
117
|
+
[executable, ...args],
|
|
115
118
|
context.cwd ?? process.cwd(),
|
|
116
119
|
environment,
|
|
117
120
|
prompt
|
|
@@ -34,6 +34,9 @@ import {
|
|
|
34
34
|
requestPath,
|
|
35
35
|
writeJson
|
|
36
36
|
} from "./NodeGeminiAcpBridge.ts"
|
|
37
|
+
import { cliExecutable } from "@llm4ts/core/Models"
|
|
38
|
+
|
|
39
|
+
const executable = cliExecutable("GeminiCli")
|
|
37
40
|
|
|
38
41
|
/**
|
|
39
42
|
* The bridge's model route (ADR 0037): `/v1/chat/completions` in OpenAI's
|
|
@@ -131,7 +134,7 @@ const geminiTurn = (
|
|
|
131
134
|
config.executor
|
|
132
135
|
.runStreamingWithStdin(
|
|
133
136
|
[
|
|
134
|
-
|
|
137
|
+
executable,
|
|
135
138
|
// Bound to its flag, so it can never read as a flag of its own.
|
|
136
139
|
`--model=${model}`,
|
|
137
140
|
"--output-format",
|
package/src/Otel.ts
CHANGED
|
@@ -12,6 +12,7 @@ import * as OtlpExporter from "effect/observability/OtlpExporter"
|
|
|
12
12
|
import * as OtlpMetrics from "effect/observability/OtlpMetrics"
|
|
13
13
|
import * as OtlpSerialization from "effect/observability/OtlpSerialization"
|
|
14
14
|
import * as OtlpTracer from "effect/observability/OtlpTracer"
|
|
15
|
+
import { settingValue } from "@llm4ts/flow/Settings"
|
|
15
16
|
import { packageVersion } from "./Package.ts"
|
|
16
17
|
|
|
17
18
|
export type OtelConfig =
|
|
@@ -23,8 +24,6 @@ export const phoenixTracesUrl = "http://localhost:6006/v1/traces"
|
|
|
23
24
|
|
|
24
25
|
const truthy = (value: string | undefined): boolean =>
|
|
25
26
|
/^(on|1|true|yes)$/iu.test(value?.trim() ?? "")
|
|
26
|
-
const falsy = (value: string | undefined): boolean =>
|
|
27
|
-
/^(off|0|false|no)$/iu.test(value?.trim() ?? "")
|
|
28
27
|
|
|
29
28
|
/**
|
|
30
29
|
* Off unless asked. An OTLP endpoint in the environment wins and is used as
|
|
@@ -34,7 +33,8 @@ const falsy = (value: string | undefined): boolean =>
|
|
|
34
33
|
export const otelConfig = (
|
|
35
34
|
environment: Readonly<Record<string, string | undefined>>
|
|
36
35
|
): OtelConfig => {
|
|
37
|
-
|
|
36
|
+
const asked = settingValue("LLM4TS_OTEL", environment)
|
|
37
|
+
if (truthy(environment.OTEL_SDK_DISABLED) || asked === false) {
|
|
38
38
|
return { mode: "off" }
|
|
39
39
|
}
|
|
40
40
|
// Under the test runner the exporter's sleeps run on a TestClock nobody
|
|
@@ -49,9 +49,7 @@ export const otelConfig = (
|
|
|
49
49
|
if (endpoint !== undefined && endpoint.length > 0) {
|
|
50
50
|
return { mode: "env", endpoint }
|
|
51
51
|
}
|
|
52
|
-
return
|
|
53
|
-
? { mode: "phoenix", tracesUrl: phoenixTracesUrl }
|
|
54
|
-
: { mode: "off" }
|
|
52
|
+
return asked === true ? { mode: "phoenix", tracesUrl: phoenixTracesUrl } : { mode: "off" }
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
export interface OtelResource {
|
package/src/Packs.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { PlanParseError } from "@llm4ts/flow/FlowError"
|
|
|
7
7
|
import { loadPack, type Pack } from "@llm4ts/flow/Pack"
|
|
8
8
|
import { loadPatternCards, type PatternCard } from "@llm4ts/flow/Patterns"
|
|
9
9
|
import type { WorkspaceError, WorkspaceShape } from "@llm4ts/flow/Workspace"
|
|
10
|
+
import { settingValue } from "@llm4ts/flow/Settings"
|
|
10
11
|
import {
|
|
11
12
|
builtinKitsDir,
|
|
12
13
|
discoverKits,
|
|
@@ -108,7 +109,7 @@ export const openPack = Effect.fn("@llm4ts/runner/Packs.openPack")(function* (op
|
|
|
108
109
|
/** Kit tiers to search; defaults to the standard tiers around `launchDir` and `flowDir`. */
|
|
109
110
|
readonly kits?: KitTierPaths
|
|
110
111
|
}): Effect.fn.Return<OpenedPack, PackNotFound | WorkspaceError | PlanParseError> {
|
|
111
|
-
const reference = options.environment
|
|
112
|
+
const reference = settingValue("LLM4TS_PACK", options.environment) ?? defaultPackName
|
|
112
113
|
const builtinDir = builtinKitsDir(options.flowDir)
|
|
113
114
|
const tiers =
|
|
114
115
|
options.kits ??
|
package/src/PiModels.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs"
|
|
2
2
|
import { homedir } from "node:os"
|
|
3
3
|
import { join } from "node:path"
|
|
4
|
+
import { settingGiven, settingText } from "@llm4ts/flow/Settings"
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Reading `pi`'s own model config (ADR 0016). Separate from `Doctor.ts` so
|
|
@@ -29,10 +30,7 @@ export const defaultReadPiModelsJson = (): string | undefined => {
|
|
|
29
30
|
|
|
30
31
|
export const geminiBridgePort = (
|
|
31
32
|
environment: Readonly<Record<string, string | undefined>>
|
|
32
|
-
): string =>
|
|
33
|
-
isSet(environment.LLM4TS_GEMINI_BRIDGE_PORT)
|
|
34
|
-
? environment.LLM4TS_GEMINI_BRIDGE_PORT.trim()
|
|
35
|
-
: defaultGeminiBridgePort
|
|
33
|
+
): string => settingText("LLM4TS_GEMINI_BRIDGE_PORT", environment) ?? defaultGeminiBridgePort
|
|
36
34
|
|
|
37
35
|
/**
|
|
38
36
|
* Which route the bridge serves (ADR 0037): `acp` (ADR 0016's Anthropic
|
|
@@ -46,12 +44,10 @@ export type GeminiBridgeMode =
|
|
|
46
44
|
export const geminiBridgeMode = (
|
|
47
45
|
environment: Readonly<Record<string, string | undefined>>
|
|
48
46
|
): GeminiBridgeMode => {
|
|
49
|
-
const
|
|
50
|
-
return
|
|
51
|
-
? { _tag: "
|
|
52
|
-
:
|
|
53
|
-
? { _tag: "Mode", mode: "model" }
|
|
54
|
-
: { _tag: "Unknown", value }
|
|
47
|
+
const given = settingGiven("LLM4TS_GEMINI_BRIDGE_MODE", environment)
|
|
48
|
+
return given._tag === "Unreadable"
|
|
49
|
+
? { _tag: "Unknown", value: given.text.toLowerCase() }
|
|
50
|
+
: { _tag: "Mode", mode: given._tag === "Read" ? given.value : "acp" }
|
|
55
51
|
}
|
|
56
52
|
|
|
57
53
|
/**
|