@llm4ts/runner 2.21.1 → 2.22.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/src/Watch.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readdirSync, existsSync, statSync } from "node:fs"
2
2
  import { join } from "node:path"
3
+ import * as Clock from "effect/Clock"
3
4
  import * as Duration from "effect/Duration"
4
5
  import * as Effect from "effect/Effect"
5
6
  import * as Queue from "effect/Queue"
@@ -42,6 +43,8 @@ export interface WatchOptions {
42
43
  readonly replay?: boolean
43
44
  /** Replay speed-up (default 10). */
44
45
  readonly speed?: number
46
+ /** A live lane with no event for this long is marked idle (`LLM4TS_IDLE_AFTER`). */
47
+ readonly idleAfterMs?: number
45
48
  }
46
49
 
47
50
  export interface WatchOutput {
@@ -68,7 +71,13 @@ export class WatchTargetMissing extends Schema.TaggedError<WatchTargetMissing>()
68
71
  { message: Schema.String }
69
72
  ) {}
70
73
 
71
- interface WatchTarget {
74
+ /** Where a run's trace and its epic are read from. */
75
+ export type TraceSources = Pick<WatchDependencies, "files" | "listTraces" | "listEpics">
76
+
77
+ /** A trace to read: given, an epic's latest run, or the newest in the repository. */
78
+ export type TraceChoice = Pick<WatchOptions, "repo" | "trace" | "epic">
79
+
80
+ export interface WatchTarget {
72
81
  readonly tracePath: string
73
82
  readonly epicDir: string | undefined
74
83
  readonly run: EpicRun | undefined
@@ -80,7 +89,7 @@ const epicsDirOf = (repo: string): string => join(repo, TraceDirectoryName, "epi
80
89
  const epicOfTrace = (
81
90
  tracePath: string,
82
91
  repo: string,
83
- dependencies: WatchDependencies
92
+ dependencies: TraceSources
84
93
  ): Effect.Effect<{ readonly dir: string; readonly run: EpicRun } | undefined, FlowError> =>
85
94
  Effect.gen(function* () {
86
95
  const epicsDir = epicsDirOf(repo)
@@ -96,9 +105,9 @@ const epicOfTrace = (
96
105
  return undefined
97
106
  })
98
107
 
99
- const resolveTarget = (
100
- options: WatchOptions,
101
- dependencies: WatchDependencies
108
+ export const resolveTraceTarget = (
109
+ options: TraceChoice,
110
+ dependencies: TraceSources
102
111
  ): Effect.Effect<WatchTarget, FlowError | WatchTargetMissing> =>
103
112
  Effect.gen(function* () {
104
113
  if (options.epic !== undefined) {
@@ -143,12 +152,14 @@ const actionWords = (run: EpicRun | undefined): string | undefined => {
143
152
  /** The tree before any event: the epic's board, when the trace belongs to one. */
144
153
  const startingTree = (
145
154
  target: WatchTarget,
146
- files: PlainFileStoreShape
155
+ files: PlainFileStoreShape,
156
+ idleAfterMs: number | undefined
147
157
  ): Effect.Effect<TreeState, FlowError> =>
148
158
  Effect.gen(function* () {
149
159
  const action = actionWords(target.run)
160
+ const idle = idleAfterMs === undefined ? {} : { idleAfterMs }
150
161
  if (target.epicDir === undefined) {
151
- return emptyTree(action === undefined ? {} : { action })
162
+ return emptyTree({ ...idle, ...(action === undefined ? {} : { action }) })
152
163
  }
153
164
  const boardDir =
154
165
  target.run?.round === undefined
@@ -159,6 +170,7 @@ const startingTree = (
159
170
  return emptyTree({
160
171
  title: board === undefined ? `epic ${epicId}` : board.title.replace(/^Epic:\s*/u, "epic "),
161
172
  stories: (board?.items ?? []).map((item) => ({ id: item.id, status: item.status })),
173
+ ...idle,
162
174
  ...(action === undefined ? {} : { action })
163
175
  })
164
176
  })
@@ -191,10 +203,12 @@ export const makeWatchProgram = Effect.fn("@llm4ts/runner/Watch.make")(function*
191
203
  options: WatchOptions,
192
204
  dependencies: WatchDependencies
193
205
  ): Effect.fn.Return<void, FlowError | WatchTargetMissing> {
194
- const target = yield* resolveTarget(options, dependencies)
206
+ const target = yield* resolveTraceTarget(options, dependencies)
195
207
  const { files, output } = dependencies
196
208
  const fold = (inputs: ReadonlyArray<TreeInput>): Effect.Effect<TreeState, FlowError> =>
197
- Effect.map(startingTree(target, files), (start) => inputs.reduce(reduceTree, start))
209
+ Effect.map(startingTree(target, files, options.idleAfterMs), (start) =>
210
+ inputs.reduce(reduceTree, start)
211
+ )
198
212
 
199
213
  if (!output.interactive) {
200
214
  const state = yield* Effect.flatMap(readInputs(target, files), fold)
@@ -242,7 +256,13 @@ export const makeWatchProgram = Effect.fn("@llm4ts/runner/Watch.make")(function*
242
256
  while (true) {
243
257
  const current = yield* Ref.get(inputs)
244
258
  const count = yield* Ref.get(shown)
245
- const state = yield* fold(current.slice(0, count))
259
+ const folded = yield* fold(current.slice(0, count))
260
+ // Following a live run, the clock moves between events; a replay
261
+ // and a finished run keep the trace's own time.
262
+ const state =
263
+ options.replay !== true && folded.ended === undefined
264
+ ? reduceTree(folded, { _tag: "Tick", at: yield* Clock.currentTimeMillis })
265
+ : folded
246
266
  const text = yield* painter.paint(
247
267
  screenLines(state, yield* Ref.get(view), output),
248
268
  `${output.columns()}x${output.rows()}`
@@ -278,6 +298,12 @@ const nodeListEpics = (directory: string): ReadonlyArray<string> =>
278
298
  ? readdirSync(directory).filter((name) => statSync(join(directory, name)).isDirectory())
279
299
  : []
280
300
 
301
+ export const nodeTraceSources: TraceSources = {
302
+ files: nodePlainFileStore,
303
+ listTraces: nodeListTraces,
304
+ listEpics: nodeListEpics
305
+ }
306
+
281
307
  export const nodeWatchDependencies = (
282
308
  environment: Readonly<Record<string, string | undefined>> = process.env
283
309
  ): WatchDependencies => {