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 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/github/license/eliyastein/agent-horde" alt="MIT license"></a>
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.0
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-horde",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Run a horde of pi agents on one task: shared ledgers, peer review, an auditable event log, and a live dashboard",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 } = windowOf(graph, window);
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
- return { width: LABEL_W + plotW + PAD, height: y + AXIS_H, t0, t1, bands, placed, x };
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 - AXIS_H;
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 span = layout.t1 - layout.t0;
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
- const y = layout.height - AXIS_H;
155
- for (let t = firstTick; t <= layout.t1; t += step) {
156
- const x = layout.x(t);
157
- parts.push(`<line class="tick" x1="${px(x)}" y1="${HEADER_H}" x2="${px(x)}" y2="${px(y)}"/>`);
158
- parts.push(`<text x="${px(x + 2)}" y="${px(y + 12)}" class="tick-label">+${e(formatElapsed(t - graph.start))}</text>`);
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,