diff --git a/docs/werewolf-design.md b/docs/werewolf-design.md new file mode 100644 index 0000000..12961f3 --- /dev/null +++ b/docs/werewolf-design.md @@ -0,0 +1,215 @@ +# 狼人杀 Workflow 设计文档 + +## 概述 + +用 Pulse workflow 实现 AI 狼人杀。每个玩家是一个 Role(背后一个 LLM), +moderator 是主持人控制阶段轮转和胜负判定。 + +## 游戏配置(9人局) + +| 身份 | 数量 | 阵营 | +|------|------|------| +| 狼人 | 3 | 狼 | +| 预言家 | 1 | 好人 | +| 女巫 | 1 | 好人 | +| 猎人 | 1 | 好人 | +| 村民 | 3 | 好人 | + +## 核心设计 + +### 信息可见性 — 关键创新点 + +每个 Role 的 `prepPrompt(chain)` 过滤 chain,只暴露该角色可见的信息: + +```typescript +function filterChainForPlayer(chain: WorkflowMessage[], playerId: string, identity: Identity): WorkflowMessage[] { + return chain.filter(msg => { + const phase = msg.meta?.phase as string; + const target = msg.meta?.visibleTo as string[] | undefined; + + // 公开阶段(白天发言、投票结果、死亡公告)所有人可见 + if (phase === 'day-speech' || phase === 'vote-result' || phase === 'death') return true; + + // 狼人夜晚讨论:只有狼人可见 + if (phase === 'wolf-night' && identity.team === 'wolf') return true; + + // 预言家验人:只有预言家自己可见 + if (phase === 'seer-check' && target?.includes(playerId)) return true; + + // 女巫信息:只有女巫自己可见 + if (phase === 'witch-action' && target?.includes(playerId)) return true; + + // 系统消息(角色分配等):只有目标可见 + if (phase === 'system' && target?.includes(playerId)) return true; + + return false; + }); +} +``` + +### Role 设计 + +不是每个玩家一个 Role。而是 **每个阶段一个 Role**,Role 内部遍历该阶段需要行动的玩家: + +```typescript +type WerewolfRoles = { + // 夜晚阶段 + 'wolf-night': Role; // 狼人讨论+投票杀谁 + 'seer-check': Role; // 预言家验人 + 'witch-action': Role; // 女巫用药 + + // 白天阶段 + 'day-speech': Role; // 所有存活玩家依次发言 + 'vote': Role; // 投票放逐 + + // 特殊 + 'hunter-shot': Role; // 猎人开枪(死亡触发) + + // 结算 + 'game-end': Role; // 生成游戏总结 +}; +``` + +### 阶段内多玩家执行 + +每个阶段 Role 内部对多个玩家分别调 LLM: + +```typescript +const daySpeechRole: Role = async (chain, topicId, store) => { + const state = parseGameState(chain); + const speeches: PlayerSpeech[] = []; + + for (const player of state.alivePlayers) { + // 过滤 chain,只给该玩家可见的信息 + const visibleChain = filterChainForPlayer(chain, player.id, player.identity); + + // 调 LLM(通过 Sigil executor) + const speech = await invokeLlm({ + system: buildPlayerPrompt(player), + messages: visibleChain, + instruction: '现在轮到你发言,分析场上局势,表达你的观点。', + }); + + speeches.push({ playerId: player.id, speech }); + } + + return { + content: speeches.map(s => `【${s.playerId}】${s.speech}`).join('\n\n'), + meta: { speeches, phase: 'day-speech', visibleTo: null }, // 公开 + }; +}; +``` + +### Moderator — 主持人 + +```typescript +function werewolfModerator( + output: ModeratorInput, + topicId: string, +): keyof WerewolfRoles | typeof END { + if (output.role === START) return 'wolf-night'; // 天黑请闭眼 + + const state = output.meta?.gameState as GameState; + + // 胜负判定 + if (state) { + const wolves = state.alive.filter(p => p.team === 'wolf'); + if (wolves.length === 0) return 'game-end'; // 好人胜 + if (wolves.length >= state.alive.length - wolves.length) return 'game-end'; // 狼人胜 + } + + // 阶段轮转 + switch (output.role) { + case 'wolf-night': return 'seer-check'; + case 'seer-check': return 'witch-action'; + case 'witch-action': return 'day-speech'; // 天亮了 + case 'day-speech': return 'vote'; + case 'vote': { + // 猎人被投票出局 → 开枪 + if (state?.lastDeath?.identity === 'hunter') return 'hunter-shot'; + return 'wolf-night'; // 下一夜 + } + case 'hunter-shot': return 'wolf-night'; + case 'game-end': return END; + default: return END; + } +} +``` + +### 游戏状态 + +不存在独立的 state 对象——游戏状态从 chain 重建(event sourcing): + +```typescript +interface GameState { + players: Player[]; // 所有玩家 + alive: Player[]; // 存活玩家 + dead: DeadPlayer[]; // 死亡玩家 + 死因 + day: number; // 第几天 + phase: string; // 当前阶段 + witchPotion: boolean; // 女巫解药是否还在 + witchPoison: boolean; // 女巫毒药是否还在 + lastDeath: DeadPlayer | null; +} + +function parseGameState(chain: WorkflowMessage[]): GameState { + // 从 chain 的 meta 中重建完整游戏状态 + // 每个阶段 Role 的 meta 都带 gameState diff +} +``` + +### 玩家 Prompt 设计 + +每个玩家的 system prompt 包含: +1. **角色身份**:你是预言家/狼人/村民... +2. **性格特征**:随机分配(谨慎/激进/善于伪装/逻辑型...) +3. **游戏规则**:简要规则提醒 +4. **目标**:你的胜利条件 + +```typescript +function buildPlayerPrompt(player: Player): string { + return `你是玩家 ${player.name},身份是【${player.identity.name}】。 +性格特征:${player.personality}。 +阵营:${player.identity.team === 'wolf' ? '狼人阵营' : '好人阵营'}。 +胜利条件:${player.identity.team === 'wolf' ? '淘汰所有好人' : '找出并淘汰所有狼人'}。 +${player.identity.abilities ? `特殊能力:${player.identity.abilities}` : ''} + +重要规则: +- 不要直接暴露自己的身份(除非策略需要) +- 根据场上信息做出合理推理 +- 发言要有逻辑,但也可以有情感和策略`; +} +``` + +## Meta Workflow 集成 + +让 meta workflow 生成这个 werewolf.ts: + +### 任务描述(给 meta workflow 的 __start__ event) + +``` +目标:实现狼人杀 workflow(werewolf.ts + werewolf.test.ts) +位置:packages/pulse/src/workflows/werewolf.ts + +要求: +1. 9人局(3狼人 + 预言家 + 女巫 + 猎人 + 3村民) +2. 信息可见性:chain 过滤,每个玩家只看到该看的 +3. 阶段:wolf-night → seer-check → witch-action → day-speech → vote → (hunter-shot) → 循环 +4. 默认 mock Role(不调 LLM),可注入真 LLM Role +5. Moderator 判胜负 + 阶段轮转 +6. 测试:mock 模式跑完整局,验证阶段流转和胜负判定 + +参考:coding-tdd.ts 的结构(WorkflowType + Roles + Moderator + Factory) +验证:bun test packages/pulse/src/workflows/werewolf.test.ts +``` + +## 后续扩展 + +1. **真 LLM 对战**:注入 Sigil executor 做的 LLM Role,看 AI 之间互相推理 +2. **人类参与**:某个玩家的 Role 改为等待用户输入(device effect 模式) +3. **观战模式**:实时输出每个阶段的公开信息 +4. **复盘**:游戏结束后展示所有信息(包括夜晚行动),像"上帝视角" +5. **更多身份**:守卫、白痴、丘比特... + +--- +小橘 🍊 (NEKO Team) diff --git a/packages/pulse/src/bin/workflow-daemon.ts b/packages/pulse/src/bin/workflow-daemon.ts index 5ea824f..6a9698f 100644 --- a/packages/pulse/src/bin/workflow-daemon.ts +++ b/packages/pulse/src/bin/workflow-daemon.ts @@ -18,10 +18,8 @@ import { createCodingWorkflow } from '../workflows/coding.js'; import { createWorkflowTicker } from '../workflows/index.js'; import { createMetaWorkflow } from '../workflows/meta.js'; import { createCursorRunner } from '../workflows/roles/agent-executor.js'; -import { createMetaArchitectRole } from '../workflows/roles/meta-architect-llm.js'; import { createMetaCoderRole } from '../workflows/roles/meta-coder-cursor.js'; import { createMetaPromoterRole } from '../workflows/roles/meta-promoter.js'; -import { createMetaReviewerRole } from '../workflows/roles/meta-reviewer-cursor.js'; import { createMetaTesterRole } from '../workflows/roles/meta-tester.js'; import { createWorkflowRule } from '../workflows/workflow-rule-adapter.js'; @@ -77,12 +75,10 @@ const logStore = createStore({ const codingWf = createCodingWorkflow(); const codingRule = createWorkflowRule(codingWf, store, logStore); -// 2. Meta workflow (real LLM + Cursor roles) +// 2. Meta workflow (simplified: coder → tester → promoter) const metaWf = createMetaWorkflow({ - architect: createMetaArchitectRole(llm), coder: createMetaCoderRole(cursorRunner, llm, ENGINE_DIR), - reviewer: createMetaReviewerRole(cursorRunner, llm, ENGINE_DIR), - tester: createMetaTesterRole(llm, { repoDir: ENGINE_DIR }), + tester: createMetaTesterRole({ repoDir: ENGINE_DIR }), promoter: createMetaPromoterRole({ repoDir: ENGINE_DIR, remote: 'origin', diff --git a/packages/pulse/src/e2e/meta-coding-optimize.ts b/packages/pulse/src/e2e/meta-coding-optimize.ts index 12e8c1b..39f0173 100644 --- a/packages/pulse/src/e2e/meta-coding-optimize.ts +++ b/packages/pulse/src/e2e/meta-coding-optimize.ts @@ -12,7 +12,6 @@ import { createOpenAiLlmClient } from '../llm-client.js'; import { createStore } from '../store.js'; import { createMetaWorkflow } from '../workflows/meta.js'; import { createCursorRunner } from '../workflows/roles/agent-executor.js'; -import { createMetaArchitectRole } from '../workflows/roles/meta-architect-llm.js'; import { createMetaCoderRole } from '../workflows/roles/meta-coder-cursor.js'; import { createWorkflowRule } from '../workflows/workflow-rule-adapter.js'; @@ -40,7 +39,6 @@ async function main() { objectsDir: join(dir, 'objects'), }); - const architect = createMetaArchitectRole(llm); const cursorRunner = createCursorRunner({ agentBin: `${process.env.HOME}/.local/bin/agent`, }); @@ -53,9 +51,7 @@ async function main() { }); const wf = createMetaWorkflow({ - architect, coder, - reviewer: stubRole as any, tester: stubRole as any, promoter: stubRole as any, }); diff --git a/packages/pulse/src/e2e/meta-full-e2e.ts b/packages/pulse/src/e2e/meta-full-e2e.ts index f78ab31..b4116a3 100644 --- a/packages/pulse/src/e2e/meta-full-e2e.ts +++ b/packages/pulse/src/e2e/meta-full-e2e.ts @@ -1,7 +1,7 @@ /** - * Meta Workflow full e2e — optimize existing coding workflow. + * Meta Workflow full e2e — simplified coder → tester → promoter flow. * - * Flow: architect(LLM) → coder(Cursor) → reviewer(Cursor) → tester(code+LLM) → promoter(git) + * Flow: coder(Agent) → tester(code) → promoter(git) * On failure: revert coding changes, fix meta, rerun. * * 小橘 🍊 (NEKO Team) @@ -14,10 +14,8 @@ import { createOpenAiLlmClient } from '../llm-client.js'; import { createStore } from '../store.js'; import { createMetaWorkflow } from '../workflows/meta.js'; import { createCursorRunner } from '../workflows/roles/agent-executor.js'; -import { createMetaArchitectRole } from '../workflows/roles/meta-architect-llm.js'; import { createMetaCoderRole } from '../workflows/roles/meta-coder-cursor.js'; import { createMetaPromoterRole } from '../workflows/roles/meta-promoter.js'; -import { createMetaReviewerRole } from '../workflows/roles/meta-reviewer-cursor.js'; import { createMetaTesterRole } from '../workflows/roles/meta-tester.js'; import { createWorkflowRule } from '../workflows/workflow-rule-adapter.js'; @@ -52,10 +50,8 @@ async function main() { }); const wf = createMetaWorkflow({ - architect: createMetaArchitectRole(llm), coder: createMetaCoderRole(cursorRunner, llm, REPO_DIR), - reviewer: createMetaReviewerRole(cursorRunner, llm, REPO_DIR), - tester: createMetaTesterRole(llm, { repoDir: REPO_DIR }), + tester: createMetaTesterRole({ repoDir: REPO_DIR }), promoter: createMetaPromoterRole({ repoDir: REPO_DIR, remote: 'gitflare', diff --git a/packages/pulse/src/e2e/meta-optimize-coding.ts b/packages/pulse/src/e2e/meta-optimize-coding.ts index f1fb972..67a8a5f 100644 --- a/packages/pulse/src/e2e/meta-optimize-coding.ts +++ b/packages/pulse/src/e2e/meta-optimize-coding.ts @@ -15,11 +15,9 @@ import { createStore } from '../store.js'; import type { MetaCoderMeta, MetaPromoterMeta, - MetaReviewerMeta, MetaTesterMeta, } from '../workflows/meta.js'; import { createMetaWorkflow } from '../workflows/meta.js'; -import { createMetaArchitectRole } from '../workflows/roles/meta-architect-llm.js'; import { createWorkflowRule } from '../workflows/workflow-rule-adapter.js'; const REPO_DIR = join(import.meta.dir, '../../../..'); @@ -40,18 +38,13 @@ async function main() { timeoutMs: 120_000, }); - const architect = createMetaArchitectRole(llm); - - // Stub remaining roles — we only want architect's analysis for now const stubRole = async () => ({ content: 'stub', meta: {} as any, }); const wf = createMetaWorkflow({ - architect, coder: stubRole as any, - reviewer: stubRole as any, tester: stubRole as any, promoter: stubRole as any, }); diff --git a/packages/pulse/src/e2e/meta-tdd-coding.ts b/packages/pulse/src/e2e/meta-tdd-coding.ts index deb5eb9..d0df7d9 100644 --- a/packages/pulse/src/e2e/meta-tdd-coding.ts +++ b/packages/pulse/src/e2e/meta-tdd-coding.ts @@ -14,10 +14,8 @@ import { createOpenAiLlmClient } from '../llm-client.js'; import { createStore } from '../store.js'; import { createMetaWorkflow } from '../workflows/meta.js'; import { createCursorRunner } from '../workflows/roles/agent-executor.js'; -import { createMetaArchitectRole } from '../workflows/roles/meta-architect-llm.js'; import { createMetaCoderRole } from '../workflows/roles/meta-coder-cursor.js'; import { createMetaPromoterRole } from '../workflows/roles/meta-promoter.js'; -import { createMetaReviewerRole } from '../workflows/roles/meta-reviewer-cursor.js'; import { createMetaTesterRole } from '../workflows/roles/meta-tester.js'; import { createWorkflowRule } from '../workflows/workflow-rule-adapter.js'; @@ -52,10 +50,8 @@ async function main() { }); const wf = createMetaWorkflow({ - architect: createMetaArchitectRole(llm), coder: createMetaCoderRole(cursorRunner, llm, REPO_DIR), - reviewer: createMetaReviewerRole(cursorRunner, llm, REPO_DIR), - tester: createMetaTesterRole(llm, { repoDir: REPO_DIR }), + tester: createMetaTesterRole({ repoDir: REPO_DIR }), promoter: createMetaPromoterRole({ repoDir: REPO_DIR, remote: 'gitflare', diff --git a/packages/pulse/src/e2e/werewolf-live.ts b/packages/pulse/src/e2e/werewolf-live.ts new file mode 100644 index 0000000..39955ac --- /dev/null +++ b/packages/pulse/src/e2e/werewolf-live.ts @@ -0,0 +1,410 @@ +/** + * 狼人杀 LLM 对战 — 接真 LLM 跑一局 + * + * bun run packages/pulse/src/e2e/werewolf-live.ts + * + * 小橘 🍊 (NEKO Team) + */ + +import { createScopedStore, createWorkflowTicker } from '../index.js'; +import { + createWerewolfWorkflow, + createPlayers, + parseGameState, + filterChainForPlayer, + type WolfNightMeta, + type SeerCheckMeta, + type WitchActionMeta, + type DaySpeechMeta, + type VoteMeta, + type HunterShotMeta, + type GameEndMeta, + type Player, + type Identity, + type GameState, +} from '../workflows/werewolf.js'; +import type { Role, WorkflowMessage } from '../workflows/workflow-type.js'; + +// ── LLM Client ───────────────────────────────────────────────── + +const DASHSCOPE_API_KEY = process.env.DASHSCOPE_API_KEY; +if (!DASHSCOPE_API_KEY) throw new Error('DASHSCOPE_API_KEY not set'); + +interface LlmMessage { + role: 'system' | 'user' | 'assistant'; + content: string; +} + +async function callLlm(messages: LlmMessage[], temperature = 0.8): Promise { + const res = await fetch('https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${DASHSCOPE_API_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: 'qwen-plus', + messages, + temperature, + max_tokens: 800, + }), + }); + const data = await res.json() as any; + return data.choices?.[0]?.message?.content ?? ''; +} + +// ── Player Prompts ───────────────────────────────────────────── + +const PERSONALITIES = [ + '谨慎冷静,善于逻辑推理', + '性格激进,喜欢主导局面', + '沉默寡言,关键时刻一击致命', + '热情开朗,善于拉票结盟', + '多疑猜忌,谁都不信', + '善于伪装,演技精湛', + '正义感强,宁杀错不放过', + '老谋深算,喜欢隐藏实力', + '直觉敏锐,靠感觉判断', +]; + +function buildSystemPrompt(player: Player, personality: string): string { + const identityInfo = player.identity.team === 'wolf' + ? `你的身份是【狼人】。你的同伴是其他狼人(玩家1、玩家2、玩家3)。\n胜利条件:淘汰所有好人。\n夜晚你和狼人同伴商量杀谁。白天你需要伪装成好人,不被投票出局。` + : `你的身份是【${player.identity.name}】。\n胜利条件:找出并淘汰所有狼人。\n${player.identity.abilities ? `特殊能力:${player.identity.abilities}` : '你没有特殊能力,靠发言和投票帮助好人阵营。'}`; + + return `你正在玩一局9人狼人杀游戏。你是 ${player.name}。 +性格特征:${personality} + +${identityInfo} + +游戏规则: +- 9人局:3狼人、1预言家、1女巫(解药+毒药各一瓶)、1猎人(死亡时可带走一人)、3村民 +- 夜晚:狼人杀人→预言家验人→女巫行动 +- 白天:所有存活玩家发言→投票放逐一人 +- 不要直接暴露身份,除非有策略目的 +- 发言简洁有力,像真人玩狼人杀一样(50-150字)`; +} + +// ── LLM Roles ────────────────────────────────────────────────── + +const players = createPlayers(); + +function getVisibleHistory(chain: WorkflowMessage[], player: Player): string { + const visible = filterChainForPlayer(chain, player.id, player.identity); + if (visible.length === 0) return '(暂无历史信息)'; + return visible.map(m => m.content).join('\n'); +} + +const llmWolfNight: Role = async (chain) => { + const state = parseGameState(chain); + const wolves = state.alive.filter(p => p.identity.team === 'wolf'); + const goodAlive = state.alive.filter(p => p.identity.team === 'good'); + + if (goodAlive.length === 0) { + return { content: '[狼人夜晚] 无好人可杀', meta: { phase: 'wolf-night', targetId: '' } }; + } + + // 让第一个存活狼人代表决策 + const leadWolf = wolves[0]; + const history = getVisibleHistory(chain, leadWolf); + + const targetList = goodAlive.map(p => `${p.id}(${p.name})`).join('、'); + const response = await callLlm([ + { role: 'system', content: buildSystemPrompt(leadWolf, PERSONALITIES[0]) }, + { role: 'user', content: `${history}\n\n现在是夜晚,你们狼人要商量杀谁。\n存活的非狼人玩家:${targetList}\n\n请直接回复你要杀的玩家ID(如 p4),一个字都不要多说。` }, + ], 0.3); + + // 解析目标 + const match = response.match(/p\d+/); + const targetId = match ? match[0] : goodAlive[0].id; + const target = goodAlive.find(p => p.id === targetId) ?? goodAlive[0]; + + console.log(` 🐺 狼人决定击杀 ${target.name}`); + + return { + content: `[狼人夜晚] 狼人决定击杀 ${target.name}`, + meta: { phase: 'wolf-night', targetId: target.id }, + }; +}; + +const llmSeerCheck: Role = async (chain) => { + const state = parseGameState(chain); + const seer = state.alive.find(p => p.identity.name === '预言家'); + if (!seer) { + return { content: '[预言家已死,跳过]', meta: { phase: 'seer-check', targetId: '', isWolf: false, visibleTo: [] } }; + } + + const others = state.alive.filter(p => p.id !== seer.id); + const history = getVisibleHistory(chain, seer); + const targetList = others.map(p => `${p.id}(${p.name})`).join('、'); + + const response = await callLlm([ + { role: 'system', content: buildSystemPrompt(seer, PERSONALITIES[3]) }, + { role: 'user', content: `${history}\n\n现在轮到你查验身份。可查验的玩家:${targetList}\n\n请直接回复你要查验的玩家ID(如 p1),一个字都不要多说。` }, + ], 0.3); + + const match = response.match(/p\d+/); + const targetId = match ? match[0] : others[0].id; + const target = others.find(p => p.id === targetId) ?? others[0]; + const isWolf = target.identity.team === 'wolf'; + + console.log(` 🔮 预言家查验 ${target.name} → ${isWolf ? '🐺狼人' : '✅好人'}`); + + return { + content: `[预言家查验] ${target.name} 的身份是${isWolf ? '狼人' : '好人'}`, + meta: { phase: 'seer-check', targetId: target.id, isWolf, visibleTo: [seer.id] }, + }; +}; + +const llmWitchAction: Role = async (chain) => { + const state = parseGameState(chain); + const witch = state.alive.find(p => p.identity.name === '女巫'); + + if (!witch) { + const nightDead: string[] = []; + if (state.lastKill) nightDead.push(state.lastKill); + const deadIds = new Set([...state.dead.map(d => d.id), ...nightDead]); + const aliveAfter = state.players.filter(p => !deadIds.has(p.id)); + const wolves = aliveAfter.filter(p => p.identity.team === 'wolf'); + const gameOver = wolves.length === 0 || wolves.length >= aliveAfter.length - wolves.length; + return { + content: '[女巫已死,跳过]', + meta: { phase: 'witch-action', saved: false, poisonTarget: null, visibleTo: [], witchPotion: state.witchPotion, witchPoison: state.witchPoison, gameOver } as any, + }; + } + + let saved = false; + let poisonTarget: string | null = null; + + if (state.lastKill && state.witchPotion) { + const killed = state.players.find(p => p.id === state.lastKill); + const response = await callLlm([ + { role: 'system', content: buildSystemPrompt(witch, PERSONALITIES[7]) }, + { role: 'user', content: `今晚 ${killed?.name ?? '某人'} 被狼人杀害。你有解药,要救吗?回复"救"或"不救"。` }, + ], 0.5); + saved = response.includes('救') && !response.includes('不救'); + if (saved) console.log(` 🧪 女巫救了 ${killed?.name}`); + } + + // 简化:mock 不用毒药 + const nightDead: string[] = []; + if (state.lastKill && !saved) nightDead.push(state.lastKill); + const deadIds = new Set([...state.dead.map(d => d.id), ...nightDead]); + const aliveAfter = state.players.filter(p => !deadIds.has(p.id)); + const wolves = aliveAfter.filter(p => p.identity.team === 'wolf'); + const gameOver = wolves.length === 0 || wolves.length >= aliveAfter.length - wolves.length; + + console.log(` 🧙 女巫${saved ? '使用了解药' : '未使用解药'}`); + + return { + content: `[女巫行动] ${saved ? '使用了解药' : '未使用解药'}`, + meta: { + phase: 'witch-action', saved, poisonTarget, visibleTo: [witch.id], + witchPotion: saved ? false : state.witchPotion, + witchPoison: poisonTarget ? false : state.witchPoison, + gameOver, + } as any, + }; +}; + +const llmDaySpeech: Role = async (chain) => { + const state = parseGameState(chain); + const speeches: { playerId: string; speech: string }[] = []; + + console.log(`\n ☀️ === 第 ${state.day} 天白天 · 发言阶段 ===\n`); + + for (let i = 0; i < state.alive.length; i++) { + const p = state.alive[i]; + const history = getVisibleHistory(chain, p); + const prevSpeeches = speeches.map(s => { + const sp = state.alive.find(pp => pp.id === s.playerId); + return `${sp?.name ?? s.playerId}:${s.speech}`; + }).join('\n'); + + const response = await callLlm([ + { role: 'system', content: buildSystemPrompt(p, PERSONALITIES[i % PERSONALITIES.length]) }, + { role: 'user', content: `${history}\n\n${prevSpeeches ? `前面玩家的发言:\n${prevSpeeches}\n\n` : ''}现在轮到你发言(50-150字),分析局势表达观点:` }, + ]); + + speeches.push({ playerId: p.id, speech: response.trim() }); + console.log(` 💬 ${p.name}(${p.identity.name}):${response.trim()}\n`); + } + + return { + content: speeches.map(s => `【${s.playerId}】${s.speech}`).join('\n\n'), + meta: { phase: 'day-speech', speeches }, + }; +}; + +const llmVote: Role = async (chain) => { + const state = parseGameState(chain); + + // Resolve night deaths for effective alive + const nightDead: string[] = []; + if (state.lastKill) { + const witchMsg = [...chain].reverse().find(m => (m.meta as any)?.phase === 'witch-action'); + if (!(witchMsg?.meta as any)?.saved) nightDead.push(state.lastKill); + } + const deadSet = new Set([...state.dead.map(d => d.id), ...nightDead]); + const effectiveAlive = state.players.filter(p => !deadSet.has(p.id)); + + console.log(` 🗳️ === 投票阶段 ===`); + + const votes: Record = {}; + for (const p of effectiveAlive) { + const others = effectiveAlive.filter(o => o.id !== p.id); + const history = getVisibleHistory(chain, p); + const targetList = others.map(o => `${o.id}(${o.name})`).join('、'); + + const response = await callLlm([ + { role: 'system', content: buildSystemPrompt(p, PERSONALITIES[effectiveAlive.indexOf(p) % PERSONALITIES.length]) }, + { role: 'user', content: `${history}\n\n现在投票环节。请投出你认为最可疑的玩家。可投:${targetList}\n\n直接回复玩家ID(如 p5),一个字都不要多说。` }, + ], 0.3); + + const match = response.match(/p\d+/); + votes[p.id] = match ? match[0] : others[0].id; + } + + // Tally + const tally: Record = {}; + for (const target of Object.values(votes)) { + tally[target] = (tally[target] || 0) + 1; + } + const maxVotes = Math.max(...Object.values(tally)); + const topIds = Object.entries(tally).filter(([, v]) => v === maxVotes).map(([k]) => k); + const eliminatedId = topIds[0]; // 平票取第一个 + + const eliminated = effectiveAlive.find(p => p.id === eliminatedId); + const aliveAfterVote = effectiveAlive.filter(p => p.id !== eliminatedId); + const wolves = aliveAfterVote.filter(p => p.identity.team === 'wolf'); + const gameOver = wolves.length === 0 || wolves.length >= aliveAfterVote.length - wolves.length; + const hunterTriggered = eliminated?.identity.name === '猎人'; + + // Print votes + for (const [voter, target] of Object.entries(votes)) { + const vName = effectiveAlive.find(p => p.id === voter)?.name ?? voter; + const tName = effectiveAlive.find(p => p.id === target)?.name ?? target; + console.log(` 🗳️ ${vName} → ${tName}`); + } + console.log(` ❌ ${eliminated?.name}(${eliminated?.identity.name})被放逐出局!\n`); + + return { + content: `[投票结果] ${eliminated?.name ?? eliminatedId} 被放逐出局`, + meta: { phase: 'vote', votes, eliminatedId, hunterTriggered, gameOver } as any, + }; +}; + +const llmHunterShot: Role = async (chain) => { + const state = parseGameState(chain); + // 猎人已死,从最后的 chain 找到猎人 + const hunter = state.players.find(p => p.identity.name === '猎人')!; + const targetList = state.alive.map(p => `${p.id}(${p.name})`).join('、'); + + const response = await callLlm([ + { role: 'system', content: buildSystemPrompt(hunter, PERSONALITIES[2]) }, + { role: 'user', content: `你被投票出局了!作为猎人你可以开枪带走一人。存活玩家:${targetList}\n\n直接回复玩家ID(如 p1):` }, + ], 0.3); + + const match = response.match(/p\d+/); + const targetId = match ? match[0] : state.alive[0].id; + const target = state.alive.find(p => p.id === targetId) ?? state.alive[0]; + const aliveAfter = state.alive.filter(p => p.id !== target.id); + const wolves = aliveAfter.filter(p => p.identity.team === 'wolf'); + const gameOver = wolves.length === 0 || wolves.length >= aliveAfter.length - wolves.length; + + console.log(` 🔫 猎人开枪带走了 ${target.name}(${target.identity.name})!\n`); + + return { + content: `[猎人开枪] 猎人带走了 ${target.name}`, + meta: { phase: 'hunter-shot', shotTarget: target.id, gameOver } as any, + }; +}; + +const llmGameEnd: Role = async (chain) => { + const state = parseGameState(chain); + const wolves = state.alive.filter(p => p.identity.team === 'wolf'); + const winner = wolves.length === 0 ? 'good' : 'wolf'; + + const summary = ` +🏆 ${winner === 'good' ? '好人阵营' : '狼人阵营'}获胜! + +存活玩家:${state.alive.map(p => `${p.name}(${p.identity.name})`).join('、') || '无'} +死亡玩家:${state.dead.map(d => `${d.name}(${d.identity.name},${d.cause})`).join('、')} + +玩家身份揭晓: +${state.players.map(p => ` ${p.name} → ${p.identity.name}(${p.identity.team === 'wolf' ? '🐺' : '✅'})`).join('\n')} +`; + + console.log(summary); + + return { + content: summary, + meta: { phase: 'game-end', winner, summary }, + }; +}; + +// ── Main ─────────────────────────────────────────────────────── + +async function main() { + console.log('🐺🌙 ============ 狼人杀 LLM 对战 ============\n'); + console.log('玩家身份(上帝视角):'); + const allPlayers = createPlayers(); + allPlayers.forEach(p => console.log(` ${p.name} → ${p.identity.name}(${p.identity.team === 'wolf' ? '🐺' : '✅'})`)); + console.log('\n游戏开始!\n'); + + const wf = createWerewolfWorkflow({ + wolfNightFn: llmWolfNight, + seerCheckFn: llmSeerCheck, + witchActionFn: llmWitchAction as any, + daySpeechFn: llmDaySpeech, + voteFn: llmVote as any, + hunterShotFn: llmHunterShot as any, + gameEndFn: llmGameEnd, + }); + + // Use in-memory store + const { createScopedStore: css } = await import('../index.js'); + const tmpDir = `/tmp/werewolf-${Date.now()}`; + const { mkdirSync } = await import('node:fs'); + mkdirSync(`${tmpDir}/scopes`, { recursive: true }); + mkdirSync(`${tmpDir}/objects`, { recursive: true }); + + const ss = css({ basePath: `${tmpDir}/scopes`, objectsDir: `${tmpDir}/objects` }); + const store = ss.scope('werewolf-game'); + + const { createWorkflowRule } = await import('../workflows/workflow-rule-adapter.js'); + const rule = createWorkflowRule(wf, store); + const ticker = createWorkflowTicker([rule]); + + // Submit start event + const startPayload = JSON.stringify({ title: 'AI 狼人杀', players: allPlayers.map(p => p.name) }); + const hash = await store.putObject(startPayload); + await store.appendEvent({ + occurredAt: Date.now(), + kind: 'werewolf.__start__', + key: 'game-1', + hash, + meta: JSON.stringify({ title: 'AI 狼人杀' }), + }); + + // Run ticks until game ends + let maxTicks = 100; + while (maxTicks-- > 0) { + const result = await ticker(); + if (!result || result.length === 0) { + // Check if game ended + const events = await store.queryByKind('werewolf.game-end', { limit: 1 }); + if (events.length > 0) { + console.log('\n🎮 游戏结束!'); + break; + } + // Might be waiting, tick again + continue; + } + } + + ss.close(); + console.log(`\n数据保存在: ${tmpDir}`); +} + +main().catch(console.error); diff --git a/packages/pulse/src/e2e/werewolf-report.ts b/packages/pulse/src/e2e/werewolf-report.ts new file mode 100644 index 0000000..58b4088 --- /dev/null +++ b/packages/pulse/src/e2e/werewolf-report.ts @@ -0,0 +1,164 @@ +#!/usr/bin/env bun +/** + * 狼人杀战报生成 — 用 Report Workflow (analyst → renderer) + * 读取狼人杀 game 数据,生成 HTML 战报 + * + * bun packages/pulse/src/e2e/werewolf-report.ts [--db /tmp/werewolf-xxx] + * + * 小橘 🍊 (NEKO Team) + */ + +import { mkdtempSync, writeFileSync, readdirSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { createScopedStore, createStore } from '../index.js'; +import { createOpenAiLlmClient } from '../llm-client.js'; +import { createReportWorkflow } from '../workflows/report.js'; +import { createAnalystRole } from '../workflows/roles/analyst-llm.js'; +import { createRendererRole } from '../workflows/roles/renderer-template.js'; +import { createWorkflowRule } from '../workflows/workflow-rule-adapter.js'; + +// ── Args ─────────────────────────────────────────────────────── +let dbDir = process.argv.find(a => a.startsWith('--db='))?.slice(5); + +if (!dbDir) { + // Auto-detect latest /tmp/werewolf-* + const dirs = readdirSync('/tmp').filter(d => d.startsWith('werewolf-')).sort().reverse(); + if (dirs.length > 0) { + dbDir = join('/tmp', dirs[0]); + console.log(`🔍 Auto-detected: ${dbDir}`); + } else { + console.error('Usage: werewolf-report.ts [--db=/tmp/werewolf-xxx]'); + process.exit(1); + } +} + +const baseUrl = process.env.PULSE_LLM_BASE_URL ?? 'https://dashscope.aliyuncs.com/compatible-mode/v1'; +const apiKey = process.env.PULSE_LLM_API_KEY ?? process.env.DASHSCOPE_API_KEY; +const model = process.env.PULSE_LLM_MODEL ?? 'qwen-plus'; + +if (!apiKey) { + console.error('❌ Requires DASHSCOPE_API_KEY or PULSE_LLM_API_KEY'); + process.exit(1); +} + +const t0 = Date.now(); +const ts = () => `[+${((Date.now() - t0) / 1000).toFixed(1)}s]`; + +// ── Read werewolf game events ────────────────────────────────── +const ss = createScopedStore({ basePath: join(dbDir, 'scopes'), objectsDir: join(dbDir, 'objects') }); +const store = ss.scope('werewolf-game'); +const allEvents = await store.getAfter(0); + +console.log(ts(), `Loaded ${allEvents.length} events from werewolf game`); + +if (allEvents.length === 0) { + console.error('No events found!'); + ss.close(); + process.exit(1); +} + +// Build timeline JSON (adapted for werewolf) +const tStart = allEvents[0].occurredAt; +const eventItems = await Promise.all(allEvents.map(async (e, i) => { + const role = e.kind.replace('werewolf.', ''); + const meta = e.meta ? JSON.parse(e.meta) : null; + let content: string | null = null; + if (e.hash) { + try { + const obj = await store.getObject(e.hash); + content = typeof obj === 'string' ? obj : JSON.stringify(obj); + } catch {} + } + return { + id: e.id, + role, + offsetMs: e.occurredAt - tStart, + durationMs: i > 0 ? e.occurredAt - allEvents[i - 1].occurredAt : 0, + meta, + content, + }; +})); + +const timelineJson = JSON.stringify({ + key: 'werewolf-game-1', + totalMs: allEvents[allEvents.length - 1].occurredAt - tStart, + events: eventItems, +}, null, 2); + +ss.close(); +console.log(ts(), `Timeline: ${eventItems.length} phases, ${((JSON.parse(timelineJson).totalMs) / 1000).toFixed(1)}s total`); + +// ── Run report workflow ──────────────────────────────────────── +const tmpDir = mkdtempSync(join(tmpdir(), 'werewolf-report-')); +const reportStore = createStore({ + eventsDbPath: join(tmpDir, 'events.db'), + objectsDir: join(tmpDir, 'objects'), +}); + +const llm = createOpenAiLlmClient({ + baseUrl, + apiKey, + model, + timeoutMs: 120_000, +}); + +const reportWorkflow = createReportWorkflow({ + analystFn: createAnalystRole(llm), + rendererFn: createRendererRole(), +}); + +const rule = createWorkflowRule(reportWorkflow, reportStore); + +// Seed +const hash = await reportStore.putObject(timelineJson); +await reportStore.appendEvent({ + occurredAt: Date.now(), + kind: 'report.__start__', + key: 'report-werewolf-game-1', + hash, + meta: JSON.stringify({ sourceKey: 'werewolf-game-1', type: 'werewolf' }), +}); + +console.log(ts(), 'Report workflow started\n'); + +// Tick until done +let tickNum = 0; +while (tickNum < 5) { + tickNum++; + console.log(`⚡ Tick ${tickNum}`); + const result = await rule.tick(); + if (result.executed.length === 0) { + console.log(' No actions — done!'); + break; + } + for (const a of result.executed) { + console.log(` ✅ ${a.role}: ${a.content?.slice(0, 80)}...`); + } +} + +// Extract HTML report +const reportEvents = await reportStore.getAfter(0); +const rendererEvt = reportEvents.find(e => e.kind === 'report.renderer'); +if (rendererEvt?.hash) { + const html = await reportStore.getObject(rendererEvt.hash) as string; + const outPath = join(tmpDir, 'werewolf-report.html'); + writeFileSync(outPath, html, 'utf-8'); + + // Also copy to workspace + const wsPath = '/home/azureuser/.openclaw/workspace/werewolf-report-v2.html'; + writeFileSync(wsPath, html, 'utf-8'); + console.log(`\n📄 Report: ${wsPath} (${(html.length / 1024).toFixed(1)} KB)`); +} + +// Print analyst findings +const analystEvt = reportEvents.find(e => e.kind === 'report.analyst'); +if (analystEvt?.meta) { + const meta = JSON.parse(analystEvt.meta); + console.log(`\n📊 Score: ${meta.score}/10`); + console.log(`✅ Highlights: ${meta.highlights?.join(', ')}`); + console.log(`⚠️ Issues: ${meta.issues?.length ? meta.issues.join(', ') : 'None'}`); +} + +await reportStore.close(); +console.log(`\n✅ Done in ${((Date.now() - t0) / 1000).toFixed(1)}s`); diff --git a/packages/pulse/src/workflows/index.ts b/packages/pulse/src/workflows/index.ts index a68bd20..88350fd 100644 --- a/packages/pulse/src/workflows/index.ts +++ b/packages/pulse/src/workflows/index.ts @@ -13,10 +13,8 @@ export { type ReviewerMeta, } from './coding.js'; export type { - MetaArchitectMeta, MetaCoderMeta, MetaPromoterMeta, - MetaReviewerMeta, MetaTesterMeta, } from './meta.js'; export { createMetaWorkflow } from './meta.js'; @@ -37,10 +35,8 @@ export type { ToolRoleConfig, } from './roles/llm-role-factory.js'; export { createLlmRole, createToolRole } from './roles/llm-role-factory.js'; -export { createMetaArchitectRole } from './roles/meta-architect-llm.js'; export { createMetaCoderRole } from './roles/meta-coder-cursor.js'; export { createMetaPromoterRole } from './roles/meta-promoter.js'; -export { createMetaReviewerRole } from './roles/meta-reviewer-cursor.js'; export { createMetaTesterRole } from './roles/meta-tester.js'; export { createRendererRole } from './roles/renderer-template.js'; export { createReviewerRole } from './roles/reviewer-cursor.js'; diff --git a/packages/pulse/src/workflows/meta.test.ts b/packages/pulse/src/workflows/meta.test.ts index 180cc73..07bda9f 100644 --- a/packages/pulse/src/workflows/meta.test.ts +++ b/packages/pulse/src/workflows/meta.test.ts @@ -10,10 +10,8 @@ import { join } from 'node:path'; import { createStore } from '../store.js'; import { createMetaWorkflow, - type MetaArchitectMeta, type MetaCoderMeta, type MetaPromoterMeta, - type MetaReviewerMeta, type MetaTesterMeta, } from './meta.js'; import { createWorkflowRule } from './workflow-rule-adapter.js'; @@ -28,23 +26,15 @@ function mockStore() { } describe('Meta Workflow', () => { - test('moderator routes: START→architect→coder→reviewer(approved)→tester(pass)→promoter→END', () => { + test('moderator routes: START→coder→tester(pass)→promoter→END', () => { const wf = createMetaWorkflow({ - architect: async () => ({ - content: '{}', - meta: { workflowName: 'test', roles: ['a'], transitions: 'S→a→E' }, - }), coder: async () => ({ content: 'ok', meta: { filesChanged: ['a.js'], testsPassed: true }, }), - reviewer: async () => ({ - content: 'LGTM', - meta: { verdict: 'approved', comments: 'ok' }, - }), tester: async () => ({ content: 'pass', - meta: { pass: true, liveOutput: 'all pass' }, + meta: { pass: true, reason: '编译通过,测试全绿' }, }), promoter: async () => ({ content: 'done', @@ -52,43 +42,22 @@ describe('Meta Workflow', () => { }), }); - expect(wf.moderator({ role: START, meta: null }, 'x')).toBe('architect'); - expect( - wf.moderator( - { - role: 'architect', - meta: { workflowName: 'test', roles: [], transitions: '' }, - }, - 'x', - ), - ).toBe('coder'); + expect(wf.moderator({ role: START, meta: null }, 'x')).toBe('coder'); expect( wf.moderator( { role: 'coder', meta: { filesChanged: [], testsPassed: true } }, 'x', ), - ).toBe('reviewer'); - expect( - wf.moderator( - { role: 'reviewer', meta: { verdict: 'approved', comments: '' } }, - 'x', - ), ).toBe('tester'); expect( wf.moderator( - { role: 'reviewer', meta: { verdict: 'rejected', comments: 'fix' } }, - 'x', - ), - ).toBe('coder'); - expect( - wf.moderator( - { role: 'tester', meta: { pass: true, liveOutput: '' } }, + { role: 'tester', meta: { pass: true, reason: '通过' } }, 'x', ), ).toBe('promoter'); expect( wf.moderator( - { role: 'tester', meta: { pass: false, liveOutput: 'fail' } }, + { role: 'tester', meta: { pass: false, reason: '失败' } }, 'x', ), ).toBe('coder'); @@ -103,21 +72,13 @@ describe('Meta Workflow', () => { test('mock: full happy path via adapter ticks', async () => { const store = mockStore(); const wf = createMetaWorkflow({ - architect: async () => ({ - content: '{"workflowName":"demo"}', - meta: { workflowName: 'demo', roles: ['a'], transitions: 'S→a→E' }, - }), coder: async () => ({ content: 'files changed', meta: { filesChanged: ['demo.js'], testsPassed: true }, }), - reviewer: async () => ({ - content: 'approved', - meta: { verdict: 'approved', comments: 'clean' }, - }), tester: async () => ({ content: 'all pass', - meta: { pass: true, liveOutput: 'ok' }, + meta: { pass: true, reason: '编译通过,测试全绿' }, }), promoter: async () => ({ content: 'commit abc', @@ -138,54 +99,40 @@ describe('Meta Workflow', () => { // Tick through all roles const roles: string[] = []; - for (let i = 0; i < 7; i++) { + for (let i = 0; i < 5; i++) { const r = await rule.tick(); if (r.executed.length === 0) break; roles.push(...r.executed.map((a) => a.role)); } - expect(roles).toEqual([ - 'architect', - 'coder', - 'reviewer', - 'tester', - 'promoter', - ]); + expect(roles).toEqual(['coder', 'tester', 'promoter']); const events = await store.getAfter(0); - expect(events.length).toBe(6); // __start__ + 5 roles + expect(events.length).toBe(4); // __start__ + 3 roles await store.close(); }); - test('mock: reviewer rejection → retry coder', async () => { + test('mock: tester failure → retry coder', async () => { const store = mockStore(); - let reviewCount = 0; + let testCount = 0; const wf = createMetaWorkflow({ - architect: async () => ({ - content: '{}', - meta: { workflowName: 'x', roles: [], transitions: '' }, - }), coder: async () => ({ content: 'code', meta: { filesChanged: [], testsPassed: true }, }), - reviewer: async () => { - reviewCount++; - if (reviewCount === 1) + tester: async () => { + testCount++; + if (testCount === 1) return { - content: 'fix imports', - meta: { verdict: 'rejected' as const, comments: 'fix imports' }, + content: 'build failed', + meta: { pass: false, reason: '编译失败' }, }; return { content: 'ok', - meta: { verdict: 'approved' as const, comments: 'ok' }, + meta: { pass: true, reason: '编译通过,测试全绿' }, }; }, - tester: async () => ({ - content: 'pass', - meta: { pass: true, liveOutput: 'ok' }, - }), promoter: async () => ({ content: 'done', meta: { commitHash: 'def', pushed: true }, @@ -202,22 +149,14 @@ describe('Meta Workflow', () => { }); const roles: string[] = []; - for (let i = 0; i < 10; i++) { + for (let i = 0; i < 8; i++) { const r = await rule.tick(); if (r.executed.length === 0) break; roles.push(...r.executed.map((a) => a.role)); } - // architect → coder → reviewer(rejected) → coder → reviewer(approved) → tester → promoter - expect(roles).toEqual([ - 'architect', - 'coder', - 'reviewer', - 'coder', - 'reviewer', - 'tester', - 'promoter', - ]); + // coder → tester(failed) → coder → tester(pass) → promoter + expect(roles).toEqual(['coder', 'tester', 'coder', 'tester', 'promoter']); await store.close(); }); }); diff --git a/packages/pulse/src/workflows/meta.ts b/packages/pulse/src/workflows/meta.ts index a59363b..0cd08aa 100644 --- a/packages/pulse/src/workflows/meta.ts +++ b/packages/pulse/src/workflows/meta.ts @@ -2,16 +2,12 @@ * Meta Workflow — workflow for developing workflows. * * Roles: - * architect (LLM) → analyze requirements, output workflow spec - * coder (Cursor) → implement roles + tests + workflow definition - * reviewer (Cursor) → tsc + bun test + code review - * tester (code+LLM) → run live test, LLM judges result - * promoter (code) → git commit + push + promote event + * coder (Agent) → implement + self-test + * tester (code) → CI-level verification, pure code + * promoter (code) → git commit + push + promote event * * Flow: - * START → architect → coder → reviewer - * → reviewer.verdict === 'approved' → tester - * → reviewer.verdict === 'rejected' → coder (retry) + * START → coder → tester * → tester.pass === true → promoter → END * → tester.pass === false → coder (retry) * @@ -28,29 +24,16 @@ import { // ── Meta Types ───────────────────────────────────────────────── -export interface MetaArchitectMeta { - [key: string]: unknown; - workflowName: string; - roles: string[]; - transitions: string; -} - export interface MetaCoderMeta { [key: string]: unknown; filesChanged: string[]; testsPassed: boolean; } -export interface MetaReviewerMeta { - [key: string]: unknown; - verdict: 'approved' | 'rejected'; - comments: string; -} - export interface MetaTesterMeta { [key: string]: unknown; pass: boolean; - liveOutput: string; + reason: string; } export interface MetaPromoterMeta { @@ -60,9 +43,7 @@ export interface MetaPromoterMeta { } export type MetaWorkflowRoles = { - architect: Role; coder: Role; - reviewer: Role; tester: Role; promoter: Role; }; @@ -75,17 +56,9 @@ function metaModerator( ): (keyof MetaWorkflowRoles & string) | typeof END { switch (input.role) { case START: - return 'architect'; - case 'architect': return 'coder'; case 'coder': - return 'reviewer'; - case 'reviewer': { - const meta = input.meta as MetaReviewerMeta | null; - const verdict = meta?.verdict; - // TODO: track retry count via chain length instead of meta field - return verdict === 'approved' ? 'tester' : 'coder'; - } + return 'tester'; case 'tester': { const meta = input.meta as MetaTesterMeta | null; const pass = meta?.pass; diff --git a/packages/pulse/src/workflows/roles/meta-architect-llm.ts b/packages/pulse/src/workflows/roles/meta-architect-llm.ts deleted file mode 100644 index 2af609b..0000000 --- a/packages/pulse/src/workflows/roles/meta-architect-llm.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Meta Architect role — LLM analyzes workflow requirements and outputs a spec. - * Uses createToolRole factory. - * - * 小橘 🍊 (NEKO Team) - */ - -import type { LlmClient } from '../../llm-client.js'; -import type { MetaArchitectMeta } from '../meta.js'; -import type { Role } from '../workflow-type.js'; -import { createToolRole } from './llm-role-factory.js'; - -const SYSTEM_PROMPT = `你是 Pulse Council v2 的 workflow 架构师。 - -你收到用户对 workflow 的需求描述。骨架文件已通过 scaffold 生成,你的任务是分析需求,输出修改规范。 - -设计原则(严格遵守): -1. Role 是纯函数,返回 { content, meta },不写 event -2. kind = {workflow}.{role},如 coding.architect -3. content 存 CAS(大内容),meta 只放决策信号 -4. moderator 是状态机转换函数,根据上一个 role 的 meta 决定下一步 -5. LLM role 用 createLlmRole/createToolRole 工厂 -6. Agent role 用 createAgentExecutorRole 工厂 -7. 纯代码 role 直接实现 Role 接口 - -参考现有 workflow: -- coding: START → architect(LLM) → coder(Cursor) → reviewer(Cursor) → END -- report: START → analyst(LLM) → renderer(代码模板) → END - -骨架已存在,你需要说明要改什么。用 extract_spec tool 输出你的设计。`; - -const SPEC_TOOL = { - type: 'function' as const, - function: { - name: 'extract_spec', - description: '输出 workflow 设计规范', - parameters: { - type: 'object', - properties: { - workflowName: { - type: 'string', - description: 'Workflow 名称(用作 event kind 前缀)', - }, - description: { - type: 'string', - description: '一段话描述 workflow 目标', - }, - roles: { - type: 'array', - items: { - type: 'object', - properties: { - name: { type: 'string', description: 'Role 名称' }, - type: { - type: 'string', - enum: ['llm', 'llm-tool', 'agent', 'code'], - description: '实现类型', - }, - description: { type: 'string', description: '职责描述' }, - inputFrom: { - type: 'string', - description: '从哪个 role 获取输入', - }, - outputMeta: { - type: 'object', - description: 'meta 字段定义(field: type)', - additionalProperties: { type: 'string' }, - }, - }, - required: ['name', 'type', 'description'], - }, - }, - transitions: { - type: 'array', - items: { - type: 'object', - properties: { - from: { type: 'string', description: '源 role(START 为起点)' }, - to: { type: 'string', description: '目标 role(END 为终点)' }, - condition: { - type: 'string', - description: '条件描述(如 verdict === approved)', - }, - }, - required: ['from', 'to'], - }, - description: '状态机转换规则', - }, - acceptanceCriteria: { - type: 'array', - items: { type: 'string' }, - description: '验收标准', - }, - }, - required: [ - 'workflowName', - 'description', - 'roles', - 'transitions', - 'acceptanceCriteria', - ], - }, - }, -}; - -interface WorkflowSpec { - workflowName: string; - description: string; - roles: Array<{ - name: string; - type: 'llm' | 'llm-tool' | 'agent' | 'code'; - description: string; - inputFrom?: string; - outputMeta?: Record; - }>; - transitions: Array<{ - from: string; - to: string; - condition?: string; - }>; - acceptanceCriteria: string[]; -} - -const DEFAULT_SPEC: WorkflowSpec = { - workflowName: 'unknown', - description: '设计解析失败', - roles: [], - transitions: [], - acceptanceCriteria: [], -}; - -export function createMetaArchitectRole( - llm: LlmClient, -): Role { - return createToolRole(llm, { - systemPrompt: SYSTEM_PROMPT, - buildUserMessage: (chain) => { - const startMsg = chain.find((m) => m.role === '__start__'); - return `Workflow 需求:\n\n${startMsg?.content ?? '(无描述)'}`; - }, - tool: SPEC_TOOL, - defaultResult: DEFAULT_SPEC, - toRoleResult: (spec) => { - const transStr = spec.transitions - .map((t) => `${t.from}→${t.to}${t.condition ? `(${t.condition})` : ''}`) - .join(', '); - return { - content: JSON.stringify(spec, null, 2), - meta: { - workflowName: spec.workflowName, - roles: spec.roles.map((r) => r.name), - transitions: transStr, - }, - }; - }, - }); -} diff --git a/packages/pulse/src/workflows/roles/meta-coder-cursor.ts b/packages/pulse/src/workflows/roles/meta-coder-cursor.ts index 4664017..f34a607 100644 --- a/packages/pulse/src/workflows/roles/meta-coder-cursor.ts +++ b/packages/pulse/src/workflows/roles/meta-coder-cursor.ts @@ -40,40 +40,31 @@ export function createMetaCoderRole( ): Role { return createAgentExecutorRole(runner, llm, { prepPrompt: (chain, _topicId) => { - const architectMsg = chain.find((m) => m.role === 'architect'); - const spec = architectMsg?.content ?? '{}'; - const reviewerMsg = [...chain] - .reverse() - .find((m) => m.role === 'reviewer'); - const reviewFeedback = reviewerMsg - ? `\n\n## 上次 Review 反馈\n${reviewerMsg.content}` - : ''; - - const prompt = `# 任务:实现 Pulse Workflow(在已有骨架上修改) - -## 设计规范 -${spec} -${reviewFeedback} + const startMsg = chain.find((m) => m.role === '__start__'); + const taskDescription = startMsg?.content ?? ''; + + // 如果有 tester 失败反馈,附加 + const testerMsg = [...chain].reverse().find((m) => m.role === 'tester'); + const testerFeedback = testerMsg ? `\n\n## 上次验证失败\n${testerMsg.content}` : ''; + + const prompt = `# 任务 +${taskDescription} +${testerFeedback} ## 参考 -- 先阅读 docs/workflow-spec.md -- 参考 packages/pulse/src/workflows/coding.ts 和 report.ts -- 骨架文件已通过 scaffold 生成,你需要在已有文件上填充实现 +- 先阅读项目结构了解上下文 +- 参考已有代码风格 ## 步骤 -1. 查看已有骨架文件(workflow 定义 + role stubs + test) -2. 填充 meta types 具体字段 -3. 实现每个 role(用 llm-role-factory 或 agent-executor 工厂) -4. 完善 moderator 状态转换逻辑(特别是条件分支) -5. 完善测试用例 -6. 更新 index.ts barrel exports -7. 运行 \`cd packages/pulse && bun run build\` -8. 运行 \`bun test packages/pulse/src/workflows/\` +1. 理解任务需求 +2. 写代码实现 +3. 运行 \`bun run build\` 确认编译通过 +4. 运行测试确认通过 +5. 不要 commit,让 workflow 处理 ## 约束 -- 不修改 workflow-rule-adapter.ts 和 workflow-type.ts -- Role 是纯函数 -- commit author: 小橘 `; +- commit author: 小橘 +- 不修改 workflow-rule-adapter.ts 和 workflow-type.ts`; return { prompt, cwd: repoDir }; }, diff --git a/packages/pulse/src/workflows/roles/meta-promoter.ts b/packages/pulse/src/workflows/roles/meta-promoter.ts index 0fa3732..0ba5239 100644 --- a/packages/pulse/src/workflows/roles/meta-promoter.ts +++ b/packages/pulse/src/workflows/roles/meta-promoter.ts @@ -20,12 +20,11 @@ export function createMetaPromoterRole(opts: { return async ( chain: WorkflowMessage[], ): Promise> => { - const architectMsg = chain.find((m) => m.role === 'architect'); + const startMsg = chain.find((m) => m.role === '__start__'); let workflowName = 'unknown'; - try { - const spec = JSON.parse(architectMsg?.content ?? '{}'); - workflowName = spec.workflowName ?? 'unknown'; - } catch {} + // 尝试从 task 描述里提取名字(第一行或 JSON 里的 name 字段) + const firstLine = (startMsg?.content ?? '').split('\n')[0]; + workflowName = firstLine.slice(0, 50) || 'unknown'; const cwd = opts.repoDir; const exec = (cmd: string) => @@ -33,7 +32,7 @@ export function createMetaPromoterRole(opts: { // Stage + commit exec('git add -A'); - const commitMsg = `feat: ${workflowName} workflow — auto-generated by meta-workflow`; + const commitMsg = `refactor: simplify meta workflow — coder → tester → promoter`; exec( `git commit -m "${commitMsg}" --author="小橘 " --allow-empty`, ); diff --git a/packages/pulse/src/workflows/roles/meta-reviewer-cursor.ts b/packages/pulse/src/workflows/roles/meta-reviewer-cursor.ts deleted file mode 100644 index 07a3783..0000000 --- a/packages/pulse/src/workflows/roles/meta-reviewer-cursor.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Meta Reviewer role — uses Cursor Agent to review workflow implementation. - * Uses createAgentExecutorRole with LLM₂ meta parsing. - * - * 小橘 🍊 (NEKO Team) - */ - -import type { LlmClient } from '../../llm-client.js'; -import type { MetaReviewerMeta } from '../meta.js'; -import type { Role } from '../workflow-type.js'; -import { type AgentRunner, createAgentExecutorRole } from './agent-executor.js'; - -const PARSE_META_TOOL = { - type: 'function' as const, - function: { - name: 'extract_reviewer_meta', - description: 'Extract review result metadata', - parameters: { - type: 'object', - properties: { - verdict: { - type: 'string', - enum: ['approved', 'rejected'], - description: 'Review verdict', - }, - comments: { - type: 'string', - description: 'Review comments summary', - }, - }, - required: ['verdict', 'comments'], - }, - }, -}; - -export function createMetaReviewerRole( - runner: AgentRunner, - llm: LlmClient, - repoDir: string, -): Role { - return createAgentExecutorRole(runner, llm, { - prepPrompt: (chain, _topicId) => { - const architectMsg = chain.find((m) => m.role === 'architect'); - const spec = architectMsg?.content ?? '{}'; - - const prompt = `# 任务:审查 Pulse Workflow 实现 - -## 原始设计规范 -${spec} - -## 审查步骤 -1. \`cd packages/pulse && bun run build\` — 编译 -2. \`bun test packages/pulse/src/workflows/\` — 测试 -3. 检查代码质量: - - Role 是否纯函数(不写 event) - - moderator 状态转换是否正确 - - meta 字段是否只包含决策信号 - - LLM role 是否用了 llm-role-factory - - barrel exports 是否更新 -4. 对比设计规范验收标准`; - - return { prompt, cwd: repoDir }; - }, - parseMeta: { - system: '从 Cursor Agent 的审查输出中提取 review 结果。', - tool: PARSE_META_TOOL, - parse: (args: string) => { - const parsed = JSON.parse(args); - return { - verdict: parsed.verdict === 'approved' ? 'approved' : 'rejected', - comments: parsed.comments ?? '', - } as MetaReviewerMeta; - }, - defaultMeta: (_output: string) => - ({ - verdict: 'rejected', - comments: '无法解析审查结果', - }) as MetaReviewerMeta, - }, - }); -} diff --git a/packages/pulse/src/workflows/roles/meta-tester.ts b/packages/pulse/src/workflows/roles/meta-tester.ts index 0422529..3d4791a 100644 --- a/packages/pulse/src/workflows/roles/meta-tester.ts +++ b/packages/pulse/src/workflows/roles/meta-tester.ts @@ -1,90 +1,65 @@ /** - * Meta Tester role — runs build+test, then LLM judges the result. + * Meta Tester role — pure code build+test validation. + * No LLM needed, just exit codes and output parsing. * * 小橘 🍊 (NEKO Team) */ import { execSync } from 'node:child_process'; import { join } from 'node:path'; -import type { LlmClient } from '../../llm-client.js'; import type { MetaTesterMeta } from '../meta.js'; import type { Role, RoleResult, WorkflowMessage } from '../workflow-type.js'; -const JUDGE_PROMPT = `你是 Pulse 工作流的测试评审官。 - -你收到一个 workflow 的 build + test 输出,需要判断是否通过。 - -判断标准: -1. 编译是否成功(无 TS 错误) -2. 测试是否全部通过(0 fail) -3. 是否有运行时异常 - -用 judge_result tool 输出判断。`; - -const JUDGE_TOOL = { - type: 'function' as const, - function: { - name: 'judge_result', - description: '判断测试结果', - parameters: { - type: 'object', - properties: { - pass: { type: 'boolean', description: '是否通过' }, - reason: { type: 'string', description: '判断理由' }, - }, - required: ['pass', 'reason'], - }, - }, -}; - -export function createMetaTesterRole( - llm: LlmClient, - opts: { repoDir: string }, -): Role { +export function createMetaTesterRole(opts: { + repoDir: string; +}): Role { return async ( chain: WorkflowMessage[], ): Promise> => { - // Step 1: Run build + test - let testOutput: string; + // Step 1: Build + let buildOk = false; + let buildOutput = ''; try { - testOutput = execSync( - 'bun run build 2>&1 && cd ../.. && bun test packages/pulse/src/workflows/ 2>&1', - { - cwd: join(opts.repoDir, 'packages/pulse'), - timeout: 60_000, - encoding: 'utf-8', - }, - ); + buildOutput = execSync('bun run build 2>&1', { + cwd: join(opts.repoDir, 'packages/pulse'), + timeout: 60_000, + encoding: 'utf-8', + }); + buildOk = true; } catch (err: any) { - testOutput = err.stdout ?? err.message; + buildOutput = err.stdout ?? err.message; } - // Step 2: LLM judges - const resp = await llm.chat({ - messages: [ - { role: 'system', content: JUDGE_PROMPT }, - { role: 'user', content: testOutput.slice(0, 4000) }, - ], - tools: [JUDGE_TOOL], - tool_choice: 'required', - }); - - let pass = false; - let reason = '判断失败'; - const toolCall = resp.tool_calls?.find( - (tc) => tc.function.name === 'judge_result', - ); - if (toolCall) { + // Step 2: Test (only if build succeeded) + let testOk = false; + let testOutput = ''; + if (buildOk) { try { - const parsed = JSON.parse(toolCall.function.arguments); - pass = parsed.pass ?? false; - reason = parsed.reason ?? ''; - } catch {} + testOutput = execSync( + 'bun test packages/pulse/src/workflows/ 2>&1', + { + cwd: opts.repoDir, + timeout: 60_000, + encoding: 'utf-8', + }, + ); + testOk = true; + } catch (err: any) { + testOutput = err.stdout ?? err.message; + } } + const pass = buildOk && testOk; + const output = buildOk ? testOutput : buildOutput; + const reason = pass + ? '编译通过,测试全绿' + : buildOk + ? '测试失败' + : '编译失败'; + return { - content: `${reason}\n\n---\n${testOutput.slice(0, 2000)}`, - meta: { pass, liveOutput: testOutput.slice(0, 2000) }, + content: `${reason}\n\n---\n${output.slice(0, 2000)}`, + meta: { pass, reason }, }; }; }