multiclaws 0.4.19 → 0.4.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import type { BasicLogger } from "../infra/logger";
1
2
  export type SessionStatus = "active" | "input-required" | "completed" | "failed" | "canceled";
2
3
  export type SessionMessage = {
3
4
  role: "user" | "agent";
@@ -20,11 +21,13 @@ export type ConversationSession = {
20
21
  export declare class SessionStore {
21
22
  private readonly filePath;
22
23
  private readonly ttlMs;
24
+ private readonly logger?;
23
25
  private store;
24
26
  private persistPending;
25
27
  constructor(opts: {
26
28
  filePath: string;
27
29
  ttlMs?: number;
30
+ logger?: BasicLogger;
28
31
  });
29
32
  create(params: {
30
33
  agentUrl: string;
@@ -31,11 +31,13 @@ function normalizeStore(raw) {
31
31
  class SessionStore {
32
32
  filePath;
33
33
  ttlMs;
34
+ logger;
34
35
  store;
35
36
  persistPending = false;
36
37
  constructor(opts) {
37
38
  this.filePath = opts.filePath;
38
39
  this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
40
+ this.logger = opts.logger;
39
41
  this.store = this.loadSync();
40
42
  }
41
43
  create(params) {
@@ -116,8 +118,9 @@ class SessionStore {
116
118
  await promises_1.default.writeFile(tmp, JSON.stringify(this.store, null, 2), "utf8");
117
119
  await promises_1.default.rename(tmp, this.filePath);
118
120
  }
119
- catch {
121
+ catch (err) {
120
122
  // best-effort
123
+ this.logger?.warn?.(`[session-store] persistAsync failed: ${err instanceof Error ? err.message : String(err)}`);
121
124
  }
122
125
  }
123
126
  prune() {
@@ -1,3 +1,4 @@
1
+ import type { BasicLogger } from "../infra/logger";
1
2
  export type TaskStatus = "queued" | "running" | "completed" | "failed";
2
3
  export type TaskRecord = {
3
4
  taskId: string;
@@ -16,12 +17,14 @@ export declare class TaskTracker {
16
17
  private readonly ttlMs;
17
18
  private readonly maxTasks;
18
19
  private readonly store;
20
+ private readonly logger?;
19
21
  private pruneTimer;
20
22
  private persistPending;
21
23
  constructor(opts?: {
22
24
  ttlMs?: number;
23
25
  maxTasks?: number;
24
26
  filePath?: string;
27
+ logger?: BasicLogger;
25
28
  });
26
29
  create(params: {
27
30
  fromPeerId: string;
@@ -62,12 +62,14 @@ class TaskTracker {
62
62
  ttlMs;
63
63
  maxTasks;
64
64
  store;
65
+ logger;
65
66
  pruneTimer = null;
66
67
  persistPending = false;
67
68
  constructor(opts) {
68
69
  this.ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
69
70
  this.maxTasks = opts?.maxTasks ?? MAX_TASKS;
70
71
  this.filePath = opts?.filePath ?? ".openclaw/multiclaws/tasks.json";
72
+ this.logger = opts?.logger;
71
73
  // Sync load at startup is acceptable (runs once)
72
74
  this.store = this.loadStoreSync();
73
75
  this.pruneTimer = setInterval(() => this.prune(), PRUNE_INTERVAL_MS);
@@ -76,6 +78,7 @@ class TaskTracker {
76
78
  }
77
79
  }
78
80
  create(params) {
81
+ this.logger?.debug?.(`[task-tracker] create(from=${params.fromPeerId}, to=${params.toPeerId})`);
79
82
  if (this.store.tasks.length >= this.maxTasks) {
80
83
  this.prune();
81
84
  }
@@ -95,6 +98,7 @@ class TaskTracker {
95
98
  };
96
99
  this.store.tasks.push(record);
97
100
  this.schedulePersist();
101
+ this.logger?.debug?.(`[task-tracker] create completed, taskId=${record.taskId}`);
98
102
  return record;
99
103
  }
100
104
  update(taskId, patch) {
@@ -153,8 +157,9 @@ class TaskTracker {
153
157
  await promises_1.default.writeFile(tmp, JSON.stringify(this.store, null, 2), "utf8");
154
158
  await promises_1.default.rename(tmp, this.filePath);
155
159
  }
156
- catch {
160
+ catch (err) {
157
161
  // best-effort persistence — in-memory state is authoritative
162
+ this.logger?.warn?.(`[task-tracker] persistAsync failed: ${err instanceof Error ? err.message : String(err)}`);
158
163
  }
159
164
  }
160
165
  prune() {
@@ -1,3 +1,4 @@
1
+ import type { BasicLogger } from "../infra/logger";
1
2
  export type TeamMember = {
2
3
  url: string;
3
4
  name: string;
@@ -21,7 +22,9 @@ export declare function encodeInvite(teamId: string, seedUrl: string): string;
21
22
  export declare function decodeInvite(code: string): InvitePayload;
22
23
  export declare class TeamStore {
23
24
  private readonly filePath;
24
- constructor(filePath: string);
25
+ private readonly logger?;
26
+ constructor(filePath: string, logger?: BasicLogger | undefined);
27
+ private log;
25
28
  private readStore;
26
29
  createTeam(params: {
27
30
  teamName: string;
@@ -46,28 +46,43 @@ function decodeInvite(code) {
46
46
  // ── TeamStore ────────────────────────────────────────────────────────
47
47
  class TeamStore {
48
48
  filePath;
49
- constructor(filePath) {
49
+ logger;
50
+ constructor(filePath, logger) {
50
51
  this.filePath = filePath;
52
+ this.logger = logger;
53
+ }
54
+ log(level, message) {
55
+ const fn = level === "debug" ? this.logger?.debug : this.logger?.[level];
56
+ fn?.(`[team-store] ${message}`);
51
57
  }
52
58
  async readStore() {
53
59
  const store = await (0, json_store_1.readJsonWithFallback)(this.filePath, emptyStore());
54
60
  return normalizeStore(store);
55
61
  }
56
62
  async createTeam(params) {
57
- return await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
58
- const store = await this.readStore();
59
- const now = Date.now();
60
- const record = {
61
- teamId: (0, node_crypto_1.randomUUID)(),
62
- teamName: params.teamName,
63
- selfUrl: params.selfUrl,
64
- members: [{ url: params.selfUrl, name: params.selfName, description: params.selfDescription, joinedAtMs: now }],
65
- createdAtMs: now,
66
- };
67
- store.teams.push(record);
68
- await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
69
- return record;
70
- });
63
+ this.log("debug", `createTeam(name=${params.teamName})`);
64
+ try {
65
+ const result = await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
66
+ const store = await this.readStore();
67
+ const now = Date.now();
68
+ const record = {
69
+ teamId: (0, node_crypto_1.randomUUID)(),
70
+ teamName: params.teamName,
71
+ selfUrl: params.selfUrl,
72
+ members: [{ url: params.selfUrl, name: params.selfName, description: params.selfDescription, joinedAtMs: now }],
73
+ createdAtMs: now,
74
+ };
75
+ store.teams.push(record);
76
+ await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
77
+ return record;
78
+ });
79
+ this.log("debug", `createTeam completed, teamId=${result.teamId}`);
80
+ return result;
81
+ }
82
+ catch (err) {
83
+ this.log("error", `createTeam failed: ${err instanceof Error ? err.message : String(err)}`);
84
+ throw err;
85
+ }
71
86
  }
72
87
  async getTeam(teamId) {
73
88
  const store = await this.readStore();
@@ -82,65 +97,99 @@ class TeamStore {
82
97
  return store.teams[0] ?? null;
83
98
  }
84
99
  async addMember(teamId, member) {
85
- return await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
86
- const store = await this.readStore();
87
- const team = store.teams.find((t) => t.teamId === teamId);
88
- if (!team)
89
- return false;
90
- const normalizedUrl = member.url.replace(/\/+$/, "");
91
- const existing = team.members.findIndex((m) => m.url.replace(/\/+$/, "") === normalizedUrl);
92
- if (existing >= 0) {
93
- team.members[existing].name = member.name;
94
- if (member.description !== undefined) {
95
- team.members[existing].description = member.description;
100
+ this.log("debug", `addMember(teamId=${teamId}, url=${member.url})`);
101
+ try {
102
+ const result = await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
103
+ const store = await this.readStore();
104
+ const team = store.teams.find((t) => t.teamId === teamId);
105
+ if (!team)
106
+ return false;
107
+ const normalizedUrl = member.url.replace(/\/+$/, "");
108
+ const existing = team.members.findIndex((m) => m.url.replace(/\/+$/, "") === normalizedUrl);
109
+ if (existing >= 0) {
110
+ team.members[existing].name = member.name;
111
+ if (member.description !== undefined) {
112
+ team.members[existing].description = member.description;
113
+ }
114
+ team.members[existing].joinedAtMs = member.joinedAtMs;
115
+ }
116
+ else {
117
+ team.members.push({ ...member, url: normalizedUrl });
96
118
  }
97
- team.members[existing].joinedAtMs = member.joinedAtMs;
98
- }
99
- else {
100
- team.members.push({ ...member, url: normalizedUrl });
101
- }
102
- await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
103
- return true;
104
- });
119
+ await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
120
+ return true;
121
+ });
122
+ this.log("debug", `addMember completed, result=${result}`);
123
+ return result;
124
+ }
125
+ catch (err) {
126
+ this.log("error", `addMember failed for teamId=${teamId}: ${err instanceof Error ? err.message : String(err)}`);
127
+ throw err;
128
+ }
105
129
  }
106
130
  async removeMember(teamId, memberUrl) {
107
- return await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
108
- const store = await this.readStore();
109
- const team = store.teams.find((t) => t.teamId === teamId);
110
- if (!team)
111
- return false;
112
- const normalizedUrl = memberUrl.replace(/\/+$/, "");
113
- const before = team.members.length;
114
- team.members = team.members.filter((m) => m.url.replace(/\/+$/, "") !== normalizedUrl);
115
- if (team.members.length === before)
116
- return false;
117
- await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
118
- return true;
119
- });
131
+ const normalizedUrl = memberUrl.replace(/\/+$/, "");
132
+ this.log("debug", `removeMember(teamId=${teamId}, url=${normalizedUrl})`);
133
+ try {
134
+ const result = await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
135
+ const store = await this.readStore();
136
+ const team = store.teams.find((t) => t.teamId === teamId);
137
+ if (!team)
138
+ return false;
139
+ const before = team.members.length;
140
+ team.members = team.members.filter((m) => m.url.replace(/\/+$/, "") !== normalizedUrl);
141
+ if (team.members.length === before)
142
+ return false;
143
+ await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
144
+ return true;
145
+ });
146
+ this.log("debug", `removeMember completed, found=${result}`);
147
+ return result;
148
+ }
149
+ catch (err) {
150
+ this.log("error", `removeMember failed for teamId=${teamId}: ${err instanceof Error ? err.message : String(err)}`);
151
+ throw err;
152
+ }
120
153
  }
121
154
  async deleteTeam(teamId) {
122
- return await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
123
- const store = await this.readStore();
124
- const before = store.teams.length;
125
- store.teams = store.teams.filter((t) => t.teamId !== teamId);
126
- if (store.teams.length === before)
127
- return false;
128
- await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
129
- return true;
130
- });
155
+ this.log("debug", `deleteTeam(teamId=${teamId})`);
156
+ try {
157
+ const result = await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
158
+ const store = await this.readStore();
159
+ const before = store.teams.length;
160
+ store.teams = store.teams.filter((t) => t.teamId !== teamId);
161
+ if (store.teams.length === before)
162
+ return false;
163
+ await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
164
+ return true;
165
+ });
166
+ this.log("debug", `deleteTeam completed, found=${result}`);
167
+ return result;
168
+ }
169
+ catch (err) {
170
+ this.log("error", `deleteTeam failed for teamId=${teamId}: ${err instanceof Error ? err.message : String(err)}`);
171
+ throw err;
172
+ }
131
173
  }
132
174
  async saveTeam(team) {
133
- await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
134
- const store = await this.readStore();
135
- const idx = store.teams.findIndex((t) => t.teamId === team.teamId);
136
- if (idx >= 0) {
137
- store.teams[idx] = team;
138
- }
139
- else {
140
- store.teams.push(team);
141
- }
142
- await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
143
- });
175
+ this.log("debug", `saveTeam(teamId=${team.teamId})`);
176
+ try {
177
+ await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
178
+ const store = await this.readStore();
179
+ const idx = store.teams.findIndex((t) => t.teamId === team.teamId);
180
+ if (idx >= 0) {
181
+ store.teams[idx] = team;
182
+ }
183
+ else {
184
+ store.teams.push(team);
185
+ }
186
+ await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
187
+ });
188
+ }
189
+ catch (err) {
190
+ this.log("error", `saveTeam failed for teamId=${team.teamId}: ${err instanceof Error ? err.message : String(err)}`);
191
+ throw err;
192
+ }
144
193
  }
145
194
  }
146
195
  exports.TeamStore = TeamStore;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multiclaws",
3
- "version": "0.4.19",
3
+ "version": "0.4.21",
4
4
  "description": "MultiClaws plugin for OpenClaw collaboration via A2A protocol",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,12 +15,6 @@
15
15
  "openclaw.plugin.json",
16
16
  "README.md"
17
17
  ],
18
- "scripts": {
19
- "build": "tsc -p tsconfig.json",
20
- "test": "vitest run",
21
- "test:watch": "vitest",
22
- "clean": "rm -rf dist"
23
- },
24
18
  "keywords": [
25
19
  "openclaw",
26
20
  "plugin",
@@ -52,5 +46,11 @@
52
46
  "@types/proper-lockfile": "^4.1.4",
53
47
  "typescript": "^5.9.2",
54
48
  "vitest": "^3.2.4"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc -p tsconfig.json",
52
+ "test": "vitest run",
53
+ "test:watch": "vitest",
54
+ "clean": "rm -rf dist"
55
55
  }
56
- }
56
+ }
@@ -102,7 +102,8 @@ multiclaws_profile_show()
102
102
  | `multiclaws_agents` | 列出所有已知智能体及 bio | — |
103
103
  | `multiclaws_add_agent` | 手动添加远端智能体 | `url`, `apiKey`(可选) |
104
104
  | `multiclaws_remove_agent` | 移除已知智能体 | `url` |
105
- | `multiclaws_delegate` | 委派任务给远端智能体 | `agentUrl`, `task` |
105
+ | `multiclaws_delegate` | 委派任务给远端智能体(自动 spawn 子 agent,立即返回) | `agentUrl`, `task` |
106
+ | `multiclaws_delegate_send` | 同步发送任务并等待结果(子 agent 内部使用,勿直接调用) | `agentUrl`, `task` |
106
107
  | `multiclaws_task_status` | 查看委派任务状态 | `taskId` |
107
108
 
108
109
  ---
@@ -137,15 +138,15 @@ multiclaws_profile_show()
137
138
  → 自动同步所有团队成员
138
139
  ```
139
140
 
140
- ### 智能委派(单轮)
141
+ ### 委派任务
141
142
 
142
- 适用于一次性任务,不需要来回沟通。
143
+ 所有委派(无论单轮还是多轮)都通过 `multiclaws_delegate` 进行。代码会自动 spawn 子 agent 执行,主 agent 立即返回,无需手动 `sessions_spawn`。
143
144
 
144
145
  ```
145
146
  1. multiclaws_agents() — 列出智能体,读 bio
146
147
  2. 选择 bio 最匹配任务的智能体
147
148
  3. multiclaws_delegate(agentUrl="...", task="...")
148
- 4. 把结果返回给用户
149
+ 代码自动 spawn 子 agent,子 agent 通过 message 实时汇报结果
149
150
  ```
150
151
 
151
152
  选择智能体时:
@@ -153,91 +154,26 @@ multiclaws_profile_show()
153
154
  - 匹配数据需求(如「检查 API 代码」→ bio 中有该代码库的智能体)
154
155
  - 多个匹配时选最具体的
155
156
 
156
- ### 多轮协作(需要来回沟通)
157
+ 需要联系多个智能体时,对每个智能体分别调用 `multiclaws_delegate`。
157
158
 
158
- 适用于需要协商、确认、多次沟通才能完成的任务(如约会议、对需求、联合调试)。
159
+ #### 示例
159
160
 
160
- **默认使用异步子 agent 模式。** 主 agent 启动子 agent 后立即返回,子 agent 自主完成全部沟通并通过 `message` 工具实时汇报进展。
161
-
162
- #### 工作流程
163
-
164
- ```
165
- 用户: "帮我和小明、小红约明天下午的会议"
166
-
167
- 1. multiclaws_agents() — 列出智能体,读 bio
168
- 2. sessions_spawn(task="<协作任务 prompt>", mode="run")
169
- 3. → 立即告诉用户: "已启动协作任务,会实时汇报进展"
170
- 4.(子 agent 在后台自主完成全部沟通,通过 message 实时汇报)
171
- 5. 子 agent 完成 → announce 回主 agent → 最终结果自动通知用户
161
+ **单人任务:**
172
162
  ```
163
+ 用户: "问一下小明他那个 API 接口的参数格式"
173
164
 
174
- #### agent task prompt 模板
175
-
176
- spawn 子 agent 时,task 必须包含以下要素:
177
-
178
- ```
179
- sessions_spawn(task="
180
- ## 任务
181
- 联系小明和小红,协商明天下午的会议时间。我这边下午 2-5 点都可以。
182
-
183
- ## 可用工具
184
- - `multiclaws_agents()` — 查看所有智能体
185
- - `multiclaws_delegate(agentUrl, task)` — 向智能体发送任务
186
-
187
- ## 执行步骤
188
- 1. 调用 multiclaws_agents() 获取智能体列表
189
- 2. 依次用 multiclaws_delegate 联系每个相关智能体
190
- 3. 每完成一个智能体的沟通,立即用 message 工具向用户汇报进展
191
- 4. 全部沟通完成后,汇总结果
192
-
193
- ## 中间汇报
194
- 每次 multiclaws_delegate 返回后,立即调用 message 工具告知用户当前进展。
195
- 例如:'已联系小明,他说明天下午 3 点和 4 点都可以。正在联系小红...'
196
-
197
- ## 完成条件
198
- 所有相关智能体都已回复,汇总最终结果。
199
- ", mode="run")
165
+ 1. multiclaws_delegate(小明, "你那个 API 接口的参数格式是什么?")
166
+ → 子 agent 自动发送、等待回复、通过 message 汇报结果
200
167
  ```
201
168
 
202
- #### 关键规则
203
-
204
- - **主 agent 不做多轮沟通。** 所有多轮协作都交给子 agent。
205
- - **子 agent 用 `message` 工具实时汇报。** 子 agent 继承了父 agent 的频道上下文,`message` 发出的消息用户能直接看到。
206
- - 每次 `multiclaws_delegate` 返回后,子 agent 必须立即用 `message` 汇报,不要等全部完成。
207
- - 如果某个智能体没有回复或返回错误,子 agent 应在汇报中说明,继续联系其他智能体。
208
- - 协商未达成一致时,子 agent 可继续发 `multiclaws_delegate`,最多 5 轮。
209
- - 涉及多个智能体时,依次联系(串行),每个完成后立即汇报。
210
-
211
- #### 示例场景
212
-
213
- **约多人会议:**
169
+ **多人任务:**
214
170
  ```
215
171
  用户: "帮我和小明、小红约明天下午的会议"
216
172
 
217
- agent:
218
- 1. multiclaws_agents() 找到小明、小红
219
- 2. sessions_spawn(task="联系小明和小红...(按模板)", mode="run")
220
- 3. 回复用户: "已启动协作任务,正在联系小明和小红,会实时汇报进展。"
221
-
222
- 子 agent(后台执行):
223
- 1. multiclaws_delegate(小明, "明天下午 2-5 点开会,你什么时候有空?")
224
- → 小明回复: "3 点和 4 点都行"
225
- → message("已联系小明,他明天下午 3 点和 4 点都可以。正在联系小红...")
226
- 2. multiclaws_delegate(小红, "明天下午 2-5 点开会,小明 3 点和 4 点都行,你呢?")
227
- → 小红回复: "3 点可以"
228
- → message("小红也确认明天下午 3 点可以。")
229
- 3. multiclaws_delegate(小明, "确认明天下午 3 点开会")
230
- 4. multiclaws_delegate(小红, "确认明天下午 3 点开会")
231
- 5. 完成 → announce: "会议已确认:明天下午 3 点,参与人:小明、小红"
232
- ```
233
-
234
- **简单单人协作:**
235
- ```
236
- 用户: "问一下小明他那个 API 接口的参数格式"
237
-
238
- (单轮任务,不需要多轮 → 直接用智能委派)
239
- 1. multiclaws_delegate(小明, "你那个 API 接口的参数格式是什么?")
240
- 2. 把结果返回给用户
173
+ 1. multiclaws_agents() → 找到小明、小红
174
+ 2. multiclaws_delegate(小明, "明天下午 2-5 点开会,你什么时候有空?")
175
+ 3. multiclaws_delegate(小红, "明天下午 2-5 点开会,你什么时候有空?")
176
+ 每个委派各自 spawn 子 agent,通过 message 实时汇报进展
241
177
  ```
242
178
 
243
179
  ---