agent-horde 0.1.0 → 0.1.1
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 +2 -2
- package/docs/REFERENCE.md +1 -1
- package/package.json +1 -1
- package/src/run-graph-svg.ts +70 -11
- package/src/run-graph.ts +13 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<a href="https://www.npmjs.com/package/agent-horde"><img src="https://img.shields.io/npm/v/agent-horde" alt="npm version"></a>
|
|
9
|
-
<a href="LICENSE"><img src="https://img.shields.io/
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
|
|
10
10
|
<img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="Node 20 or newer">
|
|
11
11
|
<a href="https://pi.dev/packages"><img src="https://img.shields.io/badge/pi-package-6f42c1" alt="pi package"></a>
|
|
12
12
|
<a href="https://github.com/eliyastein/agent-horde/issues"><img src="https://img.shields.io/github/issues/eliyastein/agent-horde" alt="open issues"></a>
|
|
@@ -88,7 +88,7 @@ pi install npm:agent-horde
|
|
|
88
88
|
Or from the repository, pinned to a tag:
|
|
89
89
|
|
|
90
90
|
```
|
|
91
|
-
pi install git:github.com/eliyastein/agent-horde@v0.1.
|
|
91
|
+
pi install git:github.com/eliyastein/agent-horde@v0.1.1
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
To hack on it, clone and install in place. pi loads the files from the clone, so edits take effect
|
package/docs/REFERENCE.md
CHANGED
|
@@ -81,7 +81,7 @@ Every page has a JSON twin under `/api/<same path>`.
|
|
|
81
81
|
| `/runs/<id>/sessions[/<file>[/<i>]]` | session files not mapped to a member (runs older than per-member session dirs) |
|
|
82
82
|
| `/runs/<id>/files/<NAME>` | any run document; 404 until written |
|
|
83
83
|
| `/runs/<id>/collaboration` | messages, review scope, artifact acceptance, resources, event audit |
|
|
84
|
-
| `/runs/<id>/graph` | the run as a swimlane: one lane per member plus the parent, phase bands, every event a link to its ledger entry (`?w=<px>`, `?from=<s>&to=<s>`) |
|
|
84
|
+
| `/runs/<id>/graph` | the run as a swimlane: one lane per member plus the parent, phase bands, every event a link to its ledger entry (`?w=<px>`, `?from=<s>&to=<s>`); the axis is real time until the last member exits, then the synthesis phases share a compressed tail |
|
|
85
85
|
| `/runs/<id>/graph.mmd` | the same run as a Mermaid flowchart with absolute dashboard links (text/plain) |
|
|
86
86
|
| `/ideas`, `/board`, `/findings`, `/claims`, `/facts`, `/resources` | the shared ledgers; `?run=<id>\|all`, `?view=clusters`, `?owner=<m>` filter them |
|
|
87
87
|
|
package/package.json
CHANGED
package/src/run-graph-svg.ts
CHANGED
|
@@ -21,6 +21,9 @@ const HEADER_H = 36;
|
|
|
21
21
|
const AXIS_H = 18;
|
|
22
22
|
const TICK_STEPS_MS = [5_000, 10_000, 15_000, 30_000, 60_000, 120_000, 300_000, 600_000, 900_000, 1_800_000, 3_600_000, 7_200_000];
|
|
23
23
|
const MAX_TICKS = 14;
|
|
24
|
+
/** Share of the plot for everything after the last member exits, when that tail would otherwise dominate. */
|
|
25
|
+
const TAIL_FRACTION = 0.15;
|
|
26
|
+
const TAIL_AXIS_H = 30;
|
|
24
27
|
|
|
25
28
|
const TONE_COLORS: Record<EdgeTone, string> = {
|
|
26
29
|
structure: "#000", collaboration: "#1f5fbf", verification: "#1a7f37", negative: "#c62828", lifecycle: "#888",
|
|
@@ -57,13 +60,49 @@ interface LaneBand {
|
|
|
57
60
|
interface Layout {
|
|
58
61
|
width: number;
|
|
59
62
|
height: number;
|
|
63
|
+
axisH: number;
|
|
60
64
|
t0: number;
|
|
61
65
|
t1: number;
|
|
66
|
+
cut?: number | undefined;
|
|
62
67
|
bands: LaneBand[];
|
|
63
68
|
placed: Map<string, Placed>;
|
|
64
69
|
x: (t: number) => number;
|
|
65
70
|
}
|
|
66
71
|
|
|
72
|
+
export interface TimeScale {
|
|
73
|
+
t0: number;
|
|
74
|
+
t1: number;
|
|
75
|
+
/** Where the axis switches from real time to the compressed tail; absent when the axis is linear. */
|
|
76
|
+
cut?: number | undefined;
|
|
77
|
+
x: (t: number) => number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function unit(value: number): number {
|
|
81
|
+
return Math.min(Math.max(0, value), 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Real time until the last member exits. The synthesis that follows can outlast discovery, so it
|
|
86
|
+
* gets a fixed share of the width instead of squeezing the members' work into the left margin.
|
|
87
|
+
* The cut applies as soon as a parent phase starts after the members are done, so a live run's
|
|
88
|
+
* main axis freezes the moment synthesis begins; without such a phase only a long tail triggers it.
|
|
89
|
+
*/
|
|
90
|
+
export function graphTimeScale(graph: RunGraph, window: GraphWindow): TimeScale {
|
|
91
|
+
const { t0, t1 } = windowOf(graph, window);
|
|
92
|
+
const plotW = window.width;
|
|
93
|
+
const cut = graph.membersEnd;
|
|
94
|
+
const phaseAfterMembers = graph.phases.some((phase) => phase.from >= cut && phase.from < t1);
|
|
95
|
+
const longTail = (t1 - cut) / (t1 - t0) > TAIL_FRACTION;
|
|
96
|
+
if (cut <= t0 || cut >= t1 || (!phaseAfterMembers && !longTail)) {
|
|
97
|
+
return { t0, t1, x: (t) => LABEL_W + unit((t - t0) / (t1 - t0)) * plotW };
|
|
98
|
+
}
|
|
99
|
+
const mainW = plotW * (1 - TAIL_FRACTION);
|
|
100
|
+
const tailW = plotW - mainW;
|
|
101
|
+
const x = (t: number): number =>
|
|
102
|
+
t <= cut ? LABEL_W + unit((t - t0) / (cut - t0)) * mainW : LABEL_W + mainW + unit((t - cut) / (t1 - cut)) * tailW;
|
|
103
|
+
return { t0, t1, cut, x };
|
|
104
|
+
}
|
|
105
|
+
|
|
67
106
|
function nodeWidth(node: GraphNode): number {
|
|
68
107
|
return node.marker ? DOT_R * 2 : Math.max(MIN_BOX_W, Math.round(node.label.length * CHAR_W) + 8);
|
|
69
108
|
}
|
|
@@ -91,9 +130,8 @@ function packLane(nodes: GraphNode[], x: (t: number) => number, top: number): {
|
|
|
91
130
|
}
|
|
92
131
|
|
|
93
132
|
function layoutGraph(graph: RunGraph, window: GraphWindow): Layout {
|
|
94
|
-
const { t0, t1 } =
|
|
133
|
+
const { t0, t1, cut, x } = graphTimeScale(graph, window);
|
|
95
134
|
const plotW = window.width;
|
|
96
|
-
const x = (t: number): number => LABEL_W + Math.min(Math.max(0, (t - t0) / (t1 - t0)), 1) * plotW;
|
|
97
135
|
const visible = graph.nodes.filter((node) => node.at >= t0 && node.at <= t1).sort((a, b) => a.at - b.at);
|
|
98
136
|
const placed = new Map<string, Placed>();
|
|
99
137
|
const bands: LaneBand[] = [];
|
|
@@ -106,7 +144,8 @@ function layoutGraph(graph: RunGraph, window: GraphWindow): Layout {
|
|
|
106
144
|
bands.push({ lane, y, h });
|
|
107
145
|
y += h;
|
|
108
146
|
}
|
|
109
|
-
|
|
147
|
+
const axisH = cut === undefined ? AXIS_H : TAIL_AXIS_H;
|
|
148
|
+
return { width: LABEL_W + plotW + PAD, height: y + axisH, axisH, t0, t1, cut, bands, placed, x };
|
|
110
149
|
}
|
|
111
150
|
|
|
112
151
|
function renderDefs(): string {
|
|
@@ -118,7 +157,7 @@ function renderDefs(): string {
|
|
|
118
157
|
}
|
|
119
158
|
|
|
120
159
|
function renderPhases(graph: RunGraph, layout: Layout): string {
|
|
121
|
-
const bottom = layout.height -
|
|
160
|
+
const bottom = layout.height - layout.axisH;
|
|
122
161
|
return graph.phases
|
|
123
162
|
.map((phase, index) => {
|
|
124
163
|
const from = Math.max(phase.from, layout.t0);
|
|
@@ -146,17 +185,36 @@ function renderLanes(layout: Layout): string {
|
|
|
146
185
|
.join("");
|
|
147
186
|
}
|
|
148
187
|
|
|
188
|
+
function tick(graph: RunGraph, layout: Layout, t: number, cls: string): string {
|
|
189
|
+
const x = layout.x(t);
|
|
190
|
+
const y = layout.height - layout.axisH;
|
|
191
|
+
const anchor = t >= layout.t1 ? ` text-anchor="end" x="${px(x - 2)}"` : ` x="${px(x + 2)}"`;
|
|
192
|
+
return (
|
|
193
|
+
`<line class="tick" x1="${px(x)}" y1="${HEADER_H}" x2="${px(x)}" y2="${px(y)}"/>` +
|
|
194
|
+
`<text${anchor} y="${px(y + 12)}" class="${cls}">+${e(formatElapsed(t - graph.start))}</text>`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Regular ticks up to the cut; past it only phase boundaries and the end, since the scale differs. */
|
|
149
199
|
function renderAxis(graph: RunGraph, layout: Layout): string {
|
|
150
|
-
const
|
|
200
|
+
const mainEnd = layout.cut ?? layout.t1;
|
|
201
|
+
const span = mainEnd - layout.t0;
|
|
151
202
|
const step = TICK_STEPS_MS.find((candidate) => span / candidate <= MAX_TICKS) ?? TICK_STEPS_MS[TICK_STEPS_MS.length - 1]!;
|
|
152
203
|
const firstTick = graph.start + Math.ceil((layout.t0 - graph.start) / step) * step;
|
|
153
204
|
const parts: string[] = [];
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
205
|
+
for (let t = firstTick; t <= mainEnd; t += step) parts.push(tick(graph, layout, t, "tick-label"));
|
|
206
|
+
if (layout.cut === undefined) return parts.join("");
|
|
207
|
+
const bottom = layout.height - layout.axisH;
|
|
208
|
+
const cx = layout.x(layout.cut);
|
|
209
|
+
parts.push(`<line class="cut" x1="${px(cx)}" y1="${HEADER_H - 16}" x2="${px(cx)}" y2="${px(bottom)}"/>`);
|
|
210
|
+
const note = `members done +${formatElapsed(layout.cut - graph.start)}; the rest is compressed`;
|
|
211
|
+
parts.push(`<text x="${px(cx - 3)}" y="${px(bottom + 24)}" text-anchor="end" class="tick-label tail">${e(note)}</text>`);
|
|
212
|
+
// One tick per distinct second: phase boundaries and the end often sit milliseconds apart.
|
|
213
|
+
const { cut, t1 } = layout;
|
|
214
|
+
const tailTicks = new Map<string, number>();
|
|
215
|
+
const candidates = graph.phases.flatMap((phase) => [phase.from, phase.to]).filter((t) => t > cut && t < t1);
|
|
216
|
+
for (const t of [...candidates, layout.t1]) tailTicks.set(formatElapsed(t - graph.start), t);
|
|
217
|
+
for (const t of [...tailTicks.values()].sort((a, b) => a - b)) parts.push(tick(graph, layout, t, "tick-label tail"));
|
|
160
218
|
return parts.join("");
|
|
161
219
|
}
|
|
162
220
|
|
|
@@ -216,6 +274,7 @@ export const GRAPH_CSS =
|
|
|
216
274
|
".graph{overflow:auto;border:1px solid #000;margin:4px 0;background:#fff}" +
|
|
217
275
|
"svg text{fill:#000}svg .phase{fill:#000;opacity:.04}svg .phase-alt{opacity:.09}svg .phase-label{font-weight:bold}" +
|
|
218
276
|
"svg .lane-line{stroke:#bbb}svg .tick{stroke:#e5e5e5}svg .tick-label{fill:#555}" +
|
|
277
|
+
"svg .cut{stroke:#999;stroke-width:1.5;stroke-dasharray:4 3}svg .tail{font-style:italic}" +
|
|
219
278
|
"svg .n{stroke:#000;stroke-width:1}svg .n-prior{stroke-dasharray:3 2}" +
|
|
220
279
|
"svg .m{stroke:#333;stroke-width:1}svg .m-rejected{stroke:#c62828;stroke-width:2}svg .m-noop{stroke-dasharray:2 1}" +
|
|
221
280
|
"svg a:hover .n,svg a:hover .m{stroke-width:2.5}svg a:hover text{text-decoration:underline}" +
|
package/src/run-graph.ts
CHANGED
|
@@ -54,6 +54,8 @@ export interface RunGraph {
|
|
|
54
54
|
runId: string;
|
|
55
55
|
start: number;
|
|
56
56
|
end: number;
|
|
57
|
+
/** When the last member activity happened; equals `end` while members still run. */
|
|
58
|
+
membersEnd: number;
|
|
57
59
|
lanes: GraphLane[];
|
|
58
60
|
phases: GraphPhase[];
|
|
59
61
|
nodes: GraphNode[];
|
|
@@ -553,6 +555,16 @@ function phasesOf(draft: Draft, nodes: GraphNode[], start: number, end: number):
|
|
|
553
555
|
return phases;
|
|
554
556
|
}
|
|
555
557
|
|
|
558
|
+
const TERMINAL_STATUSES = new Set(["done", "failed", "stopped"]);
|
|
559
|
+
|
|
560
|
+
/** The last member-lane event once every member has exited; the whole span while any still runs. */
|
|
561
|
+
function membersEndOf(nodes: GraphNode[], members: GraphMember[], live: boolean, start: number, end: number): number {
|
|
562
|
+
const running = live && members.some((m) => m.status !== undefined && !TERMINAL_STATUSES.has(m.status));
|
|
563
|
+
if (running) return end;
|
|
564
|
+
const times = nodes.filter((n) => n.lane !== "parent" && !n.placeholder).map((n) => n.at);
|
|
565
|
+
return Math.min(end, Math.max(start, ...times));
|
|
566
|
+
}
|
|
567
|
+
|
|
556
568
|
/** Project a run's events (plus what the parent knows about its members) into lanes, phases, nodes, and edges. */
|
|
557
569
|
export function buildRunGraph(input: RunGraphInput): RunGraph {
|
|
558
570
|
const events = input.events.filter((event) => event.outcome !== "intent");
|
|
@@ -570,6 +582,7 @@ export function buildRunGraph(input: RunGraphInput): RunGraph {
|
|
|
570
582
|
runId: input.runId,
|
|
571
583
|
start,
|
|
572
584
|
end,
|
|
585
|
+
membersEnd: membersEndOf(nodes, members, input.now !== undefined, start, end),
|
|
573
586
|
lanes: lanesOf(draft, nodes, members),
|
|
574
587
|
phases: phasesOf(draft, nodes, start, end),
|
|
575
588
|
nodes,
|