Nicholas Clooney

Start the Chain:我的三 agent 配置,现在只要一条命令

所属系列

在和 agent 的聊天太乱?里,我给配置又加了一个 agent:主 agent,它唯一的工作就是和我对话。它把工作交给 secondary,secondary 协调一个 Codex worker,worker 再把工作分给自己启动的 agent。我只和主 agent 说话。

这套配置最初只是一个项目 AGENTS.md 里的几段话。一周后,它已经内置进 tmux-agents,我在每个项目里都在用。

从操作步骤到一条命令

第一次是手工搭的。开一个新窗格,连接到现有的 Claude agent,告诉它现在是主 agent、只和我对话,再告诉原来那个 agent 它现在是 coordinator,还要确保主 agent 不 连接 Codex。能用,但每个项目都得把这套步骤重复一遍。

现在这些角色写在 agent-chain skill 里。我对一个 Claude agent 说 "start the chain",剩下的它自己完成:把自己改名为主 agent,在右边启动 secondary,再在 secondary 下方启动 worker。也可以在 shell 里运行:

tmux-agents start chain ~/Source/Projects/my-project

它会为这个项目开一个新窗口,并以 "start the chain" 作为第一条提示启动 Claude。

一个 tmux 窗口:左边是博客的主 agent,右上是 secondary Claude agent,右下是 Codex worker
博客的 chain,当时我正在告诉主 agent 下面"重启之后"一节该写什么:左边是主 agent,右上是 secondary,右下是 Codex worker。

实际用下来改了什么

思路没变。用了一周之后,有几个细节改了。

FYI 用 notice,不用 request。 最初的规则是给 FYI 标上"无需回复"。这不够。在 tmux-agents 里,不管内容写了什么,request 都会让接收方重新开工,发送方也会等它回复。于是一个已经做完的 agent 被一条 FYI 唤醒,如果它没回复,就会显示为需要我处理。现在有了单独的 notice 消息,不需要回复,也不需要做事。

secondary 和 worker 是 member,不是子 agent。 一开始 chain 是用普通的子 agent 搭的。所以它们会和临时帮手一起出现在子 agent 列表和状态 chip 里,清理已完成的子 agent 时也可能把它们关掉。现在它们是长期 member:不出现在默认列表里,也不参与批量清理,但需要我时照样会提醒。

chain 是看得见的。 启动的 agent 通常在隐藏窗口里运行,要从列表里查看。chain 的三个我都想放在屏幕上,所以 secondary 和 worker 以分屏的形式开在主 agent 的窗口里。

coordinator 改叫 secondary。 小改动,但现在所有 skill、指南和消息都用同一个名字称呼它。

跨项目

我最喜欢的一条规则是关于项目之间怎么沟通的。每个项目的 secondary 是它对其他项目的接口。请求在 secondary 之间传递,agent 只修改自己项目的文件。

举个真实的例子。tmux-agents 每次发布,博客都要加一条 timeline 条目。v1.12.2 发布时,tmux-agents 的 secondary 没有自己打开博客仓库去写,而是请博客的 secondary 来做。博客的 secondary 写好并提交,然后告诉博客的主 agent:提交好了,但还没推送,因为推送会部署网站。主 agent 来问我,我推送了。

没有谁伸手去改不属于自己的项目,唯一需要我决定的事,以一个问题的形式来到我面前。

重启之后

我已经不再一个个地启动 chain 了。我有一个小脚本 tmux-main-sequence,用来搭起整个 tmux session:一个 dashboard 窗口,一个 home 目录窗口,一个博客窗口,再加上配置文件里每个项目各一个窗口。除了 dashboard,每个窗口都会用同样的 "start the chain" 提示启动一个 chain。

		
  1. #!/usr/bin/env bash
  2. # tmux-main-sequence — join the standard project session group
  3. #
  4. # Usage: tmux-main-sequence [--add-missing] [--no-attach]
  5. #
  6. # Windows: 0 dashboard (shell), 1 ~, 2 blog, then configured projects.
  7. # Each client joins through its own main-N session, with independent window selection.
  8. # Existing group windows are unchanged unless --add-missing is given.
  9. # Per-client sessions are destroyed on detach; the base session stays available.
  10. # Config: ~/.config/tmux-main-sequence/projects, one directory per line.
  11. # Blank lines and full-line # comments are ignored; spaces in paths are supported.
  12. # Example config (local and untracked):
  13. # ~/Source/Projects/tmux-agents
  14. # ~/Source/Projects/projects-stone-age
  15. # ~/Source/Projects/job-hunting/career-ops
  16. set -euo pipefail
  17. usage() {
  18. cat <<'EOF'
  19. Usage: tmux-main-sequence [--add-missing] [--no-attach]
  20. Create or join the main session group through a separate main-N client session.
  21. Client sessions share windows but select them independently and vanish on detach.
  22. An existing group or a base session named main is reused.
  23. --add-missing Add missing window names with fresh agents, then join.
  24. Use the intended index if free; otherwise append at the end.
  25. --no-attach Skip joining: do not create a per-client session or attach.
  26. -h, --help Show this help.
  27. Environment:
  28. TMUX_MAIN_SEQUENCE_SESSION Group/base name (default: main; clients: main-N).
  29. TMUX_MAIN_SEQUENCE_CONFIG Project list (default: ~/.config/tmux-main-sequence/projects).
  30. TMUX_MAIN_SEQUENCE_AGENT Shell command (default: tmux-agents start claude -- "start the chain").
  31. TMUX_MAIN_SEQUENCE_SOCKET Isolated tmux socket name (tmux -L).
  32. Project list: one directory per line, with ~ expansion, blank lines and
  33. full-line # comments. Missing directories are warned about and skipped.
  34. EOF
  35. }
  36. add_missing=false
  37. attach=true
  38. for arg in "$@"; do
  39. case "$arg" in
  40. --add-missing) add_missing=true ;;
  41. --no-attach) attach=false ;;
  42. -h|--help) usage; exit 0 ;;
  43. *) printf 'tmux-main-sequence: unknown argument: %s\n' "$arg" >&2
  44. usage >&2; exit 2 ;;
  45. esac
  46. done
  47. session=${TMUX_MAIN_SEQUENCE_SESSION:-main}
  48. config=${TMUX_MAIN_SEQUENCE_CONFIG:-$HOME/.config/tmux-main-sequence/projects}
  49. script_dir=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
  50. tmux_cmd=(tmux)
  51. if [[ -n ${TMUX_MAIN_SEQUENCE_SOCKET:-} ]]; then
  52. tmux_cmd+=(-L "$TMUX_MAIN_SEQUENCE_SOCKET")
  53. fi
  54. attach_session() {
  55. local number=1 client_id
  56. if "$attach"; then
  57. while "${tmux_cmd[@]}" has-session -t "=$session-$number" 2>/dev/null; do
  58. number=$((number + 1))
  59. done
  60. client_id=$("${tmux_cmd[@]}" new-session -d -t "$session_id" -s "$session-$number" -P -F '#{session_id}')
  61. if "$fresh"; then
  62. "${tmux_cmd[@]}" select-window -t "$client_id:1"
  63. fi
  64. # Queue the option and attach together: a standalone set-option would
  65. # immediately destroy this still-unattached session before we could join it.
  66. if [[ -n ${TMUX:-} ]]; then
  67. "${tmux_cmd[@]}" set-option -t "$client_id" destroy-unattached on \; switch-client -t "$client_id"
  68. else
  69. "${tmux_cmd[@]}" set-option -t "$client_id" destroy-unattached on \; attach-session -t "$client_id"
  70. fi
  71. fi
  72. }
  73. fresh=false
  74. session_id=
  75. base_id=
  76. # A group may survive after its originally named session has been removed.
  77. while IFS='|' read -r id name group; do
  78. if [[ $group == "$session" ]]; then
  79. session_id=$id
  80. break
  81. fi
  82. if [[ $name == "$session" ]]; then base_id=$id; fi
  83. done < <("${tmux_cmd[@]}" list-sessions -F '#{session_id}|#{session_name}|#{session_group}' 2>/dev/null || true)
  84. session_id=${session_id:-$base_id}
  85. if [[ -n $session_id ]]; then
  86. if ! "$add_missing"; then
  87. attach_session
  88. exit 0
  89. fi
  90. else
  91. fresh=true
  92. session_id=$("${tmux_cmd[@]}" new-session -d -s "$session" -n dashboard -c "$HOME" -P -F '#{session_id}')
  93. "${tmux_cmd[@]}" set-option -t "$session_id" base-index 0
  94. "${tmux_cmd[@]}" set-option -t "$session_id" renumber-windows off
  95. initial_index=$("${tmux_cmd[@]}" display-message -p -t "$session_id:" '#{window_index}')
  96. if [[ $initial_index != 0 ]]; then
  97. "${tmux_cmd[@]}" move-window -s "$session_id:$initial_index" -t "$session_id:0"
  98. fi
  99. "${tmux_cmd[@]}" set-window-option -t "$session_id:0" automatic-rename off
  100. "${tmux_cmd[@]}" set-window-option -t "$session_id:0" allow-rename off
  101. fi
  102. add_window() {
  103. local index=$1 name=$2 dir=$3 start_agent=$4 existing max=-1 occupied=false pane
  104. while IFS= read -r existing; do
  105. [[ $existing != "$name" ]] || return 0
  106. done < <("${tmux_cmd[@]}" list-windows -t "$session_id" -F '#{window_name}')
  107. if [[ ! -d $dir ]]; then
  108. printf 'tmux-main-sequence: skipping missing directory: %s\n' "$dir" >&2
  109. return 0
  110. fi
  111. while IFS= read -r existing; do
  112. if (( existing == index )); then occupied=true; fi
  113. if (( existing > max )); then max=$existing; fi
  114. done < <("${tmux_cmd[@]}" list-windows -t "$session_id" -F '#{window_index}')
  115. if "$occupied"; then
  116. printf 'tmux-main-sequence: index %s occupied; appending %s at %s\n' "$index" "$name" "$((max + 1))" >&2
  117. index=$((max + 1))
  118. fi
  119. if "$start_agent"; then
  120. "$script_dir/tmux-chain" "$dir" --target "$session_id:$index" --name "$name" >/dev/null
  121. else
  122. pane=$("${tmux_cmd[@]}" new-window -d -t "$session_id:$index" -n "$name" -c "$dir" -P -F '#{pane_id}')
  123. "${tmux_cmd[@]}" set-window-option -t "$pane" automatic-rename off
  124. "${tmux_cmd[@]}" set-window-option -t "$pane" allow-rename off
  125. fi
  126. }
  127. add_window 0 dashboard "$HOME" false
  128. add_window 1 '~' "$HOME" true
  129. add_window 2 blog "$HOME/Source/Projects/subspace-collection/blog.clooney.io" true
  130. index=3
  131. if [[ -f $config ]]; then
  132. while IFS= read -r dir || [[ -n $dir ]]; do
  133. # Trim surrounding whitespace, preserving spaces inside directory names.
  134. dir="${dir#"${dir%%[![:space:]]*}"}"
  135. dir="${dir%"${dir##*[![:space:]]}"}"
  136. case "$dir" in
  137. ''|'#'*) continue ;;
  138. '~') dir=$HOME ;;
  139. '~/'*) dir="$HOME/${dir:2}" ;;
  140. esac
  141. while [[ $dir != / && $dir == */ ]]; do dir=${dir%/}; done
  142. add_window "$index" "$(basename "$dir")" "$dir" true
  143. index=$((index + 1))
  144. done < "$config"
  145. fi
  146. if "$fresh"; then
  147. "${tmux_cmd[@]}" select-window -t "$session_id:1"
  148. fi
  149. attach_session

这很好用,直到机器重启。tmux 和里面所有的 agent 一起消失。再运行 tmux-main-sequence,窗口都回来了,也启动了新的 chain,但它们都是新的 session。主 agent 不记得我们在聊什么,secondary 也忘了做过哪些决定。

这不是我的脚本特有的问题。任何用 tmux-agents start chain 的人都会遇到:重启后 chain 从头开始,上下文没了。

解决办法很简单。Claude Code 和 Codex 都会把过去的 session 保存在磁盘上,也都有可以搜索它们的 /resume 命令。所以重启之后,我在主 agent 的窗格里运行 /resume,搜到之前的 session,接着聊。

通常我只恢复这一个。没有重要的工作在进行时,新的 secondary 和 worker 就够用了。但要具体情况具体看:如果 secondary 或 worker 正在做重要的事,我也会恢复它们,让工作从停下的地方继续,而不是从头再来。

session 有名字的话,恢复起来方便得多。如果它们叫 "blog main agent"、"blog secondary" 这样的名字,一秒就能找到。没有名字的话,你得翻一整列第一条提示全是 "start the chain" 的 session。

试试看

用 skills CLI 安装 tmux-agents,agent-chain skill 也包含在内:

npx skills add TheClooneyCollection/tmux-agents -g

然后告诉你的 agent "set up tmux-agents",装好之后,再说 "start the chain"。