multiclaws 0.4.0 → 0.4.2
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.js +15 -0
- package/dist/index.js +40 -1
- package/dist/service/a2a-adapter.d.ts +1 -0
- package/dist/service/a2a-adapter.js +53 -9
- package/dist/service/agent-registry.d.ts +9 -0
- package/dist/service/agent-registry.js +41 -0
- package/dist/service/multiclaws-service.d.ts +20 -1
- package/dist/service/multiclaws-service.js +196 -47
- package/dist/service/session-store.js +7 -3
- package/package.json +1 -1
- package/skills/multiclaws/SKILL.md +11 -4
package/dist/gateway/handlers.js
CHANGED
|
@@ -18,6 +18,10 @@ const sessionReplySchema = zod_1.z.object({
|
|
|
18
18
|
});
|
|
19
19
|
const sessionStatusSchema = zod_1.z.object({ sessionId: zod_1.z.string().trim().min(1).optional() });
|
|
20
20
|
const sessionEndSchema = zod_1.z.object({ sessionId: nonEmptyString });
|
|
21
|
+
const sessionWaitAllSchema = zod_1.z.object({
|
|
22
|
+
sessionIds: zod_1.z.array(nonEmptyString).min(1),
|
|
23
|
+
timeoutMs: zod_1.z.number().positive().optional(),
|
|
24
|
+
});
|
|
21
25
|
const profileSetSchema = zod_1.z.object({
|
|
22
26
|
ownerName: zod_1.z.string().trim().optional(),
|
|
23
27
|
bio: zod_1.z.string().optional(),
|
|
@@ -106,6 +110,17 @@ function createGatewayHandlers(getService) {
|
|
|
106
110
|
safeHandle(respond, "session_status_failed", error);
|
|
107
111
|
}
|
|
108
112
|
},
|
|
113
|
+
"multiclaws.session.wait_all": async ({ params, respond }) => {
|
|
114
|
+
try {
|
|
115
|
+
const parsed = sessionWaitAllSchema.parse(params);
|
|
116
|
+
const service = getService();
|
|
117
|
+
const result = await service.waitForSessions(parsed);
|
|
118
|
+
respond(true, result);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
safeHandle(respond, "session_wait_all_failed", error);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
109
124
|
"multiclaws.session.end": async ({ params, respond }) => {
|
|
110
125
|
try {
|
|
111
126
|
const parsed = sessionEndSchema.parse(params);
|
package/dist/index.js
CHANGED
|
@@ -64,7 +64,10 @@ function createTools(getService) {
|
|
|
64
64
|
throw new Error("url is required");
|
|
65
65
|
const apiKey = typeof args.apiKey === "string" ? args.apiKey.trim() : undefined;
|
|
66
66
|
const agent = await service.addAgent({ url, apiKey });
|
|
67
|
-
|
|
67
|
+
const status = agent.reachable
|
|
68
|
+
? `Agent added: ${agent.name} (${agent.url})`
|
|
69
|
+
: `⚠️ Agent added but NOT reachable: ${agent.url} — agent card could not be fetched. Verify the URL and ensure the agent is running.`;
|
|
70
|
+
return textResult(status, agent);
|
|
68
71
|
},
|
|
69
72
|
};
|
|
70
73
|
const multiclawsRemoveAgent = {
|
|
@@ -155,6 +158,30 @@ function createTools(getService) {
|
|
|
155
158
|
return textResult(JSON.stringify({ sessions }, null, 2), { sessions });
|
|
156
159
|
},
|
|
157
160
|
};
|
|
161
|
+
const multiclawsSessionWaitAll = {
|
|
162
|
+
name: "multiclaws_session_wait_all",
|
|
163
|
+
description: "Wait for multiple sessions to complete, then return all results at once. Use this when you have started multiple sessions concurrently and need all results before synthesizing an answer. Returns early if any session needs input (input-required). Default timeout: 5 minutes.",
|
|
164
|
+
parameters: {
|
|
165
|
+
type: "object",
|
|
166
|
+
additionalProperties: false,
|
|
167
|
+
properties: {
|
|
168
|
+
sessionIds: { type: "array", items: { type: "string" } },
|
|
169
|
+
timeoutMs: { type: "number" },
|
|
170
|
+
},
|
|
171
|
+
required: ["sessionIds"],
|
|
172
|
+
},
|
|
173
|
+
execute: async (_toolCallId, args) => {
|
|
174
|
+
const service = requireService(getService());
|
|
175
|
+
const sessionIds = Array.isArray(args.sessionIds)
|
|
176
|
+
? args.sessionIds.map((s) => String(s).trim()).filter(Boolean)
|
|
177
|
+
: [];
|
|
178
|
+
if (!sessionIds.length)
|
|
179
|
+
throw new Error("sessionIds must be a non-empty array");
|
|
180
|
+
const timeoutMs = typeof args.timeoutMs === "number" ? args.timeoutMs : undefined;
|
|
181
|
+
const result = await service.waitForSessions({ sessionIds, timeoutMs });
|
|
182
|
+
return textResult(JSON.stringify(result, null, 2), result);
|
|
183
|
+
},
|
|
184
|
+
};
|
|
158
185
|
const multiclawsSessionEnd = {
|
|
159
186
|
name: "multiclaws_session_end",
|
|
160
187
|
description: "Cancel and close a collaboration session.",
|
|
@@ -327,6 +354,7 @@ function createTools(getService) {
|
|
|
327
354
|
multiclawsSessionStart,
|
|
328
355
|
multiclawsSessionReply,
|
|
329
356
|
multiclawsSessionStatus,
|
|
357
|
+
multiclawsSessionWaitAll,
|
|
330
358
|
multiclawsSessionEnd,
|
|
331
359
|
multiclawsTeamCreate,
|
|
332
360
|
multiclawsTeamJoin,
|
|
@@ -412,6 +440,17 @@ const plugin = {
|
|
|
412
440
|
});
|
|
413
441
|
api.on("gateway_start", () => {
|
|
414
442
|
structured.logger.info("[multiclaws] gateway_start observed");
|
|
443
|
+
// Re-read gateway config in case token became available after initial registration
|
|
444
|
+
if (service && !gatewayConfig) {
|
|
445
|
+
const gw = api.config?.gateway;
|
|
446
|
+
const port = typeof gw?.port === "number" ? gw.port : 18789;
|
|
447
|
+
const token = typeof gw?.auth?.token === "string" ? gw.auth.token : null;
|
|
448
|
+
if (token) {
|
|
449
|
+
const newConfig = { port, token };
|
|
450
|
+
service.updateGatewayConfig(newConfig);
|
|
451
|
+
structured.logger.info("[multiclaws] gateway config updated from gateway_start event");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
415
454
|
});
|
|
416
455
|
api.on("gateway_stop", () => {
|
|
417
456
|
structured.logger.info("[multiclaws] gateway_stop observed");
|
|
@@ -24,6 +24,7 @@ export declare class OpenClawAgentExecutor implements AgentExecutor {
|
|
|
24
24
|
private gatewayConfig;
|
|
25
25
|
private readonly taskTracker;
|
|
26
26
|
private readonly logger;
|
|
27
|
+
private readonly a2aToTracker;
|
|
27
28
|
constructor(options: A2AAdapterOptions);
|
|
28
29
|
execute(context: RequestContext, eventBus: ExecutionEventBus): Promise<void>;
|
|
29
30
|
/**
|
|
@@ -10,6 +10,34 @@ function extractTextFromMessage(message) {
|
|
|
10
10
|
.map((p) => p.text)
|
|
11
11
|
.join("\n");
|
|
12
12
|
}
|
|
13
|
+
function buildTaskWithHistory(context) {
|
|
14
|
+
const currentText = extractTextFromMessage(context.userMessage);
|
|
15
|
+
const history = context.task?.history ?? [];
|
|
16
|
+
if (history.length <= 1) {
|
|
17
|
+
// First message — no prior context
|
|
18
|
+
return currentText;
|
|
19
|
+
}
|
|
20
|
+
// Build context from previous exchanges (exclude the last message, that's currentText)
|
|
21
|
+
const prior = history
|
|
22
|
+
.slice(0, -1)
|
|
23
|
+
.slice(-8) // keep last 8 messages max to avoid huge prompts
|
|
24
|
+
.map((m) => {
|
|
25
|
+
const text = extractTextFromMessage(m);
|
|
26
|
+
const role = m.role === "agent" ? "[agent]" : "[user]";
|
|
27
|
+
return `[${role}]: ${text}`;
|
|
28
|
+
})
|
|
29
|
+
.filter((line) => line.length > 10)
|
|
30
|
+
.join("\n");
|
|
31
|
+
if (!prior)
|
|
32
|
+
return currentText;
|
|
33
|
+
return [
|
|
34
|
+
"[conversation history]",
|
|
35
|
+
prior,
|
|
36
|
+
"",
|
|
37
|
+
"[latest message]",
|
|
38
|
+
currentText,
|
|
39
|
+
].join("\n");
|
|
40
|
+
}
|
|
13
41
|
function sleep(ms) {
|
|
14
42
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
43
|
}
|
|
@@ -42,13 +70,15 @@ class OpenClawAgentExecutor {
|
|
|
42
70
|
gatewayConfig;
|
|
43
71
|
taskTracker;
|
|
44
72
|
logger;
|
|
73
|
+
// Map A2A task IDs → internal tracker IDs so cancelTask can find the right record
|
|
74
|
+
a2aToTracker = new Map();
|
|
45
75
|
constructor(options) {
|
|
46
76
|
this.gatewayConfig = options.gatewayConfig;
|
|
47
77
|
this.taskTracker = options.taskTracker;
|
|
48
78
|
this.logger = options.logger;
|
|
49
79
|
}
|
|
50
80
|
async execute(context, eventBus) {
|
|
51
|
-
const taskText =
|
|
81
|
+
const taskText = buildTaskWithHistory(context);
|
|
52
82
|
const taskId = context.taskId;
|
|
53
83
|
if (!taskText.trim()) {
|
|
54
84
|
this.publishMessage(eventBus, "Error: empty task received.");
|
|
@@ -56,14 +86,16 @@ class OpenClawAgentExecutor {
|
|
|
56
86
|
return;
|
|
57
87
|
}
|
|
58
88
|
const fromAgent = context.userMessage.metadata?.agentUrl ?? "unknown";
|
|
59
|
-
this.taskTracker.create({
|
|
89
|
+
const tracked = this.taskTracker.create({
|
|
60
90
|
fromPeerId: fromAgent,
|
|
61
91
|
toPeerId: "local",
|
|
62
92
|
task: taskText,
|
|
63
93
|
});
|
|
94
|
+
const trackedId = tracked.taskId;
|
|
95
|
+
this.a2aToTracker.set(taskId, trackedId);
|
|
64
96
|
if (!this.gatewayConfig) {
|
|
65
97
|
this.logger.error("[a2a-adapter] gateway config not available, cannot execute task");
|
|
66
|
-
this.taskTracker.update(
|
|
98
|
+
this.taskTracker.update(trackedId, { status: "failed", error: "gateway config not available" });
|
|
67
99
|
this.publishMessage(eventBus, "Error: gateway config not available, cannot execute task.");
|
|
68
100
|
eventBus.finished();
|
|
69
101
|
return;
|
|
@@ -90,16 +122,19 @@ class OpenClawAgentExecutor {
|
|
|
90
122
|
this.logger.info(`[a2a-adapter] task ${taskId} spawned as ${childSessionKey}, waiting for result...`);
|
|
91
123
|
const output = await this.waitForCompletion(childSessionKey, 180_000);
|
|
92
124
|
// 3. Return result
|
|
93
|
-
this.taskTracker.update(
|
|
125
|
+
this.taskTracker.update(trackedId, { status: "completed", result: output });
|
|
94
126
|
this.logger.info(`[a2a-adapter] task ${taskId} completed`);
|
|
95
127
|
this.publishMessage(eventBus, output || "Task completed with no output.");
|
|
96
128
|
}
|
|
97
129
|
catch (err) {
|
|
98
130
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
99
131
|
this.logger.error(`[a2a-adapter] task execution failed: ${errorMsg}`);
|
|
100
|
-
this.taskTracker.update(
|
|
132
|
+
this.taskTracker.update(trackedId, { status: "failed", error: errorMsg });
|
|
101
133
|
this.publishMessage(eventBus, `Error: ${errorMsg}`);
|
|
102
134
|
}
|
|
135
|
+
finally {
|
|
136
|
+
this.a2aToTracker.delete(taskId);
|
|
137
|
+
}
|
|
103
138
|
eventBus.finished();
|
|
104
139
|
}
|
|
105
140
|
/**
|
|
@@ -155,14 +190,21 @@ class OpenClawAgentExecutor {
|
|
|
155
190
|
const messages = (details.messages ?? []);
|
|
156
191
|
if (messages.length === 0)
|
|
157
192
|
return null;
|
|
158
|
-
// If no explicit flag,
|
|
193
|
+
// If no explicit flag, use conservative heuristic: only consider
|
|
194
|
+
// complete if the last message is an assistant message with text
|
|
195
|
+
// and NO tool calls (tool calls indicate ongoing work)
|
|
159
196
|
if (details.isComplete === undefined) {
|
|
160
197
|
const lastMsg = messages[messages.length - 1];
|
|
161
|
-
if (lastMsg
|
|
198
|
+
if (!lastMsg || lastMsg.role !== "assistant")
|
|
199
|
+
return null;
|
|
200
|
+
if (Array.isArray(lastMsg.content)) {
|
|
162
201
|
const content = lastMsg.content;
|
|
163
202
|
const hasToolCalls = content.some((c) => c?.type === "toolCall" || c?.type === "tool_use");
|
|
203
|
+
// If there are ANY tool calls, assume still running
|
|
204
|
+
if (hasToolCalls)
|
|
205
|
+
return null;
|
|
164
206
|
const hasText = content.some((c) => c?.type === "text" && typeof c.text === "string" && c.text.trim());
|
|
165
|
-
if (
|
|
207
|
+
if (!hasText)
|
|
166
208
|
return null;
|
|
167
209
|
}
|
|
168
210
|
}
|
|
@@ -188,7 +230,9 @@ class OpenClawAgentExecutor {
|
|
|
188
230
|
return null;
|
|
189
231
|
}
|
|
190
232
|
async cancelTask(taskId, eventBus) {
|
|
191
|
-
this.
|
|
233
|
+
const trackedId = this.a2aToTracker.get(taskId) ?? taskId;
|
|
234
|
+
this.taskTracker.update(trackedId, { status: "failed", error: "canceled" });
|
|
235
|
+
this.a2aToTracker.delete(taskId);
|
|
192
236
|
this.publishMessage(eventBus, "Task was canceled.");
|
|
193
237
|
eventBus.finished();
|
|
194
238
|
}
|
|
@@ -6,6 +6,8 @@ export type AgentRecord = {
|
|
|
6
6
|
apiKey?: string;
|
|
7
7
|
addedAtMs: number;
|
|
8
8
|
lastSeenAtMs: number;
|
|
9
|
+
/** Which teams synced this agent. Empty or undefined = manually added. */
|
|
10
|
+
teamIds?: string[];
|
|
9
11
|
};
|
|
10
12
|
export declare class AgentRegistry {
|
|
11
13
|
private readonly filePath;
|
|
@@ -21,6 +23,13 @@ export declare class AgentRegistry {
|
|
|
21
23
|
remove(url: string): Promise<boolean>;
|
|
22
24
|
list(): Promise<AgentRecord[]>;
|
|
23
25
|
get(url: string): Promise<AgentRecord | null>;
|
|
26
|
+
addTeamSource(url: string, teamId: string): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Remove a team source from an agent. Returns true if the agent
|
|
29
|
+
* was fully removed (no remaining sources), false otherwise.
|
|
30
|
+
* Manually-added agents (no teamIds) are never removed by this method.
|
|
31
|
+
*/
|
|
32
|
+
removeTeamSource(url: string, teamId: string): Promise<boolean>;
|
|
24
33
|
updateDescription(url: string, description: string): Promise<void>;
|
|
25
34
|
updateLastSeen(url: string): Promise<void>;
|
|
26
35
|
}
|
|
@@ -73,6 +73,47 @@ class AgentRegistry {
|
|
|
73
73
|
const normalizedUrl = url.replace(/\/+$/, "");
|
|
74
74
|
return store.agents.find((a) => a.url === normalizedUrl) ?? null;
|
|
75
75
|
}
|
|
76
|
+
async addTeamSource(url, teamId) {
|
|
77
|
+
await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
|
|
78
|
+
const store = await this.readStore();
|
|
79
|
+
const normalizedUrl = url.replace(/\/+$/, "");
|
|
80
|
+
const agent = store.agents.find((a) => a.url === normalizedUrl);
|
|
81
|
+
if (agent) {
|
|
82
|
+
const teams = new Set(agent.teamIds ?? []);
|
|
83
|
+
teams.add(teamId);
|
|
84
|
+
agent.teamIds = [...teams];
|
|
85
|
+
await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Remove a team source from an agent. Returns true if the agent
|
|
91
|
+
* was fully removed (no remaining sources), false otherwise.
|
|
92
|
+
* Manually-added agents (no teamIds) are never removed by this method.
|
|
93
|
+
*/
|
|
94
|
+
async removeTeamSource(url, teamId) {
|
|
95
|
+
return await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
|
|
96
|
+
const store = await this.readStore();
|
|
97
|
+
const normalizedUrl = url.replace(/\/+$/, "");
|
|
98
|
+
const agent = store.agents.find((a) => a.url === normalizedUrl);
|
|
99
|
+
if (!agent)
|
|
100
|
+
return false;
|
|
101
|
+
// Agent was manually added (no team tracking) — never auto-remove
|
|
102
|
+
if (!agent.teamIds || agent.teamIds.length === 0)
|
|
103
|
+
return false;
|
|
104
|
+
const teams = new Set(agent.teamIds);
|
|
105
|
+
teams.delete(teamId);
|
|
106
|
+
if (teams.size === 0) {
|
|
107
|
+
// No team sources remain — remove entirely
|
|
108
|
+
store.agents = store.agents.filter((a) => a.url !== normalizedUrl);
|
|
109
|
+
await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
agent.teamIds = [...teams];
|
|
113
|
+
await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
|
|
114
|
+
return false;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
76
117
|
async updateDescription(url, description) {
|
|
77
118
|
await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
|
|
78
119
|
const store = await this.readStore();
|
|
@@ -36,6 +36,8 @@ export declare class MulticlawsService extends EventEmitter {
|
|
|
36
36
|
private readonly profileStore;
|
|
37
37
|
private readonly taskTracker;
|
|
38
38
|
private readonly sessionStore;
|
|
39
|
+
private readonly sessionLocks;
|
|
40
|
+
private readonly sessionAborts;
|
|
39
41
|
private agentExecutor;
|
|
40
42
|
private a2aRequestHandler;
|
|
41
43
|
private agentCard;
|
|
@@ -51,7 +53,9 @@ export declare class MulticlawsService extends EventEmitter {
|
|
|
51
53
|
addAgent(params: {
|
|
52
54
|
url: string;
|
|
53
55
|
apiKey?: string;
|
|
54
|
-
}): Promise<AgentRecord
|
|
56
|
+
}): Promise<AgentRecord & {
|
|
57
|
+
reachable: boolean;
|
|
58
|
+
}>;
|
|
55
59
|
removeAgent(url: string): Promise<boolean>;
|
|
56
60
|
startSession(params: {
|
|
57
61
|
agentUrl: string;
|
|
@@ -63,8 +67,23 @@ export declare class MulticlawsService extends EventEmitter {
|
|
|
63
67
|
}): Promise<SessionReplyResult>;
|
|
64
68
|
getSession(sessionId: string): ConversationSession | null;
|
|
65
69
|
listSessions(): ConversationSession[];
|
|
70
|
+
waitForSessions(params: {
|
|
71
|
+
sessionIds: string[];
|
|
72
|
+
timeoutMs?: number;
|
|
73
|
+
}): Promise<{
|
|
74
|
+
results: Array<{
|
|
75
|
+
sessionId: string;
|
|
76
|
+
status: string;
|
|
77
|
+
agentName: string;
|
|
78
|
+
lastMessage?: string;
|
|
79
|
+
error?: string;
|
|
80
|
+
}>;
|
|
81
|
+
timedOut: boolean;
|
|
82
|
+
}>;
|
|
66
83
|
endSession(sessionId: string): boolean;
|
|
84
|
+
private acquireSessionLock;
|
|
67
85
|
private runSession;
|
|
86
|
+
private extractResultState;
|
|
68
87
|
private handleSessionResult;
|
|
69
88
|
private notifySessionUpdate;
|
|
70
89
|
getProfile(): Promise<AgentProfile>;
|
|
@@ -37,6 +37,10 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
37
37
|
profileStore;
|
|
38
38
|
taskTracker;
|
|
39
39
|
sessionStore;
|
|
40
|
+
// Fix #5: per-session lock to prevent concurrent runSession calls
|
|
41
|
+
sessionLocks = new Map();
|
|
42
|
+
// Per-session AbortController so endSession can cancel in-flight runSession
|
|
43
|
+
sessionAborts = new Map();
|
|
40
44
|
agentExecutor = null;
|
|
41
45
|
a2aRequestHandler = null;
|
|
42
46
|
agentCard = null;
|
|
@@ -153,6 +157,18 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
153
157
|
if (!this.started)
|
|
154
158
|
return;
|
|
155
159
|
this.started = false;
|
|
160
|
+
// Abort all in-flight sessions so they don't hang
|
|
161
|
+
for (const [, abort] of this.sessionAborts) {
|
|
162
|
+
abort.abort();
|
|
163
|
+
}
|
|
164
|
+
// Wait for session locks to drain (with a cap)
|
|
165
|
+
if (this.sessionLocks.size > 0) {
|
|
166
|
+
const pending = [...this.sessionLocks.values()];
|
|
167
|
+
await Promise.race([
|
|
168
|
+
Promise.allSettled(pending),
|
|
169
|
+
new Promise((r) => setTimeout(r, 5_000)),
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
156
172
|
this.taskTracker.destroy();
|
|
157
173
|
this.httpRateLimiter.destroy();
|
|
158
174
|
await new Promise((resolve) => {
|
|
@@ -165,6 +181,7 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
165
181
|
this.httpServer = null;
|
|
166
182
|
}
|
|
167
183
|
updateGatewayConfig(config) {
|
|
184
|
+
this.options.gatewayConfig = config;
|
|
168
185
|
this.agentExecutor?.updateGatewayConfig(config);
|
|
169
186
|
}
|
|
170
187
|
/* ---------------------------------------------------------------- */
|
|
@@ -178,20 +195,23 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
178
195
|
try {
|
|
179
196
|
const client = await this.clientFactory.createFromUrl(normalizedUrl);
|
|
180
197
|
const card = await client.getAgentCard();
|
|
181
|
-
|
|
198
|
+
const record = await this.agentRegistry.add({
|
|
182
199
|
url: normalizedUrl,
|
|
183
200
|
name: card.name ?? normalizedUrl,
|
|
184
201
|
description: card.description ?? "",
|
|
185
202
|
skills: card.skills?.map((s) => s.name ?? s.id) ?? [],
|
|
186
203
|
apiKey: params.apiKey,
|
|
187
204
|
});
|
|
205
|
+
return { ...record, reachable: true };
|
|
188
206
|
}
|
|
189
207
|
catch {
|
|
190
|
-
|
|
208
|
+
this.log("warn", `agent at ${normalizedUrl} is not reachable, adding with limited info`);
|
|
209
|
+
const record = await this.agentRegistry.add({
|
|
191
210
|
url: normalizedUrl,
|
|
192
211
|
name: normalizedUrl,
|
|
193
212
|
apiKey: params.apiKey,
|
|
194
213
|
});
|
|
214
|
+
return { ...record, reachable: false };
|
|
195
215
|
}
|
|
196
216
|
}
|
|
197
217
|
async removeAgent(url) {
|
|
@@ -205,27 +225,29 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
205
225
|
/* ---------------------------------------------------------------- */
|
|
206
226
|
async startSession(params) {
|
|
207
227
|
const agentRecord = await this.agentRegistry.get(params.agentUrl);
|
|
228
|
+
// Fix #3: throw instead of returning empty sessionId
|
|
208
229
|
if (!agentRecord) {
|
|
209
|
-
|
|
230
|
+
throw new Error(`unknown agent: ${params.agentUrl}`);
|
|
210
231
|
}
|
|
211
|
-
|
|
232
|
+
// Fix #4: don't pre-generate contextId; let server assign it.
|
|
233
|
+
// Use a local placeholder that gets replaced after first response.
|
|
212
234
|
const session = this.sessionStore.create({
|
|
213
235
|
agentUrl: params.agentUrl,
|
|
214
236
|
agentName: agentRecord.name,
|
|
215
|
-
contextId,
|
|
237
|
+
contextId: "", // will be filled in from server response
|
|
216
238
|
});
|
|
217
239
|
this.sessionStore.appendMessage(session.sessionId, {
|
|
218
240
|
role: "user",
|
|
219
241
|
content: params.message,
|
|
220
242
|
timestampMs: Date.now(),
|
|
221
243
|
});
|
|
222
|
-
void this.runSession({
|
|
244
|
+
void this.acquireSessionLock(session.sessionId, () => this.runSession({
|
|
223
245
|
sessionId: session.sessionId,
|
|
224
246
|
agentRecord,
|
|
225
247
|
message: params.message,
|
|
226
|
-
contextId,
|
|
248
|
+
contextId: undefined, // first message: no contextId
|
|
227
249
|
taskId: undefined,
|
|
228
|
-
});
|
|
250
|
+
}));
|
|
229
251
|
return { sessionId: session.sessionId, status: "running" };
|
|
230
252
|
}
|
|
231
253
|
async sendSessionMessage(params) {
|
|
@@ -247,13 +269,14 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
247
269
|
this.sessionStore.update(params.sessionId, { status: "failed", error: "agent no longer registered" });
|
|
248
270
|
return { sessionId: params.sessionId, status: "failed", error: "agent no longer registered" };
|
|
249
271
|
}
|
|
250
|
-
|
|
272
|
+
// Fix #5: acquire lock to prevent concurrent runSession on same session
|
|
273
|
+
void this.acquireSessionLock(params.sessionId, () => this.runSession({
|
|
251
274
|
sessionId: params.sessionId,
|
|
252
275
|
agentRecord,
|
|
253
276
|
message: params.message,
|
|
254
|
-
contextId: session.contextId,
|
|
277
|
+
contextId: session.contextId || undefined,
|
|
255
278
|
taskId: session.currentTaskId,
|
|
256
|
-
});
|
|
279
|
+
}));
|
|
257
280
|
return { sessionId: params.sessionId, status: "ok" };
|
|
258
281
|
}
|
|
259
282
|
getSession(sessionId) {
|
|
@@ -262,54 +285,150 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
262
285
|
listSessions() {
|
|
263
286
|
return this.sessionStore.list();
|
|
264
287
|
}
|
|
288
|
+
async waitForSessions(params) {
|
|
289
|
+
const timeout = params.timeoutMs ?? 5 * 60 * 1000;
|
|
290
|
+
const deadline = Date.now() + timeout;
|
|
291
|
+
const terminalStates = new Set(["completed", "failed", "canceled"]);
|
|
292
|
+
const getResults = () => params.sessionIds.map((id) => {
|
|
293
|
+
const session = this.sessionStore.get(id);
|
|
294
|
+
if (!session)
|
|
295
|
+
return { sessionId: id, agentName: "unknown", status: "not_found" };
|
|
296
|
+
const lastAgent = [...session.messages].reverse().find((m) => m.role === "agent");
|
|
297
|
+
return {
|
|
298
|
+
sessionId: id,
|
|
299
|
+
agentName: session.agentName,
|
|
300
|
+
status: session.status,
|
|
301
|
+
lastMessage: lastAgent?.content,
|
|
302
|
+
error: session.error,
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
while (Date.now() < deadline) {
|
|
306
|
+
const results = getResults();
|
|
307
|
+
const allSettled = results.every((r) => terminalStates.has(r.status) || r.status === "not_found");
|
|
308
|
+
if (allSettled)
|
|
309
|
+
return { results, timedOut: false };
|
|
310
|
+
// Return early if any session needs input — AI must handle it before continuing
|
|
311
|
+
const needsInput = results.some((r) => r.status === "input-required");
|
|
312
|
+
if (needsInput)
|
|
313
|
+
return { results, timedOut: false };
|
|
314
|
+
await new Promise((r) => setTimeout(r, 1_000));
|
|
315
|
+
}
|
|
316
|
+
return { results: getResults(), timedOut: true };
|
|
317
|
+
}
|
|
265
318
|
endSession(sessionId) {
|
|
266
319
|
const session = this.sessionStore.get(sessionId);
|
|
267
320
|
if (!session)
|
|
268
321
|
return false;
|
|
269
322
|
this.sessionStore.update(sessionId, { status: "canceled" });
|
|
323
|
+
// Signal the in-flight runSession to abort
|
|
324
|
+
const abort = this.sessionAborts.get(sessionId);
|
|
325
|
+
if (abort)
|
|
326
|
+
abort.abort();
|
|
270
327
|
return true;
|
|
271
328
|
}
|
|
329
|
+
// Fix #5: serialise concurrent calls on the same session
|
|
330
|
+
async acquireSessionLock(sessionId, fn) {
|
|
331
|
+
const prev = this.sessionLocks.get(sessionId) ?? Promise.resolve();
|
|
332
|
+
let release;
|
|
333
|
+
const next = new Promise((r) => { release = r; });
|
|
334
|
+
this.sessionLocks.set(sessionId, next);
|
|
335
|
+
try {
|
|
336
|
+
await prev;
|
|
337
|
+
await fn();
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
release();
|
|
341
|
+
if (this.sessionLocks.get(sessionId) === next) {
|
|
342
|
+
this.sessionLocks.delete(sessionId);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
272
346
|
async runSession(params) {
|
|
273
|
-
const timeout = params.timeoutMs ?? 5 * 60 * 1000;
|
|
274
|
-
const
|
|
275
|
-
const
|
|
347
|
+
const timeout = params.timeoutMs ?? 5 * 60 * 1000;
|
|
348
|
+
const deadline = Date.now() + timeout;
|
|
349
|
+
const abortController = new AbortController();
|
|
350
|
+
this.sessionAborts.set(params.sessionId, abortController);
|
|
351
|
+
const timer = setTimeout(() => abortController.abort(), timeout);
|
|
276
352
|
try {
|
|
277
353
|
const client = await this.createA2AClient(params.agentRecord);
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
354
|
+
const withAbort = (p) => Promise.race([
|
|
355
|
+
p,
|
|
356
|
+
new Promise((_, reject) => {
|
|
357
|
+
if (abortController.signal.aborted) {
|
|
358
|
+
reject(new Error("session canceled"));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
abortController.signal.addEventListener("abort", () => reject(new Error(this.sessionStore.get(params.sessionId)?.status === "canceled"
|
|
362
|
+
? "session canceled"
|
|
363
|
+
: "session timeout")));
|
|
288
364
|
}),
|
|
289
|
-
new Promise((_, reject) => timeoutController.signal.addEventListener("abort", () => reject(new Error("session timeout")))),
|
|
290
365
|
]);
|
|
366
|
+
let result = await withAbort(client.sendMessage({
|
|
367
|
+
message: {
|
|
368
|
+
kind: "message",
|
|
369
|
+
role: "user",
|
|
370
|
+
parts: [{ kind: "text", text: params.message }],
|
|
371
|
+
messageId: (0, node_crypto_1.randomUUID)(),
|
|
372
|
+
// Fix #4: only pass contextId if we have a server-assigned one
|
|
373
|
+
...(params.contextId ? { contextId: params.contextId } : {}),
|
|
374
|
+
...(params.taskId ? { taskId: params.taskId } : {}),
|
|
375
|
+
},
|
|
376
|
+
}));
|
|
377
|
+
// Fix #1: poll until terminal state if server returns working/submitted
|
|
378
|
+
const POLL_DELAYS = [1000, 2000, 3000, 5000];
|
|
379
|
+
let pollAttempt = 0;
|
|
380
|
+
while (true) {
|
|
381
|
+
const state = this.extractResultState(result);
|
|
382
|
+
const remoteTaskId = "id" in result ? result.id : undefined;
|
|
383
|
+
if (state !== "working" && state !== "submitted")
|
|
384
|
+
break;
|
|
385
|
+
if (!remoteTaskId)
|
|
386
|
+
break; // can't poll without task id
|
|
387
|
+
if (Date.now() >= deadline)
|
|
388
|
+
throw new Error("session timeout");
|
|
389
|
+
const delay = POLL_DELAYS[Math.min(pollAttempt, POLL_DELAYS.length - 1)];
|
|
390
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
391
|
+
pollAttempt++;
|
|
392
|
+
result = await withAbort(client.getTask({ id: remoteTaskId, historyLength: 10 }));
|
|
393
|
+
}
|
|
394
|
+
// Check if session was canceled while we were running
|
|
395
|
+
const current = this.sessionStore.get(params.sessionId);
|
|
396
|
+
if (current?.status === "canceled")
|
|
397
|
+
return;
|
|
291
398
|
await this.handleSessionResult(params.sessionId, result);
|
|
292
399
|
}
|
|
293
400
|
catch (err) {
|
|
401
|
+
// Don't overwrite a user-initiated cancel
|
|
402
|
+
const current = this.sessionStore.get(params.sessionId);
|
|
403
|
+
if (current?.status === "canceled")
|
|
404
|
+
return;
|
|
294
405
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
295
406
|
this.sessionStore.update(params.sessionId, { status: "failed", error: errorMsg });
|
|
296
407
|
await this.notifySessionUpdate(params.sessionId, "failed");
|
|
297
408
|
}
|
|
298
409
|
finally {
|
|
299
410
|
clearTimeout(timer);
|
|
411
|
+
this.sessionAborts.delete(params.sessionId);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
extractResultState(result) {
|
|
415
|
+
if ("status" in result && result.status) {
|
|
416
|
+
return result.status?.state ?? "unknown";
|
|
300
417
|
}
|
|
418
|
+
return "completed"; // plain Message = completed
|
|
301
419
|
}
|
|
302
420
|
async handleSessionResult(sessionId, result) {
|
|
303
|
-
// Extract content
|
|
304
421
|
let content = "";
|
|
305
422
|
let state = "completed";
|
|
306
423
|
let remoteTaskId;
|
|
424
|
+
let serverContextId;
|
|
307
425
|
if ("status" in result && result.status) {
|
|
308
426
|
const task = result;
|
|
309
427
|
state = task.status?.state ?? "completed";
|
|
310
428
|
remoteTaskId = task.id;
|
|
429
|
+
// Fix #4: capture server-assigned contextId
|
|
430
|
+
serverContextId = task.contextId;
|
|
311
431
|
content = this.extractArtifactText(task);
|
|
312
|
-
// Also try to get text from task messages if artifacts empty
|
|
313
432
|
if (!content && task.history?.length) {
|
|
314
433
|
const lastAgentMsg = [...task.history].reverse().find((m) => m.role === "agent");
|
|
315
434
|
if (lastAgentMsg) {
|
|
@@ -323,23 +442,24 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
323
442
|
else {
|
|
324
443
|
const msg = result;
|
|
325
444
|
remoteTaskId = msg.taskId;
|
|
445
|
+
serverContextId = msg.contextId;
|
|
326
446
|
content = msg.parts
|
|
327
447
|
?.filter((p) => p.kind === "text")
|
|
328
448
|
.map((p) => p.text)
|
|
329
449
|
.join("\n") ?? "";
|
|
330
450
|
}
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
// Update session state
|
|
451
|
+
// Fix #6: always record agent message, use placeholder when content is empty
|
|
452
|
+
this.sessionStore.appendMessage(sessionId, {
|
|
453
|
+
role: "agent",
|
|
454
|
+
content: content || "(no text output)",
|
|
455
|
+
timestampMs: Date.now(),
|
|
456
|
+
taskId: remoteTaskId,
|
|
457
|
+
});
|
|
458
|
+
// Fix #4: update contextId with server-assigned value
|
|
459
|
+
const contextUpdate = serverContextId ? { contextId: serverContextId } : {};
|
|
341
460
|
if (state === "input-required" || state === "auth-required") {
|
|
342
461
|
this.sessionStore.update(sessionId, {
|
|
462
|
+
...contextUpdate,
|
|
343
463
|
status: "input-required",
|
|
344
464
|
currentTaskId: remoteTaskId,
|
|
345
465
|
});
|
|
@@ -347,36 +467,54 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
347
467
|
}
|
|
348
468
|
else if (state === "failed" || state === "rejected") {
|
|
349
469
|
this.sessionStore.update(sessionId, {
|
|
470
|
+
...contextUpdate,
|
|
350
471
|
status: "failed",
|
|
351
472
|
currentTaskId: remoteTaskId,
|
|
352
473
|
error: content || "remote task failed",
|
|
353
474
|
});
|
|
354
475
|
await this.notifySessionUpdate(sessionId, "failed");
|
|
355
476
|
}
|
|
356
|
-
else if (state === "completed"
|
|
477
|
+
else if (state === "completed") {
|
|
357
478
|
this.sessionStore.update(sessionId, {
|
|
479
|
+
...contextUpdate,
|
|
358
480
|
status: "completed",
|
|
359
481
|
currentTaskId: remoteTaskId,
|
|
360
482
|
});
|
|
361
483
|
await this.notifySessionUpdate(sessionId, "completed");
|
|
362
484
|
}
|
|
485
|
+
else if (state === "canceled") {
|
|
486
|
+
// Fix #2: canceled remote task → local status "canceled", not "completed"
|
|
487
|
+
this.sessionStore.update(sessionId, {
|
|
488
|
+
...contextUpdate,
|
|
489
|
+
status: "canceled",
|
|
490
|
+
currentTaskId: remoteTaskId,
|
|
491
|
+
error: "remote task was canceled",
|
|
492
|
+
});
|
|
493
|
+
await this.notifySessionUpdate(sessionId, "failed");
|
|
494
|
+
}
|
|
363
495
|
else {
|
|
364
|
-
// working / submitted / unknown
|
|
365
|
-
this.sessionStore.update(sessionId, { currentTaskId: remoteTaskId });
|
|
496
|
+
// working / submitted / unknown: runSession's polling loop handles these
|
|
497
|
+
this.sessionStore.update(sessionId, { ...contextUpdate, currentTaskId: remoteTaskId });
|
|
366
498
|
}
|
|
367
499
|
}
|
|
368
500
|
async notifySessionUpdate(sessionId, event) {
|
|
369
|
-
if (!this.options.gatewayConfig)
|
|
501
|
+
if (!this.options.gatewayConfig) {
|
|
502
|
+
this.log("warn", `session ${sessionId} ${event} but gateway config unavailable — user won't be notified. Check gateway.auth.token in config.`);
|
|
370
503
|
return;
|
|
504
|
+
}
|
|
371
505
|
const session = this.sessionStore.get(sessionId);
|
|
372
506
|
if (!session)
|
|
373
507
|
return;
|
|
374
508
|
const lastAgentMsg = [...session.messages].reverse().find((m) => m.role === "agent");
|
|
375
|
-
const
|
|
509
|
+
const rawContent = lastAgentMsg?.content ?? "";
|
|
510
|
+
// Don't show the placeholder text in user-facing notifications
|
|
511
|
+
const content = rawContent === "(no text output)" ? "" : rawContent;
|
|
376
512
|
const agentName = session.agentName;
|
|
377
513
|
let message;
|
|
378
514
|
if (event === "completed") {
|
|
379
|
-
message =
|
|
515
|
+
message = content
|
|
516
|
+
? [`✅ **${agentName} 任务完成** (session: \`${sessionId}\`)`, "", content].join("\n")
|
|
517
|
+
: `✅ **${agentName} 任务完成** (session: \`${sessionId}\`) — 任务已执行但无文本输出,可能产生了 artifacts。`;
|
|
380
518
|
}
|
|
381
519
|
else if (event === "input-required") {
|
|
382
520
|
message = [
|
|
@@ -556,7 +694,7 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
556
694
|
}
|
|
557
695
|
}));
|
|
558
696
|
for (const m of others) {
|
|
559
|
-
await this.agentRegistry.
|
|
697
|
+
await this.agentRegistry.removeTeamSource(m.url, team.teamId);
|
|
560
698
|
}
|
|
561
699
|
await this.teamStore.deleteTeam(team.teamId);
|
|
562
700
|
this.log("info", `left team ${team.teamId}`);
|
|
@@ -632,10 +770,14 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
632
770
|
name: member.name,
|
|
633
771
|
description: member.description,
|
|
634
772
|
});
|
|
773
|
+
await this.agentRegistry.addTeamSource(normalizedUrl, team.teamId);
|
|
635
774
|
// Broadcast to other members if new
|
|
636
775
|
if (!alreadyKnown) {
|
|
637
776
|
const selfNormalized = this.selfUrl.replace(/\/+$/, "");
|
|
638
|
-
|
|
777
|
+
// Re-read team after addMember to get the latest member list,
|
|
778
|
+
// avoiding missed broadcasts when multiple members join concurrently
|
|
779
|
+
const freshTeam = await this.teamStore.getTeam(team.teamId);
|
|
780
|
+
const others = (freshTeam?.members ?? team.members).filter((m) => m.url.replace(/\/+$/, "") !== normalizedUrl &&
|
|
639
781
|
m.url.replace(/\/+$/, "") !== selfNormalized);
|
|
640
782
|
for (const other of others) {
|
|
641
783
|
void this.fetchWithRetry(`${other.url}/team/${team.teamId}/announce`, {
|
|
@@ -755,14 +897,20 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
755
897
|
const client = await this.clientFactory.createFromUrl(m.url);
|
|
756
898
|
const card = await client.getAgentCard();
|
|
757
899
|
if (card.description) {
|
|
758
|
-
|
|
900
|
+
// Use addMember (which uses withJsonLock) instead of saveTeam
|
|
901
|
+
// to avoid overwriting concurrent member additions
|
|
902
|
+
await this.teamStore.addMember(team.teamId, {
|
|
903
|
+
url: m.url,
|
|
904
|
+
name: m.name,
|
|
905
|
+
description: card.description,
|
|
906
|
+
joinedAtMs: m.joinedAtMs,
|
|
907
|
+
});
|
|
759
908
|
}
|
|
760
909
|
}
|
|
761
910
|
catch {
|
|
762
911
|
this.log("warn", `failed to fetch Agent Card from ${m.url}`);
|
|
763
912
|
}
|
|
764
913
|
}));
|
|
765
|
-
await this.teamStore.saveTeam(team);
|
|
766
914
|
}
|
|
767
915
|
async syncTeamToRegistry(team) {
|
|
768
916
|
const selfNormalized = this.selfUrl.replace(/\/+$/, "");
|
|
@@ -774,6 +922,7 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
774
922
|
name: member.name,
|
|
775
923
|
description: member.description,
|
|
776
924
|
});
|
|
925
|
+
await this.agentRegistry.addTeamSource(member.url, team.teamId);
|
|
777
926
|
}
|
|
778
927
|
}
|
|
779
928
|
async createA2AClient(agent) {
|
|
@@ -10,6 +10,7 @@ const promises_1 = __importDefault(require("node:fs/promises"));
|
|
|
10
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
11
|
const DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
12
12
|
const MAX_SESSIONS = 1_000;
|
|
13
|
+
const MAX_MESSAGES_PER_SESSION = 200;
|
|
13
14
|
function emptyStore() {
|
|
14
15
|
return { version: 1, sessions: [] };
|
|
15
16
|
}
|
|
@@ -66,9 +67,12 @@ class SessionStore {
|
|
|
66
67
|
const session = this.get(sessionId);
|
|
67
68
|
if (!session)
|
|
68
69
|
return null;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
let messages = [...session.messages, msg];
|
|
71
|
+
// Truncate old messages, keeping the most recent ones
|
|
72
|
+
if (messages.length > MAX_MESSAGES_PER_SESSION) {
|
|
73
|
+
messages = messages.slice(-MAX_MESSAGES_PER_SESSION);
|
|
74
|
+
}
|
|
75
|
+
return this.update(sessionId, { messages });
|
|
72
76
|
}
|
|
73
77
|
loadSync() {
|
|
74
78
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(this.filePath), { recursive: true });
|
package/package.json
CHANGED
|
@@ -49,13 +49,19 @@ multiclaws_session_status(sessionId="...") → 查看单个会话及消息历史
|
|
|
49
49
|
multiclaws_session_end(sessionId="...") → 取消并关闭会话
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
###
|
|
53
|
-
|
|
52
|
+
### 并发协作(自动汇总)
|
|
53
|
+
同时开启多个 session,等所有结果后汇总:
|
|
54
54
|
```
|
|
55
|
-
multiclaws_session_start(agentUrl=B, message="
|
|
56
|
-
multiclaws_session_start(agentUrl=C, message="
|
|
55
|
+
id1 = multiclaws_session_start(agentUrl=B, message="子任务1")
|
|
56
|
+
id2 = multiclaws_session_start(agentUrl=C, message="子任务2")
|
|
57
|
+
results = multiclaws_session_wait_all(sessionIds=[id1, id2])
|
|
58
|
+
→ 阻塞直到全部完成,返回所有结果
|
|
59
|
+
→ AI 汇总后回复用户
|
|
57
60
|
```
|
|
58
61
|
|
|
62
|
+
**注意**:若任何 session 变为 `input-required`,`wait_all` 会提前返回,
|
|
63
|
+
AI 应先用 `session_reply` 处理,再继续等待剩余 session。
|
|
64
|
+
|
|
59
65
|
### 链式协作(A→B→C)
|
|
60
66
|
B 内部可以自己调用 `multiclaws_session_start` 委派给 C,结果自然冒泡回 A。
|
|
61
67
|
|
|
@@ -99,6 +105,7 @@ multiclaws_profile_show()
|
|
|
99
105
|
| `multiclaws_session_start` | 开始协作会话(替代旧 delegate) | `agentUrl`, `message` |
|
|
100
106
|
| `multiclaws_session_reply` | 在会话中发送后续消息 | `sessionId`, `message` |
|
|
101
107
|
| `multiclaws_session_status` | 查看会话状态和消息历史 | `sessionId`(可选,不传返回全部) |
|
|
108
|
+
| `multiclaws_session_wait_all` | 等待多个会话全部完成,返回所有结果 | `sessionIds[]`, `timeoutMs`(可选) |
|
|
102
109
|
| `multiclaws_session_end` | 取消/关闭会话 | `sessionId` |
|
|
103
110
|
|
|
104
111
|
### 档案
|