refactor: simplify meta workflow — coder → tester → promoter

This commit is contained in:
2026-04-18 07:03:33 +00:00
parent 2a17dcc892
commit 1afce01335
16 changed files with 886 additions and 485 deletions
+215
View File
@@ -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<WolfNightMeta>; // 狼人讨论+投票杀谁
'seer-check': Role<SeerCheckMeta>; // 预言家验人
'witch-action': Role<WitchActionMeta>; // 女巫用药
// 白天阶段
'day-speech': Role<DaySpeechMeta>; // 所有存活玩家依次发言
'vote': Role<VoteMeta>; // 投票放逐
// 特殊
'hunter-shot': Role<HunterShotMeta>; // 猎人开枪(死亡触发)
// 结算
'game-end': Role<GameEndMeta>; // 生成游戏总结
};
```
### 阶段内多玩家执行
每个阶段 Role 内部对多个玩家分别调 LLM:
```typescript
const daySpeechRole: Role<DaySpeechMeta> = 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<WerewolfRoles>,
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)
+2 -6
View File
@@ -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',
@@ -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,
});
+3 -7
View File
@@ -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',
@@ -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,
});
+1 -5
View File
@@ -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',
+410
View File
@@ -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<string> {
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<WolfNightMeta> = 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<SeerCheckMeta> = 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<WitchActionMeta & { gameOver?: boolean }> = 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<DaySpeechMeta> = 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<VoteMeta & { hunterTriggered?: boolean; gameOver?: boolean }> = 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<string, string> = {};
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<string, number> = {};
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<HunterShotMeta & { gameOver?: boolean }> = 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<GameEndMeta> = 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);
+164
View File
@@ -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`);
-4
View File
@@ -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';
+20 -81
View File
@@ -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();
});
});
+6 -33
View File
@@ -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<MetaArchitectMeta>;
coder: Role<MetaCoderMeta>;
reviewer: Role<MetaReviewerMeta>;
tester: Role<MetaTesterMeta>;
promoter: Role<MetaPromoterMeta>;
};
@@ -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;
@@ -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<string, string>;
}>;
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<MetaArchitectMeta> {
return createToolRole<MetaArchitectMeta, WorkflowSpec>(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,
},
};
},
});
}
@@ -40,40 +40,31 @@ export function createMetaCoderRole(
): Role<MetaCoderMeta> {
return createAgentExecutorRole<MetaCoderMeta>(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: 小橘 <xiaoju@shazhou.work>`;
- commit author: 小橘 <xiaoju@shazhou.work>
- 不修改 workflow-rule-adapter.ts 和 workflow-type.ts`;
return { prompt, cwd: repoDir };
},
@@ -20,12 +20,11 @@ export function createMetaPromoterRole(opts: {
return async (
chain: WorkflowMessage[],
): Promise<RoleResult<MetaPromoterMeta>> => {
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="小橘 <xiaoju@shazhou.work>" --allow-empty`,
);
@@ -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<MetaReviewerMeta> {
return createAgentExecutorRole<MetaReviewerMeta>(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,
},
});
}
@@ -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<MetaTesterMeta> {
export function createMetaTesterRole(opts: {
repoDir: string;
}): Role<MetaTesterMeta> {
return async (
chain: WorkflowMessage[],
): Promise<RoleResult<MetaTesterMeta>> => {
// 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 },
};
};
}