@lawrence369/loop-cli 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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/agent/activity.d.ts +64 -0
- package/dist/agent/activity.js +265 -0
- package/dist/agent/launcher.d.ts +42 -0
- package/dist/agent/launcher.js +243 -0
- package/dist/agent/pty-session.d.ts +113 -0
- package/dist/agent/pty-session.js +490 -0
- package/dist/agent/ready-detector.d.ts +46 -0
- package/dist/agent/ready-detector.js +86 -0
- package/dist/agent/wrapper.d.ts +18 -0
- package/dist/agent/wrapper.js +110 -0
- package/dist/bin/lclaude.d.ts +3 -0
- package/dist/bin/lclaude.js +7 -0
- package/dist/bin/lcodex.d.ts +3 -0
- package/dist/bin/lcodex.js +7 -0
- package/dist/bin/lgemini.d.ts +3 -0
- package/dist/bin/lgemini.js +7 -0
- package/dist/bus/daemon.d.ts +56 -0
- package/dist/bus/daemon.js +135 -0
- package/dist/bus/event-bus.d.ts +105 -0
- package/dist/bus/event-bus.js +157 -0
- package/dist/bus/message.d.ts +48 -0
- package/dist/bus/message.js +129 -0
- package/dist/bus/queue.d.ts +50 -0
- package/dist/bus/queue.js +100 -0
- package/dist/bus/store.d.ts +88 -0
- package/dist/bus/store.js +212 -0
- package/dist/bus/subscriber.d.ts +76 -0
- package/dist/bus/subscriber.js +187 -0
- package/dist/config/index.d.ts +8 -0
- package/dist/config/index.js +72 -0
- package/dist/config/schema.d.ts +18 -0
- package/dist/config/schema.js +58 -0
- package/dist/core/conversation.d.ts +34 -0
- package/dist/core/conversation.js +289 -0
- package/dist/core/engine.d.ts +40 -0
- package/dist/core/engine.js +288 -0
- package/dist/core/loop.d.ts +33 -0
- package/dist/core/loop.js +209 -0
- package/dist/core/protocol.d.ts +60 -0
- package/dist/core/protocol.js +162 -0
- package/dist/core/scoring.d.ts +34 -0
- package/dist/core/scoring.js +69 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +408 -0
- package/dist/orchestrator/daemon.d.ts +74 -0
- package/dist/orchestrator/daemon.js +294 -0
- package/dist/orchestrator/group.d.ts +73 -0
- package/dist/orchestrator/group.js +166 -0
- package/dist/orchestrator/ipc-server.d.ts +60 -0
- package/dist/orchestrator/ipc-server.js +166 -0
- package/dist/orchestrator/scheduler.d.ts +32 -0
- package/dist/orchestrator/scheduler.js +95 -0
- package/dist/plan/context.d.ts +8 -0
- package/dist/plan/context.js +42 -0
- package/dist/plan/decisions.d.ts +18 -0
- package/dist/plan/decisions.js +143 -0
- package/dist/plan/shared-plan.d.ts +33 -0
- package/dist/plan/shared-plan.js +211 -0
- package/dist/skills/executor.d.ts +7 -0
- package/dist/skills/executor.js +11 -0
- package/dist/skills/loader.d.ts +16 -0
- package/dist/skills/loader.js +80 -0
- package/dist/skills/registry.d.ts +13 -0
- package/dist/skills/registry.js +54 -0
- package/dist/terminal/adapter.d.ts +61 -0
- package/dist/terminal/adapter.js +42 -0
- package/dist/terminal/detect.d.ts +30 -0
- package/dist/terminal/detect.js +77 -0
- package/dist/terminal/iterm2-adapter.d.ts +19 -0
- package/dist/terminal/iterm2-adapter.js +120 -0
- package/dist/terminal/pty-adapter.d.ts +18 -0
- package/dist/terminal/pty-adapter.js +84 -0
- package/dist/terminal/terminal-adapter.d.ts +17 -0
- package/dist/terminal/terminal-adapter.js +94 -0
- package/dist/terminal/tmux-adapter.d.ts +18 -0
- package/dist/terminal/tmux-adapter.js +127 -0
- package/dist/ui/banner.d.ts +3 -0
- package/dist/ui/banner.js +145 -0
- package/dist/ui/colors.d.ts +41 -0
- package/dist/ui/colors.js +65 -0
- package/dist/ui/dashboard.d.ts +32 -0
- package/dist/ui/dashboard.js +138 -0
- package/dist/ui/input.d.ts +10 -0
- package/dist/ui/input.js +96 -0
- package/dist/ui/interactive.d.ts +13 -0
- package/dist/ui/interactive.js +230 -0
- package/dist/ui/renderer.d.ts +33 -0
- package/dist/ui/renderer.js +106 -0
- package/dist/utils/ansi.d.ts +11 -0
- package/dist/utils/ansi.js +16 -0
- package/dist/utils/fs.d.ts +34 -0
- package/dist/utils/fs.js +115 -0
- package/dist/utils/lock.d.ts +12 -0
- package/dist/utils/lock.js +116 -0
- package/dist/utils/process.d.ts +31 -0
- package/dist/utils/process.js +111 -0
- package/dist/utils/pty-filter.d.ts +31 -0
- package/dist/utils/pty-filter.js +187 -0
- package/package.json +71 -0
- package/skills/loop/SKILL.md +19 -0
- package/skills/plan/SKILL.md +9 -0
- package/skills/review/SKILL.md +14 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { EventBus } from "../bus/event-bus.js";
|
|
2
|
+
import { type IpcRequest, type IpcResponse } from "./ipc-server.js";
|
|
3
|
+
/**
|
|
4
|
+
* Status information for the orchestrator daemon.
|
|
5
|
+
*/
|
|
6
|
+
export interface DaemonStatus {
|
|
7
|
+
pid: number;
|
|
8
|
+
uptime: number;
|
|
9
|
+
agents: number;
|
|
10
|
+
busEvents: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The OrchestratorDaemon manages the lifecycle of:
|
|
14
|
+
* - EventBus (message routing)
|
|
15
|
+
* - BusDaemon (queue polling / delivery)
|
|
16
|
+
* - IpcServer (Unix socket for commands)
|
|
17
|
+
*
|
|
18
|
+
* It persists a PID file and logs to the run directory.
|
|
19
|
+
*/
|
|
20
|
+
export declare class OrchestratorDaemon {
|
|
21
|
+
readonly projectRoot: string;
|
|
22
|
+
private readonly loopDir;
|
|
23
|
+
private readonly runDir;
|
|
24
|
+
private readonly pidPath;
|
|
25
|
+
private readonly logPath;
|
|
26
|
+
private readonly socketPath;
|
|
27
|
+
private eventBus;
|
|
28
|
+
private busDaemon;
|
|
29
|
+
private ipcServer;
|
|
30
|
+
private startTime;
|
|
31
|
+
private running;
|
|
32
|
+
constructor(projectRoot: string);
|
|
33
|
+
/**
|
|
34
|
+
* Start the daemon. Initializes bus, starts polling, opens IPC socket.
|
|
35
|
+
*/
|
|
36
|
+
start(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Stop the daemon gracefully.
|
|
39
|
+
*/
|
|
40
|
+
stop(): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Ensure the daemon is running. If not, start it.
|
|
43
|
+
*/
|
|
44
|
+
ensureRunning(): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Check if the daemon is currently running.
|
|
47
|
+
*/
|
|
48
|
+
isRunning(): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Get the current daemon status.
|
|
51
|
+
*/
|
|
52
|
+
getStatus(): Promise<DaemonStatus>;
|
|
53
|
+
/**
|
|
54
|
+
* Handle an incoming IPC request.
|
|
55
|
+
*/
|
|
56
|
+
handleRequest(req: IpcRequest): Promise<IpcResponse>;
|
|
57
|
+
/**
|
|
58
|
+
* Get the event bus instance.
|
|
59
|
+
*/
|
|
60
|
+
getEventBus(): EventBus;
|
|
61
|
+
/**
|
|
62
|
+
* Get the PID file path (for external tools to check).
|
|
63
|
+
*/
|
|
64
|
+
getPidPath(): string;
|
|
65
|
+
/**
|
|
66
|
+
* Get the socket path (for IPC clients).
|
|
67
|
+
*/
|
|
68
|
+
getSocketPath(): string;
|
|
69
|
+
/**
|
|
70
|
+
* Get the log path.
|
|
71
|
+
*/
|
|
72
|
+
getLogPath(): string;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=daemon.d.ts.map
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { EventBus } from "../bus/event-bus.js";
|
|
3
|
+
import { BusDaemon } from "../bus/daemon.js";
|
|
4
|
+
import { IpcServer } from "./ipc-server.js";
|
|
5
|
+
import { isProcessAlive, writePidFile, readPidFile, removePidFile, setupSignalHandlers, } from "../utils/process.js";
|
|
6
|
+
import { ensureDir } from "../utils/fs.js";
|
|
7
|
+
/**
|
|
8
|
+
* The OrchestratorDaemon manages the lifecycle of:
|
|
9
|
+
* - EventBus (message routing)
|
|
10
|
+
* - BusDaemon (queue polling / delivery)
|
|
11
|
+
* - IpcServer (Unix socket for commands)
|
|
12
|
+
*
|
|
13
|
+
* It persists a PID file and logs to the run directory.
|
|
14
|
+
*/
|
|
15
|
+
export class OrchestratorDaemon {
|
|
16
|
+
projectRoot;
|
|
17
|
+
loopDir;
|
|
18
|
+
runDir;
|
|
19
|
+
pidPath;
|
|
20
|
+
logPath;
|
|
21
|
+
socketPath;
|
|
22
|
+
eventBus;
|
|
23
|
+
busDaemon = null;
|
|
24
|
+
ipcServer = null;
|
|
25
|
+
startTime = 0;
|
|
26
|
+
running = false;
|
|
27
|
+
constructor(projectRoot) {
|
|
28
|
+
this.projectRoot = projectRoot;
|
|
29
|
+
this.loopDir = join(projectRoot, ".loop");
|
|
30
|
+
this.runDir = join(this.loopDir, "run");
|
|
31
|
+
this.pidPath = join(this.runDir, "loop-daemon.pid");
|
|
32
|
+
this.logPath = join(this.runDir, "loop-daemon.log");
|
|
33
|
+
this.socketPath = join(this.runDir, "loop.sock");
|
|
34
|
+
this.eventBus = new EventBus(projectRoot);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Start the daemon. Initializes bus, starts polling, opens IPC socket.
|
|
38
|
+
*/
|
|
39
|
+
async start() {
|
|
40
|
+
if (this.running)
|
|
41
|
+
return;
|
|
42
|
+
// Ensure run directory exists
|
|
43
|
+
await ensureDir(this.runDir);
|
|
44
|
+
// Check if another daemon is already running
|
|
45
|
+
const existingPid = readPidFile(this.pidPath);
|
|
46
|
+
if (existingPid !== null && isProcessAlive(existingPid)) {
|
|
47
|
+
throw new Error(`Daemon already running (pid=${existingPid})`);
|
|
48
|
+
}
|
|
49
|
+
// Initialize the event bus
|
|
50
|
+
await this.eventBus.init();
|
|
51
|
+
// Write PID file
|
|
52
|
+
writePidFile(this.pidPath);
|
|
53
|
+
this.startTime = Date.now();
|
|
54
|
+
this.running = true;
|
|
55
|
+
// Start bus daemon
|
|
56
|
+
this.busDaemon = new BusDaemon(this.eventBus, { pollIntervalMs: 1000 });
|
|
57
|
+
await this.busDaemon.start();
|
|
58
|
+
// Start IPC server
|
|
59
|
+
this.ipcServer = new IpcServer(this.socketPath, (req) => this.handleRequest(req));
|
|
60
|
+
await this.ipcServer.start();
|
|
61
|
+
// Set up signal handlers
|
|
62
|
+
setupSignalHandlers(async () => {
|
|
63
|
+
await this.stop();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Stop the daemon gracefully.
|
|
68
|
+
*/
|
|
69
|
+
async stop() {
|
|
70
|
+
if (!this.running)
|
|
71
|
+
return;
|
|
72
|
+
this.running = false;
|
|
73
|
+
// Stop IPC server
|
|
74
|
+
if (this.ipcServer) {
|
|
75
|
+
await this.ipcServer.stop();
|
|
76
|
+
this.ipcServer = null;
|
|
77
|
+
}
|
|
78
|
+
// Stop bus daemon
|
|
79
|
+
if (this.busDaemon) {
|
|
80
|
+
await this.busDaemon.stop();
|
|
81
|
+
this.busDaemon = null;
|
|
82
|
+
}
|
|
83
|
+
// Shut down event bus
|
|
84
|
+
await this.eventBus.shutdown();
|
|
85
|
+
// Remove PID file
|
|
86
|
+
removePidFile(this.pidPath);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Ensure the daemon is running. If not, start it.
|
|
90
|
+
*/
|
|
91
|
+
async ensureRunning() {
|
|
92
|
+
if (this.running)
|
|
93
|
+
return;
|
|
94
|
+
const existingPid = readPidFile(this.pidPath);
|
|
95
|
+
if (existingPid !== null && isProcessAlive(existingPid)) {
|
|
96
|
+
// Another daemon is running, that's fine
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await this.start();
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Check if the daemon is currently running.
|
|
103
|
+
*/
|
|
104
|
+
isRunning() {
|
|
105
|
+
if (this.running)
|
|
106
|
+
return true;
|
|
107
|
+
// Also check via PID file (might be running in another process)
|
|
108
|
+
const pid = readPidFile(this.pidPath);
|
|
109
|
+
return pid !== null && isProcessAlive(pid);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get the current daemon status.
|
|
113
|
+
*/
|
|
114
|
+
async getStatus() {
|
|
115
|
+
const busStatus = await this.eventBus.status();
|
|
116
|
+
return {
|
|
117
|
+
pid: process.pid,
|
|
118
|
+
uptime: this.startTime > 0 ? Math.floor((Date.now() - this.startTime) / 1000) : 0,
|
|
119
|
+
agents: busStatus.agents,
|
|
120
|
+
busEvents: busStatus.events,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Handle an incoming IPC request.
|
|
125
|
+
*/
|
|
126
|
+
async handleRequest(req) {
|
|
127
|
+
// Validate incoming request structure
|
|
128
|
+
if (!req || typeof req !== "object" || typeof req.type !== "string") {
|
|
129
|
+
return { success: false, type: "ERROR", error: "Invalid request: missing type" };
|
|
130
|
+
}
|
|
131
|
+
if (req.data !== undefined && (typeof req.data !== "object" || req.data === null)) {
|
|
132
|
+
return { success: false, type: "ERROR", error: "Invalid request: data must be an object" };
|
|
133
|
+
}
|
|
134
|
+
// Ensure data is always an object for safe property access
|
|
135
|
+
if (!req.data)
|
|
136
|
+
req.data = {};
|
|
137
|
+
try {
|
|
138
|
+
switch (req.type) {
|
|
139
|
+
case "STATUS": {
|
|
140
|
+
const status = await this.getStatus();
|
|
141
|
+
const busStatus = await this.eventBus.status();
|
|
142
|
+
return {
|
|
143
|
+
success: true,
|
|
144
|
+
type: "STATUS",
|
|
145
|
+
data: {
|
|
146
|
+
...status,
|
|
147
|
+
agentList: busStatus.agentList,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
case "REGISTER_AGENT": {
|
|
152
|
+
const agentType = String(req.data.agent_type ?? "claude");
|
|
153
|
+
const subscriberId = await this.eventBus.join(agentType, {
|
|
154
|
+
nickname: req.data.nickname,
|
|
155
|
+
pid: req.data.pid,
|
|
156
|
+
tty: req.data.tty,
|
|
157
|
+
launch_mode: req.data.launch_mode,
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
success: true,
|
|
161
|
+
type: "REGISTER_AGENT",
|
|
162
|
+
data: { subscriber_id: subscriberId },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
case "AGENT_READY": {
|
|
166
|
+
const subscriberId = String(req.data.subscriber_id ?? "");
|
|
167
|
+
await this.eventBus.getSubscriberManager().updateMetadata(subscriberId, {
|
|
168
|
+
activity_state: "idle",
|
|
169
|
+
last_seen: new Date().toISOString(),
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
success: true,
|
|
173
|
+
type: "AGENT_READY",
|
|
174
|
+
data: { subscriber_id: subscriberId },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
case "AGENT_REPORT": {
|
|
178
|
+
const subscriberId = String(req.data.subscriber_id ?? "");
|
|
179
|
+
await this.eventBus.getSubscriberManager().updateMetadata(subscriberId, {
|
|
180
|
+
last_seen: new Date().toISOString(),
|
|
181
|
+
activity_state: String(req.data.activity_state ?? "working"),
|
|
182
|
+
});
|
|
183
|
+
return {
|
|
184
|
+
success: true,
|
|
185
|
+
type: "AGENT_REPORT",
|
|
186
|
+
data: { subscriber_id: subscriberId },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
case "BUS_SEND": {
|
|
190
|
+
const publisher = String(req.data.publisher ?? "unknown");
|
|
191
|
+
const target = String(req.data.target ?? "");
|
|
192
|
+
const message = String(req.data.message ?? "");
|
|
193
|
+
const event = await this.eventBus.send(publisher, target, message);
|
|
194
|
+
return {
|
|
195
|
+
success: true,
|
|
196
|
+
type: "BUS_SEND",
|
|
197
|
+
data: { seq: event.seq, target: event.target },
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
case "BUS_CHECK": {
|
|
201
|
+
const subscriberId = String(req.data.subscriber_id ?? "");
|
|
202
|
+
const events = await this.eventBus.check(subscriberId);
|
|
203
|
+
return {
|
|
204
|
+
success: true,
|
|
205
|
+
type: "BUS_CHECK",
|
|
206
|
+
data: {
|
|
207
|
+
subscriber_id: subscriberId,
|
|
208
|
+
count: events.length,
|
|
209
|
+
events: events,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
case "LAUNCH_AGENT": {
|
|
214
|
+
// Placeholder - actual agent launching is handled by the agent launcher
|
|
215
|
+
return {
|
|
216
|
+
success: true,
|
|
217
|
+
type: "LAUNCH_AGENT",
|
|
218
|
+
data: { message: "Agent launch request received" },
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
case "CLOSE_AGENT": {
|
|
222
|
+
const subscriberId = String(req.data.subscriber_id ?? req.data.agent_id ?? "");
|
|
223
|
+
await this.eventBus.leave(subscriberId);
|
|
224
|
+
return {
|
|
225
|
+
success: true,
|
|
226
|
+
type: "CLOSE_AGENT",
|
|
227
|
+
data: { subscriber_id: subscriberId },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
case "RESUME_AGENTS": {
|
|
231
|
+
return {
|
|
232
|
+
success: true,
|
|
233
|
+
type: "RESUME_AGENTS",
|
|
234
|
+
data: { message: "Resume request received" },
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
case "LAUNCH_GROUP": {
|
|
238
|
+
return {
|
|
239
|
+
success: true,
|
|
240
|
+
type: "LAUNCH_GROUP",
|
|
241
|
+
data: { message: "Group launch request received" },
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
case "STOP_GROUP": {
|
|
245
|
+
return {
|
|
246
|
+
success: true,
|
|
247
|
+
type: "STOP_GROUP",
|
|
248
|
+
data: { message: "Group stop request received" },
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
default: {
|
|
252
|
+
return {
|
|
253
|
+
success: false,
|
|
254
|
+
type: "ERROR",
|
|
255
|
+
error: `Unknown request type: ${req.type}`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
262
|
+
return {
|
|
263
|
+
success: false,
|
|
264
|
+
type: req.type,
|
|
265
|
+
error: message,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Get the event bus instance.
|
|
271
|
+
*/
|
|
272
|
+
getEventBus() {
|
|
273
|
+
return this.eventBus;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Get the PID file path (for external tools to check).
|
|
277
|
+
*/
|
|
278
|
+
getPidPath() {
|
|
279
|
+
return this.pidPath;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Get the socket path (for IPC clients).
|
|
283
|
+
*/
|
|
284
|
+
getSocketPath() {
|
|
285
|
+
return this.socketPath;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Get the log path.
|
|
289
|
+
*/
|
|
290
|
+
getLogPath() {
|
|
291
|
+
return this.logPath;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=daemon.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { OrchestratorDaemon } from "./daemon.js";
|
|
2
|
+
/**
|
|
3
|
+
* Describes a single agent within a group.
|
|
4
|
+
*/
|
|
5
|
+
export interface GroupAgent {
|
|
6
|
+
/** Display name / nickname for this agent */
|
|
7
|
+
name: string;
|
|
8
|
+
/** Agent engine type (e.g. "claude", "gemini", "codex") */
|
|
9
|
+
engine: string;
|
|
10
|
+
/** Optional role description */
|
|
11
|
+
role?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Describes a group of agents to be launched together.
|
|
15
|
+
*/
|
|
16
|
+
export interface AgentGroup {
|
|
17
|
+
/** Unique group name */
|
|
18
|
+
name: string;
|
|
19
|
+
/** Agents in this group */
|
|
20
|
+
agents: GroupAgent[];
|
|
21
|
+
/** How agents are executed */
|
|
22
|
+
strategy: "parallel" | "sequential" | "pipeline";
|
|
23
|
+
}
|
|
24
|
+
/** Runtime state for a group member */
|
|
25
|
+
interface GroupMemberState {
|
|
26
|
+
name: string;
|
|
27
|
+
engine: string;
|
|
28
|
+
subscriberId: string;
|
|
29
|
+
status: "pending" | "active" | "stopped" | "failed";
|
|
30
|
+
launchedAt: string;
|
|
31
|
+
stoppedAt: string;
|
|
32
|
+
}
|
|
33
|
+
/** Runtime state for a group */
|
|
34
|
+
interface GroupState {
|
|
35
|
+
name: string;
|
|
36
|
+
status: "starting" | "active" | "stopped" | "failed";
|
|
37
|
+
strategy: "parallel" | "sequential" | "pipeline";
|
|
38
|
+
members: GroupMemberState[];
|
|
39
|
+
createdAt: string;
|
|
40
|
+
updatedAt: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Orchestrates groups of agents: launching them according to a strategy,
|
|
44
|
+
* tracking their lifecycle, and stopping them together.
|
|
45
|
+
*/
|
|
46
|
+
export declare class GroupOrchestrator {
|
|
47
|
+
private readonly daemon;
|
|
48
|
+
private groups;
|
|
49
|
+
constructor(daemon: OrchestratorDaemon);
|
|
50
|
+
/**
|
|
51
|
+
* Launch a group of agents.
|
|
52
|
+
* Returns the subscriber IDs of all launched agents.
|
|
53
|
+
*/
|
|
54
|
+
launchGroup(group: AgentGroup): Promise<string[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Stop all agents in a group.
|
|
57
|
+
*/
|
|
58
|
+
stopGroup(name: string): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* List all known groups and their current state.
|
|
61
|
+
*/
|
|
62
|
+
listGroups(): Promise<AgentGroup[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Get the state of a specific group.
|
|
65
|
+
*/
|
|
66
|
+
getGroupState(name: string): GroupState | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Launch a single agent on the bus, returning its subscriber ID.
|
|
69
|
+
*/
|
|
70
|
+
private launchSingleAgent;
|
|
71
|
+
}
|
|
72
|
+
export {};
|
|
73
|
+
//# sourceMappingURL=group.d.ts.map
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrates groups of agents: launching them according to a strategy,
|
|
3
|
+
* tracking their lifecycle, and stopping them together.
|
|
4
|
+
*/
|
|
5
|
+
export class GroupOrchestrator {
|
|
6
|
+
daemon;
|
|
7
|
+
groups = new Map();
|
|
8
|
+
constructor(daemon) {
|
|
9
|
+
this.daemon = daemon;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Launch a group of agents.
|
|
13
|
+
* Returns the subscriber IDs of all launched agents.
|
|
14
|
+
*/
|
|
15
|
+
async launchGroup(group) {
|
|
16
|
+
const eventBus = this.daemon.getEventBus();
|
|
17
|
+
const now = new Date().toISOString();
|
|
18
|
+
const state = {
|
|
19
|
+
name: group.name,
|
|
20
|
+
status: "starting",
|
|
21
|
+
strategy: group.strategy,
|
|
22
|
+
members: group.agents.map((agent) => ({
|
|
23
|
+
name: agent.name,
|
|
24
|
+
engine: agent.engine,
|
|
25
|
+
subscriberId: "",
|
|
26
|
+
status: "pending",
|
|
27
|
+
launchedAt: "",
|
|
28
|
+
stoppedAt: "",
|
|
29
|
+
})),
|
|
30
|
+
createdAt: now,
|
|
31
|
+
updatedAt: now,
|
|
32
|
+
};
|
|
33
|
+
this.groups.set(group.name, state);
|
|
34
|
+
const subscriberIds = [];
|
|
35
|
+
try {
|
|
36
|
+
switch (group.strategy) {
|
|
37
|
+
case "parallel": {
|
|
38
|
+
// Launch all agents concurrently
|
|
39
|
+
const promises = group.agents.map(async (agent, idx) => {
|
|
40
|
+
const subscriberId = await this.launchSingleAgent(eventBus, agent);
|
|
41
|
+
const member = state.members[idx];
|
|
42
|
+
if (member) {
|
|
43
|
+
member.subscriberId = subscriberId;
|
|
44
|
+
member.status = "active";
|
|
45
|
+
member.launchedAt = new Date().toISOString();
|
|
46
|
+
}
|
|
47
|
+
return subscriberId;
|
|
48
|
+
});
|
|
49
|
+
const results = await Promise.allSettled(promises);
|
|
50
|
+
for (let i = 0; i < results.length; i++) {
|
|
51
|
+
const result = results[i];
|
|
52
|
+
if (result.status === "fulfilled") {
|
|
53
|
+
subscriberIds.push(result.value);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const member = state.members[i];
|
|
57
|
+
if (member) {
|
|
58
|
+
member.status = "failed";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case "sequential":
|
|
65
|
+
case "pipeline": {
|
|
66
|
+
// Launch agents one at a time in order
|
|
67
|
+
for (let i = 0; i < group.agents.length; i++) {
|
|
68
|
+
const agent = group.agents[i];
|
|
69
|
+
const member = state.members[i];
|
|
70
|
+
try {
|
|
71
|
+
const subscriberId = await this.launchSingleAgent(eventBus, agent);
|
|
72
|
+
member.subscriberId = subscriberId;
|
|
73
|
+
member.status = "active";
|
|
74
|
+
member.launchedAt = new Date().toISOString();
|
|
75
|
+
subscriberIds.push(subscriberId);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
member.status = "failed";
|
|
79
|
+
// For pipeline, stop on first failure
|
|
80
|
+
if (group.strategy === "pipeline") {
|
|
81
|
+
state.status = "failed";
|
|
82
|
+
state.updatedAt = new Date().toISOString();
|
|
83
|
+
this.groups.set(group.name, state);
|
|
84
|
+
return subscriberIds;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Determine overall group status
|
|
92
|
+
const allActive = state.members.every((m) => m.status === "active");
|
|
93
|
+
const anyFailed = state.members.some((m) => m.status === "failed");
|
|
94
|
+
state.status = allActive ? "active" : anyFailed ? "failed" : "active";
|
|
95
|
+
state.updatedAt = new Date().toISOString();
|
|
96
|
+
this.groups.set(group.name, state);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
state.status = "failed";
|
|
100
|
+
state.updatedAt = new Date().toISOString();
|
|
101
|
+
this.groups.set(group.name, state);
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
return subscriberIds;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Stop all agents in a group.
|
|
108
|
+
*/
|
|
109
|
+
async stopGroup(name) {
|
|
110
|
+
const state = this.groups.get(name);
|
|
111
|
+
if (!state) {
|
|
112
|
+
throw new Error(`Group "${name}" not found`);
|
|
113
|
+
}
|
|
114
|
+
const eventBus = this.daemon.getEventBus();
|
|
115
|
+
// Stop in reverse order
|
|
116
|
+
for (let i = state.members.length - 1; i >= 0; i--) {
|
|
117
|
+
const member = state.members[i];
|
|
118
|
+
if (member.status !== "active" || !member.subscriberId)
|
|
119
|
+
continue;
|
|
120
|
+
try {
|
|
121
|
+
await eventBus.leave(member.subscriberId);
|
|
122
|
+
member.status = "stopped";
|
|
123
|
+
member.stoppedAt = new Date().toISOString();
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Best effort - continue stopping others
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
state.status = "stopped";
|
|
130
|
+
state.updatedAt = new Date().toISOString();
|
|
131
|
+
this.groups.set(name, state);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* List all known groups and their current state.
|
|
135
|
+
*/
|
|
136
|
+
async listGroups() {
|
|
137
|
+
const groups = [];
|
|
138
|
+
for (const state of this.groups.values()) {
|
|
139
|
+
groups.push({
|
|
140
|
+
name: state.name,
|
|
141
|
+
strategy: state.strategy,
|
|
142
|
+
agents: state.members.map((m) => ({
|
|
143
|
+
name: m.name,
|
|
144
|
+
engine: m.engine,
|
|
145
|
+
})),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return groups;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get the state of a specific group.
|
|
152
|
+
*/
|
|
153
|
+
getGroupState(name) {
|
|
154
|
+
return this.groups.get(name);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Launch a single agent on the bus, returning its subscriber ID.
|
|
158
|
+
*/
|
|
159
|
+
async launchSingleAgent(eventBus, agent) {
|
|
160
|
+
return eventBus.join(agent.engine, {
|
|
161
|
+
nickname: agent.name,
|
|
162
|
+
activity_state: "starting",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=group.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request types the IPC server handles.
|
|
3
|
+
*/
|
|
4
|
+
export type IpcRequestType = "REGISTER_AGENT" | "AGENT_READY" | "AGENT_REPORT" | "LAUNCH_AGENT" | "CLOSE_AGENT" | "RESUME_AGENTS" | "STATUS" | "BUS_SEND" | "BUS_CHECK" | "LAUNCH_GROUP" | "STOP_GROUP";
|
|
5
|
+
/**
|
|
6
|
+
* Incoming IPC request.
|
|
7
|
+
*/
|
|
8
|
+
export interface IpcRequest {
|
|
9
|
+
type: IpcRequestType;
|
|
10
|
+
data: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Outgoing IPC response.
|
|
14
|
+
*/
|
|
15
|
+
export interface IpcResponse {
|
|
16
|
+
success: boolean;
|
|
17
|
+
type: string;
|
|
18
|
+
data?: Record<string, unknown>;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Handler function signature for processing requests. */
|
|
22
|
+
export type IpcRequestHandler = (req: IpcRequest) => Promise<IpcResponse>;
|
|
23
|
+
/**
|
|
24
|
+
* Unix domain socket IPC server.
|
|
25
|
+
*
|
|
26
|
+
* Protocol: newline-delimited JSON (each message is a JSON object
|
|
27
|
+
* followed by a newline character).
|
|
28
|
+
*/
|
|
29
|
+
export declare class IpcServer {
|
|
30
|
+
private readonly socketPath;
|
|
31
|
+
private readonly handler;
|
|
32
|
+
private server;
|
|
33
|
+
private sockets;
|
|
34
|
+
constructor(socketPath: string, handler: IpcRequestHandler);
|
|
35
|
+
/**
|
|
36
|
+
* Start listening on the Unix socket.
|
|
37
|
+
*/
|
|
38
|
+
start(): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Stop the IPC server and close all connections.
|
|
41
|
+
*/
|
|
42
|
+
stop(): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Send a message to all connected clients.
|
|
45
|
+
*/
|
|
46
|
+
broadcast(payload: IpcResponse): void;
|
|
47
|
+
/**
|
|
48
|
+
* Get the number of connected clients.
|
|
49
|
+
*/
|
|
50
|
+
get clientCount(): number;
|
|
51
|
+
/**
|
|
52
|
+
* Handle a new socket connection.
|
|
53
|
+
*/
|
|
54
|
+
private handleConnection;
|
|
55
|
+
/**
|
|
56
|
+
* Process a single JSON line from a client.
|
|
57
|
+
*/
|
|
58
|
+
private processLine;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=ipc-server.d.ts.map
|