multiclaws 0.3.4 → 0.4.0
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 +49 -19
- package/dist/index.js +69 -23
- package/dist/service/multiclaws-service.d.ts +23 -9
- package/dist/service/multiclaws-service.js +198 -42
- package/dist/service/session-store.d.ts +43 -0
- package/dist/service/session-store.js +124 -0
- package/package.json +1 -1
- package/skills/multiclaws/SKILL.md +81 -100
package/dist/gateway/handlers.js
CHANGED
|
@@ -8,11 +8,16 @@ const agentAddSchema = zod_1.z.object({
|
|
|
8
8
|
apiKey: zod_1.z.string().trim().min(1).optional(),
|
|
9
9
|
});
|
|
10
10
|
const agentRemoveSchema = zod_1.z.object({ url: nonEmptyString });
|
|
11
|
-
const
|
|
11
|
+
const sessionStartSchema = zod_1.z.object({
|
|
12
12
|
agentUrl: nonEmptyString,
|
|
13
|
-
|
|
13
|
+
message: nonEmptyString,
|
|
14
14
|
});
|
|
15
|
-
const
|
|
15
|
+
const sessionReplySchema = zod_1.z.object({
|
|
16
|
+
sessionId: nonEmptyString,
|
|
17
|
+
message: nonEmptyString,
|
|
18
|
+
});
|
|
19
|
+
const sessionStatusSchema = zod_1.z.object({ sessionId: zod_1.z.string().trim().min(1).optional() });
|
|
20
|
+
const sessionEndSchema = zod_1.z.object({ sessionId: nonEmptyString });
|
|
16
21
|
const profileSetSchema = zod_1.z.object({
|
|
17
22
|
ownerName: zod_1.z.string().trim().optional(),
|
|
18
23
|
bio: zod_1.z.string().optional(),
|
|
@@ -57,34 +62,59 @@ function createGatewayHandlers(getService) {
|
|
|
57
62
|
safeHandle(respond, "invalid_params", error);
|
|
58
63
|
}
|
|
59
64
|
},
|
|
60
|
-
/* ──
|
|
61
|
-
"multiclaws.
|
|
65
|
+
/* ── Session handlers ───────────────────────────────────────── */
|
|
66
|
+
"multiclaws.session.start": async ({ params, respond }) => {
|
|
62
67
|
try {
|
|
63
|
-
const parsed =
|
|
68
|
+
const parsed = sessionStartSchema.parse(params);
|
|
64
69
|
const service = getService();
|
|
65
|
-
const result = await service.
|
|
70
|
+
const result = await service.startSession(parsed);
|
|
66
71
|
respond(true, result);
|
|
67
72
|
}
|
|
68
73
|
catch (error) {
|
|
69
|
-
safeHandle(respond, "
|
|
74
|
+
safeHandle(respond, "session_start_failed", error);
|
|
70
75
|
}
|
|
71
76
|
},
|
|
72
|
-
"multiclaws.
|
|
77
|
+
"multiclaws.session.reply": async ({ params, respond }) => {
|
|
73
78
|
try {
|
|
74
|
-
const parsed =
|
|
79
|
+
const parsed = sessionReplySchema.parse(params);
|
|
75
80
|
const service = getService();
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
const result = await service.sendSessionMessage(parsed);
|
|
82
|
+
respond(true, result);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
safeHandle(respond, "session_reply_failed", error);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"multiclaws.session.status": async ({ params, respond }) => {
|
|
89
|
+
try {
|
|
90
|
+
const parsed = sessionStatusSchema.parse(params);
|
|
91
|
+
const service = getService();
|
|
92
|
+
if (parsed.sessionId) {
|
|
93
|
+
const session = service.getSession(parsed.sessionId);
|
|
94
|
+
if (!session) {
|
|
95
|
+
respond(false, undefined, { code: "not_found", message: `session not found: ${parsed.sessionId}` });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
respond(true, session);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const sessions = service.listSessions();
|
|
102
|
+
respond(true, { sessions });
|
|
83
103
|
}
|
|
84
|
-
respond(true, { task });
|
|
85
104
|
}
|
|
86
105
|
catch (error) {
|
|
87
|
-
safeHandle(respond, "
|
|
106
|
+
safeHandle(respond, "session_status_failed", error);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"multiclaws.session.end": async ({ params, respond }) => {
|
|
110
|
+
try {
|
|
111
|
+
const parsed = sessionEndSchema.parse(params);
|
|
112
|
+
const service = getService();
|
|
113
|
+
const ok = service.endSession(parsed.sessionId);
|
|
114
|
+
respond(true, { ended: ok });
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
safeHandle(respond, "session_end_failed", error);
|
|
88
118
|
}
|
|
89
119
|
},
|
|
90
120
|
/* ── Team handlers ──────────────────────────────────────────── */
|
package/dist/index.js
CHANGED
|
@@ -87,48 +87,92 @@ function createTools(getService) {
|
|
|
87
87
|
return textResult(removed ? `Agent ${url} removed.` : `Agent ${url} not found.`);
|
|
88
88
|
},
|
|
89
89
|
};
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
/* ── Session tools (multi-turn collaboration) ─────────────────── */
|
|
91
|
+
const multiclawsSessionStart = {
|
|
92
|
+
name: "multiclaws_session_start",
|
|
93
|
+
description: "Start a multi-turn collaboration session with a remote agent. Sends the first message and returns immediately with a sessionId (async). The agent's response will be pushed as a message when ready. Covers both single-turn and multi-turn use cases.",
|
|
93
94
|
parameters: {
|
|
94
95
|
type: "object",
|
|
95
96
|
additionalProperties: false,
|
|
96
97
|
properties: {
|
|
97
98
|
agentUrl: { type: "string" },
|
|
98
|
-
|
|
99
|
+
message: { type: "string" },
|
|
99
100
|
},
|
|
100
|
-
required: ["agentUrl", "
|
|
101
|
+
required: ["agentUrl", "message"],
|
|
101
102
|
},
|
|
102
103
|
execute: async (_toolCallId, args) => {
|
|
103
104
|
const service = requireService(getService());
|
|
104
105
|
const agentUrl = typeof args.agentUrl === "string" ? args.agentUrl.trim() : "";
|
|
105
|
-
const
|
|
106
|
-
if (!agentUrl || !
|
|
107
|
-
throw new Error("agentUrl and
|
|
108
|
-
const result = await service.
|
|
106
|
+
const message = typeof args.message === "string" ? args.message.trim() : "";
|
|
107
|
+
if (!agentUrl || !message)
|
|
108
|
+
throw new Error("agentUrl and message are required");
|
|
109
|
+
const result = await service.startSession({ agentUrl, message });
|
|
109
110
|
return textResult(JSON.stringify(result, null, 2), result);
|
|
110
111
|
},
|
|
111
112
|
};
|
|
112
|
-
const
|
|
113
|
-
name: "
|
|
114
|
-
description: "
|
|
113
|
+
const multiclawsSessionReply = {
|
|
114
|
+
name: "multiclaws_session_reply",
|
|
115
|
+
description: "Send a follow-up message in an existing collaboration session. Use when the remote agent returns 'input-required' or to continue a multi-turn conversation.",
|
|
115
116
|
parameters: {
|
|
116
117
|
type: "object",
|
|
117
118
|
additionalProperties: false,
|
|
118
119
|
properties: {
|
|
119
|
-
|
|
120
|
+
sessionId: { type: "string" },
|
|
121
|
+
message: { type: "string" },
|
|
120
122
|
},
|
|
121
|
-
required: ["
|
|
123
|
+
required: ["sessionId", "message"],
|
|
122
124
|
},
|
|
123
125
|
execute: async (_toolCallId, args) => {
|
|
124
126
|
const service = requireService(getService());
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
const sessionId = typeof args.sessionId === "string" ? args.sessionId.trim() : "";
|
|
128
|
+
const message = typeof args.message === "string" ? args.message.trim() : "";
|
|
129
|
+
if (!sessionId || !message)
|
|
130
|
+
throw new Error("sessionId and message are required");
|
|
131
|
+
const result = await service.sendSessionMessage({ sessionId, message });
|
|
132
|
+
return textResult(JSON.stringify(result, null, 2), result);
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
const multiclawsSessionStatus = {
|
|
136
|
+
name: "multiclaws_session_status",
|
|
137
|
+
description: "Get the status and message history of a collaboration session. If sessionId is omitted, lists all sessions.",
|
|
138
|
+
parameters: {
|
|
139
|
+
type: "object",
|
|
140
|
+
additionalProperties: false,
|
|
141
|
+
properties: {
|
|
142
|
+
sessionId: { type: "string" },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
execute: async (_toolCallId, args) => {
|
|
146
|
+
const service = requireService(getService());
|
|
147
|
+
const sessionId = typeof args.sessionId === "string" ? args.sessionId.trim() : "";
|
|
148
|
+
if (sessionId) {
|
|
149
|
+
const session = service.getSession(sessionId);
|
|
150
|
+
if (!session)
|
|
151
|
+
throw new Error(`session not found: ${sessionId}`);
|
|
152
|
+
return textResult(JSON.stringify(session, null, 2), session);
|
|
153
|
+
}
|
|
154
|
+
const sessions = service.listSessions();
|
|
155
|
+
return textResult(JSON.stringify({ sessions }, null, 2), { sessions });
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
const multiclawsSessionEnd = {
|
|
159
|
+
name: "multiclaws_session_end",
|
|
160
|
+
description: "Cancel and close a collaboration session.",
|
|
161
|
+
parameters: {
|
|
162
|
+
type: "object",
|
|
163
|
+
additionalProperties: false,
|
|
164
|
+
properties: {
|
|
165
|
+
sessionId: { type: "string" },
|
|
166
|
+
},
|
|
167
|
+
required: ["sessionId"],
|
|
168
|
+
},
|
|
169
|
+
execute: async (_toolCallId, args) => {
|
|
170
|
+
const service = requireService(getService());
|
|
171
|
+
const sessionId = typeof args.sessionId === "string" ? args.sessionId.trim() : "";
|
|
172
|
+
if (!sessionId)
|
|
173
|
+
throw new Error("sessionId is required");
|
|
174
|
+
const ok = service.endSession(sessionId);
|
|
175
|
+
return textResult(ok ? `Session ${sessionId} ended.` : `Session ${sessionId} not found.`);
|
|
132
176
|
},
|
|
133
177
|
};
|
|
134
178
|
/* ── Team tools ───────────────────────────────────────────────── */
|
|
@@ -280,8 +324,10 @@ function createTools(getService) {
|
|
|
280
324
|
multiclawsAgents,
|
|
281
325
|
multiclawsAddAgent,
|
|
282
326
|
multiclawsRemoveAgent,
|
|
283
|
-
|
|
284
|
-
|
|
327
|
+
multiclawsSessionStart,
|
|
328
|
+
multiclawsSessionReply,
|
|
329
|
+
multiclawsSessionStatus,
|
|
330
|
+
multiclawsSessionEnd,
|
|
285
331
|
multiclawsTeamCreate,
|
|
286
332
|
multiclawsTeamJoin,
|
|
287
333
|
multiclawsTeamLeave,
|
|
@@ -2,6 +2,7 @@ import { EventEmitter } from "node:events";
|
|
|
2
2
|
import { type AgentRecord } from "./agent-registry";
|
|
3
3
|
import { type AgentProfile } from "./agent-profile";
|
|
4
4
|
import { type TeamRecord, type TeamMember } from "../team/team-store";
|
|
5
|
+
import { type ConversationSession } from "./session-store";
|
|
5
6
|
import type { GatewayConfig } from "../infra/gateway-client";
|
|
6
7
|
export type MulticlawsServiceOptions = {
|
|
7
8
|
stateDir: string;
|
|
@@ -16,10 +17,14 @@ export type MulticlawsServiceOptions = {
|
|
|
16
17
|
debug?: (message: string) => void;
|
|
17
18
|
};
|
|
18
19
|
};
|
|
19
|
-
export type
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
export type SessionStartResult = {
|
|
21
|
+
sessionId: string;
|
|
22
|
+
status: "running" | "failed";
|
|
23
|
+
error?: string;
|
|
24
|
+
};
|
|
25
|
+
export type SessionReplyResult = {
|
|
26
|
+
sessionId: string;
|
|
27
|
+
status: "ok" | "failed";
|
|
23
28
|
error?: string;
|
|
24
29
|
};
|
|
25
30
|
export declare class MulticlawsService extends EventEmitter {
|
|
@@ -30,6 +35,7 @@ export declare class MulticlawsService extends EventEmitter {
|
|
|
30
35
|
private readonly teamStore;
|
|
31
36
|
private readonly profileStore;
|
|
32
37
|
private readonly taskTracker;
|
|
38
|
+
private readonly sessionStore;
|
|
33
39
|
private agentExecutor;
|
|
34
40
|
private a2aRequestHandler;
|
|
35
41
|
private agentCard;
|
|
@@ -47,11 +53,20 @@ export declare class MulticlawsService extends EventEmitter {
|
|
|
47
53
|
apiKey?: string;
|
|
48
54
|
}): Promise<AgentRecord>;
|
|
49
55
|
removeAgent(url: string): Promise<boolean>;
|
|
50
|
-
|
|
56
|
+
startSession(params: {
|
|
51
57
|
agentUrl: string;
|
|
52
|
-
|
|
53
|
-
}): Promise<
|
|
54
|
-
|
|
58
|
+
message: string;
|
|
59
|
+
}): Promise<SessionStartResult>;
|
|
60
|
+
sendSessionMessage(params: {
|
|
61
|
+
sessionId: string;
|
|
62
|
+
message: string;
|
|
63
|
+
}): Promise<SessionReplyResult>;
|
|
64
|
+
getSession(sessionId: string): ConversationSession | null;
|
|
65
|
+
listSessions(): ConversationSession[];
|
|
66
|
+
endSession(sessionId: string): boolean;
|
|
67
|
+
private runSession;
|
|
68
|
+
private handleSessionResult;
|
|
69
|
+
private notifySessionUpdate;
|
|
55
70
|
getProfile(): Promise<AgentProfile>;
|
|
56
71
|
setProfile(patch: {
|
|
57
72
|
ownerName?: string;
|
|
@@ -84,7 +99,6 @@ export declare class MulticlawsService extends EventEmitter {
|
|
|
84
99
|
private fetchMemberDescriptions;
|
|
85
100
|
private syncTeamToRegistry;
|
|
86
101
|
private createA2AClient;
|
|
87
|
-
private processTaskResult;
|
|
88
102
|
private extractArtifactText;
|
|
89
103
|
private notifyTailscaleSetup;
|
|
90
104
|
/** Fetch with up to 2 retries and exponential backoff. */
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.MulticlawsService = void 0;
|
|
7
7
|
const node_events_1 = require("node:events");
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
9
|
const node_os_1 = __importDefault(require("node:os"));
|
|
9
10
|
const node_http_1 = __importDefault(require("node:http"));
|
|
10
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -20,6 +21,7 @@ const agent_registry_1 = require("./agent-registry");
|
|
|
20
21
|
const agent_profile_1 = require("./agent-profile");
|
|
21
22
|
const team_store_1 = require("../team/team-store");
|
|
22
23
|
const tracker_1 = require("../task/tracker");
|
|
24
|
+
const session_store_1 = require("./session-store");
|
|
23
25
|
const zod_1 = require("zod");
|
|
24
26
|
const gateway_client_1 = require("../infra/gateway-client");
|
|
25
27
|
const rate_limiter_1 = require("../infra/rate-limiter");
|
|
@@ -34,6 +36,7 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
34
36
|
teamStore;
|
|
35
37
|
profileStore;
|
|
36
38
|
taskTracker;
|
|
39
|
+
sessionStore;
|
|
37
40
|
agentExecutor = null;
|
|
38
41
|
a2aRequestHandler = null;
|
|
39
42
|
agentCard = null;
|
|
@@ -51,6 +54,9 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
51
54
|
this.taskTracker = new tracker_1.TaskTracker({
|
|
52
55
|
filePath: node_path_1.default.join(multiclawsStateDir, "tasks.json"),
|
|
53
56
|
});
|
|
57
|
+
this.sessionStore = new session_store_1.SessionStore({
|
|
58
|
+
filePath: node_path_1.default.join(multiclawsStateDir, "sessions.json"),
|
|
59
|
+
});
|
|
54
60
|
const port = options.port ?? 3100;
|
|
55
61
|
// selfUrl resolved later in start() after Tailscale detection; use placeholder for now
|
|
56
62
|
this.selfUrl = options.selfUrl ?? `http://${getLocalIp()}:${port}`;
|
|
@@ -194,37 +200,208 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
194
200
|
/* ---------------------------------------------------------------- */
|
|
195
201
|
/* Task delegation */
|
|
196
202
|
/* ---------------------------------------------------------------- */
|
|
197
|
-
|
|
203
|
+
/* ---------------------------------------------------------------- */
|
|
204
|
+
/* Session management (multi-turn collaboration) */
|
|
205
|
+
/* ---------------------------------------------------------------- */
|
|
206
|
+
async startSession(params) {
|
|
198
207
|
const agentRecord = await this.agentRegistry.get(params.agentUrl);
|
|
199
208
|
if (!agentRecord) {
|
|
200
|
-
return { status: "failed", error: `unknown agent: ${params.agentUrl}` };
|
|
209
|
+
return { sessionId: "", status: "failed", error: `unknown agent: ${params.agentUrl}` };
|
|
201
210
|
}
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
211
|
+
const contextId = (0, node_crypto_1.randomUUID)();
|
|
212
|
+
const session = this.sessionStore.create({
|
|
213
|
+
agentUrl: params.agentUrl,
|
|
214
|
+
agentName: agentRecord.name,
|
|
215
|
+
contextId,
|
|
216
|
+
});
|
|
217
|
+
this.sessionStore.appendMessage(session.sessionId, {
|
|
218
|
+
role: "user",
|
|
219
|
+
content: params.message,
|
|
220
|
+
timestampMs: Date.now(),
|
|
206
221
|
});
|
|
207
|
-
this.
|
|
222
|
+
void this.runSession({
|
|
223
|
+
sessionId: session.sessionId,
|
|
224
|
+
agentRecord,
|
|
225
|
+
message: params.message,
|
|
226
|
+
contextId,
|
|
227
|
+
taskId: undefined,
|
|
228
|
+
});
|
|
229
|
+
return { sessionId: session.sessionId, status: "running" };
|
|
230
|
+
}
|
|
231
|
+
async sendSessionMessage(params) {
|
|
232
|
+
const session = this.sessionStore.get(params.sessionId);
|
|
233
|
+
if (!session) {
|
|
234
|
+
return { sessionId: params.sessionId, status: "failed", error: "session not found" };
|
|
235
|
+
}
|
|
236
|
+
if (session.status !== "input-required" && session.status !== "active") {
|
|
237
|
+
return { sessionId: params.sessionId, status: "failed", error: `session is ${session.status}, cannot send message` };
|
|
238
|
+
}
|
|
239
|
+
this.sessionStore.appendMessage(params.sessionId, {
|
|
240
|
+
role: "user",
|
|
241
|
+
content: params.message,
|
|
242
|
+
timestampMs: Date.now(),
|
|
243
|
+
});
|
|
244
|
+
this.sessionStore.update(params.sessionId, { status: "active" });
|
|
245
|
+
const agentRecord = await this.agentRegistry.get(session.agentUrl);
|
|
246
|
+
if (!agentRecord) {
|
|
247
|
+
this.sessionStore.update(params.sessionId, { status: "failed", error: "agent no longer registered" });
|
|
248
|
+
return { sessionId: params.sessionId, status: "failed", error: "agent no longer registered" };
|
|
249
|
+
}
|
|
250
|
+
void this.runSession({
|
|
251
|
+
sessionId: params.sessionId,
|
|
252
|
+
agentRecord,
|
|
253
|
+
message: params.message,
|
|
254
|
+
contextId: session.contextId,
|
|
255
|
+
taskId: session.currentTaskId,
|
|
256
|
+
});
|
|
257
|
+
return { sessionId: params.sessionId, status: "ok" };
|
|
258
|
+
}
|
|
259
|
+
getSession(sessionId) {
|
|
260
|
+
return this.sessionStore.get(sessionId);
|
|
261
|
+
}
|
|
262
|
+
listSessions() {
|
|
263
|
+
return this.sessionStore.list();
|
|
264
|
+
}
|
|
265
|
+
endSession(sessionId) {
|
|
266
|
+
const session = this.sessionStore.get(sessionId);
|
|
267
|
+
if (!session)
|
|
268
|
+
return false;
|
|
269
|
+
this.sessionStore.update(sessionId, { status: "canceled" });
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
async runSession(params) {
|
|
273
|
+
const timeout = params.timeoutMs ?? 5 * 60 * 1000; // 5 min default
|
|
274
|
+
const timeoutController = new AbortController();
|
|
275
|
+
const timer = setTimeout(() => timeoutController.abort(), timeout);
|
|
208
276
|
try {
|
|
209
|
-
const client = await this.createA2AClient(agentRecord);
|
|
210
|
-
const result = await
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
277
|
+
const client = await this.createA2AClient(params.agentRecord);
|
|
278
|
+
const result = await Promise.race([
|
|
279
|
+
client.sendMessage({
|
|
280
|
+
message: {
|
|
281
|
+
kind: "message",
|
|
282
|
+
role: "user",
|
|
283
|
+
parts: [{ kind: "text", text: params.message }],
|
|
284
|
+
messageId: (0, node_crypto_1.randomUUID)(),
|
|
285
|
+
contextId: params.contextId,
|
|
286
|
+
...(params.taskId ? { taskId: params.taskId } : {}),
|
|
287
|
+
},
|
|
288
|
+
}),
|
|
289
|
+
new Promise((_, reject) => timeoutController.signal.addEventListener("abort", () => reject(new Error("session timeout")))),
|
|
290
|
+
]);
|
|
291
|
+
await this.handleSessionResult(params.sessionId, result);
|
|
219
292
|
}
|
|
220
293
|
catch (err) {
|
|
221
294
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
222
|
-
this.
|
|
223
|
-
|
|
295
|
+
this.sessionStore.update(params.sessionId, { status: "failed", error: errorMsg });
|
|
296
|
+
await this.notifySessionUpdate(params.sessionId, "failed");
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
clearTimeout(timer);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async handleSessionResult(sessionId, result) {
|
|
303
|
+
// Extract content
|
|
304
|
+
let content = "";
|
|
305
|
+
let state = "completed";
|
|
306
|
+
let remoteTaskId;
|
|
307
|
+
if ("status" in result && result.status) {
|
|
308
|
+
const task = result;
|
|
309
|
+
state = task.status?.state ?? "completed";
|
|
310
|
+
remoteTaskId = task.id;
|
|
311
|
+
content = this.extractArtifactText(task);
|
|
312
|
+
// Also try to get text from task messages if artifacts empty
|
|
313
|
+
if (!content && task.history?.length) {
|
|
314
|
+
const lastAgentMsg = [...task.history].reverse().find((m) => m.role === "agent");
|
|
315
|
+
if (lastAgentMsg) {
|
|
316
|
+
content = lastAgentMsg.parts
|
|
317
|
+
?.filter((p) => p.kind === "text")
|
|
318
|
+
.map((p) => p.text)
|
|
319
|
+
.join("\n") ?? "";
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
const msg = result;
|
|
325
|
+
remoteTaskId = msg.taskId;
|
|
326
|
+
content = msg.parts
|
|
327
|
+
?.filter((p) => p.kind === "text")
|
|
328
|
+
.map((p) => p.text)
|
|
329
|
+
.join("\n") ?? "";
|
|
330
|
+
}
|
|
331
|
+
// Append agent message to history
|
|
332
|
+
if (content) {
|
|
333
|
+
this.sessionStore.appendMessage(sessionId, {
|
|
334
|
+
role: "agent",
|
|
335
|
+
content,
|
|
336
|
+
timestampMs: Date.now(),
|
|
337
|
+
taskId: remoteTaskId,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
// Update session state
|
|
341
|
+
if (state === "input-required" || state === "auth-required") {
|
|
342
|
+
this.sessionStore.update(sessionId, {
|
|
343
|
+
status: "input-required",
|
|
344
|
+
currentTaskId: remoteTaskId,
|
|
345
|
+
});
|
|
346
|
+
await this.notifySessionUpdate(sessionId, "input-required");
|
|
347
|
+
}
|
|
348
|
+
else if (state === "failed" || state === "rejected") {
|
|
349
|
+
this.sessionStore.update(sessionId, {
|
|
350
|
+
status: "failed",
|
|
351
|
+
currentTaskId: remoteTaskId,
|
|
352
|
+
error: content || "remote task failed",
|
|
353
|
+
});
|
|
354
|
+
await this.notifySessionUpdate(sessionId, "failed");
|
|
355
|
+
}
|
|
356
|
+
else if (state === "completed" || state === "canceled") {
|
|
357
|
+
this.sessionStore.update(sessionId, {
|
|
358
|
+
status: "completed",
|
|
359
|
+
currentTaskId: remoteTaskId,
|
|
360
|
+
});
|
|
361
|
+
await this.notifySessionUpdate(sessionId, "completed");
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
// working / submitted / unknown — still in progress, no notification
|
|
365
|
+
this.sessionStore.update(sessionId, { currentTaskId: remoteTaskId });
|
|
224
366
|
}
|
|
225
367
|
}
|
|
226
|
-
|
|
227
|
-
|
|
368
|
+
async notifySessionUpdate(sessionId, event) {
|
|
369
|
+
if (!this.options.gatewayConfig)
|
|
370
|
+
return;
|
|
371
|
+
const session = this.sessionStore.get(sessionId);
|
|
372
|
+
if (!session)
|
|
373
|
+
return;
|
|
374
|
+
const lastAgentMsg = [...session.messages].reverse().find((m) => m.role === "agent");
|
|
375
|
+
const content = lastAgentMsg?.content ?? "";
|
|
376
|
+
const agentName = session.agentName;
|
|
377
|
+
let message;
|
|
378
|
+
if (event === "completed") {
|
|
379
|
+
message = [`✅ **${agentName} 任务完成** (session: \`${sessionId}\`)`, "", content].join("\n");
|
|
380
|
+
}
|
|
381
|
+
else if (event === "input-required") {
|
|
382
|
+
message = [
|
|
383
|
+
`📨 **${agentName} 需要补充信息** (session: \`${sessionId}\`)`,
|
|
384
|
+
"",
|
|
385
|
+
content,
|
|
386
|
+
"",
|
|
387
|
+
`→ 回复请用 \`multiclaws_session_reply\` 工具,sessionId: \`${sessionId}\``,
|
|
388
|
+
].join("\n");
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
const error = session.error ?? content;
|
|
392
|
+
message = [`❌ **${agentName} 任务失败** (session: \`${sessionId}\`)`, "", error].join("\n");
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
await (0, gateway_client_1.invokeGatewayTool)({
|
|
396
|
+
gateway: this.options.gatewayConfig,
|
|
397
|
+
tool: "message",
|
|
398
|
+
args: { action: "send", message },
|
|
399
|
+
timeoutMs: 5_000,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
this.log("warn", `[multiclaws] failed to notify session update: ${sessionId}`);
|
|
404
|
+
}
|
|
228
405
|
}
|
|
229
406
|
/* ---------------------------------------------------------------- */
|
|
230
407
|
/* Profile */
|
|
@@ -602,27 +779,6 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
602
779
|
async createA2AClient(agent) {
|
|
603
780
|
return await this.clientFactory.createFromUrl(agent.url);
|
|
604
781
|
}
|
|
605
|
-
processTaskResult(trackId, result) {
|
|
606
|
-
if ("status" in result && result.status) {
|
|
607
|
-
const task = result;
|
|
608
|
-
const state = task.status?.state ?? "unknown";
|
|
609
|
-
const output = this.extractArtifactText(task);
|
|
610
|
-
if (state === "completed") {
|
|
611
|
-
this.taskTracker.update(trackId, { status: "completed", result: output });
|
|
612
|
-
}
|
|
613
|
-
else if (state === "failed") {
|
|
614
|
-
this.taskTracker.update(trackId, { status: "failed", error: output || "remote task failed" });
|
|
615
|
-
}
|
|
616
|
-
return { taskId: task.id, output, status: state };
|
|
617
|
-
}
|
|
618
|
-
const msg = result;
|
|
619
|
-
const text = msg.parts
|
|
620
|
-
?.filter((p) => p.kind === "text")
|
|
621
|
-
.map((p) => p.text)
|
|
622
|
-
.join("\n") ?? "";
|
|
623
|
-
this.taskTracker.update(trackId, { status: "completed", result: text });
|
|
624
|
-
return { taskId: trackId, output: text, status: "completed" };
|
|
625
|
-
}
|
|
626
782
|
extractArtifactText(task) {
|
|
627
783
|
if (!task.artifacts?.length)
|
|
628
784
|
return "";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type SessionStatus = "active" | "input-required" | "completed" | "failed" | "canceled";
|
|
2
|
+
export type SessionMessage = {
|
|
3
|
+
role: "user" | "agent";
|
|
4
|
+
content: string;
|
|
5
|
+
timestampMs: number;
|
|
6
|
+
taskId?: string;
|
|
7
|
+
};
|
|
8
|
+
export type ConversationSession = {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
agentUrl: string;
|
|
11
|
+
agentName: string;
|
|
12
|
+
contextId: string;
|
|
13
|
+
currentTaskId?: string;
|
|
14
|
+
status: SessionStatus;
|
|
15
|
+
messages: SessionMessage[];
|
|
16
|
+
createdAtMs: number;
|
|
17
|
+
updatedAtMs: number;
|
|
18
|
+
error?: string;
|
|
19
|
+
};
|
|
20
|
+
export declare class SessionStore {
|
|
21
|
+
private readonly filePath;
|
|
22
|
+
private readonly ttlMs;
|
|
23
|
+
private store;
|
|
24
|
+
private persistPending;
|
|
25
|
+
constructor(opts: {
|
|
26
|
+
filePath: string;
|
|
27
|
+
ttlMs?: number;
|
|
28
|
+
});
|
|
29
|
+
create(params: {
|
|
30
|
+
agentUrl: string;
|
|
31
|
+
agentName: string;
|
|
32
|
+
contextId: string;
|
|
33
|
+
}): ConversationSession;
|
|
34
|
+
get(sessionId: string): ConversationSession | null;
|
|
35
|
+
list(): ConversationSession[];
|
|
36
|
+
update(sessionId: string, patch: Partial<Omit<ConversationSession, "sessionId" | "createdAtMs">>): ConversationSession | null;
|
|
37
|
+
appendMessage(sessionId: string, msg: SessionMessage): ConversationSession | null;
|
|
38
|
+
private loadSync;
|
|
39
|
+
private schedulePersist;
|
|
40
|
+
private persistAsync;
|
|
41
|
+
private prune;
|
|
42
|
+
private evictOldest;
|
|
43
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SessionStore = void 0;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
12
|
+
const MAX_SESSIONS = 1_000;
|
|
13
|
+
function emptyStore() {
|
|
14
|
+
return { version: 1, sessions: [] };
|
|
15
|
+
}
|
|
16
|
+
class SessionStore {
|
|
17
|
+
filePath;
|
|
18
|
+
ttlMs;
|
|
19
|
+
store;
|
|
20
|
+
persistPending = false;
|
|
21
|
+
constructor(opts) {
|
|
22
|
+
this.filePath = opts.filePath;
|
|
23
|
+
this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
24
|
+
this.store = this.loadSync();
|
|
25
|
+
}
|
|
26
|
+
create(params) {
|
|
27
|
+
this.prune();
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
const session = {
|
|
30
|
+
sessionId: (0, node_crypto_1.randomUUID)(),
|
|
31
|
+
agentUrl: params.agentUrl,
|
|
32
|
+
agentName: params.agentName,
|
|
33
|
+
contextId: params.contextId,
|
|
34
|
+
status: "active",
|
|
35
|
+
messages: [],
|
|
36
|
+
createdAtMs: now,
|
|
37
|
+
updatedAtMs: now,
|
|
38
|
+
};
|
|
39
|
+
if (this.store.sessions.length >= MAX_SESSIONS) {
|
|
40
|
+
this.evictOldest();
|
|
41
|
+
}
|
|
42
|
+
this.store.sessions.push(session);
|
|
43
|
+
this.schedulePersist();
|
|
44
|
+
return session;
|
|
45
|
+
}
|
|
46
|
+
get(sessionId) {
|
|
47
|
+
return this.store.sessions.find((s) => s.sessionId === sessionId) ?? null;
|
|
48
|
+
}
|
|
49
|
+
list() {
|
|
50
|
+
return [...this.store.sessions].sort((a, b) => b.updatedAtMs - a.updatedAtMs);
|
|
51
|
+
}
|
|
52
|
+
update(sessionId, patch) {
|
|
53
|
+
const idx = this.store.sessions.findIndex((s) => s.sessionId === sessionId);
|
|
54
|
+
if (idx < 0)
|
|
55
|
+
return null;
|
|
56
|
+
const next = {
|
|
57
|
+
...this.store.sessions[idx],
|
|
58
|
+
...patch,
|
|
59
|
+
updatedAtMs: Date.now(),
|
|
60
|
+
};
|
|
61
|
+
this.store.sessions[idx] = next;
|
|
62
|
+
this.schedulePersist();
|
|
63
|
+
return next;
|
|
64
|
+
}
|
|
65
|
+
appendMessage(sessionId, msg) {
|
|
66
|
+
const session = this.get(sessionId);
|
|
67
|
+
if (!session)
|
|
68
|
+
return null;
|
|
69
|
+
return this.update(sessionId, {
|
|
70
|
+
messages: [...session.messages, msg],
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
loadSync() {
|
|
74
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(this.filePath), { recursive: true });
|
|
75
|
+
try {
|
|
76
|
+
const raw = JSON.parse(node_fs_1.default.readFileSync(this.filePath, "utf8"));
|
|
77
|
+
if (raw.version !== 1 || !Array.isArray(raw.sessions))
|
|
78
|
+
return emptyStore();
|
|
79
|
+
return raw;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
const store = emptyStore();
|
|
83
|
+
node_fs_1.default.writeFileSync(this.filePath, JSON.stringify(store, null, 2), "utf8");
|
|
84
|
+
return store;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
schedulePersist() {
|
|
88
|
+
if (this.persistPending)
|
|
89
|
+
return;
|
|
90
|
+
this.persistPending = true;
|
|
91
|
+
queueMicrotask(() => {
|
|
92
|
+
this.persistPending = false;
|
|
93
|
+
void this.persistAsync();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async persistAsync() {
|
|
97
|
+
try {
|
|
98
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(this.filePath), { recursive: true });
|
|
99
|
+
const tmp = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
100
|
+
await promises_1.default.writeFile(tmp, JSON.stringify(this.store, null, 2), "utf8");
|
|
101
|
+
await promises_1.default.rename(tmp, this.filePath);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// best-effort
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
prune() {
|
|
108
|
+
const cutoff = Date.now() - this.ttlMs;
|
|
109
|
+
this.store.sessions = this.store.sessions.filter((s) => {
|
|
110
|
+
if (s.updatedAtMs >= cutoff)
|
|
111
|
+
return true;
|
|
112
|
+
return s.status !== "completed" && s.status !== "failed" && s.status !== "canceled";
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
evictOldest() {
|
|
116
|
+
const removable = [...this.store.sessions]
|
|
117
|
+
.filter((s) => s.status === "completed" || s.status === "failed" || s.status === "canceled")
|
|
118
|
+
.sort((a, b) => a.updatedAtMs - b.updatedAtMs)
|
|
119
|
+
.slice(0, Math.max(1, Math.floor(MAX_SESSIONS / 4)));
|
|
120
|
+
const ids = new Set(removable.map((s) => s.sessionId));
|
|
121
|
+
this.store.sessions = this.store.sessions.filter((s) => !ids.has(s.sessionId));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.SessionStore = SessionStore;
|
package/package.json
CHANGED
|
@@ -1,87 +1,111 @@
|
|
|
1
1
|
# MultiClaws — 技能行为
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
MultiClaws 让多个 OpenClaw 实例通过 [A2A(Agent-to-Agent)](https://google.github.io/A2A/) 协议组成团队、互相委派任务。
|
|
3
|
+
MultiClaws 让多个 OpenClaw 实例通过 [A2A(Agent-to-Agent)](https://google.github.io/A2A/) 协议组成团队、互相委派和协作任务。
|
|
6
4
|
|
|
7
5
|
---
|
|
8
6
|
|
|
9
7
|
## 1. 首次安装:档案初始化(由插件 hook 触发)
|
|
10
8
|
|
|
11
9
|
首次安装后,插件会通过 `before_prompt_build` hook 自动在系统提示中注入初始化任务。
|
|
12
|
-
**无需每次对话手动检查 `multiclaws_profile_pending_review()`**,hook 已处理触发时机。
|
|
13
10
|
|
|
14
11
|
当 hook 注入了初始化任务时,按以下步骤执行:
|
|
15
12
|
|
|
16
|
-
1. **扫描当前环境**,自动生成 bio(markdown
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
|
|
13
|
+
1. **扫描当前环境**,自动生成 bio(markdown 格式)
|
|
14
|
+
2. 向用户展示,逐一确认:
|
|
15
|
+
- **名字**:询问是否需要修改(需用户明确回答)
|
|
16
|
+
- **Bio**:询问是否需要修改(需用户明确回答)
|
|
17
|
+
- **网络情况**:告知「同局域网开箱即用;跨网络需安装 Tailscale」,无需用户回答
|
|
18
|
+
3. 调用 `multiclaws_profile_set(ownerName="...", bio="...")` 保存
|
|
19
|
+
4. 调用 `multiclaws_profile_clear_pending_review()` 完成初始化
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
- **名字**:展示推断出的名字,询问是否需要修改(需用户明确回答)
|
|
25
|
-
- **Bio**:展示生成的 bio,询问是否需要修改(需用户明确回答)
|
|
26
|
-
- **网络情况**:告知用户「同局域网开箱即用;跨网络需安装 Tailscale(https://tailscale.com/download)并重启 OpenClaw」,无需用户回答
|
|
21
|
+
---
|
|
27
22
|
|
|
28
|
-
|
|
23
|
+
## 2. 协作任务(Session)
|
|
29
24
|
|
|
30
|
-
|
|
25
|
+
**所有委派任务均通过 session 进行**,支持单轮和多轮场景。
|
|
31
26
|
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
|
|
27
|
+
### 开始协作
|
|
28
|
+
```
|
|
29
|
+
multiclaws_session_start(agentUrl="...", message="任务描述")
|
|
30
|
+
→ 立即返回 sessionId,任务在后台运行
|
|
31
|
+
→ 完成后自动推送消息通知
|
|
32
|
+
```
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
### 远端需要补充信息(input-required)
|
|
35
|
+
收到 `📨 AgentName 需要补充信息` 通知后:
|
|
36
|
+
```
|
|
37
|
+
multiclaws_session_reply(sessionId="...", message="补充内容")
|
|
38
|
+
→ 继续会话,后台处理,完成后推送通知
|
|
39
|
+
```
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
### 查看会话状态
|
|
42
|
+
```
|
|
43
|
+
multiclaws_session_status() → 列出所有会话
|
|
44
|
+
multiclaws_session_status(sessionId="...") → 查看单个会话及消息历史
|
|
45
|
+
```
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
### 结束会话
|
|
48
|
+
```
|
|
49
|
+
multiclaws_session_end(sessionId="...") → 取消并关闭会话
|
|
47
50
|
```
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
### 并发协作
|
|
53
|
+
可同时开启多个 session,各自独立运行:
|
|
54
|
+
```
|
|
55
|
+
multiclaws_session_start(agentUrl=B, message="任务1") → sessionId_1
|
|
56
|
+
multiclaws_session_start(agentUrl=C, message="任务2") → sessionId_2
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 链式协作(A→B→C)
|
|
60
|
+
B 内部可以自己调用 `multiclaws_session_start` 委派给 C,结果自然冒泡回 A。
|
|
50
61
|
|
|
51
|
-
|
|
62
|
+
---
|
|
52
63
|
|
|
53
|
-
|
|
64
|
+
## 3. 智能委派流程
|
|
54
65
|
|
|
55
66
|
```
|
|
56
|
-
|
|
67
|
+
1. multiclaws_team_members() → 列出所有成员,读 bio
|
|
68
|
+
2. 选择 bio 最匹配任务的 agent
|
|
69
|
+
3. multiclaws_session_start(agentUrl, message)
|
|
70
|
+
4. 等待推送通知(或用 session_status 查进度)
|
|
71
|
+
5. 如收到 input-required 通知 → multiclaws_session_reply 回复
|
|
57
72
|
```
|
|
58
73
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
4. 然后继续团队操作
|
|
74
|
+
**选择 agent 原则:**
|
|
75
|
+
- 匹配任务领域(「财务报告」→ 有财务技能的 agent)
|
|
76
|
+
- 匹配数据访问(「检查代码」→ bio 中有该代码库的 agent)
|
|
77
|
+
- 多个匹配时选最具体的
|
|
64
78
|
|
|
65
79
|
---
|
|
66
80
|
|
|
67
|
-
##
|
|
81
|
+
## 4. 团队操作前检查档案
|
|
68
82
|
|
|
69
|
-
|
|
70
|
-
- 用户连接了新渠道或数据源
|
|
71
|
-
- 用户安装了新 skill 或插件
|
|
72
|
-
- 用户的角色或关注点发生变化
|
|
83
|
+
在创建或加入团队之前:
|
|
73
84
|
|
|
74
|
-
|
|
85
|
+
```
|
|
86
|
+
multiclaws_profile_show()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
如果 `bio` 为空或 `ownerName` 为空,先完成档案设置再继续。
|
|
75
90
|
|
|
76
91
|
---
|
|
77
92
|
|
|
78
93
|
## 工具列表
|
|
79
94
|
|
|
95
|
+
### 协作 Session
|
|
96
|
+
|
|
97
|
+
| 工具 | 说明 | 参数 |
|
|
98
|
+
|------|------|------|
|
|
99
|
+
| `multiclaws_session_start` | 开始协作会话(替代旧 delegate) | `agentUrl`, `message` |
|
|
100
|
+
| `multiclaws_session_reply` | 在会话中发送后续消息 | `sessionId`, `message` |
|
|
101
|
+
| `multiclaws_session_status` | 查看会话状态和消息历史 | `sessionId`(可选,不传返回全部) |
|
|
102
|
+
| `multiclaws_session_end` | 取消/关闭会话 | `sessionId` |
|
|
103
|
+
|
|
80
104
|
### 档案
|
|
81
105
|
|
|
82
106
|
| 工具 | 说明 | 参数 |
|
|
83
107
|
|------|------|------|
|
|
84
|
-
| `multiclaws_profile_set` | 设置名字和 bio | `ownerName`(可选), `bio
|
|
108
|
+
| `multiclaws_profile_set` | 设置名字和 bio | `ownerName`(可选), `bio`(可选) |
|
|
85
109
|
| `multiclaws_profile_show` | 查看当前档案 | — |
|
|
86
110
|
| `multiclaws_profile_pending_review` | 检查是否有待确认的首次档案 | — |
|
|
87
111
|
| `multiclaws_profile_clear_pending_review` | 清除待确认标记 | — |
|
|
@@ -93,75 +117,32 @@ multiclaws_profile_show()
|
|
|
93
117
|
| `multiclaws_team_create` | 创建团队,返回邀请码 | `name` |
|
|
94
118
|
| `multiclaws_team_join` | 用邀请码加入团队 | `inviteCode` |
|
|
95
119
|
| `multiclaws_team_leave` | 离开团队 | `teamId`(可选) |
|
|
96
|
-
| `multiclaws_team_members` |
|
|
120
|
+
| `multiclaws_team_members` | 列出所有团队和成员 | `teamId`(可选) |
|
|
97
121
|
|
|
98
|
-
###
|
|
122
|
+
### 智能体
|
|
99
123
|
|
|
100
124
|
| 工具 | 说明 | 参数 |
|
|
101
125
|
|------|------|------|
|
|
102
|
-
| `multiclaws_agents` |
|
|
103
|
-
| `multiclaws_add_agent` |
|
|
104
|
-
| `multiclaws_remove_agent` |
|
|
105
|
-
| `multiclaws_delegate` | 委派任务给远端智能体 | `agentUrl`, `task` |
|
|
106
|
-
| `multiclaws_task_status` | 查看委派任务状态 | `taskId` |
|
|
126
|
+
| `multiclaws_agents` | 列出已知 agent 及 bio | — |
|
|
127
|
+
| `multiclaws_add_agent` | 手动添加 agent | `url`, `apiKey`(可选) |
|
|
128
|
+
| `multiclaws_remove_agent` | 移除 agent | `url` |
|
|
107
129
|
|
|
108
130
|
---
|
|
109
131
|
|
|
110
132
|
## 重要规则
|
|
111
133
|
|
|
112
|
-
- **不要问用户 IP
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
- **名字和 bio 必须用户明确确认**;网络情况仅告知,无需用户回答。
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## 工作流
|
|
121
|
-
|
|
122
|
-
### 创建团队
|
|
123
|
-
|
|
124
|
-
```
|
|
125
|
-
1. multiclaws_profile_show() — 检查档案
|
|
126
|
-
2.(如果为空)自动生成并设置 bio,确认名字和 bio
|
|
127
|
-
3. multiclaws_team_create(name="...") — 返回 inviteCode (mc:xxxx)
|
|
128
|
-
4. 告诉用户把邀请码分享给队友
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### 加入团队
|
|
132
|
-
|
|
133
|
-
```
|
|
134
|
-
1. multiclaws_profile_show() — 检查档案
|
|
135
|
-
2.(如果为空)自动生成并设置 bio,确认名字和 bio
|
|
136
|
-
3. multiclaws_team_join(inviteCode="mc:xxxx")
|
|
137
|
-
→ 自动同步所有团队成员
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### 智能委派
|
|
141
|
-
|
|
142
|
-
```
|
|
143
|
-
1. multiclaws_agents() — 列出智能体,读 bio
|
|
144
|
-
2. 选择 bio 最匹配任务的智能体
|
|
145
|
-
3. multiclaws_delegate(agentUrl="...", task="...")
|
|
146
|
-
4. 把结果返回给用户
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
选择智能体时:
|
|
150
|
-
- 匹配任务领域和 bio(如「财务报告」→ 有财务技能的智能体)
|
|
151
|
-
- 匹配数据需求(如「检查 API 代码」→ bio 中有该代码库的智能体)
|
|
152
|
-
- 多个匹配时选最具体的
|
|
134
|
+
- **不要问用户 IP 或 selfUrl**,插件自动处理
|
|
135
|
+
- **Bio 是 markdown**,写得让另一个 AI 能读懂这个 agent 能做什么
|
|
136
|
+
- **名字和 bio 必须用户明确确认**;网络情况仅告知
|
|
137
|
+
- **session 超时默认 5 分钟**,超时后 status 变 failed 并推送通知
|
|
153
138
|
|
|
154
139
|
---
|
|
155
140
|
|
|
156
141
|
## 跨网络
|
|
157
142
|
|
|
158
|
-
**同局域网:**
|
|
159
|
-
|
|
160
|
-
**不同网络:** 每人安装 [Tailscale](https://tailscale.com/download) 并登录同一 tailnet,插件自动检测 Tailscale IP。
|
|
143
|
+
**同局域网:** 开箱即用。
|
|
161
144
|
|
|
162
|
-
|
|
145
|
+
**不同网络:** 安装 [Tailscale](https://tailscale.com/download) 并登录同一 tailnet,插件自动检测。
|
|
163
146
|
|
|
164
|
-
|
|
165
|
-
>
|
|
166
|
-
> 2. 登录同一个 Tailscale 账号(或同一 tailnet)
|
|
167
|
-
> 3. 重启 OpenClaw,插件会自动检测 Tailscale IP
|
|
147
|
+
网络错误时引导用户:
|
|
148
|
+
> 跨网络需要安装 Tailscale:https://tailscale.com/download,登录后重启 OpenClaw。
|