@weshipwork/pi-herd 0.1.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/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @weshipwork/pi-herd
2
+
3
+ Read-only Transcript mirror plumbing for Pi Subagents in Herdr.
4
+
5
+ This package is the v1 mirror-mode tracer bullet from the project ADRs:
6
+
7
+ - Subagents still run in the Delegator Pi process.
8
+ - A Delegator-side bridge serves structured mirror events over one Unix socket.
9
+ - `pi-herd-viewer` connects from a Herdr pane and renders a read-only transcript/status stream.
10
+
11
+ ## Viewer
12
+
13
+ ```sh
14
+ pi-herd-viewer --socket /path/to/pi-herd.sock --agent-id <subagent-id>
15
+ ```
16
+
17
+ The current tracer bullet supports:
18
+
19
+ - a read-only observation-source interface for Subagent snapshots and live updates;
20
+ - a forked `pi-subagents` mirror-service adapter shape;
21
+ - a Delegator-side `MirrorBridge` that forwards those updates to `MirrorServer`;
22
+ - initial snapshots, assistant text deltas, and status updates in the viewer stream.
23
+ - a Pi extension entrypoint that starts a session-scoped mirror runtime when an observation source is installed;
24
+ - Herdr pane launch for `pi-herd-viewer` mirrors.
25
+
26
+ Full chronological transcripts, tool folding, and scroll/focus keybindings are later slices.
27
+
28
+ ## Pi extension integration
29
+
30
+ `./extensions/herd.ts` starts only inside Herdr (`HERDR_ENV` and `HERDR_PANE_ID`) and requires a read-only observation source from the forked `pi-subagents` runtime:
31
+
32
+ ```ts
33
+ import { installPiHerdObservationSource } from "@weshipwork/pi-herd/extensions/herd";
34
+
35
+ installPiHerdObservationSource(source);
36
+ ```
37
+
38
+ The expected fork seam is represented by `PiSubagentsMirrorService` in `src/pi-subagents-observation-source.ts`.
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { streamMirrorEvents } from "../src/mirror-client.js";
3
+ import { renderMirrorEvent } from "../src/viewer-render.js";
4
+
5
+ interface ViewerArgs {
6
+ socketPath: string;
7
+ agentId: string;
8
+ }
9
+
10
+ function parseArgs(argv: string[]): ViewerArgs {
11
+ const socketIndex = argv.indexOf("--socket");
12
+ const agentIndex = argv.indexOf("--agent-id");
13
+ const socketPath = socketIndex === -1 ? undefined : argv[socketIndex + 1];
14
+ const agentId = agentIndex === -1 ? undefined : argv[agentIndex + 1];
15
+ if (!socketPath || !agentId) {
16
+ throw new Error("Usage: pi-herd-viewer --socket <path> --agent-id <id>");
17
+ }
18
+ return { socketPath, agentId };
19
+ }
20
+
21
+ try {
22
+ const args = parseArgs(process.argv.slice(2));
23
+ await streamMirrorEvents(args, (event) => process.stdout.write(renderMirrorEvent(event)));
24
+ } catch (error) {
25
+ const message = error instanceof Error ? error.message : String(error);
26
+ process.stderr.write(`${message}\n`);
27
+ process.exitCode = 1;
28
+ }
@@ -0,0 +1,76 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { HerdrMirrorPaneLauncher } from "../src/herdr-pane-launcher.js";
5
+ import { MirrorRuntime } from "../src/mirror-runtime.js";
6
+ import type { MirrorObservationSource } from "../src/mirror-observation.js";
7
+ import { MirrorServer } from "../src/mirror-server.js";
8
+ import { PiSubagentsObservationSource, type PiSubagentsMirrorService } from "../src/pi-subagents-observation-source.js";
9
+
10
+ export const PI_HERD_OBSERVATION_SOURCE_KEY = "__piHerdObservationSource";
11
+ export const PI_SUBAGENTS_MIRROR_SERVICE_KEY = "__piSubagentsMirrorService";
12
+
13
+ export type MirrorObservationSourceResolver = () => MirrorObservationSource | undefined;
14
+
15
+ export function installPiHerdObservationSource(source: MirrorObservationSource): void {
16
+ Reflect.set(globalThis, PI_HERD_OBSERVATION_SOURCE_KEY, source);
17
+ }
18
+
19
+ export function resolveGlobalObservationSource(): MirrorObservationSource | undefined {
20
+ const directSource = Reflect.get(globalThis, PI_HERD_OBSERVATION_SOURCE_KEY) as unknown;
21
+ if (isMirrorObservationSource(directSource)) return directSource;
22
+
23
+ const piSubagentsService = Reflect.get(globalThis, PI_SUBAGENTS_MIRROR_SERVICE_KEY) as unknown;
24
+ if (!isPiSubagentsMirrorService(piSubagentsService)) return undefined;
25
+ return new PiSubagentsObservationSource(piSubagentsService);
26
+ }
27
+
28
+ export function createPiHerdExtension(resolveSource: MirrorObservationSourceResolver = resolveGlobalObservationSource) {
29
+ return function piHerdExtension(pi: ExtensionAPI): void {
30
+ const herdrEnv = process.env.HERDR_ENV;
31
+ const currentPaneId = process.env.HERDR_PANE_ID;
32
+ if (!herdrEnv || !currentPaneId) return;
33
+
34
+ let runtime: MirrorRuntime | undefined;
35
+
36
+ pi.on("session_start", async () => {
37
+ const source = resolveSource();
38
+ if (!source || runtime) return;
39
+ const socketPath = join(tmpdir(), `pi-herd-${process.pid}-${Date.now()}.sock`);
40
+ const server = new MirrorServer({ socketPath });
41
+ const launcher = new HerdrMirrorPaneLauncher({ pi, currentPaneId });
42
+ runtime = new MirrorRuntime({ source, server, launcher, socketPath });
43
+ await runtime.start();
44
+ });
45
+
46
+ pi.on("session_shutdown", async () => {
47
+ const activeRuntime = runtime;
48
+ runtime = undefined;
49
+ await activeRuntime?.stop();
50
+ });
51
+ };
52
+ }
53
+
54
+ function isMirrorObservationSource(value: unknown): value is MirrorObservationSource {
55
+ return (
56
+ typeof value === "object" &&
57
+ value !== null &&
58
+ "listAgents" in value &&
59
+ "subscribe" in value &&
60
+ typeof value.listAgents === "function" &&
61
+ typeof value.subscribe === "function"
62
+ );
63
+ }
64
+
65
+ function isPiSubagentsMirrorService(value: unknown): value is PiSubagentsMirrorService {
66
+ return (
67
+ typeof value === "object" &&
68
+ value !== null &&
69
+ "listMirrorSnapshots" in value &&
70
+ "subscribeToMirrorEvents" in value &&
71
+ typeof value.listMirrorSnapshots === "function" &&
72
+ typeof value.subscribeToMirrorEvents === "function"
73
+ );
74
+ }
75
+
76
+ export default createPiHerdExtension();
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@weshipwork/pi-herd",
3
+ "version": "0.1.0",
4
+ "description": "Read-only Herdr transcript mirrors for Pi Subagents.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "keywords": [
8
+ "pi-package",
9
+ "pi",
10
+ "pi-coding-agent",
11
+ "herdr",
12
+ "subagents",
13
+ "mirror"
14
+ ],
15
+ "pi": {
16
+ "extensions": [
17
+ "./extensions/herd.ts"
18
+ ]
19
+ },
20
+ "bin": {
21
+ "pi-herd-viewer": "./bin/pi-herd-viewer.ts"
22
+ },
23
+ "scripts": {
24
+ "typecheck": "tsc -p tsconfig.json --noEmit",
25
+ "test": "node --import tsx --test \"tests/**/*.test.ts\"",
26
+ "check": "pnpm run typecheck && pnpm test"
27
+ },
28
+ "peerDependencies": {
29
+ "@earendil-works/pi-coding-agent": "*",
30
+ "@weshipwork/pi-subagents": "*"
31
+ },
32
+ "peerDependenciesMeta": {
33
+ "@earendil-works/pi-coding-agent": {
34
+ "optional": true
35
+ },
36
+ "@weshipwork/pi-subagents": {
37
+ "optional": true
38
+ }
39
+ },
40
+ "devDependencies": {
41
+ "@earendil-works/pi-coding-agent": "^0.79.3",
42
+ "@weshipwork/pi-subagents": "workspace:*",
43
+ "@types/node": "^24.10.1",
44
+ "tsx": "^4.20.6",
45
+ "typescript": "^5.9.3"
46
+ },
47
+ "files": [
48
+ "src/",
49
+ "bin/",
50
+ "extensions/",
51
+ "README.md",
52
+ "package.json",
53
+ "tsconfig.json"
54
+ ],
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "git+https://github.com/weshipwork/threeonefour.git",
58
+ "directory": "packages/pi-herd"
59
+ },
60
+ "homepage": "https://github.com/weshipwork/threeonefour/tree/main/packages/pi-herd",
61
+ "bugs": {
62
+ "url": "https://github.com/weshipwork/threeonefour/issues"
63
+ },
64
+ "author": "brandon@weship.work",
65
+ "publishConfig": {
66
+ "access": "public"
67
+ },
68
+ "engines": {
69
+ "node": ">=22.19.0"
70
+ },
71
+ "packageManager": "pnpm@11.6.0"
72
+ }
@@ -0,0 +1,203 @@
1
+ import { dirname } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import type { MirrorAgentSnapshot } from "./mirror-types.js";
4
+
5
+ export interface PiExecResult {
6
+ code: number;
7
+ stdout: string;
8
+ stderr: string;
9
+ killed?: boolean;
10
+ }
11
+
12
+ export interface PiExecutor {
13
+ exec(command: string, args: readonly string[], options?: { signal?: AbortSignal }): Promise<PiExecResult>;
14
+ }
15
+
16
+ export interface HerdrMirrorPaneLauncherOptions {
17
+ pi: PiExecutor;
18
+ currentPaneId: string;
19
+ signal?: AbortSignal;
20
+ }
21
+
22
+ interface HerdrPaneEnvelope {
23
+ result?: HerdrPaneResult;
24
+ error?: HerdrErrorResult;
25
+ }
26
+
27
+ interface HerdrPaneResult {
28
+ pane?: HerdrPaneInfo;
29
+ }
30
+
31
+ interface HerdrTabEnvelope {
32
+ result?: HerdrTabResult;
33
+ error?: HerdrErrorResult;
34
+ }
35
+
36
+ interface HerdrTabResult {
37
+ tab?: HerdrTabInfo;
38
+ root_pane?: HerdrPaneInfo;
39
+ }
40
+
41
+ interface HerdrTabInfo {
42
+ tab_id?: string;
43
+ }
44
+
45
+ interface HerdrPaneInfo {
46
+ pane_id?: string;
47
+ }
48
+
49
+ interface HerdrErrorResult {
50
+ code?: string;
51
+ message?: string;
52
+ }
53
+
54
+ interface CategoryTab {
55
+ tabId: string;
56
+ rootPaneId: string;
57
+ paneIds: string[];
58
+ }
59
+
60
+ const AGENT_CATEGORY_RULES = [
61
+ { pattern: /scout|explore|research|inspect|search|find/i, label: "scouts" },
62
+ { pattern: /debug|diagnos|triage|bug|fix/i, label: "debuggers" },
63
+ { pattern: /plan|architect|design/i, label: "planners" },
64
+ { pattern: /review|audit|security/i, label: "reviewers" },
65
+ { pattern: /test|qa|spec/i, label: "testers" },
66
+ { pattern: /doc|write|readme/i, label: "documenters" },
67
+ ] as const;
68
+
69
+ function shellQuote(value: string): string {
70
+ return `'${value.replaceAll("'", "'\\''")}'`;
71
+ }
72
+
73
+ function viewerCommand(socketPath: string, agentId: string): string {
74
+ const sourceDir = dirname(fileURLToPath(import.meta.url));
75
+ const packageDir = dirname(sourceDir);
76
+ const viewerPath = fileURLToPath(new URL("../bin/pi-herd-viewer.ts", import.meta.url));
77
+ return [
78
+ "pnpm",
79
+ "--dir",
80
+ shellQuote(packageDir),
81
+ "exec",
82
+ "tsx",
83
+ shellQuote(viewerPath),
84
+ "--socket",
85
+ shellQuote(socketPath),
86
+ "--agent-id",
87
+ shellQuote(agentId),
88
+ ].join(" ");
89
+ }
90
+
91
+ function parseSplitPaneId(stdout: string): string {
92
+ let envelope: HerdrPaneEnvelope;
93
+ try {
94
+ envelope = JSON.parse(stdout) as HerdrPaneEnvelope;
95
+ } catch {
96
+ throw new Error("Failed to parse Herdr pane split response.");
97
+ }
98
+ if (envelope.error) {
99
+ throw new Error(envelope.error.message || envelope.error.code || "Herdr pane split failed.");
100
+ }
101
+ const paneId = envelope.result?.pane?.pane_id;
102
+ if (!paneId) throw new Error("Herdr pane split response did not include a pane id.");
103
+ return paneId;
104
+ }
105
+
106
+ function parseCreatedTab(stdout: string): CategoryTab {
107
+ let envelope: HerdrTabEnvelope;
108
+ try {
109
+ envelope = JSON.parse(stdout) as HerdrTabEnvelope;
110
+ } catch {
111
+ throw new Error("Failed to parse Herdr tab create response.");
112
+ }
113
+ if (envelope.error) {
114
+ throw new Error(envelope.error.message || envelope.error.code || "Herdr tab create failed.");
115
+ }
116
+ const tabId = envelope.result?.tab?.tab_id;
117
+ const paneId = envelope.result?.root_pane?.pane_id;
118
+ if (!tabId) throw new Error("Herdr tab create response did not include a tab id.");
119
+ if (!paneId) throw new Error("Herdr tab create response did not include a root pane id.");
120
+ return { tabId, rootPaneId: paneId, paneIds: [] };
121
+ }
122
+
123
+ function normalizeCategoryFallback(type: string): string {
124
+ const words = type
125
+ .trim()
126
+ .toLowerCase()
127
+ .replaceAll(/[^a-z0-9]+/g, " ")
128
+ .trim()
129
+ .split(/\s+/)
130
+ .filter(Boolean);
131
+ const base = words.at(-1) ?? "agents";
132
+ if (base.endsWith("s")) return base;
133
+ if (base.endsWith("y")) return `${base.slice(0, -1)}ies`;
134
+ return `${base}s`;
135
+ }
136
+
137
+ export function agentCategoryLabel(agent: MirrorAgentSnapshot): string {
138
+ for (const rule of AGENT_CATEGORY_RULES) {
139
+ if (rule.pattern.test(agent.type)) return rule.label;
140
+ }
141
+ for (const rule of AGENT_CATEGORY_RULES) {
142
+ if (rule.pattern.test(agent.description)) return rule.label;
143
+ }
144
+ return normalizeCategoryFallback(agent.type);
145
+ }
146
+
147
+ export class HerdrMirrorPaneLauncher {
148
+ private readonly paneIdsByAgentId = new Map<string, string>();
149
+ private readonly tabsByCategory = new Map<string, CategoryTab>();
150
+
151
+ constructor(private readonly options: HerdrMirrorPaneLauncherOptions) {}
152
+
153
+ async launch(agent: MirrorAgentSnapshot, socketPath: string): Promise<string> {
154
+ const existingPaneId = this.paneIdsByAgentId.get(agent.id);
155
+ if (existingPaneId) return existingPaneId;
156
+
157
+ const category = agentCategoryLabel(agent);
158
+ const categoryTab = await this.getOrCreateCategoryTab(category);
159
+ const paneId = await this.allocatePane(categoryTab);
160
+ await this.execHerdr(["pane", "run", paneId, viewerCommand(socketPath, agent.id)]);
161
+ this.paneIdsByAgentId.set(agent.id, paneId);
162
+ return paneId;
163
+ }
164
+
165
+ async closeAll(): Promise<void> {
166
+ const tabIds = [...this.tabsByCategory.values()].map((tab) => tab.tabId);
167
+ this.paneIdsByAgentId.clear();
168
+ this.tabsByCategory.clear();
169
+ await Promise.all(tabIds.map((tabId) => this.execHerdr(["tab", "close", tabId]).catch(() => undefined)));
170
+ }
171
+
172
+ private async getOrCreateCategoryTab(category: string): Promise<CategoryTab> {
173
+ const existing = this.tabsByCategory.get(category);
174
+ if (existing) return existing;
175
+
176
+ const created = await this.execHerdr(["tab", "create", "--label", `herd: ${category}`, "--no-focus"]);
177
+ const tab = parseCreatedTab(created.stdout.trim());
178
+ this.tabsByCategory.set(category, tab);
179
+ return tab;
180
+ }
181
+
182
+ private async allocatePane(tab: CategoryTab): Promise<string> {
183
+ if (tab.paneIds.length === 0) {
184
+ tab.paneIds.push(tab.rootPaneId);
185
+ return tab.rootPaneId;
186
+ }
187
+
188
+ const split = await this.execHerdr(["pane", "split", tab.rootPaneId, "--direction", "right", "--no-focus"]);
189
+ const paneId = parseSplitPaneId(split.stdout.trim());
190
+ tab.paneIds.push(paneId);
191
+ return paneId;
192
+ }
193
+
194
+ private async execHerdr(args: readonly string[]): Promise<PiExecResult> {
195
+ const result = await this.options.pi.exec("herdr", args, { signal: this.options.signal });
196
+ if (this.options.signal?.aborted || result.killed) throw new Error("Aborted");
197
+ if (result.code !== 0) {
198
+ const message = result.stderr.trim() || result.stdout.trim() || `herdr ${args.join(" ")} failed with exit code ${result.code}`;
199
+ throw new Error(message);
200
+ }
201
+ return result;
202
+ }
203
+ }
@@ -0,0 +1,54 @@
1
+ import { MIRROR_EVENT_TYPE, type MirrorEvent } from "./mirror-types.js";
2
+ import type { MirrorObservationEvent, MirrorObservationSource, MirrorObservationUnsubscribe } from "./mirror-observation.js";
3
+ import { MIRROR_OBSERVATION_EVENT_TYPE } from "./mirror-observation.js";
4
+ import type { MirrorServer } from "./mirror-server.js";
5
+
6
+ export interface MirrorBridgeOptions {
7
+ source: MirrorObservationSource;
8
+ server: MirrorServer;
9
+ }
10
+
11
+ export class MirrorBridge {
12
+ private unsubscribe: MirrorObservationUnsubscribe | undefined;
13
+ private sequence = 1;
14
+ private started = false;
15
+
16
+ constructor(private readonly options: MirrorBridgeOptions) {}
17
+
18
+ async start(): Promise<void> {
19
+ if (this.started) return;
20
+ this.started = true;
21
+ await this.options.server.start();
22
+ this.unsubscribe = this.options.source.subscribe((event) => this.handleObservation(event));
23
+ const agents = await this.options.source.listAgents();
24
+ for (const agent of agents) {
25
+ this.options.server.upsertSnapshot(agent);
26
+ }
27
+ }
28
+
29
+ async stop(): Promise<void> {
30
+ this.unsubscribe?.();
31
+ this.unsubscribe = undefined;
32
+ this.started = false;
33
+ await this.options.server.stop();
34
+ }
35
+
36
+ private async handleObservation(event: MirrorObservationEvent): Promise<void> {
37
+ if (event.type === MIRROR_OBSERVATION_EVENT_TYPE.SNAPSHOT) {
38
+ this.options.server.upsertSnapshot(event.agent);
39
+ return;
40
+ }
41
+ await this.options.server.publish(this.toMirrorEvent(event));
42
+ }
43
+
44
+ private toMirrorEvent(event: Exclude<MirrorObservationEvent, { type: typeof MIRROR_OBSERVATION_EVENT_TYPE.SNAPSHOT }>): MirrorEvent {
45
+ const sequence = this.sequence;
46
+ this.sequence += 1;
47
+ switch (event.type) {
48
+ case MIRROR_OBSERVATION_EVENT_TYPE.STATUS:
49
+ return { type: MIRROR_EVENT_TYPE.STATUS, agentId: event.agentId, status: event.status, sequence };
50
+ case MIRROR_OBSERVATION_EVENT_TYPE.ASSISTANT_TEXT_DELTA:
51
+ return { type: MIRROR_EVENT_TYPE.ASSISTANT_TEXT_DELTA, agentId: event.agentId, delta: event.delta, sequence };
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,33 @@
1
+ import { connect, type Socket } from "node:net";
2
+ import type { MirrorEvent } from "./mirror-types.js";
3
+
4
+ export interface MirrorClientOptions {
5
+ socketPath: string;
6
+ agentId: string;
7
+ }
8
+
9
+ export async function streamMirrorEvents(
10
+ options: MirrorClientOptions,
11
+ onEvent: (event: MirrorEvent) => void,
12
+ ): Promise<Socket> {
13
+ const socket = connect(options.socketPath);
14
+ let buffer = "";
15
+ socket.setEncoding("utf8");
16
+ socket.on("data", (chunk) => {
17
+ buffer += chunk;
18
+ for (;;) {
19
+ const newline = buffer.indexOf("\n");
20
+ if (newline === -1) break;
21
+ const line = buffer.slice(0, newline);
22
+ buffer = buffer.slice(newline + 1);
23
+ if (!line.trim()) continue;
24
+ onEvent(JSON.parse(line) as MirrorEvent);
25
+ }
26
+ });
27
+ await new Promise<void>((resolve, reject) => {
28
+ socket.once("connect", resolve);
29
+ socket.once("error", reject);
30
+ });
31
+ socket.write(`${JSON.stringify({ agentId: options.agentId })}\n`);
32
+ return socket;
33
+ }
@@ -0,0 +1,40 @@
1
+ import type { MirrorAgentSnapshot, SubagentStatus } from "./mirror-types.js";
2
+
3
+ export const MIRROR_OBSERVATION_EVENT_TYPE = {
4
+ SNAPSHOT: "snapshot",
5
+ STATUS: "status",
6
+ ASSISTANT_TEXT_DELTA: "assistant_text_delta",
7
+ } as const;
8
+
9
+ export type MirrorObservationEventType = (typeof MIRROR_OBSERVATION_EVENT_TYPE)[keyof typeof MIRROR_OBSERVATION_EVENT_TYPE];
10
+
11
+ export interface MirrorSnapshotObservationEvent {
12
+ type: typeof MIRROR_OBSERVATION_EVENT_TYPE.SNAPSHOT;
13
+ agent: MirrorAgentSnapshot;
14
+ }
15
+
16
+ export interface MirrorStatusObservationEvent {
17
+ type: typeof MIRROR_OBSERVATION_EVENT_TYPE.STATUS;
18
+ agentId: string;
19
+ status: SubagentStatus;
20
+ }
21
+
22
+ export interface MirrorAssistantTextDeltaObservationEvent {
23
+ type: typeof MIRROR_OBSERVATION_EVENT_TYPE.ASSISTANT_TEXT_DELTA;
24
+ agentId: string;
25
+ delta: string;
26
+ }
27
+
28
+ export type MirrorObservationEvent =
29
+ | MirrorSnapshotObservationEvent
30
+ | MirrorStatusObservationEvent
31
+ | MirrorAssistantTextDeltaObservationEvent;
32
+
33
+ export type MirrorObservationListener = (event: MirrorObservationEvent) => void | Promise<void>;
34
+
35
+ export type MirrorObservationUnsubscribe = () => void;
36
+
37
+ export interface MirrorObservationSource {
38
+ listAgents(): readonly MirrorAgentSnapshot[] | Promise<readonly MirrorAgentSnapshot[]>;
39
+ subscribe(listener: MirrorObservationListener): MirrorObservationUnsubscribe;
40
+ }
@@ -0,0 +1,69 @@
1
+ import type { HerdrMirrorPaneLauncher } from "./herdr-pane-launcher.js";
2
+ import { MirrorBridge } from "./mirror-bridge.js";
3
+ import {
4
+ MIRROR_OBSERVATION_EVENT_TYPE,
5
+ type MirrorObservationEvent,
6
+ type MirrorObservationSource,
7
+ type MirrorObservationUnsubscribe,
8
+ } from "./mirror-observation.js";
9
+ import type { MirrorServer } from "./mirror-server.js";
10
+ import type { MirrorAgentSnapshot } from "./mirror-types.js";
11
+
12
+ export interface MirrorPaneLauncher {
13
+ launch(agent: MirrorAgentSnapshot, socketPath: string): Promise<string>;
14
+ closeAll(): Promise<void>;
15
+ }
16
+
17
+ export interface MirrorRuntimeOptions {
18
+ source: MirrorObservationSource;
19
+ server: MirrorServer;
20
+ launcher: MirrorPaneLauncher | HerdrMirrorPaneLauncher;
21
+ socketPath: string;
22
+ }
23
+
24
+ export class MirrorRuntime {
25
+ private readonly bridge: MirrorBridge;
26
+ private readonly launchedAgentIds = new Set<string>();
27
+ private unsubscribe: MirrorObservationUnsubscribe | undefined;
28
+ private started = false;
29
+
30
+ constructor(private readonly options: MirrorRuntimeOptions) {
31
+ this.bridge = new MirrorBridge({ source: options.source, server: options.server });
32
+ }
33
+
34
+ async start(): Promise<void> {
35
+ if (this.started) return;
36
+ this.started = true;
37
+ this.unsubscribe = this.options.source.subscribe((event) => this.handleObservation(event));
38
+ await this.bridge.start();
39
+ const agents = await this.options.source.listAgents();
40
+ await Promise.all(agents.map((agent) => this.launchOnce(agent)));
41
+ }
42
+
43
+ async stop(): Promise<void> {
44
+ this.unsubscribe?.();
45
+ this.unsubscribe = undefined;
46
+ this.launchedAgentIds.clear();
47
+ this.started = false;
48
+ await Promise.all([
49
+ this.bridge.stop(),
50
+ this.options.launcher.closeAll(),
51
+ ]);
52
+ }
53
+
54
+ private async handleObservation(event: MirrorObservationEvent): Promise<void> {
55
+ if (event.type !== MIRROR_OBSERVATION_EVENT_TYPE.SNAPSHOT) return;
56
+ await this.launchOnce(event.agent);
57
+ }
58
+
59
+ private async launchOnce(agent: MirrorAgentSnapshot): Promise<void> {
60
+ if (this.launchedAgentIds.has(agent.id)) return;
61
+ this.launchedAgentIds.add(agent.id);
62
+ try {
63
+ await this.options.launcher.launch(agent, this.options.socketPath);
64
+ } catch (error) {
65
+ this.launchedAgentIds.delete(agent.id);
66
+ throw error;
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,119 @@
1
+ import { createServer, type Server, type Socket } from "node:net";
2
+ import { rm } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { mkdirSync } from "node:fs";
5
+ import { MIRROR_EVENT_TYPE, type MirrorAgentSnapshot, type MirrorEvent, type MirrorSubscribeRequest } from "./mirror-types.js";
6
+
7
+ export interface MirrorServerOptions {
8
+ socketPath: string;
9
+ }
10
+
11
+ interface MirrorClient {
12
+ socket: Socket;
13
+ agentId?: string;
14
+ }
15
+
16
+ function encodeEvent(event: MirrorEvent): string {
17
+ return `${JSON.stringify(event)}\n`;
18
+ }
19
+
20
+ function shouldSendToClient(event: MirrorEvent, agentId: string | undefined): boolean {
21
+ if (event.type === MIRROR_EVENT_TYPE.ERROR) return event.agentId == null || event.agentId === agentId;
22
+ if (event.type === MIRROR_EVENT_TYPE.SNAPSHOT) return event.agent.id === agentId;
23
+ return event.agentId === agentId;
24
+ }
25
+
26
+ function parseSubscribeRequest(line: string): MirrorSubscribeRequest {
27
+ const value = JSON.parse(line) as Partial<MirrorSubscribeRequest>;
28
+ if (typeof value.agentId !== "string" || value.agentId.length === 0) {
29
+ throw new Error("Mirror subscribe request requires agentId.");
30
+ }
31
+ return { agentId: value.agentId };
32
+ }
33
+
34
+ export class MirrorServer {
35
+ private server: Server | undefined;
36
+ private readonly clients = new Set<MirrorClient>();
37
+ private readonly snapshots = new Map<string, MirrorAgentSnapshot>();
38
+
39
+ constructor(private readonly options: MirrorServerOptions) {}
40
+
41
+ async start(): Promise<void> {
42
+ if (this.server) return;
43
+ mkdirSync(dirname(this.options.socketPath), { recursive: true });
44
+ await rm(this.options.socketPath, { force: true });
45
+ this.server = createServer((socket) => this.handleConnection(socket));
46
+ await new Promise<void>((resolve, reject) => {
47
+ this.server?.once("error", reject);
48
+ this.server?.listen(this.options.socketPath, () => {
49
+ this.server?.off("error", reject);
50
+ resolve();
51
+ });
52
+ });
53
+ }
54
+
55
+ async stop(): Promise<void> {
56
+ for (const client of this.clients) client.socket.destroy();
57
+ this.clients.clear();
58
+ const server = this.server;
59
+ this.server = undefined;
60
+ if (server) {
61
+ await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
62
+ }
63
+ await rm(this.options.socketPath, { force: true });
64
+ }
65
+
66
+ upsertSnapshot(snapshot: MirrorAgentSnapshot): void {
67
+ this.snapshots.set(snapshot.id, snapshot);
68
+ }
69
+
70
+ async publish(event: MirrorEvent): Promise<void> {
71
+ if (event.type === MIRROR_EVENT_TYPE.ASSISTANT_TEXT_DELTA) {
72
+ const snapshot = this.snapshots.get(event.agentId);
73
+ if (snapshot) {
74
+ this.snapshots.set(event.agentId, {
75
+ ...snapshot,
76
+ assistantText: snapshot.assistantText + event.delta,
77
+ updatedAt: Date.now(),
78
+ });
79
+ }
80
+ }
81
+ if (event.type === MIRROR_EVENT_TYPE.STATUS) {
82
+ const snapshot = this.snapshots.get(event.agentId);
83
+ if (snapshot) {
84
+ this.snapshots.set(event.agentId, { ...snapshot, status: event.status, updatedAt: Date.now() });
85
+ }
86
+ }
87
+ for (const client of this.clients) {
88
+ if (!shouldSendToClient(event, client.agentId)) continue;
89
+ client.socket.write(encodeEvent(event));
90
+ }
91
+ }
92
+
93
+ private handleConnection(socket: Socket): void {
94
+ const client: MirrorClient = { socket };
95
+ this.clients.add(client);
96
+ let buffer = "";
97
+ socket.setEncoding("utf8");
98
+ socket.on("data", (chunk) => {
99
+ buffer += chunk;
100
+ const newline = buffer.indexOf("\n");
101
+ if (newline === -1 || client.agentId) return;
102
+ const line = buffer.slice(0, newline);
103
+ try {
104
+ const request = parseSubscribeRequest(line);
105
+ client.agentId = request.agentId;
106
+ const snapshot = this.snapshots.get(request.agentId);
107
+ if (snapshot) {
108
+ socket.write(encodeEvent({ type: MIRROR_EVENT_TYPE.SNAPSHOT, agent: snapshot, sequence: 0 }));
109
+ }
110
+ } catch (error) {
111
+ const message = error instanceof Error ? error.message : "Invalid mirror subscribe request.";
112
+ socket.write(encodeEvent({ type: MIRROR_EVENT_TYPE.ERROR, message, sequence: 0 }));
113
+ socket.end();
114
+ }
115
+ });
116
+ socket.on("close", () => this.clients.delete(client));
117
+ socket.on("error", () => this.clients.delete(client));
118
+ }
119
+ }
@@ -0,0 +1,64 @@
1
+ export const SUBAGENT_STATUS = {
2
+ QUEUED: "queued",
3
+ RUNNING: "running",
4
+ COMPLETED: "completed",
5
+ ABORTED: "aborted",
6
+ STEERED: "steered",
7
+ ERROR: "error",
8
+ STOPPED: "stopped",
9
+ } as const;
10
+
11
+ export type SubagentStatus = (typeof SUBAGENT_STATUS)[keyof typeof SUBAGENT_STATUS];
12
+
13
+ export const MIRROR_EVENT_TYPE = {
14
+ SNAPSHOT: "snapshot",
15
+ STATUS: "status",
16
+ ASSISTANT_TEXT_DELTA: "assistant_text_delta",
17
+ ERROR: "error",
18
+ } as const;
19
+
20
+ export type MirrorEventType = (typeof MIRROR_EVENT_TYPE)[keyof typeof MIRROR_EVENT_TYPE];
21
+
22
+ export interface MirrorAgentSnapshot {
23
+ id: string;
24
+ type: string;
25
+ description: string;
26
+ status: SubagentStatus;
27
+ assistantText: string;
28
+ updatedAt: number;
29
+ result?: string;
30
+ error?: string;
31
+ }
32
+
33
+ export interface SnapshotMirrorEvent {
34
+ type: typeof MIRROR_EVENT_TYPE.SNAPSHOT;
35
+ agent: MirrorAgentSnapshot;
36
+ sequence: number;
37
+ }
38
+
39
+ export interface StatusMirrorEvent {
40
+ type: typeof MIRROR_EVENT_TYPE.STATUS;
41
+ agentId: string;
42
+ status: SubagentStatus;
43
+ sequence: number;
44
+ }
45
+
46
+ export interface AssistantTextDeltaMirrorEvent {
47
+ type: typeof MIRROR_EVENT_TYPE.ASSISTANT_TEXT_DELTA;
48
+ agentId: string;
49
+ delta: string;
50
+ sequence: number;
51
+ }
52
+
53
+ export interface ErrorMirrorEvent {
54
+ type: typeof MIRROR_EVENT_TYPE.ERROR;
55
+ agentId?: string;
56
+ message: string;
57
+ sequence: number;
58
+ }
59
+
60
+ export type MirrorEvent = SnapshotMirrorEvent | StatusMirrorEvent | AssistantTextDeltaMirrorEvent | ErrorMirrorEvent;
61
+
62
+ export interface MirrorSubscribeRequest {
63
+ agentId: string;
64
+ }
@@ -0,0 +1,69 @@
1
+ import {
2
+ MIRROR_OBSERVATION_EVENT_TYPE,
3
+ type MirrorObservationEvent,
4
+ type MirrorObservationListener,
5
+ type MirrorObservationSource,
6
+ type MirrorObservationUnsubscribe,
7
+ } from "./mirror-observation.js";
8
+ import type { MirrorAgentSnapshot, SubagentStatus } from "./mirror-types.js";
9
+
10
+ export const PI_SUBAGENTS_MIRROR_EVENT_TYPE = {
11
+ AGENT_SNAPSHOT: "agent_snapshot",
12
+ STATUS: "status",
13
+ ASSISTANT_TEXT_DELTA: "assistant_text_delta",
14
+ } as const;
15
+
16
+ export type PiSubagentsMirrorEventType =
17
+ (typeof PI_SUBAGENTS_MIRROR_EVENT_TYPE)[keyof typeof PI_SUBAGENTS_MIRROR_EVENT_TYPE];
18
+
19
+ export interface PiSubagentsAgentSnapshotEvent {
20
+ type: typeof PI_SUBAGENTS_MIRROR_EVENT_TYPE.AGENT_SNAPSHOT;
21
+ agent: MirrorAgentSnapshot;
22
+ }
23
+
24
+ export interface PiSubagentsStatusEvent {
25
+ type: typeof PI_SUBAGENTS_MIRROR_EVENT_TYPE.STATUS;
26
+ agentId: string;
27
+ status: SubagentStatus;
28
+ }
29
+
30
+ export interface PiSubagentsAssistantTextDeltaEvent {
31
+ type: typeof PI_SUBAGENTS_MIRROR_EVENT_TYPE.ASSISTANT_TEXT_DELTA;
32
+ agentId: string;
33
+ delta: string;
34
+ }
35
+
36
+ export type PiSubagentsMirrorEvent =
37
+ | PiSubagentsAgentSnapshotEvent
38
+ | PiSubagentsStatusEvent
39
+ | PiSubagentsAssistantTextDeltaEvent;
40
+
41
+ export type PiSubagentsMirrorListener = (event: PiSubagentsMirrorEvent) => void | Promise<void>;
42
+
43
+ export interface PiSubagentsMirrorService {
44
+ listMirrorSnapshots(): readonly MirrorAgentSnapshot[] | Promise<readonly MirrorAgentSnapshot[]>;
45
+ subscribeToMirrorEvents(listener: PiSubagentsMirrorListener): MirrorObservationUnsubscribe;
46
+ }
47
+
48
+ export class PiSubagentsObservationSource implements MirrorObservationSource {
49
+ constructor(private readonly service: PiSubagentsMirrorService) {}
50
+
51
+ listAgents(): readonly MirrorAgentSnapshot[] | Promise<readonly MirrorAgentSnapshot[]> {
52
+ return this.service.listMirrorSnapshots();
53
+ }
54
+
55
+ subscribe(listener: MirrorObservationListener): MirrorObservationUnsubscribe {
56
+ return this.service.subscribeToMirrorEvents((event) => listener(toMirrorObservation(event)));
57
+ }
58
+ }
59
+
60
+ function toMirrorObservation(event: PiSubagentsMirrorEvent): MirrorObservationEvent {
61
+ switch (event.type) {
62
+ case PI_SUBAGENTS_MIRROR_EVENT_TYPE.AGENT_SNAPSHOT:
63
+ return { type: MIRROR_OBSERVATION_EVENT_TYPE.SNAPSHOT, agent: event.agent };
64
+ case PI_SUBAGENTS_MIRROR_EVENT_TYPE.STATUS:
65
+ return { type: MIRROR_OBSERVATION_EVENT_TYPE.STATUS, agentId: event.agentId, status: event.status };
66
+ case PI_SUBAGENTS_MIRROR_EVENT_TYPE.ASSISTANT_TEXT_DELTA:
67
+ return { type: MIRROR_OBSERVATION_EVENT_TYPE.ASSISTANT_TEXT_DELTA, agentId: event.agentId, delta: event.delta };
68
+ }
69
+ }
@@ -0,0 +1,16 @@
1
+ import { MIRROR_EVENT_TYPE, type MirrorEvent } from "./mirror-types.js";
2
+
3
+ export function renderMirrorEvent(event: MirrorEvent): string {
4
+ switch (event.type) {
5
+ case MIRROR_EVENT_TYPE.SNAPSHOT: {
6
+ const header = `[${event.agent.status}] ${event.agent.type} — ${event.agent.description}`;
7
+ return event.agent.assistantText ? `${header}\n${event.agent.assistantText}` : header;
8
+ }
9
+ case MIRROR_EVENT_TYPE.STATUS:
10
+ return `\n[${event.status}]`;
11
+ case MIRROR_EVENT_TYPE.ASSISTANT_TEXT_DELTA:
12
+ return event.delta;
13
+ case MIRROR_EVENT_TYPE.ERROR:
14
+ return `\n[mirror error] ${event.message}\n`;
15
+ }
16
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "include": ["src/**/*.ts", "bin/**/*.ts", "extensions/**/*.ts", "tests/**/*.ts"]
4
+ }