ai-battle 0.2.1 → 0.2.2

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.
Files changed (3) hide show
  1. package/README.md +26 -2
  2. package/ai-battle.sh +200 -19
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -10,17 +10,41 @@
10
10
 
11
11
  <p align="center">
12
12
  <a href="https://www.npmjs.com/package/ai-battle"><img src="https://img.shields.io/npm/v/ai-battle?style=flat-square&logo=npm&logoColor=white&color=CB3837" alt="npm version" /></a>
13
+ <a href="https://github.com/Alfonsxh/ai-battle/actions/workflows/publish.yml"><img src="https://img.shields.io/github/actions/workflow/status/Alfonsxh/ai-battle/publish.yml?style=flat-square&logo=githubactions&logoColor=white" alt="publish" /></a>
13
14
  <img src="https://img.shields.io/badge/Bash-4%2B-4EAA25?style=flat-square&logo=gnubash&logoColor=white" alt="Bash 4+" />
14
15
  <img src="https://img.shields.io/badge/Dep-jq-blue?style=flat-square" alt="jq" />
15
16
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow?style=flat-square" alt="MIT License" /></a>
16
17
  </p>
17
18
 
18
19
  <p align="center">
19
- <a href="README_CN.md">📖 中文文档</a>
20
+ <a href="README_CN.md">中文</a> ·
21
+ <a href="https://www.npmjs.com/package/ai-battle">NPM</a> ·
22
+ <a href="https://github.com/Alfonsxh/ai-battle/issues">Issues</a> ·
23
+ <a href="https://github.com/Alfonsxh/ai-battle/pulls">PRs</a> ·
24
+ <a href="LICENSE">License</a>
20
25
  </p>
21
26
 
22
27
  ---
23
28
 
29
+ <details>
30
+ <summary><b>Table of Contents</b></summary>
31
+
32
+ - [Features](#features)
33
+ - [Quick Start](#quick-start)
34
+ - [Installation](#installation)
35
+ - [Prerequisites](#prerequisites)
36
+ - [Usage](#usage)
37
+ - [Examples](#examples)
38
+ - [How It Works](#how-it-works)
39
+ - [Built-in Agents](#built-in-agents)
40
+ - [Output Structure](#output-structure)
41
+ - [Extend Agent](#extend-agent)
42
+ - [Environment Variables](#environment-variables)
43
+ - [Contributing](#contributing)
44
+ - [License](#license)
45
+
46
+ </details>
47
+
24
48
  ## ✨ Features
25
49
 
26
50
  | Feature | Description |
@@ -70,7 +94,7 @@ npm install -g ai-battle
70
94
 
71
95
  ## 📖 Usage
72
96
 
73
- ```
97
+ ```text
74
98
  ai-battle [options]
75
99
  ai-battle help
76
100
  ```
package/ai-battle.sh CHANGED
@@ -23,12 +23,31 @@ if [ -f ".env" ]; then
23
23
  fi
24
24
 
25
25
  # ======================== 版本(从 package.json 读取) ========================
26
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
+ # 解析脚本真实路径(支持全局安装后经由软链接启动)
27
+ resolve_script_dir() {
28
+ local src="${BASH_SOURCE[0]}"
29
+
30
+ while [ -L "$src" ]; do
31
+ local dir target
32
+ dir="$(cd -P "$(dirname "$src")" && pwd)"
33
+ target="$(readlink "$src")"
34
+ if [[ "$target" != /* ]]; then
35
+ src="${dir}/${target}"
36
+ else
37
+ src="$target"
38
+ fi
39
+ done
40
+
41
+ cd -P "$(dirname "$src")" && pwd
42
+ }
43
+
44
+ SCRIPT_DIR="$(resolve_script_dir)"
45
+
27
46
  # 优先使用 npm/npx 注入的环境变量,fallback 到 node 读取,最后 grep 提取
28
47
  if [ -n "${npm_package_version:-}" ]; then
29
48
  VERSION="$npm_package_version"
30
49
  else
31
- VERSION=$(node -p "require('${SCRIPT_DIR}/package.json').version" 2>/dev/null \
50
+ VERSION=$(node -p "require(process.argv[1]).version" "${SCRIPT_DIR}/package.json" 2>/dev/null \
32
51
  || grep -o '"version": *"[^"]*"' "${SCRIPT_DIR}/package.json" 2>/dev/null | head -1 | grep -o '[0-9][^"]*' \
33
52
  || echo "0.0.0")
34
53
  fi
@@ -50,6 +69,8 @@ DEFAULT_MAX_ROUNDS=10
50
69
  WORK_DIR=".ai-battle"
51
70
  PROBLEM_FILE="problem.md"
52
71
  ROUNDS_DIR="${WORK_DIR}/rounds"
72
+ ORDERS_DIR="${WORK_DIR}/orders"
73
+ ORDER_HISTORY_FILE="${WORK_DIR}/order_history.jsonl"
53
74
  CONSENSUS_FILE="${WORK_DIR}/consensus.md"
54
75
  LOG_FILE="${WORK_DIR}/battle.log"
55
76
  CONFIG_FILE="${WORK_DIR}/config.json"
@@ -566,6 +587,138 @@ agent_md_file() {
566
587
  echo "${AGENTS_DIR}/${filename}"
567
588
  }
568
589
 
590
+ # 查找 agent 在数组中的索引
591
+ # 参数: $1=目标 agent $2...=agent 列表
592
+ # 输出: 索引(找不到返回 -1)
593
+ find_agent_index() {
594
+ local target="$1"
595
+ shift
596
+ local agents=("$@")
597
+
598
+ for ((idx=0; idx<${#agents[@]}; idx++)); do
599
+ if [ "${agents[$idx]}" = "$target" ]; then
600
+ echo "$idx"
601
+ return 0
602
+ fi
603
+ done
604
+
605
+ echo "-1"
606
+ return 0
607
+ }
608
+
609
+ # 校验轮次顺序 CSV 是否与当前 agent 列表一一对应
610
+ # 参数: $1=order_csv $2...=agent 列表
611
+ # 返回: 0=有效, 1=无效
612
+ validate_round_order_csv() {
613
+ local order_csv="$1"
614
+ shift
615
+ local agents=("$@")
616
+ local order=()
617
+
618
+ [ -n "$order_csv" ] || return 1
619
+ IFS=',' read -ra order <<< "$order_csv"
620
+
621
+ if [ "${#order[@]}" -ne "${#agents[@]}" ]; then
622
+ return 1
623
+ fi
624
+
625
+ for ((idx=0; idx<${#order[@]}; idx++)); do
626
+ order[$idx]=$(echo "${order[$idx]}" | xargs)
627
+ done
628
+
629
+ # 每个 agent 必须且仅出现一次,避免顺序文件损坏导致错位
630
+ for expected in "${agents[@]}"; do
631
+ local count=0
632
+ for got in "${order[@]}"; do
633
+ if [ "$got" = "$expected" ]; then
634
+ count=$((count + 1))
635
+ fi
636
+ done
637
+ if [ "$count" -ne 1 ]; then
638
+ return 1
639
+ fi
640
+ done
641
+
642
+ return 0
643
+ }
644
+
645
+ # Fisher-Yates 洗牌,生成当前轮次的随机顺序
646
+ # 参数: $@=agent 列表
647
+ # 输出: csv 字符串(如 a,b,c)
648
+ shuffle_round_order_csv() {
649
+ local shuffled=("$@")
650
+ local n=${#shuffled[@]}
651
+
652
+ if [ "$n" -eq 0 ]; then
653
+ echo ""
654
+ return 0
655
+ fi
656
+
657
+ for ((i=n-1; i>0; i--)); do
658
+ local j=$((RANDOM % (i + 1)))
659
+ local tmp="${shuffled[$i]}"
660
+ shuffled[$i]="${shuffled[$j]}"
661
+ shuffled[$j]="$tmp"
662
+ done
663
+
664
+ local csv="${shuffled[0]}"
665
+ for ((i=1; i<n; i++)); do
666
+ csv+=",${shuffled[$i]}"
667
+ done
668
+ echo "$csv"
669
+ }
670
+
671
+ # 记录轮次顺序历史(jsonl),用于兜底追踪和恢复审计
672
+ # 参数: $1=round $2=order_csv $3=source(random|recovered)
673
+ record_round_order() {
674
+ local round="$1"
675
+ local order_csv="$2"
676
+ local source="${3:-random}"
677
+ local ts
678
+ ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
679
+
680
+ local order_json
681
+ order_json=$(printf '%s' "$order_csv" | jq -R 'split(",")')
682
+
683
+ jq -cn \
684
+ --arg ts "$ts" \
685
+ --arg source "$source" \
686
+ --argjson round "$round" \
687
+ --argjson order "$order_json" \
688
+ '{ts: $ts, round: $round, source: $source, order: $order}' \
689
+ >> "$ORDER_HISTORY_FILE"
690
+ }
691
+
692
+ # 读取或生成某一轮的顺序:
693
+ # 1) 若顺序文件存在且有效,复用;2) 否则随机生成并落盘
694
+ # 参数: $1=round $2...=agent 列表
695
+ # 输出: order_csv
696
+ resolve_round_order_csv() {
697
+ local round="$1"
698
+ shift
699
+ local agents=("$@")
700
+ local order_file="${ORDERS_DIR}/round_${round}.order"
701
+ local order_csv=""
702
+ local source="recovered"
703
+
704
+ if [ -f "$order_file" ] && [ -s "$order_file" ]; then
705
+ order_csv=$(tr -d '\r\n' < "$order_file")
706
+ if ! validate_round_order_csv "$order_csv" "${agents[@]}"; then
707
+ log_and_print "${YELLOW}⚠️ Round ${round} 顺序文件无效,已重新随机${NC}"
708
+ order_csv=""
709
+ fi
710
+ fi
711
+
712
+ if [ -z "$order_csv" ]; then
713
+ order_csv=$(shuffle_round_order_csv "${agents[@]}")
714
+ echo "$order_csv" > "$order_file"
715
+ source="random"
716
+ fi
717
+
718
+ record_round_order "$round" "$order_csv" "$source"
719
+ echo "$order_csv"
720
+ }
721
+
569
722
  # 上帝视角: 等待用户输入补充信息
570
723
  # 参数: $1=当前轮次
571
724
  # 输出: stdout 用户输入的补充信息(可为空)
@@ -1069,8 +1222,8 @@ cmd_run() {
1069
1222
  exit 1
1070
1223
  fi
1071
1224
 
1072
- # 创建 rounds 目录
1073
- mkdir -p "$ROUNDS_DIR" "$SESSIONS_DIR" "$AGENTS_DIR"
1225
+ # 创建运行目录(包含顺序记录目录)
1226
+ mkdir -p "$ROUNDS_DIR" "$SESSIONS_DIR" "$AGENTS_DIR" "$ORDERS_DIR"
1074
1227
 
1075
1228
  # 动态生成各 Agent 的指令文件(新建和恢复模式都需要更新)
1076
1229
  for agent in "${available_agents[@]}"; do
@@ -1117,6 +1270,7 @@ cmd_run() {
1117
1270
  log_and_print " 📝 问题: $(head -1 "$PROBLEM_FILE")"
1118
1271
  log_and_print " 🤖 Agent: ${available_agents[*]}"
1119
1272
  log_and_print " 🔄 最大轮次: $max_rounds"
1273
+ log_and_print " 🔀 发言顺序: 每轮随机(自动记录并可恢复)"
1120
1274
  if $god_mode; then
1121
1275
  log_and_print "${CYAN} 👁️ 上帝视角: 开启${NC}"
1122
1276
  fi
@@ -1160,7 +1314,7 @@ cmd_run() {
1160
1314
  round=$((prev_round + 1))
1161
1315
 
1162
1316
  # 更新配置: 状态为 running,max_rounds 使用命令行参数
1163
- jq --argjson m "$max_rounds" '.status = "running" | .max_rounds = $m' \
1317
+ jq --argjson m "$max_rounds" '.status = "running" | .max_rounds = $m | .order_mode = "round_random"' \
1164
1318
  "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" \
1165
1319
  && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
1166
1320
 
@@ -1197,7 +1351,7 @@ cmd_run() {
1197
1351
  --argjson max_rounds "$max_rounds" \
1198
1352
  --arg problem "$problem" \
1199
1353
  '{agents: $agents, max_rounds: $max_rounds, problem: $problem,
1200
- status: "running", current_round: 0}' \
1354
+ status: "running", current_round: 0, order_mode: "round_random", last_round_order: ""}' \
1201
1355
  > "$CONFIG_FILE"
1202
1356
 
1203
1357
  # 清空日志
@@ -1353,10 +1507,21 @@ ${all_responses_r1}请进行裁判总结。" "referee_round_1")
1353
1507
  # 提取为函数逻辑,供主循环和追加轮次复用
1354
1508
  while [ "$round" -le "$max_rounds" ]; do
1355
1509
  local remaining=$((max_rounds - round))
1356
-
1357
- # 每个 agent 依次发言,看到所有其他 agent 的上一轮回复
1358
- for ((i=0; i<agent_count; i++)); do
1359
- local agent="${available_agents[$i]}"
1510
+ local round_order_csv
1511
+ round_order_csv=$(resolve_round_order_csv "$round" "${available_agents[@]}")
1512
+ local round_order=()
1513
+ IFS=',' read -ra round_order <<< "$round_order_csv"
1514
+
1515
+ log_and_print "${CYAN}🔀 Round $round 顺序: ${round_order[*]}${NC}"
1516
+
1517
+ # 每轮默认随机顺序发言,优先复用已落盘顺序(便于恢复)
1518
+ for agent in "${round_order[@]}"; do
1519
+ local i
1520
+ i=$(find_agent_index "$agent" "${available_agents[@]}")
1521
+ if [ "$i" -lt 0 ]; then
1522
+ log_and_print "${YELLOW}⚠️ 跳过未知 agent: ${agent}${NC}"
1523
+ continue
1524
+ fi
1360
1525
  local base
1361
1526
  base=$(agent_base "$agent")
1362
1527
  local color
@@ -1368,8 +1533,8 @@ ${all_responses_r1}请进行裁判总结。" "referee_round_1")
1368
1533
  # 构建其他 agent 回复的 XML 块
1369
1534
  local others_responses=""
1370
1535
  for ((j=0; j<agent_count; j++)); do
1371
- if [ "$j" != "$i" ]; then
1372
- local other="${available_agents[$j]}"
1536
+ local other="${available_agents[$j]}"
1537
+ if [ "$other" != "$agent" ]; then
1373
1538
  others_responses+="<${other}_response>
1374
1539
  ${responses[$j]}
1375
1540
  </${other}_response>
@@ -1502,7 +1667,8 @@ ${all_responses}请进行裁判总结。" "referee_round_${round}")
1502
1667
  fi
1503
1668
 
1504
1669
  # 更新配置
1505
- jq --argjson r "$round" '.current_round = $r | .status = "running"' \
1670
+ jq --argjson r "$round" --arg o "$round_order_csv" \
1671
+ '.current_round = $r | .status = "running" | .last_round_order = $o' \
1506
1672
  "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
1507
1673
 
1508
1674
  # 上帝视角: 每轮结束后注入(裁判总结后再让 god 输入)
@@ -1554,9 +1720,20 @@ ${all_responses}请进行裁判总结。" "referee_round_${round}")
1554
1720
  # 继续讨论循环(逻辑与上方 Round 2+ 完全相同)
1555
1721
  while [ "$round" -le "$max_rounds" ]; do
1556
1722
  local remaining=$((max_rounds - round))
1557
-
1558
- for ((i=0; i<agent_count; i++)); do
1559
- local agent="${available_agents[$i]}"
1723
+ local round_order_csv
1724
+ round_order_csv=$(resolve_round_order_csv "$round" "${available_agents[@]}")
1725
+ local round_order=()
1726
+ IFS=',' read -ra round_order <<< "$round_order_csv"
1727
+
1728
+ log_and_print "${CYAN}🔀 Round $round 顺序: ${round_order[*]}${NC}"
1729
+
1730
+ for agent in "${round_order[@]}"; do
1731
+ local i
1732
+ i=$(find_agent_index "$agent" "${available_agents[@]}")
1733
+ if [ "$i" -lt 0 ]; then
1734
+ log_and_print "${YELLOW}⚠️ 跳过未知 agent: ${agent}${NC}"
1735
+ continue
1736
+ fi
1560
1737
  local base
1561
1738
  base=$(agent_base "$agent")
1562
1739
  local color
@@ -1568,8 +1745,8 @@ ${all_responses}请进行裁判总结。" "referee_round_${round}")
1568
1745
  # 构建其他 agent 回复的 XML 块
1569
1746
  local others_responses=""
1570
1747
  for ((j=0; j<agent_count; j++)); do
1571
- if [ "$j" != "$i" ]; then
1572
- local other="${available_agents[$j]}"
1748
+ local other="${available_agents[$j]}"
1749
+ if [ "$other" != "$agent" ]; then
1573
1750
  others_responses+="<${other}_response>
1574
1751
  ${responses[$j]}
1575
1752
  </${other}_response>
@@ -1697,7 +1874,8 @@ ${all_responses}请进行裁判总结。" "referee_round_${round}")
1697
1874
  fi
1698
1875
 
1699
1876
  # 更新配置
1700
- jq --argjson r "$round" '.current_round = $r | .status = "running"' \
1877
+ jq --argjson r "$round" --arg o "$round_order_csv" \
1878
+ '.current_round = $r | .status = "running" | .last_round_order = $o' \
1701
1879
  "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
1702
1880
 
1703
1881
  # 上帝视角: 每轮结束后注入(裁判总结后再让 god 输入)
@@ -1751,6 +1929,7 @@ cmd_help() {
1751
1929
  --agents, -a <a1,a2> Select participating agents (default: claude,codex)
1752
1930
  Supports same-type agents: --agents gemini,gemini
1753
1931
  --rounds, -r <N> Max discussion rounds (default: 10)
1932
+ Speaking order is randomized every round by default
1754
1933
  --god, -g Enable god mode (inject instructions after each round)
1755
1934
  --referee [agent] Enable referee mode (summarize each round, detect
1756
1935
  consensus, generate final summary)
@@ -1788,6 +1967,8 @@ cmd_help() {
1788
1967
  .ai-battle/rounds/ Per-round records (round_N_<agent>.md)
1789
1968
  .ai-battle/rounds/referee_*.md Referee summaries (--referee)
1790
1969
  .ai-battle/rounds/god_*.md God mode injections (--god)
1970
+ .ai-battle/orders/round_*.order Per-round speaking order (fallback/recovery)
1971
+ .ai-battle/order_history.jsonl Full order history (audit trail)
1791
1972
  .ai-battle/sessions/ Raw Agent CLI output
1792
1973
  .ai-battle/consensus.md Consensus conclusion (if reached)
1793
1974
  .ai-battle/battle.log Full log
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "ai-battle",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "让多个 AI Agent 对同一问题进行结构化圆桌讨论",
5
5
  "bin": {
6
- "ai-battle": "./ai-battle.sh"
6
+ "ai-battle": "ai-battle.sh"
7
7
  },
8
8
  "files": [
9
9
  "ai-battle.sh",
@@ -22,8 +22,8 @@
22
22
  ],
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "https://github.com/Alfonsxh/ai-battle.git"
25
+ "url": "git+https://github.com/Alfonsxh/ai-battle.git"
26
26
  },
27
27
  "author": "Alfons",
28
28
  "license": "MIT"
29
- }
29
+ }