@robbiesrobotics/alice-agents 1.4.4 → 1.4.6
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 +19 -3
- package/bin/alice-install.mjs +22 -1
- package/lib/agent-registry.mjs +33 -0
- package/lib/coding-agent.mjs +187 -0
- package/lib/config-merger.mjs +5 -1
- package/lib/doctor.mjs +123 -43
- package/lib/installer.mjs +83 -19
- package/lib/license.mjs +158 -14
- package/lib/manifest.mjs +18 -9
- package/lib/mission-control.mjs +31 -0
- package/lib/release-guard.mjs +131 -0
- package/lib/skills.mjs +0 -1
- package/lib/workspace-scaffolder.mjs +11 -16
- package/package.json +5 -3
- package/templates/agents-pro.json +550 -0
- package/templates/mission-control-bridge/index.ts +494 -32
- package/templates/workspaces/dylan/SOUL.md +1 -1
- package/templates/workspaces/dylan/TOOLS.md +4 -4
- package/templates/workspaces/felix/SOUL.md +1 -1
- package/templates/workspaces/felix/TOOLS.md +3 -3
|
@@ -2,35 +2,119 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/diagnostics-otel";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
emptyPluginConfigSchema,
|
|
7
|
+
onDiagnosticEvent,
|
|
8
|
+
} from "openclaw/plugin-sdk/diagnostics-otel";
|
|
6
9
|
import type { DiagnosticEventPayload } from "openclaw/plugin-sdk/diagnostics-otel";
|
|
7
10
|
|
|
8
11
|
const OPENCLAW_HOME = process.env.OPENCLAW_HOME ?? join(homedir(), ".openclaw");
|
|
9
12
|
const MC_CONFIG_PATH = join(OPENCLAW_HOME, ".alice-mission-control.json");
|
|
10
|
-
const
|
|
13
|
+
const DEFAULT_DASHBOARD_URL = "https://alice.av3.ai";
|
|
14
|
+
const DEFAULT_ADMIN_URL = "https://admin.av3.ai";
|
|
15
|
+
const DEFAULT_INGEST_URL = `${DEFAULT_DASHBOARD_URL}/api/v1/ingest`;
|
|
16
|
+
const DEFAULT_RUNTIME_BASE_URL = `${DEFAULT_DASHBOARD_URL}/api/v1/runtime`;
|
|
17
|
+
const DEFAULT_GATEWAY_URL = process.env.OPENCLAW_GATEWAY_URL ?? "http://127.0.0.1:18789";
|
|
18
|
+
const DEFAULT_GATEWAY_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN ?? process.env.MC_GATEWAY_TOKEN ?? "";
|
|
19
|
+
const DEFAULT_CHAT_USER = process.env.MC_CHAT_USER ?? "mission-control-worker";
|
|
20
|
+
const HEARTBEAT_INTERVAL_MS = 15000;
|
|
21
|
+
const COMMAND_POLL_INTERVAL_MS = 5000;
|
|
11
22
|
|
|
12
|
-
|
|
23
|
+
type JsonRecord = Record<string, unknown>;
|
|
24
|
+
|
|
25
|
+
interface RuntimeNodeRegistrationPayload {
|
|
26
|
+
nodeId: string;
|
|
27
|
+
installId: string;
|
|
28
|
+
nodeName: string;
|
|
29
|
+
teamId?: string;
|
|
30
|
+
sourceNode: string;
|
|
31
|
+
connectionMode: "relay";
|
|
32
|
+
gatewayUrl: string;
|
|
33
|
+
runtimeVersion: string;
|
|
34
|
+
platform: string;
|
|
35
|
+
capabilities: JsonRecord;
|
|
36
|
+
metadata: JsonRecord;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface RuntimeCommand {
|
|
40
|
+
id: string;
|
|
41
|
+
nodeId: string;
|
|
42
|
+
type: string;
|
|
43
|
+
status: string;
|
|
44
|
+
threadId: string | null;
|
|
45
|
+
sessionId: string | null;
|
|
46
|
+
payload: JsonRecord;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readMissionControlConfig(): JsonRecord {
|
|
13
50
|
try {
|
|
14
51
|
if (!existsSync(MC_CONFIG_PATH)) return {};
|
|
15
|
-
return JSON.parse(readFileSync(MC_CONFIG_PATH, "utf8"));
|
|
52
|
+
return JSON.parse(readFileSync(MC_CONFIG_PATH, "utf8")) as JsonRecord;
|
|
16
53
|
} catch {
|
|
17
54
|
return {};
|
|
18
55
|
}
|
|
19
56
|
}
|
|
20
57
|
|
|
21
|
-
function getCloudConfig():
|
|
58
|
+
function getCloudConfig(): JsonRecord {
|
|
22
59
|
const fileConfig = readMissionControlConfig();
|
|
23
|
-
return typeof fileConfig.cloud === "object" && fileConfig.cloud
|
|
60
|
+
return typeof fileConfig.cloud === "object" && fileConfig.cloud
|
|
61
|
+
? (fileConfig.cloud as JsonRecord)
|
|
62
|
+
: {};
|
|
24
63
|
}
|
|
25
64
|
|
|
26
65
|
function getString(value: unknown, fallback = ""): string {
|
|
27
66
|
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
28
67
|
}
|
|
29
68
|
|
|
69
|
+
function normalizeUrl(value: string): string {
|
|
70
|
+
return value.replace(/\/+$/, "");
|
|
71
|
+
}
|
|
72
|
+
|
|
30
73
|
const cloudConfig = getCloudConfig();
|
|
31
|
-
const
|
|
74
|
+
const DASHBOARD_URL = normalizeUrl(
|
|
75
|
+
getString(process.env.MC_DASHBOARD_URL, getString(cloudConfig.dashboardUrl, DEFAULT_DASHBOARD_URL)),
|
|
76
|
+
);
|
|
77
|
+
const ADMIN_URL = normalizeUrl(
|
|
78
|
+
getString(process.env.MC_ADMIN_URL, getString(cloudConfig.adminUrl, DEFAULT_ADMIN_URL)),
|
|
79
|
+
);
|
|
80
|
+
const INGEST_URL = normalizeUrl(
|
|
81
|
+
getString(process.env.MC_INGEST_URL, getString(cloudConfig.ingestUrl, DEFAULT_INGEST_URL)),
|
|
82
|
+
);
|
|
83
|
+
const RUNTIME_BASE_URL = normalizeUrl(
|
|
84
|
+
getString(process.env.MC_RUNTIME_BASE_URL, getString(cloudConfig.runtimeBaseUrl, DEFAULT_RUNTIME_BASE_URL)),
|
|
85
|
+
);
|
|
86
|
+
const NODE_REGISTER_URL = normalizeUrl(
|
|
87
|
+
getString(process.env.MC_NODE_REGISTER_URL, getString(cloudConfig.nodeRegisterUrl, `${RUNTIME_BASE_URL}/nodes/register`)),
|
|
88
|
+
);
|
|
89
|
+
const NODE_HEARTBEAT_URL = normalizeUrl(
|
|
90
|
+
getString(
|
|
91
|
+
process.env.MC_NODE_HEARTBEAT_URL,
|
|
92
|
+
getString(cloudConfig.nodeHeartbeatUrl, `${RUNTIME_BASE_URL}/nodes/heartbeat`),
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
const ADMIN_HEARTBEAT_URL = normalizeUrl(
|
|
96
|
+
getString(
|
|
97
|
+
process.env.MC_ADMIN_HEARTBEAT_URL,
|
|
98
|
+
getString(cloudConfig.adminHeartbeatUrl, `${ADMIN_URL}/api/admin/v1/node-heartbeat`),
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
const COMMANDS_URL = normalizeUrl(
|
|
102
|
+
getString(process.env.MC_COMMANDS_URL, getString(cloudConfig.commandsUrl, `${RUNTIME_BASE_URL}/commands`)),
|
|
103
|
+
);
|
|
32
104
|
const INGEST_TOKEN = getString(process.env.MC_INGEST_TOKEN, getString(cloudConfig.ingestToken));
|
|
105
|
+
const WORKER_TOKEN = getString(
|
|
106
|
+
process.env.MC_RUNTIME_WORKER_TOKEN,
|
|
107
|
+
getString(cloudConfig.workerToken, INGEST_TOKEN),
|
|
108
|
+
);
|
|
33
109
|
const SOURCE_NODE = getString(process.env.MC_SOURCE_NODE, getString(cloudConfig.sourceNode, "openclaw-local"));
|
|
110
|
+
const TEAM_ID = getString(process.env.MC_TEAM_ID, getString(cloudConfig.teamId));
|
|
111
|
+
const TEAM_SLUG = getString(process.env.MC_TEAM_SLUG, getString(cloudConfig.teamSlug));
|
|
112
|
+
const TEAM_NAME = getString(process.env.MC_TEAM_NAME, getString(cloudConfig.teamName));
|
|
113
|
+
const TEAM_PLAN = getString(process.env.MC_TEAM_PLAN, getString(cloudConfig.teamPlan));
|
|
114
|
+
const INSTALL_ID = getString(process.env.MC_INSTALL_ID, SOURCE_NODE);
|
|
115
|
+
const NODE_ID = getString(process.env.MC_NODE_ID, SOURCE_NODE);
|
|
116
|
+
const GATEWAY_URL = normalizeUrl(getString(process.env.MC_GATEWAY_URL, DEFAULT_GATEWAY_URL));
|
|
117
|
+
const GATEWAY_TOKEN = getString(process.env.MC_GATEWAY_TOKEN, DEFAULT_GATEWAY_TOKEN);
|
|
34
118
|
|
|
35
119
|
function now(): string {
|
|
36
120
|
return new Date().toISOString();
|
|
@@ -43,30 +127,325 @@ function sessionKeyToAgentId(sessionKey?: string): string {
|
|
|
43
127
|
}
|
|
44
128
|
|
|
45
129
|
let seq = 0;
|
|
46
|
-
function
|
|
47
|
-
return
|
|
130
|
+
function nextId(prefix: string): string {
|
|
131
|
+
return `${prefix}-${Date.now()}-${++seq}`;
|
|
48
132
|
}
|
|
49
133
|
|
|
50
|
-
|
|
51
|
-
|
|
134
|
+
function authHeaders(token: string): Record<string, string> {
|
|
135
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function headers(token = ""): Record<string, string> {
|
|
139
|
+
return {
|
|
52
140
|
"Content-Type": "application/json",
|
|
141
|
+
...authHeaders(token),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function postJson(url: string, token: string, body: object): Promise<Response> {
|
|
146
|
+
return fetch(url, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: headers(token),
|
|
149
|
+
body: JSON.stringify(body),
|
|
150
|
+
signal: AbortSignal.timeout(8000),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function postToIngest(events: object[]): Promise<void> {
|
|
155
|
+
try {
|
|
156
|
+
const res = await postJson(INGEST_URL, INGEST_TOKEN, events);
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
console.warn(`[mc-bridge] ingest HTTP ${res.status} — ${await res.text().catch(() => "")}`);
|
|
159
|
+
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.warn(`[mc-bridge] ingest post failed: ${String(err)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildNodeRegistrationPayload(): RuntimeNodeRegistrationPayload {
|
|
166
|
+
return {
|
|
167
|
+
nodeId: NODE_ID,
|
|
168
|
+
installId: INSTALL_ID,
|
|
169
|
+
nodeName: SOURCE_NODE,
|
|
170
|
+
...(TEAM_ID ? { teamId: TEAM_ID } : {}),
|
|
171
|
+
sourceNode: SOURCE_NODE,
|
|
172
|
+
connectionMode: "relay",
|
|
173
|
+
gatewayUrl: GATEWAY_URL,
|
|
174
|
+
runtimeVersion: process.version,
|
|
175
|
+
platform: process.platform,
|
|
176
|
+
capabilities: {
|
|
177
|
+
telemetry: true,
|
|
178
|
+
commands: true,
|
|
179
|
+
directGateway: true,
|
|
180
|
+
commandTypes: ["agent.message.send"],
|
|
181
|
+
},
|
|
182
|
+
metadata: {
|
|
183
|
+
dashboardUrl: DASHBOARD_URL,
|
|
184
|
+
pluginId: "mission-control-bridge",
|
|
185
|
+
...(TEAM_SLUG ? { teamSlug: TEAM_SLUG } : {}),
|
|
186
|
+
...(TEAM_NAME ? { teamName: TEAM_NAME } : {}),
|
|
187
|
+
...(TEAM_PLAN ? { teamPlan: TEAM_PLAN } : {}),
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function registerNode(logger: { info(message: string): void; warn(message: string): void }): Promise<void> {
|
|
193
|
+
if (!WORKER_TOKEN) {
|
|
194
|
+
logger.warn("[mc-bridge] worker token missing; node registration skipped");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const res = await postJson(NODE_REGISTER_URL, WORKER_TOKEN, buildNodeRegistrationPayload());
|
|
199
|
+
if (!res.ok) {
|
|
200
|
+
logger.warn(`[mc-bridge] register failed ${res.status}: ${await res.text().catch(() => "")}`);
|
|
201
|
+
}
|
|
202
|
+
} catch (err) {
|
|
203
|
+
logger.warn(`[mc-bridge] register failed: ${String(err)}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function heartbeatNode(logger: { warn(message: string): void }): Promise<void> {
|
|
208
|
+
if (!WORKER_TOKEN) return;
|
|
209
|
+
const payload = {
|
|
210
|
+
nodeId: NODE_ID,
|
|
211
|
+
...(TEAM_ID ? { teamId: TEAM_ID } : {}),
|
|
212
|
+
status: "online",
|
|
213
|
+
runtimeVersion: process.version,
|
|
214
|
+
platform: process.platform,
|
|
215
|
+
capabilities: {
|
|
216
|
+
telemetry: true,
|
|
217
|
+
commands: true,
|
|
218
|
+
directGateway: true,
|
|
219
|
+
commandTypes: ["agent.message.send"],
|
|
220
|
+
},
|
|
221
|
+
metadata: {
|
|
222
|
+
dashboardUrl: DASHBOARD_URL,
|
|
223
|
+
...(TEAM_SLUG ? { teamSlug: TEAM_SLUG } : {}),
|
|
224
|
+
...(TEAM_NAME ? { teamName: TEAM_NAME } : {}),
|
|
225
|
+
...(TEAM_PLAN ? { teamPlan: TEAM_PLAN } : {}),
|
|
226
|
+
},
|
|
53
227
|
};
|
|
54
|
-
|
|
55
|
-
|
|
228
|
+
try {
|
|
229
|
+
const res = await postJson(NODE_HEARTBEAT_URL, WORKER_TOKEN, payload);
|
|
230
|
+
if (!res.ok) {
|
|
231
|
+
logger.warn(`[mc-bridge] heartbeat failed ${res.status}: ${await res.text().catch(() => "")}`);
|
|
232
|
+
}
|
|
233
|
+
} catch (err) {
|
|
234
|
+
logger.warn(`[mc-bridge] heartbeat failed: ${String(err)}`);
|
|
56
235
|
}
|
|
57
236
|
|
|
58
237
|
try {
|
|
59
|
-
const res = await
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
238
|
+
const res = await postJson(ADMIN_HEARTBEAT_URL, WORKER_TOKEN, {
|
|
239
|
+
instanceId: NODE_ID,
|
|
240
|
+
nodeName: SOURCE_NODE,
|
|
241
|
+
...(TEAM_ID ? { teamId: TEAM_ID } : {}),
|
|
242
|
+
...(TEAM_SLUG ? { teamSlug: TEAM_SLUG } : {}),
|
|
243
|
+
...(TEAM_NAME ? { teamName: TEAM_NAME } : {}),
|
|
244
|
+
...(TEAM_PLAN ? { teamPlan: TEAM_PLAN } : {}),
|
|
245
|
+
tailscaleIp: getString(process.env.TAILSCALE_IP),
|
|
246
|
+
deployedVersion: getString(process.env.MC_DEPLOYED_VERSION),
|
|
247
|
+
runtimeVersion: process.version,
|
|
248
|
+
status: "healthy",
|
|
249
|
+
publicUrl: DASHBOARD_URL,
|
|
250
|
+
region: getString(process.env.MC_REGION),
|
|
251
|
+
backupStatus: "unknown",
|
|
252
|
+
memorySyncStatus: "healthy",
|
|
253
|
+
});
|
|
254
|
+
if (!res.ok) {
|
|
255
|
+
logger.warn(`[mc-bridge] admin heartbeat failed ${res.status}: ${await res.text().catch(() => "")}`);
|
|
256
|
+
}
|
|
257
|
+
} catch (err) {
|
|
258
|
+
logger.warn(`[mc-bridge] admin heartbeat failed: ${String(err)}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function listQueuedCommands(logger: { warn(message: string): void }): Promise<RuntimeCommand[]> {
|
|
263
|
+
if (!WORKER_TOKEN) return [];
|
|
264
|
+
const url = `${COMMANDS_URL}?nodeId=${encodeURIComponent(NODE_ID)}&limit=5`;
|
|
265
|
+
try {
|
|
266
|
+
const res = await fetch(url, {
|
|
267
|
+
method: "GET",
|
|
268
|
+
headers: authHeaders(WORKER_TOKEN),
|
|
63
269
|
signal: AbortSignal.timeout(8000),
|
|
64
270
|
});
|
|
65
271
|
if (!res.ok) {
|
|
66
|
-
|
|
272
|
+
logger.warn(`[mc-bridge] command poll failed ${res.status}: ${await res.text().catch(() => "")}`);
|
|
273
|
+
return [];
|
|
67
274
|
}
|
|
275
|
+
const data = (await res.json()) as { commands?: RuntimeCommand[] };
|
|
276
|
+
return Array.isArray(data.commands) ? data.commands : [];
|
|
68
277
|
} catch (err) {
|
|
69
|
-
|
|
278
|
+
logger.warn(`[mc-bridge] command poll failed: ${String(err)}`);
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function leaseCommand(commandId: string, logger: { warn(message: string): void }): Promise<boolean> {
|
|
284
|
+
try {
|
|
285
|
+
const res = await postJson(`${COMMANDS_URL}/${commandId}/lease`, WORKER_TOKEN, {
|
|
286
|
+
nodeId: NODE_ID,
|
|
287
|
+
leaseOwner: NODE_ID,
|
|
288
|
+
});
|
|
289
|
+
if (res.ok) return true;
|
|
290
|
+
if (res.status !== 409) {
|
|
291
|
+
logger.warn(`[mc-bridge] command lease failed ${res.status}: ${await res.text().catch(() => "")}`);
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
} catch (err) {
|
|
295
|
+
logger.warn(`[mc-bridge] command lease failed: ${String(err)}`);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function emitCommandEvent(
|
|
301
|
+
commandId: string,
|
|
302
|
+
eventType: string,
|
|
303
|
+
payload: JsonRecord,
|
|
304
|
+
logger: { warn(message: string): void },
|
|
305
|
+
): Promise<void> {
|
|
306
|
+
try {
|
|
307
|
+
const res = await postJson(`${COMMANDS_URL}/${commandId}/events`, WORKER_TOKEN, {
|
|
308
|
+
nodeId: NODE_ID,
|
|
309
|
+
eventType,
|
|
310
|
+
payload,
|
|
311
|
+
});
|
|
312
|
+
if (!res.ok) {
|
|
313
|
+
logger.warn(`[mc-bridge] command event failed ${res.status}: ${await res.text().catch(() => "")}`);
|
|
314
|
+
}
|
|
315
|
+
} catch (err) {
|
|
316
|
+
logger.warn(`[mc-bridge] command event failed: ${String(err)}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function completeCommand(
|
|
321
|
+
commandId: string,
|
|
322
|
+
result: JsonRecord,
|
|
323
|
+
logger: { warn(message: string): void },
|
|
324
|
+
): Promise<void> {
|
|
325
|
+
try {
|
|
326
|
+
const res = await postJson(`${COMMANDS_URL}/${commandId}/complete`, WORKER_TOKEN, {
|
|
327
|
+
nodeId: NODE_ID,
|
|
328
|
+
result,
|
|
329
|
+
});
|
|
330
|
+
if (!res.ok) {
|
|
331
|
+
logger.warn(`[mc-bridge] command completion failed ${res.status}: ${await res.text().catch(() => "")}`);
|
|
332
|
+
}
|
|
333
|
+
} catch (err) {
|
|
334
|
+
logger.warn(`[mc-bridge] command completion failed: ${String(err)}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function failCommand(
|
|
339
|
+
commandId: string,
|
|
340
|
+
error: string,
|
|
341
|
+
logger: { warn(message: string): void },
|
|
342
|
+
): Promise<void> {
|
|
343
|
+
try {
|
|
344
|
+
const res = await postJson(`${COMMANDS_URL}/${commandId}/fail`, WORKER_TOKEN, {
|
|
345
|
+
nodeId: NODE_ID,
|
|
346
|
+
error,
|
|
347
|
+
});
|
|
348
|
+
if (!res.ok) {
|
|
349
|
+
logger.warn(`[mc-bridge] command failure update failed ${res.status}: ${await res.text().catch(() => "")}`);
|
|
350
|
+
}
|
|
351
|
+
} catch (err) {
|
|
352
|
+
logger.warn(`[mc-bridge] command failure update failed: ${String(err)}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function executeAgentMessage(command: RuntimeCommand): Promise<JsonRecord> {
|
|
357
|
+
const payload = command.payload ?? {};
|
|
358
|
+
const message = getString(payload.message);
|
|
359
|
+
const agentId = getString(payload.agentId, "olivia");
|
|
360
|
+
const chatUser = getString(payload.user, DEFAULT_CHAT_USER);
|
|
361
|
+
|
|
362
|
+
if (!message) {
|
|
363
|
+
throw new Error("agent.message.send requires payload.message");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const gatewayHeaders: Record<string, string> = {
|
|
367
|
+
"Content-Type": "application/json",
|
|
368
|
+
};
|
|
369
|
+
if (GATEWAY_TOKEN) {
|
|
370
|
+
gatewayHeaders.Authorization = `Bearer ${GATEWAY_TOKEN}`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const res = await fetch(`${GATEWAY_URL}/v1/responses`, {
|
|
374
|
+
method: "POST",
|
|
375
|
+
headers: gatewayHeaders,
|
|
376
|
+
body: JSON.stringify({
|
|
377
|
+
model: `openclaw:${agentId}`,
|
|
378
|
+
input: message,
|
|
379
|
+
user: chatUser,
|
|
380
|
+
stream: false,
|
|
381
|
+
}),
|
|
382
|
+
signal: AbortSignal.timeout(30000),
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
if (!res.ok) {
|
|
386
|
+
throw new Error(`gateway ${res.status}: ${await res.text().catch(() => "")}`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const data = (await res.json()) as JsonRecord;
|
|
390
|
+
const outputText = extractOutputText(data);
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
agentId,
|
|
394
|
+
user: chatUser,
|
|
395
|
+
responseId: getString(data.id),
|
|
396
|
+
outputText,
|
|
397
|
+
raw: data,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function extractOutputText(data: JsonRecord): string {
|
|
402
|
+
const output = Array.isArray(data.output) ? data.output : [];
|
|
403
|
+
const parts: string[] = [];
|
|
404
|
+
|
|
405
|
+
for (const item of output) {
|
|
406
|
+
if (!item || typeof item !== "object") continue;
|
|
407
|
+
const content = Array.isArray((item as JsonRecord).content) ? ((item as JsonRecord).content as unknown[]) : [];
|
|
408
|
+
for (const block of content) {
|
|
409
|
+
if (!block || typeof block !== "object") continue;
|
|
410
|
+
const record = block as JsonRecord;
|
|
411
|
+
const text =
|
|
412
|
+
getString(record.text) ||
|
|
413
|
+
getString(record.value) ||
|
|
414
|
+
getString(typeof record.output_text === "string" ? record.output_text : "");
|
|
415
|
+
if (text) parts.push(text);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return parts.join("\n\n").trim();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function executeCommand(
|
|
423
|
+
command: RuntimeCommand,
|
|
424
|
+
logger: { info(message: string): void; warn(message: string): void },
|
|
425
|
+
): Promise<void> {
|
|
426
|
+
await emitCommandEvent(command.id, "command.started", {
|
|
427
|
+
commandType: command.type,
|
|
428
|
+
startedAt: now(),
|
|
429
|
+
}, logger);
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
switch (command.type) {
|
|
433
|
+
case "agent.message.send": {
|
|
434
|
+
const result = await executeAgentMessage(command);
|
|
435
|
+
await emitCommandEvent(command.id, "agent.message.completed", result, logger);
|
|
436
|
+
await completeCommand(command.id, result, logger);
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
default:
|
|
440
|
+
throw new Error(`unsupported command type: ${command.type}`);
|
|
441
|
+
}
|
|
442
|
+
} catch (err) {
|
|
443
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
444
|
+
await emitCommandEvent(command.id, "command.failed", {
|
|
445
|
+
error: message,
|
|
446
|
+
failedAt: now(),
|
|
447
|
+
}, logger);
|
|
448
|
+
await failCommand(command.id, message, logger);
|
|
70
449
|
}
|
|
71
450
|
}
|
|
72
451
|
|
|
@@ -75,7 +454,7 @@ function handleModelUsage(evt: Extract<DiagnosticEventPayload, { type: "model.us
|
|
|
75
454
|
const totalTokens = evt.usage.total ?? (evt.usage.input ?? 0) + (evt.usage.output ?? 0);
|
|
76
455
|
|
|
77
456
|
const event = {
|
|
78
|
-
event_id:
|
|
457
|
+
event_id: nextId("mc-bridge"),
|
|
79
458
|
event_type: "agent.session.completed",
|
|
80
459
|
event_version: "1.0",
|
|
81
460
|
source_system: "openclaw",
|
|
@@ -85,8 +464,9 @@ function handleModelUsage(evt: Extract<DiagnosticEventPayload, { type: "model.us
|
|
|
85
464
|
actor_type: "agent",
|
|
86
465
|
correlation_id: evt.sessionId ?? null,
|
|
87
466
|
payload: {
|
|
88
|
-
session_id: evt.sessionId ?? evt.sessionKey ??
|
|
467
|
+
session_id: evt.sessionId ?? evt.sessionKey ?? nextId("sess"),
|
|
89
468
|
agent_id: agentId,
|
|
469
|
+
...(TEAM_ID ? { teamId: TEAM_ID } : {}),
|
|
90
470
|
model: evt.model ?? "unknown",
|
|
91
471
|
channel: evt.channel ?? "unknown",
|
|
92
472
|
total_tokens: totalTokens,
|
|
@@ -102,7 +482,7 @@ function handleModelUsage(evt: Extract<DiagnosticEventPayload, { type: "model.us
|
|
|
102
482
|
},
|
|
103
483
|
};
|
|
104
484
|
|
|
105
|
-
postToIngest([event]);
|
|
485
|
+
void postToIngest([event]);
|
|
106
486
|
}
|
|
107
487
|
|
|
108
488
|
function handleMessageProcessed(evt: Extract<DiagnosticEventPayload, { type: "message.processed" }>) {
|
|
@@ -118,7 +498,7 @@ function handleMessageProcessed(evt: Extract<DiagnosticEventPayload, { type: "me
|
|
|
118
498
|
if (!eventType) return;
|
|
119
499
|
|
|
120
500
|
const event = {
|
|
121
|
-
event_id:
|
|
501
|
+
event_id: nextId("mc-bridge"),
|
|
122
502
|
event_type: eventType,
|
|
123
503
|
event_version: "1.0",
|
|
124
504
|
source_system: "openclaw",
|
|
@@ -128,8 +508,9 @@ function handleMessageProcessed(evt: Extract<DiagnosticEventPayload, { type: "me
|
|
|
128
508
|
actor_type: "agent",
|
|
129
509
|
correlation_id: evt.sessionId ?? null,
|
|
130
510
|
payload: {
|
|
131
|
-
session_id: evt.sessionId ?? evt.sessionKey ??
|
|
511
|
+
session_id: evt.sessionId ?? evt.sessionKey ?? nextId("sess"),
|
|
132
512
|
agent_id: agentId,
|
|
513
|
+
...(TEAM_ID ? { teamId: TEAM_ID } : {}),
|
|
133
514
|
channel: evt.channel ?? "unknown",
|
|
134
515
|
status: evt.outcome === "error" ? "failed" : "completed",
|
|
135
516
|
duration_ms: evt.durationMs ?? 0,
|
|
@@ -137,7 +518,7 @@ function handleMessageProcessed(evt: Extract<DiagnosticEventPayload, { type: "me
|
|
|
137
518
|
},
|
|
138
519
|
};
|
|
139
520
|
|
|
140
|
-
postToIngest([event]);
|
|
521
|
+
void postToIngest([event]);
|
|
141
522
|
}
|
|
142
523
|
|
|
143
524
|
function handleSessionState(evt: Extract<DiagnosticEventPayload, { type: "session.state" }>) {
|
|
@@ -151,7 +532,7 @@ function handleSessionState(evt: Extract<DiagnosticEventPayload, { type: "sessio
|
|
|
151
532
|
if (!eventType) return;
|
|
152
533
|
|
|
153
534
|
const event = {
|
|
154
|
-
event_id:
|
|
535
|
+
event_id: nextId("mc-bridge"),
|
|
155
536
|
event_type: eventType,
|
|
156
537
|
event_version: "1.0",
|
|
157
538
|
source_system: "openclaw",
|
|
@@ -161,20 +542,21 @@ function handleSessionState(evt: Extract<DiagnosticEventPayload, { type: "sessio
|
|
|
161
542
|
actor_type: "agent",
|
|
162
543
|
correlation_id: evt.sessionId ?? null,
|
|
163
544
|
payload: {
|
|
164
|
-
session_id: evt.sessionId ?? evt.sessionKey ??
|
|
545
|
+
session_id: evt.sessionId ?? evt.sessionKey ?? nextId("sess"),
|
|
165
546
|
agent_id: agentId,
|
|
547
|
+
...(TEAM_ID ? { teamId: TEAM_ID } : {}),
|
|
166
548
|
state: evt.state,
|
|
167
549
|
prev_state: evt.prevState ?? null,
|
|
168
550
|
},
|
|
169
551
|
};
|
|
170
552
|
|
|
171
|
-
postToIngest([event]);
|
|
553
|
+
void postToIngest([event]);
|
|
172
554
|
}
|
|
173
555
|
|
|
174
556
|
const plugin = {
|
|
175
557
|
id: "mission-control-bridge",
|
|
176
558
|
name: "Mission Control Bridge",
|
|
177
|
-
description: "Forwards OpenClaw diagnostic events to
|
|
559
|
+
description: "Forwards OpenClaw diagnostic events to A.L.I.C.E. Mission Control and executes hosted runtime commands",
|
|
178
560
|
configSchema: emptyPluginConfigSchema(),
|
|
179
561
|
|
|
180
562
|
register(api: OpenClawPluginApi) {
|
|
@@ -183,6 +565,10 @@ const plugin = {
|
|
|
183
565
|
|
|
184
566
|
async start(ctx) {
|
|
185
567
|
ctx.logger.info(`[mc-bridge] starting — ingest URL: ${INGEST_URL}`);
|
|
568
|
+
ctx.logger.info(`[mc-bridge] runtime worker — commands URL: ${COMMANDS_URL}`);
|
|
569
|
+
|
|
570
|
+
let pollInFlight = false;
|
|
571
|
+
let stopped = false;
|
|
186
572
|
|
|
187
573
|
const unsubscribe = onDiagnosticEvent((evt: DiagnosticEventPayload) => {
|
|
188
574
|
try {
|
|
@@ -202,11 +588,25 @@ const plugin = {
|
|
|
202
588
|
}
|
|
203
589
|
});
|
|
204
590
|
|
|
205
|
-
|
|
591
|
+
const pollCommands = async () => {
|
|
592
|
+
if (stopped || pollInFlight || !WORKER_TOKEN) return;
|
|
593
|
+
pollInFlight = true;
|
|
594
|
+
try {
|
|
595
|
+
const commands = await listQueuedCommands(ctx.logger);
|
|
596
|
+
for (const command of commands) {
|
|
597
|
+
if (!(await leaseCommand(command.id, ctx.logger))) continue;
|
|
598
|
+
await executeCommand(command, ctx.logger);
|
|
599
|
+
}
|
|
600
|
+
} finally {
|
|
601
|
+
pollInFlight = false;
|
|
602
|
+
}
|
|
603
|
+
};
|
|
206
604
|
|
|
605
|
+
await registerNode(ctx.logger);
|
|
606
|
+
await heartbeatNode(ctx.logger);
|
|
207
607
|
await postToIngest([
|
|
208
608
|
{
|
|
209
|
-
event_id:
|
|
609
|
+
event_id: nextId("mc-bridge"),
|
|
210
610
|
event_type: "node.registered",
|
|
211
611
|
event_version: "1.0",
|
|
212
612
|
source_system: "openclaw",
|
|
@@ -214,17 +614,79 @@ const plugin = {
|
|
|
214
614
|
occurred_at: now(),
|
|
215
615
|
payload: {
|
|
216
616
|
node_name: SOURCE_NODE,
|
|
617
|
+
node_id: NODE_ID,
|
|
618
|
+
...(TEAM_ID ? { teamId: TEAM_ID } : {}),
|
|
619
|
+
install_id: INSTALL_ID,
|
|
217
620
|
platform: process.platform,
|
|
218
621
|
node_version: process.version,
|
|
622
|
+
gateway_url: GATEWAY_URL,
|
|
219
623
|
},
|
|
220
624
|
},
|
|
221
625
|
]);
|
|
626
|
+
|
|
627
|
+
const heartbeatTimer = setInterval(() => {
|
|
628
|
+
void heartbeatNode(ctx.logger);
|
|
629
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
630
|
+
|
|
631
|
+
const pollTimer = setInterval(() => {
|
|
632
|
+
void pollCommands();
|
|
633
|
+
}, COMMAND_POLL_INTERVAL_MS);
|
|
634
|
+
|
|
635
|
+
await pollCommands();
|
|
636
|
+
|
|
637
|
+
(
|
|
638
|
+
this as {
|
|
639
|
+
_unsub?: () => void;
|
|
640
|
+
_heartbeatTimer?: ReturnType<typeof setInterval>;
|
|
641
|
+
_pollTimer?: ReturnType<typeof setInterval>;
|
|
642
|
+
_stop?: () => void;
|
|
643
|
+
}
|
|
644
|
+
)._unsub = unsubscribe;
|
|
645
|
+
(
|
|
646
|
+
this as {
|
|
647
|
+
_unsub?: () => void;
|
|
648
|
+
_heartbeatTimer?: ReturnType<typeof setInterval>;
|
|
649
|
+
_pollTimer?: ReturnType<typeof setInterval>;
|
|
650
|
+
_stop?: () => void;
|
|
651
|
+
}
|
|
652
|
+
)._heartbeatTimer = heartbeatTimer;
|
|
653
|
+
(
|
|
654
|
+
this as {
|
|
655
|
+
_unsub?: () => void;
|
|
656
|
+
_heartbeatTimer?: ReturnType<typeof setInterval>;
|
|
657
|
+
_pollTimer?: ReturnType<typeof setInterval>;
|
|
658
|
+
_stop?: () => void;
|
|
659
|
+
}
|
|
660
|
+
)._pollTimer = pollTimer;
|
|
661
|
+
(
|
|
662
|
+
this as {
|
|
663
|
+
_unsub?: () => void;
|
|
664
|
+
_heartbeatTimer?: ReturnType<typeof setInterval>;
|
|
665
|
+
_pollTimer?: ReturnType<typeof setInterval>;
|
|
666
|
+
_stop?: () => void;
|
|
667
|
+
}
|
|
668
|
+
)._stop = () => {
|
|
669
|
+
stopped = true;
|
|
670
|
+
clearInterval(heartbeatTimer);
|
|
671
|
+
clearInterval(pollTimer);
|
|
672
|
+
};
|
|
222
673
|
},
|
|
223
674
|
|
|
224
675
|
async stop() {
|
|
225
|
-
const self = this as {
|
|
676
|
+
const self = this as {
|
|
677
|
+
_unsub?: () => void;
|
|
678
|
+
_heartbeatTimer?: ReturnType<typeof setInterval>;
|
|
679
|
+
_pollTimer?: ReturnType<typeof setInterval>;
|
|
680
|
+
_stop?: () => void;
|
|
681
|
+
};
|
|
226
682
|
self._unsub?.();
|
|
683
|
+
self._stop?.();
|
|
684
|
+
if (self._heartbeatTimer) clearInterval(self._heartbeatTimer);
|
|
685
|
+
if (self._pollTimer) clearInterval(self._pollTimer);
|
|
227
686
|
self._unsub = undefined;
|
|
687
|
+
self._heartbeatTimer = undefined;
|
|
688
|
+
self._pollTimer = undefined;
|
|
689
|
+
self._stop = undefined;
|
|
228
690
|
},
|
|
229
691
|
});
|
|
230
692
|
},
|
|
@@ -35,7 +35,7 @@ Measured, precise, craftsman-energy. You take pride in clean implementations. Yo
|
|
|
35
35
|
|
|
36
36
|
## Tools
|
|
37
37
|
|
|
38
|
-
- Use the `
|
|
38
|
+
- Use the `coding-agent` skill for any non-trivial multi-file coding task
|
|
39
39
|
- Use `exec` to run tests, check build output, and verify implementations
|
|
40
40
|
- Use `read` to understand the codebase before proposing changes
|
|
41
41
|
- Use `web_search` for API docs, error messages, and library references
|