@agents-uni/zhenhuan 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Zhen Huan CLI - 后宫命令行工具
3
+ */
4
+ import { Command } from 'commander';
5
+ import chalk from 'chalk';
6
+ import { PalaceOrchestrator } from '../orchestrator/index.js';
7
+ import { startServer } from '../server/index.js';
8
+ const program = new Command();
9
+ program
10
+ .name('zhenhuan')
11
+ .description('甄嬛后宫 Agent 赛马系统 —— 你是皇帝,Agent 为你竞争')
12
+ .version('0.1.0');
13
+ // ─── serve ──────────────────────────────────
14
+ program
15
+ .command('serve')
16
+ .description('启动后宫服务器')
17
+ .option('-p, --port <port>', '端口号', '8089')
18
+ .option('-s, --spec <path>', '规范文件路径', 'universe.yaml')
19
+ .action(async (opts) => {
20
+ // Auto-register in uni-registry on serve
21
+ try {
22
+ const { resolve } = await import('node:path');
23
+ const { registerUni, parseSpecFile } = await import('@agents-uni/core');
24
+ const specPath = resolve(opts.spec);
25
+ const config = parseSpecFile(specPath);
26
+ registerUni(config, specPath);
27
+ console.log(chalk.gray(` ✓ 已注册到 uni-registry`));
28
+ }
29
+ catch {
30
+ // Registry registration is non-critical
31
+ }
32
+ await startServer({
33
+ port: parseInt(opts.port, 10),
34
+ specPath: opts.spec,
35
+ });
36
+ });
37
+ // ─── status ─────────────────────────────────
38
+ program
39
+ .command('status')
40
+ .description('查看后宫状态')
41
+ .option('-s, --spec <path>', '规范文件路径', 'universe.yaml')
42
+ .action(async (opts) => {
43
+ const orchestrator = await PalaceOrchestrator.fromSpec(opts.spec);
44
+ const state = orchestrator.getState();
45
+ console.log(chalk.yellow('\n═══ 后宫品级表 ═══\n'));
46
+ // Sort by rank level descending
47
+ const sorted = [...state.agents].sort((a, b) => b.rankLevel - a.rankLevel);
48
+ for (const agent of sorted) {
49
+ const statusIcon = agent.status === 'active' || agent.status === 'idle' ? '●' : '○';
50
+ const statusColor = agent.status === 'active' || agent.status === 'idle' ? chalk.green : chalk.red;
51
+ console.log(` ${statusColor(statusIcon)} ${chalk.bold(agent.name.padEnd(10))} ` +
52
+ `${chalk.cyan(agent.rank.padEnd(6))} ` +
53
+ `ELO: ${chalk.yellow(String(agent.elo).padStart(4))} ` +
54
+ `圣宠: ${chalk.magenta(String(agent.favor).padStart(3))}`);
55
+ }
56
+ if (state.factions.length > 0) {
57
+ console.log(chalk.yellow('\n═══ 势力格局 ═══\n'));
58
+ for (const faction of state.factions) {
59
+ console.log(` ${chalk.bold(faction.leader)} 派系: ` +
60
+ `${faction.members.join(', ')} ` +
61
+ `(影响力: ${chalk.cyan(String(faction.influence))})`);
62
+ }
63
+ }
64
+ if (state.coldPalaceInmates.length > 0) {
65
+ console.log(chalk.yellow('\n═══ 冷宫 ═══\n'));
66
+ for (const inmate of state.coldPalaceInmates) {
67
+ console.log(` ${chalk.gray('○')} ${inmate}`);
68
+ }
69
+ }
70
+ console.log();
71
+ });
72
+ // ─── leaderboard ────────────────────────────
73
+ program
74
+ .command('leaderboard')
75
+ .description('查看 ELO 排行榜')
76
+ .option('-s, --spec <path>', '规范文件路径', 'universe.yaml')
77
+ .action(async (opts) => {
78
+ const orchestrator = await PalaceOrchestrator.fromSpec(opts.spec);
79
+ const board = orchestrator.getLeaderboard();
80
+ console.log(chalk.yellow('\n═══ ELO 排行榜 ═══\n'));
81
+ console.log(` ${chalk.gray('排名')} ${chalk.gray('ID'.padEnd(16))} ` +
82
+ `${chalk.gray('ELO'.padStart(5))} ${chalk.gray('胜'.padStart(3))} ` +
83
+ `${chalk.gray('负'.padStart(3))} ${chalk.gray('胜率'.padStart(5))}`);
84
+ console.log(chalk.gray(' ─'.repeat(20)));
85
+ board.forEach((record, index) => {
86
+ const winRate = record.matchCount > 0
87
+ ? (record.winCount / record.matchCount * 100).toFixed(0) + '%'
88
+ : ' -';
89
+ const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : ' ';
90
+ console.log(` ${medal} ${chalk.bold(record.agentId.padEnd(16))} ` +
91
+ `${chalk.yellow(String(record.rating).padStart(5))} ` +
92
+ `${chalk.green(String(record.winCount).padStart(3))} ` +
93
+ `${chalk.red(String(record.lossCount).padStart(3))} ` +
94
+ `${winRate.padStart(5)}`);
95
+ });
96
+ console.log();
97
+ });
98
+ // ─── court ──────────────────────────────────
99
+ program
100
+ .command('court')
101
+ .description('召开朝会')
102
+ .option('-s, --spec <path>', '规范文件路径', 'universe.yaml')
103
+ .action(async (opts) => {
104
+ const orchestrator = await PalaceOrchestrator.fromSpec(opts.spec);
105
+ console.log(chalk.yellow('\n═══ 朝会开始 ═══\n'));
106
+ await orchestrator.runCourtAssembly();
107
+ const state = orchestrator.getState();
108
+ for (const event of state.recentEvents.slice(-5)) {
109
+ console.log(` ${chalk.gray(event.timestamp.slice(0, 19))} ${event.description}`);
110
+ }
111
+ console.log(chalk.yellow('\n═══ 朝会结束 ═══\n'));
112
+ });
113
+ // ─── select ────────────────────────────
114
+ program
115
+ .command('select')
116
+ .description('选秀 — 一键注册新 Agent 并加入后宫竞技')
117
+ .requiredOption('--id <id>', 'Agent ID')
118
+ .requiredOption('--name <name>', 'Agent 名称')
119
+ .option('--role <role>', '初始品级', '答应')
120
+ .option('--rank <rank>', '品级数值 (10=答应, 20=常在, ...)', '10')
121
+ .option('--register', '同时注册到 openclaw.json', false)
122
+ .option('--openclaw-dir <dir>', 'OpenClaw 目录', '')
123
+ .option('-s, --spec <path>', '规范文件路径', 'universe.yaml')
124
+ .action(async (opts) => {
125
+ const orchestrator = await PalaceOrchestrator.fromSpec(opts.spec);
126
+ const agentDef = {
127
+ id: opts.id,
128
+ name: opts.name,
129
+ role: { title: opts.role, duties: [], permissions: [] },
130
+ rank: parseInt(opts.rank, 10),
131
+ };
132
+ console.log(chalk.yellow('\n═══ 选秀大典 ═══\n'));
133
+ console.log(` 候选: ${chalk.bold(agentDef.name)} (${chalk.cyan(agentDef.id)})`);
134
+ console.log(` 品级: ${chalk.cyan(agentDef.role.title)}`);
135
+ const result = await orchestrator.ceremonies.conductSelection([agentDef]);
136
+ const selected = result.outcomes.filter(o => o.action === 'selected');
137
+ const failed = result.outcomes.filter(o => o.action === 'selection_failed');
138
+ if (selected.length > 0) {
139
+ // Register in ELO arena
140
+ orchestrator.arena.register(agentDef.id, 1000);
141
+ console.log(chalk.green(`\n ✓ ${agentDef.name} 入宫成功!初始 ELO: 1000`));
142
+ // Auto-register in openclaw.json if requested
143
+ if (opts.register) {
144
+ const { registerAgentsInOpenClaw } = await import('@agents-uni/core');
145
+ const uniConfig = orchestrator.universe.config;
146
+ const miniConfig = {
147
+ ...uniConfig,
148
+ agents: [agentDef],
149
+ };
150
+ const registered = registerAgentsInOpenClaw(miniConfig, opts.openclawDir || undefined);
151
+ if (registered.length > 0) {
152
+ console.log(chalk.green(` ✓ 已注册到 openclaw.json`));
153
+ }
154
+ else {
155
+ console.log(chalk.gray(` - openclaw.json 中已存在或未找到配置文件`));
156
+ }
157
+ }
158
+ }
159
+ if (failed.length > 0) {
160
+ for (const f of failed) {
161
+ console.log(chalk.red(`\n ✗ 选秀失败: ${f.details.reason}`));
162
+ }
163
+ }
164
+ console.log(chalk.gray(`\n ${result.narrative}`));
165
+ console.log();
166
+ });
167
+ // ─── race ───────────────────────────────────
168
+ program
169
+ .command('race')
170
+ .description('发起赛马竞技 — 下发任务到 OpenClaw 工作区,收集产出并评判')
171
+ .requiredOption('-t, --task <title>', '赛马任务标题')
172
+ .option('-d, --description <desc>', '任务描述', '')
173
+ .option('--timeout <ms>', '超时时间(毫秒)', '60000')
174
+ .option('-s, --spec <path>', '规范文件路径', 'universe.yaml')
175
+ .option('--agents <ids>', '参赛 Agent ID (逗号分隔,默认全部嫔妃)', '')
176
+ .action(async (opts) => {
177
+ const orchestrator = await PalaceOrchestrator.fromSpec(opts.spec);
178
+ // Determine participants: all agents by default (emperor is the user, not an agent)
179
+ let participants;
180
+ if (opts.agents) {
181
+ participants = opts.agents.split(',').map((s) => s.trim());
182
+ }
183
+ else {
184
+ const state = orchestrator.getState();
185
+ participants = state.agents
186
+ .filter(a => a.rank !== '未知')
187
+ .map(a => a.id);
188
+ }
189
+ if (participants.length < 2) {
190
+ console.log(chalk.red('\n ✗ 至少需要 2 名参赛者\n'));
191
+ process.exit(1);
192
+ }
193
+ const taskId = `race-${Date.now()}`;
194
+ const timeoutMs = parseInt(opts.timeout, 10);
195
+ console.log(chalk.yellow('\n═══ 赛马开始 ═══\n'));
196
+ console.log(` 任务: ${chalk.bold(opts.task)}`);
197
+ console.log(` 参赛: ${chalk.cyan(participants.join(', '))}`);
198
+ console.log(` 超时: ${chalk.gray(timeoutMs / 1000 + 's')}`);
199
+ console.log(` 任务文件已下发到各 OpenClaw workspace 的 TASK.md`);
200
+ console.log(chalk.gray('\n 等待各 Agent 提交 SUBMISSION.md ...\n'));
201
+ const { dispatch, race } = await orchestrator.dispatchAndRace({
202
+ id: taskId,
203
+ title: opts.task,
204
+ description: opts.description || opts.task,
205
+ criteria: [
206
+ { name: '质量', weight: 0.5, description: '输出质量和完整度' },
207
+ { name: '创意', weight: 0.3, description: '创新性和独特视角' },
208
+ { name: '速度', weight: 0.2, description: '完成速度' },
209
+ ],
210
+ timeoutMs,
211
+ participants,
212
+ },
213
+ // Default judge (placeholder — in production, call LLM)
214
+ async (task, entries) => {
215
+ return entries.map(entry => {
216
+ const criterionScores = new Map();
217
+ for (const criterion of task.criteria) {
218
+ criterionScores.set(criterion.name, 50 + Math.random() * 50);
219
+ }
220
+ let totalScore = 0;
221
+ let totalWeight = 0;
222
+ for (const criterion of task.criteria) {
223
+ totalScore += (criterionScores.get(criterion.name) ?? 50) * criterion.weight;
224
+ totalWeight += criterion.weight;
225
+ }
226
+ if (totalWeight > 0)
227
+ totalScore /= totalWeight;
228
+ return { agentId: entry.agentId, criterionScores, totalScore, feedback: '评分完毕' };
229
+ });
230
+ });
231
+ // Report dispatch results
232
+ console.log(chalk.green(` ✓ 收到 ${dispatch.submissions.length} 份提交`));
233
+ if (dispatch.timedOut.length > 0) {
234
+ console.log(chalk.red(` ✗ 超时未提交: ${dispatch.timedOut.join(', ')}`));
235
+ }
236
+ // Report race results
237
+ if (race) {
238
+ console.log(chalk.yellow('\n═══ 赛马结果 ═══\n'));
239
+ race.rankings.forEach((agentId, index) => {
240
+ const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : ' ';
241
+ const judgment = race.judgments.find(j => j.agentId === agentId);
242
+ const score = judgment ? judgment.totalScore.toFixed(1) : '-';
243
+ const eloChange = race.eloChanges.get(agentId) ?? 0;
244
+ const eloStr = eloChange >= 0 ? chalk.green(`+${eloChange}`) : chalk.red(`${eloChange}`);
245
+ console.log(` ${medal} ${chalk.bold(agentId.padEnd(16))} 得分: ${chalk.yellow(score)} ELO: ${eloStr}`);
246
+ });
247
+ }
248
+ else {
249
+ console.log(chalk.gray('\n 提交不足 2 份,无法进行评判\n'));
250
+ }
251
+ console.log();
252
+ });
253
+ program.parse();
@@ -0,0 +1,62 @@
1
+ /**
2
+ * ELO Rating System - 后宫竞技评分
3
+ *
4
+ * Implements ELO-based competitive ranking for palace agents.
5
+ * Agents gain/lose ELO through task competitions, direct challenges,
6
+ * and performance evaluations.
7
+ */
8
+ export interface EloRecord {
9
+ agentId: string;
10
+ rating: number;
11
+ peakRating: number;
12
+ matchCount: number;
13
+ winCount: number;
14
+ lossCount: number;
15
+ drawCount: number;
16
+ streak: number;
17
+ history: EloChange[];
18
+ }
19
+ export interface EloChange {
20
+ timestamp: string;
21
+ opponent: string;
22
+ oldRating: number;
23
+ newRating: number;
24
+ result: 'win' | 'loss' | 'draw';
25
+ reason: string;
26
+ }
27
+ export interface MatchResult {
28
+ winner: string;
29
+ loser: string;
30
+ isDraw: boolean;
31
+ reason: string;
32
+ /** Score difference (0-1, higher = more decisive) */
33
+ margin: number;
34
+ }
35
+ /**
36
+ * ELO arena for palace agent competitions.
37
+ */
38
+ export declare class EloArena {
39
+ private records;
40
+ constructor();
41
+ /** Register an agent with starting ELO */
42
+ register(agentId: string, initialRating?: number): void;
43
+ /** Get an agent's current rating */
44
+ getRating(agentId: string): number;
45
+ /** Get an agent's full record */
46
+ getRecord(agentId: string): EloRecord | undefined;
47
+ /** Record a match result and update ELO ratings */
48
+ recordMatch(result: MatchResult): {
49
+ winnerDelta: number;
50
+ loserDelta: number;
51
+ };
52
+ /** Get leaderboard sorted by ELO rating */
53
+ getLeaderboard(): EloRecord[];
54
+ /** Get win rate for an agent */
55
+ getWinRate(agentId: string): number;
56
+ /** Calculate expected score between two ratings */
57
+ private expectedScore;
58
+ /** Determine K-factor based on agent experience and rating */
59
+ private getKFactor;
60
+ /** Ensure an agent is registered before a match */
61
+ private ensureRegistered;
62
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * ELO Rating System - 后宫竞技评分
3
+ *
4
+ * Implements ELO-based competitive ranking for palace agents.
5
+ * Agents gain/lose ELO through task competitions, direct challenges,
6
+ * and performance evaluations.
7
+ */
8
+ /** Default K-factor for ELO calculation */
9
+ const DEFAULT_K = 32;
10
+ /** K-factor for new agents (first 10 matches) */
11
+ const PROVISIONAL_K = 48;
12
+ /** K-factor for high-rated agents (2000+) */
13
+ const HIGH_RATING_K = 16;
14
+ /** Default starting rating */
15
+ const DEFAULT_RATING = 1200;
16
+ /**
17
+ * ELO arena for palace agent competitions.
18
+ */
19
+ export class EloArena {
20
+ records;
21
+ constructor() {
22
+ this.records = new Map();
23
+ }
24
+ /** Register an agent with starting ELO */
25
+ register(agentId, initialRating = DEFAULT_RATING) {
26
+ if (this.records.has(agentId))
27
+ return;
28
+ this.records.set(agentId, {
29
+ agentId,
30
+ rating: initialRating,
31
+ peakRating: initialRating,
32
+ matchCount: 0,
33
+ winCount: 0,
34
+ lossCount: 0,
35
+ drawCount: 0,
36
+ streak: 0,
37
+ history: [],
38
+ });
39
+ }
40
+ /** Get an agent's current rating */
41
+ getRating(agentId) {
42
+ return this.records.get(agentId)?.rating ?? DEFAULT_RATING;
43
+ }
44
+ /** Get an agent's full record */
45
+ getRecord(agentId) {
46
+ return this.records.get(agentId);
47
+ }
48
+ /** Record a match result and update ELO ratings */
49
+ recordMatch(result) {
50
+ this.ensureRegistered(result.winner);
51
+ this.ensureRegistered(result.loser);
52
+ const winnerRecord = this.records.get(result.winner);
53
+ const loserRecord = this.records.get(result.loser);
54
+ const kWinner = this.getKFactor(winnerRecord);
55
+ const kLoser = this.getKFactor(loserRecord);
56
+ // Expected scores
57
+ const expectedWinner = this.expectedScore(winnerRecord.rating, loserRecord.rating);
58
+ const expectedLoser = 1 - expectedWinner;
59
+ let actualWinner;
60
+ let actualLoser;
61
+ if (result.isDraw) {
62
+ actualWinner = 0.5;
63
+ actualLoser = 0.5;
64
+ }
65
+ else {
66
+ // Margin-adjusted score (0.75-1.0 for winner based on margin)
67
+ actualWinner = 0.75 + result.margin * 0.25;
68
+ actualLoser = 0;
69
+ }
70
+ const winnerDelta = Math.round(kWinner * (actualWinner - expectedWinner));
71
+ const loserDelta = Math.round(kLoser * (actualLoser - expectedLoser));
72
+ const now = new Date().toISOString();
73
+ // Update winner
74
+ const oldWinnerRating = winnerRecord.rating;
75
+ winnerRecord.rating += winnerDelta;
76
+ winnerRecord.peakRating = Math.max(winnerRecord.peakRating, winnerRecord.rating);
77
+ winnerRecord.matchCount++;
78
+ if (result.isDraw) {
79
+ winnerRecord.drawCount++;
80
+ winnerRecord.streak = 0;
81
+ }
82
+ else {
83
+ winnerRecord.winCount++;
84
+ winnerRecord.streak = winnerRecord.streak > 0 ? winnerRecord.streak + 1 : 1;
85
+ }
86
+ winnerRecord.history.push({
87
+ timestamp: now,
88
+ opponent: result.loser,
89
+ oldRating: oldWinnerRating,
90
+ newRating: winnerRecord.rating,
91
+ result: result.isDraw ? 'draw' : 'win',
92
+ reason: result.reason,
93
+ });
94
+ // Update loser
95
+ const oldLoserRating = loserRecord.rating;
96
+ loserRecord.rating += loserDelta;
97
+ loserRecord.rating = Math.max(loserRecord.rating, 100); // floor at 100
98
+ loserRecord.matchCount++;
99
+ if (result.isDraw) {
100
+ loserRecord.drawCount++;
101
+ loserRecord.streak = 0;
102
+ }
103
+ else {
104
+ loserRecord.lossCount++;
105
+ loserRecord.streak = loserRecord.streak < 0 ? loserRecord.streak - 1 : -1;
106
+ }
107
+ loserRecord.history.push({
108
+ timestamp: now,
109
+ opponent: result.winner,
110
+ oldRating: oldLoserRating,
111
+ newRating: loserRecord.rating,
112
+ result: result.isDraw ? 'draw' : 'loss',
113
+ reason: result.reason,
114
+ });
115
+ return { winnerDelta, loserDelta };
116
+ }
117
+ /** Get leaderboard sorted by ELO rating */
118
+ getLeaderboard() {
119
+ return [...this.records.values()].sort((a, b) => b.rating - a.rating);
120
+ }
121
+ /** Get win rate for an agent */
122
+ getWinRate(agentId) {
123
+ const record = this.records.get(agentId);
124
+ if (!record || record.matchCount === 0)
125
+ return 0;
126
+ return record.winCount / record.matchCount;
127
+ }
128
+ /** Calculate expected score between two ratings */
129
+ expectedScore(ratingA, ratingB) {
130
+ return 1 / (1 + Math.pow(10, (ratingB - ratingA) / 400));
131
+ }
132
+ /** Determine K-factor based on agent experience and rating */
133
+ getKFactor(record) {
134
+ if (record.matchCount < 10)
135
+ return PROVISIONAL_K;
136
+ if (record.rating >= 2000)
137
+ return HIGH_RATING_K;
138
+ return DEFAULT_K;
139
+ }
140
+ /** Ensure an agent is registered before a match */
141
+ ensureRegistered(agentId) {
142
+ if (!this.records.has(agentId)) {
143
+ this.register(agentId);
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Horse Race Engine - 赛马机制
3
+ *
4
+ * Multiple agents compete on the same task in parallel.
5
+ * A judge evaluates outputs and determines winners/losers.
6
+ * Results feed back into ELO ratings and rank progression.
7
+ */
8
+ import type { EloArena } from './elo.js';
9
+ export interface HorseRaceTask {
10
+ id: string;
11
+ title: string;
12
+ description: string;
13
+ /** Maximum time allowed (ms) */
14
+ timeLimit: number;
15
+ /** Difficulty tier (1-5) */
16
+ difficulty: number;
17
+ /** Category for scoring */
18
+ category: string;
19
+ /** Evaluation criteria with weights */
20
+ criteria: EvaluationCriterion[];
21
+ }
22
+ export interface EvaluationCriterion {
23
+ name: string;
24
+ weight: number;
25
+ description: string;
26
+ }
27
+ export interface RaceEntry {
28
+ agentId: string;
29
+ output: string;
30
+ completedAt: string;
31
+ /** Time taken in ms */
32
+ duration: number;
33
+ }
34
+ export interface JudgmentScore {
35
+ agentId: string;
36
+ criterionScores: Map<string, number>;
37
+ totalScore: number;
38
+ feedback: string;
39
+ }
40
+ export interface RaceResult {
41
+ taskId: string;
42
+ startedAt: string;
43
+ completedAt: string;
44
+ entries: RaceEntry[];
45
+ judgments: JudgmentScore[];
46
+ rankings: string[];
47
+ eloChanges: Map<string, number>;
48
+ narrative: string;
49
+ }
50
+ export type JudgeFunction = (task: HorseRaceTask, entries: RaceEntry[]) => Promise<JudgmentScore[]>;
51
+ /**
52
+ * Runs competitive horse races between agents.
53
+ */
54
+ export declare class HorseRaceEngine {
55
+ private arena;
56
+ private history;
57
+ constructor(arena: EloArena);
58
+ /**
59
+ * Evaluate a completed race: judge entries, update ELO, return results.
60
+ */
61
+ evaluateRace(task: HorseRaceTask, entries: RaceEntry[], judge: JudgeFunction): Promise<RaceResult>;
62
+ /** Get race history */
63
+ getHistory(): RaceResult[];
64
+ /** Get an agent's race statistics */
65
+ getAgentStats(agentId: string): {
66
+ totalRaces: number;
67
+ firstPlace: number;
68
+ topThree: number;
69
+ averageScore: number;
70
+ };
71
+ private generateNarrative;
72
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Horse Race Engine - 赛马机制
3
+ *
4
+ * Multiple agents compete on the same task in parallel.
5
+ * A judge evaluates outputs and determines winners/losers.
6
+ * Results feed back into ELO ratings and rank progression.
7
+ */
8
+ /**
9
+ * Runs competitive horse races between agents.
10
+ */
11
+ export class HorseRaceEngine {
12
+ arena;
13
+ history;
14
+ constructor(arena) {
15
+ this.arena = arena;
16
+ this.history = [];
17
+ }
18
+ /**
19
+ * Evaluate a completed race: judge entries, update ELO, return results.
20
+ */
21
+ async evaluateRace(task, entries, judge) {
22
+ if (entries.length < 2) {
23
+ throw new Error('Horse race requires at least 2 entries');
24
+ }
25
+ // Get judgments from the judge function
26
+ const judgments = await judge(task, entries);
27
+ // Sort by total score to get rankings
28
+ const sorted = [...judgments].sort((a, b) => b.totalScore - a.totalScore);
29
+ const rankings = sorted.map(j => j.agentId);
30
+ // Update ELO for all pairwise matchups
31
+ const eloChanges = new Map();
32
+ for (const id of rankings) {
33
+ eloChanges.set(id, 0);
34
+ }
35
+ for (let i = 0; i < sorted.length; i++) {
36
+ for (let j = i + 1; j < sorted.length; j++) {
37
+ const higher = sorted[i];
38
+ const lower = sorted[j];
39
+ const scoreDiff = higher.totalScore - lower.totalScore;
40
+ const matchResult = {
41
+ winner: higher.agentId,
42
+ loser: lower.agentId,
43
+ isDraw: scoreDiff < 5, // within 5 points = draw
44
+ reason: `赛马: ${task.title}`,
45
+ margin: Math.min(scoreDiff / 100, 1),
46
+ };
47
+ const deltas = this.arena.recordMatch(matchResult);
48
+ eloChanges.set(higher.agentId, (eloChanges.get(higher.agentId) ?? 0) + deltas.winnerDelta);
49
+ eloChanges.set(lower.agentId, (eloChanges.get(lower.agentId) ?? 0) + deltas.loserDelta);
50
+ }
51
+ }
52
+ const result = {
53
+ taskId: task.id,
54
+ startedAt: entries[0]?.completedAt ?? new Date().toISOString(),
55
+ completedAt: new Date().toISOString(),
56
+ entries,
57
+ judgments,
58
+ rankings,
59
+ eloChanges,
60
+ narrative: this.generateNarrative(task, sorted, rankings),
61
+ };
62
+ this.history.push(result);
63
+ return result;
64
+ }
65
+ /** Get race history */
66
+ getHistory() {
67
+ return [...this.history];
68
+ }
69
+ /** Get an agent's race statistics */
70
+ getAgentStats(agentId) {
71
+ let totalRaces = 0;
72
+ let firstPlace = 0;
73
+ let topThree = 0;
74
+ let totalScore = 0;
75
+ for (const race of this.history) {
76
+ const rankIndex = race.rankings.indexOf(agentId);
77
+ if (rankIndex === -1)
78
+ continue;
79
+ totalRaces++;
80
+ if (rankIndex === 0)
81
+ firstPlace++;
82
+ if (rankIndex < 3)
83
+ topThree++;
84
+ const judgment = race.judgments.find(j => j.agentId === agentId);
85
+ if (judgment) {
86
+ totalScore += judgment.totalScore;
87
+ }
88
+ }
89
+ return {
90
+ totalRaces,
91
+ firstPlace,
92
+ topThree,
93
+ averageScore: totalRaces > 0 ? totalScore / totalRaces : 0,
94
+ };
95
+ }
96
+ generateNarrative(task, sorted, rankings) {
97
+ const parts = [`赛马「${task.title}」结果揭晓:`];
98
+ if (rankings.length > 0) {
99
+ const winner = sorted[0];
100
+ parts.push(`魁首: ${winner.agentId} (${winner.totalScore.toFixed(1)}分)`);
101
+ }
102
+ if (rankings.length > 1) {
103
+ parts.push(`次席: ${sorted[1].agentId} (${sorted[1].totalScore.toFixed(1)}分)`);
104
+ }
105
+ if (rankings.length > 2) {
106
+ parts.push(`探花: ${sorted[2].agentId} (${sorted[2].totalScore.toFixed(1)}分)`);
107
+ }
108
+ return parts.join(' ');
109
+ }
110
+ }
@@ -0,0 +1,6 @@
1
+ export { EloArena } from './elo.js';
2
+ export type { EloRecord, EloChange, MatchResult } from './elo.js';
3
+ export { HorseRaceEngine } from './horse-race.js';
4
+ export type { HorseRaceTask, EvaluationCriterion, RaceEntry, JudgmentScore, RaceResult, JudgeFunction, } from './horse-race.js';
5
+ export { SeasonEngine } from './season.js';
6
+ export type { Season, SeasonConfig, SeasonResult, SeasonStanding } from './season.js';
@@ -0,0 +1,3 @@
1
+ export { EloArena } from './elo.js';
2
+ export { HorseRaceEngine } from './horse-race.js';
3
+ export { SeasonEngine } from './season.js';