@llm4ts/runner 2.21.0 → 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/dist/AgentTree.d.ts +32 -0
- package/dist/AgentTree.d.ts.map +1 -1
- package/dist/AgentTree.js +94 -10
- package/dist/AgentTree.js.map +1 -1
- package/dist/AgentTreeSurface.d.ts +22 -1
- package/dist/AgentTreeSurface.d.ts.map +1 -1
- package/dist/AgentTreeSurface.js +50 -11
- package/dist/AgentTreeSurface.js.map +1 -1
- package/dist/FlowRunner.d.ts.map +1 -1
- package/dist/FlowRunner.js +10 -7
- package/dist/FlowRunner.js.map +1 -1
- package/dist/Profile.d.ts +166 -0
- package/dist/Profile.d.ts.map +1 -0
- package/dist/Profile.js +497 -0
- package/dist/Profile.js.map +1 -0
- package/dist/Terminal.d.ts.map +1 -1
- package/dist/Terminal.js +2 -0
- package/dist/Terminal.js.map +1 -1
- package/dist/Watch.d.ts +15 -0
- package/dist/Watch.d.ts.map +1 -1
- package/dist/Watch.js +26 -12
- package/dist/Watch.js.map +1 -1
- package/package.json +4 -3
- package/src/AgentTree.ts +136 -10
- package/src/AgentTreeSurface.ts +76 -17
- package/src/FlowRunner.ts +19 -11
- package/src/Profile.ts +645 -0
- package/src/Terminal.ts +2 -0
- package/src/Watch.ts +50 -17
package/src/AgentTreeSurface.ts
CHANGED
|
@@ -38,18 +38,69 @@ export interface AgentTreeHost {
|
|
|
38
38
|
readonly close: Effect.Effect<void>
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
41
|
+
const beginUpdate = "\u001b[?2026h"
|
|
42
|
+
const endUpdate = "\u001b[?2026l"
|
|
43
|
+
const clearScreen = "\u001b[H\u001b[2J"
|
|
44
|
+
const at = (row: number): string => `\u001b[${row};1H`
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* What to send to turn the screen showing `previous` into `next`: the whole
|
|
48
|
+
* frame after a clear when there is no previous one (first paint, resize, a
|
|
49
|
+
* prompt in between), otherwise only the rows that changed — never a clear,
|
|
50
|
+
* which is what flickers under tmux and Windows terminals — and nothing at
|
|
51
|
+
* all when the frame is the same. Each paint is one synchronized update, which
|
|
52
|
+
* terminals that know it show at once and the rest ignore. Rows are drawn
|
|
53
|
+
* at full width, so an overwritten row leaves nothing behind.
|
|
54
|
+
*/
|
|
55
|
+
export const paintFrame = (
|
|
56
|
+
previous: ReadonlyArray<string> | undefined,
|
|
57
|
+
next: ReadonlyArray<string>
|
|
58
|
+
): string => {
|
|
59
|
+
if (previous === undefined) {
|
|
60
|
+
return `${beginUpdate}${clearScreen}${next.map((line, row) => `${at(row + 1)}${line}`).join("")}${endUpdate}`
|
|
61
|
+
}
|
|
62
|
+
const changed = next.flatMap((line, row) =>
|
|
63
|
+
line === previous[row] ? [] : [`${at(row + 1)}${line}`]
|
|
64
|
+
)
|
|
65
|
+
const shorter = next.length < previous.length ? `${at(next.length + 1)}\u001b[J` : ""
|
|
66
|
+
return changed.length === 0 && shorter === ""
|
|
67
|
+
? ""
|
|
68
|
+
: `${beginUpdate}${changed.join("")}${shorter}${endUpdate}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A full-screen view's painter: remembers what is on screen, and at which size. */
|
|
72
|
+
export interface ScreenPainter {
|
|
73
|
+
/** What to write to show `lines` at terminal size `size`; empty when nothing changed. */
|
|
74
|
+
readonly paint: (lines: ReadonlyArray<string>, size: string) => Effect.Effect<string>
|
|
75
|
+
/** The screen was cleared or covered: the next paint is a whole one. */
|
|
76
|
+
readonly reset: Effect.Effect<void>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const makeScreenPainter: Effect.Effect<ScreenPainter> = Effect.map(
|
|
80
|
+
Ref.make<{ readonly lines: ReadonlyArray<string>; readonly size: string } | undefined>(undefined),
|
|
81
|
+
(shown) => ({
|
|
82
|
+
paint: (lines, size) =>
|
|
83
|
+
Ref.modify(shown, (previous) => [
|
|
84
|
+
paintFrame(previous?.size === size ? previous.lines : undefined, lines),
|
|
85
|
+
{ lines, size }
|
|
86
|
+
]),
|
|
87
|
+
reset: Ref.set(shown, undefined)
|
|
88
|
+
})
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
/** The alternate screen, cursor hidden, no auto-wrap: a full-width last row never scrolls. */
|
|
92
|
+
export const enterScreen = "\u001b[?1049h\u001b[?25l\u001b[?7l"
|
|
93
|
+
export const leaveScreen = "\u001b[?7h\u001b[?25h\u001b[?1049l"
|
|
44
94
|
const redrawEvery = "100 millis"
|
|
45
95
|
|
|
46
96
|
export const makeAgentTreeHost = Effect.fn("@llm4ts/runner/AgentTreeSurface.make")(function* (
|
|
47
97
|
hub: FlowEventHub,
|
|
48
98
|
classic: TerminalSurface,
|
|
49
99
|
output: AgentTreeOutput,
|
|
50
|
-
keys: Stream.Stream<string
|
|
100
|
+
keys: Stream.Stream<string>,
|
|
101
|
+
idleAfterMs?: number
|
|
51
102
|
): Effect.fn.Return<AgentTreeHost, never, Scope.Scope> {
|
|
52
|
-
const state = yield* Ref.make(emptyTree())
|
|
103
|
+
const state = yield* Ref.make(emptyTree(idleAfterMs === undefined ? {} : { idleAfterMs }))
|
|
53
104
|
const view = yield* Ref.make<TreeView>(initialView)
|
|
54
105
|
const active = yield* Ref.make(true)
|
|
55
106
|
const consumed = yield* Ref.make(0)
|
|
@@ -65,19 +116,28 @@ export const makeAgentTreeHost = Effect.fn("@llm4ts/runner/AgentTreeSurface.make
|
|
|
65
116
|
Effect.forkScoped
|
|
66
117
|
)
|
|
67
118
|
|
|
68
|
-
const render = (rows: number | undefined): Effect.Effect<string
|
|
119
|
+
const render = (rows: number | undefined): Effect.Effect<ReadonlyArray<string>> =>
|
|
69
120
|
Effect.gen(function* () {
|
|
70
|
-
|
|
121
|
+
return renderTree(yield* Ref.get(state), {
|
|
71
122
|
width: Math.max(90, output.columns() ?? 90),
|
|
72
123
|
colour: output.colour,
|
|
73
124
|
view: yield* Ref.get(view),
|
|
74
125
|
...(rows === undefined ? {} : { height: rows })
|
|
75
126
|
})
|
|
76
|
-
return lines.join("\n")
|
|
77
127
|
})
|
|
78
128
|
/** The full screen fits the terminal; the last frame left behind is whole. */
|
|
79
|
-
const frame = render(undefined)
|
|
80
|
-
const
|
|
129
|
+
const frame = Effect.map(render(undefined), (lines) => lines.join("\n"))
|
|
130
|
+
const painter = yield* makeScreenPainter
|
|
131
|
+
const repaint = Effect.gen(function* () {
|
|
132
|
+
// The clock moves between events: lane timers and the idle marker run.
|
|
133
|
+
const now = yield* Clock.currentTimeMillis
|
|
134
|
+
yield* Ref.update(state, (current) => reduceTree(current, { _tag: "Tick", at: now }))
|
|
135
|
+
const rows = output.rows()
|
|
136
|
+
const text = yield* painter.paint(yield* render(rows), `${output.columns()}x${rows}`)
|
|
137
|
+
if (text.length > 0) {
|
|
138
|
+
yield* output.write(text)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
81
141
|
|
|
82
142
|
/** Runs `effect` only while the tree has the screen, under the drawing lock. */
|
|
83
143
|
const whileShown = (effect: Effect.Effect<void>): Effect.Effect<void> =>
|
|
@@ -93,12 +153,9 @@ export const makeAgentTreeHost = Effect.fn("@llm4ts/runner/AgentTreeSurface.make
|
|
|
93
153
|
|
|
94
154
|
yield* output.write(enterScreen)
|
|
95
155
|
yield* Effect.addFinalizer(() => leave(false))
|
|
96
|
-
yield* Effect.forever(
|
|
97
|
-
Effect.
|
|
98
|
-
|
|
99
|
-
whileShown(Effect.flatMap(screen, (text) => output.write(`${home}${text}`)))
|
|
100
|
-
)
|
|
101
|
-
).pipe(Effect.forkScoped)
|
|
156
|
+
yield* Effect.forever(Effect.andThen(Effect.sleep(redrawEvery), whileShown(repaint))).pipe(
|
|
157
|
+
Effect.forkScoped
|
|
158
|
+
)
|
|
102
159
|
yield* Stream.runForEach(keys, (key) =>
|
|
103
160
|
Effect.gen(function* () {
|
|
104
161
|
const next = onTreeKey(yield* Ref.get(view), key, yield* Ref.get(state))
|
|
@@ -123,7 +180,9 @@ export const makeAgentTreeHost = Effect.fn("@llm4ts/runner/AgentTreeSurface.make
|
|
|
123
180
|
lock.withPermit(Effect.andThen(Ref.set(active, false), output.write(leaveScreen))),
|
|
124
181
|
() => classic.suspend(effect),
|
|
125
182
|
() =>
|
|
126
|
-
lock.withPermit(
|
|
183
|
+
lock.withPermit(
|
|
184
|
+
Effect.all([Ref.set(active, true), painter.reset, output.write(enterScreen)])
|
|
185
|
+
)
|
|
127
186
|
)
|
|
128
187
|
: classic.suspend(effect)
|
|
129
188
|
)
|
package/src/FlowRunner.ts
CHANGED
|
@@ -58,6 +58,8 @@ import {
|
|
|
58
58
|
} from "@llm4ts/flow/FlowEvents"
|
|
59
59
|
import { FlowContext, type ContextOptions, type FlowContextShape } from "@llm4ts/flow/FlowContext"
|
|
60
60
|
import { makeFlowRecorder, type RunOutcome } from "@llm4ts/flow/FlowRecorder"
|
|
61
|
+
import { timedSeat } from "@llm4ts/flow/Timing"
|
|
62
|
+
import { idleAfterFrom } from "./AgentTree.ts"
|
|
61
63
|
import { makeAgentTreeHost, nodeTreeKeys } from "./AgentTreeSurface.ts"
|
|
62
64
|
import { makeJudgmentLog, type JudgmentLogShape } from "@llm4ts/flow/JudgmentLog"
|
|
63
65
|
import { makeGitHubTool } from "@llm4ts/flow/GitHubTool"
|
|
@@ -454,13 +456,14 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
454
456
|
laneEvents: FlowEventsShape = events
|
|
455
457
|
): FlowContextShape =>
|
|
456
458
|
FlowContext.of({
|
|
457
|
-
|
|
458
|
-
|
|
459
|
+
// Every call is timed on the lane's events, under its role.
|
|
460
|
+
reasoning: timedSeat(seats.reasoning, laneEvents, "reasoning"),
|
|
461
|
+
coder: { ...seats.coder, ...timedSeat(seats.coder, laneEvents, "coder") },
|
|
459
462
|
judgment: seats.judgment,
|
|
460
463
|
git: makeGitTool(dependencies.process, workDir, laneEvents),
|
|
461
464
|
hosting: makeGitHubTool(dependencies.process, workDir, laneEvents),
|
|
462
465
|
events: laneEvents,
|
|
463
|
-
reviewers: seats.reviewers,
|
|
466
|
+
reviewers: seats.reviewers.map((reviewer) => timedSeat(reviewer, laneEvents, "reviewer")),
|
|
464
467
|
coderCapabilities: seats.coder.capabilities,
|
|
465
468
|
userPrompt: options.userPrompt,
|
|
466
469
|
workDir,
|
|
@@ -618,12 +621,16 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
618
621
|
laneEvents: FlowEventsShape
|
|
619
622
|
): RosterView => ({
|
|
620
623
|
forRole: (role) =>
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
624
|
+
timedSeat(
|
|
625
|
+
rosterSeat(roster, source, role, workDir, {
|
|
626
|
+
events: laneEvents,
|
|
627
|
+
avoid: Effect.map(held.executor, (id) => (id === undefined ? [] : [id])),
|
|
628
|
+
borrow: held.lease,
|
|
629
|
+
label
|
|
630
|
+
}),
|
|
631
|
+
laneEvents,
|
|
632
|
+
role
|
|
633
|
+
),
|
|
627
634
|
available: roster.available,
|
|
628
635
|
slots: roster.slots,
|
|
629
636
|
executor: held.executor,
|
|
@@ -647,7 +654,7 @@ export const makeFlowRunnerContext = Effect.fn("@llm4ts/runner/FlowRunner.makeCo
|
|
|
647
654
|
const judgment = yield* judgmentFor(judgmentSeat, judgmentConfig, runEnvironment)
|
|
648
655
|
return FlowContext.of({
|
|
649
656
|
reasoning: view.forRole(rebind ? "planner" : "reviewer"),
|
|
650
|
-
coder: held.service,
|
|
657
|
+
coder: timedSeat(held.service, laneEvents, "coder"),
|
|
651
658
|
judgment,
|
|
652
659
|
git: makeGitTool(dependencies.process, workDir, laneEvents),
|
|
653
660
|
hosting: makeGitHubTool(dependencies.process, workDir, laneEvents),
|
|
@@ -765,7 +772,8 @@ export const runWithBundle = Effect.fn("@llm4ts/runner/FlowRunner.runWithBundle"
|
|
|
765
772
|
rows: () => process.stdout.rows,
|
|
766
773
|
colour: true
|
|
767
774
|
},
|
|
768
|
-
nodeTreeKeys("interrupt")
|
|
775
|
+
nodeTreeKeys("interrupt"),
|
|
776
|
+
idleAfterFrom(environment)
|
|
769
777
|
)
|
|
770
778
|
: undefined
|
|
771
779
|
const surface = tree?.surface ?? classic
|