feat: add leader AgentBus task summaries

This commit is contained in:
inman committed 2026-09-07 13:01:01 +08:00
1 parent f4664997a8
commit 1a3ab63700
18 files changed
+2158 -19

No files matched your search

+139
View File
@@ -6,6 +6,8 @@ import {
AgentBusListener,
type AgentBusSocket,
type AgentBusTaskGateway,
type LeaderNotificationGateway,
createLeaderTaskSummaryFrame,
createTaskResultFrame,
createTaskProgressFrame,
extractAgentBusBusinessText,
@@ -31,6 +33,7 @@ import {
} from '../src/agentbus-delivery.js';
import { resolveBusinessRoute } from '../src/business-routes.js';
import type { AgentBusDelivery, PublicTask } from '../src/task-service.js';
import type { LeaderTaskSummaryDelivery } from '../src/leader-notification-service.js';
function makeTask(status: string, overrides: Partial<PublicTask> = {}): PublicTask {
return {
@@ -410,6 +413,142 @@ test('AgentBus protocol helpers preserve reply routing fields', () => {
assert.equal(fallbackProgress.conversation_id, 'agentbus:channel:wechat:user-2');
});
test('leader summary frame uses explicit proactive routing and cannot become an inbound task', () => {
const delivery: LeaderTaskSummaryDelivery = {
id: '11111111-1111-4111-8111-111111111111',
channel_id: '22222222-2222-4222-8222-222222222222',
task_id: 'TASK-20260907-001',
recipient_address: 'channel:wechat:leader-a',
recipient_fingerprint: 'abc123def456',
conversation_id: 'wechat-conversation-a',
conversation_fingerprint: 'def456abc123',
payload: {
event: 'task.summary',
status: 'completed',
task_id: 'TASK-20260907-001',
text: '【员工任务摘要】\n员工:employee-a'
},
attempt_count: 1
};
const frame = createLeaderTaskSummaryFrame(delivery, {
id: 'session-leader-1',
epoch: 9,
address: 'bot:leader-a:listener'
});
assert.equal(frame.id, `leader-summary-${delivery.id}`);
assert.equal(frame.from, 'bot:leader-a:listener');
assert.equal(frame.to, delivery.recipient_address);
assert.equal(frame.conversation_id, delivery.conversation_id);
assert.equal(Object.hasOwn(frame, 'reply_to'), false);
assert.deepEqual(frame.payload, delivery.payload);
assert.equal(isInboundAgentBusTask(frame), false);
});
test('listener sends employee outbox first, then a redacted-log leader summary batch', async (t) => {
const socket = new FakeSocket();
const order: string[] = [];
const delivered: string[] = [];
const released: string[] = [];
const logs: Array<Record<string, unknown>> = [];
const delivery: LeaderTaskSummaryDelivery = {
id: '33333333-3333-4333-8333-333333333333',
channel_id: 'channel-leader-a',
task_id: 'TASK-PRIVATE-1',
recipient_address: 'channel:wechat:private-leader-address',
recipient_fingerprint: 'a1b2c3d4e5f6',
conversation_id: 'private-wechat-conversation',
conversation_fingerprint: 'f6e5d4c3b2a1',
payload: {
event: 'task.summary',
status: 'completed',
task_id: 'TASK-PRIVATE-1',
text: '【员工任务摘要】\n员工:private-employee'
},
attempt_count: 1
};
let claimed = false;
const tasks: AgentBusTaskGateway = {
events: new EventEmitter(),
async ingestMessage() {
return { task: makeTask('failed'), attached: false, created: true };
},
async getTask() {
return makeTask('failed');
},
async listAgentBusFinalizationCandidates() {
return [];
},
async enqueueAgentBusResult() {},
async claimAgentBusDeliveries() {
order.push('employee');
return [];
},
async markAgentBusDeliveryDelivered() {},
async markAgentBusDeliveryFailed() {},
async releaseAgentBusDeliveries() {}
};
const leaderNotifications: LeaderNotificationGateway = {
async claimDeliveries() {
order.push('leader');
if (claimed) return [];
claimed = true;
return [delivery];
},
async markDeliveryDelivered(deliveryId) {
delivered.push(deliveryId);
},
async markDeliveryFailed() {},
async releaseDeliveries(channelId) {
released.push(channelId);
}
};
const listener = new AgentBusListener({
config: testConfig(),
tasks,
leaderNotifications,
organizationId: 'org-1',
scheduleParseQueue: async () => {},
socketFactory: () => socket as unknown as AgentBusSocket,
channel: {
id: 'channel-leader-a',
displayName: '组长 A',
wsUrl: 'wss://mesh.nianxx.cn/ws',
wsToken: 'leader-channel-token',
botAddress: 'bot:leader-a:listener',
ownerUserId: 'leader-a',
ownerRole: 'team_lead'
},
logger: {
info(metadata) { logs.push(metadata); },
warn(metadata) { logs.push(metadata); },
error(metadata) { logs.push(metadata); }
}
});
t.after(() => listener.stop());
listener.start();
socket.readyState = 1;
socket.emit('open');
socket.emit('message', JSON.stringify({
id: 'ready-leader-summary',
type: 'event',
session_id: 'session-leader-summary',
epoch: 1,
to: 'bot:leader-a:listener',
payload: { event: 'session.ready' }
}));
await waitFor(() => socket.sent.length === 1 && delivered.length === 1);
assert.deepEqual(order.slice(0, 2), ['employee', 'leader']);
assert.equal(delivered[0], delivery.id);
assert.equal(socket.sent[0].reply_to, undefined);
assert.equal((socket.sent[0].payload as Record<string, unknown>).event, 'task.summary');
const logText = JSON.stringify(logs);
assert.doesNotMatch(logText, /private-leader-address|private-wechat-conversation|private-employee/);
assert.match(logText, /a1b2c3d4e5f6/);
listener.stop();
await waitFor(() => released.includes('channel-leader-a'));
});
test('AgentBus result text uses the unified important message and preserves the confirmation gate', () => {
const needsInput = makeTask('awaiting_user_input', {
input_request: {