@llm4ts/runner 3.4.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/dist/Connectors.d.ts +0 -6
- package/dist/Connectors.d.ts.map +1 -1
- package/dist/Connectors.js +0 -9
- package/dist/Connectors.js.map +1 -1
- package/dist/EventPresentation.d.ts.map +1 -1
- package/dist/EventPresentation.js +0 -1
- package/dist/EventPresentation.js.map +1 -1
- package/dist/FlowRunner.d.ts +10 -3
- package/dist/FlowRunner.d.ts.map +1 -1
- package/dist/FlowRunner.js +20 -2
- package/dist/FlowRunner.js.map +1 -1
- package/dist/NodeGeminiCliExecutor.d.ts.map +1 -1
- package/dist/NodeGeminiCliExecutor.js +1 -0
- package/dist/NodeGeminiCliExecutor.js.map +1 -1
- package/dist/NodeGeminiModelBridge.d.ts.map +1 -1
- package/dist/NodeGeminiModelBridge.js +1 -0
- 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 +1 -0
- 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/Terminal.d.ts +1 -2
- package/dist/Terminal.d.ts.map +1 -1
- package/dist/Terminal.js +1 -4
- package/dist/Terminal.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 +8 -21
- package/src/Connectors.ts +0 -10
- package/src/EventPresentation.ts +0 -1
- package/src/FlowRunner.ts +26 -6
- package/src/NodeGeminiCliExecutor.ts +1 -0
- package/src/NodeGeminiModelBridge.ts +1 -0
- package/src/NodeHttpClient.ts +1 -0
- package/src/Otel.ts +1 -0
- package/src/Package.ts +1 -0
- package/src/Terminal.ts +1 -5
- package/src/WindowsCommand.ts +1 -0
- package/src/index.ts +0 -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 -313
- package/dist/Profile.d.ts.map +0 -1
- package/dist/Profile.js +0 -937
- 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 -70
- package/dist/RunReaders.d.ts.map +0 -1
- package/dist/RunReaders.js +0 -236
- 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 -1196
- package/src/ReviewFingerprint.ts +0 -13
- package/src/RunReaders.ts +0 -370
- package/src/StoryReaders.ts +0 -157
- package/src/Watch.ts +0 -446
package/src/Watch.ts
DELETED
|
@@ -1,446 +0,0 @@
|
|
|
1
|
-
import { readdirSync, existsSync, statSync } from "node:fs"
|
|
2
|
-
import { join } from "node:path"
|
|
3
|
-
import * as Clock from "effect/Clock"
|
|
4
|
-
import * as Duration from "effect/Duration"
|
|
5
|
-
import * as Effect from "effect/Effect"
|
|
6
|
-
import * as Queue from "effect/Queue"
|
|
7
|
-
import * as Ref from "effect/Ref"
|
|
8
|
-
import * as Schema from "effect/Schema"
|
|
9
|
-
import * as Stream from "effect/Stream"
|
|
10
|
-
import { Board, BoardVersion } from "@llm4ts/flow/BoardSync"
|
|
11
|
-
import { readEpicRuns, type EpicRun } from "@llm4ts/flow/EpicRuns"
|
|
12
|
-
import type { FlowError } from "@llm4ts/flow/FlowError"
|
|
13
|
-
import { loadVersioned, type PlainFileStoreShape } from "@llm4ts/flow/Persistence"
|
|
14
|
-
import { readTrace } from "@llm4ts/flow/Replay"
|
|
15
|
-
import {
|
|
16
|
-
emptyTree,
|
|
17
|
-
initialView,
|
|
18
|
-
onTreeKey,
|
|
19
|
-
reduceTree,
|
|
20
|
-
renderTree,
|
|
21
|
-
tailTargetOf,
|
|
22
|
-
treeInputsOfTrace,
|
|
23
|
-
type TreeInput,
|
|
24
|
-
type TreeState,
|
|
25
|
-
type TreeView
|
|
26
|
-
} from "./AgentTree.ts"
|
|
27
|
-
import { enterScreen, leaveScreen, makeScreenPainter, nodeTreeKeys } from "./AgentTreeSurface.ts"
|
|
28
|
-
import {
|
|
29
|
-
loadTranscript,
|
|
30
|
-
nodeTranscriptFiles,
|
|
31
|
-
renderTranscript,
|
|
32
|
-
tailLinesOf,
|
|
33
|
-
transcriptFileOf,
|
|
34
|
-
type TailTarget,
|
|
35
|
-
type TranscriptFiles
|
|
36
|
-
} from "./Transcripts.ts"
|
|
37
|
-
import { nodeListTraces, TraceDirectoryName } from "./Costs.ts"
|
|
38
|
-
import { nodePlainFileStore } from "./NodePlainFileStore.ts"
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* `llm4ts watch` (ADR 0022): the agent tree over a trace file — a live run
|
|
42
|
-
* followed as it grows, a finished run's last frame, or a replay on the
|
|
43
|
-
* trace's own timestamps. The trace is the only source; the epic's board
|
|
44
|
-
* adds the stories that have not started.
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
export interface WatchOptions {
|
|
48
|
-
readonly repo: string
|
|
49
|
-
/** A trace file; the newest in the repository's `.llm4ts/` when omitted. */
|
|
50
|
-
readonly trace?: string
|
|
51
|
-
/** An epic id: its latest recorded run. */
|
|
52
|
-
readonly epic?: string
|
|
53
|
-
readonly replay?: boolean
|
|
54
|
-
/** Replay speed-up (default 10). */
|
|
55
|
-
readonly speed?: number
|
|
56
|
-
/** A live lane with no event for this long is marked idle (`LLM4TS_IDLE_AFTER`). */
|
|
57
|
-
readonly idleAfterMs?: number
|
|
58
|
-
/** Print this story's or executor's transcript as it grows, instead of the tree. */
|
|
59
|
-
readonly tail?: string
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface WatchOutput {
|
|
63
|
-
readonly write: (text: string) => Effect.Effect<void>
|
|
64
|
-
readonly columns: () => number | undefined
|
|
65
|
-
readonly rows: () => number | undefined
|
|
66
|
-
/** A terminal a person looks at: full-screen, keys, following. */
|
|
67
|
-
readonly interactive: boolean
|
|
68
|
-
readonly colour: boolean
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface WatchDependencies {
|
|
72
|
-
readonly files: PlainFileStoreShape
|
|
73
|
-
readonly listTraces: (directory: string) => ReadonlyArray<string>
|
|
74
|
-
/** Epic folder names under `.llm4ts/epics/`. */
|
|
75
|
-
readonly listEpics: (directory: string) => ReadonlyArray<string>
|
|
76
|
-
readonly output: WatchOutput
|
|
77
|
-
/** Key names as `onTreeKey` reads them: "up", "enter", "q", … */
|
|
78
|
-
readonly keys: Stream.Stream<string>
|
|
79
|
-
/** A run's transcript files (`--transcript`); the disk by default. */
|
|
80
|
-
readonly transcripts?: TranscriptFiles
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export class WatchTargetMissing extends Schema.TaggedError<WatchTargetMissing>()(
|
|
84
|
-
"WatchTargetMissing",
|
|
85
|
-
{ message: Schema.String }
|
|
86
|
-
) {}
|
|
87
|
-
|
|
88
|
-
/** Where a run's trace and its epic are read from. */
|
|
89
|
-
export type TraceSources = Pick<WatchDependencies, "files" | "listTraces" | "listEpics">
|
|
90
|
-
|
|
91
|
-
/** A trace to read: given, an epic's latest run, or the newest in the repository. */
|
|
92
|
-
export type TraceChoice = Pick<WatchOptions, "repo" | "trace" | "epic">
|
|
93
|
-
|
|
94
|
-
export interface WatchTarget {
|
|
95
|
-
readonly tracePath: string
|
|
96
|
-
readonly epicDir: string | undefined
|
|
97
|
-
readonly run: EpicRun | undefined
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const epicsDirOf = (repo: string): string => join(repo, TraceDirectoryName, "epics")
|
|
101
|
-
|
|
102
|
-
/** The epic whose `runs.jsonl` names this trace, if one does. */
|
|
103
|
-
const epicOfTrace = (
|
|
104
|
-
tracePath: string,
|
|
105
|
-
repo: string,
|
|
106
|
-
dependencies: TraceSources
|
|
107
|
-
): Effect.Effect<{ readonly dir: string; readonly run: EpicRun } | undefined, FlowError> =>
|
|
108
|
-
Effect.gen(function* () {
|
|
109
|
-
const epicsDir = epicsDirOf(repo)
|
|
110
|
-
for (const epic of dependencies.listEpics(epicsDir)) {
|
|
111
|
-
const dir = join(epicsDir, epic)
|
|
112
|
-
const run = [...(yield* readEpicRuns(dependencies.files, dir))]
|
|
113
|
-
.reverse()
|
|
114
|
-
.find((candidate) => candidate.tracePath === tracePath)
|
|
115
|
-
if (run !== undefined) {
|
|
116
|
-
return { dir, run }
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return undefined
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
export const resolveTraceTarget = (
|
|
123
|
-
options: TraceChoice,
|
|
124
|
-
dependencies: TraceSources
|
|
125
|
-
): Effect.Effect<WatchTarget, FlowError | WatchTargetMissing> =>
|
|
126
|
-
Effect.gen(function* () {
|
|
127
|
-
if (options.epic !== undefined) {
|
|
128
|
-
const dir = join(epicsDirOf(options.repo), options.epic)
|
|
129
|
-
const run = (yield* readEpicRuns(dependencies.files, dir)).at(-1)
|
|
130
|
-
if (run === undefined) {
|
|
131
|
-
return yield* WatchTargetMissing.make({
|
|
132
|
-
message: `epic ${options.epic} has no recorded run in ${dir}/runs.jsonl`
|
|
133
|
-
})
|
|
134
|
-
}
|
|
135
|
-
return { tracePath: run.tracePath, epicDir: dir, run }
|
|
136
|
-
}
|
|
137
|
-
const tracePath =
|
|
138
|
-
options.trace ?? dependencies.listTraces(join(options.repo, TraceDirectoryName)).at(-1)
|
|
139
|
-
if (tracePath === undefined) {
|
|
140
|
-
return yield* WatchTargetMissing.make({
|
|
141
|
-
message: `no trace in ${join(options.repo, TraceDirectoryName)}; pass a trace file or --epic`
|
|
142
|
-
})
|
|
143
|
-
}
|
|
144
|
-
const epic = yield* epicOfTrace(tracePath, options.repo, dependencies)
|
|
145
|
-
return { tracePath, epicDir: epic?.dir, run: epic?.run }
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
const actionWords = (run: EpicRun | undefined): string | undefined => {
|
|
149
|
-
if (run === undefined) {
|
|
150
|
-
return undefined
|
|
151
|
-
}
|
|
152
|
-
switch (run.action) {
|
|
153
|
-
case "RunPlan":
|
|
154
|
-
return "run plan"
|
|
155
|
-
case "RunRound":
|
|
156
|
-
return `round ${run.round ?? "?"}`
|
|
157
|
-
case "PlanRound":
|
|
158
|
-
return `plan round ${run.round ?? "?"}`
|
|
159
|
-
case "Land":
|
|
160
|
-
return "land"
|
|
161
|
-
default:
|
|
162
|
-
return run.action
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** The tree before any event: the epic's board, when the trace belongs to one. */
|
|
167
|
-
const startingTree = (
|
|
168
|
-
target: WatchTarget,
|
|
169
|
-
files: PlainFileStoreShape,
|
|
170
|
-
idleAfterMs: number | undefined
|
|
171
|
-
): Effect.Effect<TreeState, FlowError> =>
|
|
172
|
-
Effect.gen(function* () {
|
|
173
|
-
const action = actionWords(target.run)
|
|
174
|
-
const idle = idleAfterMs === undefined ? {} : { idleAfterMs }
|
|
175
|
-
if (target.epicDir === undefined) {
|
|
176
|
-
return emptyTree({ ...idle, ...(action === undefined ? {} : { action }) })
|
|
177
|
-
}
|
|
178
|
-
const boardDir =
|
|
179
|
-
target.run?.round === undefined
|
|
180
|
-
? target.epicDir
|
|
181
|
-
: join(target.epicDir, "rounds", String(target.run.round))
|
|
182
|
-
const board = yield* loadVersioned(files, join(boardDir, "board.json"), BoardVersion, Board)
|
|
183
|
-
const epicId = target.epicDir.split("/").at(-1) ?? "epic"
|
|
184
|
-
return emptyTree({
|
|
185
|
-
title: board === undefined ? `epic ${epicId}` : board.title.replace(/^Epic:\s*/u, "epic "),
|
|
186
|
-
stories: (board?.items ?? []).map((item) => ({ id: item.id, status: item.status })),
|
|
187
|
-
...idle,
|
|
188
|
-
...(action === undefined ? {} : { action })
|
|
189
|
-
})
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
const readInputs = (
|
|
193
|
-
target: WatchTarget,
|
|
194
|
-
files: PlainFileStoreShape
|
|
195
|
-
): Effect.Effect<ReadonlyArray<TreeInput>, FlowError> =>
|
|
196
|
-
Effect.map(readTrace(files, target.tracePath), treeInputsOfTrace)
|
|
197
|
-
|
|
198
|
-
const pollInterval = Duration.millis(500)
|
|
199
|
-
const replayGapCap = 2_000
|
|
200
|
-
|
|
201
|
-
const draw = (
|
|
202
|
-
state: TreeState,
|
|
203
|
-
view: TreeView,
|
|
204
|
-
output: WatchOutput,
|
|
205
|
-
height?: number,
|
|
206
|
-
tail?: ReadonlyArray<string>
|
|
207
|
-
): string =>
|
|
208
|
-
renderTree(state, {
|
|
209
|
-
width: Math.max(90, output.columns() ?? 90),
|
|
210
|
-
colour: output.colour,
|
|
211
|
-
view,
|
|
212
|
-
...(height === undefined ? {} : { height }),
|
|
213
|
-
...(tail === undefined ? {} : { tail })
|
|
214
|
-
}).join("\n")
|
|
215
|
-
|
|
216
|
-
/** A full-screen frame: no taller than the terminal. */
|
|
217
|
-
const screenLines = (
|
|
218
|
-
state: TreeState,
|
|
219
|
-
view: TreeView,
|
|
220
|
-
output: WatchOutput,
|
|
221
|
-
tail?: ReadonlyArray<string>
|
|
222
|
-
): ReadonlyArray<string> => draw(state, view, output, output.rows(), tail).split("\n")
|
|
223
|
-
|
|
224
|
-
/** Rows the tail pane shows. */
|
|
225
|
-
const tailRows = 16
|
|
226
|
-
|
|
227
|
-
/** Where a trace's run kept its transcript: `.llm4ts/transcripts/<run-id>/`. */
|
|
228
|
-
const transcriptDirOf = (
|
|
229
|
-
repo: string,
|
|
230
|
-
target: WatchTarget,
|
|
231
|
-
files: PlainFileStoreShape
|
|
232
|
-
): Effect.Effect<string | undefined, FlowError> =>
|
|
233
|
-
Effect.map(readTrace(files, target.tracePath), (lines) => {
|
|
234
|
-
const runId = lines[0]?.runId
|
|
235
|
-
return runId === undefined ? undefined : join(repo, TraceDirectoryName, "transcripts", runId)
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
export const makeWatchProgram = Effect.fn("@llm4ts/runner/Watch.make")(function* (
|
|
239
|
-
options: WatchOptions,
|
|
240
|
-
dependencies: WatchDependencies
|
|
241
|
-
): Effect.fn.Return<void, FlowError | WatchTargetMissing> {
|
|
242
|
-
const target = yield* resolveTraceTarget(options, dependencies)
|
|
243
|
-
const { files, output } = dependencies
|
|
244
|
-
const fold = (inputs: ReadonlyArray<TreeInput>): Effect.Effect<TreeState, FlowError> =>
|
|
245
|
-
Effect.map(startingTree(target, files, options.idleAfterMs), (start) =>
|
|
246
|
-
inputs.reduce(reduceTree, start)
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
const transcripts = dependencies.transcripts ?? nodeTranscriptFiles
|
|
250
|
-
const transcriptDir = yield* transcriptDirOf(options.repo, target, files)
|
|
251
|
-
|
|
252
|
-
if (options.tail !== undefined) {
|
|
253
|
-
if (transcriptDir === undefined || transcripts.list(transcriptDir).length === 0) {
|
|
254
|
-
return yield* WatchTargetMissing.make({
|
|
255
|
-
message: "no transcript for this run — start it with llm4ts run … --transcript"
|
|
256
|
-
})
|
|
257
|
-
}
|
|
258
|
-
return yield* followTail(options.tail, transcriptDir, transcripts, output, () =>
|
|
259
|
-
Effect.map(readInputs(target, files), (inputs) =>
|
|
260
|
-
inputs.some((input) => input._tag === "RunEnded")
|
|
261
|
-
)
|
|
262
|
-
)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/** The open tail's lines, for the view's selection. */
|
|
266
|
-
const tailFor = (view: TreeView): Effect.Effect<ReadonlyArray<string> | undefined> => {
|
|
267
|
-
const selected = tailTargetOf(view)
|
|
268
|
-
return !view.tail || selected === undefined || transcriptDir === undefined
|
|
269
|
-
? Effect.succeed(undefined)
|
|
270
|
-
: Effect.map(loadTranscript(transcripts, transcriptDir, selected), (entries) =>
|
|
271
|
-
tailLinesOf(
|
|
272
|
-
entries,
|
|
273
|
-
selected,
|
|
274
|
-
view.tailRole,
|
|
275
|
-
Math.max(90, output.columns() ?? 90) - 32,
|
|
276
|
-
tailRows,
|
|
277
|
-
view.tailBack
|
|
278
|
-
)
|
|
279
|
-
)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (!output.interactive) {
|
|
283
|
-
const state = yield* Effect.flatMap(readInputs(target, files), fold)
|
|
284
|
-
return yield* output.write(`${draw(state, initialView, output)}\n`)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
yield* Effect.scoped(
|
|
288
|
-
Effect.gen(function* () {
|
|
289
|
-
const keys = yield* Queue.unbounded<string>()
|
|
290
|
-
yield* Effect.forkScoped(
|
|
291
|
-
Stream.runForEach(dependencies.keys, (key) => Queue.offer(keys, key))
|
|
292
|
-
)
|
|
293
|
-
const view = yield* Ref.make<TreeView>(initialView)
|
|
294
|
-
const all = yield* readInputs(target, files)
|
|
295
|
-
// Replay steps through the inputs on their timestamps; otherwise the
|
|
296
|
-
// whole trace is shown and re-read while the run goes on.
|
|
297
|
-
const shown = yield* Ref.make(options.replay === true ? Math.min(1, all.length) : all.length)
|
|
298
|
-
const inputs = yield* Ref.make(all)
|
|
299
|
-
const speed = Math.max(1, options.speed ?? 10)
|
|
300
|
-
|
|
301
|
-
/** Waits for a key or for `wait`; true when the key was `q`. */
|
|
302
|
-
const awaitKey = (wait: Duration.Duration): Effect.Effect<boolean> =>
|
|
303
|
-
Effect.raceFirst(
|
|
304
|
-
Effect.flatMap(Queue.take(keys), (key) =>
|
|
305
|
-
Effect.gen(function* () {
|
|
306
|
-
const state = yield* Effect.orDie(
|
|
307
|
-
Effect.flatMap(Ref.get(inputs), (current) =>
|
|
308
|
-
Effect.flatMap(Ref.get(shown), (count) => fold(current.slice(0, count)))
|
|
309
|
-
)
|
|
310
|
-
)
|
|
311
|
-
const next = onTreeKey(yield* Ref.get(view), key, state)
|
|
312
|
-
if (next === "quit") {
|
|
313
|
-
return true
|
|
314
|
-
}
|
|
315
|
-
yield* Ref.set(view, next)
|
|
316
|
-
return false
|
|
317
|
-
})
|
|
318
|
-
),
|
|
319
|
-
Effect.as(Effect.sleep(wait), false)
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
const painter = yield* makeScreenPainter
|
|
323
|
-
yield* output.write(enterScreen)
|
|
324
|
-
yield* Effect.gen(function* () {
|
|
325
|
-
while (true) {
|
|
326
|
-
const current = yield* Ref.get(inputs)
|
|
327
|
-
const count = yield* Ref.get(shown)
|
|
328
|
-
const folded = yield* fold(current.slice(0, count))
|
|
329
|
-
// Following a live run, the clock moves between events; a replay
|
|
330
|
-
// and a finished run keep the trace's own time.
|
|
331
|
-
const state =
|
|
332
|
-
options.replay !== true && folded.ended === undefined
|
|
333
|
-
? reduceTree(folded, { _tag: "Tick", at: yield* Clock.currentTimeMillis })
|
|
334
|
-
: folded
|
|
335
|
-
const text = yield* painter.paint(
|
|
336
|
-
screenLines(state, yield* Ref.get(view), output, yield* tailFor(yield* Ref.get(view))),
|
|
337
|
-
`${output.columns()}x${output.rows()}`
|
|
338
|
-
)
|
|
339
|
-
if (text.length > 0) {
|
|
340
|
-
yield* output.write(text)
|
|
341
|
-
}
|
|
342
|
-
const replaying = options.replay === true && count < current.length
|
|
343
|
-
const gap = replaying
|
|
344
|
-
? Math.min(replayGapCap, (current[count]?.at ?? 0) - (current[count - 1]?.at ?? 0)) /
|
|
345
|
-
speed
|
|
346
|
-
: Duration.toMillis(pollInterval)
|
|
347
|
-
if (yield* awaitKey(Duration.millis(Math.max(0, gap)))) {
|
|
348
|
-
return
|
|
349
|
-
}
|
|
350
|
-
if (replaying) {
|
|
351
|
-
yield* Ref.update(shown, (shownCount) => shownCount + 1)
|
|
352
|
-
} else if (state.ended === undefined) {
|
|
353
|
-
const reread = yield* readInputs(target, files)
|
|
354
|
-
yield* Ref.set(inputs, reread)
|
|
355
|
-
yield* Ref.set(shown, reread.length)
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}).pipe(Effect.ensuring(output.write(leaveScreen)))
|
|
359
|
-
})
|
|
360
|
-
)
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
// ── node wiring ─────────────────────────────────────────────────────────────
|
|
364
|
-
|
|
365
|
-
/** Folder names under an `epics/` directory; none when it is absent. */
|
|
366
|
-
export const nodeListEpics = (directory: string): ReadonlyArray<string> =>
|
|
367
|
-
existsSync(directory)
|
|
368
|
-
? readdirSync(directory).filter((name) => statSync(join(directory, name)).isDirectory())
|
|
369
|
-
: []
|
|
370
|
-
|
|
371
|
-
export const nodeTraceSources: TraceSources = {
|
|
372
|
-
files: nodePlainFileStore,
|
|
373
|
-
listTraces: nodeListTraces,
|
|
374
|
-
listEpics: nodeListEpics
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
export const nodeWatchDependencies = (
|
|
378
|
-
environment: Readonly<Record<string, string | undefined>> = process.env
|
|
379
|
-
): WatchDependencies => {
|
|
380
|
-
const interactive = process.stdout.isTTY === true && process.stdin.isTTY === true
|
|
381
|
-
return {
|
|
382
|
-
files: nodePlainFileStore,
|
|
383
|
-
listTraces: nodeListTraces,
|
|
384
|
-
listEpics: nodeListEpics,
|
|
385
|
-
output: {
|
|
386
|
-
write: (text) => Effect.sync(() => void process.stdout.write(text)),
|
|
387
|
-
columns: () => process.stdout.columns,
|
|
388
|
-
rows: () => process.stdout.rows,
|
|
389
|
-
interactive,
|
|
390
|
-
colour: process.stdout.isTTY === true && environment.NO_COLOR === undefined
|
|
391
|
-
},
|
|
392
|
-
keys: nodeTreeKeys("detach")
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// ── tail ────────────────────────────────────────────────────────────────────
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* `llm4ts watch --tail <story|executor>`: the transcript printed as it grows,
|
|
400
|
-
* like `tail -f` — a call's reply streams in as written — until the run ends.
|
|
401
|
-
* A name with its own file is a story; any other is an executor.
|
|
402
|
-
*/
|
|
403
|
-
const followTail = (
|
|
404
|
-
name: string,
|
|
405
|
-
directory: string,
|
|
406
|
-
transcripts: TranscriptFiles,
|
|
407
|
-
output: WatchOutput,
|
|
408
|
-
ended: () => Effect.Effect<boolean, FlowError>
|
|
409
|
-
): Effect.Effect<void, FlowError> =>
|
|
410
|
-
Effect.gen(function* () {
|
|
411
|
-
const target: TailTarget = transcripts.list(directory).includes(transcriptFileOf(name))
|
|
412
|
-
? { lane: name }
|
|
413
|
-
: { executor: name }
|
|
414
|
-
const width = Math.max(60, output.columns() ?? 100)
|
|
415
|
-
let shown = 0
|
|
416
|
-
let start: number | undefined
|
|
417
|
-
const wanted = new Set<string>()
|
|
418
|
-
let replying: string | undefined
|
|
419
|
-
while (true) {
|
|
420
|
-
const done = yield* ended()
|
|
421
|
-
const entries = (yield* loadTranscript(transcripts, directory, target)) ?? []
|
|
422
|
-
for (const entry of entries.slice(shown)) {
|
|
423
|
-
start = start ?? entry.at
|
|
424
|
-
if (entry._tag === "Call" && ("lane" in target || entry.executor === target.executor)) {
|
|
425
|
-
wanted.add(entry.call)
|
|
426
|
-
}
|
|
427
|
-
if (!wanted.has(entry.call)) {
|
|
428
|
-
continue
|
|
429
|
-
}
|
|
430
|
-
if (entry._tag === "Reply") {
|
|
431
|
-
// A reply streams: its text as it arrived, on one growing paragraph.
|
|
432
|
-
yield* output.write(`${replying === entry.call ? "" : "◀ "}${entry.text}`)
|
|
433
|
-
replying = entry.call
|
|
434
|
-
continue
|
|
435
|
-
}
|
|
436
|
-
const lines = renderTranscript([entry], { width, since: start })
|
|
437
|
-
yield* output.write(`${replying === undefined ? "" : "\n"}${lines.join("\n")}\n`)
|
|
438
|
-
replying = undefined
|
|
439
|
-
}
|
|
440
|
-
shown = entries.length
|
|
441
|
-
if (done) {
|
|
442
|
-
return
|
|
443
|
-
}
|
|
444
|
-
yield* Effect.sleep(pollInterval)
|
|
445
|
-
}
|
|
446
|
-
})
|