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.
- package/dist/gateway/handlers.d.ts +2 -1
- package/dist/gateway/handlers.js +63 -13
- package/dist/index.js +227 -83
- package/dist/infra/logger.d.ts +1 -0
- package/dist/infra/logger.js +7 -0
- package/dist/service/a2a-adapter.js +10 -2
- package/dist/service/agent-profile.d.ts +4 -1
- package/dist/service/agent-profile.js +30 -9
- package/dist/service/agent-registry.d.ts +4 -1
- package/dist/service/agent-registry.js +76 -45
- package/dist/service/multiclaws-service.d.ts +20 -0
- package/dist/service/multiclaws-service.js +487 -255
- package/dist/service/session-store.d.ts +3 -0
- package/dist/service/session-store.js +4 -1
- package/dist/task/tracker.d.ts +3 -0
- package/dist/task/tracker.js +6 -1
- package/dist/team/team-store.d.ts +4 -1
- package/dist/team/team-store.js +116 -67
- package/package.json +8 -8
- package/skills/multiclaws/SKILL.md +16 -80
|
@@ -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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (!this.options.
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
this.frpTunnel
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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(`${
|
|
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
|
-
|
|
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
|
-
|
|
397
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
this.
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
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)
|