a2a-xmtp 1.4.1 → 1.4.3
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/package.json
CHANGED
|
@@ -39,16 +39,11 @@ export class GroupScheduler {
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* 初始化或获取群组状态。成员变化时自动重算。
|
|
42
|
-
* @param
|
|
43
|
-
* 非 agent(如群主/人类)不参与 speaking order。
|
|
42
|
+
* @param members 参与轮询的成员地址(调用方已按角色过滤,仅含 agent)
|
|
44
43
|
*/
|
|
45
|
-
getOrInitGroup(groupId: string, members: string[]
|
|
44
|
+
getOrInitGroup(groupId: string, members: string[]): GroupConversationState {
|
|
46
45
|
const existing = this.groups.get(groupId);
|
|
47
|
-
|
|
48
|
-
const agentMembers = agentAddresses
|
|
49
|
-
? members.filter((m) => agentAddresses.has(m.toLowerCase()))
|
|
50
|
-
: members;
|
|
51
|
-
const order = this.computeSpeakingOrder(groupId, agentMembers);
|
|
46
|
+
const order = this.computeSpeakingOrder(groupId, members);
|
|
52
47
|
|
|
53
48
|
// 成员变化 → 重算并重置计数
|
|
54
49
|
if (existing && !arraysEqual(existing.speakingOrder, order)) {
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
// ============================================================
|
|
5
5
|
|
|
6
6
|
import type { XmtpBridge } from "../transport/xmtp-bridge.js";
|
|
7
|
-
import type { IdentityRegistry } from "../transport/identity-registry.js";
|
|
8
7
|
import { formatA2AMessage, type A2AInjectPayload } from "../types.js";
|
|
9
8
|
import { GroupScheduler } from "./group-scheduler.js";
|
|
10
9
|
import type { PolicyEngine } from "./policy-engine.js";
|
|
@@ -50,7 +49,6 @@ export class MessageOrchestrator {
|
|
|
50
49
|
private logger: Logger,
|
|
51
50
|
private policyEngine: PolicyEngine,
|
|
52
51
|
groupScheduler: GroupScheduler,
|
|
53
|
-
private registry: IdentityRegistry,
|
|
54
52
|
) {
|
|
55
53
|
this.groupScheduler = groupScheduler;
|
|
56
54
|
}
|
|
@@ -68,6 +66,14 @@ export class MessageOrchestrator {
|
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
68
|
|
|
69
|
+
/**
|
|
70
|
+
* 群聊 self 消息计数(由 bridge.onSelfGroupMessage 触发)
|
|
71
|
+
* 保持 messageCount 与其他 agent 同步。
|
|
72
|
+
*/
|
|
73
|
+
handleSelfGroupMessage(conversationId: string): void {
|
|
74
|
+
this.groupScheduler.recordMessage(conversationId);
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
/**
|
|
72
78
|
* 处理收到的 XMTP 消息:群聊 round-robin 调度 → LLM 推理 → 安全校验 → 回复
|
|
73
79
|
*/
|
|
@@ -124,16 +130,21 @@ export class MessageOrchestrator {
|
|
|
124
130
|
const replyText = this.extractReplyText(messages);
|
|
125
131
|
if (!replyText) return;
|
|
126
132
|
|
|
133
|
+
// 发送前最终检查 turn budget(收窄 race window)
|
|
134
|
+
if (payload.conversation.isGroup && this.policyEngine.isTurnExhausted(payload.conversation.id)) {
|
|
135
|
+
this.logger.info(
|
|
136
|
+
`[a2a-xmtp] Turn budget exhausted before send in ${payload.conversation.id}, dropping reply`,
|
|
137
|
+
);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
127
141
|
// recordTurn 由 bridge.sendMessage 内部调用,此处不重复
|
|
128
142
|
await bridge.sendMessage(payload.from.xmtpAddress, replyText, {
|
|
129
143
|
conversationId: payload.conversation.id,
|
|
130
144
|
});
|
|
131
145
|
|
|
132
|
-
//
|
|
146
|
+
// 群聊:清除 claim(self 消息计数由 bridge.onSelfGroupMessage 处理)
|
|
133
147
|
if (payload.conversation.isGroup) {
|
|
134
|
-
// 自己发的消息 handleIncoming 不会收到(跳过 self),
|
|
135
|
-
// 手动递增以保持与其他 agent 的 messageCount 同步
|
|
136
|
-
this.groupScheduler.recordMessage(payload.conversation.id);
|
|
137
148
|
this.groupScheduler.clearClaim(payload.conversation.id);
|
|
138
149
|
}
|
|
139
150
|
|
|
@@ -155,20 +166,17 @@ export class MessageOrchestrator {
|
|
|
155
166
|
payload: A2AInjectPayload,
|
|
156
167
|
): Promise<boolean> {
|
|
157
168
|
const convId = payload.conversation.id;
|
|
158
|
-
const members = payload.conversation.participants;
|
|
159
169
|
const myAddress = bridge.address;
|
|
160
170
|
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
}
|
|
171
|
+
// 用 XMTP 原生角色信息筛选:只有普通 Member (permissionLevel=0) 参与轮询
|
|
172
|
+
// Admin/SuperAdmin 通常是人类,不参与 round-robin
|
|
173
|
+
const details = payload.conversation.participantDetails;
|
|
174
|
+
const agentMembers = details
|
|
175
|
+
? details.filter((p) => p.permissionLevel === 0).map((p) => p.address)
|
|
176
|
+
: payload.conversation.participants; // fallback:无角色信息时用全部成员
|
|
169
177
|
|
|
170
|
-
//
|
|
171
|
-
this.groupScheduler.getOrInitGroup(convId,
|
|
178
|
+
// 初始化群组状态
|
|
179
|
+
this.groupScheduler.getOrInitGroup(convId, agentMembers);
|
|
172
180
|
|
|
173
181
|
// 检查 turn budget 是否耗尽
|
|
174
182
|
if (this.policyEngine.isTurnExhausted(convId)) {
|
|
@@ -30,7 +30,9 @@ export class PolicyEngine {
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* 入站检查:consent + TTL + turn budget(不检查 cool-down,
|
|
33
|
-
*
|
|
33
|
+
* 否则刚发完消息后收到的回复会被静默丢弃)。
|
|
34
|
+
* 注意:turn budget 拦截不影响 messageCount 同步,因为
|
|
35
|
+
* self 消息通过 bridge.onSelfGroupMessage 独立计数。
|
|
34
36
|
*/
|
|
35
37
|
checkIncoming(params: PolicyCheckParams): PolicyCheckResult {
|
|
36
38
|
const consent = this.getConsent(params.from);
|
|
@@ -43,7 +45,6 @@ export class PolicyEngine {
|
|
|
43
45
|
reason: `Sender ${params.from} not explicitly allowed (consent: ${consent})`,
|
|
44
46
|
};
|
|
45
47
|
}
|
|
46
|
-
// 仅检查 TTL + turn budget,不检查 cool-down
|
|
47
48
|
const state = this.getOrCreateState(params.conversationId);
|
|
48
49
|
const now = Date.now();
|
|
49
50
|
const ttlMs = this.policy.ttlMinutes * 60 * 1000;
|
package/src/index.ts
CHANGED
|
@@ -163,7 +163,6 @@ export default definePluginEntry({
|
|
|
163
163
|
ctx.logger,
|
|
164
164
|
policyEngine,
|
|
165
165
|
groupScheduler,
|
|
166
|
-
registry,
|
|
167
166
|
);
|
|
168
167
|
|
|
169
168
|
// 初始化主 Agent 的 XMTP Bridge
|
|
@@ -187,6 +186,10 @@ export default definePluginEntry({
|
|
|
187
186
|
bridge.onClaim = (conversationId, senderAddress, messageId) =>
|
|
188
187
|
orchestrator.handleClaim(conversationId, senderAddress, messageId);
|
|
189
188
|
|
|
189
|
+
// 群聊 self 消息计数回调:保持 messageCount 同步
|
|
190
|
+
bridge.onSelfGroupMessage = (conversationId) =>
|
|
191
|
+
orchestrator.handleSelfGroupMessage(conversationId);
|
|
192
|
+
|
|
190
193
|
await bridge.start();
|
|
191
194
|
bridges.set(AGENT_ID, bridge);
|
|
192
195
|
|
|
@@ -7,7 +7,7 @@ import { Agent, createUser, createSigner } from "@xmtp/agent-sdk";
|
|
|
7
7
|
import type { IdentityRegistry } from "./identity-registry.js";
|
|
8
8
|
import { PolicyEngine } from "../coordination/policy-engine.js";
|
|
9
9
|
import type { XmtpWalletConfig, InboxMessage, A2AContentType, A2AInjectPayload, GroupInfo } from "../types.js";
|
|
10
|
-
import { createA2APayload, CLAIM_PREFIX } from "../types.js";
|
|
10
|
+
import { createA2APayload, CLAIM_PREFIX, type GroupParticipant } from "../types.js";
|
|
11
11
|
import { GroupScheduler } from "../coordination/group-scheduler.js";
|
|
12
12
|
|
|
13
13
|
/** IdentifierKind.Ethereum = 0 (const enum, 不能在 isolatedModules 下直接访问) */
|
|
@@ -28,6 +28,9 @@ export class XmtpBridge {
|
|
|
28
28
|
/** Claim 消息回调(由 orchestrator 设置,用于群聊调度) */
|
|
29
29
|
onClaim?: (conversationId: string, senderAddress: string, messageId: string) => void;
|
|
30
30
|
|
|
31
|
+
/** 群聊 self 消息计数回调(由 orchestrator 设置,用于 round-robin 同步) */
|
|
32
|
+
onSelfGroupMessage?: (conversationId: string) => void;
|
|
33
|
+
|
|
31
34
|
constructor(
|
|
32
35
|
readonly agentId: string,
|
|
33
36
|
private walletConfig: XmtpWalletConfig,
|
|
@@ -232,9 +235,7 @@ export class XmtpBridge {
|
|
|
232
235
|
|
|
233
236
|
private async handleIncoming(ctx: any, contentType: A2AContentType): Promise<void> {
|
|
234
237
|
const senderAddress: string = await ctx.getSenderAddress();
|
|
235
|
-
|
|
236
|
-
if (senderAddress.toLowerCase() === this.address.toLowerCase()) return;
|
|
237
|
-
|
|
238
|
+
const isSelf = senderAddress.toLowerCase() === this.address.toLowerCase();
|
|
238
239
|
const content = String(ctx.message.content);
|
|
239
240
|
|
|
240
241
|
// 识别 claim 消息:不计数、不缓存、通知调度器
|
|
@@ -246,6 +247,15 @@ export class XmtpBridge {
|
|
|
246
247
|
return;
|
|
247
248
|
}
|
|
248
249
|
|
|
250
|
+
// 群聊 self 消息:仅计数(保持 messageCount 同步),不触发响应
|
|
251
|
+
const isGroup = ctx.conversation.isGroup ?? false;
|
|
252
|
+
if (isSelf) {
|
|
253
|
+
if (isGroup && this.onSelfGroupMessage) {
|
|
254
|
+
this.onSelfGroupMessage(ctx.conversation.id);
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
249
259
|
const senderAgentId = await this.registry.resolveAgentId(senderAddress);
|
|
250
260
|
|
|
251
261
|
const policyResult = this.policyEngine.checkIncoming({
|
|
@@ -258,15 +268,19 @@ export class XmtpBridge {
|
|
|
258
268
|
const convState = this.policyEngine.getConversationState(ctx.conversation.id);
|
|
259
269
|
const turn = convState?.turn ?? 0;
|
|
260
270
|
|
|
261
|
-
//
|
|
271
|
+
// 群聊时获取参与者列表(含角色信息)
|
|
262
272
|
let participants: string[] = [];
|
|
263
|
-
|
|
273
|
+
let participantDetails: GroupParticipant[] | undefined;
|
|
264
274
|
if (isGroup) {
|
|
265
275
|
try {
|
|
266
276
|
const members = await ctx.conversation.members();
|
|
267
277
|
participants = members.map((m: any) =>
|
|
268
278
|
m.accountAddresses?.[0]?.toLowerCase() ?? m.inboxId,
|
|
269
279
|
);
|
|
280
|
+
participantDetails = members.map((m: any) => ({
|
|
281
|
+
address: (m.accountAddresses?.[0]?.toLowerCase() ?? m.inboxId) as string,
|
|
282
|
+
permissionLevel: (m.permissionLevel ?? 0) as number,
|
|
283
|
+
}));
|
|
270
284
|
} catch { /* 获取成员失败时保持空数组 */ }
|
|
271
285
|
}
|
|
272
286
|
|
|
@@ -276,6 +290,7 @@ export class XmtpBridge {
|
|
|
276
290
|
conversationId: ctx.conversation.id,
|
|
277
291
|
isGroup,
|
|
278
292
|
participants,
|
|
293
|
+
participantDetails,
|
|
279
294
|
messageId: ctx.message.id ?? crypto.randomUUID(),
|
|
280
295
|
content,
|
|
281
296
|
contentType,
|
package/src/types.ts
CHANGED
|
@@ -12,6 +12,13 @@ export interface XmtpWalletConfig {
|
|
|
12
12
|
|
|
13
13
|
export type XmtpEnv = "dev" | "production";
|
|
14
14
|
|
|
15
|
+
/** 群成员信息(含角色) */
|
|
16
|
+
export interface GroupParticipant {
|
|
17
|
+
address: string;
|
|
18
|
+
/** XMTP PermissionLevel: 0=Member, 1=Admin, 2=SuperAdmin */
|
|
19
|
+
permissionLevel: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
/** 注入 Agent session 的消息格式 */
|
|
16
23
|
export interface A2AInjectPayload {
|
|
17
24
|
type: "a2a-xmtp";
|
|
@@ -24,6 +31,8 @@ export interface A2AInjectPayload {
|
|
|
24
31
|
id: string;
|
|
25
32
|
isGroup: boolean;
|
|
26
33
|
participants: string[];
|
|
34
|
+
/** 群成员详细信息(含角色),仅群聊时有值 */
|
|
35
|
+
participantDetails?: GroupParticipant[];
|
|
27
36
|
};
|
|
28
37
|
message: {
|
|
29
38
|
id: string;
|
|
@@ -86,8 +95,8 @@ export interface GroupConversationState {
|
|
|
86
95
|
lastClaim: { sender: string; timestamp: number; messageId: string } | null;
|
|
87
96
|
}
|
|
88
97
|
|
|
89
|
-
/** Claim
|
|
90
|
-
export const CLAIM_PREFIX = "
|
|
98
|
+
/** Claim 消息前缀(可见标记,避免 XMTP 传输时被 strip) */
|
|
99
|
+
export const CLAIM_PREFIX = "__CLAIM__:";
|
|
91
100
|
|
|
92
101
|
/** 默认群聊调度配置 */
|
|
93
102
|
export const DEFAULT_GROUP_SCHEDULING: GroupSchedulingConfig = {
|
|
@@ -150,6 +159,7 @@ export function createA2APayload(params: {
|
|
|
150
159
|
conversationId: string;
|
|
151
160
|
isGroup: boolean;
|
|
152
161
|
participants: string[];
|
|
162
|
+
participantDetails?: GroupParticipant[];
|
|
153
163
|
messageId: string;
|
|
154
164
|
content: string;
|
|
155
165
|
contentType: A2AContentType;
|
|
@@ -168,6 +178,7 @@ export function createA2APayload(params: {
|
|
|
168
178
|
id: params.conversationId,
|
|
169
179
|
isGroup: params.isGroup,
|
|
170
180
|
participants: params.participants,
|
|
181
|
+
participantDetails: params.participantDetails,
|
|
171
182
|
},
|
|
172
183
|
message: {
|
|
173
184
|
id: params.messageId,
|