aemeathcli 1.0.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 +607 -0
- package/dist/App-P4MYD4QY.js +2719 -0
- package/dist/App-P4MYD4QY.js.map +1 -0
- package/dist/api-key-fallback-YQQBOQIL.js +11 -0
- package/dist/api-key-fallback-YQQBOQIL.js.map +1 -0
- package/dist/chunk-4IJD72YB.js +184 -0
- package/dist/chunk-4IJD72YB.js.map +1 -0
- package/dist/chunk-6PDJ45T4.js +325 -0
- package/dist/chunk-6PDJ45T4.js.map +1 -0
- package/dist/chunk-CARHU3DO.js +562 -0
- package/dist/chunk-CARHU3DO.js.map +1 -0
- package/dist/chunk-CGEV3ARR.js +80 -0
- package/dist/chunk-CGEV3ARR.js.map +1 -0
- package/dist/chunk-CS5X3BWX.js +27 -0
- package/dist/chunk-CS5X3BWX.js.map +1 -0
- package/dist/chunk-CYQNBB25.js +44 -0
- package/dist/chunk-CYQNBB25.js.map +1 -0
- package/dist/chunk-DAHGLHNR.js +657 -0
- package/dist/chunk-DAHGLHNR.js.map +1 -0
- package/dist/chunk-H66O5Z2V.js +305 -0
- package/dist/chunk-H66O5Z2V.js.map +1 -0
- package/dist/chunk-HCIHOHLX.js +322 -0
- package/dist/chunk-HCIHOHLX.js.map +1 -0
- package/dist/chunk-HMJRPNPZ.js +1031 -0
- package/dist/chunk-HMJRPNPZ.js.map +1 -0
- package/dist/chunk-I5PZ4JTS.js +119 -0
- package/dist/chunk-I5PZ4JTS.js.map +1 -0
- package/dist/chunk-IYW62KKR.js +255 -0
- package/dist/chunk-IYW62KKR.js.map +1 -0
- package/dist/chunk-JAXXTYID.js +51 -0
- package/dist/chunk-JAXXTYID.js.map +1 -0
- package/dist/chunk-LSOYPSAT.js +183 -0
- package/dist/chunk-LSOYPSAT.js.map +1 -0
- package/dist/chunk-MFBHNWGV.js +416 -0
- package/dist/chunk-MFBHNWGV.js.map +1 -0
- package/dist/chunk-MXZSI3AY.js +311 -0
- package/dist/chunk-MXZSI3AY.js.map +1 -0
- package/dist/chunk-NBR3GHMT.js +72 -0
- package/dist/chunk-NBR3GHMT.js.map +1 -0
- package/dist/chunk-O3ZF22SW.js +246 -0
- package/dist/chunk-O3ZF22SW.js.map +1 -0
- package/dist/chunk-SUSJPZU2.js +181 -0
- package/dist/chunk-SUSJPZU2.js.map +1 -0
- package/dist/chunk-TEVZS4FA.js +310 -0
- package/dist/chunk-TEVZS4FA.js.map +1 -0
- package/dist/chunk-UY2SYSEZ.js +211 -0
- package/dist/chunk-UY2SYSEZ.js.map +1 -0
- package/dist/chunk-WAHVZH7V.js +260 -0
- package/dist/chunk-WAHVZH7V.js.map +1 -0
- package/dist/chunk-WPP3PEDE.js +234 -0
- package/dist/chunk-WPP3PEDE.js.map +1 -0
- package/dist/chunk-Y5XVD2CD.js +1610 -0
- package/dist/chunk-Y5XVD2CD.js.map +1 -0
- package/dist/chunk-YL5XFHR3.js +56 -0
- package/dist/chunk-YL5XFHR3.js.map +1 -0
- package/dist/chunk-ZGOHARPV.js +122 -0
- package/dist/chunk-ZGOHARPV.js.map +1 -0
- package/dist/claude-adapter-QMLFMSP3.js +6 -0
- package/dist/claude-adapter-QMLFMSP3.js.map +1 -0
- package/dist/claude-login-5WELXPKT.js +324 -0
- package/dist/claude-login-5WELXPKT.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +703 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-login-7HHLJHBF.js +164 -0
- package/dist/codex-login-7HHLJHBF.js.map +1 -0
- package/dist/config-store-W6FBCQAQ.js +6 -0
- package/dist/config-store-W6FBCQAQ.js.map +1 -0
- package/dist/executor-6RIKIGXK.js +4 -0
- package/dist/executor-6RIKIGXK.js.map +1 -0
- package/dist/gemini-adapter-6JIHZ7WI.js +6 -0
- package/dist/gemini-adapter-6JIHZ7WI.js.map +1 -0
- package/dist/gemini-login-ZZLYC3J6.js +346 -0
- package/dist/gemini-login-ZZLYC3J6.js.map +1 -0
- package/dist/index.d.ts +2210 -0
- package/dist/index.js +1419 -0
- package/dist/index.js.map +1 -0
- package/dist/kimi-adapter-JN4HFFHU.js +6 -0
- package/dist/kimi-adapter-JN4HFFHU.js.map +1 -0
- package/dist/kimi-login-CZPS63NK.js +149 -0
- package/dist/kimi-login-CZPS63NK.js.map +1 -0
- package/dist/native-cli-adapters-OLW3XX57.js +6 -0
- package/dist/native-cli-adapters-OLW3XX57.js.map +1 -0
- package/dist/ollama-adapter-OJQ3FKWK.js +6 -0
- package/dist/ollama-adapter-OJQ3FKWK.js.map +1 -0
- package/dist/openai-adapter-XU46EN7B.js +6 -0
- package/dist/openai-adapter-XU46EN7B.js.map +1 -0
- package/dist/registry-4KD24ZC3.js +6 -0
- package/dist/registry-4KD24ZC3.js.map +1 -0
- package/dist/registry-H7B3AHPQ.js +5 -0
- package/dist/registry-H7B3AHPQ.js.map +1 -0
- package/dist/server-manager-PTGBHCLS.js +5 -0
- package/dist/server-manager-PTGBHCLS.js.map +1 -0
- package/dist/session-manager-ECEEACGY.js +12 -0
- package/dist/session-manager-ECEEACGY.js.map +1 -0
- package/dist/team-manager-HC4XGCFY.js +11 -0
- package/dist/team-manager-HC4XGCFY.js.map +1 -0
- package/dist/tmux-manager-GPYZ3WQH.js +6 -0
- package/dist/tmux-manager-GPYZ3WQH.js.map +1 -0
- package/dist/tools-TSMXMHIF.js +6 -0
- package/dist/tools-TSMXMHIF.js.map +1 -0
- package/package.json +89 -0
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
import { getEventBus } from './chunk-YL5XFHR3.js';
|
|
2
|
+
import { getIPCSocketPath, getTasksDir, ensureDirectory, getTeamsDir } from './chunk-NBR3GHMT.js';
|
|
3
|
+
import { AgentSpawnError } from './chunk-ZGOHARPV.js';
|
|
4
|
+
import { logger } from './chunk-JAXXTYID.js';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { writeFileSync, renameSync, unlinkSync, existsSync, readFileSync, readdirSync, rmSync } from 'fs';
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
import { fork } from 'child_process';
|
|
9
|
+
|
|
10
|
+
var DEFAULT_SHUTDOWN_TIMEOUT_MS = 1e4;
|
|
11
|
+
var DEFAULT_REGISTRATION_TIMEOUT_MS = 15e3;
|
|
12
|
+
var AgentProcess = class {
|
|
13
|
+
config;
|
|
14
|
+
teamName;
|
|
15
|
+
sessionId;
|
|
16
|
+
cliEntryPoint;
|
|
17
|
+
customEnv;
|
|
18
|
+
shutdownTimeoutMs;
|
|
19
|
+
registrationTimeoutMs;
|
|
20
|
+
messageCallbacks = /* @__PURE__ */ new Set();
|
|
21
|
+
child = null;
|
|
22
|
+
status = "idle";
|
|
23
|
+
currentTaskId;
|
|
24
|
+
nextMessageId = 1;
|
|
25
|
+
constructor(config, options) {
|
|
26
|
+
this.config = config;
|
|
27
|
+
this.teamName = options.teamName;
|
|
28
|
+
this.sessionId = options.sessionId;
|
|
29
|
+
this.cliEntryPoint = options.cliEntryPoint ?? process.argv[1] ?? "aemeathcli";
|
|
30
|
+
this.customEnv = options.env ?? {};
|
|
31
|
+
this.shutdownTimeoutMs = options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
32
|
+
this.registrationTimeoutMs = options.registrationTimeoutMs ?? DEFAULT_REGISTRATION_TIMEOUT_MS;
|
|
33
|
+
}
|
|
34
|
+
/** Spawn the child process. Throws AgentSpawnError on failure. */
|
|
35
|
+
async start() {
|
|
36
|
+
if (this.child) {
|
|
37
|
+
throw new AgentSpawnError(
|
|
38
|
+
this.config.name,
|
|
39
|
+
"Agent process already running"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const args = [
|
|
43
|
+
"--agent",
|
|
44
|
+
"--team",
|
|
45
|
+
this.teamName,
|
|
46
|
+
"--name",
|
|
47
|
+
this.config.name,
|
|
48
|
+
"--model",
|
|
49
|
+
this.config.model,
|
|
50
|
+
"--role",
|
|
51
|
+
this.config.role
|
|
52
|
+
];
|
|
53
|
+
const socketPath = getIPCSocketPath(this.sessionId);
|
|
54
|
+
const env = {
|
|
55
|
+
...process.env,
|
|
56
|
+
...this.customEnv,
|
|
57
|
+
AEMEATHCLI_AGENT_MODE: "1",
|
|
58
|
+
AEMEATHCLI_TEAM_NAME: this.teamName,
|
|
59
|
+
AEMEATHCLI_AGENT_ID: this.config.agentId,
|
|
60
|
+
AEMEATHCLI_AGENT_NAME: this.config.name,
|
|
61
|
+
AEMEATHCLI_IPC_SOCKET: socketPath,
|
|
62
|
+
// Prefer SDK adapters when API keys are available (not OAuth/native login).
|
|
63
|
+
// Native login credentials always use native CLI adapters regardless of
|
|
64
|
+
// this flag — the registry enforces this to avoid "invalid API key" errors.
|
|
65
|
+
AEMEATHCLI_PREFER_SDK: "1",
|
|
66
|
+
// Increase timeout for native CLI fallback (agent tasks can be long).
|
|
67
|
+
AEMEATHCLI_NATIVE_CLI_TIMEOUT_MS: "300000"
|
|
68
|
+
};
|
|
69
|
+
try {
|
|
70
|
+
this.child = fork(this.cliEntryPoint, args, {
|
|
71
|
+
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
|
72
|
+
env,
|
|
73
|
+
detached: false
|
|
74
|
+
});
|
|
75
|
+
this.setupChildListeners();
|
|
76
|
+
this.setStatus("idle");
|
|
77
|
+
await this.waitForRegistration();
|
|
78
|
+
getEventBus().emit("agent:spawned", {
|
|
79
|
+
agentName: this.config.name,
|
|
80
|
+
model: this.config.model
|
|
81
|
+
});
|
|
82
|
+
logger.info(
|
|
83
|
+
{
|
|
84
|
+
agent: this.config.name,
|
|
85
|
+
pid: this.child.pid,
|
|
86
|
+
model: this.config.model
|
|
87
|
+
},
|
|
88
|
+
"Agent process spawned"
|
|
89
|
+
);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (this.child) {
|
|
92
|
+
this.child.kill("SIGTERM");
|
|
93
|
+
this.child = null;
|
|
94
|
+
}
|
|
95
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
96
|
+
throw new AgentSpawnError(this.config.name, reason);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Gracefully stop the agent. Falls back to SIGTERM after timeout. */
|
|
100
|
+
async stop() {
|
|
101
|
+
if (!this.child) return;
|
|
102
|
+
const child = this.child;
|
|
103
|
+
this.sendIPC("hub.shutdown", { reason: "team_cleanup" });
|
|
104
|
+
await new Promise((resolve) => {
|
|
105
|
+
const timer = setTimeout(() => {
|
|
106
|
+
if (child.exitCode === null) {
|
|
107
|
+
logger.warn(
|
|
108
|
+
{ agent: this.config.name },
|
|
109
|
+
"Force-killing unresponsive agent"
|
|
110
|
+
);
|
|
111
|
+
child.kill("SIGTERM");
|
|
112
|
+
}
|
|
113
|
+
resolve();
|
|
114
|
+
}, this.shutdownTimeoutMs);
|
|
115
|
+
child.once("exit", () => {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
resolve();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
this.cleanup();
|
|
121
|
+
logger.info({ agent: this.config.name }, "Agent process stopped");
|
|
122
|
+
}
|
|
123
|
+
/** Kill and restart the agent process. */
|
|
124
|
+
async restart() {
|
|
125
|
+
await this.stop();
|
|
126
|
+
await this.start();
|
|
127
|
+
}
|
|
128
|
+
/** Send a JSON-RPC 2.0 message to the child process. Returns message ID. */
|
|
129
|
+
sendIPC(method, params) {
|
|
130
|
+
if (!this.child?.connected) {
|
|
131
|
+
logger.warn(
|
|
132
|
+
{ agent: this.config.name, method },
|
|
133
|
+
"Cannot send IPC: child not connected"
|
|
134
|
+
);
|
|
135
|
+
return -1;
|
|
136
|
+
}
|
|
137
|
+
const id = this.nextMessageId++;
|
|
138
|
+
const message = {
|
|
139
|
+
jsonrpc: "2.0",
|
|
140
|
+
method,
|
|
141
|
+
params,
|
|
142
|
+
id
|
|
143
|
+
};
|
|
144
|
+
this.child.send(message);
|
|
145
|
+
return id;
|
|
146
|
+
}
|
|
147
|
+
/** Assign a task to this agent via IPC. */
|
|
148
|
+
assignTask(taskId, subject, description) {
|
|
149
|
+
this.currentTaskId = taskId;
|
|
150
|
+
return this.sendIPC("hub.taskAssign", { taskId, subject, description });
|
|
151
|
+
}
|
|
152
|
+
/** Register a callback for IPC messages from the child process. */
|
|
153
|
+
onMessage(callback) {
|
|
154
|
+
this.messageCallbacks.add(callback);
|
|
155
|
+
return () => {
|
|
156
|
+
this.messageCallbacks.delete(callback);
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** Get the current agent state snapshot. */
|
|
160
|
+
getState() {
|
|
161
|
+
return {
|
|
162
|
+
config: this.config,
|
|
163
|
+
status: this.status,
|
|
164
|
+
currentTaskId: this.currentTaskId
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** Get the current status. */
|
|
168
|
+
getStatus() {
|
|
169
|
+
return this.status;
|
|
170
|
+
}
|
|
171
|
+
/** Get the child process PID, or undefined if not running. */
|
|
172
|
+
getPid() {
|
|
173
|
+
return this.child?.pid;
|
|
174
|
+
}
|
|
175
|
+
/** Check if the child process is alive. */
|
|
176
|
+
isAlive() {
|
|
177
|
+
return this.child !== null && this.child.exitCode === null;
|
|
178
|
+
}
|
|
179
|
+
// ── Private ──────────────────────────────────────────────────────────
|
|
180
|
+
setupChildListeners() {
|
|
181
|
+
const child = this.child;
|
|
182
|
+
if (!child) return;
|
|
183
|
+
child.stdout?.on("data", (chunk) => {
|
|
184
|
+
const content = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
185
|
+
if (content.length > 0) {
|
|
186
|
+
logger.debug(
|
|
187
|
+
{ agent: this.config.name, bytes: content.length },
|
|
188
|
+
"Agent stdout (suppressed from UI)"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
child.stderr?.on("data", (chunk) => {
|
|
193
|
+
const content = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
194
|
+
if (content.length > 0) {
|
|
195
|
+
logger.warn(
|
|
196
|
+
{ agent: this.config.name, stderr: content.slice(0, 200) },
|
|
197
|
+
"Agent stderr"
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
child.on("message", (raw) => {
|
|
202
|
+
this.handleChildMessage(raw);
|
|
203
|
+
});
|
|
204
|
+
child.on("error", (error) => {
|
|
205
|
+
logger.error(
|
|
206
|
+
{ agent: this.config.name, error: error.message },
|
|
207
|
+
"Agent process error"
|
|
208
|
+
);
|
|
209
|
+
this.setStatus("error");
|
|
210
|
+
});
|
|
211
|
+
child.on("exit", (code, signal) => {
|
|
212
|
+
logger.info(
|
|
213
|
+
{ agent: this.config.name, code, signal },
|
|
214
|
+
"Agent process exited"
|
|
215
|
+
);
|
|
216
|
+
this.setStatus("shutdown");
|
|
217
|
+
this.child = null;
|
|
218
|
+
});
|
|
219
|
+
child.on("disconnect", () => {
|
|
220
|
+
logger.debug({ agent: this.config.name }, "Agent IPC disconnected");
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
handleChildMessage(raw) {
|
|
224
|
+
if (!isIPCMessage(raw)) {
|
|
225
|
+
logger.warn(
|
|
226
|
+
{ agent: this.config.name },
|
|
227
|
+
"Received non-IPC message from child"
|
|
228
|
+
);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const { method, params } = raw;
|
|
232
|
+
switch (method) {
|
|
233
|
+
case "agent.register":
|
|
234
|
+
logger.debug({ agent: this.config.name }, "Agent registered via IPC");
|
|
235
|
+
break;
|
|
236
|
+
case "agent.taskUpdate": {
|
|
237
|
+
const taskStatus = params["status"];
|
|
238
|
+
if (typeof taskStatus === "string") {
|
|
239
|
+
if (taskStatus === "in_progress") {
|
|
240
|
+
this.setStatus("active");
|
|
241
|
+
} else if (taskStatus === "completed") {
|
|
242
|
+
this.currentTaskId = void 0;
|
|
243
|
+
this.setStatus("idle");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
this.notifyCallbacks(method, params);
|
|
250
|
+
}
|
|
251
|
+
/** Wait for the child to send agent.register. Rejects on timeout or early exit. */
|
|
252
|
+
async waitForRegistration() {
|
|
253
|
+
return new Promise((resolve, reject) => {
|
|
254
|
+
const timeout = setTimeout(() => {
|
|
255
|
+
this.child?.removeListener("message", onMessage);
|
|
256
|
+
reject(new Error("Agent registration timeout"));
|
|
257
|
+
}, this.registrationTimeoutMs);
|
|
258
|
+
const onMessage = (raw) => {
|
|
259
|
+
if (isIPCMessage(raw) && raw.method === "agent.register") {
|
|
260
|
+
clearTimeout(timeout);
|
|
261
|
+
this.child?.removeListener("message", onMessage);
|
|
262
|
+
this.child?.removeListener("exit", onExit);
|
|
263
|
+
resolve();
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
const onExit = () => {
|
|
267
|
+
clearTimeout(timeout);
|
|
268
|
+
this.child?.removeListener("message", onMessage);
|
|
269
|
+
reject(new Error("Agent process exited before registration"));
|
|
270
|
+
};
|
|
271
|
+
this.child?.on("message", onMessage);
|
|
272
|
+
this.child?.once("exit", onExit);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
setStatus(status) {
|
|
276
|
+
this.status = status;
|
|
277
|
+
getEventBus().emit("agent:status", {
|
|
278
|
+
agentName: this.config.name,
|
|
279
|
+
status
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
notifyCallbacks(method, params) {
|
|
283
|
+
for (const callback of this.messageCallbacks) {
|
|
284
|
+
try {
|
|
285
|
+
callback(method, params);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
288
|
+
logger.error(
|
|
289
|
+
{ agent: this.config.name, error: reason },
|
|
290
|
+
"Message callback threw"
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
cleanup() {
|
|
296
|
+
this.child = null;
|
|
297
|
+
this.currentTaskId = void 0;
|
|
298
|
+
this.messageCallbacks.clear();
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
function isIPCMessage(value) {
|
|
302
|
+
if (typeof value !== "object" || value === null) return false;
|
|
303
|
+
const obj = value;
|
|
304
|
+
return obj["jsonrpc"] === "2.0" && typeof obj["method"] === "string" && typeof obj["params"] === "object" && obj["params"] !== null;
|
|
305
|
+
}
|
|
306
|
+
var MessageBus = class {
|
|
307
|
+
handlers = /* @__PURE__ */ new Map();
|
|
308
|
+
queues = /* @__PURE__ */ new Map();
|
|
309
|
+
statuses = /* @__PURE__ */ new Map();
|
|
310
|
+
transport;
|
|
311
|
+
transportUnsubscribe;
|
|
312
|
+
destroyed = false;
|
|
313
|
+
constructor(options) {
|
|
314
|
+
this.transport = options?.transport;
|
|
315
|
+
if (this.transport) {
|
|
316
|
+
this.transportUnsubscribe = this.transport.onReceive((message) => {
|
|
317
|
+
this.routeIncoming(message);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/** Register an agent as available for message delivery. */
|
|
322
|
+
registerAgent(agentId) {
|
|
323
|
+
if (!this.handlers.has(agentId)) {
|
|
324
|
+
this.handlers.set(agentId, /* @__PURE__ */ new Set());
|
|
325
|
+
}
|
|
326
|
+
if (!this.queues.has(agentId)) {
|
|
327
|
+
this.queues.set(agentId, []);
|
|
328
|
+
}
|
|
329
|
+
this.statuses.set(agentId, "idle");
|
|
330
|
+
logger.debug({ agentId }, "Agent registered on message bus");
|
|
331
|
+
}
|
|
332
|
+
/** Remove an agent from the bus. Pending messages are discarded. */
|
|
333
|
+
unregisterAgent(agentId) {
|
|
334
|
+
this.handlers.delete(agentId);
|
|
335
|
+
this.queues.delete(agentId);
|
|
336
|
+
this.statuses.delete(agentId);
|
|
337
|
+
logger.debug({ agentId }, "Agent unregistered from message bus");
|
|
338
|
+
}
|
|
339
|
+
/** Subscribe to messages delivered to an agent. Returns unsubscribe function. */
|
|
340
|
+
subscribe(agentId, handler) {
|
|
341
|
+
let handlerSet = this.handlers.get(agentId);
|
|
342
|
+
if (!handlerSet) {
|
|
343
|
+
handlerSet = /* @__PURE__ */ new Set();
|
|
344
|
+
this.handlers.set(agentId, handlerSet);
|
|
345
|
+
}
|
|
346
|
+
handlerSet.add(handler);
|
|
347
|
+
return () => {
|
|
348
|
+
handlerSet.delete(handler);
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
/** Route a message to its recipient(s). Returns true if delivered or queued. */
|
|
352
|
+
send(message) {
|
|
353
|
+
if (this.destroyed) {
|
|
354
|
+
logger.warn("MessageBus is destroyed, dropping message");
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
if (message.type === "broadcast") {
|
|
358
|
+
return this.broadcastToAll(message);
|
|
359
|
+
}
|
|
360
|
+
if (!message.recipientId) {
|
|
361
|
+
logger.warn(
|
|
362
|
+
{ type: message.type, senderId: message.senderId },
|
|
363
|
+
"Non-broadcast message missing recipientId"
|
|
364
|
+
);
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
return this.deliverToAgent(message.recipientId, message);
|
|
368
|
+
}
|
|
369
|
+
/** Create a well-formed message and send it. Returns the created message. */
|
|
370
|
+
createAndSend(type, senderId, recipientId, content, extra) {
|
|
371
|
+
const message = {
|
|
372
|
+
type,
|
|
373
|
+
senderId,
|
|
374
|
+
recipientId,
|
|
375
|
+
content,
|
|
376
|
+
summary: extra?.summary,
|
|
377
|
+
requestId: extra?.requestId ?? randomUUID(),
|
|
378
|
+
approve: extra?.approve,
|
|
379
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
380
|
+
};
|
|
381
|
+
this.send(message);
|
|
382
|
+
return message;
|
|
383
|
+
}
|
|
384
|
+
/** Update an agent's status. Drains the queue when transitioning to idle. */
|
|
385
|
+
setAgentStatus(agentId, status) {
|
|
386
|
+
const previous = this.statuses.get(agentId);
|
|
387
|
+
this.statuses.set(agentId, status);
|
|
388
|
+
if (status === "idle" && previous === "active") {
|
|
389
|
+
this.drainQueue(agentId);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/** Get the number of queued messages for an agent. */
|
|
393
|
+
getQueueSize(agentId) {
|
|
394
|
+
return this.queues.get(agentId)?.length ?? 0;
|
|
395
|
+
}
|
|
396
|
+
/** Get all registered agent IDs. */
|
|
397
|
+
getRegisteredAgents() {
|
|
398
|
+
return [...this.handlers.keys()];
|
|
399
|
+
}
|
|
400
|
+
/** Tear down the message bus and release resources. */
|
|
401
|
+
destroy() {
|
|
402
|
+
this.destroyed = true;
|
|
403
|
+
this.transportUnsubscribe?.();
|
|
404
|
+
this.handlers.clear();
|
|
405
|
+
this.queues.clear();
|
|
406
|
+
this.statuses.clear();
|
|
407
|
+
logger.debug("MessageBus destroyed");
|
|
408
|
+
}
|
|
409
|
+
// ── Private Routing ─────────────────────────────────────────────────
|
|
410
|
+
broadcastToAll(message) {
|
|
411
|
+
let delivered = false;
|
|
412
|
+
for (const agentId of this.handlers.keys()) {
|
|
413
|
+
if (agentId === message.senderId) continue;
|
|
414
|
+
const copy = { ...message, recipientId: agentId };
|
|
415
|
+
this.deliverToAgent(agentId, copy);
|
|
416
|
+
delivered = true;
|
|
417
|
+
}
|
|
418
|
+
return delivered;
|
|
419
|
+
}
|
|
420
|
+
deliverToAgent(agentId, message) {
|
|
421
|
+
const status = this.statuses.get(agentId);
|
|
422
|
+
if (status === "active") {
|
|
423
|
+
this.enqueue(agentId, message);
|
|
424
|
+
logger.debug(
|
|
425
|
+
{ agentId, type: message.type },
|
|
426
|
+
"Agent busy, message queued"
|
|
427
|
+
);
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
const handlerSet = this.handlers.get(agentId);
|
|
431
|
+
if (handlerSet && handlerSet.size > 0) {
|
|
432
|
+
this.invokeHandlers(agentId, handlerSet, message);
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
if (this.transport) {
|
|
436
|
+
this.transport.send(agentId, message).catch((error) => {
|
|
437
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
438
|
+
logger.error({ agentId, error: reason }, "Transport delivery failed");
|
|
439
|
+
});
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
this.enqueue(agentId, message);
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
invokeHandlers(agentId, handlerSet, message) {
|
|
446
|
+
for (const handler of handlerSet) {
|
|
447
|
+
try {
|
|
448
|
+
handler(message);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
451
|
+
logger.error({ agentId, error: reason }, "Message handler threw");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
getEventBus().emit("agent:message", {
|
|
455
|
+
from: message.senderId,
|
|
456
|
+
to: agentId,
|
|
457
|
+
content: message.content
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
enqueue(agentId, message) {
|
|
461
|
+
let queue = this.queues.get(agentId);
|
|
462
|
+
if (!queue) {
|
|
463
|
+
queue = [];
|
|
464
|
+
this.queues.set(agentId, queue);
|
|
465
|
+
}
|
|
466
|
+
queue.push(message);
|
|
467
|
+
}
|
|
468
|
+
drainQueue(agentId) {
|
|
469
|
+
const queue = this.queues.get(agentId);
|
|
470
|
+
if (!queue || queue.length === 0) return;
|
|
471
|
+
const handlerSet = this.handlers.get(agentId);
|
|
472
|
+
if (!handlerSet || handlerSet.size === 0) return;
|
|
473
|
+
const pending = queue.splice(0);
|
|
474
|
+
logger.debug({ agentId, count: pending.length }, "Draining message queue");
|
|
475
|
+
for (const message of pending) {
|
|
476
|
+
this.invokeHandlers(agentId, handlerSet, message);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
routeIncoming(message) {
|
|
480
|
+
if (message.type === "broadcast") {
|
|
481
|
+
this.broadcastToAll(message);
|
|
482
|
+
} else if (message.recipientId) {
|
|
483
|
+
this.deliverToAgent(message.recipientId, message);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
var TaskStore = class _TaskStore {
|
|
488
|
+
storeDir;
|
|
489
|
+
constructor(teamName) {
|
|
490
|
+
this.storeDir = join(getTasksDir(), teamName);
|
|
491
|
+
ensureDirectory(this.storeDir);
|
|
492
|
+
this.cleanupTempFiles();
|
|
493
|
+
}
|
|
494
|
+
/** Persist a task to disk with atomic write (temp + rename). */
|
|
495
|
+
save(task) {
|
|
496
|
+
const serialized = _TaskStore.serialize(task);
|
|
497
|
+
const filePath = this.getTaskFilePath(task.id);
|
|
498
|
+
const tmpPath = `${filePath}.tmp`;
|
|
499
|
+
try {
|
|
500
|
+
writeFileSync(tmpPath, JSON.stringify(serialized, null, 2), {
|
|
501
|
+
encoding: "utf-8",
|
|
502
|
+
mode: 420
|
|
503
|
+
});
|
|
504
|
+
renameSync(tmpPath, filePath);
|
|
505
|
+
logger.debug({ taskId: task.id }, "Task persisted to disk");
|
|
506
|
+
} catch (error) {
|
|
507
|
+
try {
|
|
508
|
+
unlinkSync(tmpPath);
|
|
509
|
+
} catch {
|
|
510
|
+
}
|
|
511
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
512
|
+
logger.error({ taskId: task.id, error: reason }, "Failed to save task");
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/** Load a single task by ID. Throws if not found. */
|
|
517
|
+
load(taskId) {
|
|
518
|
+
const filePath = this.getTaskFilePath(taskId);
|
|
519
|
+
if (!existsSync(filePath)) {
|
|
520
|
+
throw new Error(`Task file not found: ${taskId}`);
|
|
521
|
+
}
|
|
522
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
523
|
+
const data = JSON.parse(raw);
|
|
524
|
+
return _TaskStore.deserialize(data);
|
|
525
|
+
}
|
|
526
|
+
/** Load all tasks for this team, sorted by creation time. */
|
|
527
|
+
loadAll() {
|
|
528
|
+
if (!existsSync(this.storeDir)) {
|
|
529
|
+
return [];
|
|
530
|
+
}
|
|
531
|
+
const files = readdirSync(this.storeDir).filter(
|
|
532
|
+
(f) => f.endsWith(".json") && !f.endsWith(".tmp")
|
|
533
|
+
);
|
|
534
|
+
const tasks = [];
|
|
535
|
+
for (const file of files) {
|
|
536
|
+
try {
|
|
537
|
+
const raw = readFileSync(join(this.storeDir, file), "utf-8");
|
|
538
|
+
const data = JSON.parse(raw);
|
|
539
|
+
tasks.push(_TaskStore.deserialize(data));
|
|
540
|
+
} catch (error) {
|
|
541
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
542
|
+
logger.warn({ file, error: reason }, "Skipping corrupt task file");
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return tasks.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
546
|
+
}
|
|
547
|
+
/** Remove a task file from disk. Returns true if deleted. */
|
|
548
|
+
remove(taskId) {
|
|
549
|
+
const filePath = this.getTaskFilePath(taskId);
|
|
550
|
+
if (!existsSync(filePath)) {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
unlinkSync(filePath);
|
|
555
|
+
logger.debug({ taskId }, "Task file removed");
|
|
556
|
+
return true;
|
|
557
|
+
} catch (error) {
|
|
558
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
559
|
+
logger.error({ taskId, error: reason }, "Failed to remove task file");
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/** Check whether a task file exists on disk. */
|
|
564
|
+
exists(taskId) {
|
|
565
|
+
return existsSync(this.getTaskFilePath(taskId));
|
|
566
|
+
}
|
|
567
|
+
/** Absolute path to this team's task store directory. */
|
|
568
|
+
getStorePath() {
|
|
569
|
+
return this.storeDir;
|
|
570
|
+
}
|
|
571
|
+
// ── Private ──────────────────────────────────────────────────────────
|
|
572
|
+
getTaskFilePath(taskId) {
|
|
573
|
+
return join(this.storeDir, `${taskId}.json`);
|
|
574
|
+
}
|
|
575
|
+
/** Remove orphaned .tmp files left from interrupted writes. */
|
|
576
|
+
cleanupTempFiles() {
|
|
577
|
+
if (!existsSync(this.storeDir)) return;
|
|
578
|
+
const tmpFiles = readdirSync(this.storeDir).filter(
|
|
579
|
+
(f) => f.endsWith(".tmp")
|
|
580
|
+
);
|
|
581
|
+
for (const file of tmpFiles) {
|
|
582
|
+
try {
|
|
583
|
+
unlinkSync(join(this.storeDir, file));
|
|
584
|
+
} catch {
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (tmpFiles.length > 0) {
|
|
588
|
+
logger.info(
|
|
589
|
+
{ count: tmpFiles.length },
|
|
590
|
+
"Cleaned up stale temp task files"
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
static serialize(task) {
|
|
595
|
+
return {
|
|
596
|
+
id: task.id,
|
|
597
|
+
subject: task.subject,
|
|
598
|
+
description: task.description,
|
|
599
|
+
status: task.status,
|
|
600
|
+
owner: task.owner,
|
|
601
|
+
model: task.model,
|
|
602
|
+
role: task.role,
|
|
603
|
+
blocks: [...task.blocks],
|
|
604
|
+
blockedBy: [...task.blockedBy],
|
|
605
|
+
createdAt: task.createdAt.toISOString(),
|
|
606
|
+
updatedAt: task.updatedAt.toISOString()
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
static deserialize(data) {
|
|
610
|
+
return {
|
|
611
|
+
id: data.id,
|
|
612
|
+
subject: data.subject,
|
|
613
|
+
description: data.description,
|
|
614
|
+
status: data.status,
|
|
615
|
+
owner: data.owner,
|
|
616
|
+
model: data.model,
|
|
617
|
+
role: data.role,
|
|
618
|
+
blocks: [...data.blocks],
|
|
619
|
+
blockedBy: [...data.blockedBy],
|
|
620
|
+
createdAt: new Date(data.createdAt),
|
|
621
|
+
updatedAt: new Date(data.updatedAt)
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
var DEFAULT_TIMEOUT_MS = 3e5;
|
|
626
|
+
var DEFAULT_LEADER_ID = "leader";
|
|
627
|
+
var PlanApproval = class {
|
|
628
|
+
messageBus;
|
|
629
|
+
leaderId;
|
|
630
|
+
timeoutMs;
|
|
631
|
+
pending = /* @__PURE__ */ new Map();
|
|
632
|
+
destroyed = false;
|
|
633
|
+
constructor(messageBus, options) {
|
|
634
|
+
this.messageBus = messageBus;
|
|
635
|
+
this.leaderId = options?.leaderId ?? DEFAULT_LEADER_ID;
|
|
636
|
+
this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Submit a plan for approval. Returns a promise that resolves
|
|
640
|
+
* when the leader approves or rejects, or rejects on timeout.
|
|
641
|
+
*/
|
|
642
|
+
submitPlan(agentId, plan) {
|
|
643
|
+
if (this.destroyed) {
|
|
644
|
+
return Promise.reject(new Error("PlanApproval has been destroyed"));
|
|
645
|
+
}
|
|
646
|
+
const requestId = randomUUID();
|
|
647
|
+
return new Promise((resolve, reject) => {
|
|
648
|
+
const timer = setTimeout(() => {
|
|
649
|
+
this.pending.delete(requestId);
|
|
650
|
+
reject(new Error(`Plan approval timed out after ${this.timeoutMs}ms`));
|
|
651
|
+
}, this.timeoutMs);
|
|
652
|
+
const request = {
|
|
653
|
+
requestId,
|
|
654
|
+
agentId,
|
|
655
|
+
plan,
|
|
656
|
+
submittedAt: /* @__PURE__ */ new Date(),
|
|
657
|
+
resolve,
|
|
658
|
+
reject,
|
|
659
|
+
timer
|
|
660
|
+
};
|
|
661
|
+
this.pending.set(requestId, request);
|
|
662
|
+
this.messageBus.createAndSend(
|
|
663
|
+
"plan_approval_request",
|
|
664
|
+
agentId,
|
|
665
|
+
this.leaderId,
|
|
666
|
+
plan,
|
|
667
|
+
{ requestId, summary: `Plan from ${agentId} awaiting review` }
|
|
668
|
+
);
|
|
669
|
+
logger.info({ requestId, agentId }, "Plan submitted for approval");
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
/** Leader approves a pending plan. Returns false if no matching request. */
|
|
673
|
+
approvePlan(requestId, responderId) {
|
|
674
|
+
const request = this.pending.get(requestId);
|
|
675
|
+
if (!request) {
|
|
676
|
+
logger.warn({ requestId }, "No pending plan for this requestId");
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
clearTimeout(request.timer);
|
|
680
|
+
this.pending.delete(requestId);
|
|
681
|
+
const result = {
|
|
682
|
+
approved: true,
|
|
683
|
+
requestId,
|
|
684
|
+
respondedBy: responderId,
|
|
685
|
+
respondedAt: /* @__PURE__ */ new Date()
|
|
686
|
+
};
|
|
687
|
+
this.messageBus.createAndSend(
|
|
688
|
+
"plan_approval_response",
|
|
689
|
+
responderId,
|
|
690
|
+
request.agentId,
|
|
691
|
+
"Plan approved",
|
|
692
|
+
{ requestId, approve: true }
|
|
693
|
+
);
|
|
694
|
+
request.resolve(result);
|
|
695
|
+
logger.info({ requestId, agentId: request.agentId }, "Plan approved");
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
/** Leader rejects a pending plan with feedback. */
|
|
699
|
+
rejectPlan(requestId, responderId, feedback) {
|
|
700
|
+
const request = this.pending.get(requestId);
|
|
701
|
+
if (!request) {
|
|
702
|
+
logger.warn({ requestId }, "No pending plan for this requestId");
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
clearTimeout(request.timer);
|
|
706
|
+
this.pending.delete(requestId);
|
|
707
|
+
const result = {
|
|
708
|
+
approved: false,
|
|
709
|
+
feedback,
|
|
710
|
+
requestId,
|
|
711
|
+
respondedBy: responderId,
|
|
712
|
+
respondedAt: /* @__PURE__ */ new Date()
|
|
713
|
+
};
|
|
714
|
+
this.messageBus.createAndSend(
|
|
715
|
+
"plan_approval_response",
|
|
716
|
+
responderId,
|
|
717
|
+
request.agentId,
|
|
718
|
+
feedback,
|
|
719
|
+
{ requestId, approve: false }
|
|
720
|
+
);
|
|
721
|
+
request.resolve(result);
|
|
722
|
+
logger.info(
|
|
723
|
+
{ requestId, agentId: request.agentId, feedback },
|
|
724
|
+
"Plan rejected"
|
|
725
|
+
);
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Handle an incoming plan_approval_response message.
|
|
730
|
+
* Call this from a message bus subscription to close the request loop.
|
|
731
|
+
*/
|
|
732
|
+
handleResponse(message) {
|
|
733
|
+
if (message.type !== "plan_approval_response" || !message.requestId) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const request = this.pending.get(message.requestId);
|
|
737
|
+
if (!request) return;
|
|
738
|
+
clearTimeout(request.timer);
|
|
739
|
+
this.pending.delete(message.requestId);
|
|
740
|
+
const result = {
|
|
741
|
+
approved: message.approve === true,
|
|
742
|
+
feedback: message.approve === true ? void 0 : message.content,
|
|
743
|
+
requestId: message.requestId,
|
|
744
|
+
respondedBy: message.senderId,
|
|
745
|
+
respondedAt: /* @__PURE__ */ new Date()
|
|
746
|
+
};
|
|
747
|
+
request.resolve(result);
|
|
748
|
+
}
|
|
749
|
+
/** Cancel a pending plan request. */
|
|
750
|
+
cancelPlan(requestId) {
|
|
751
|
+
const request = this.pending.get(requestId);
|
|
752
|
+
if (!request) return false;
|
|
753
|
+
clearTimeout(request.timer);
|
|
754
|
+
this.pending.delete(requestId);
|
|
755
|
+
request.reject(new Error("Plan approval cancelled"));
|
|
756
|
+
logger.info({ requestId, agentId: request.agentId }, "Plan cancelled");
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
759
|
+
/** Get all pending plan requests (for leader UI). */
|
|
760
|
+
getPendingPlans() {
|
|
761
|
+
return [...this.pending.values()].map((req) => ({
|
|
762
|
+
requestId: req.requestId,
|
|
763
|
+
agentId: req.agentId,
|
|
764
|
+
plan: req.plan,
|
|
765
|
+
submittedAt: req.submittedAt
|
|
766
|
+
}));
|
|
767
|
+
}
|
|
768
|
+
/** Get the count of pending plans. */
|
|
769
|
+
getPendingCount() {
|
|
770
|
+
return this.pending.size;
|
|
771
|
+
}
|
|
772
|
+
/** Tear down: cancel all pending plans and release resources. */
|
|
773
|
+
destroy() {
|
|
774
|
+
this.destroyed = true;
|
|
775
|
+
for (const [, request] of this.pending) {
|
|
776
|
+
clearTimeout(request.timer);
|
|
777
|
+
request.reject(new Error("PlanApproval destroyed"));
|
|
778
|
+
}
|
|
779
|
+
this.pending.clear();
|
|
780
|
+
logger.debug("PlanApproval destroyed");
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// src/teams/team-manager.ts
|
|
785
|
+
var TEAM_NAME_PATTERN = /^[\w-]+$/;
|
|
786
|
+
var TeamManager = class {
|
|
787
|
+
activeTeams = /* @__PURE__ */ new Map();
|
|
788
|
+
/** Create a new team: config, directories, and agent process handles. */
|
|
789
|
+
async createTeam(name, options) {
|
|
790
|
+
if (this.activeTeams.has(name)) {
|
|
791
|
+
throw new Error(`Team already exists: ${name}`);
|
|
792
|
+
}
|
|
793
|
+
if (!TEAM_NAME_PATTERN.test(name)) {
|
|
794
|
+
throw new Error(
|
|
795
|
+
`Invalid team name: "${name}". Use alphanumeric characters, dashes, or underscores.`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
const teamDir = join(getTeamsDir(), name);
|
|
799
|
+
if (existsSync(teamDir)) {
|
|
800
|
+
throw new Error(`Team directory already exists: ${teamDir}`);
|
|
801
|
+
}
|
|
802
|
+
const members = options.agents.map((def) => ({
|
|
803
|
+
name: def.name,
|
|
804
|
+
agentId: randomUUID(),
|
|
805
|
+
agentType: def.agentType,
|
|
806
|
+
model: def.model,
|
|
807
|
+
provider: def.provider,
|
|
808
|
+
role: def.role
|
|
809
|
+
}));
|
|
810
|
+
const config = {
|
|
811
|
+
teamName: name,
|
|
812
|
+
description: options.description,
|
|
813
|
+
status: "active",
|
|
814
|
+
members,
|
|
815
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
816
|
+
};
|
|
817
|
+
ensureDirectory(teamDir);
|
|
818
|
+
ensureDirectory(join(getTasksDir(), name));
|
|
819
|
+
this.saveTeamConfig(name, config);
|
|
820
|
+
const sessionId = options.sessionId ?? randomUUID();
|
|
821
|
+
const messageBus = new MessageBus();
|
|
822
|
+
const taskStore = new TaskStore(name);
|
|
823
|
+
const planApproval = new PlanApproval(messageBus);
|
|
824
|
+
const processes = /* @__PURE__ */ new Map();
|
|
825
|
+
for (const member of members) {
|
|
826
|
+
const processOptions = {
|
|
827
|
+
teamName: name,
|
|
828
|
+
sessionId,
|
|
829
|
+
cliEntryPoint: options.cliEntryPoint
|
|
830
|
+
};
|
|
831
|
+
const agentProcess = new AgentProcess(member, processOptions);
|
|
832
|
+
processes.set(member.name, agentProcess);
|
|
833
|
+
messageBus.registerAgent(member.agentId);
|
|
834
|
+
}
|
|
835
|
+
const activeTeam = {
|
|
836
|
+
config,
|
|
837
|
+
processes,
|
|
838
|
+
messageBus,
|
|
839
|
+
taskStore,
|
|
840
|
+
planApproval,
|
|
841
|
+
sessionId
|
|
842
|
+
};
|
|
843
|
+
this.activeTeams.set(name, activeTeam);
|
|
844
|
+
getEventBus().emit("team:created", {
|
|
845
|
+
teamName: name,
|
|
846
|
+
agentCount: members.length
|
|
847
|
+
});
|
|
848
|
+
logger.info(
|
|
849
|
+
{ team: name, agents: members.length, sessionId },
|
|
850
|
+
"Team created"
|
|
851
|
+
);
|
|
852
|
+
return config;
|
|
853
|
+
}
|
|
854
|
+
/** Start all agent processes for a team. */
|
|
855
|
+
async startAgents(teamName) {
|
|
856
|
+
const team = this.getActiveTeam(teamName);
|
|
857
|
+
const startPromises = [];
|
|
858
|
+
for (const [agentName, agentProcess] of team.processes) {
|
|
859
|
+
startPromises.push(
|
|
860
|
+
agentProcess.start().catch((error) => {
|
|
861
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
862
|
+
logger.error(
|
|
863
|
+
{ team: teamName, agent: agentName, error: reason },
|
|
864
|
+
"Failed to start agent"
|
|
865
|
+
);
|
|
866
|
+
})
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
await Promise.allSettled(startPromises);
|
|
870
|
+
}
|
|
871
|
+
/** Gracefully shutdown and remove a team. */
|
|
872
|
+
async deleteTeam(name) {
|
|
873
|
+
const active = this.activeTeams.get(name);
|
|
874
|
+
if (active) {
|
|
875
|
+
await this.shutdownAgents(active);
|
|
876
|
+
active.planApproval.destroy();
|
|
877
|
+
active.messageBus.destroy();
|
|
878
|
+
this.activeTeams.delete(name);
|
|
879
|
+
}
|
|
880
|
+
const teamDir = join(getTeamsDir(), name);
|
|
881
|
+
if (existsSync(teamDir)) {
|
|
882
|
+
rmSync(teamDir, { recursive: true, force: true });
|
|
883
|
+
}
|
|
884
|
+
const taskDir = join(getTasksDir(), name);
|
|
885
|
+
if (existsSync(taskDir)) {
|
|
886
|
+
rmSync(taskDir, { recursive: true, force: true });
|
|
887
|
+
}
|
|
888
|
+
getEventBus().emit("team:deleted", { teamName: name });
|
|
889
|
+
logger.info({ team: name }, "Team deleted");
|
|
890
|
+
}
|
|
891
|
+
/** List all teams from disk (active and inactive). */
|
|
892
|
+
listTeams() {
|
|
893
|
+
const teamsDir = getTeamsDir();
|
|
894
|
+
if (!existsSync(teamsDir)) return [];
|
|
895
|
+
const entries = readdirSync(teamsDir, { withFileTypes: true });
|
|
896
|
+
const configs = [];
|
|
897
|
+
for (const entry of entries) {
|
|
898
|
+
if (!entry.isDirectory()) continue;
|
|
899
|
+
try {
|
|
900
|
+
const teamConfig = this.loadTeamConfig(entry.name);
|
|
901
|
+
configs.push(teamConfig);
|
|
902
|
+
} catch (error) {
|
|
903
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
904
|
+
logger.warn(
|
|
905
|
+
{ team: entry.name, error: reason },
|
|
906
|
+
"Skipping unreadable team"
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return configs;
|
|
911
|
+
}
|
|
912
|
+
/** Load a team config from disk. Throws if not found. */
|
|
913
|
+
getTeam(name) {
|
|
914
|
+
return this.loadTeamConfig(name);
|
|
915
|
+
}
|
|
916
|
+
/** Get the runtime state of a specific agent within an active team. */
|
|
917
|
+
getAgentState(teamName, agentName) {
|
|
918
|
+
const team = this.activeTeams.get(teamName);
|
|
919
|
+
if (!team) return void 0;
|
|
920
|
+
const agentProcess = team.processes.get(agentName);
|
|
921
|
+
return agentProcess?.getState();
|
|
922
|
+
}
|
|
923
|
+
/** Get all agent states for an active team. */
|
|
924
|
+
getAgentStates(teamName) {
|
|
925
|
+
const team = this.activeTeams.get(teamName);
|
|
926
|
+
if (!team) return [];
|
|
927
|
+
return [...team.processes.values()].map((p) => p.getState());
|
|
928
|
+
}
|
|
929
|
+
/** Get the message bus for an active team. */
|
|
930
|
+
getMessageBus(teamName) {
|
|
931
|
+
return this.activeTeams.get(teamName)?.messageBus;
|
|
932
|
+
}
|
|
933
|
+
/** Get the task store for an active team. */
|
|
934
|
+
getTaskStore(teamName) {
|
|
935
|
+
return this.activeTeams.get(teamName)?.taskStore;
|
|
936
|
+
}
|
|
937
|
+
/** Get the plan approval handler for an active team. */
|
|
938
|
+
getPlanApproval(teamName) {
|
|
939
|
+
return this.activeTeams.get(teamName)?.planApproval;
|
|
940
|
+
}
|
|
941
|
+
/** Register a callback for IPC messages from all agents in a team. Returns cleanup function. */
|
|
942
|
+
onAgentMessages(teamName, callback) {
|
|
943
|
+
const team = this.getActiveTeam(teamName);
|
|
944
|
+
const cleanups = [];
|
|
945
|
+
for (const [agentName, agentProcess] of team.processes) {
|
|
946
|
+
const unsubscribe = agentProcess.onMessage((method, params) => {
|
|
947
|
+
callback(agentName, method, params);
|
|
948
|
+
});
|
|
949
|
+
cleanups.push(unsubscribe);
|
|
950
|
+
}
|
|
951
|
+
return () => {
|
|
952
|
+
for (const cleanup of cleanups) cleanup();
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
/** Assign a task to a specific agent via IPC. */
|
|
956
|
+
assignTask(teamName, agentName, taskId, subject, description) {
|
|
957
|
+
const team = this.getActiveTeam(teamName);
|
|
958
|
+
const agentProcess = team.processes.get(agentName);
|
|
959
|
+
if (agentProcess) {
|
|
960
|
+
agentProcess.assignTask(taskId, subject, description);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
/** Check whether a team is currently active in memory. */
|
|
964
|
+
isTeamActive(name) {
|
|
965
|
+
return this.activeTeams.has(name);
|
|
966
|
+
}
|
|
967
|
+
/** Shut down all active teams. Call during application cleanup. */
|
|
968
|
+
async shutdownAll() {
|
|
969
|
+
const names = [...this.activeTeams.keys()];
|
|
970
|
+
await Promise.allSettled(names.map((n) => this.deleteTeam(n)));
|
|
971
|
+
}
|
|
972
|
+
// ── Private ──────────────────────────────────────────────────────────
|
|
973
|
+
getActiveTeam(teamName) {
|
|
974
|
+
const team = this.activeTeams.get(teamName);
|
|
975
|
+
if (!team) {
|
|
976
|
+
throw new Error(`Team not active: ${teamName}`);
|
|
977
|
+
}
|
|
978
|
+
return team;
|
|
979
|
+
}
|
|
980
|
+
saveTeamConfig(name, config) {
|
|
981
|
+
const teamDir = join(getTeamsDir(), name);
|
|
982
|
+
ensureDirectory(teamDir);
|
|
983
|
+
const serialized = {
|
|
984
|
+
teamName: config.teamName,
|
|
985
|
+
description: config.description,
|
|
986
|
+
status: config.status,
|
|
987
|
+
members: config.members,
|
|
988
|
+
createdAt: config.createdAt.toISOString()
|
|
989
|
+
};
|
|
990
|
+
const configPath = join(teamDir, "config.json");
|
|
991
|
+
writeFileSync(configPath, JSON.stringify(serialized, null, 2), {
|
|
992
|
+
encoding: "utf-8",
|
|
993
|
+
mode: 420
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
loadTeamConfig(name) {
|
|
997
|
+
const configPath = join(getTeamsDir(), name, "config.json");
|
|
998
|
+
if (!existsSync(configPath)) {
|
|
999
|
+
throw new Error(`Team config not found: ${name}`);
|
|
1000
|
+
}
|
|
1001
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
1002
|
+
const data = JSON.parse(raw);
|
|
1003
|
+
return {
|
|
1004
|
+
teamName: data.teamName,
|
|
1005
|
+
description: data.description,
|
|
1006
|
+
status: data.status,
|
|
1007
|
+
members: data.members,
|
|
1008
|
+
createdAt: new Date(data.createdAt)
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
async shutdownAgents(team) {
|
|
1012
|
+
const stopPromises = [];
|
|
1013
|
+
for (const [agentName, agentProcess] of team.processes) {
|
|
1014
|
+
stopPromises.push(
|
|
1015
|
+
agentProcess.stop().catch((error) => {
|
|
1016
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1017
|
+
logger.error(
|
|
1018
|
+
{ agent: agentName, error: reason },
|
|
1019
|
+
"Error stopping agent"
|
|
1020
|
+
);
|
|
1021
|
+
})
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
await Promise.allSettled(stopPromises);
|
|
1025
|
+
team.processes.clear();
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
export { AgentProcess, MessageBus, PlanApproval, TaskStore, TeamManager };
|
|
1030
|
+
//# sourceMappingURL=chunk-HMJRPNPZ.js.map
|
|
1031
|
+
//# sourceMappingURL=chunk-HMJRPNPZ.js.map
|