refactor #22
@@ -475,6 +475,8 @@ moderator bot 在新 channel 中发布 kickoff message。
|
|||||||
### 12.2 展开讨论
|
### 12.2 展开讨论
|
||||||
参与 Agent 在该 channel 中按现有顺序讨论机制发言。
|
参与 Agent 在该 channel 中按现有顺序讨论机制发言。
|
||||||
|
|
||||||
|
讨论 channel 的 participant 集合继续复用现有 channel member bootstrap 逻辑:由 `plugin/core/turn-bootstrap.ts` 调用 `fetchVisibleChannelBotAccountIds(...)` 基于 Discord 可见成员与已有 account 映射发现可参与的 bot account,再交给 `initTurnOrder(...)` 建立轮转状态,而不是为 discussion 模式额外维护一套成员发现流程。
|
||||||
|
|
||||||
如果轮转一圈无人发言,则 moderator 提醒发起者:
|
如果轮转一圈无人发言,则 moderator 提醒发起者:
|
||||||
- 若已达成目标,请写总结文档并 callback
|
- 若已达成目标,请写总结文档并 callback
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
- [x] 梳理 initiator identity 的可获取路径
|
- [x] 梳理 initiator identity 的可获取路径
|
||||||
- [x] 确认 callback 时如何稳定识别 initiator account/session
|
- [x] 确认 callback 时如何稳定识别 initiator account/session
|
||||||
- [x] 确认 discussion channel 创建后 turn order 是否需立即 bootstrap
|
- [x] 确认 discussion channel 创建后 turn order 是否需立即 bootstrap
|
||||||
- [ ] 确认 discussion participant 的成员集合获取方式是否可直接复用现有逻辑
|
- [x] 确认 discussion participant 的成员集合获取方式是否可直接复用现有逻辑
|
||||||
|
|
||||||
### A9. `plugin/index.ts`
|
### A9. `plugin/index.ts`
|
||||||
- [x] 注入新增 discussion metadata/service 模块依赖
|
- [x] 注入新增 discussion metadata/service 模块依赖
|
||||||
@@ -238,8 +238,8 @@
|
|||||||
|
|
||||||
#### A13.3 turn / hook 测试
|
#### A13.3 turn / hook 测试
|
||||||
- [x] 测试 discussion channel 空转后发送 idle reminder
|
- [x] 测试 discussion channel 空转后发送 idle reminder
|
||||||
- [ ] 测试普通 channel 空转逻辑不受影响
|
- [x] 测试普通 channel 空转逻辑不受影响
|
||||||
- [ ] 测试 callback 成功后 discussion channel 不再 handoff
|
- [x] 测试 callback 成功后 discussion channel 不再 handoff
|
||||||
- [x] 测试 closed discussion channel 新消息不会继续唤醒 Agent
|
- [x] 测试 closed discussion channel 新消息不会继续唤醒 Agent
|
||||||
|
|
||||||
#### A13.4 路径校验测试
|
#### A13.4 路径校验测试
|
||||||
|
|||||||
1
plugin/channel-resolver.js
Normal file
1
plugin/channel-resolver.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './channel-resolver.ts';
|
||||||
1
plugin/rules.js
Normal file
1
plugin/rules.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './rules.ts';
|
||||||
1
plugin/turn-manager.js
Normal file
1
plugin/turn-manager.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './turn-manager.ts';
|
||||||
131
test/discussion-hooks.test.ts
Normal file
131
test/discussion-hooks.test.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import { registerBeforeMessageWriteHook } from '../plugin/hooks/before-message-write.ts';
|
||||||
|
import { registerMessageSentHook } from '../plugin/hooks/message-sent.ts';
|
||||||
|
import { initTurnOrder, onNewMessage, getTurnDebugInfo, resetTurn } from '../plugin/turn-manager.ts';
|
||||||
|
|
||||||
|
type Handler = (event: Record<string, unknown>, ctx: Record<string, unknown>) => unknown;
|
||||||
|
|
||||||
|
function makeApi() {
|
||||||
|
const handlers = new Map<string, Handler>();
|
||||||
|
return {
|
||||||
|
handlers,
|
||||||
|
logger: {
|
||||||
|
info: (_msg: string) => {},
|
||||||
|
warn: (_msg: string) => {},
|
||||||
|
},
|
||||||
|
on(name: string, handler: Handler) {
|
||||||
|
handlers.set(name, handler);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('before_message_write leaves ordinary channels dormant without sending a discussion idle reminder', async () => {
|
||||||
|
const channelId = 'normal-channel';
|
||||||
|
resetTurn(channelId);
|
||||||
|
initTurnOrder(channelId, ['agent-a', 'agent-b']);
|
||||||
|
onNewMessage(channelId, 'human-user', true);
|
||||||
|
|
||||||
|
const state = getTurnDebugInfo(channelId);
|
||||||
|
const [firstSpeaker, secondSpeaker] = state.turnOrder as string[];
|
||||||
|
assert.ok(firstSpeaker);
|
||||||
|
assert.ok(secondSpeaker);
|
||||||
|
|
||||||
|
const sessionChannelId = new Map<string, string>([
|
||||||
|
['sess-a', channelId],
|
||||||
|
['sess-b', channelId],
|
||||||
|
]);
|
||||||
|
const sessionAccountId = new Map<string, string>([
|
||||||
|
['sess-a', firstSpeaker],
|
||||||
|
['sess-b', secondSpeaker],
|
||||||
|
]);
|
||||||
|
const sessionAllowed = new Map<string, boolean>([
|
||||||
|
['sess-a', true],
|
||||||
|
['sess-b', true],
|
||||||
|
]);
|
||||||
|
const sessionTurnHandled = new Set<string>();
|
||||||
|
|
||||||
|
const idleReminderCalls: string[] = [];
|
||||||
|
const moderatorMessages: string[] = [];
|
||||||
|
const api = makeApi();
|
||||||
|
|
||||||
|
registerBeforeMessageWriteHook({
|
||||||
|
api: api as any,
|
||||||
|
baseConfig: { endSymbols: ['🔚'], moderatorBotToken: 'bot-token' } as any,
|
||||||
|
policyState: { channelPolicies: {} },
|
||||||
|
sessionAllowed,
|
||||||
|
sessionChannelId,
|
||||||
|
sessionAccountId,
|
||||||
|
sessionTurnHandled,
|
||||||
|
ensurePolicyStateLoaded: () => {},
|
||||||
|
shouldDebugLog: () => false,
|
||||||
|
ensureTurnOrder: () => {},
|
||||||
|
resolveDiscordUserId: () => undefined,
|
||||||
|
isMultiMessageMode: () => false,
|
||||||
|
sendModeratorMessage: async (_token, _channelId, content) => {
|
||||||
|
moderatorMessages.push(content);
|
||||||
|
return { ok: true };
|
||||||
|
},
|
||||||
|
discussionService: {
|
||||||
|
maybeSendIdleReminder: async (id) => {
|
||||||
|
idleReminderCalls.push(id);
|
||||||
|
},
|
||||||
|
getDiscussion: () => undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const beforeMessageWrite = api.handlers.get('before_message_write');
|
||||||
|
assert.ok(beforeMessageWrite);
|
||||||
|
|
||||||
|
await beforeMessageWrite?.({ message: { role: 'assistant', content: 'NO_REPLY' } }, { sessionKey: 'sess-a' });
|
||||||
|
await beforeMessageWrite?.({ message: { role: 'assistant', content: 'NO_REPLY' } }, { sessionKey: 'sess-b' });
|
||||||
|
|
||||||
|
assert.deepEqual(idleReminderCalls, []);
|
||||||
|
assert.deepEqual(moderatorMessages, []);
|
||||||
|
assert.equal(getTurnDebugInfo(channelId).dormant, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('message_sent skips handoff after discuss-callback has closed the discussion channel', async () => {
|
||||||
|
const channelId = 'discussion-closed-channel';
|
||||||
|
resetTurn(channelId);
|
||||||
|
initTurnOrder(channelId, ['agent-a', 'agent-b']);
|
||||||
|
onNewMessage(channelId, 'human-user', true);
|
||||||
|
|
||||||
|
const state = getTurnDebugInfo(channelId);
|
||||||
|
const currentSpeaker = state.currentSpeaker as string;
|
||||||
|
assert.ok(currentSpeaker);
|
||||||
|
|
||||||
|
const moderatorMessages: string[] = [];
|
||||||
|
const api = makeApi();
|
||||||
|
|
||||||
|
registerMessageSentHook({
|
||||||
|
api: api as any,
|
||||||
|
baseConfig: {
|
||||||
|
endSymbols: ['🔚'],
|
||||||
|
moderatorBotToken: 'bot-token',
|
||||||
|
schedulingIdentifier: '➡️',
|
||||||
|
} as any,
|
||||||
|
policyState: { channelPolicies: {} },
|
||||||
|
sessionChannelId: new Map([['sess-closed', channelId]]),
|
||||||
|
sessionAccountId: new Map([['sess-closed', currentSpeaker]]),
|
||||||
|
sessionTurnHandled: new Set(),
|
||||||
|
ensurePolicyStateLoaded: () => {},
|
||||||
|
resolveDiscordUserId: () => 'discord-user-next',
|
||||||
|
sendModeratorMessage: async (_token, _channelId, content) => {
|
||||||
|
moderatorMessages.push(content);
|
||||||
|
return { ok: true };
|
||||||
|
},
|
||||||
|
discussionService: {
|
||||||
|
isClosedDiscussion: (id) => id === channelId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageSent = api.handlers.get('message_sent');
|
||||||
|
assert.ok(messageSent);
|
||||||
|
|
||||||
|
await messageSent?.({ content: 'NO_REPLY' }, { sessionKey: 'sess-closed', accountId: currentSpeaker, channelId });
|
||||||
|
|
||||||
|
assert.deepEqual(moderatorMessages, []);
|
||||||
|
assert.equal(getTurnDebugInfo(channelId).currentSpeaker, currentSpeaker);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user