猫咖
首页博客工具
搜索
语言
切换网站风格
选择主题颜色
点击特效
主题

猫咖 · 持续更新中,源码见 GitHub。

Index 17: Autonomous Agents — 空闲循环 / 自动认领

2026年8月13日
AI智能体Kotlin后端

系列

使用Kotlin从0开发一个ClaudeCode

系列

使用Kotlin从0开发一个ClaudeCode

进度 17 / 21

使用Kotlin从0开发一个ClaudeCode

上一篇

Index 16: Team Protocols — 关机握手 / 计划审批

下一篇

Index 18: Worktree Isolation —— 任务-目录绑定

目标

s15/s16 给了多智能体协作的通信与协议,但所有任务仍需用户手动派发——用户得盯住 /task 和 /team,逐个催。有一类需求它覆盖不了:用户不在终端前时,系统自己干活。"把符号表优化任务交给闲着的审查员"、"任务清单里 3 个就绪任务自动分给空闲 teammate"——这些需要的是自治,不是手动调度。

Index 17 引入自治层,对齐 Claude Code 的 autonomous/loop 模式:

  1. AutoClaimer — 自动认领:就绪任务(s12 TaskScheduler.readyTasks)→ 空闲 teammate(s15 IDLE 成员),派发后标记 IN_PROGRESS,teammate 处理完回到 IDLE 时自动标记 COMPLETED
  2. IdleLoop — 空闲循环:后台协程周期性驱动 AutoClaimer,仅在主 REPL 空闲时 tick,把自治活动经 s13 通知队列展示给用户
  3. /auto 命令 — 自治模式的开关与状态

完成后的效果——用户建好任务、派生好 teammate,走开一会儿,回来发现任务已自动做完:

TEXT
$ cat-code
> /task add t3 跑基准测试
> 派生一个研究员和审查员
🤖 spawn teammate 'researcher' ...
🤖 spawn teammate 'reviewer' ...
# ── 用户走开 30 秒,IdleLoop 后台自动认领 ──
# 回来输入任意字符,回合间 drain:
🔔 auto - task t1 claimed by researcher
🔔 auto - task t3 claimed by reviewer
🔔 auto - task t1 completed by researcher
> /task list
Tasks (3):
  t1  [COMPLETED] 调研符号表缓存      ← 研究员空闲时自动做完
  t2  [PENDING]   实现哈希索引        ← 等更多空闲队友或主智能体继续
  t3  [COMPLETED] 跑基准测试
> /auto
Autonomous agents: ON (tick every 10s)
  claimed: 2 completed: 2
> /auto off
Autonomous agents: OFF

为什么需要

现实问题

s12 给了持久任务队列(TaskRecord + readyTasks),s15 给了空闲 teammate 池(IDLE 成员),s16 给了关机/计划协议。但它们之间是断的:

  • 就绪任务没人领 — /task 列出 [PENDING] 调研符号表,用户得手动挑一个 teammate 说"去做 t1"。任务在队列里躺着,空闲队友在 IDLE 挂着,中间没有自动连接。
  • 空闲队友闲置 — teammate 跑完初始任务进入 IDLE 等消息,没有新指令就一直闲着。而任务队列里可能积压 5 个就绪任务。
  • 用户必须在场 — 所有派发都发生在用户输入之后。用户走开 = 系统停摆,尽管任务和队友都"ready"。

依赖图表明 s17 是任务系统与团队系统的汇合点:

TEXT
s05 TodoWrite -> s12 TaskSystem -> s13 BackgroundTasks -> s14 CronScheduler
                              ↘ s17 AutonomousAgents
s06 Subagent ──→ s15 AgentTeams ──→ s16 TeamProtocols ──→ s17 AutonomousAgents

设计原则

原则取舍依据
完成检测用 IDLE 状态推断teammate 消费协程处理任务消息后自然 IDLE——不需要给 teammate 加"任务完成"协议,消费循环零改动
空闲循环是真·后台IdleLoop 是后台协程(非回合间 drain),用户不在时也干活——这才是"自治"
busy 门控保护主循环主 REPL 处理回合时 IdleLoop 跳过 tick,避免后台自治与主 AgentLoop 抢注意力
只认领不派生AutoClaimer 把任务派给现有 IDLE 队友;spawn 仍是主智能体的职责
活动经 s13 通知展示认领/完成推 NotificationQueue,用户下回合 drain 感知——复用既有异步感知通道
TaskStore 线程安全s17 首次后台线程写 TaskStore,@Synchronized 单操作原子(读-改-写竞态已文档化接受)

核心设计与实现

架构全景

TEXT
                         ┌─────────────────────────────────────────────┐
                         │               ReplLoop (主)                  │
                         │  ┌──────────────┐   ┌────────────────────┐  │
                         │  │  IdleLoop    │   │  TaskStore (s12)   │  │
                         │  │  (后台协程)   │──▶│  (磁盘持久化)        │  │
                         │  └──────┬───────┘   └─────────▲──────────┘  │
                         │   仅主空闲时 tick              │ update 状态   │
                         │   通知 → s13 Queue             │              │
                         └─────────┼─────────────────────┼──────────────┘
                                   │  AutoClaimer.tick()   │
                                   │  ①完成检测  ②就绪认领   │
        ┌──────────────────────────┼─────────────────────┼──────────┐
        │                    ┌─────▼─────┐          ┌─────▼─────┐    │
        │                    │ TeamStore │          │ MessageBus│    │
        │                    │ (s15 IDLE │          │ (s15 路由) │    │
        │                    │  成员查询) │          └─────┬─────┘    │
        │                    └─────┬─────┘                │ send      │
        │                          │ status              │ task 消息  │
        │                 ┌────────▼────────┐             │            │
        │                 │  researcher(IDLE)│◄────────────┘            │
        │                 │  消费协程 resume  │  "Autonomous task t1: …"  │
        │                 └─────────────────┘   RUNNING→IDLE            │
        └───────────────────────────────────────────────────────────────┘

模块依赖(新增 autonomous 包):

  • autonomous → task(TaskScheduler/TaskStore)、autonomous → team(TeamStore/MessageBus/TeamMessage)、autonomous → background(NotificationQueue)
  • repl → autonomous:ReplLoop 创建/启动,AutoCommand 控制
  • 不依赖 agent 包:自治层只操作 store 与消息总线,不直接碰 AgentLoop

新文件一览(src/main/kotlin/com/sepcai/code/autonomous/ 4 个 + repl/commands/AutoCommand.kt):

文件职责
AutonomousConfig.kttick 间隔、每 tick 认领上限 + DEFAULT_ 常量
ClaimTracker.kttaskId→teammate 认领映射(ConcurrentHashMap 线程安全)
AutoClaimer.kttick():完成检测 + 就绪认领 + 派发,返回 ClaimResult
IdleLoop.kt后台周期 tick + shouldRun 门控 + setEnabled 开关 + 通知 + 计数
repl/commands/AutoCommand.kt/auto status/on/off/tasks

修改文件:TaskStore.kt(@Synchronized,独立 refactor(s12) 提交)、ReplLoop.kt(busy 门控 + autoScope + 启动/清理)、ReplCommand.kt(ReplContext +autoClaimer/+idleLoop)、TaskToolTest.kt(flaky 修复,独立 test(s06) 提交)。


逐类拆解

AutoClaimer(自动认领)—— 自治核心

KOTLIN
// AutoClaimer.kt(节选)
class AutoClaimer(
    private val taskStore: TaskStore,      // s12
    private val teamStore: TeamStore,      // s15
    private val messageBus: MessageBus,    // s15
    private val tracker: ClaimTracker = ClaimTracker(),
    private val config: AutonomousConfig = AutonomousConfig()
) {
    fun tick(): ClaimResult {
        val completed = mutableListOf<String>()
        val claimed = mutableListOf<String>()

        // ① 完成检测(先于②,避免本 tick 新建认领被误判完成)
        for ((taskId, teammate) in tracker.snapshot()) {
            when (teamStore.get(teammate)?.status) {
                TeamStatus.IDLE -> {
                    taskStore.findById(taskId)?.let { task ->
                        taskStore.update(task.copy(status = TaskStatus.COMPLETED))
                        completed.add(taskId)
                    }
                    tracker.release(taskId)
                }
                TeamStatus.RUNNING -> {}          // 仍在处理
                else -> {                         // 队友被停/失败 -> 任务回 PENDING 可重认领
                    taskStore.findById(taskId)?.let {
                        taskStore.update(it.copy(status = TaskStatus.PENDING))
                    }
                    tracker.release(taskId)
                }
            }
        }

        // ② 就绪认领:就绪 PENDING 任务 → 空闲且无认领的队友
        val ready = TaskScheduler.readyTasks(taskStore.loadAll())
            .filter { it.status == TaskStatus.PENDING }
            .take(config.maxTasksPerTick)
        val idle = teamStore.all().filter {
            it.status == TeamStatus.IDLE && !tracker.hasPendingAssignment(it.name)
        }
        for ((task, teammate) in ready.zip(idle)) {
            tracker.assign(task.id, teammate.name)
            taskStore.update(task.copy(status = TaskStatus.IN_PROGRESS))
            messageBus.send(TeamMessage("main", teammate.name, formatTask(task), "task ${task.id}"))
            claimed.add(task.id)
        }
        return ClaimResult(claimed, completed)
    }
}

关键设计点:

  1. 完成检测基于 IDLE 状态推断。teammate 消费协程处理任务消息(resume 路径)→ RUNNING → IDLE。AutoClaimer 观察到「已分配队友 + 队友 IDLE」即判定任务完成。teammate 不需要知道任务是 TaskRecord——它只当普通消息处理。这是"零协议成本"的自治:复用 s15 的消费循环,不加任何新消息类型。

  2. tick 顺序防误判。① 完成检测在 ② 认领之前执行。本 tick 新建的认领(② 中 assign)不在 ① 的遍历快照里,绝不会在同一 tick 被误判完成。teammate 处理消息的时间远小于 tick 间隔(10s),下一 tick 时 IDLE 即已完成。

  3. hasPendingAssignment 防重复派发。步骤② 的空闲队友过滤排除已有认领的——一个 teammate 每个 tick 至多接一个任务(zip 配对天然保证)。

  4. 任务回滚。队友被停止/失败(终态)时,任务回 PENDING 可被重新认领——自治系统对队友死亡有弹性。

完成检测的正确性论证

IDLE 推断完成依赖一个关键前提:已认领任务的队友回到 IDLE 必然意味着处理完该任务。论证:

  1. 队友被认领时是 IDLE(步骤②只选 IDLE 成员)。
  2. 认领后消息立即入队:messageBus.send 把任务消息投递到队友 inbox(Channel 不丢消息)。
  3. 队友消费协程收到消息 → RUNNING:markRunning(IDLE→RUNNING)→ loop.run(task) → markIdle(RUNNING→IDLE)。
  4. 因此「队友 IDLE + 有认领」只有两种可能:刚认领未开始(本 tick 内),或处理完刚回到 IDLE。

tick 顺序消除歧义:① 完成检测在 ② 认领之前——本 tick 新建的认领(②)不在 ① 的遍历快照里,不会在同一 tick 被判定完成。所以「有认领 + IDLE」在①中出现时,必然来自上一 tick 或更早的认领,即队友已处理完。配合 tick 间隔(10s)远大于处理时间,判定可靠。

边界:队友被停止(终态)时 IDLE 推断失效——此时队友状态是 COMPLETED/FAILED,走 else 分支把任务回 PENDING。三种队友状态(IDLE/RUNNING/终态)覆盖所有情况,没有遗漏。

ClaimTracker(认领映射)

KOTLIN
// ClaimTracker.kt —— 线程安全(ConcurrentHashMap)
class ClaimTracker {
    private val assignments = ConcurrentHashMap<String, String>()
    fun assign(taskId: String, teammate: String)
    fun release(taskId: String)
    fun teammateFor(taskId: String): String?
    fun hasPendingAssignment(teammate: String): Boolean   // 防重复派发
    fun snapshot(): Map<String, String>                    // 遍历用拷贝
}

IdleLoop(空闲循环)

KOTLIN
// IdleLoop.kt(节选)
class IdleLoop(
    private val autoClaimer: AutoClaimer,
    private val notifications: NotificationQueue,      // s13
    private val config: AutonomousConfig = AutonomousConfig(),
    private val delayMillis: Long = config.tickIntervalMillis,
    private val shouldRun: () -> Boolean = { true }    // 主 REPL 空闲门控
) {
    fun start(scope: CoroutineScope) {
        if (job?.isActive == true) return
        job = scope.launch {
            while (isActive) {
                if (enabled.get() && shouldRun()) {
                    val result = autoClaimer.tick()
                    result.claimed.forEach { id ->
                        claimedCount.incrementAndGet()
                        notifications.push(BackgroundNotification(id, BackgroundStatus.RUNNING, "autonomous task $id claimed"))
                    }
                    result.completed.forEach { id ->
                        completedCount.incrementAndGet()
                        notifications.push(BackgroundNotification(id, BackgroundStatus.COMPLETED, "autonomous task $id completed"))
                    }
                }
                delay(delayMillis)
            }
        }
    }
    fun setEnabled(enabled: Boolean)   // /auto on|off
    fun isEnabled(): Boolean
    fun stop() / isRunning() / claimedCount() / completedCount() / tickIntervalMillis()
}

两层门控:enabled(/auto 开关,用户显式控制)与 shouldRun(主 REPL 空闲门控,ReplLoop 注入 { !busy })。协程生命周期由 ReplLoop 管理(start/stop),自治活动由 enabled 开关控制——两者解耦:/auto off 不杀协程,只让循环空转。

AutoCommand

KOTLIN
// AutoCommand.kt(节选)
override suspend fun execute(args: String, ctx: ReplContext): ReplCommandResult {
    return when (args.trim()) {
        "", "status" -> status(ctx)
        "on" -> { ctx.idleLoop.setEnabled(true); println("Autonomous agents: ON"); Continue }
        "off" -> { ctx.idleLoop.setEnabled(false); println("Autonomous agents: OFF"); Continue }
        "tasks" -> tasks(ctx)
        else -> { println("Unknown subcommand"); println(USAGE); Continue }
    }
}

private fun status(ctx: ReplContext): ReplCommandResult {
    val state = if (ctx.idleLoop.isEnabled()) "ON" else "OFF"
    println("Autonomous agents: $state (tick every ${ctx.idleLoop.tickIntervalMillis() / 1000}s)")
    println("  claimed: ${ctx.idleLoop.claimedCount()}  completed: ${ctx.idleLoop.completedCount()}")
    val ready = TaskScheduler.readyTasks(ctx.taskStore.loadAll()).filter { it.status == TaskStatus.PENDING }
    if (ready.isNotEmpty()) println("  ${ready.size} ready task(s) awaiting claim -- /auto tasks")
    return ReplCommandResult.Continue
}

private fun tasks(ctx: ReplContext): ReplCommandResult {
    val ready = TaskScheduler.readyTasks(ctx.taskStore.loadAll()).filter { it.status == TaskStatus.PENDING }
    val claims = ctx.autoClaimer.claims()
    if (ready.isEmpty() && claims.isEmpty()) { println("No ready tasks or active claims."); return Continue }
    ready.forEach { println("  ${it.id}  ${it.title}") }
    claims.forEach { (taskId, teammate) -> println("  $taskId -> $teammate") }
    return ReplCommandResult.Continue
}

子命令:/auto(状态)/on/off(开关)/tasks(就绪任务 + 认领映射)。

接线细节:ReplLoop

KOTLIN
// ReplLoop.kt
private val busy = AtomicBoolean(false)   // s17: 主 REPL 忙态门控
private lateinit var autoClaimer: AutoClaimer
private lateinit var idleLoop: IdleLoop
private lateinit var autoScope: CoroutineScope
KOTLIN
// ReplLoop.start() 内,s15 team 设置之后:
autoScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
autoClaimer = AutoClaimer(taskStore, teamStore, messageBus)   // 复用 s12/s15 的 store
idleLoop = IdleLoop(
    autoClaimer = autoClaimer,
    notifications = notificationQueue,       // s13 通知队列
    shouldRun = { !busy.get() }              // 主 REPL 空闲门控
)
idleLoop.start(autoScope)

busy 门控(主循环内):

KOTLIN
busy.set(true)
try {
    when {
        input.startsWith(COMMAND_PREFIX) -> { val c = handleCommand(input); if (!c) break }
        else -> handleChat(input)
    }
} finally {
    busy.set(false)
}

退出清理(finally):idleLoop.stop() + autoScope.cancel()。

ReplContext +2 字段:autoClaimer(/auto tasks 读认领)、idleLoop(/auto status/on/off)。测试支持 TeamCtxFields 同步 +2。

错误处理

失败场景行为
无就绪任务tick 的认领为空(no-op)
无空闲队友就绪任务等待,不派发(不自治 spawn)
队友被停/失败(有认领)任务回 PENDING,可被重新认领
任务已删除(有认领)释放认领,跳过
/auto 重复 on/offsetEnabled 幂等
IdleLoop 协程重复 startno-op(已有活跃任务)
会话退出idleLoop.stop + autoScope.cancel

与 s12/s15 的衔接

  • s12 → s17:TaskScheduler.readyTasks 是就绪任务的事实来源;TaskStore 是任务的单一存储。s17 只读任务状态 + 更新状态,不新增任务。
  • s15 → s17:TeamStore 的 IDLE 状态是"可认领"的信号(s15 设计 IDLE 时就预留了);MessageBus 承担任务消息路由;teammate 消费协程的 resume 路径零改动。
  • s16 → s17:s16 的关机握手与 s17 的自治正交但协作——自治派发的任务可能被关机打断(队友终态 → 任务回 PENDING)。

与 s12/s15/s16 的对比总览

维度s12 TaskSystems15 Agent Teamss16 Team Protocolss17 Autonomous
核心任务持久化 + 依赖调度命名 teammate + 消息关机握手 + 计划审批空闲循环 + 自动认领
角色任务队列执行者池协议层调度桥
数据TaskRecord (磁盘)TeamMember (内存)ShutdownStatus/PlanProposal (内存)ClaimTracker (内存)
驱动用户 /task用户/LLM 消息主回合间 drain后台周期 tick
时间维度跨会话会话内会话内会话内(用户不在场也跑)
与用户交互/task 命令/team 命令/team plans/stop/auto 命令
状态机PENDING→IN_PROGRESS→COMPLETEDPENDING→RUNNING↔IDLEREQUESTED→ACK→COMPLETED(复用前两者)
依赖独立→ subagent, task→ team (新增)→ task, team, background

定位:s17 是调度桥——把 s12 的任务队列(何时做)与 s15 的执行者池(谁来做的空闲成员)连接起来,在 s16 协议的保护下自动运转。它是阶段四里第一个"让系统自己动起来"的索引。


端到端:任务自治生命周期

TEXT
Step 1  用户 /task add t1 "调研符号表缓存" + /team spawn 研究员
        │ TaskStore.save(t1) → PENDING;teammate 消费协程跑完初始 prompt → IDLE
        ▼
Step 2  IdleLoop 后台 tick(主 REPL 空闲,busy=false,enabled=true)
        │ AutoClaimer.tick()(AutoClaimer.kt:52)
        │   ① 完成检测:无认领 → 跳过
        │   ② 就绪认领:t1(PENDING) → researcher(IDLE)
        │     ├─ tracker.assign("t1", "researcher")(ClaimTracker.kt:28)
        │     ├─ taskStore.update(t1 → IN_PROGRESS)
        │     └─ messageBus.send(TeamMessage("main", "researcher", "Autonomous task t1: …"))
        │   通知:🔔 auto - task t1 claimed
        ▼
Step 3  researcher 消费协程收到任务消息(resume)
        │   markRunning → loop.run("Autonomous task t1: 调研符号表缓存…") → markIdle
        │   researcher 处理完回到 IDLE
        ▼
Step 4  下一次 IdleLoop tick
        │ AutoClaimer.tick()
        │   ① 完成检测:t1→researcher,researcher 现在 IDLE → t1 COMPLETED
        │     ├─ taskStore.update(t1 → COMPLETED)
        │     └─ tracker.release("t1")
        │   通知:🔔 auto - task t1 completed
        ▼
Step 5  用户回来输入任意字符 → 回合间 drainNotifications()
        │   🔔 auto - task t1 claimed by researcher
        │   🔔 auto - task t1 completed by researcher
        ▼
Step 6  用户 /task list → t1 [COMPLETED];/auto → ON, claimed: 1 completed: 1

这个生命周期把 s12(任务)、s15(团队)、s13(通知)三个系统串成一条自治链:任务就绪 → 自动认领 → 队友处理 → 自动完成 → 用户感知。中间没有任何手动步骤。


会话与线程安全

s17 首次引入后台线程写 TaskStore(IdleLoop 在 Dispatchers.Default 上 tick,/task 命令在主线程)。三个层面的处理:

  1. TaskStore @Synchronized(refactor(s12)):全部公开方法原子化。这是最小改造——TeamStore/MessageBus/NotificationQueue 已是 ConcurrentHashMap/CLQ 线程安全,无需改。
  2. 已知局限(文档化):跨操作的读-改-写(loadAll 后 update)在并发时理论上有 lost-update 窗口。AutoClaimer 只更新自己刚读到的任务,/task 通常操作不同任务——实际风险极低,接受。
  3. busy 门控:主 REPL 处理回合时 IdleLoop 跳过 tick,减少主线程与后台对同一批 store 的并发窗口。

会话退出:finally 里 idleLoop.stop() + autoScope.cancel()——与 s13/s14/s15 的 scope 清理一致。


测试策略

测试类覆盖点
AutonomousConfigTest (2)默认值、自定义值、tickIntervalMillis
ClaimTrackerTest (7)assign/release/teammateFor、覆盖、hasPendingAssignment、快照独立
AutoClaimerTest (9)认领就绪任务+派发、队友 IDLE→COMPLETED、同 tick 不误判完成、无就绪/无空闲不动作、单队友单任务、队友终态→回 PENDING、maxTasksPerTick、跳过 BLOCKED
IdleLoopTest (5)周期 tick 认领、通知推送、shouldRun=false 跳过、start 幂等、stop 停止
AutoCommandTest (6)/auto 状态/on/off/tasks、空提示、未知子命令
TaskToolTest 修复BlockingLlm 使并发上限测试确定性(独立 test(s06) 提交)

测试设计坑(记录如下):

  1. 完成检测的 tick 时序:认领任务后,同一 tick 不能误判完成(① 先于 ②)。测试断言「tick 1 只 claim、teammate 走 RUNNING→IDLE 后 tick 2 才 complete」——用模拟的 teamStore 状态转换驱动,不启动真实消费协程。

  2. 通知在 tick() 返回后才推送:awaitCondition { taskStore.status == IN_PROGRESS } 可能早于通知 push。修复:等 notifications.hasPending() 而非仅等任务状态。

  3. stop 与 in-flight tick 的竞态:stop() 不中断已开始的 tick。测试等 claimedCount() >= 1(tick 的 forEach 完成)后再断言计数稳定,而非读瞬时值。

  4. poll() 两次取空(AutoClaimerTest 初版踩坑):连续两次 inbox.poll() 第一次消费、第二次 null。改为 val msg = poll()!! 再断言两个 content。

  5. 顺带修复 pre-existing flaky:TaskToolTest 并发上限测试依赖"第一个子代理仍 RUNNING",快速 FakeLLM 会提前完成导致 flaky。用 BlockingLlm(awaitCancellation())占住名额,独立 test(s06) 提交。


开发过程记录

设计矛盾 1:完成检测——IDLE 推断 vs 显式完成协议

初版考虑给 teammate 加"任务完成"信号(teammate 处理完任务消息后发一条 DONE 消息回 main)。审查发现:

  • 加显式协议需要 teammate 感知"这是任务消息"(区分普通消息与任务消息),污染消费协程。
  • 而 teammate 处理消息后自然回到 IDLE 已经是事实——IDLE 状态本身就是完成信号。

取舍:用 IDLE 状态推断完成,消费循环零改动。代价是推断是"近似"的(IDLE 可能表示"处理完"或"刚派生完"),但结合认领映射(tracker 记录"这个任务分配给了这个队友"),推断是可靠的——只有被分配了任务的队友回到 IDLE 才判完成。

设计矛盾 2:空闲循环真并发 vs 回合间 drain

s15/s16 的自治活动都是回合间 drain(同步、主线程)。IdleLoop 若也做成 drain,就只是"每次用户输入后跑一次认领"——用户不在时系统仍停摆,不叫自治。

取舍:IdleLoop 是真后台协程,用户不在时也 tick。代价是引入并发写 TaskStore 的线程安全问题(@Synchronized 解决)+ busy 门控(避免与主 AgentLoop 抢注意力)。这是"自治"价值的必然成本。

设计矛盾 3:enabled 开关与协程生命周期解耦

/auto off 应该做什么?两个选择:

  • 杀掉 IdleLoop 协程(stop)——但协程生命周期归 ReplLoop 管,命令里没有 scope 引用。
  • 只关自治活动(setEnabled)——协程常驻空转。

取舍:setEnabled 与 start/stop 解耦。/auto on|off 只切 enabled 开关,协程常驻(ReplLoop 启动/退出控制)。这样 AutoCommand 不需要持有 scope,且反复 on/off 无协程重建开销。

设计矛盾 4:自治不派生

用户离开前建了任务但忘了派生 teammate——AutoClaimer 要不要自动 spawn 队友来干活?

取舍:不自治 spawn。派生是创建新资源的动作(LLM 调用、工具隔离),自动 spawn 可能失控(无限派队友)。AutoClaimer 只认领现有 IDLE 成员;没有空闲队友时任务等待,并可在 /auto status 提示"有就绪任务待认领"。这是"自治执行"与"自主扩容"的边界——s17 只做前者。

IdleLoop 与 CronScheduler 的对比(两个后台 ticker)

s14 的 CronScheduler 也是后台周期循环——为什么 s17 不直接复用它的模式?对比:

维度CronScheduler (s14)IdleLoop (s17)
触发条件按 cron 时间规则按固定 tick 间隔 + 空闲门控
触发动作派发 shell 命令派发任务消息给 teammate
状态来源CronStore(磁盘)TaskStore(磁盘)+ TeamStore(内存)
活动感知通知 s13 Queue通知 s13 Queue
有状态吗nextFireCache(下次触发缓存)ClaimTracker(认领映射)
是否可开关enabled 字段(任务级)setEnabled(系统级,/auto)

共同骨架:start(scope) 幂等、stop() 取消、while(isActive){ tick(); delay(interval) }。s13 确立的「后台协程 + 通知队列 + scope 生命周期」模式,在 s14(cron)和 s17(自治)被复用。区别在于 tick 的内容——cron 按时间规则触发 shell,IdleLoop 按就绪度派发任务。

关键取舍汇总

决策选择理由
完成检测IDLE 状态推断(无新协议)teammate 不感知 TaskRecord;消费循环零改动
IdleLoop 并发后台协程 + busy 门控真·空闲干活;忙时不影响主 AgentLoop
TaskStore 并发@Synchronized 单操作最小改造;读-改-写竞态极低风险已文档化
认领上限maxTasksPerTick=3防止一次 tick 把队友全塞满
不自治 spawn只认领现有 IDLE 成员派生是主智能体职责;自主扩容失控风险
/auto 开关setEnabled 与协程解耦命令无需 scope;on/off 无重建开销

真实对话示例:用户离开,系统自治

把自治机制放进一个完整场景——用户建任务、派生队友、走开、回来验收:

TEXT
# ── 用户离开前:建任务 + 派生队友 ──
> /task add t1 调研符号表缓存
Task created: t1
> /task add t2 实现哈希索引
Task created: t2
> /task add t3 跑基准测试
Task created: t3          ← t2 阻塞于 t1,t3 阻塞于 t2(依赖链)
> 派生一个研究员和一个审查员
🤖 spawn teammate 'researcher' ...
🤖 spawn teammate 'reviewer' ...
> /team
Team members (2):
  researcher  [IDLE]
  reviewer    [IDLE]
> /task list
Tasks (3):
  t1  [PENDING]   调研符号表缓存        ← 就绪
  t2  [PENDING]   实现哈希索引          ← BLOCKED by t1
  t3  [PENDING]   跑基准测试            ← BLOCKED by t2

# ── 用户走开。IdleLoop 每 10s tick 一次 ──
#    tick1: 就绪 t1 → researcher(IDLE) 认领
#    tick2: researcher 已 IDLE(处理完 t1)→ t1 COMPLETED;t2 就绪 → reviewer 认领
#    tick3: reviewer 完成 t2 → t2 COMPLETED;t3 就绪 → researcher 认领
#    tick4: researcher 完成 t3 → t3 COMPLETED

# ── 用户回来,输入任意字符,回合间 drain ──
> 今天进展如何?
🔔 auto - task t1 claimed by researcher
🔔 auto - task t1 completed by researcher
🔔 auto - task t2 claimed by reviewer
🔔 auto - task t2 completed by reviewer
🔔 auto - task t3 claimed by researcher
🔔 auto - task t3 completed by researcher
[Agent] 三号任务全部完成。依赖链 t1→t2→t3 已按顺序自动执行。

# ── 用户验收 ──
> /task list
Tasks (3):
  t1  [COMPLETED] 调研符号表缓存
  t2  [COMPLETED] 实现哈希索引
  t3  [COMPLETED] 跑基准测试
> /auto
Autonomous agents: ON (tick every 10s)
  claimed: 3 completed: 3
> /auto off
Autonomous agents: OFF

这个示例展示了 s17 与 s12 的依赖调度联动:t1→t2→t3 的依赖链由 TaskScheduler.readyTasks 保证——每个 tick 只有就绪任务被认领,完成一个才解锁下一个。依赖感知 + 自治认领合起来,让整个任务流水线在用户离开时自动跑完。这是 s12 依赖系统 + s17 自治层的组合价值。


改动足迹与增量统计

新增文件(git status 视角):

TEXT
autonomous/                     (4 个新文件)
├── AutonomousConfig.kt         # tick 间隔/上限配置
├── ClaimTracker.kt             # 认领映射(taskId→teammate)
├── AutoClaimer.kt              # tick():完成检测 + 就绪认领
└── IdleLoop.kt                 # 后台周期 tick + 开关 + 通知
repl/commands/AutoCommand.kt    # /auto 控制命令

修改文件(2 个 main + 7 个 test):

TEXT
main: ReplLoop.kt(busy 门控/autoScope/启动清理)、ReplCommand.kt(ReplContext +2)
test: AutoClaimerTest(+9)、IdleLoopTest(+5)、AutoCommandTest(+6)、
      ClaimTrackerTest(+7)、AutonomousConfigTest(+2)、
      TestTeamSupport(+2 字段)、6 个命令测试 ReplContext 补字段

独立提交(本次顺带):refactor(s12) TaskStore @Synchronized(s17 并发访问前提)、test(s06) TaskToolTest flaky 修复(BlockingLlm 确定性)。

测试增量:s17 新增 ~29 个用例(s16 结束约 646,s17 结束约 675)。全量 ./gradlew test 绿。

本次实现的实际成本:核心自治逻辑(AutoClaimer + IdleLoop)约半天;接线(ReplContext +2 字段扩散到 6 个命令测试 + ReplLoop busy 门控)占另一半。ReplContext 字段已 18 个——s20 Comprehensive Agent 前必须聚合(已写入博客"下一站"提醒)。


常见问题

Q:用户走开时任务自动被队友认领,会不会失控? 不会。三个保护:① 只认领现有 IDLE 队友,不自治 spawn;② maxTasksPerTick=3 限制单次 tick 派发量;③ enabled 开关(/auto off)可随时停。此外任务有依赖(blockedBy),只有就绪任务会被认领,依赖链天然节流。

Q:teammate 怎么知道自己在做任务? 它不知道。任务消息只是普通的 TeamMessage(MESSAGE kind),teammate 当"主智能体给它发的一条消息"处理。这是刻意的——自治层的认领逻辑完全在系统侧,teammate 不需要感知 TaskRecord。代价是 teammate 无法区分"普通指令"和"自治任务",但这对执行没有影响。

Q:自治任务的结果谁看? teammate 处理任务消息后的回复(如有)走 send_message 到 main 收件箱,回合间 drain 展示。任务的 COMPLETED 状态在 /task list 可见,认领/完成活动在 /auto 可见。结果与状态双通道可观测。

Q:和 s06 的 TaskTool(子代理)什么关系? s06 task 是主智能体手动派发匿名子代理(任务级)。s17 AutoClaimer 是自动把 s12 持久任务派发给 s15 命名 teammate(队列级)。两者正交:手动派发即时子任务用 s06,自动处理持久任务队列用 s17。


下一站

s17 为阶段四后续 Index 留下什么基础:

  1. s18 Worktree Isolation — 自治派发的任务消息是"把任务交给 teammate"的载体,s18 可让 AutoClaimer 在派发时同时为任务创建 worktree 隔离。
  2. s19 MCP Plugin — 自治任务可能触发 teammate 调用 MCP 工具;IdleLoop 的活动通知机制可扩展到 MCP 工具结果。
  3. s20 Comprehensive Agent — IdleLoop 的"后台周期 tick"将成为"全部机制归到一个循环"的运行骨架;主 inbox 自动续跑(s17 明确未做)也在这里补上。

未完事项:主 inbox 的自动续跑(teammate 消息自动注入主 AgentLoop)与同步 REPL 结构冲突,留 s20;自治任务没有并发上限保护(任务消息可能塞满队友 inbox);权限/计划审批仍需用户输入,无法完全自治。

目录

当前章节:目标

  • 1. 目标
  • 2. ── 用户走开 30 秒,IdleLoop 后台自动认领 ──
  • 3. 回来输入任意字符,回合间 drain:
  • 4. 为什么需要
  • 5. 现实问题
  • 6. 设计原则
  • 7. 核心设计与实现
  • 8. 架构全景
  • 9. 逐类拆解
  • 10. 接线细节:ReplLoop
  • 11. 错误处理
  • 12. 与 s12/s15 的衔接
  • 13. 与 s12/s15/s16 的对比总览
  • 14. 端到端:任务自治生命周期
  • 15. 会话与线程安全
  • 16. 测试策略
  • 17. 开发过程记录
  • 18. 设计矛盾 1:完成检测——IDLE 推断 vs 显式完成协议
  • 19. 设计矛盾 2:空闲循环真并发 vs 回合间 drain
  • 20. 设计矛盾 3:enabled 开关与协程生命周期解耦
  • 21. 设计矛盾 4:自治不派生
  • 22. IdleLoop 与 CronScheduler 的对比(两个后台 ticker)
  • 23. 关键取舍汇总
  • 24. 真实对话示例:用户离开,系统自治
  • 25. ── 用户离开前:建任务 + 派生队友 ──
  • 26. ── 用户走开。IdleLoop 每 10s tick 一次 ──
  • 27. tick1: 就绪 t1 → researcher(IDLE) 认领
  • 28. tick2: researcher 已 IDLE(处理完 t1)→ t1 COMPLETED;t2 就绪 → reviewer 认领
  • 29. tick3: reviewer 完成 t2 → t2 COMPLETED;t3 就绪 → researcher 认领
  • 30. tick4: researcher 完成 t3 → t3 COMPLETED
  • 31. ── 用户回来,输入任意字符,回合间 drain ──
  • 32. ── 用户验收 ──
  • 33. 改动足迹与增量统计
  • 34. 常见问题
  • 35. 下一站
回到顶部

相关推荐

查看全部文章
Index 21: Terminal UX —— Claude Code 风格终端交互

Index 21: Terminal UX —— Claude Code 风格终端交互

2026年8月21日

Index 21 将 cat-code 终端交互升级为 Claude Code 风格,解决旧 REPL 黑箱、无中断及输入体验差的问题。通过 JLine3 与 Mordant 实现历史补全、实时工具可见性、Spinner 状态及 Esc 中断。核心采用 UI 与 AgentLoop 解耦的事件流架构,支持内联权限菜单与优雅降级。同时配置 logback 收敛控制台日志,确保 TUI 清爽且功能无损,显著提升可用性。

Index 20: Comprehensive Agent —— 全机制集成(收口)

Index 20: Comprehensive Agent —— 全机制集成(收口)

2026年8月13日

作为收官之作,把 s01-s19 的二十个核心机制整合为一个全面智能体:统一的状态查询与运行周期、自治认领与工作区隔离的贯通,以及 /status 命令的全局可视化。全机制协同运转,标志着 Cat-Code 从零完整复刻 Claude Code 核心能力的收官。

Index 19: MCP Plugin —— 多传输 / 通道路由 / 工具池组装

Index 19: MCP Plugin —— 多传输 / 通道路由 / 工具池组装

2026年8月13日

引入 MCP(Model Context Protocol)插件机制,通过多传输适配与通道路由,把外部 MCP 服务器的工具动态接入智能体的工具池。工具注册从静态编译期扩展为运行时动态组装,让智能体能力随外部服务即插即用。