multiclaws 0.4.19 → 0.4.21

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.
@@ -21,8 +21,33 @@ const agent_profile_1 = require("./agent-profile");
21
21
  const team_store_1 = require("../team/team-store");
22
22
  const tracker_1 = require("../task/tracker");
23
23
  const zod_1 = require("zod");
24
+ const gateway_client_1 = require("../infra/gateway-client");
24
25
  const rate_limiter_1 = require("../infra/rate-limiter");
25
26
  /* ------------------------------------------------------------------ */
27
+ /* Delegation prompt builder */
28
+ /* ------------------------------------------------------------------ */
29
+ function buildDelegationPrompt(agent, task) {
30
+ const bioSnippet = agent.description
31
+ ? `\n**智能体能力**: ${agent.description.slice(0, 500)}`
32
+ : "";
33
+ return `## 委派任务
34
+ 向远端智能体发送任务并汇报结果。
35
+
36
+ **目标智能体**: ${agent.name} (${agent.url})${bioSnippet}
37
+ **任务内容**: ${task}
38
+
39
+ ## 执行步骤
40
+ 1. 调用 multiclaws_delegate_send(agentUrl="${agent.url}", task="${task.replace(/"/g, '\\"')}") 发送任务
41
+ 2. 收到回复后,用 message 工具将结果汇报给用户
42
+ 3. 如果需要进一步沟通,可再次调用 multiclaws_delegate_send(最多 5 轮)
43
+ 4. 每次收到回复后立即用 message 汇报进展
44
+
45
+ ## 规则
46
+ - 使用 multiclaws_delegate_send(不是 multiclaws_delegate)发送任务
47
+ - 最多 5 轮沟通
48
+ - 遇到错误时在汇报中说明原因`;
49
+ }
50
+ /* ------------------------------------------------------------------ */
26
51
  /* Service */
27
52
  /* ------------------------------------------------------------------ */
28
53
  class MulticlawsService extends node_events_1.EventEmitter {
@@ -41,121 +66,139 @@ class MulticlawsService extends node_events_1.EventEmitter {
41
66
  frpTunnel = null;
42
67
  selfUrl;
43
68
  profileDescription = "OpenClaw agent";
69
+ gatewayConfig;
44
70
  constructor(options) {
45
71
  super();
46
72
  this.options = options;
47
73
  const multiclawsStateDir = node_path_1.default.join(options.stateDir, "multiclaws");
48
- this.agentRegistry = new agent_registry_1.AgentRegistry(node_path_1.default.join(multiclawsStateDir, "agents.json"));
49
- this.teamStore = new team_store_1.TeamStore(node_path_1.default.join(multiclawsStateDir, "teams.json"));
50
- this.profileStore = new agent_profile_1.ProfileStore(node_path_1.default.join(multiclawsStateDir, "profile.json"));
74
+ this.agentRegistry = new agent_registry_1.AgentRegistry(node_path_1.default.join(multiclawsStateDir, "agents.json"), options.logger);
75
+ this.teamStore = new team_store_1.TeamStore(node_path_1.default.join(multiclawsStateDir, "teams.json"), options.logger);
76
+ this.profileStore = new agent_profile_1.ProfileStore(node_path_1.default.join(multiclawsStateDir, "profile.json"), options.logger);
51
77
  this.taskTracker = new tracker_1.TaskTracker({
52
78
  filePath: node_path_1.default.join(multiclawsStateDir, "tasks.json"),
79
+ logger: options.logger,
53
80
  });
54
81
  // selfUrl resolved later in start() after FRP tunnel setup
55
82
  this.selfUrl = options.selfUrl ?? "";
83
+ this.gatewayConfig = options.gatewayConfig ?? null;
56
84
  }
57
85
  async start() {
58
86
  if (this.started)
59
87
  return;
60
- // Resolve selfUrl: explicit config > FRP tunnel
61
- if (!this.options.selfUrl) {
62
- const port = this.options.port ?? 3100;
63
- if (!this.options.tunnel || this.options.tunnel.type !== "frp") {
64
- throw new Error("multiclaws requires either 'selfUrl' or 'tunnel' configuration. " +
65
- "Please configure tunnel in plugin settings.");
88
+ this.log("debug", `start(port=${this.options.port ?? 3100}, selfUrl=${this.options.selfUrl ?? "auto"})`);
89
+ try {
90
+ // Resolve selfUrl: explicit config > FRP tunnel
91
+ if (!this.options.selfUrl) {
92
+ const port = this.options.port ?? 3100;
93
+ if (!this.options.tunnel || this.options.tunnel.type !== "frp") {
94
+ throw new Error("multiclaws requires either 'selfUrl' or 'tunnel' configuration. " +
95
+ "Please configure tunnel in plugin settings.");
96
+ }
97
+ this.frpTunnel = new frp_1.FrpTunnelManager({
98
+ config: this.options.tunnel,
99
+ localPort: port,
100
+ stateDir: node_path_1.default.join(this.options.stateDir, "multiclaws"),
101
+ logger: this.options.logger,
102
+ });
103
+ const publicUrl = await this.frpTunnel.start();
104
+ this.selfUrl = publicUrl;
105
+ this.log("info", `FRP tunnel ready: ${publicUrl}`);
66
106
  }
67
- this.frpTunnel = new frp_1.FrpTunnelManager({
68
- config: this.options.tunnel,
69
- localPort: port,
70
- stateDir: node_path_1.default.join(this.options.stateDir, "multiclaws"),
71
- logger: this.options.logger,
107
+ // Load profile for AgentCard description
108
+ let profile = await this.profileStore.load();
109
+ const isIncompleteProfile = !profile.ownerName?.trim() || !profile.bio?.trim();
110
+ if (!profile.ownerName?.trim()) {
111
+ profile.ownerName = this.options.displayName ?? node_os_1.default.hostname();
112
+ await this.profileStore.save(profile);
113
+ }
114
+ if (isIncompleteProfile) {
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
+ logger,
72
123
  });
73
- const publicUrl = await this.frpTunnel.start();
74
- this.selfUrl = publicUrl;
75
- this.log("info", `FRP tunnel ready: ${publicUrl}`);
76
- }
77
- // Load profile for AgentCard description
78
- let profile = await this.profileStore.load();
79
- const isIncompleteProfile = !profile.ownerName?.trim() || !profile.bio?.trim();
80
- if (!profile.ownerName?.trim()) {
81
- profile.ownerName = this.options.displayName ?? node_os_1.default.hostname();
82
- await this.profileStore.save(profile);
124
+ this.agentCard = {
125
+ name: this.options.displayName ?? (profile.ownerName || "OpenClaw Agent"),
126
+ description: this.profileDescription,
127
+ url: this.selfUrl,
128
+ version: "0.3.0",
129
+ protocolVersion: "0.2.2",
130
+ defaultInputModes: ["text/plain"],
131
+ defaultOutputModes: ["text/plain"],
132
+ capabilities: { streaming: false, pushNotifications: false },
133
+ skills: [
134
+ {
135
+ id: "general",
136
+ name: "General Task",
137
+ description: "Execute any delegated task via OpenClaw",
138
+ tags: ["task", "delegation", "general"],
139
+ },
140
+ ],
141
+ };
142
+ const taskStore = new server_1.InMemoryTaskStore();
143
+ this.a2aRequestHandler = new server_1.DefaultRequestHandler(this.agentCard, taskStore, this.agentExecutor);
144
+ const app = (0, express_1.default)();
145
+ app.use(express_1.default.json({ limit: "1mb" }));
146
+ // Rate limiting
147
+ app.use((req, res, next) => {
148
+ const clientIp = req.ip ?? req.socket.remoteAddress ?? "unknown";
149
+ if (!this.httpRateLimiter.allow(clientIp)) {
150
+ res.status(429).json({ error: "rate limited" });
151
+ return;
152
+ }
153
+ next();
154
+ });
155
+ // Team + profile REST endpoints
156
+ this.mountTeamRoutes(app);
157
+ // A2A endpoints
158
+ app.use("/.well-known/agent-card.json", (0, express_2.agentCardHandler)({
159
+ agentCardProvider: this.a2aRequestHandler,
160
+ }));
161
+ app.use("/", (0, express_2.jsonRpcHandler)({
162
+ requestHandler: this.a2aRequestHandler,
163
+ userBuilder: express_2.UserBuilder.noAuthentication,
164
+ }));
165
+ const listenPort = this.options.port ?? 3100;
166
+ this.httpServer = node_http_1.default.createServer(app);
167
+ await new Promise((resolve) => this.httpServer.listen(listenPort, "0.0.0.0", resolve));
168
+ this.started = true;
169
+ this.log("info", `multiclaws A2A service listening on :${listenPort}`);
83
170
  }
84
- if (isIncompleteProfile) {
85
- await this.setPendingProfileReview();
171
+ catch (err) {
172
+ this.log("error", `start failed: ${err instanceof Error ? err.message : String(err)}`);
173
+ throw err;
86
174
  }
87
- this.profileDescription = (0, agent_profile_1.renderProfileDescription)(profile);
88
- const logger = this.options.logger ?? { info: () => { }, warn: () => { }, error: () => { } };
89
- this.agentExecutor = new a2a_adapter_1.OpenClawAgentExecutor({
90
- gatewayConfig: this.options.gatewayConfig ?? null,
91
- taskTracker: this.taskTracker,
92
- logger,
93
- });
94
- this.agentCard = {
95
- name: this.options.displayName ?? (profile.ownerName || "OpenClaw Agent"),
96
- description: this.profileDescription,
97
- url: this.selfUrl,
98
- version: "0.3.0",
99
- protocolVersion: "0.2.2",
100
- defaultInputModes: ["text/plain"],
101
- defaultOutputModes: ["text/plain"],
102
- capabilities: { streaming: false, pushNotifications: false },
103
- skills: [
104
- {
105
- id: "general",
106
- name: "General Task",
107
- description: "Execute any delegated task via OpenClaw",
108
- tags: ["task", "delegation", "general"],
109
- },
110
- ],
111
- };
112
- const taskStore = new server_1.InMemoryTaskStore();
113
- this.a2aRequestHandler = new server_1.DefaultRequestHandler(this.agentCard, taskStore, this.agentExecutor);
114
- const app = (0, express_1.default)();
115
- app.use(express_1.default.json({ limit: "1mb" }));
116
- // Rate limiting
117
- app.use((req, res, next) => {
118
- const clientIp = req.ip ?? req.socket.remoteAddress ?? "unknown";
119
- if (!this.httpRateLimiter.allow(clientIp)) {
120
- res.status(429).json({ error: "rate limited" });
121
- return;
122
- }
123
- next();
124
- });
125
- // Team + profile REST endpoints
126
- this.mountTeamRoutes(app);
127
- // A2A endpoints
128
- app.use("/.well-known/agent-card.json", (0, express_2.agentCardHandler)({
129
- agentCardProvider: this.a2aRequestHandler,
130
- }));
131
- app.use("/", (0, express_2.jsonRpcHandler)({
132
- requestHandler: this.a2aRequestHandler,
133
- userBuilder: express_2.UserBuilder.noAuthentication,
134
- }));
135
- const listenPort = this.options.port ?? 3100;
136
- this.httpServer = node_http_1.default.createServer(app);
137
- await new Promise((resolve) => this.httpServer.listen(listenPort, "0.0.0.0", resolve));
138
- this.started = true;
139
- this.log("info", `multiclaws A2A service listening on :${listenPort}`);
140
175
  }
141
176
  async stop() {
142
177
  if (!this.started)
143
178
  return;
144
- this.started = false;
145
- this.taskTracker.destroy();
146
- this.httpRateLimiter.destroy();
147
- if (this.frpTunnel) {
148
- await this.frpTunnel.stop();
149
- this.frpTunnel = null;
150
- }
151
- await new Promise((resolve) => {
152
- if (!this.httpServer) {
153
- resolve();
154
- return;
179
+ this.log("debug", "stopping");
180
+ try {
181
+ this.started = false;
182
+ this.taskTracker.destroy();
183
+ this.httpRateLimiter.destroy();
184
+ if (this.frpTunnel) {
185
+ await this.frpTunnel.stop();
186
+ this.frpTunnel = null;
155
187
  }
156
- this.httpServer.close(() => resolve());
157
- });
158
- this.httpServer = null;
188
+ await new Promise((resolve) => {
189
+ if (!this.httpServer) {
190
+ resolve();
191
+ return;
192
+ }
193
+ this.httpServer.close(() => resolve());
194
+ });
195
+ this.httpServer = null;
196
+ this.log("debug", "stopped");
197
+ }
198
+ catch (err) {
199
+ this.log("error", `stop failed: ${err instanceof Error ? err.message : String(err)}`);
200
+ throw err;
201
+ }
159
202
  }
160
203
  updateGatewayConfig(config) {
161
204
  this.agentExecutor?.updateGatewayConfig(config);
@@ -168,18 +211,22 @@ class MulticlawsService extends node_events_1.EventEmitter {
168
211
  }
169
212
  async addAgent(params) {
170
213
  const normalizedUrl = params.url.replace(/\/+$/, "");
214
+ this.log("debug", `addAgent(url=${normalizedUrl})`);
171
215
  try {
172
216
  const client = await this.clientFactory.createFromUrl(normalizedUrl);
173
217
  const card = await client.getAgentCard();
174
- return await this.agentRegistry.add({
218
+ const result = await this.agentRegistry.add({
175
219
  url: normalizedUrl,
176
220
  name: card.name ?? normalizedUrl,
177
221
  description: card.description ?? "",
178
222
  skills: card.skills?.map((s) => s.name ?? s.id) ?? [],
179
223
  apiKey: params.apiKey,
180
224
  });
225
+ this.log("debug", `addAgent completed, name=${result.name}`);
226
+ return result;
181
227
  }
182
228
  catch {
229
+ this.log("debug", `addAgent: card fetch failed for ${normalizedUrl}, adding with URL as name`);
183
230
  return await this.agentRegistry.add({
184
231
  url: normalizedUrl,
185
232
  name: normalizedUrl,
@@ -188,15 +235,78 @@ class MulticlawsService extends node_events_1.EventEmitter {
188
235
  }
189
236
  }
190
237
  async removeAgent(url) {
191
- return await this.agentRegistry.remove(url);
238
+ this.log("debug", `removeAgent(url=${url})`);
239
+ try {
240
+ const result = await this.agentRegistry.remove(url);
241
+ this.log("debug", `removeAgent completed, result=${result}`);
242
+ return result;
243
+ }
244
+ catch (err) {
245
+ this.log("error", `removeAgent failed: ${err instanceof Error ? err.message : String(err)}`);
246
+ throw err;
247
+ }
192
248
  }
193
249
  /* ---------------------------------------------------------------- */
194
250
  /* Task delegation */
195
251
  /* ---------------------------------------------------------------- */
196
252
  async delegateTask(params) {
253
+ this.log("info", `delegateTask(agentUrl=${params.agentUrl}, task=${params.task.slice(0, 80)})`);
197
254
  await this.requireCompleteProfile();
198
255
  const agentRecord = await this.agentRegistry.get(params.agentUrl);
199
256
  if (!agentRecord) {
257
+ this.log("warn", `delegateTask: unknown agent ${params.agentUrl}`);
258
+ return { status: "failed", error: `unknown agent: ${params.agentUrl}` };
259
+ }
260
+ const track = this.taskTracker.create({
261
+ fromPeerId: "local",
262
+ toPeerId: params.agentUrl,
263
+ task: params.task,
264
+ });
265
+ this.taskTracker.update(track.taskId, { status: "running" });
266
+ try {
267
+ const client = await this.createA2AClient(agentRecord);
268
+ // Fire-and-forget execution: keep running in the background so that
269
+ // the gateway call can return quickly and the task can outlive
270
+ // the gateway's HTTP timeout.
271
+ void (async () => {
272
+ try {
273
+ const result = await client.sendMessage({
274
+ message: {
275
+ kind: "message",
276
+ role: "user",
277
+ parts: [{ kind: "text", text: params.task }],
278
+ messageId: track.taskId,
279
+ },
280
+ });
281
+ this.processTaskResult(track.taskId, result);
282
+ }
283
+ catch (err) {
284
+ const errorMsg = err instanceof Error ? err.message : String(err);
285
+ this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
286
+ this.log("warn", `delegateTask background execution for ${track.taskId} failed: ${errorMsg}`);
287
+ }
288
+ })();
289
+ // Return immediately so that gateway tool invocations are fast and
290
+ // do not depend on the remote agent's total execution time.
291
+ return { taskId: track.taskId, status: "running" };
292
+ }
293
+ catch (err) {
294
+ const errorMsg = err instanceof Error ? err.message : String(err);
295
+ this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
296
+ this.log("error", `delegateTask failed for ${track.taskId}: ${errorMsg}`);
297
+ return { taskId: track.taskId, status: "failed", error: errorMsg };
298
+ }
299
+ }
300
+ /**
301
+ * Synchronous delegation: sends A2A task and waits for the result.
302
+ * Used by sub-agents internally via the multiclaws_delegate_send tool.
303
+ */
304
+ async delegateTaskSync(params) {
305
+ this.log("info", `delegateTaskSync(agentUrl=${params.agentUrl}, task=${params.task.slice(0, 80)})`);
306
+ await this.requireCompleteProfile();
307
+ const agentRecord = await this.agentRegistry.get(params.agentUrl);
308
+ if (!agentRecord) {
309
+ this.log("warn", `delegateTaskSync: unknown agent ${params.agentUrl}`);
200
310
  return { status: "failed", error: `unknown agent: ${params.agentUrl}` };
201
311
  }
202
312
  const track = this.taskTracker.create({
@@ -215,14 +325,45 @@ class MulticlawsService extends node_events_1.EventEmitter {
215
325
  messageId: track.taskId,
216
326
  },
217
327
  });
218
- return this.processTaskResult(track.taskId, result);
328
+ const taskResult = this.processTaskResult(track.taskId, result);
329
+ this.log("debug", `delegateTaskSync completed for ${track.taskId}`);
330
+ return taskResult;
219
331
  }
220
332
  catch (err) {
221
333
  const errorMsg = err instanceof Error ? err.message : String(err);
222
334
  this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
335
+ this.log("error", `delegateTaskSync failed for ${track.taskId}: ${errorMsg}`);
223
336
  return { taskId: track.taskId, status: "failed", error: errorMsg };
224
337
  }
225
338
  }
339
+ /**
340
+ * Spawn a sub-agent to handle delegation asynchronously.
341
+ * The sub-agent uses multiclaws_delegate_send internally and
342
+ * reports results back to the user via the message tool.
343
+ */
344
+ async spawnDelegation(params) {
345
+ this.log("info", `spawnDelegation(agentUrl=${params.agentUrl}, task=${params.task.slice(0, 80)})`);
346
+ await this.requireCompleteProfile();
347
+ const agent = await this.agentRegistry.get(params.agentUrl);
348
+ if (!agent) {
349
+ this.log("warn", `spawnDelegation: unknown agent ${params.agentUrl}`);
350
+ throw new Error(`unknown agent: ${params.agentUrl}`);
351
+ }
352
+ if (!this.gatewayConfig) {
353
+ this.log("error", "spawnDelegation: gateway config not available");
354
+ throw new Error("gateway config not available — cannot spawn sub-agent");
355
+ }
356
+ const prompt = buildDelegationPrompt(agent, params.task);
357
+ await (0, gateway_client_1.invokeGatewayTool)({
358
+ gateway: this.gatewayConfig,
359
+ tool: "sessions_spawn",
360
+ args: { task: prompt, mode: "run" },
361
+ sessionKey: `delegate-${Date.now()}`,
362
+ timeoutMs: 15_000,
363
+ });
364
+ this.log("info", `spawnDelegation completed: sub-agent spawned for ${agent.name}`);
365
+ return { message: `已启动子 agent 向 ${agent.name} 委派任务` };
366
+ }
226
367
  getTaskStatus(taskId) {
227
368
  return this.taskTracker.get(taskId);
228
369
  }
@@ -243,10 +384,18 @@ class MulticlawsService extends node_events_1.EventEmitter {
243
384
  }
244
385
  }
245
386
  async setProfile(patch) {
246
- const profile = await this.profileStore.update(patch);
247
- this.updateProfileDescription(profile);
248
- await this.broadcastProfileToTeams();
249
- return profile;
387
+ this.log("debug", `setProfile(keys=${Object.keys(patch).join(",")})`);
388
+ try {
389
+ const profile = await this.profileStore.update(patch);
390
+ this.updateProfileDescription(profile);
391
+ await this.broadcastProfileToTeams();
392
+ this.log("debug", "setProfile completed");
393
+ return profile;
394
+ }
395
+ catch (err) {
396
+ this.log("error", `setProfile failed: ${err instanceof Error ? err.message : String(err)}`);
397
+ throw err;
398
+ }
250
399
  }
251
400
  updateProfileDescription(profile) {
252
401
  this.profileDescription = (0, agent_profile_1.renderProfileDescription)(profile);
@@ -277,10 +426,18 @@ class MulticlawsService extends node_events_1.EventEmitter {
277
426
  };
278
427
  }
279
428
  async setPendingProfileReview() {
280
- const p = this.getPendingReviewPath();
281
- await (0, json_store_1.writeJsonAtomically)(p, { pending: true });
429
+ this.log("debug", "setPendingProfileReview");
430
+ try {
431
+ const p = this.getPendingReviewPath();
432
+ await (0, json_store_1.writeJsonAtomically)(p, { pending: true });
433
+ }
434
+ catch (err) {
435
+ this.log("error", `setPendingProfileReview failed: ${err instanceof Error ? err.message : String(err)}`);
436
+ throw err;
437
+ }
282
438
  }
283
439
  async clearPendingProfileReview() {
440
+ this.log("debug", "clearPendingProfileReview");
284
441
  const p = this.getPendingReviewPath();
285
442
  try {
286
443
  await promises_1.default.unlink(p);
@@ -293,111 +450,142 @@ class MulticlawsService extends node_events_1.EventEmitter {
293
450
  /* Team management */
294
451
  /* ---------------------------------------------------------------- */
295
452
  async createTeam(name) {
296
- await this.requireCompleteProfile();
297
- const team = await this.teamStore.createTeam({
298
- teamName: name,
299
- selfUrl: this.selfUrl,
300
- selfName: this.options.displayName ?? node_os_1.default.hostname(),
301
- selfDescription: this.profileDescription,
302
- });
303
- this.log("info", `team created: ${team.teamId} (${team.teamName})`);
304
- return team;
305
- }
306
- async createInvite(teamId) {
307
- const team = teamId
308
- ? await this.teamStore.getTeam(teamId)
309
- : await this.teamStore.getFirstTeam();
310
- if (!team)
311
- throw new Error(teamId ? `team not found: ${teamId}` : "no team exists");
312
- return (0, team_store_1.encodeInvite)(team.teamId, this.selfUrl);
313
- }
314
- async joinTeam(inviteCode) {
315
- await this.requireCompleteProfile();
316
- const invite = (0, team_store_1.decodeInvite)(inviteCode);
317
- const seedUrl = invite.u.replace(/\/+$/, "");
318
- // 1. Fetch member list from seed
319
- let membersRes;
453
+ this.log("debug", `createTeam(name=${name})`);
320
454
  try {
321
- membersRes = await fetch(`${seedUrl}/team/${invite.t}/members`);
455
+ await this.requireCompleteProfile();
456
+ const team = await this.teamStore.createTeam({
457
+ teamName: name,
458
+ selfUrl: this.selfUrl,
459
+ selfName: this.options.displayName ?? node_os_1.default.hostname(),
460
+ selfDescription: this.profileDescription,
461
+ });
462
+ this.log("info", `team created: ${team.teamId} (${team.teamName})`);
463
+ return team;
322
464
  }
323
465
  catch (err) {
324
- throw new Error(`Unable to reach team seed node at ${seedUrl}: ${err instanceof Error ? err.message : String(err)}`);
325
- }
326
- if (!membersRes.ok) {
327
- throw new Error(`failed to fetch team members from ${seedUrl}: HTTP ${membersRes.status}`);
328
- }
329
- const { team: remoteTeam } = (await membersRes.json());
330
- // 2. Announce self to seed (seed broadcasts to others)
331
- const selfMember = {
332
- url: this.selfUrl,
333
- name: this.options.displayName ?? node_os_1.default.hostname(),
334
- description: this.profileDescription,
335
- joinedAtMs: Date.now(),
336
- };
337
- let announceRes;
466
+ this.log("error", `createTeam failed: ${err instanceof Error ? err.message : String(err)}`);
467
+ throw err;
468
+ }
469
+ }
470
+ async createInvite(teamId) {
471
+ this.log("debug", `createInvite(teamId=${teamId ?? "first"})`);
338
472
  try {
339
- announceRes = await fetch(`${seedUrl}/team/${invite.t}/announce`, {
340
- method: "POST",
341
- headers: { "Content-Type": "application/json" },
342
- body: JSON.stringify(selfMember),
343
- });
473
+ const team = teamId
474
+ ? await this.teamStore.getTeam(teamId)
475
+ : await this.teamStore.getFirstTeam();
476
+ if (!team)
477
+ throw new Error(teamId ? `team not found: ${teamId}` : "no team exists");
478
+ const code = (0, team_store_1.encodeInvite)(team.teamId, this.selfUrl);
479
+ this.log("debug", "createInvite completed");
480
+ return code;
344
481
  }
345
482
  catch (err) {
346
- throw new Error(`Failed to announce self to seed ${seedUrl}: ${err instanceof Error ? err.message : String(err)}`);
347
- }
348
- if (!announceRes.ok) {
349
- throw new Error(`failed to announce to seed ${seedUrl}: HTTP ${announceRes.status}`);
483
+ this.log("error", `createInvite failed: ${err instanceof Error ? err.message : String(err)}`);
484
+ throw err;
350
485
  }
351
- // 3. Store team locally
352
- const allMembers = [...remoteTeam.members];
353
- const selfNormalized = this.selfUrl.replace(/\/+$/, "");
354
- if (!allMembers.some((m) => m.url.replace(/\/+$/, "") === selfNormalized)) {
355
- allMembers.push(selfMember);
356
- }
357
- const team = {
358
- teamId: invite.t,
359
- teamName: remoteTeam.teamName,
360
- selfUrl: this.selfUrl,
361
- members: allMembers,
362
- createdAtMs: Date.now(),
363
- };
364
- await this.teamStore.saveTeam(team);
365
- // 4. Fetch Agent Cards for members without descriptions, then sync to registry
366
- await this.fetchMemberDescriptions(team);
367
- await this.syncTeamToRegistry(team);
368
- this.log("info", `joined team ${team.teamId} (${team.teamName}) with ${allMembers.length} members`);
369
- return team;
370
486
  }
371
- async leaveTeam(teamId) {
372
- const team = teamId
373
- ? await this.teamStore.getTeam(teamId)
374
- : await this.teamStore.getFirstTeam();
375
- if (!team)
376
- throw new Error(teamId ? `team not found: ${teamId}` : "no team exists");
377
- const selfNormalized = this.selfUrl.replace(/\/+$/, "");
378
- const selfMember = {
379
- url: this.selfUrl,
380
- name: this.options.displayName ?? node_os_1.default.hostname(),
381
- joinedAtMs: 0,
382
- };
383
- const others = team.members.filter((m) => m.url.replace(/\/+$/, "") !== selfNormalized);
384
- await Promise.allSettled(others.map(async (m) => {
487
+ async joinTeam(inviteCode) {
488
+ this.log("info", "joinTeam starting");
489
+ try {
490
+ await this.requireCompleteProfile();
491
+ const invite = (0, team_store_1.decodeInvite)(inviteCode);
492
+ const seedUrl = invite.u.replace(/\/+$/, "");
493
+ this.log("debug", `joinTeam: seedUrl=${seedUrl}, teamId=${invite.t}`);
494
+ // 1. Fetch member list from seed
495
+ let membersRes;
496
+ try {
497
+ membersRes = await fetch(`${seedUrl}/team/${invite.t}/members`);
498
+ }
499
+ catch (err) {
500
+ throw new Error(`Unable to reach team seed node at ${seedUrl}: ${err instanceof Error ? err.message : String(err)}`);
501
+ }
502
+ if (!membersRes.ok) {
503
+ throw new Error(`failed to fetch team members from ${seedUrl}: HTTP ${membersRes.status}`);
504
+ }
505
+ const { team: remoteTeam } = (await membersRes.json());
506
+ // 2. Announce self to seed (seed broadcasts to others)
507
+ const selfMember = {
508
+ url: this.selfUrl,
509
+ name: this.options.displayName ?? node_os_1.default.hostname(),
510
+ description: this.profileDescription,
511
+ joinedAtMs: Date.now(),
512
+ };
513
+ let announceRes;
385
514
  try {
386
- await fetch(`${m.url}/team/${team.teamId}/leave`, {
515
+ announceRes = await fetch(`${seedUrl}/team/${invite.t}/announce`, {
387
516
  method: "POST",
388
517
  headers: { "Content-Type": "application/json" },
389
518
  body: JSON.stringify(selfMember),
390
519
  });
391
520
  }
392
- catch {
393
- this.log("warn", `failed to notify ${m.url} about leaving`);
521
+ catch (err) {
522
+ throw new Error(`Failed to announce self to seed ${seedUrl}: ${err instanceof Error ? err.message : String(err)}`);
523
+ }
524
+ if (!announceRes.ok) {
525
+ throw new Error(`failed to announce to seed ${seedUrl}: HTTP ${announceRes.status}`);
526
+ }
527
+ // 3. Store team locally
528
+ const allMembers = [...remoteTeam.members];
529
+ const selfNormalized = this.selfUrl.replace(/\/+$/, "");
530
+ if (!allMembers.some((m) => m.url.replace(/\/+$/, "") === selfNormalized)) {
531
+ allMembers.push(selfMember);
532
+ }
533
+ const team = {
534
+ teamId: invite.t,
535
+ teamName: remoteTeam.teamName,
536
+ selfUrl: this.selfUrl,
537
+ members: allMembers,
538
+ createdAtMs: Date.now(),
539
+ };
540
+ await this.teamStore.saveTeam(team);
541
+ // 4. Fetch Agent Cards for members without descriptions, then sync to registry
542
+ await this.fetchMemberDescriptions(team);
543
+ await this.syncTeamToRegistry(team);
544
+ this.log("info", `joined team ${team.teamId} (${team.teamName}) with ${allMembers.length} members`);
545
+ return team;
546
+ }
547
+ catch (err) {
548
+ this.log("error", `joinTeam failed: ${err instanceof Error ? err.message : String(err)}`);
549
+ throw err;
550
+ }
551
+ }
552
+ async leaveTeam(teamId) {
553
+ this.log("info", `leaveTeam(teamId=${teamId ?? "first"})`);
554
+ try {
555
+ const team = teamId
556
+ ? await this.teamStore.getTeam(teamId)
557
+ : await this.teamStore.getFirstTeam();
558
+ if (!team)
559
+ throw new Error(teamId ? `team not found: ${teamId}` : "no team exists");
560
+ const selfNormalized = this.selfUrl.replace(/\/+$/, "");
561
+ const selfMember = {
562
+ url: this.selfUrl,
563
+ name: this.options.displayName ?? node_os_1.default.hostname(),
564
+ joinedAtMs: 0,
565
+ };
566
+ const others = team.members.filter((m) => m.url.replace(/\/+$/, "") !== selfNormalized);
567
+ await Promise.allSettled(others.map(async (m) => {
568
+ try {
569
+ await fetch(`${m.url}/team/${team.teamId}/leave`, {
570
+ method: "POST",
571
+ headers: { "Content-Type": "application/json" },
572
+ body: JSON.stringify(selfMember),
573
+ });
574
+ }
575
+ catch {
576
+ this.log("warn", `failed to notify ${m.url} about leaving`);
577
+ }
578
+ }));
579
+ for (const m of others) {
580
+ await this.agentRegistry.remove(m.url);
394
581
  }
395
- }));
396
- for (const m of others) {
397
- await this.agentRegistry.remove(m.url);
582
+ await this.teamStore.deleteTeam(team.teamId);
583
+ this.log("info", `left team ${team.teamId}`);
584
+ }
585
+ catch (err) {
586
+ this.log("error", `leaveTeam failed: ${err instanceof Error ? err.message : String(err)}`);
587
+ throw err;
398
588
  }
399
- await this.teamStore.deleteTeam(team.teamId);
400
- this.log("info", `left team ${team.teamId}`);
401
589
  }
402
590
  async listTeamMembers(teamId) {
403
591
  if (teamId) {
@@ -441,10 +629,12 @@ class MulticlawsService extends node_events_1.EventEmitter {
441
629
  res.json({ team: { teamName: team.teamName, members: team.members } });
442
630
  }
443
631
  catch (err) {
632
+ this.log("error", `GET /team/${req.params.id}/members failed: ${err instanceof Error ? err.message : String(err)}`);
444
633
  res.status(500).json({ error: String(err) });
445
634
  }
446
635
  });
447
636
  app.post("/team/:id/announce", async (req, res) => {
637
+ this.log("debug", `POST /team/${req.params.id}/announce from ${req.body?.url}`);
448
638
  try {
449
639
  const team = await this.teamStore.getTeam(req.params.id);
450
640
  if (!team) {
@@ -493,10 +683,12 @@ class MulticlawsService extends node_events_1.EventEmitter {
493
683
  res.json({ ok: true });
494
684
  }
495
685
  catch (err) {
686
+ this.log("error", `POST /team/${req.params.id}/announce failed: ${err instanceof Error ? err.message : String(err)}`);
496
687
  res.status(500).json({ error: String(err) });
497
688
  }
498
689
  });
499
690
  app.post("/team/:id/leave", async (req, res) => {
691
+ this.log("debug", `POST /team/${req.params.id}/leave from ${req.body?.url}`);
500
692
  try {
501
693
  const team = await this.teamStore.getTeam(req.params.id);
502
694
  if (!team) {
@@ -514,11 +706,13 @@ class MulticlawsService extends node_events_1.EventEmitter {
514
706
  res.json({ ok: true });
515
707
  }
516
708
  catch (err) {
709
+ this.log("error", `POST /team/${req.params.id}/leave failed: ${err instanceof Error ? err.message : String(err)}`);
517
710
  res.status(500).json({ error: String(err) });
518
711
  }
519
712
  });
520
713
  // Profile update broadcast receiver
521
714
  app.post("/team/:id/profile-update", async (req, res) => {
715
+ this.log("debug", `POST /team/${req.params.id}/profile-update from ${req.body?.url}`);
522
716
  try {
523
717
  const team = await this.teamStore.getTeam(req.params.id);
524
718
  if (!team) {
@@ -548,6 +742,7 @@ class MulticlawsService extends node_events_1.EventEmitter {
548
742
  res.json({ ok: true });
549
743
  }
550
744
  catch (err) {
745
+ this.log("error", `POST /team/${req.params.id}/profile-update failed: ${err instanceof Error ? err.message : String(err)}`);
551
746
  res.status(500).json({ error: String(err) });
552
747
  }
553
748
  });
@@ -556,62 +751,85 @@ class MulticlawsService extends node_events_1.EventEmitter {
556
751
  /* Private helpers */
557
752
  /* ---------------------------------------------------------------- */
558
753
  async broadcastProfileToTeams() {
559
- const teams = await this.teamStore.listTeams();
560
- const selfNormalized = this.selfUrl.replace(/\/+$/, "");
561
- const displayName = this.options.displayName ?? node_os_1.default.hostname();
562
- for (const team of teams) {
563
- // Update self in team store
564
- await this.teamStore.addMember(team.teamId, {
565
- url: this.selfUrl,
566
- name: displayName,
567
- description: this.profileDescription,
568
- joinedAtMs: Date.now(),
569
- });
570
- // Broadcast to other members
571
- const others = team.members.filter((m) => m.url.replace(/\/+$/, "") !== selfNormalized);
572
- for (const member of others) {
573
- void this.fetchWithRetry(`${member.url}/team/${team.teamId}/profile-update`, {
574
- method: "POST",
575
- headers: { "Content-Type": "application/json" },
576
- body: JSON.stringify({
577
- url: this.selfUrl,
578
- name: displayName,
579
- description: this.profileDescription,
580
- }),
581
- }).catch(() => {
582
- this.log("warn", `profile broadcast to ${member.url} failed`);
754
+ this.log("debug", "broadcastProfileToTeams");
755
+ try {
756
+ const teams = await this.teamStore.listTeams();
757
+ const selfNormalized = this.selfUrl.replace(/\/+$/, "");
758
+ const displayName = this.options.displayName ?? node_os_1.default.hostname();
759
+ for (const team of teams) {
760
+ // Update self in team store
761
+ await this.teamStore.addMember(team.teamId, {
762
+ url: this.selfUrl,
763
+ name: displayName,
764
+ description: this.profileDescription,
765
+ joinedAtMs: Date.now(),
583
766
  });
767
+ // Broadcast to other members
768
+ const others = team.members.filter((m) => m.url.replace(/\/+$/, "") !== selfNormalized);
769
+ for (const member of others) {
770
+ void this.fetchWithRetry(`${member.url}/team/${team.teamId}/profile-update`, {
771
+ method: "POST",
772
+ headers: { "Content-Type": "application/json" },
773
+ body: JSON.stringify({
774
+ url: this.selfUrl,
775
+ name: displayName,
776
+ description: this.profileDescription,
777
+ }),
778
+ }).catch(() => {
779
+ this.log("warn", `profile broadcast to ${member.url} failed`);
780
+ });
781
+ }
584
782
  }
783
+ this.log("debug", "broadcastProfileToTeams completed");
784
+ }
785
+ catch (err) {
786
+ this.log("error", `broadcastProfileToTeams failed: ${err instanceof Error ? err.message : String(err)}`);
787
+ throw err;
585
788
  }
586
789
  }
587
790
  async fetchMemberDescriptions(team) {
588
791
  const selfNormalized = this.selfUrl.replace(/\/+$/, "");
589
- await Promise.allSettled(team.members
590
- .filter((m) => m.url.replace(/\/+$/, "") !== selfNormalized && !m.description)
591
- .map(async (m) => {
592
- try {
593
- const client = await this.clientFactory.createFromUrl(m.url);
594
- const card = await client.getAgentCard();
595
- if (card.description) {
596
- m.description = card.description;
792
+ const membersToFetch = team.members.filter((m) => m.url.replace(/\/+$/, "") !== selfNormalized && !m.description);
793
+ this.log("debug", `fetchMemberDescriptions(teamId=${team.teamId}, count=${membersToFetch.length})`);
794
+ try {
795
+ await Promise.allSettled(membersToFetch.map(async (m) => {
796
+ try {
797
+ const client = await this.clientFactory.createFromUrl(m.url);
798
+ const card = await client.getAgentCard();
799
+ if (card.description) {
800
+ m.description = card.description;
801
+ }
597
802
  }
598
- }
599
- catch {
600
- this.log("warn", `failed to fetch Agent Card from ${m.url}`);
601
- }
602
- }));
603
- await this.teamStore.saveTeam(team);
803
+ catch {
804
+ this.log("warn", `failed to fetch Agent Card from ${m.url}`);
805
+ }
806
+ }));
807
+ await this.teamStore.saveTeam(team);
808
+ this.log("debug", "fetchMemberDescriptions completed");
809
+ }
810
+ catch (err) {
811
+ this.log("error", `fetchMemberDescriptions failed: ${err instanceof Error ? err.message : String(err)}`);
812
+ throw err;
813
+ }
604
814
  }
605
815
  async syncTeamToRegistry(team) {
606
- const selfNormalized = this.selfUrl.replace(/\/+$/, "");
607
- for (const member of team.members) {
608
- if (member.url.replace(/\/+$/, "") === selfNormalized)
609
- continue;
610
- await this.agentRegistry.add({
611
- url: member.url,
612
- name: member.name,
613
- description: member.description,
614
- });
816
+ this.log("debug", `syncTeamToRegistry(teamId=${team.teamId})`);
817
+ try {
818
+ const selfNormalized = this.selfUrl.replace(/\/+$/, "");
819
+ for (const member of team.members) {
820
+ if (member.url.replace(/\/+$/, "") === selfNormalized)
821
+ continue;
822
+ await this.agentRegistry.add({
823
+ url: member.url,
824
+ name: member.name,
825
+ description: member.description,
826
+ });
827
+ }
828
+ this.log("debug", "syncTeamToRegistry completed");
829
+ }
830
+ catch (err) {
831
+ this.log("error", `syncTeamToRegistry failed: ${err instanceof Error ? err.message : String(err)}`);
832
+ throw err;
615
833
  }
616
834
  }
617
835
  async createA2AClient(agent) {
@@ -623,25 +841,39 @@ class MulticlawsService extends node_events_1.EventEmitter {
623
841
  * return the final Task or Message as soon as B signals completion.
624
842
  */
625
843
  processTaskResult(trackId, result) {
626
- if ("status" in result && result.status) {
627
- const task = result;
628
- const state = task.status?.state ?? "unknown";
629
- const output = this.extractArtifactText(task);
630
- if (state === "completed") {
631
- this.taskTracker.update(trackId, { status: "completed", result: output });
632
- }
633
- else if (state === "failed") {
634
- this.taskTracker.update(trackId, { status: "failed", error: output || "remote task failed" });
844
+ this.log("debug", `processTaskResult(trackId=${trackId})`);
845
+ try {
846
+ if ("status" in result && result.status) {
847
+ const task = result;
848
+ const state = task.status?.state ?? "unknown";
849
+ const output = this.extractArtifactText(task);
850
+ if (state === "completed") {
851
+ this.taskTracker.update(trackId, { status: "completed", result: output });
852
+ }
853
+ else if (state === "failed") {
854
+ this.taskTracker.update(trackId, { status: "failed", error: output || "remote task failed" });
855
+ }
856
+ else {
857
+ // For any other state (unknown, working, etc.), mark as failed to avoid
858
+ // tasks stuck in "running" forever until TTL prune.
859
+ this.taskTracker.update(trackId, { status: "failed", error: `unexpected remote state: ${state}` });
860
+ }
861
+ this.log("debug", `processTaskResult completed, status=${state}`);
862
+ return { taskId: task.id, output, status: state };
635
863
  }
636
- return { taskId: task.id, output, status: state };
864
+ const msg = result;
865
+ const text = msg.parts
866
+ ?.filter((p) => p.kind === "text")
867
+ .map((p) => p.text)
868
+ .join("\n") ?? "";
869
+ this.taskTracker.update(trackId, { status: "completed", result: text });
870
+ this.log("debug", "processTaskResult completed, status=completed (message)");
871
+ return { taskId: trackId, output: text, status: "completed" };
872
+ }
873
+ catch (err) {
874
+ this.log("error", `processTaskResult failed for ${trackId}: ${err instanceof Error ? err.message : String(err)}`);
875
+ throw err;
637
876
  }
638
- const msg = result;
639
- const text = msg.parts
640
- ?.filter((p) => p.kind === "text")
641
- .map((p) => p.text)
642
- .join("\n") ?? "";
643
- this.taskTracker.update(trackId, { status: "completed", result: text });
644
- return { taskId: trackId, output: text, status: "completed" };
645
877
  }
646
878
  extractArtifactText(task) {
647
879
  if (!task.artifacts?.length)