multiclaws 0.4.6 → 0.4.7
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 +41 -91
- package/dist/index.js +58 -156
- package/dist/infra/gateway-client.js +0 -10
- package/dist/infra/tailscale.d.ts +1 -1
- package/dist/infra/tailscale.js +5 -14
- package/dist/service/a2a-adapter.d.ts +0 -1
- package/dist/service/a2a-adapter.js +10 -56
- package/dist/service/agent-registry.d.ts +0 -9
- package/dist/service/agent-registry.js +1 -45
- package/dist/service/multiclaws-service.d.ts +13 -43
- package/dist/service/multiclaws-service.js +72 -376
- package/package.json +1 -1
- package/skills/multiclaws/SKILL.md +100 -89
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OpenClawAgentExecutor = void 0;
|
|
4
|
-
const node_crypto_1 = require("node:crypto");
|
|
5
4
|
const gateway_client_1 = require("../infra/gateway-client");
|
|
6
5
|
function extractTextFromMessage(message) {
|
|
7
6
|
if (!message.parts)
|
|
@@ -11,34 +10,6 @@ function extractTextFromMessage(message) {
|
|
|
11
10
|
.map((p) => p.text)
|
|
12
11
|
.join("\n");
|
|
13
12
|
}
|
|
14
|
-
function buildTaskWithHistory(context) {
|
|
15
|
-
const currentText = extractTextFromMessage(context.userMessage);
|
|
16
|
-
const history = context.task?.history ?? [];
|
|
17
|
-
if (history.length <= 1) {
|
|
18
|
-
// First message — no prior context
|
|
19
|
-
return currentText;
|
|
20
|
-
}
|
|
21
|
-
// Build context from previous exchanges (exclude the last message, that's currentText)
|
|
22
|
-
const prior = history
|
|
23
|
-
.slice(0, -1)
|
|
24
|
-
.slice(-8) // keep last 8 messages max to avoid huge prompts
|
|
25
|
-
.map((m) => {
|
|
26
|
-
const text = extractTextFromMessage(m);
|
|
27
|
-
const role = m.role === "agent" ? "agent" : "user";
|
|
28
|
-
return `[${role}]: ${text}`;
|
|
29
|
-
})
|
|
30
|
-
.filter((line) => line.length > 10)
|
|
31
|
-
.join("\n");
|
|
32
|
-
if (!prior)
|
|
33
|
-
return currentText;
|
|
34
|
-
return [
|
|
35
|
-
"[conversation history]",
|
|
36
|
-
prior,
|
|
37
|
-
"",
|
|
38
|
-
"[latest message]",
|
|
39
|
-
currentText,
|
|
40
|
-
].join("\n");
|
|
41
|
-
}
|
|
42
13
|
function sleep(ms) {
|
|
43
14
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
15
|
}
|
|
@@ -71,15 +42,13 @@ class OpenClawAgentExecutor {
|
|
|
71
42
|
gatewayConfig;
|
|
72
43
|
taskTracker;
|
|
73
44
|
logger;
|
|
74
|
-
// Map A2A task IDs → internal tracker IDs so cancelTask can find the right record
|
|
75
|
-
a2aToTracker = new Map();
|
|
76
45
|
constructor(options) {
|
|
77
46
|
this.gatewayConfig = options.gatewayConfig;
|
|
78
47
|
this.taskTracker = options.taskTracker;
|
|
79
48
|
this.logger = options.logger;
|
|
80
49
|
}
|
|
81
50
|
async execute(context, eventBus) {
|
|
82
|
-
const taskText =
|
|
51
|
+
const taskText = extractTextFromMessage(context.userMessage);
|
|
83
52
|
const taskId = context.taskId;
|
|
84
53
|
if (!taskText.trim()) {
|
|
85
54
|
this.publishMessage(eventBus, "Error: empty task received.");
|
|
@@ -87,17 +56,14 @@ class OpenClawAgentExecutor {
|
|
|
87
56
|
return;
|
|
88
57
|
}
|
|
89
58
|
const fromAgent = context.userMessage.metadata?.agentUrl ?? "unknown";
|
|
90
|
-
|
|
59
|
+
this.taskTracker.create({
|
|
91
60
|
fromPeerId: fromAgent,
|
|
92
61
|
toPeerId: "local",
|
|
93
62
|
task: taskText,
|
|
94
63
|
});
|
|
95
|
-
const trackedId = tracked.taskId;
|
|
96
|
-
this.a2aToTracker.set(taskId, trackedId);
|
|
97
64
|
if (!this.gatewayConfig) {
|
|
98
65
|
this.logger.error("[a2a-adapter] gateway config not available, cannot execute task");
|
|
99
|
-
this.taskTracker.update(
|
|
100
|
-
this.a2aToTracker.delete(taskId);
|
|
66
|
+
this.taskTracker.update(taskId, { status: "failed", error: "gateway config not available" });
|
|
101
67
|
this.publishMessage(eventBus, "Error: gateway config not available, cannot execute task.");
|
|
102
68
|
eventBus.finished();
|
|
103
69
|
return;
|
|
@@ -124,19 +90,16 @@ class OpenClawAgentExecutor {
|
|
|
124
90
|
this.logger.info(`[a2a-adapter] task ${taskId} spawned as ${childSessionKey}, waiting for result...`);
|
|
125
91
|
const output = await this.waitForCompletion(childSessionKey, 180_000);
|
|
126
92
|
// 3. Return result
|
|
127
|
-
this.taskTracker.update(
|
|
93
|
+
this.taskTracker.update(taskId, { status: "completed", result: output });
|
|
128
94
|
this.logger.info(`[a2a-adapter] task ${taskId} completed`);
|
|
129
95
|
this.publishMessage(eventBus, output || "Task completed with no output.");
|
|
130
96
|
}
|
|
131
97
|
catch (err) {
|
|
132
98
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
133
99
|
this.logger.error(`[a2a-adapter] task execution failed: ${errorMsg}`);
|
|
134
|
-
this.taskTracker.update(
|
|
100
|
+
this.taskTracker.update(taskId, { status: "failed", error: errorMsg });
|
|
135
101
|
this.publishMessage(eventBus, `Error: ${errorMsg}`);
|
|
136
102
|
}
|
|
137
|
-
finally {
|
|
138
|
-
this.a2aToTracker.delete(taskId);
|
|
139
|
-
}
|
|
140
103
|
eventBus.finished();
|
|
141
104
|
}
|
|
142
105
|
/**
|
|
@@ -192,21 +155,14 @@ class OpenClawAgentExecutor {
|
|
|
192
155
|
const messages = (details.messages ?? []);
|
|
193
156
|
if (messages.length === 0)
|
|
194
157
|
return null;
|
|
195
|
-
// If no explicit flag,
|
|
196
|
-
// complete if the last message is an assistant message with text
|
|
197
|
-
// and NO tool calls (tool calls indicate ongoing work)
|
|
158
|
+
// If no explicit flag, check the last message for signs of ongoing execution
|
|
198
159
|
if (details.isComplete === undefined) {
|
|
199
160
|
const lastMsg = messages[messages.length - 1];
|
|
200
|
-
if (
|
|
201
|
-
return null;
|
|
202
|
-
if (Array.isArray(lastMsg.content)) {
|
|
161
|
+
if (lastMsg && Array.isArray(lastMsg.content)) {
|
|
203
162
|
const content = lastMsg.content;
|
|
204
163
|
const hasToolCalls = content.some((c) => c?.type === "toolCall" || c?.type === "tool_use");
|
|
205
|
-
// If there are ANY tool calls, assume still running
|
|
206
|
-
if (hasToolCalls)
|
|
207
|
-
return null;
|
|
208
164
|
const hasText = content.some((c) => c?.type === "text" && typeof c.text === "string" && c.text.trim());
|
|
209
|
-
if (!hasText)
|
|
165
|
+
if (hasToolCalls && !hasText)
|
|
210
166
|
return null;
|
|
211
167
|
}
|
|
212
168
|
}
|
|
@@ -232,9 +188,7 @@ class OpenClawAgentExecutor {
|
|
|
232
188
|
return null;
|
|
233
189
|
}
|
|
234
190
|
async cancelTask(taskId, eventBus) {
|
|
235
|
-
|
|
236
|
-
this.taskTracker.update(trackedId, { status: "failed", error: "canceled" });
|
|
237
|
-
this.a2aToTracker.delete(taskId);
|
|
191
|
+
this.taskTracker.update(taskId, { status: "failed", error: "canceled" });
|
|
238
192
|
this.publishMessage(eventBus, "Task was canceled.");
|
|
239
193
|
eventBus.finished();
|
|
240
194
|
}
|
|
@@ -245,7 +199,7 @@ class OpenClawAgentExecutor {
|
|
|
245
199
|
const message = {
|
|
246
200
|
kind: "message",
|
|
247
201
|
role: "agent",
|
|
248
|
-
messageId:
|
|
202
|
+
messageId: `msg-${Date.now()}`,
|
|
249
203
|
parts: [{ kind: "text", text }],
|
|
250
204
|
};
|
|
251
205
|
eventBus.publish(message);
|
|
@@ -6,8 +6,6 @@ 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[];
|
|
11
9
|
};
|
|
12
10
|
export declare class AgentRegistry {
|
|
13
11
|
private readonly filePath;
|
|
@@ -23,13 +21,6 @@ export declare class AgentRegistry {
|
|
|
23
21
|
remove(url: string): Promise<boolean>;
|
|
24
22
|
list(): Promise<AgentRecord[]>;
|
|
25
23
|
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>;
|
|
33
24
|
updateDescription(url: string, description: string): Promise<void>;
|
|
34
25
|
updateLastSeen(url: string): Promise<void>;
|
|
35
26
|
}
|
|
@@ -32,17 +32,14 @@ class AgentRegistry {
|
|
|
32
32
|
const normalizedUrl = params.url.replace(/\/+$/, "");
|
|
33
33
|
const existing = store.agents.findIndex((a) => a.url === normalizedUrl);
|
|
34
34
|
const now = Date.now();
|
|
35
|
-
const prev = existing >= 0 ? store.agents[existing] : null;
|
|
36
35
|
const record = {
|
|
37
36
|
url: normalizedUrl,
|
|
38
37
|
name: params.name,
|
|
39
38
|
description: params.description ?? "",
|
|
40
39
|
skills: params.skills ?? [],
|
|
41
40
|
apiKey: params.apiKey,
|
|
42
|
-
addedAtMs:
|
|
41
|
+
addedAtMs: existing >= 0 ? store.agents[existing].addedAtMs : now,
|
|
43
42
|
lastSeenAtMs: now,
|
|
44
|
-
// Preserve existing teamIds so that team associations are not lost on upsert
|
|
45
|
-
...(prev?.teamIds?.length ? { teamIds: prev.teamIds } : {}),
|
|
46
43
|
};
|
|
47
44
|
if (existing >= 0) {
|
|
48
45
|
store.agents[existing] = record;
|
|
@@ -76,47 +73,6 @@ class AgentRegistry {
|
|
|
76
73
|
const normalizedUrl = url.replace(/\/+$/, "");
|
|
77
74
|
return store.agents.find((a) => a.url === normalizedUrl) ?? null;
|
|
78
75
|
}
|
|
79
|
-
async addTeamSource(url, teamId) {
|
|
80
|
-
await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
|
|
81
|
-
const store = await this.readStore();
|
|
82
|
-
const normalizedUrl = url.replace(/\/+$/, "");
|
|
83
|
-
const agent = store.agents.find((a) => a.url === normalizedUrl);
|
|
84
|
-
if (agent) {
|
|
85
|
-
const teams = new Set(agent.teamIds ?? []);
|
|
86
|
-
teams.add(teamId);
|
|
87
|
-
agent.teamIds = [...teams];
|
|
88
|
-
await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Remove a team source from an agent. Returns true if the agent
|
|
94
|
-
* was fully removed (no remaining sources), false otherwise.
|
|
95
|
-
* Manually-added agents (no teamIds) are never removed by this method.
|
|
96
|
-
*/
|
|
97
|
-
async removeTeamSource(url, teamId) {
|
|
98
|
-
return await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
|
|
99
|
-
const store = await this.readStore();
|
|
100
|
-
const normalizedUrl = url.replace(/\/+$/, "");
|
|
101
|
-
const agent = store.agents.find((a) => a.url === normalizedUrl);
|
|
102
|
-
if (!agent)
|
|
103
|
-
return false;
|
|
104
|
-
// Agent was manually added (no team tracking) — never auto-remove
|
|
105
|
-
if (!agent.teamIds || agent.teamIds.length === 0)
|
|
106
|
-
return false;
|
|
107
|
-
const teams = new Set(agent.teamIds);
|
|
108
|
-
teams.delete(teamId);
|
|
109
|
-
if (teams.size === 0) {
|
|
110
|
-
// No team sources remain — remove entirely
|
|
111
|
-
store.agents = store.agents.filter((a) => a.url !== normalizedUrl);
|
|
112
|
-
await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
agent.teamIds = [...teams];
|
|
116
|
-
await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
|
|
117
|
-
return false;
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
76
|
async updateDescription(url, description) {
|
|
121
77
|
await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
|
|
122
78
|
const store = await this.readStore();
|
|
@@ -2,7 +2,6 @@ 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";
|
|
6
5
|
import type { GatewayConfig } from "../infra/gateway-client";
|
|
7
6
|
export type MulticlawsServiceOptions = {
|
|
8
7
|
stateDir: string;
|
|
@@ -17,14 +16,10 @@ export type MulticlawsServiceOptions = {
|
|
|
17
16
|
debug?: (message: string) => void;
|
|
18
17
|
};
|
|
19
18
|
};
|
|
20
|
-
export type
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
};
|
|
25
|
-
export type SessionReplyResult = {
|
|
26
|
-
sessionId: string;
|
|
27
|
-
status: "ok" | "failed";
|
|
19
|
+
export type DelegateTaskResult = {
|
|
20
|
+
taskId?: string;
|
|
21
|
+
output?: string;
|
|
22
|
+
status: string;
|
|
28
23
|
error?: string;
|
|
29
24
|
};
|
|
30
25
|
export declare class MulticlawsService extends EventEmitter {
|
|
@@ -35,9 +30,6 @@ export declare class MulticlawsService extends EventEmitter {
|
|
|
35
30
|
private readonly teamStore;
|
|
36
31
|
private readonly profileStore;
|
|
37
32
|
private readonly taskTracker;
|
|
38
|
-
private readonly sessionStore;
|
|
39
|
-
private readonly sessionLocks;
|
|
40
|
-
private readonly sessionAborts;
|
|
41
33
|
private agentExecutor;
|
|
42
34
|
private a2aRequestHandler;
|
|
43
35
|
private agentCard;
|
|
@@ -45,51 +37,28 @@ export declare class MulticlawsService extends EventEmitter {
|
|
|
45
37
|
private readonly httpRateLimiter;
|
|
46
38
|
private selfUrl;
|
|
47
39
|
private profileDescription;
|
|
48
|
-
private tailscaleStatus;
|
|
49
40
|
constructor(options: MulticlawsServiceOptions);
|
|
50
41
|
start(): Promise<void>;
|
|
51
42
|
stop(): Promise<void>;
|
|
52
43
|
updateGatewayConfig(config: GatewayConfig): void;
|
|
53
44
|
listAgents(): Promise<AgentRecord[]>;
|
|
45
|
+
addAgent(params: {
|
|
46
|
+
url: string;
|
|
47
|
+
apiKey?: string;
|
|
48
|
+
}): Promise<AgentRecord>;
|
|
54
49
|
removeAgent(url: string): Promise<boolean>;
|
|
55
|
-
|
|
50
|
+
delegateTask(params: {
|
|
56
51
|
agentUrl: string;
|
|
57
|
-
|
|
58
|
-
}): Promise<
|
|
59
|
-
|
|
60
|
-
sessionId: string;
|
|
61
|
-
message: string;
|
|
62
|
-
}): Promise<SessionReplyResult>;
|
|
63
|
-
getSession(sessionId: string): ConversationSession | null;
|
|
64
|
-
listSessions(): ConversationSession[];
|
|
65
|
-
waitForSessions(params: {
|
|
66
|
-
sessionIds: string[];
|
|
67
|
-
timeoutMs?: number;
|
|
68
|
-
}): Promise<{
|
|
69
|
-
results: Array<{
|
|
70
|
-
sessionId: string;
|
|
71
|
-
status: string;
|
|
72
|
-
agentName: string;
|
|
73
|
-
lastMessage?: string;
|
|
74
|
-
error?: string;
|
|
75
|
-
}>;
|
|
76
|
-
timedOut: boolean;
|
|
77
|
-
}>;
|
|
78
|
-
endSession(sessionId: string): boolean;
|
|
79
|
-
private acquireSessionLock;
|
|
80
|
-
private runSession;
|
|
81
|
-
private extractResultState;
|
|
82
|
-
private handleSessionResult;
|
|
83
|
-
private notifySessionUpdate;
|
|
52
|
+
task: string;
|
|
53
|
+
}): Promise<DelegateTaskResult>;
|
|
54
|
+
getTaskStatus(taskId: string): import("../task/tracker").TaskRecord | null;
|
|
84
55
|
getProfile(): Promise<AgentProfile>;
|
|
85
56
|
setProfile(patch: {
|
|
86
57
|
ownerName?: string;
|
|
87
58
|
bio?: string;
|
|
88
59
|
}): Promise<AgentProfile>;
|
|
89
|
-
private autoClearPendingReviewIfReady;
|
|
90
60
|
private updateProfileDescription;
|
|
91
61
|
private getPendingReviewPath;
|
|
92
|
-
getTailscaleStatus(): "ready" | "needs_auth" | "not_installed" | "unavailable";
|
|
93
62
|
getPendingProfileReview(): Promise<{
|
|
94
63
|
pending: boolean;
|
|
95
64
|
profile?: AgentProfile;
|
|
@@ -115,6 +84,7 @@ export declare class MulticlawsService extends EventEmitter {
|
|
|
115
84
|
private fetchMemberDescriptions;
|
|
116
85
|
private syncTeamToRegistry;
|
|
117
86
|
private createA2AClient;
|
|
87
|
+
private processTaskResult;
|
|
118
88
|
private extractArtifactText;
|
|
119
89
|
private notifyTailscaleSetup;
|
|
120
90
|
/** Fetch with up to 2 retries and exponential backoff. */
|