multiclaws 0.4.42 → 0.4.43

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.
Files changed (38) hide show
  1. package/README.md +2 -0
  2. package/dist/gateway/handlers.d.ts +4 -4
  3. package/dist/gateway/handlers.js +239 -239
  4. package/dist/index.d.ts +8 -8
  5. package/dist/index.js +710 -710
  6. package/dist/infra/frp.d.ts +55 -55
  7. package/dist/infra/frp.js +398 -398
  8. package/dist/infra/gateway-client.d.ts +27 -27
  9. package/dist/infra/gateway-client.js +136 -136
  10. package/dist/infra/json-store.d.ts +4 -4
  11. package/dist/infra/json-store.js +57 -57
  12. package/dist/infra/logger.d.ts +14 -14
  13. package/dist/infra/logger.js +25 -25
  14. package/dist/infra/rate-limiter.d.ts +19 -19
  15. package/dist/infra/rate-limiter.js +69 -69
  16. package/dist/infra/tailscale.d.ts +19 -19
  17. package/dist/infra/tailscale.js +120 -120
  18. package/dist/infra/telemetry.d.ts +3 -3
  19. package/dist/infra/telemetry.js +17 -17
  20. package/dist/infra/version.d.ts +1 -1
  21. package/dist/infra/version.js +19 -19
  22. package/dist/service/a2a-adapter.d.ts +80 -80
  23. package/dist/service/a2a-adapter.js +505 -505
  24. package/dist/service/agent-profile.d.ts +17 -17
  25. package/dist/service/agent-profile.js +58 -58
  26. package/dist/service/agent-registry.d.ts +29 -29
  27. package/dist/service/agent-registry.js +131 -131
  28. package/dist/service/multiclaws-service.d.ts +150 -150
  29. package/dist/service/multiclaws-service.js +1137 -1137
  30. package/dist/service/session-store.d.ts +46 -46
  31. package/dist/service/session-store.js +143 -143
  32. package/dist/task/tracker.d.ts +46 -46
  33. package/dist/task/tracker.js +191 -191
  34. package/dist/team/team-store.d.ts +42 -42
  35. package/dist/team/team-store.js +195 -195
  36. package/dist/types/openclaw.d.ts +109 -109
  37. package/dist/types/openclaw.js +2 -2
  38. package/package.json +1 -1
@@ -1,36 +1,36 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.MulticlawsService = void 0;
7
- const node_events_1 = require("node:events");
8
- const node_os_1 = __importDefault(require("node:os"));
9
- const node_http_1 = __importDefault(require("node:http"));
10
- const node_path_1 = __importDefault(require("node:path"));
11
- const promises_1 = __importDefault(require("node:fs/promises"));
12
- const frp_1 = require("../infra/frp");
13
- const json_store_1 = require("../infra/json-store");
14
- const express_1 = __importDefault(require("express"));
15
- const server_1 = require("@a2a-js/sdk/server");
16
- const express_2 = require("@a2a-js/sdk/server/express");
17
- const client_1 = require("@a2a-js/sdk/client");
18
- const version_1 = require("../infra/version");
19
- const a2a_adapter_1 = require("./a2a-adapter");
20
- const agent_registry_1 = require("./agent-registry");
21
- const agent_profile_1 = require("./agent-profile");
22
- const team_store_1 = require("../team/team-store");
23
- const tracker_1 = require("../task/tracker");
24
- const zod_1 = require("zod");
25
- const gateway_client_1 = require("../infra/gateway-client");
26
- const rate_limiter_1 = require("../infra/rate-limiter");
27
- /* ------------------------------------------------------------------ */
28
- /* Delegation prompt builder */
29
- /* ------------------------------------------------------------------ */
30
- function buildDelegationPrompt(agent, task) {
31
- const bioSnippet = agent.description
32
- ? `\n**智能体能力**: ${agent.description.slice(0, 500)}`
33
- : "";
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MulticlawsService = void 0;
7
+ const node_events_1 = require("node:events");
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const node_http_1 = __importDefault(require("node:http"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const promises_1 = __importDefault(require("node:fs/promises"));
12
+ const frp_1 = require("../infra/frp");
13
+ const json_store_1 = require("../infra/json-store");
14
+ const express_1 = __importDefault(require("express"));
15
+ const server_1 = require("@a2a-js/sdk/server");
16
+ const express_2 = require("@a2a-js/sdk/server/express");
17
+ const client_1 = require("@a2a-js/sdk/client");
18
+ const version_1 = require("../infra/version");
19
+ const a2a_adapter_1 = require("./a2a-adapter");
20
+ const agent_registry_1 = require("./agent-registry");
21
+ const agent_profile_1 = require("./agent-profile");
22
+ const team_store_1 = require("../team/team-store");
23
+ const tracker_1 = require("../task/tracker");
24
+ const zod_1 = require("zod");
25
+ const gateway_client_1 = require("../infra/gateway-client");
26
+ const rate_limiter_1 = require("../infra/rate-limiter");
27
+ /* ------------------------------------------------------------------ */
28
+ /* Delegation prompt builder */
29
+ /* ------------------------------------------------------------------ */
30
+ function buildDelegationPrompt(agent, task) {
31
+ const bioSnippet = agent.description
32
+ ? `\n**智能体能力**: ${agent.description.slice(0, 500)}`
33
+ : "";
34
34
  return `## 委派任务
35
35
  向远端智能体发送任务并汇报结果。
36
36
 
@@ -47,1107 +47,1107 @@ function buildDelegationPrompt(agent, task) {
47
47
  - 使用 multiclaws_delegate_send(不是 multiclaws_delegate)发送任务
48
48
  - 使用 multiclaws_notify(不是 message)将结果推送给用户
49
49
  - 最多 5 轮沟通
50
- - 遇到错误时在 multiclaws_notify 中说明失败原因`;
51
- }
52
- /* ------------------------------------------------------------------ */
53
- /* Service */
54
- /* ------------------------------------------------------------------ */
55
- class MulticlawsService extends node_events_1.EventEmitter {
56
- options;
57
- started = false;
58
- httpServer = null;
59
- agentRegistry;
60
- teamStore;
61
- profileStore;
62
- taskTracker;
63
- agentExecutor = null;
64
- a2aRequestHandler = null;
65
- agentCard = null;
66
- clientFactory = new client_1.ClientFactory();
67
- httpRateLimiter = new rate_limiter_1.RateLimiter({ windowMs: 60_000, maxRequests: 60 });
68
- frpTunnel = null;
69
- selfUrl;
70
- profileDescription = "OpenClaw agent";
71
- gatewayConfig;
72
- resolvedCwd;
73
- notificationTargets = new Map();
74
- constructor(options) {
75
- super();
76
- this.options = options;
77
- const multiclawsStateDir = node_path_1.default.join(options.stateDir, "multiclaws");
78
- this.agentRegistry = new agent_registry_1.AgentRegistry(node_path_1.default.join(multiclawsStateDir, "agents.json"), options.logger);
79
- this.teamStore = new team_store_1.TeamStore(node_path_1.default.join(multiclawsStateDir, "teams.json"), options.logger);
80
- this.profileStore = new agent_profile_1.ProfileStore(node_path_1.default.join(multiclawsStateDir, "profile.json"), options.logger);
81
- this.taskTracker = new tracker_1.TaskTracker({
82
- filePath: node_path_1.default.join(multiclawsStateDir, "tasks.json"),
83
- logger: options.logger,
84
- });
85
- // selfUrl resolved later in start() after FRP tunnel setup
86
- this.selfUrl = options.selfUrl ?? "";
87
- this.gatewayConfig = options.gatewayConfig ?? null;
88
- this.resolvedCwd = options.cwd || node_os_1.default.homedir();
89
- }
90
- async start() {
91
- if (this.started)
92
- return;
93
- this.log("debug", `start(port=${this.options.port ?? 3100}, selfUrl=${this.options.selfUrl ?? "auto"})`);
94
- try {
95
- // Resolve selfUrl: explicit config > FRP tunnel
96
- if (!this.options.selfUrl) {
97
- const port = this.options.port ?? 3100;
98
- if (!this.options.tunnel || this.options.tunnel.type !== "frp") {
99
- throw new Error("multiclaws requires either 'selfUrl' or 'tunnel' configuration. " +
100
- "Please configure tunnel in plugin settings.");
101
- }
102
- this.frpTunnel = new frp_1.FrpTunnelManager({
103
- config: this.options.tunnel,
104
- localPort: port,
105
- stateDir: node_path_1.default.join(this.options.stateDir, "multiclaws"),
106
- logger: this.options.logger,
107
- });
108
- const publicUrl = await this.frpTunnel.start();
109
- this.selfUrl = publicUrl;
110
- this.log("info", `FRP tunnel ready: ${publicUrl}`);
111
- }
112
- // Load profile for AgentCard description
113
- const profile = await this.profileStore.load();
114
- if (!profile.ownerName?.trim()) {
115
- await this.setPendingProfileReview();
116
- }
117
- this.profileDescription = (0, agent_profile_1.renderProfileDescription)(profile);
118
- const logger = this.options.logger ?? { info: () => { }, warn: () => { }, error: () => { } };
119
- this.agentExecutor = new a2a_adapter_1.OpenClawAgentExecutor({
120
- gatewayConfig: this.options.gatewayConfig ?? null,
121
- taskTracker: this.taskTracker,
122
- cwd: this.resolvedCwd,
123
- getNotificationTargets: () => this.notificationTargets,
124
- registerDiscoveredTarget: (sessionKey) => {
125
- this.addNotificationTarget(`web:${sessionKey}`, { type: "web", sessionKey });
126
- },
127
- logger,
128
- });
129
- this.agentCard = {
130
- name: profile.ownerName?.trim() ? (0, agent_profile_1.formatAgentCardName)(profile.ownerName.trim()) : "OpenClaw Agent",
131
- description: this.profileDescription,
132
- url: this.selfUrl,
133
- version: version_1.PLUGIN_VERSION,
134
- protocolVersion: "0.2.2",
135
- defaultInputModes: ["text/plain"],
136
- defaultOutputModes: ["text/plain"],
137
- capabilities: { streaming: false, pushNotifications: false },
138
- skills: [
139
- {
140
- id: "general",
141
- name: "General Task",
142
- description: "Execute any delegated task via OpenClaw",
143
- tags: ["task", "delegation", "general"],
144
- },
145
- ],
146
- };
147
- const taskStore = new server_1.InMemoryTaskStore();
148
- this.a2aRequestHandler = new server_1.DefaultRequestHandler(this.agentCard, taskStore, this.agentExecutor);
149
- const app = (0, express_1.default)();
150
- app.use(express_1.default.json({ limit: "1mb" }));
151
- // Rate limiting
152
- app.use((req, res, next) => {
153
- const clientIp = req.ip ?? req.socket.remoteAddress ?? "unknown";
154
- if (!this.httpRateLimiter.allow(clientIp)) {
155
- res.status(429).json({ error: "rate limited" });
156
- return;
157
- }
158
- next();
159
- });
160
- // Team + profile REST endpoints
161
- this.mountTeamRoutes(app);
162
- // A2A endpoints
163
- app.use("/.well-known/agent-card.json", (0, express_2.agentCardHandler)({
164
- agentCardProvider: this.a2aRequestHandler,
165
- }));
166
- app.use("/", (0, express_2.jsonRpcHandler)({
167
- requestHandler: this.a2aRequestHandler,
168
- userBuilder: express_2.UserBuilder.noAuthentication,
169
- }));
170
- const listenPort = this.options.port ?? 3100;
171
- this.httpServer = node_http_1.default.createServer(app);
172
- await new Promise((resolve) => this.httpServer.listen(listenPort, "0.0.0.0", resolve));
173
- this.started = true;
174
- this.log("info", `multiclaws A2A service listening on :${listenPort}`);
175
- }
176
- catch (err) {
177
- this.log("error", `start failed: ${err instanceof Error ? err.message : String(err)}`);
178
- throw err;
179
- }
180
- }
181
- async stop() {
182
- if (!this.started)
183
- return;
184
- this.log("debug", "stopping");
185
- try {
186
- this.started = false;
187
- this.taskTracker.destroy();
188
- this.httpRateLimiter.destroy();
189
- if (this.frpTunnel) {
190
- await this.frpTunnel.stop();
191
- this.frpTunnel = null;
192
- }
193
- await new Promise((resolve) => {
194
- if (!this.httpServer) {
195
- resolve();
196
- return;
197
- }
198
- this.httpServer.close(() => resolve());
199
- });
200
- this.httpServer = null;
201
- this.log("debug", "stopped");
202
- }
203
- catch (err) {
204
- this.log("error", `stop failed: ${err instanceof Error ? err.message : String(err)}`);
205
- throw err;
206
- }
207
- }
208
- updateGatewayConfig(config) {
209
- this.agentExecutor?.updateGatewayConfig(config);
210
- }
211
- /* ---------------------------------------------------------------- */
212
- /* Agent management */
213
- /* ---------------------------------------------------------------- */
214
- async listAgents() {
215
- return await this.agentRegistry.list();
216
- }
217
- async addAgent(params) {
218
- const normalizedUrl = params.url.replace(/\/+$/, "");
219
- this.log("debug", `addAgent(url=${normalizedUrl})`);
220
- try {
221
- const client = await this.clientFactory.createFromUrl(normalizedUrl);
222
- const card = await client.getAgentCard();
223
- const result = await this.agentRegistry.add({
224
- url: normalizedUrl,
225
- name: card.name ?? normalizedUrl,
226
- description: card.description ?? "",
227
- skills: card.skills?.map((s) => s.name ?? s.id) ?? [],
228
- apiKey: params.apiKey,
229
- });
230
- this.log("debug", `addAgent completed, name=${result.name}`);
231
- return result;
232
- }
233
- catch {
234
- this.log("debug", `addAgent: card fetch failed for ${normalizedUrl}, adding with URL as name`);
235
- return await this.agentRegistry.add({
236
- url: normalizedUrl,
237
- name: normalizedUrl,
238
- apiKey: params.apiKey,
239
- });
240
- }
241
- }
242
- async removeAgent(url) {
243
- this.log("debug", `removeAgent(url=${url})`);
244
- try {
245
- const result = await this.agentRegistry.remove(url);
246
- this.log("debug", `removeAgent completed, result=${result}`);
247
- return result;
248
- }
249
- catch (err) {
250
- this.log("error", `removeAgent failed: ${err instanceof Error ? err.message : String(err)}`);
251
- throw err;
252
- }
253
- }
254
- /* ---------------------------------------------------------------- */
255
- /* Task delegation */
256
- /* ---------------------------------------------------------------- */
257
- async delegateTask(params) {
258
- this.log("info", `[delegate] ▶ delegateTask(agentUrl=${params.agentUrl}, taskLen=${params.task.length})`);
259
- this.log("info", `[delegate] task preview: "${params.task.slice(0, 120)}"`);
260
- // Step 1: Check profile
261
- this.log("info", `[delegate] [step:profile-check] verifying profile completeness`);
262
- try {
263
- await this.requireCompleteProfile();
264
- }
265
- catch (err) {
266
- this.log("error", `[delegate] [step:profile-check] ✗ profile incomplete: ${err instanceof Error ? err.message : String(err)}`);
267
- return { status: "failed", error: err instanceof Error ? err.message : String(err) };
268
- }
269
- // Step 2: Look up agent
270
- this.log("info", `[delegate] [step:agent-lookup] looking up agent: ${params.agentUrl}`);
271
- const agentRecord = await this.agentRegistry.get(params.agentUrl);
272
- if (!agentRecord) {
273
- this.log("warn", `[delegate] [step:agent-lookup] ✗ unknown agent: ${params.agentUrl} → aborting`);
274
- return { status: "failed", error: `unknown agent: ${params.agentUrl}` };
275
- }
276
- this.log("info", `[delegate] [step:agent-lookup] ✓ found: ${agentRecord.name} (${agentRecord.url})`);
277
- // Step 3: Track task
278
- const track = this.taskTracker.create({
279
- fromPeerId: "local",
280
- toPeerId: params.agentUrl,
281
- task: params.task,
282
- });
283
- this.taskTracker.update(track.taskId, { status: "running" });
284
- this.log("info", `[delegate] [step:track] taskId=${track.taskId}, status=running`);
285
- try {
286
- // Step 4: Create A2A client
287
- this.log("info", `[delegate] ${track.taskId} [step:create-client] creating A2A client for ${agentRecord.url}`);
288
- const client = await this.createA2AClient(agentRecord);
289
- this.log("info", `[delegate] ${track.taskId} [step:create-client] ✓ client created → starting fire-and-forget send`);
290
- // Step 5: Fire-and-forget execution
291
- void (async () => {
292
- try {
293
- this.log("info", `[delegate] ${track.taskId} [step:background-send] sending A2A message to ${agentRecord.name}...`);
294
- const result = await client.sendMessage({
295
- message: {
296
- kind: "message",
297
- role: "user",
298
- parts: [{ kind: "text", text: params.task }],
299
- messageId: track.taskId,
300
- },
301
- });
302
- this.log("info", `[delegate] ${track.taskId} [step:background-send] ✓ A2A response received → processing result`);
303
- this.processTaskResult(track.taskId, result);
304
- }
305
- catch (err) {
306
- const errorMsg = err instanceof Error ? err.message : String(err);
307
- this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
308
- this.log("error", `[delegate] ${track.taskId} [step:background-send] ✗ caught error: ${errorMsg} → task marked failed`);
309
- }
310
- })();
311
- this.log("info", `[delegate] ${track.taskId} [step:return] returned immediately (fire-and-forget), background send in progress`);
312
- return { taskId: track.taskId, status: "running" };
313
- }
314
- catch (err) {
315
- const errorMsg = err instanceof Error ? err.message : String(err);
316
- this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
317
- this.log("error", `[delegate] ${track.taskId} [step:catch] ✗ caught error during client creation: ${errorMsg} → task marked failed`);
318
- return { taskId: track.taskId, status: "failed", error: errorMsg };
319
- }
320
- }
321
- /**
322
- * Synchronous delegation: sends A2A task and waits for the result.
323
- * Used by sub-agents internally via the multiclaws_delegate_send tool.
324
- */
325
- async delegateTaskSync(params) {
326
- this.log("info", `[delegate-sync] ▶ delegateTaskSync(agentUrl=${params.agentUrl}, taskLen=${params.task.length})`);
327
- this.log("info", `[delegate-sync] task preview: "${params.task.slice(0, 120)}"`);
328
- // Step 1: Check profile
329
- this.log("info", `[delegate-sync] [step:profile-check] verifying profile completeness`);
330
- try {
331
- await this.requireCompleteProfile();
332
- }
333
- catch (err) {
334
- this.log("error", `[delegate-sync] [step:profile-check] ✗ profile incomplete: ${err instanceof Error ? err.message : String(err)}`);
335
- return { status: "failed", error: err instanceof Error ? err.message : String(err) };
336
- }
337
- // Step 2: Look up agent
338
- this.log("info", `[delegate-sync] [step:agent-lookup] looking up agent: ${params.agentUrl}`);
339
- const agentRecord = await this.agentRegistry.get(params.agentUrl);
340
- if (!agentRecord) {
341
- this.log("warn", `[delegate-sync] [step:agent-lookup] ✗ unknown agent: ${params.agentUrl} → aborting`);
342
- return { status: "failed", error: `unknown agent: ${params.agentUrl}` };
343
- }
344
- this.log("info", `[delegate-sync] [step:agent-lookup] ✓ found: ${agentRecord.name} (${agentRecord.url})`);
345
- // Step 3: Track task
346
- const track = this.taskTracker.create({
347
- fromPeerId: "local",
348
- toPeerId: params.agentUrl,
349
- task: params.task,
350
- });
351
- this.taskTracker.update(track.taskId, { status: "running" });
352
- this.log("info", `[delegate-sync] [step:track] taskId=${track.taskId}, status=running`);
353
- try {
354
- // Step 4: Create A2A client
355
- this.log("info", `[delegate-sync] ${track.taskId} [step:create-client] creating A2A client for ${agentRecord.url}`);
356
- const client = await this.createA2AClient(agentRecord);
357
- // Step 5: Send A2A message (synchronous — blocks until response)
358
- this.log("info", `[delegate-sync] ${track.taskId} [step:send] sending A2A message (sync, metadata: selfUrl=${this.selfUrl}, selfName=${this.agentCard?.name ?? "unknown"})...`);
359
- const result = await client.sendMessage({
360
- message: {
361
- kind: "message",
362
- role: "user",
363
- parts: [{ kind: "text", text: params.task }],
364
- messageId: track.taskId,
365
- metadata: {
366
- agentUrl: this.selfUrl,
367
- agentName: this.agentCard?.name ?? "unknown",
368
- },
369
- },
370
- });
371
- this.log("info", `[delegate-sync] ${track.taskId} [step:send] ✓ A2A response received → processing result`);
372
- // Step 6: Process result
373
- const taskResult = this.processTaskResult(track.taskId, result);
374
- this.log("info", `[delegate-sync] ${track.taskId} [step:completed] ✓ status=${taskResult.status}, outputLen=${taskResult.output?.length ?? 0}, preview="${(taskResult.output ?? "").slice(0, 120)}"`);
375
- return taskResult;
376
- }
377
- catch (err) {
378
- const errorMsg = err instanceof Error ? err.message : String(err);
379
- this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
380
- this.log("error", `[delegate-sync] ${track.taskId} [step:catch] ✗ caught error: ${errorMsg} → task marked failed`);
381
- return { taskId: track.taskId, status: "failed", error: errorMsg };
382
- }
383
- }
384
- /**
385
- * Spawn a sub-agent to handle delegation asynchronously.
386
- * The sub-agent uses multiclaws_delegate_send internally and
387
- * reports results back to the user via the message tool.
388
- */
389
- async spawnDelegation(params) {
390
- this.log("info", `[spawn-delegate] ▶ spawnDelegation(agentUrl=${params.agentUrl}, taskLen=${params.task.length})`);
391
- this.log("info", `[spawn-delegate] task preview: "${params.task.slice(0, 120)}"`);
392
- // Step 1: Check profile
393
- this.log("info", `[spawn-delegate] [step:profile-check] verifying profile completeness`);
394
- try {
395
- await this.requireCompleteProfile();
396
- }
397
- catch (err) {
398
- this.log("error", `[spawn-delegate] [step:profile-check] ✗ profile incomplete: ${err instanceof Error ? err.message : String(err)}`);
399
- throw err;
400
- }
401
- // Step 2: Look up agent
402
- this.log("info", `[spawn-delegate] [step:agent-lookup] looking up agent: ${params.agentUrl}`);
403
- const agent = await this.agentRegistry.get(params.agentUrl);
404
- if (!agent) {
405
- this.log("warn", `[spawn-delegate] [step:agent-lookup] ✗ unknown agent: ${params.agentUrl} → aborting`);
406
- throw new Error(`unknown agent: ${params.agentUrl}`);
407
- }
408
- this.log("info", `[spawn-delegate] [step:agent-lookup] ✓ found: ${agent.name} (${agent.url})`);
409
- // Step 3: Check gateway config
410
- if (!this.gatewayConfig) {
411
- this.log("error", `[spawn-delegate] [step:gateway-check] ✗ gateway config not available → aborting`);
412
- throw new Error("gateway config not available — cannot spawn sub-agent");
413
- }
414
- // Step 4: Spawn sub-agent
415
- const prompt = buildDelegationPrompt(agent, params.task);
416
- const sessionKey = `delegate-${Date.now()}`;
417
- this.log("info", `[spawn-delegate] [step:spawn] calling sessions_spawn (cwd=${this.resolvedCwd}, sessionKey=${sessionKey}, promptLen=${prompt.length})`);
418
- try {
419
- const spawnResult = await (0, gateway_client_1.invokeGatewayTool)({
420
- gateway: this.gatewayConfig,
421
- tool: "sessions_spawn",
422
- args: { task: prompt, mode: "run", cwd: this.resolvedCwd },
423
- sessionKey,
424
- timeoutMs: 15_000,
425
- });
426
- this.log("info", `[spawn-delegate] [step:spawn] ✓ sub-agent spawned for ${agent.name} — result=${JSON.stringify(spawnResult).slice(0, 200)}`);
427
- return { message: `已启动子 agent 向 ${agent.name} 委派任务` };
428
- }
429
- catch (err) {
430
- this.log("error", `[spawn-delegate] [step:spawn] ✗ sessions_spawn failed: ${err instanceof Error ? err.message : String(err)} → aborting`);
431
- throw err;
432
- }
433
- }
434
- getTaskStatus(taskId) {
435
- return this.taskTracker.get(taskId);
436
- }
437
- /* ---------------------------------------------------------------- */
438
- /* Profile */
439
- /* ---------------------------------------------------------------- */
440
- async getProfile() {
441
- return await this.profileStore.load();
442
- }
443
- /**
444
- * Throws if the profile is incomplete (ownerName or bio missing).
445
- * Call this before any action that exposes the user's identity to other agents.
446
- */
447
- async requireCompleteProfile() {
448
- const profile = await this.profileStore.load();
449
- if (!profile.ownerName?.trim()) {
450
- throw new Error("档案未完成设置。请先调用 multiclaws_profile_set(ownerName=\"你的名字\") 设置用户名后再继续。");
451
- }
452
- }
453
- async setProfile(patch) {
454
- this.log("debug", `setProfile(keys=${Object.keys(patch).join(",")})`);
455
- try {
456
- const profile = await this.profileStore.update(patch);
457
- this.updateProfileDescription(profile);
458
- await this.broadcastProfileToTeams();
459
- this.log("debug", "setProfile completed");
460
- return profile;
461
- }
462
- catch (err) {
463
- this.log("error", `setProfile failed: ${err instanceof Error ? err.message : String(err)}`);
464
- throw err;
465
- }
466
- }
467
- updateProfileDescription(profile) {
468
- this.profileDescription = (0, agent_profile_1.renderProfileDescription)(profile);
469
- if (this.agentCard) {
470
- this.agentCard.description = this.profileDescription;
471
- if (profile.ownerName?.trim()) {
472
- this.agentCard.name = (0, agent_profile_1.formatAgentCardName)(profile.ownerName.trim());
473
- }
474
- }
475
- }
476
- /* ---------------------------------------------------------------- */
477
- /* Pending profile review (install / first-run) */
478
- /* ---------------------------------------------------------------- */
479
- getPendingReviewPath() {
480
- return node_path_1.default.join(this.options.stateDir, "multiclaws", "pending-profile-review.json");
481
- }
482
- async getPendingProfileReview() {
483
- const p = this.getPendingReviewPath();
484
- const data = await (0, json_store_1.readJsonWithFallback)(p, {});
485
- if (data.pending !== true) {
486
- return { pending: false };
487
- }
488
- const profile = await this.profileStore.load();
489
- return {
490
- pending: true,
491
- profile,
492
- message: "这是您当前的 MultiClaws 档案,是否需要修改名字、角色、数据源或能力?",
493
- };
494
- }
495
- async setPendingProfileReview() {
496
- this.log("debug", "setPendingProfileReview");
497
- try {
498
- const p = this.getPendingReviewPath();
499
- await (0, json_store_1.writeJsonAtomically)(p, { pending: true });
500
- }
501
- catch (err) {
502
- this.log("error", `setPendingProfileReview failed: ${err instanceof Error ? err.message : String(err)}`);
503
- throw err;
504
- }
505
- }
506
- async clearPendingProfileReview() {
507
- this.log("debug", "clearPendingProfileReview");
508
- const p = this.getPendingReviewPath();
509
- try {
510
- await promises_1.default.unlink(p);
511
- }
512
- catch {
513
- // ignore if missing
514
- }
515
- }
516
- /* ---------------------------------------------------------------- */
517
- /* Team management */
518
- /* ---------------------------------------------------------------- */
519
- async createTeam(name) {
520
- this.log("debug", `createTeam(name=${name})`);
521
- try {
522
- await this.requireCompleteProfile();
523
- const team = await this.teamStore.createTeam({
524
- teamName: name,
525
- selfUrl: this.selfUrl,
526
- selfName: this.getFormattedName(),
527
- selfDescription: this.profileDescription,
528
- });
529
- this.log("info", `team created: ${team.teamId} (${team.teamName})`);
530
- return team;
531
- }
532
- catch (err) {
533
- this.log("error", `createTeam failed: ${err instanceof Error ? err.message : String(err)}`);
534
- throw err;
535
- }
536
- }
537
- async createInvite(teamId) {
538
- this.log("debug", `createInvite(teamId=${teamId ?? "first"})`);
539
- try {
540
- const team = teamId
541
- ? await this.teamStore.getTeam(teamId)
542
- : await this.teamStore.getFirstTeam();
543
- if (!team)
544
- throw new Error(teamId ? `team not found: ${teamId}` : "no team exists");
545
- const code = (0, team_store_1.encodeInvite)(team.teamId, this.selfUrl);
546
- this.log("debug", "createInvite completed");
547
- return code;
548
- }
549
- catch (err) {
550
- this.log("error", `createInvite failed: ${err instanceof Error ? err.message : String(err)}`);
551
- throw err;
552
- }
553
- }
554
- async joinTeam(inviteCode) {
555
- this.log("info", "joinTeam starting");
556
- try {
557
- await this.requireCompleteProfile();
558
- const invite = (0, team_store_1.decodeInvite)(inviteCode);
559
- const seedUrl = invite.u.replace(/\/+$/, "");
560
- this.log("debug", `joinTeam: seedUrl=${seedUrl}, teamId=${invite.t}`);
561
- // 1. Fetch member list from seed
562
- let membersRes;
563
- try {
564
- membersRes = await fetch(`${seedUrl}/team/${invite.t}/members`);
565
- }
566
- catch (err) {
567
- throw new Error(`Unable to reach team seed node at ${seedUrl}: ${err instanceof Error ? err.message : String(err)}`);
568
- }
569
- if (!membersRes.ok) {
570
- throw new Error(`failed to fetch team members from ${seedUrl}: HTTP ${membersRes.status}`);
571
- }
572
- const { team: remoteTeam } = (await membersRes.json());
573
- // 2. Announce self to seed (seed broadcasts to others)
574
- const selfMember = {
575
- url: this.selfUrl,
576
- name: this.getFormattedName(),
577
- description: this.profileDescription,
578
- joinedAtMs: Date.now(),
579
- };
580
- let announceRes;
581
- try {
582
- announceRes = await fetch(`${seedUrl}/team/${invite.t}/announce`, {
583
- method: "POST",
584
- headers: { "Content-Type": "application/json" },
585
- body: JSON.stringify(selfMember),
586
- });
587
- }
588
- catch (err) {
589
- throw new Error(`Failed to announce self to seed ${seedUrl}: ${err instanceof Error ? err.message : String(err)}`);
590
- }
591
- if (!announceRes.ok) {
592
- throw new Error(`failed to announce to seed ${seedUrl}: HTTP ${announceRes.status}`);
593
- }
594
- // 3. Store team locally
595
- const allMembers = [...remoteTeam.members];
596
- const selfNormalized = this.selfUrl.replace(/\/+$/, "");
597
- if (!allMembers.some((m) => m.url.replace(/\/+$/, "") === selfNormalized)) {
598
- allMembers.push(selfMember);
599
- }
600
- const team = {
601
- teamId: invite.t,
602
- teamName: remoteTeam.teamName,
603
- selfUrl: this.selfUrl,
604
- members: allMembers,
605
- createdAtMs: Date.now(),
606
- };
607
- await this.teamStore.saveTeam(team);
608
- // 4. Fetch Agent Cards for members without descriptions, then sync to registry
609
- await this.fetchMemberDescriptions(team);
610
- await this.syncTeamToRegistry(team);
611
- this.log("info", `joined team ${team.teamId} (${team.teamName}) with ${allMembers.length} members`);
612
- return team;
613
- }
614
- catch (err) {
615
- this.log("error", `joinTeam failed: ${err instanceof Error ? err.message : String(err)}`);
616
- throw err;
617
- }
618
- }
619
- async leaveTeam(teamId) {
620
- this.log("info", `leaveTeam(teamId=${teamId ?? "first"})`);
621
- try {
622
- const team = teamId
623
- ? await this.teamStore.getTeam(teamId)
624
- : await this.teamStore.getFirstTeam();
625
- if (!team)
626
- throw new Error(teamId ? `team not found: ${teamId}` : "no team exists");
627
- const selfNormalized = this.selfUrl.replace(/\/+$/, "");
628
- const selfMember = {
629
- url: this.selfUrl,
630
- name: this.getFormattedName(),
631
- joinedAtMs: 0,
632
- };
633
- const others = team.members.filter((m) => m.url.replace(/\/+$/, "") !== selfNormalized);
634
- await Promise.allSettled(others.map(async (m) => {
635
- try {
636
- await fetch(`${m.url}/team/${team.teamId}/leave`, {
637
- method: "POST",
638
- headers: { "Content-Type": "application/json" },
639
- body: JSON.stringify(selfMember),
640
- });
641
- }
642
- catch {
643
- this.log("warn", `failed to notify ${m.url} about leaving`);
644
- }
645
- }));
646
- for (const m of others) {
647
- await this.agentRegistry.remove(m.url);
648
- }
649
- await this.teamStore.deleteTeam(team.teamId);
650
- this.log("info", `left team ${team.teamId}`);
651
- }
652
- catch (err) {
653
- this.log("error", `leaveTeam failed: ${err instanceof Error ? err.message : String(err)}`);
654
- throw err;
655
- }
656
- }
657
- async listTeamMembers(teamId) {
658
- if (teamId) {
659
- const team = await this.teamStore.getTeam(teamId);
660
- if (!team)
661
- return null;
662
- return { team, members: team.members };
663
- }
664
- const all = await this.teamStore.listTeams();
665
- if (all.length === 0)
666
- return null;
667
- if (all.length === 1)
668
- return { team: all[0], members: all[0].members };
669
- return { teams: all.map((team) => ({ team, members: team.members })) };
670
- }
671
- /* ---------------------------------------------------------------- */
672
- /* Team REST routes */
673
- /* ---------------------------------------------------------------- */
674
- mountTeamRoutes(app) {
675
- const announceBodySchema = zod_1.z.object({
676
- url: zod_1.z.string().trim().min(1),
677
- name: zod_1.z.string().trim().min(1),
678
- description: zod_1.z.string().trim().optional(),
679
- joinedAtMs: zod_1.z.number().optional(),
680
- });
681
- const leaveBodySchema = zod_1.z.object({
682
- url: zod_1.z.string().trim().min(1),
683
- });
684
- const profileUpdateBodySchema = zod_1.z.object({
685
- url: zod_1.z.string().trim().min(1),
686
- name: zod_1.z.string().trim().optional(),
687
- description: zod_1.z.string().optional(),
688
- });
689
- app.get("/team/:id/members", async (req, res) => {
690
- try {
691
- const team = await this.teamStore.getTeam(req.params.id);
692
- if (!team) {
693
- res.status(404).json({ error: "team not found" });
694
- return;
695
- }
696
- res.json({ team: { teamName: team.teamName, members: team.members } });
697
- }
698
- catch (err) {
699
- this.log("error", `GET /team/${req.params.id}/members failed: ${err instanceof Error ? err.message : String(err)}`);
700
- res.status(500).json({ error: String(err) });
701
- }
702
- });
703
- app.post("/team/:id/announce", async (req, res) => {
704
- this.log("debug", `POST /team/${req.params.id}/announce from ${req.body?.url}`);
705
- try {
706
- const team = await this.teamStore.getTeam(req.params.id);
707
- if (!team) {
708
- this.log("debug", `announce: team ${req.params.id} not found`);
709
- res.status(404).json({ error: "team not found" });
710
- return;
711
- }
712
- const parsed = announceBodySchema.safeParse(req.body);
713
- if (!parsed.success) {
714
- this.log("debug", `announce: invalid body: ${parsed.error.message}`);
715
- res.status(400).json({ error: parsed.error.message });
716
- return;
717
- }
718
- const member = parsed.data;
719
- this.log("debug", `announce: member=${member.name}, url=${member.url}`);
720
- const normalizedUrl = member.url.replace(/\/+$/, "");
721
- const alreadyKnown = team.members.some((m) => m.url.replace(/\/+$/, "") === normalizedUrl);
722
- this.log("debug", `announce: alreadyKnown=${alreadyKnown}`);
723
- await this.teamStore.addMember(team.teamId, {
724
- url: normalizedUrl,
725
- name: member.name,
726
- description: member.description,
727
- joinedAtMs: member.joinedAtMs ?? Date.now(),
728
- });
729
- this.log("debug", `announce: addMember completed`);
730
- await this.agentRegistry.add({
731
- url: normalizedUrl,
732
- name: member.name,
733
- description: member.description,
734
- });
735
- this.log("debug", `announce: agentRegistry.add completed`);
736
- // Broadcast to other members if new
737
- if (!alreadyKnown) {
738
- const selfNormalized = this.selfUrl.replace(/\/+$/, "");
739
- const others = team.members.filter((m) => m.url.replace(/\/+$/, "") !== normalizedUrl &&
740
- m.url.replace(/\/+$/, "") !== selfNormalized);
741
- this.log("debug", `announce: broadcasting to ${others.length} other members`);
742
- for (const other of others) {
743
- void this.fetchWithRetry(`${other.url}/team/${team.teamId}/announce`, {
744
- method: "POST",
745
- headers: { "Content-Type": "application/json" },
746
- body: JSON.stringify({
747
- url: normalizedUrl,
748
- name: member.name,
749
- description: member.description,
750
- joinedAtMs: member.joinedAtMs ?? Date.now(),
751
- }),
752
- }).catch((err) => {
753
- this.log("warn", `broadcast to ${other.url} failed: ${err instanceof Error ? err.message : String(err)}`);
754
- });
755
- }
756
- // Notify local user that a new member joined
757
- this.log("debug", `announce: notifying local user about ${member.name}`);
758
- void this.notifyUser(`📢 **${member.name}** 已加入团队「${team.teamName}」`);
759
- }
760
- this.log("debug", `announce: completed for ${member.name}`);
761
- res.json({ ok: true });
762
- }
763
- catch (err) {
764
- this.log("error", `POST /team/${req.params.id}/announce failed: ${err instanceof Error ? err.message : String(err)}`);
765
- res.status(500).json({ error: String(err) });
766
- }
767
- });
768
- app.post("/team/:id/leave", async (req, res) => {
769
- this.log("debug", `POST /team/${req.params.id}/leave from ${req.body?.url}`);
770
- try {
771
- const team = await this.teamStore.getTeam(req.params.id);
772
- if (!team) {
773
- res.status(404).json({ error: "team not found" });
774
- return;
775
- }
776
- const parsed = leaveBodySchema.safeParse(req.body);
777
- if (!parsed.success) {
778
- res.status(400).json({ error: parsed.error.message });
779
- return;
780
- }
781
- const normalizedUrl = parsed.data.url.replace(/\/+$/, "");
782
- await this.teamStore.removeMember(team.teamId, normalizedUrl);
783
- await this.agentRegistry.remove(normalizedUrl);
784
- res.json({ ok: true });
785
- }
786
- catch (err) {
787
- this.log("error", `POST /team/${req.params.id}/leave failed: ${err instanceof Error ? err.message : String(err)}`);
788
- res.status(500).json({ error: String(err) });
789
- }
790
- });
791
- // Profile update broadcast receiver
792
- app.post("/team/:id/profile-update", async (req, res) => {
793
- this.log("debug", `POST /team/${req.params.id}/profile-update from ${req.body?.url}`);
794
- try {
795
- const team = await this.teamStore.getTeam(req.params.id);
796
- if (!team) {
797
- res.status(404).json({ error: "team not found" });
798
- return;
799
- }
800
- const parsed = profileUpdateBodySchema.safeParse(req.body);
801
- if (!parsed.success) {
802
- res.status(400).json({ error: parsed.error.message });
803
- return;
804
- }
805
- const { url, name, description } = parsed.data;
806
- const normalizedUrl = url.replace(/\/+$/, "");
807
- // Update team member description
808
- const existing = team.members.find((m) => m.url.replace(/\/+$/, "") === normalizedUrl);
809
- if (existing) {
810
- if (name)
811
- existing.name = name;
812
- if (description !== undefined)
813
- existing.description = description;
814
- await this.teamStore.saveTeam(team);
815
- }
816
- // Update agent registry description
817
- if (description !== undefined) {
818
- await this.agentRegistry.updateDescription(normalizedUrl, description);
819
- }
820
- res.json({ ok: true });
821
- }
822
- catch (err) {
823
- this.log("error", `POST /team/${req.params.id}/profile-update failed: ${err instanceof Error ? err.message : String(err)}`);
824
- res.status(500).json({ error: String(err) });
825
- }
826
- });
827
- }
828
- /* ---------------------------------------------------------------- */
829
- /* Private helpers */
830
- /* ---------------------------------------------------------------- */
831
- async broadcastProfileToTeams() {
832
- this.log("debug", "broadcastProfileToTeams");
833
- try {
834
- const teams = await this.teamStore.listTeams();
835
- const selfNormalized = this.selfUrl.replace(/\/+$/, "");
836
- const agentName = this.getFormattedName();
837
- for (const team of teams) {
838
- // Update self in team store
839
- await this.teamStore.addMember(team.teamId, {
840
- url: this.selfUrl,
841
- name: agentName,
842
- description: this.profileDescription,
843
- joinedAtMs: Date.now(),
844
- });
845
- // Broadcast to other members
846
- const others = team.members.filter((m) => m.url.replace(/\/+$/, "") !== selfNormalized);
847
- for (const member of others) {
848
- void this.fetchWithRetry(`${member.url}/team/${team.teamId}/profile-update`, {
849
- method: "POST",
850
- headers: { "Content-Type": "application/json" },
851
- body: JSON.stringify({
852
- url: this.selfUrl,
853
- name: agentName,
854
- description: this.profileDescription,
855
- }),
856
- }).catch(() => {
857
- this.log("warn", `profile broadcast to ${member.url} failed`);
858
- });
859
- }
860
- }
861
- this.log("debug", "broadcastProfileToTeams completed");
862
- }
863
- catch (err) {
864
- this.log("error", `broadcastProfileToTeams failed: ${err instanceof Error ? err.message : String(err)}`);
865
- throw err;
866
- }
867
- }
868
- async fetchMemberDescriptions(team) {
869
- const selfNormalized = this.selfUrl.replace(/\/+$/, "");
870
- const membersToFetch = team.members.filter((m) => m.url.replace(/\/+$/, "") !== selfNormalized && !m.description);
871
- this.log("debug", `fetchMemberDescriptions(teamId=${team.teamId}, count=${membersToFetch.length})`);
872
- try {
873
- await Promise.allSettled(membersToFetch.map(async (m) => {
874
- try {
875
- const client = await this.clientFactory.createFromUrl(m.url);
876
- const card = await client.getAgentCard();
877
- if (card.description) {
878
- m.description = card.description;
879
- }
880
- }
881
- catch {
882
- this.log("warn", `failed to fetch Agent Card from ${m.url}`);
883
- }
884
- }));
885
- await this.teamStore.saveTeam(team);
886
- this.log("debug", "fetchMemberDescriptions completed");
887
- }
888
- catch (err) {
889
- this.log("error", `fetchMemberDescriptions failed: ${err instanceof Error ? err.message : String(err)}`);
890
- throw err;
891
- }
892
- }
893
- async syncTeamToRegistry(team) {
894
- this.log("debug", `syncTeamToRegistry(teamId=${team.teamId})`);
895
- try {
896
- const selfNormalized = this.selfUrl.replace(/\/+$/, "");
897
- for (const member of team.members) {
898
- if (member.url.replace(/\/+$/, "") === selfNormalized)
899
- continue;
900
- await this.agentRegistry.add({
901
- url: member.url,
902
- name: member.name,
903
- description: member.description,
904
- });
905
- }
906
- this.log("debug", "syncTeamToRegistry completed");
907
- }
908
- catch (err) {
909
- this.log("error", `syncTeamToRegistry failed: ${err instanceof Error ? err.message : String(err)}`);
910
- throw err;
911
- }
912
- }
913
- async createA2AClient(agent) {
914
- this.log("info", `[a2a-client] creating client for ${agent.name} (${agent.url})`);
915
- try {
916
- const client = await this.clientFactory.createFromUrl(agent.url);
917
- this.log("info", `[a2a-client] ✓ client created for ${agent.name} (${agent.url})`);
918
- return client;
919
- }
920
- catch (err) {
921
- this.log("error", `[a2a-client] ✗ failed to create client for ${agent.name} (${agent.url}): ${err instanceof Error ? err.message : String(err)}`);
922
- throw err;
923
- }
924
- }
925
- /**
926
- * Send a message using A2A streaming to minimize latency.
927
- * Instead of a single blocking HTTP call, consume the SSE stream and
928
- * return the final Task or Message as soon as B signals completion.
929
- */
930
- processTaskResult(trackId, result) {
931
- this.log("info", `[process-result] processing result for ${trackId}, resultType=${("status" in result && result.status) ? "Task" : "Message"}`);
932
- try {
933
- if ("status" in result && result.status) {
934
- const task = result;
935
- const state = task.status?.state ?? "unknown";
936
- const output = this.extractArtifactText(task);
937
- this.log("info", `[process-result] ${trackId} Task response — state=${state}, outputLen=${output.length}, preview=${output.slice(0, 120)}`);
938
- if (state === "completed") {
939
- this.taskTracker.update(trackId, { status: "completed", result: output });
940
- this.log("info", `[process-result] ✓ ${trackId} marked completed`);
941
- }
942
- else if (state === "failed") {
943
- this.taskTracker.update(trackId, { status: "failed", error: output || "remote task failed" });
944
- this.log("warn", `[process-result] ✗ ${trackId} marked failed — error=${output || "remote task failed"}`);
945
- }
946
- else {
947
- // For any other state (unknown, working, etc.), mark as failed to avoid
948
- // tasks stuck in "running" forever until TTL prune.
949
- this.taskTracker.update(trackId, { status: "failed", error: `unexpected remote state: ${state}` });
950
- this.log("warn", `[process-result] ✗ ${trackId} unexpected state=${state}, marked failed`);
951
- }
952
- return { taskId: task.id, output, status: state };
953
- }
954
- const msg = result;
955
- const text = msg.parts
956
- ?.filter((p) => p.kind === "text")
957
- .map((p) => p.text)
958
- .join("\n") ?? "";
959
- this.taskTracker.update(trackId, { status: "completed", result: text });
960
- this.log("info", `[process-result] ✓ ${trackId} Message response — completed, textLen=${text.length}, preview=${text.slice(0, 120)}`);
961
- return { taskId: trackId, output: text, status: "completed" };
962
- }
963
- catch (err) {
964
- this.log("error", `[process-result] ✗ ${trackId} processing failed: ${err instanceof Error ? err.message : String(err)}`);
965
- throw err;
966
- }
967
- }
968
- extractArtifactText(task) {
969
- if (!task.artifacts?.length)
970
- return "";
971
- return task.artifacts
972
- .flatMap((a) => a.parts ?? [])
973
- .filter((p) => p.kind === "text")
974
- .map((p) => p.text)
975
- .join("\n");
976
- }
977
- /** Fetch with up to 2 retries and exponential backoff. */
978
- async fetchWithRetry(url, init, retries = 2) {
979
- let lastError = null;
980
- for (let attempt = 0; attempt <= retries; attempt++) {
981
- try {
982
- const res = await fetch(url, init);
983
- if (res.ok || attempt === retries)
984
- return res;
985
- lastError = new Error(`HTTP ${res.status}`);
986
- }
987
- catch (err) {
988
- lastError = err instanceof Error ? err : new Error(String(err));
989
- if (attempt === retries)
990
- break;
991
- }
992
- await new Promise((r) => setTimeout(r, 200 * 2 ** attempt));
993
- }
994
- throw lastError;
995
- }
996
- /**
997
- * Called by the `multiclaws_task_respond` tool when the local human
998
- * approves or rejects a pending risky incoming task.
999
- */
1000
- respondToTask(taskId, approved) {
1001
- this.log("info", `respondToTask(taskId=${taskId}, approved=${approved})`);
1002
- if (!this.agentExecutor) {
1003
- this.log("warn", `respondToTask: no agentExecutor available for taskId=${taskId}`);
1004
- return false;
1005
- }
1006
- const resolved = this.agentExecutor.resolveApproval(taskId, approved);
1007
- this.log("info", `respondToTask: ${resolved ? "✓" : "✗"} taskId=${taskId} ${resolved ? "resolved" : "no pending approval found"}`);
1008
- return resolved;
1009
- }
1010
- /** Resolve a pending A2A callback from sub-agent. */
1011
- resolveA2ACallback(taskId, result) {
1012
- this.log("info", `[a2a-callback] resolveA2ACallback(taskId=${taskId}, resultLen=${result.length})`);
1013
- if (!this.agentExecutor) {
1014
- this.log("warn", `[a2a-callback] ✗ no agentExecutor available for taskId=${taskId}`);
1015
- return false;
1016
- }
1017
- const resolved = this.agentExecutor.resolveCallback(taskId, result);
1018
- this.log("info", `[a2a-callback] ${resolved ? "✓" : "✗"} taskId=${taskId} ${resolved ? "resolved" : "no pending callback found"}`);
1019
- return resolved;
1020
- }
1021
- addNotificationTarget(key, target) {
1022
- if (!this.notificationTargets.has(key)) {
1023
- this.notificationTargets.set(key, target);
1024
- this.log("debug", `notification target registered: ${key} (total: ${this.notificationTargets.size})`);
1025
- }
1026
- }
1027
- /** Consistent name for this agent: AgentCard.name or fallback. */
1028
- getFormattedName() {
1029
- return this.agentCard?.name ?? "OpenClaw Agent";
1030
- }
1031
- /** Discover the most recently active non-internal session via sessions_list. */
1032
- async discoverActiveSession() {
1033
- if (!this.gatewayConfig) {
1034
- this.log("warn", `discoverActiveSession: skipped — no gateway config`);
1035
- return null;
1036
- }
1037
- try {
1038
- this.log("info", `discoverActiveSession: calling sessions_list (limit=10, activeMinutes=120)`);
1039
- const raw = await (0, gateway_client_1.invokeGatewayTool)({
1040
- gateway: this.gatewayConfig,
1041
- tool: "sessions_list",
1042
- args: { limit: 10, activeMinutes: 120 },
1043
- timeoutMs: 5_000,
1044
- });
1045
- this.log("info", `discoverActiveSession: raw result = ${JSON.stringify(raw).slice(0, 500)}`);
1046
- // Unwrap gateway tool standard response: { content: [{ type: "text", text: "..." }] }
1047
- let parsed = raw;
1048
- if (raw?.content?.[0]?.type === "text") {
1049
- try {
1050
- parsed = JSON.parse(raw.content[0].text);
1051
- this.log("info", `discoverActiveSession: unwrapped gateway response successfully`);
1052
- }
1053
- catch (parseErr) {
1054
- this.log("warn", `discoverActiveSession: failed to parse content[0].text as JSON — ${parseErr instanceof Error ? parseErr.message : String(parseErr)}, using raw object`);
1055
- }
1056
- }
1057
- const sessions = parsed?.sessions ?? [];
1058
- this.log("info", `discoverActiveSession: found ${sessions.length} sessions from gateway`);
1059
- const INTERNAL_PREFIXES = ["delegate-", "a2a-"];
1060
- // sessions_list returns "key" not "sessionKey"
1061
- const session = sessions.find((s) => {
1062
- const k = (s.key ?? s.sessionKey);
1063
- return k && !INTERNAL_PREFIXES.some((p) => k.startsWith(p));
1064
- });
1065
- const matchedKey = (session?.key ?? session?.sessionKey);
1066
- if (matchedKey) {
1067
- this.log("info", `discoverActiveSession: ✓ matched session ${matchedKey}`);
1068
- }
1069
- else {
1070
- this.log("warn", `discoverActiveSession: ✗ all ${sessions.length} sessions filtered or empty`);
1071
- sessions.forEach((s, i) => this.log("info", `discoverActiveSession: session[${i}]: key=${(s.key ?? s.sessionKey) ?? "(no key)"}`));
1072
- }
1073
- return matchedKey ?? null;
1074
- }
1075
- catch (err) {
1076
- this.log("error", `discoverActiveSession: ✗ caught error — ${err instanceof Error ? err.message : String(err)}, returning null`);
1077
- return null;
1078
- }
1079
- }
1080
- async notifyUser(message) {
1081
- this.log("info", `notifyUser: targets=${this.notificationTargets.size}, msgLen=${message.length}, preview="${message.slice(0, 80)}"`);
1082
- if (!this.gatewayConfig) {
1083
- this.log("warn", "notifyUser: skipped — no gatewayConfig, message lost");
1084
- return;
1085
- }
1086
- // Fallback: no registered targets yet (e.g. right after gateway restart)
1087
- if (this.notificationTargets.size === 0) {
1088
- this.log("info", "notifyUser: no registered targets → falling back to discoverActiveSession()");
1089
- const sessionKey = await this.discoverActiveSession();
1090
- if (sessionKey) {
1091
- this.log("info", `notifyUser: fallback discovered session ${sessionKey} → calling sessions_send`);
1092
- try {
1093
- await (0, gateway_client_1.invokeGatewayTool)({
1094
- gateway: this.gatewayConfig,
1095
- tool: "sessions_send",
1096
- args: { sessionKey, message },
1097
- timeoutMs: 5_000,
1098
- });
1099
- this.log("info", `notifyUser: ✓ fallback sessions_send to ${sessionKey} succeeded`);
1100
- this.addNotificationTarget(`web:${sessionKey}`, { type: "web", sessionKey });
1101
- this.log("info", `notifyUser: registered ${sessionKey} as notification target for future use`);
1102
- }
1103
- catch (err) {
1104
- this.log("error", `notifyUser: ✗ fallback sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
1105
- }
1106
- }
1107
- else {
1108
- this.log("warn", "notifyUser: ✗ discoverActiveSession returned null — no active session found, message lost");
1109
- }
1110
- return;
1111
- }
1112
- const entries = [...this.notificationTargets.entries()];
1113
- this.log("info", `notifyUser: sending to ${entries.length} registered target(s): [${entries.map(([k]) => k).join(", ")}]`);
1114
- const results = await Promise.allSettled(entries.map(async ([key, target]) => {
1115
- this.log("info", `notifyUser: → ${key} (type=${target.type})`);
1116
- try {
1117
- await (target.type === "channel"
1118
- ? (0, gateway_client_1.invokeGatewayTool)({
1119
- gateway: this.gatewayConfig,
1120
- tool: "message",
1121
- args: { action: "send", target: target.conversationId, message },
1122
- timeoutMs: 5_000,
1123
- })
1124
- : (0, gateway_client_1.invokeGatewayTool)({
1125
- gateway: this.gatewayConfig,
1126
- tool: "sessions_send",
1127
- args: { sessionKey: target.sessionKey, message },
1128
- timeoutMs: 5_000,
1129
- }));
1130
- this.log("info", `notifyUser: ✓ ${key} (${target.type}) succeeded`);
1131
- }
1132
- catch (err) {
1133
- this.log("error", `notifyUser: ✗ ${key} (${target.type}) failed: ${err instanceof Error ? err.message : String(err)}`);
1134
- throw err;
1135
- }
1136
- }));
1137
- const okCount = results.filter((r) => r.status === "fulfilled").length;
1138
- const failCount = results.filter((r) => r.status === "rejected").length;
1139
- if (failCount === 0) {
1140
- this.log("info", `notifyUser: ✓ all ${okCount} targets succeeded`);
1141
- }
1142
- else if (failCount === entries.length) {
1143
- this.log("error", `notifyUser: ✗ ALL ${failCount} targets failed`);
1144
- }
1145
- else {
1146
- this.log("warn", `notifyUser: ${okCount} ok, ${failCount} FAILED out of ${entries.length} targets`);
1147
- }
1148
- }
1149
- log(level, message) {
1150
- this.options.logger?.[level]?.(`[multiclaws] ${message}`);
1151
- }
1152
- }
1153
- exports.MulticlawsService = MulticlawsService;
50
+ - 遇到错误时在 multiclaws_notify 中说明失败原因`;
51
+ }
52
+ /* ------------------------------------------------------------------ */
53
+ /* Service */
54
+ /* ------------------------------------------------------------------ */
55
+ class MulticlawsService extends node_events_1.EventEmitter {
56
+ options;
57
+ started = false;
58
+ httpServer = null;
59
+ agentRegistry;
60
+ teamStore;
61
+ profileStore;
62
+ taskTracker;
63
+ agentExecutor = null;
64
+ a2aRequestHandler = null;
65
+ agentCard = null;
66
+ clientFactory = new client_1.ClientFactory();
67
+ httpRateLimiter = new rate_limiter_1.RateLimiter({ windowMs: 60_000, maxRequests: 60 });
68
+ frpTunnel = null;
69
+ selfUrl;
70
+ profileDescription = "OpenClaw agent";
71
+ gatewayConfig;
72
+ resolvedCwd;
73
+ notificationTargets = new Map();
74
+ constructor(options) {
75
+ super();
76
+ this.options = options;
77
+ const multiclawsStateDir = node_path_1.default.join(options.stateDir, "multiclaws");
78
+ this.agentRegistry = new agent_registry_1.AgentRegistry(node_path_1.default.join(multiclawsStateDir, "agents.json"), options.logger);
79
+ this.teamStore = new team_store_1.TeamStore(node_path_1.default.join(multiclawsStateDir, "teams.json"), options.logger);
80
+ this.profileStore = new agent_profile_1.ProfileStore(node_path_1.default.join(multiclawsStateDir, "profile.json"), options.logger);
81
+ this.taskTracker = new tracker_1.TaskTracker({
82
+ filePath: node_path_1.default.join(multiclawsStateDir, "tasks.json"),
83
+ logger: options.logger,
84
+ });
85
+ // selfUrl resolved later in start() after FRP tunnel setup
86
+ this.selfUrl = options.selfUrl ?? "";
87
+ this.gatewayConfig = options.gatewayConfig ?? null;
88
+ this.resolvedCwd = options.cwd || node_os_1.default.homedir();
89
+ }
90
+ async start() {
91
+ if (this.started)
92
+ return;
93
+ this.log("debug", `start(port=${this.options.port ?? 3100}, selfUrl=${this.options.selfUrl ?? "auto"})`);
94
+ try {
95
+ // Resolve selfUrl: explicit config > FRP tunnel
96
+ if (!this.options.selfUrl) {
97
+ const port = this.options.port ?? 3100;
98
+ if (!this.options.tunnel || this.options.tunnel.type !== "frp") {
99
+ throw new Error("multiclaws requires either 'selfUrl' or 'tunnel' configuration. " +
100
+ "Please configure tunnel in plugin settings.");
101
+ }
102
+ this.frpTunnel = new frp_1.FrpTunnelManager({
103
+ config: this.options.tunnel,
104
+ localPort: port,
105
+ stateDir: node_path_1.default.join(this.options.stateDir, "multiclaws"),
106
+ logger: this.options.logger,
107
+ });
108
+ const publicUrl = await this.frpTunnel.start();
109
+ this.selfUrl = publicUrl;
110
+ this.log("info", `FRP tunnel ready: ${publicUrl}`);
111
+ }
112
+ // Load profile for AgentCard description
113
+ const profile = await this.profileStore.load();
114
+ if (!profile.ownerName?.trim()) {
115
+ await this.setPendingProfileReview();
116
+ }
117
+ this.profileDescription = (0, agent_profile_1.renderProfileDescription)(profile);
118
+ const logger = this.options.logger ?? { info: () => { }, warn: () => { }, error: () => { } };
119
+ this.agentExecutor = new a2a_adapter_1.OpenClawAgentExecutor({
120
+ gatewayConfig: this.options.gatewayConfig ?? null,
121
+ taskTracker: this.taskTracker,
122
+ cwd: this.resolvedCwd,
123
+ getNotificationTargets: () => this.notificationTargets,
124
+ registerDiscoveredTarget: (sessionKey) => {
125
+ this.addNotificationTarget(`web:${sessionKey}`, { type: "web", sessionKey });
126
+ },
127
+ logger,
128
+ });
129
+ this.agentCard = {
130
+ name: profile.ownerName?.trim() ? (0, agent_profile_1.formatAgentCardName)(profile.ownerName.trim()) : "OpenClaw Agent",
131
+ description: this.profileDescription,
132
+ url: this.selfUrl,
133
+ version: version_1.PLUGIN_VERSION,
134
+ protocolVersion: "0.2.2",
135
+ defaultInputModes: ["text/plain"],
136
+ defaultOutputModes: ["text/plain"],
137
+ capabilities: { streaming: false, pushNotifications: false },
138
+ skills: [
139
+ {
140
+ id: "general",
141
+ name: "General Task",
142
+ description: "Execute any delegated task via OpenClaw",
143
+ tags: ["task", "delegation", "general"],
144
+ },
145
+ ],
146
+ };
147
+ const taskStore = new server_1.InMemoryTaskStore();
148
+ this.a2aRequestHandler = new server_1.DefaultRequestHandler(this.agentCard, taskStore, this.agentExecutor);
149
+ const app = (0, express_1.default)();
150
+ app.use(express_1.default.json({ limit: "1mb" }));
151
+ // Rate limiting
152
+ app.use((req, res, next) => {
153
+ const clientIp = req.ip ?? req.socket.remoteAddress ?? "unknown";
154
+ if (!this.httpRateLimiter.allow(clientIp)) {
155
+ res.status(429).json({ error: "rate limited" });
156
+ return;
157
+ }
158
+ next();
159
+ });
160
+ // Team + profile REST endpoints
161
+ this.mountTeamRoutes(app);
162
+ // A2A endpoints
163
+ app.use("/.well-known/agent-card.json", (0, express_2.agentCardHandler)({
164
+ agentCardProvider: this.a2aRequestHandler,
165
+ }));
166
+ app.use("/", (0, express_2.jsonRpcHandler)({
167
+ requestHandler: this.a2aRequestHandler,
168
+ userBuilder: express_2.UserBuilder.noAuthentication,
169
+ }));
170
+ const listenPort = this.options.port ?? 3100;
171
+ this.httpServer = node_http_1.default.createServer(app);
172
+ await new Promise((resolve) => this.httpServer.listen(listenPort, "0.0.0.0", resolve));
173
+ this.started = true;
174
+ this.log("info", `multiclaws A2A service listening on :${listenPort}`);
175
+ }
176
+ catch (err) {
177
+ this.log("error", `start failed: ${err instanceof Error ? err.message : String(err)}`);
178
+ throw err;
179
+ }
180
+ }
181
+ async stop() {
182
+ if (!this.started)
183
+ return;
184
+ this.log("debug", "stopping");
185
+ try {
186
+ this.started = false;
187
+ this.taskTracker.destroy();
188
+ this.httpRateLimiter.destroy();
189
+ if (this.frpTunnel) {
190
+ await this.frpTunnel.stop();
191
+ this.frpTunnel = null;
192
+ }
193
+ await new Promise((resolve) => {
194
+ if (!this.httpServer) {
195
+ resolve();
196
+ return;
197
+ }
198
+ this.httpServer.close(() => resolve());
199
+ });
200
+ this.httpServer = null;
201
+ this.log("debug", "stopped");
202
+ }
203
+ catch (err) {
204
+ this.log("error", `stop failed: ${err instanceof Error ? err.message : String(err)}`);
205
+ throw err;
206
+ }
207
+ }
208
+ updateGatewayConfig(config) {
209
+ this.agentExecutor?.updateGatewayConfig(config);
210
+ }
211
+ /* ---------------------------------------------------------------- */
212
+ /* Agent management */
213
+ /* ---------------------------------------------------------------- */
214
+ async listAgents() {
215
+ return await this.agentRegistry.list();
216
+ }
217
+ async addAgent(params) {
218
+ const normalizedUrl = params.url.replace(/\/+$/, "");
219
+ this.log("debug", `addAgent(url=${normalizedUrl})`);
220
+ try {
221
+ const client = await this.clientFactory.createFromUrl(normalizedUrl);
222
+ const card = await client.getAgentCard();
223
+ const result = await this.agentRegistry.add({
224
+ url: normalizedUrl,
225
+ name: card.name ?? normalizedUrl,
226
+ description: card.description ?? "",
227
+ skills: card.skills?.map((s) => s.name ?? s.id) ?? [],
228
+ apiKey: params.apiKey,
229
+ });
230
+ this.log("debug", `addAgent completed, name=${result.name}`);
231
+ return result;
232
+ }
233
+ catch {
234
+ this.log("debug", `addAgent: card fetch failed for ${normalizedUrl}, adding with URL as name`);
235
+ return await this.agentRegistry.add({
236
+ url: normalizedUrl,
237
+ name: normalizedUrl,
238
+ apiKey: params.apiKey,
239
+ });
240
+ }
241
+ }
242
+ async removeAgent(url) {
243
+ this.log("debug", `removeAgent(url=${url})`);
244
+ try {
245
+ const result = await this.agentRegistry.remove(url);
246
+ this.log("debug", `removeAgent completed, result=${result}`);
247
+ return result;
248
+ }
249
+ catch (err) {
250
+ this.log("error", `removeAgent failed: ${err instanceof Error ? err.message : String(err)}`);
251
+ throw err;
252
+ }
253
+ }
254
+ /* ---------------------------------------------------------------- */
255
+ /* Task delegation */
256
+ /* ---------------------------------------------------------------- */
257
+ async delegateTask(params) {
258
+ this.log("info", `[delegate] ▶ delegateTask(agentUrl=${params.agentUrl}, taskLen=${params.task.length})`);
259
+ this.log("info", `[delegate] task preview: "${params.task.slice(0, 120)}"`);
260
+ // Step 1: Check profile
261
+ this.log("info", `[delegate] [step:profile-check] verifying profile completeness`);
262
+ try {
263
+ await this.requireCompleteProfile();
264
+ }
265
+ catch (err) {
266
+ this.log("error", `[delegate] [step:profile-check] ✗ profile incomplete: ${err instanceof Error ? err.message : String(err)}`);
267
+ return { status: "failed", error: err instanceof Error ? err.message : String(err) };
268
+ }
269
+ // Step 2: Look up agent
270
+ this.log("info", `[delegate] [step:agent-lookup] looking up agent: ${params.agentUrl}`);
271
+ const agentRecord = await this.agentRegistry.get(params.agentUrl);
272
+ if (!agentRecord) {
273
+ this.log("warn", `[delegate] [step:agent-lookup] ✗ unknown agent: ${params.agentUrl} → aborting`);
274
+ return { status: "failed", error: `unknown agent: ${params.agentUrl}` };
275
+ }
276
+ this.log("info", `[delegate] [step:agent-lookup] ✓ found: ${agentRecord.name} (${agentRecord.url})`);
277
+ // Step 3: Track task
278
+ const track = this.taskTracker.create({
279
+ fromPeerId: "local",
280
+ toPeerId: params.agentUrl,
281
+ task: params.task,
282
+ });
283
+ this.taskTracker.update(track.taskId, { status: "running" });
284
+ this.log("info", `[delegate] [step:track] taskId=${track.taskId}, status=running`);
285
+ try {
286
+ // Step 4: Create A2A client
287
+ this.log("info", `[delegate] ${track.taskId} [step:create-client] creating A2A client for ${agentRecord.url}`);
288
+ const client = await this.createA2AClient(agentRecord);
289
+ this.log("info", `[delegate] ${track.taskId} [step:create-client] ✓ client created → starting fire-and-forget send`);
290
+ // Step 5: Fire-and-forget execution
291
+ void (async () => {
292
+ try {
293
+ this.log("info", `[delegate] ${track.taskId} [step:background-send] sending A2A message to ${agentRecord.name}...`);
294
+ const result = await client.sendMessage({
295
+ message: {
296
+ kind: "message",
297
+ role: "user",
298
+ parts: [{ kind: "text", text: params.task }],
299
+ messageId: track.taskId,
300
+ },
301
+ });
302
+ this.log("info", `[delegate] ${track.taskId} [step:background-send] ✓ A2A response received → processing result`);
303
+ this.processTaskResult(track.taskId, result);
304
+ }
305
+ catch (err) {
306
+ const errorMsg = err instanceof Error ? err.message : String(err);
307
+ this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
308
+ this.log("error", `[delegate] ${track.taskId} [step:background-send] ✗ caught error: ${errorMsg} → task marked failed`);
309
+ }
310
+ })();
311
+ this.log("info", `[delegate] ${track.taskId} [step:return] returned immediately (fire-and-forget), background send in progress`);
312
+ return { taskId: track.taskId, status: "running" };
313
+ }
314
+ catch (err) {
315
+ const errorMsg = err instanceof Error ? err.message : String(err);
316
+ this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
317
+ this.log("error", `[delegate] ${track.taskId} [step:catch] ✗ caught error during client creation: ${errorMsg} → task marked failed`);
318
+ return { taskId: track.taskId, status: "failed", error: errorMsg };
319
+ }
320
+ }
321
+ /**
322
+ * Synchronous delegation: sends A2A task and waits for the result.
323
+ * Used by sub-agents internally via the multiclaws_delegate_send tool.
324
+ */
325
+ async delegateTaskSync(params) {
326
+ this.log("info", `[delegate-sync] ▶ delegateTaskSync(agentUrl=${params.agentUrl}, taskLen=${params.task.length})`);
327
+ this.log("info", `[delegate-sync] task preview: "${params.task.slice(0, 120)}"`);
328
+ // Step 1: Check profile
329
+ this.log("info", `[delegate-sync] [step:profile-check] verifying profile completeness`);
330
+ try {
331
+ await this.requireCompleteProfile();
332
+ }
333
+ catch (err) {
334
+ this.log("error", `[delegate-sync] [step:profile-check] ✗ profile incomplete: ${err instanceof Error ? err.message : String(err)}`);
335
+ return { status: "failed", error: err instanceof Error ? err.message : String(err) };
336
+ }
337
+ // Step 2: Look up agent
338
+ this.log("info", `[delegate-sync] [step:agent-lookup] looking up agent: ${params.agentUrl}`);
339
+ const agentRecord = await this.agentRegistry.get(params.agentUrl);
340
+ if (!agentRecord) {
341
+ this.log("warn", `[delegate-sync] [step:agent-lookup] ✗ unknown agent: ${params.agentUrl} → aborting`);
342
+ return { status: "failed", error: `unknown agent: ${params.agentUrl}` };
343
+ }
344
+ this.log("info", `[delegate-sync] [step:agent-lookup] ✓ found: ${agentRecord.name} (${agentRecord.url})`);
345
+ // Step 3: Track task
346
+ const track = this.taskTracker.create({
347
+ fromPeerId: "local",
348
+ toPeerId: params.agentUrl,
349
+ task: params.task,
350
+ });
351
+ this.taskTracker.update(track.taskId, { status: "running" });
352
+ this.log("info", `[delegate-sync] [step:track] taskId=${track.taskId}, status=running`);
353
+ try {
354
+ // Step 4: Create A2A client
355
+ this.log("info", `[delegate-sync] ${track.taskId} [step:create-client] creating A2A client for ${agentRecord.url}`);
356
+ const client = await this.createA2AClient(agentRecord);
357
+ // Step 5: Send A2A message (synchronous — blocks until response)
358
+ this.log("info", `[delegate-sync] ${track.taskId} [step:send] sending A2A message (sync, metadata: selfUrl=${this.selfUrl}, selfName=${this.agentCard?.name ?? "unknown"})...`);
359
+ const result = await client.sendMessage({
360
+ message: {
361
+ kind: "message",
362
+ role: "user",
363
+ parts: [{ kind: "text", text: params.task }],
364
+ messageId: track.taskId,
365
+ metadata: {
366
+ agentUrl: this.selfUrl,
367
+ agentName: this.agentCard?.name ?? "unknown",
368
+ },
369
+ },
370
+ });
371
+ this.log("info", `[delegate-sync] ${track.taskId} [step:send] ✓ A2A response received → processing result`);
372
+ // Step 6: Process result
373
+ const taskResult = this.processTaskResult(track.taskId, result);
374
+ this.log("info", `[delegate-sync] ${track.taskId} [step:completed] ✓ status=${taskResult.status}, outputLen=${taskResult.output?.length ?? 0}, preview="${(taskResult.output ?? "").slice(0, 120)}"`);
375
+ return taskResult;
376
+ }
377
+ catch (err) {
378
+ const errorMsg = err instanceof Error ? err.message : String(err);
379
+ this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
380
+ this.log("error", `[delegate-sync] ${track.taskId} [step:catch] ✗ caught error: ${errorMsg} → task marked failed`);
381
+ return { taskId: track.taskId, status: "failed", error: errorMsg };
382
+ }
383
+ }
384
+ /**
385
+ * Spawn a sub-agent to handle delegation asynchronously.
386
+ * The sub-agent uses multiclaws_delegate_send internally and
387
+ * reports results back to the user via the message tool.
388
+ */
389
+ async spawnDelegation(params) {
390
+ this.log("info", `[spawn-delegate] ▶ spawnDelegation(agentUrl=${params.agentUrl}, taskLen=${params.task.length})`);
391
+ this.log("info", `[spawn-delegate] task preview: "${params.task.slice(0, 120)}"`);
392
+ // Step 1: Check profile
393
+ this.log("info", `[spawn-delegate] [step:profile-check] verifying profile completeness`);
394
+ try {
395
+ await this.requireCompleteProfile();
396
+ }
397
+ catch (err) {
398
+ this.log("error", `[spawn-delegate] [step:profile-check] ✗ profile incomplete: ${err instanceof Error ? err.message : String(err)}`);
399
+ throw err;
400
+ }
401
+ // Step 2: Look up agent
402
+ this.log("info", `[spawn-delegate] [step:agent-lookup] looking up agent: ${params.agentUrl}`);
403
+ const agent = await this.agentRegistry.get(params.agentUrl);
404
+ if (!agent) {
405
+ this.log("warn", `[spawn-delegate] [step:agent-lookup] ✗ unknown agent: ${params.agentUrl} → aborting`);
406
+ throw new Error(`unknown agent: ${params.agentUrl}`);
407
+ }
408
+ this.log("info", `[spawn-delegate] [step:agent-lookup] ✓ found: ${agent.name} (${agent.url})`);
409
+ // Step 3: Check gateway config
410
+ if (!this.gatewayConfig) {
411
+ this.log("error", `[spawn-delegate] [step:gateway-check] ✗ gateway config not available → aborting`);
412
+ throw new Error("gateway config not available — cannot spawn sub-agent");
413
+ }
414
+ // Step 4: Spawn sub-agent
415
+ const prompt = buildDelegationPrompt(agent, params.task);
416
+ const sessionKey = `delegate-${Date.now()}`;
417
+ this.log("info", `[spawn-delegate] [step:spawn] calling sessions_spawn (cwd=${this.resolvedCwd}, sessionKey=${sessionKey}, promptLen=${prompt.length})`);
418
+ try {
419
+ const spawnResult = await (0, gateway_client_1.invokeGatewayTool)({
420
+ gateway: this.gatewayConfig,
421
+ tool: "sessions_spawn",
422
+ args: { task: prompt, mode: "run", cwd: this.resolvedCwd },
423
+ sessionKey,
424
+ timeoutMs: 15_000,
425
+ });
426
+ this.log("info", `[spawn-delegate] [step:spawn] ✓ sub-agent spawned for ${agent.name} — result=${JSON.stringify(spawnResult).slice(0, 200)}`);
427
+ return { message: `已启动子 agent 向 ${agent.name} 委派任务` };
428
+ }
429
+ catch (err) {
430
+ this.log("error", `[spawn-delegate] [step:spawn] ✗ sessions_spawn failed: ${err instanceof Error ? err.message : String(err)} → aborting`);
431
+ throw err;
432
+ }
433
+ }
434
+ getTaskStatus(taskId) {
435
+ return this.taskTracker.get(taskId);
436
+ }
437
+ /* ---------------------------------------------------------------- */
438
+ /* Profile */
439
+ /* ---------------------------------------------------------------- */
440
+ async getProfile() {
441
+ return await this.profileStore.load();
442
+ }
443
+ /**
444
+ * Throws if the profile is incomplete (ownerName or bio missing).
445
+ * Call this before any action that exposes the user's identity to other agents.
446
+ */
447
+ async requireCompleteProfile() {
448
+ const profile = await this.profileStore.load();
449
+ if (!profile.ownerName?.trim()) {
450
+ throw new Error("档案未完成设置。请先调用 multiclaws_profile_set(ownerName=\"你的名字\") 设置用户名后再继续。");
451
+ }
452
+ }
453
+ async setProfile(patch) {
454
+ this.log("debug", `setProfile(keys=${Object.keys(patch).join(",")})`);
455
+ try {
456
+ const profile = await this.profileStore.update(patch);
457
+ this.updateProfileDescription(profile);
458
+ await this.broadcastProfileToTeams();
459
+ this.log("debug", "setProfile completed");
460
+ return profile;
461
+ }
462
+ catch (err) {
463
+ this.log("error", `setProfile failed: ${err instanceof Error ? err.message : String(err)}`);
464
+ throw err;
465
+ }
466
+ }
467
+ updateProfileDescription(profile) {
468
+ this.profileDescription = (0, agent_profile_1.renderProfileDescription)(profile);
469
+ if (this.agentCard) {
470
+ this.agentCard.description = this.profileDescription;
471
+ if (profile.ownerName?.trim()) {
472
+ this.agentCard.name = (0, agent_profile_1.formatAgentCardName)(profile.ownerName.trim());
473
+ }
474
+ }
475
+ }
476
+ /* ---------------------------------------------------------------- */
477
+ /* Pending profile review (install / first-run) */
478
+ /* ---------------------------------------------------------------- */
479
+ getPendingReviewPath() {
480
+ return node_path_1.default.join(this.options.stateDir, "multiclaws", "pending-profile-review.json");
481
+ }
482
+ async getPendingProfileReview() {
483
+ const p = this.getPendingReviewPath();
484
+ const data = await (0, json_store_1.readJsonWithFallback)(p, {});
485
+ if (data.pending !== true) {
486
+ return { pending: false };
487
+ }
488
+ const profile = await this.profileStore.load();
489
+ return {
490
+ pending: true,
491
+ profile,
492
+ message: "这是您当前的 MultiClaws 档案,是否需要修改名字、角色、数据源或能力?",
493
+ };
494
+ }
495
+ async setPendingProfileReview() {
496
+ this.log("debug", "setPendingProfileReview");
497
+ try {
498
+ const p = this.getPendingReviewPath();
499
+ await (0, json_store_1.writeJsonAtomically)(p, { pending: true });
500
+ }
501
+ catch (err) {
502
+ this.log("error", `setPendingProfileReview failed: ${err instanceof Error ? err.message : String(err)}`);
503
+ throw err;
504
+ }
505
+ }
506
+ async clearPendingProfileReview() {
507
+ this.log("debug", "clearPendingProfileReview");
508
+ const p = this.getPendingReviewPath();
509
+ try {
510
+ await promises_1.default.unlink(p);
511
+ }
512
+ catch {
513
+ // ignore if missing
514
+ }
515
+ }
516
+ /* ---------------------------------------------------------------- */
517
+ /* Team management */
518
+ /* ---------------------------------------------------------------- */
519
+ async createTeam(name) {
520
+ this.log("debug", `createTeam(name=${name})`);
521
+ try {
522
+ await this.requireCompleteProfile();
523
+ const team = await this.teamStore.createTeam({
524
+ teamName: name,
525
+ selfUrl: this.selfUrl,
526
+ selfName: this.getFormattedName(),
527
+ selfDescription: this.profileDescription,
528
+ });
529
+ this.log("info", `team created: ${team.teamId} (${team.teamName})`);
530
+ return team;
531
+ }
532
+ catch (err) {
533
+ this.log("error", `createTeam failed: ${err instanceof Error ? err.message : String(err)}`);
534
+ throw err;
535
+ }
536
+ }
537
+ async createInvite(teamId) {
538
+ this.log("debug", `createInvite(teamId=${teamId ?? "first"})`);
539
+ try {
540
+ const team = teamId
541
+ ? await this.teamStore.getTeam(teamId)
542
+ : await this.teamStore.getFirstTeam();
543
+ if (!team)
544
+ throw new Error(teamId ? `team not found: ${teamId}` : "no team exists");
545
+ const code = (0, team_store_1.encodeInvite)(team.teamId, this.selfUrl);
546
+ this.log("debug", "createInvite completed");
547
+ return code;
548
+ }
549
+ catch (err) {
550
+ this.log("error", `createInvite failed: ${err instanceof Error ? err.message : String(err)}`);
551
+ throw err;
552
+ }
553
+ }
554
+ async joinTeam(inviteCode) {
555
+ this.log("info", "joinTeam starting");
556
+ try {
557
+ await this.requireCompleteProfile();
558
+ const invite = (0, team_store_1.decodeInvite)(inviteCode);
559
+ const seedUrl = invite.u.replace(/\/+$/, "");
560
+ this.log("debug", `joinTeam: seedUrl=${seedUrl}, teamId=${invite.t}`);
561
+ // 1. Fetch member list from seed
562
+ let membersRes;
563
+ try {
564
+ membersRes = await fetch(`${seedUrl}/team/${invite.t}/members`);
565
+ }
566
+ catch (err) {
567
+ throw new Error(`Unable to reach team seed node at ${seedUrl}: ${err instanceof Error ? err.message : String(err)}`);
568
+ }
569
+ if (!membersRes.ok) {
570
+ throw new Error(`failed to fetch team members from ${seedUrl}: HTTP ${membersRes.status}`);
571
+ }
572
+ const { team: remoteTeam } = (await membersRes.json());
573
+ // 2. Announce self to seed (seed broadcasts to others)
574
+ const selfMember = {
575
+ url: this.selfUrl,
576
+ name: this.getFormattedName(),
577
+ description: this.profileDescription,
578
+ joinedAtMs: Date.now(),
579
+ };
580
+ let announceRes;
581
+ try {
582
+ announceRes = await fetch(`${seedUrl}/team/${invite.t}/announce`, {
583
+ method: "POST",
584
+ headers: { "Content-Type": "application/json" },
585
+ body: JSON.stringify(selfMember),
586
+ });
587
+ }
588
+ catch (err) {
589
+ throw new Error(`Failed to announce self to seed ${seedUrl}: ${err instanceof Error ? err.message : String(err)}`);
590
+ }
591
+ if (!announceRes.ok) {
592
+ throw new Error(`failed to announce to seed ${seedUrl}: HTTP ${announceRes.status}`);
593
+ }
594
+ // 3. Store team locally
595
+ const allMembers = [...remoteTeam.members];
596
+ const selfNormalized = this.selfUrl.replace(/\/+$/, "");
597
+ if (!allMembers.some((m) => m.url.replace(/\/+$/, "") === selfNormalized)) {
598
+ allMembers.push(selfMember);
599
+ }
600
+ const team = {
601
+ teamId: invite.t,
602
+ teamName: remoteTeam.teamName,
603
+ selfUrl: this.selfUrl,
604
+ members: allMembers,
605
+ createdAtMs: Date.now(),
606
+ };
607
+ await this.teamStore.saveTeam(team);
608
+ // 4. Fetch Agent Cards for members without descriptions, then sync to registry
609
+ await this.fetchMemberDescriptions(team);
610
+ await this.syncTeamToRegistry(team);
611
+ this.log("info", `joined team ${team.teamId} (${team.teamName}) with ${allMembers.length} members`);
612
+ return team;
613
+ }
614
+ catch (err) {
615
+ this.log("error", `joinTeam failed: ${err instanceof Error ? err.message : String(err)}`);
616
+ throw err;
617
+ }
618
+ }
619
+ async leaveTeam(teamId) {
620
+ this.log("info", `leaveTeam(teamId=${teamId ?? "first"})`);
621
+ try {
622
+ const team = teamId
623
+ ? await this.teamStore.getTeam(teamId)
624
+ : await this.teamStore.getFirstTeam();
625
+ if (!team)
626
+ throw new Error(teamId ? `team not found: ${teamId}` : "no team exists");
627
+ const selfNormalized = this.selfUrl.replace(/\/+$/, "");
628
+ const selfMember = {
629
+ url: this.selfUrl,
630
+ name: this.getFormattedName(),
631
+ joinedAtMs: 0,
632
+ };
633
+ const others = team.members.filter((m) => m.url.replace(/\/+$/, "") !== selfNormalized);
634
+ await Promise.allSettled(others.map(async (m) => {
635
+ try {
636
+ await fetch(`${m.url}/team/${team.teamId}/leave`, {
637
+ method: "POST",
638
+ headers: { "Content-Type": "application/json" },
639
+ body: JSON.stringify(selfMember),
640
+ });
641
+ }
642
+ catch {
643
+ this.log("warn", `failed to notify ${m.url} about leaving`);
644
+ }
645
+ }));
646
+ for (const m of others) {
647
+ await this.agentRegistry.remove(m.url);
648
+ }
649
+ await this.teamStore.deleteTeam(team.teamId);
650
+ this.log("info", `left team ${team.teamId}`);
651
+ }
652
+ catch (err) {
653
+ this.log("error", `leaveTeam failed: ${err instanceof Error ? err.message : String(err)}`);
654
+ throw err;
655
+ }
656
+ }
657
+ async listTeamMembers(teamId) {
658
+ if (teamId) {
659
+ const team = await this.teamStore.getTeam(teamId);
660
+ if (!team)
661
+ return null;
662
+ return { team, members: team.members };
663
+ }
664
+ const all = await this.teamStore.listTeams();
665
+ if (all.length === 0)
666
+ return null;
667
+ if (all.length === 1)
668
+ return { team: all[0], members: all[0].members };
669
+ return { teams: all.map((team) => ({ team, members: team.members })) };
670
+ }
671
+ /* ---------------------------------------------------------------- */
672
+ /* Team REST routes */
673
+ /* ---------------------------------------------------------------- */
674
+ mountTeamRoutes(app) {
675
+ const announceBodySchema = zod_1.z.object({
676
+ url: zod_1.z.string().trim().min(1),
677
+ name: zod_1.z.string().trim().min(1),
678
+ description: zod_1.z.string().trim().optional(),
679
+ joinedAtMs: zod_1.z.number().optional(),
680
+ });
681
+ const leaveBodySchema = zod_1.z.object({
682
+ url: zod_1.z.string().trim().min(1),
683
+ });
684
+ const profileUpdateBodySchema = zod_1.z.object({
685
+ url: zod_1.z.string().trim().min(1),
686
+ name: zod_1.z.string().trim().optional(),
687
+ description: zod_1.z.string().optional(),
688
+ });
689
+ app.get("/team/:id/members", async (req, res) => {
690
+ try {
691
+ const team = await this.teamStore.getTeam(req.params.id);
692
+ if (!team) {
693
+ res.status(404).json({ error: "team not found" });
694
+ return;
695
+ }
696
+ res.json({ team: { teamName: team.teamName, members: team.members } });
697
+ }
698
+ catch (err) {
699
+ this.log("error", `GET /team/${req.params.id}/members failed: ${err instanceof Error ? err.message : String(err)}`);
700
+ res.status(500).json({ error: String(err) });
701
+ }
702
+ });
703
+ app.post("/team/:id/announce", async (req, res) => {
704
+ this.log("debug", `POST /team/${req.params.id}/announce from ${req.body?.url}`);
705
+ try {
706
+ const team = await this.teamStore.getTeam(req.params.id);
707
+ if (!team) {
708
+ this.log("debug", `announce: team ${req.params.id} not found`);
709
+ res.status(404).json({ error: "team not found" });
710
+ return;
711
+ }
712
+ const parsed = announceBodySchema.safeParse(req.body);
713
+ if (!parsed.success) {
714
+ this.log("debug", `announce: invalid body: ${parsed.error.message}`);
715
+ res.status(400).json({ error: parsed.error.message });
716
+ return;
717
+ }
718
+ const member = parsed.data;
719
+ this.log("debug", `announce: member=${member.name}, url=${member.url}`);
720
+ const normalizedUrl = member.url.replace(/\/+$/, "");
721
+ const alreadyKnown = team.members.some((m) => m.url.replace(/\/+$/, "") === normalizedUrl);
722
+ this.log("debug", `announce: alreadyKnown=${alreadyKnown}`);
723
+ await this.teamStore.addMember(team.teamId, {
724
+ url: normalizedUrl,
725
+ name: member.name,
726
+ description: member.description,
727
+ joinedAtMs: member.joinedAtMs ?? Date.now(),
728
+ });
729
+ this.log("debug", `announce: addMember completed`);
730
+ await this.agentRegistry.add({
731
+ url: normalizedUrl,
732
+ name: member.name,
733
+ description: member.description,
734
+ });
735
+ this.log("debug", `announce: agentRegistry.add completed`);
736
+ // Broadcast to other members if new
737
+ if (!alreadyKnown) {
738
+ const selfNormalized = this.selfUrl.replace(/\/+$/, "");
739
+ const others = team.members.filter((m) => m.url.replace(/\/+$/, "") !== normalizedUrl &&
740
+ m.url.replace(/\/+$/, "") !== selfNormalized);
741
+ this.log("debug", `announce: broadcasting to ${others.length} other members`);
742
+ for (const other of others) {
743
+ void this.fetchWithRetry(`${other.url}/team/${team.teamId}/announce`, {
744
+ method: "POST",
745
+ headers: { "Content-Type": "application/json" },
746
+ body: JSON.stringify({
747
+ url: normalizedUrl,
748
+ name: member.name,
749
+ description: member.description,
750
+ joinedAtMs: member.joinedAtMs ?? Date.now(),
751
+ }),
752
+ }).catch((err) => {
753
+ this.log("warn", `broadcast to ${other.url} failed: ${err instanceof Error ? err.message : String(err)}`);
754
+ });
755
+ }
756
+ // Notify local user that a new member joined
757
+ this.log("debug", `announce: notifying local user about ${member.name}`);
758
+ void this.notifyUser(`📢 **${member.name}** 已加入团队「${team.teamName}」`);
759
+ }
760
+ this.log("debug", `announce: completed for ${member.name}`);
761
+ res.json({ ok: true });
762
+ }
763
+ catch (err) {
764
+ this.log("error", `POST /team/${req.params.id}/announce failed: ${err instanceof Error ? err.message : String(err)}`);
765
+ res.status(500).json({ error: String(err) });
766
+ }
767
+ });
768
+ app.post("/team/:id/leave", async (req, res) => {
769
+ this.log("debug", `POST /team/${req.params.id}/leave from ${req.body?.url}`);
770
+ try {
771
+ const team = await this.teamStore.getTeam(req.params.id);
772
+ if (!team) {
773
+ res.status(404).json({ error: "team not found" });
774
+ return;
775
+ }
776
+ const parsed = leaveBodySchema.safeParse(req.body);
777
+ if (!parsed.success) {
778
+ res.status(400).json({ error: parsed.error.message });
779
+ return;
780
+ }
781
+ const normalizedUrl = parsed.data.url.replace(/\/+$/, "");
782
+ await this.teamStore.removeMember(team.teamId, normalizedUrl);
783
+ await this.agentRegistry.remove(normalizedUrl);
784
+ res.json({ ok: true });
785
+ }
786
+ catch (err) {
787
+ this.log("error", `POST /team/${req.params.id}/leave failed: ${err instanceof Error ? err.message : String(err)}`);
788
+ res.status(500).json({ error: String(err) });
789
+ }
790
+ });
791
+ // Profile update broadcast receiver
792
+ app.post("/team/:id/profile-update", async (req, res) => {
793
+ this.log("debug", `POST /team/${req.params.id}/profile-update from ${req.body?.url}`);
794
+ try {
795
+ const team = await this.teamStore.getTeam(req.params.id);
796
+ if (!team) {
797
+ res.status(404).json({ error: "team not found" });
798
+ return;
799
+ }
800
+ const parsed = profileUpdateBodySchema.safeParse(req.body);
801
+ if (!parsed.success) {
802
+ res.status(400).json({ error: parsed.error.message });
803
+ return;
804
+ }
805
+ const { url, name, description } = parsed.data;
806
+ const normalizedUrl = url.replace(/\/+$/, "");
807
+ // Update team member description
808
+ const existing = team.members.find((m) => m.url.replace(/\/+$/, "") === normalizedUrl);
809
+ if (existing) {
810
+ if (name)
811
+ existing.name = name;
812
+ if (description !== undefined)
813
+ existing.description = description;
814
+ await this.teamStore.saveTeam(team);
815
+ }
816
+ // Update agent registry description
817
+ if (description !== undefined) {
818
+ await this.agentRegistry.updateDescription(normalizedUrl, description);
819
+ }
820
+ res.json({ ok: true });
821
+ }
822
+ catch (err) {
823
+ this.log("error", `POST /team/${req.params.id}/profile-update failed: ${err instanceof Error ? err.message : String(err)}`);
824
+ res.status(500).json({ error: String(err) });
825
+ }
826
+ });
827
+ }
828
+ /* ---------------------------------------------------------------- */
829
+ /* Private helpers */
830
+ /* ---------------------------------------------------------------- */
831
+ async broadcastProfileToTeams() {
832
+ this.log("debug", "broadcastProfileToTeams");
833
+ try {
834
+ const teams = await this.teamStore.listTeams();
835
+ const selfNormalized = this.selfUrl.replace(/\/+$/, "");
836
+ const agentName = this.getFormattedName();
837
+ for (const team of teams) {
838
+ // Update self in team store
839
+ await this.teamStore.addMember(team.teamId, {
840
+ url: this.selfUrl,
841
+ name: agentName,
842
+ description: this.profileDescription,
843
+ joinedAtMs: Date.now(),
844
+ });
845
+ // Broadcast to other members
846
+ const others = team.members.filter((m) => m.url.replace(/\/+$/, "") !== selfNormalized);
847
+ for (const member of others) {
848
+ void this.fetchWithRetry(`${member.url}/team/${team.teamId}/profile-update`, {
849
+ method: "POST",
850
+ headers: { "Content-Type": "application/json" },
851
+ body: JSON.stringify({
852
+ url: this.selfUrl,
853
+ name: agentName,
854
+ description: this.profileDescription,
855
+ }),
856
+ }).catch(() => {
857
+ this.log("warn", `profile broadcast to ${member.url} failed`);
858
+ });
859
+ }
860
+ }
861
+ this.log("debug", "broadcastProfileToTeams completed");
862
+ }
863
+ catch (err) {
864
+ this.log("error", `broadcastProfileToTeams failed: ${err instanceof Error ? err.message : String(err)}`);
865
+ throw err;
866
+ }
867
+ }
868
+ async fetchMemberDescriptions(team) {
869
+ const selfNormalized = this.selfUrl.replace(/\/+$/, "");
870
+ const membersToFetch = team.members.filter((m) => m.url.replace(/\/+$/, "") !== selfNormalized && !m.description);
871
+ this.log("debug", `fetchMemberDescriptions(teamId=${team.teamId}, count=${membersToFetch.length})`);
872
+ try {
873
+ await Promise.allSettled(membersToFetch.map(async (m) => {
874
+ try {
875
+ const client = await this.clientFactory.createFromUrl(m.url);
876
+ const card = await client.getAgentCard();
877
+ if (card.description) {
878
+ m.description = card.description;
879
+ }
880
+ }
881
+ catch {
882
+ this.log("warn", `failed to fetch Agent Card from ${m.url}`);
883
+ }
884
+ }));
885
+ await this.teamStore.saveTeam(team);
886
+ this.log("debug", "fetchMemberDescriptions completed");
887
+ }
888
+ catch (err) {
889
+ this.log("error", `fetchMemberDescriptions failed: ${err instanceof Error ? err.message : String(err)}`);
890
+ throw err;
891
+ }
892
+ }
893
+ async syncTeamToRegistry(team) {
894
+ this.log("debug", `syncTeamToRegistry(teamId=${team.teamId})`);
895
+ try {
896
+ const selfNormalized = this.selfUrl.replace(/\/+$/, "");
897
+ for (const member of team.members) {
898
+ if (member.url.replace(/\/+$/, "") === selfNormalized)
899
+ continue;
900
+ await this.agentRegistry.add({
901
+ url: member.url,
902
+ name: member.name,
903
+ description: member.description,
904
+ });
905
+ }
906
+ this.log("debug", "syncTeamToRegistry completed");
907
+ }
908
+ catch (err) {
909
+ this.log("error", `syncTeamToRegistry failed: ${err instanceof Error ? err.message : String(err)}`);
910
+ throw err;
911
+ }
912
+ }
913
+ async createA2AClient(agent) {
914
+ this.log("info", `[a2a-client] creating client for ${agent.name} (${agent.url})`);
915
+ try {
916
+ const client = await this.clientFactory.createFromUrl(agent.url);
917
+ this.log("info", `[a2a-client] ✓ client created for ${agent.name} (${agent.url})`);
918
+ return client;
919
+ }
920
+ catch (err) {
921
+ this.log("error", `[a2a-client] ✗ failed to create client for ${agent.name} (${agent.url}): ${err instanceof Error ? err.message : String(err)}`);
922
+ throw err;
923
+ }
924
+ }
925
+ /**
926
+ * Send a message using A2A streaming to minimize latency.
927
+ * Instead of a single blocking HTTP call, consume the SSE stream and
928
+ * return the final Task or Message as soon as B signals completion.
929
+ */
930
+ processTaskResult(trackId, result) {
931
+ this.log("info", `[process-result] processing result for ${trackId}, resultType=${("status" in result && result.status) ? "Task" : "Message"}`);
932
+ try {
933
+ if ("status" in result && result.status) {
934
+ const task = result;
935
+ const state = task.status?.state ?? "unknown";
936
+ const output = this.extractArtifactText(task);
937
+ this.log("info", `[process-result] ${trackId} Task response — state=${state}, outputLen=${output.length}, preview=${output.slice(0, 120)}`);
938
+ if (state === "completed") {
939
+ this.taskTracker.update(trackId, { status: "completed", result: output });
940
+ this.log("info", `[process-result] ✓ ${trackId} marked completed`);
941
+ }
942
+ else if (state === "failed") {
943
+ this.taskTracker.update(trackId, { status: "failed", error: output || "remote task failed" });
944
+ this.log("warn", `[process-result] ✗ ${trackId} marked failed — error=${output || "remote task failed"}`);
945
+ }
946
+ else {
947
+ // For any other state (unknown, working, etc.), mark as failed to avoid
948
+ // tasks stuck in "running" forever until TTL prune.
949
+ this.taskTracker.update(trackId, { status: "failed", error: `unexpected remote state: ${state}` });
950
+ this.log("warn", `[process-result] ✗ ${trackId} unexpected state=${state}, marked failed`);
951
+ }
952
+ return { taskId: task.id, output, status: state };
953
+ }
954
+ const msg = result;
955
+ const text = msg.parts
956
+ ?.filter((p) => p.kind === "text")
957
+ .map((p) => p.text)
958
+ .join("\n") ?? "";
959
+ this.taskTracker.update(trackId, { status: "completed", result: text });
960
+ this.log("info", `[process-result] ✓ ${trackId} Message response — completed, textLen=${text.length}, preview=${text.slice(0, 120)}`);
961
+ return { taskId: trackId, output: text, status: "completed" };
962
+ }
963
+ catch (err) {
964
+ this.log("error", `[process-result] ✗ ${trackId} processing failed: ${err instanceof Error ? err.message : String(err)}`);
965
+ throw err;
966
+ }
967
+ }
968
+ extractArtifactText(task) {
969
+ if (!task.artifacts?.length)
970
+ return "";
971
+ return task.artifacts
972
+ .flatMap((a) => a.parts ?? [])
973
+ .filter((p) => p.kind === "text")
974
+ .map((p) => p.text)
975
+ .join("\n");
976
+ }
977
+ /** Fetch with up to 2 retries and exponential backoff. */
978
+ async fetchWithRetry(url, init, retries = 2) {
979
+ let lastError = null;
980
+ for (let attempt = 0; attempt <= retries; attempt++) {
981
+ try {
982
+ const res = await fetch(url, init);
983
+ if (res.ok || attempt === retries)
984
+ return res;
985
+ lastError = new Error(`HTTP ${res.status}`);
986
+ }
987
+ catch (err) {
988
+ lastError = err instanceof Error ? err : new Error(String(err));
989
+ if (attempt === retries)
990
+ break;
991
+ }
992
+ await new Promise((r) => setTimeout(r, 200 * 2 ** attempt));
993
+ }
994
+ throw lastError;
995
+ }
996
+ /**
997
+ * Called by the `multiclaws_task_respond` tool when the local human
998
+ * approves or rejects a pending risky incoming task.
999
+ */
1000
+ respondToTask(taskId, approved) {
1001
+ this.log("info", `respondToTask(taskId=${taskId}, approved=${approved})`);
1002
+ if (!this.agentExecutor) {
1003
+ this.log("warn", `respondToTask: no agentExecutor available for taskId=${taskId}`);
1004
+ return false;
1005
+ }
1006
+ const resolved = this.agentExecutor.resolveApproval(taskId, approved);
1007
+ this.log("info", `respondToTask: ${resolved ? "✓" : "✗"} taskId=${taskId} ${resolved ? "resolved" : "no pending approval found"}`);
1008
+ return resolved;
1009
+ }
1010
+ /** Resolve a pending A2A callback from sub-agent. */
1011
+ resolveA2ACallback(taskId, result) {
1012
+ this.log("info", `[a2a-callback] resolveA2ACallback(taskId=${taskId}, resultLen=${result.length})`);
1013
+ if (!this.agentExecutor) {
1014
+ this.log("warn", `[a2a-callback] ✗ no agentExecutor available for taskId=${taskId}`);
1015
+ return false;
1016
+ }
1017
+ const resolved = this.agentExecutor.resolveCallback(taskId, result);
1018
+ this.log("info", `[a2a-callback] ${resolved ? "✓" : "✗"} taskId=${taskId} ${resolved ? "resolved" : "no pending callback found"}`);
1019
+ return resolved;
1020
+ }
1021
+ addNotificationTarget(key, target) {
1022
+ if (!this.notificationTargets.has(key)) {
1023
+ this.notificationTargets.set(key, target);
1024
+ this.log("debug", `notification target registered: ${key} (total: ${this.notificationTargets.size})`);
1025
+ }
1026
+ }
1027
+ /** Consistent name for this agent: AgentCard.name or fallback. */
1028
+ getFormattedName() {
1029
+ return this.agentCard?.name ?? "OpenClaw Agent";
1030
+ }
1031
+ /** Discover the most recently active non-internal session via sessions_list. */
1032
+ async discoverActiveSession() {
1033
+ if (!this.gatewayConfig) {
1034
+ this.log("warn", `discoverActiveSession: skipped — no gateway config`);
1035
+ return null;
1036
+ }
1037
+ try {
1038
+ this.log("info", `discoverActiveSession: calling sessions_list (limit=10, activeMinutes=120)`);
1039
+ const raw = await (0, gateway_client_1.invokeGatewayTool)({
1040
+ gateway: this.gatewayConfig,
1041
+ tool: "sessions_list",
1042
+ args: { limit: 10, activeMinutes: 120 },
1043
+ timeoutMs: 5_000,
1044
+ });
1045
+ this.log("info", `discoverActiveSession: raw result = ${JSON.stringify(raw).slice(0, 500)}`);
1046
+ // Unwrap gateway tool standard response: { content: [{ type: "text", text: "..." }] }
1047
+ let parsed = raw;
1048
+ if (raw?.content?.[0]?.type === "text") {
1049
+ try {
1050
+ parsed = JSON.parse(raw.content[0].text);
1051
+ this.log("info", `discoverActiveSession: unwrapped gateway response successfully`);
1052
+ }
1053
+ catch (parseErr) {
1054
+ this.log("warn", `discoverActiveSession: failed to parse content[0].text as JSON — ${parseErr instanceof Error ? parseErr.message : String(parseErr)}, using raw object`);
1055
+ }
1056
+ }
1057
+ const sessions = parsed?.sessions ?? [];
1058
+ this.log("info", `discoverActiveSession: found ${sessions.length} sessions from gateway`);
1059
+ const INTERNAL_PREFIXES = ["delegate-", "a2a-"];
1060
+ // sessions_list returns "key" not "sessionKey"
1061
+ const session = sessions.find((s) => {
1062
+ const k = (s.key ?? s.sessionKey);
1063
+ return k && !INTERNAL_PREFIXES.some((p) => k.startsWith(p));
1064
+ });
1065
+ const matchedKey = (session?.key ?? session?.sessionKey);
1066
+ if (matchedKey) {
1067
+ this.log("info", `discoverActiveSession: ✓ matched session ${matchedKey}`);
1068
+ }
1069
+ else {
1070
+ this.log("warn", `discoverActiveSession: ✗ all ${sessions.length} sessions filtered or empty`);
1071
+ sessions.forEach((s, i) => this.log("info", `discoverActiveSession: session[${i}]: key=${(s.key ?? s.sessionKey) ?? "(no key)"}`));
1072
+ }
1073
+ return matchedKey ?? null;
1074
+ }
1075
+ catch (err) {
1076
+ this.log("error", `discoverActiveSession: ✗ caught error — ${err instanceof Error ? err.message : String(err)}, returning null`);
1077
+ return null;
1078
+ }
1079
+ }
1080
+ async notifyUser(message) {
1081
+ this.log("info", `notifyUser: targets=${this.notificationTargets.size}, msgLen=${message.length}, preview="${message.slice(0, 80)}"`);
1082
+ if (!this.gatewayConfig) {
1083
+ this.log("warn", "notifyUser: skipped — no gatewayConfig, message lost");
1084
+ return;
1085
+ }
1086
+ // Fallback: no registered targets yet (e.g. right after gateway restart)
1087
+ if (this.notificationTargets.size === 0) {
1088
+ this.log("info", "notifyUser: no registered targets → falling back to discoverActiveSession()");
1089
+ const sessionKey = await this.discoverActiveSession();
1090
+ if (sessionKey) {
1091
+ this.log("info", `notifyUser: fallback discovered session ${sessionKey} → calling sessions_send`);
1092
+ try {
1093
+ await (0, gateway_client_1.invokeGatewayTool)({
1094
+ gateway: this.gatewayConfig,
1095
+ tool: "sessions_send",
1096
+ args: { sessionKey, message },
1097
+ timeoutMs: 5_000,
1098
+ });
1099
+ this.log("info", `notifyUser: ✓ fallback sessions_send to ${sessionKey} succeeded`);
1100
+ this.addNotificationTarget(`web:${sessionKey}`, { type: "web", sessionKey });
1101
+ this.log("info", `notifyUser: registered ${sessionKey} as notification target for future use`);
1102
+ }
1103
+ catch (err) {
1104
+ this.log("error", `notifyUser: ✗ fallback sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
1105
+ }
1106
+ }
1107
+ else {
1108
+ this.log("warn", "notifyUser: ✗ discoverActiveSession returned null — no active session found, message lost");
1109
+ }
1110
+ return;
1111
+ }
1112
+ const entries = [...this.notificationTargets.entries()];
1113
+ this.log("info", `notifyUser: sending to ${entries.length} registered target(s): [${entries.map(([k]) => k).join(", ")}]`);
1114
+ const results = await Promise.allSettled(entries.map(async ([key, target]) => {
1115
+ this.log("info", `notifyUser: → ${key} (type=${target.type})`);
1116
+ try {
1117
+ await (target.type === "channel"
1118
+ ? (0, gateway_client_1.invokeGatewayTool)({
1119
+ gateway: this.gatewayConfig,
1120
+ tool: "message",
1121
+ args: { action: "send", target: target.conversationId, message },
1122
+ timeoutMs: 5_000,
1123
+ })
1124
+ : (0, gateway_client_1.invokeGatewayTool)({
1125
+ gateway: this.gatewayConfig,
1126
+ tool: "sessions_send",
1127
+ args: { sessionKey: target.sessionKey, message },
1128
+ timeoutMs: 5_000,
1129
+ }));
1130
+ this.log("info", `notifyUser: ✓ ${key} (${target.type}) succeeded`);
1131
+ }
1132
+ catch (err) {
1133
+ this.log("error", `notifyUser: ✗ ${key} (${target.type}) failed: ${err instanceof Error ? err.message : String(err)}`);
1134
+ throw err;
1135
+ }
1136
+ }));
1137
+ const okCount = results.filter((r) => r.status === "fulfilled").length;
1138
+ const failCount = results.filter((r) => r.status === "rejected").length;
1139
+ if (failCount === 0) {
1140
+ this.log("info", `notifyUser: ✓ all ${okCount} targets succeeded`);
1141
+ }
1142
+ else if (failCount === entries.length) {
1143
+ this.log("error", `notifyUser: ✗ ALL ${failCount} targets failed`);
1144
+ }
1145
+ else {
1146
+ this.log("warn", `notifyUser: ${okCount} ok, ${failCount} FAILED out of ${entries.length} targets`);
1147
+ }
1148
+ }
1149
+ log(level, message) {
1150
+ this.options.logger?.[level]?.(`[multiclaws] ${message}`);
1151
+ }
1152
+ }
1153
+ exports.MulticlawsService = MulticlawsService;