refactor: simplify meta workflow — coder → tester → promoter
This commit is contained in:
@@ -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)
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
@@ -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`);
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user