refactor #22

Merged
hzhang merged 33 commits from refactor into main 2026-04-10 07:49:57 +00:00
4 changed files with 112 additions and 10 deletions
Showing only changes of commit b40838f259 - Show all commits

View File

@@ -255,10 +255,11 @@ After callback:
[Discussion Idle]
No agent responded in the latest discussion round.
If the discussion goal has been achieved, the initiator should now:
If the discussion goal has already been achieved, the initiator should now:
1. write the discussion summary to a file in the workspace
2. call discuss-callback with the summary file path
This reminder does not mean the discussion was automatically summarized or closed.
If more discussion is still needed, continue the discussion in this channel.
```
@@ -284,6 +285,7 @@ If more discussion is still needed, continue the discussion in this channel.
This discussion channel has been closed.
It is now kept for archive/reference only.
Further discussion in this channel is ignored.
If follow-up work is needed, continue it from the origin work channel instead.
```
这部分实现明确采用已有“在指定 session 上临时覆盖为 no-reply 模型”的方式,而不是修改 Agent 的全局默认模型。

View File

@@ -152,9 +152,9 @@
- [x] 模板中明确 `discuss-callback(summaryPath)` 调用要求
#### A10.2 idle reminder
- [. ] 定稿 discussion idle 模板
- [. ] 模板中提醒 initiator写总结文件并 callback
- [. ] 避免提醒文案歧义或像自动总结器
- [x] 定稿 discussion idle 模板
- [x] 模板中提醒 initiator写总结文件并 callback
- [x] 避免提醒文案歧义或像自动总结器
#### A10.3 origin callback message
- [x] 定稿发回原工作 channel 的结果通知模板
@@ -163,8 +163,8 @@
- [x] 模板中明确“继续基于该总结文件推进原任务”
#### A10.4 closed reply
- [. ] 定稿 closed channel 固定回复模板
- [. ] 明确 channel 已关闭,仅做留档使用
- [x] 定稿 closed channel 固定回复模板
- [x] 明确 channel 已关闭,仅做留档使用
### A11. `discuss-callback` 详细校验任务
- [x] 校验当前 channel 必须是 discussion channel
@@ -212,9 +212,9 @@
- [x] 测试绝对路径越界失败
#### A13.5 回调链路测试
- [. ] 测试 callback 成功后 moderator 在 origin channel 发出通知
- [. ] 测试 origin channel 收到路径后能继续原工作流
- [. ] 测试 discussion channel 后续只保留留档行为
- [x] 测试 callback 成功后 moderator 在 origin channel 发出通知
- [ ] 测试 origin channel 收到路径后能继续原工作流
- [x] 测试 discussion channel 后续只保留留档行为
#### A13.6 文档交付
- [. ] 根据最终代码实现更新 `plans/CSM.md`

View File

@@ -31,10 +31,11 @@ export function buildDiscussionIdleReminderMessage(): string {
"[Discussion Idle]",
"",
"No agent responded in the latest discussion round.",
"If the discussion goal has been achieved, the initiator should now:",
"If the discussion goal has already been achieved, the initiator should now:",
"1. write the discussion summary to a file in the workspace",
"2. call discuss-callback with the summary file path",
"",
"This reminder does not mean the discussion was automatically summarized or closed.",
"If more discussion is still needed, continue the discussion in this channel.",
].join("\n");
}
@@ -46,6 +47,7 @@ export function buildDiscussionClosedMessage(): string {
"This discussion channel has been closed.",
"It is now kept for archive/reference only.",
"Further discussion in this channel is ignored.",
"If follow-up work is needed, continue it from the origin work channel instead.",
].join("\n");
}

View File

@@ -5,6 +5,7 @@ import os from 'node:os';
import path from 'node:path';
import { createDiscussionService } from '../plugin/core/discussion-service.ts';
import { buildDiscussionClosedMessage, buildDiscussionOriginCallbackMessage } from '../plugin/core/discussion-messages.ts';
function makeLogger() {
return {
@@ -244,3 +245,100 @@ test('handleCallback rejects an absolute path outside the initiator workspace',
/summaryPath must stay inside the initiator workspace/,
);
});
test('handleCallback notifies the origin channel with the resolved summary path', async () => {
const workspace = makeWorkspace();
const summaryRelPath = path.join('plans', 'discussion-summary.md');
const summaryAbsPath = path.join(workspace, summaryRelPath);
fs.mkdirSync(path.dirname(summaryAbsPath), { recursive: true });
fs.writeFileSync(summaryAbsPath, '# done\n');
const fetchCalls: Array<{ url: string; body: any }> = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
const body = init?.body ? JSON.parse(String(init.body)) : undefined;
fetchCalls.push({ url: String(url), body });
return new Response(JSON.stringify({ id: `msg-${fetchCalls.length}` }), { status: 200 });
}) as typeof fetch;
try {
const service = createDiscussionService({
api: makeApi() as any,
workspaceRoot: workspace,
moderatorBotToken: 'bot-token',
forceNoReplyForSession: () => {},
});
await service.initDiscussion({
discussionChannelId: 'discussion-origin-1',
originChannelId: 'origin-8',
initiatorAgentId: 'agent-theta',
initiatorSessionId: 'session-theta',
discussGuide: 'Notify the origin channel.',
});
await service.handleCallback({
channelId: 'discussion-origin-1',
summaryPath: summaryRelPath,
callerAgentId: 'agent-theta',
callerSessionKey: 'session-theta',
});
assert.equal(fetchCalls.length, 2);
assert.equal(fetchCalls[1]?.url, 'https://discord.com/api/v10/channels/origin-8/messages');
assert.equal(
fetchCalls[1]?.body?.content,
buildDiscussionOriginCallbackMessage(fs.realpathSync.native(summaryAbsPath), 'discussion-origin-1'),
);
} finally {
globalThis.fetch = originalFetch;
}
});
test('maybeReplyClosedChannel sends the archive-only closed message for later channel activity', async () => {
const workspace = makeWorkspace();
const summaryRelPath = 'summary.md';
const summaryAbsPath = path.join(workspace, summaryRelPath);
fs.writeFileSync(summaryAbsPath, 'closed\n');
const fetchCalls: Array<{ url: string; body: any }> = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
const body = init?.body ? JSON.parse(String(init.body)) : undefined;
fetchCalls.push({ url: String(url), body });
return new Response(JSON.stringify({ id: `msg-${fetchCalls.length}` }), { status: 200 });
}) as typeof fetch;
try {
const service = createDiscussionService({
api: makeApi() as any,
workspaceRoot: workspace,
moderatorBotToken: 'bot-token',
moderatorUserId: 'moderator-user',
forceNoReplyForSession: () => {},
});
await service.initDiscussion({
discussionChannelId: 'discussion-closed-1',
originChannelId: 'origin-9',
initiatorAgentId: 'agent-iota',
initiatorSessionId: 'session-iota',
discussGuide: 'Close and archive.',
});
await service.handleCallback({
channelId: 'discussion-closed-1',
summaryPath: summaryRelPath,
callerAgentId: 'agent-iota',
callerSessionKey: 'session-iota',
});
const handled = await service.maybeReplyClosedChannel('discussion-closed-1', 'human-user');
assert.equal(handled, true);
assert.equal(fetchCalls.length, 3);
assert.equal(fetchCalls[2]?.url, 'https://discord.com/api/v10/channels/discussion-closed-1/messages');
assert.equal(fetchCalls[2]?.body?.content, buildDiscussionClosedMessage());
} finally {
globalThis.fetch = originalFetch;
}
});