You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
claude-code/docs/ja/s09-agent-teams.md

7.8 KiB

s09: Agent Teams (エージェントチーム)

s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > [ s09 ] s10 > s11 > s12

"一人で終わらないなら、チームメイトに任せる" -- 永続チームメイト + JSONL メールボックス。

Harness 層: チームメールボックス -- 複数モデルをファイルで協調。

問題

サブエージェント (s04) は使い捨てだ: 生成し、作業し、要約を返し、消滅する。アイデンティティもなく、呼び出し間の記憶もない。バックグラウンドタスク (s08) はシェルコマンドを実行するが、LLM 誘導の意思決定はできない。

本物のチームワークには3つが必要: (1) 複数ターンの会話を超えて存続する永続エージェント、(2) アイデンティティとライフサイクル管理、(3) エージェント間の通信チャネル。

解決策

Teammate lifecycle:
  spawn -> WORKING -> IDLE -> WORKING -> ... -> SHUTDOWN

Communication:
  .team/
    config.json           <- team roster + statuses
    inbox/
      alice.jsonl         <- append-only, drain-on-read
      bob.jsonl
      lead.jsonl

              +--------+    send("alice","bob","...")    +--------+
              | alice  | -----------------------------> |  bob   |
              | loop   |    bob.jsonl << {json_line}    |  loop  |
              +--------+                                +--------+
                   ^                                         |
                   |        BUS.read_inbox("alice")          |
                   +---- alice.jsonl -> read + drain ---------+

仕組み

  1. TeammateManager が config.json でチーム名簿を管理する。
// src/main/java/io/mybatis/learn/s09/TeammateManager.java
public class TeammateManager {
    private final ChatModel chatModel;
    private final MessageBus bus;
    private final Path configPath;
    private final ObjectMapper mapper = new ObjectMapper();
    private Map<String, Object> config;
    // Python は threading.Thread + dict を使用、Java は ConcurrentHashMap で天然スレッドセーフ
    private final Map<String, Thread> threads = new ConcurrentHashMap<>();

    public TeammateManager(ChatModel chatModel, MessageBus bus, Path teamDir) {
        this.chatModel = chatModel;
        this.bus = bus;
        this.configPath = teamDir.resolve("config.json");
        Files.createDirectories(teamDir);
        this.config = loadConfig();
    }
  1. spawn() がチームメイトを作成し、スレッド内でエージェントループを開始する。
// Python は threading.Thread を使用、Java は Thread.startVirtualThread() 仮想スレッドを使用
public synchronized String spawn(String name, String role, String prompt) {
    Map<String, Object> member = new LinkedHashMap<>();
    member.put("name", name);
    member.put("role", role);
    member.put("status", "working");
    ((List<Map<String, Object>>) config.get("members")).add(member);
    saveConfig();

    // 仮想スレッド: 軽量、JVM スケジュール、OS スレッドを占有しない
    Thread thread = Thread.startVirtualThread(
            () -> teammateLoop(name, role, prompt));
    threads.put(name, thread);
    return "Spawned '" + name + "' (role: " + role + ")";
}
  1. MessageBus: 追記専用の JSONL インボックス。send() が1行を追記し、read_inbox() がすべて読み取ってドレインする。
// src/main/java/io/mybatis/learn/core/team/MessageBus.java
// Python は GIL で暗黙的にスレッドセーフ、Java は synchronized で明示的に保証
public class MessageBus {
    private final Path inboxDir;
    private final ObjectMapper mapper = new ObjectMapper();

    public synchronized String send(String sender, String to, String content,
                                    String msgType, Map<String, Object> extra) {
        Map<String, Object> msg = new LinkedHashMap<>();
        msg.put("type", msgType);
        msg.put("from", sender);
        msg.put("content", content);
        msg.put("timestamp", System.currentTimeMillis() / 1000.0);
        if (extra != null) msg.putAll(extra);

        Path inbox = inboxDir.resolve(to + ".jsonl");
        Files.writeString(inbox, mapper.writeValueAsString(msg) + "\n",
                StandardOpenOption.CREATE, StandardOpenOption.APPEND);
        return "Sent " + msgType + " to " + to;
    }

    public synchronized List<Map<String, Object>> readInbox(String name) {
        Path inbox = inboxDir.resolve(name + ".jsonl");
        if (!Files.exists(inbox)) return List.of();
        List<Map<String, Object>> messages = new ArrayList<>();
        for (String line : Files.readAllLines(inbox)) {
            if (!line.isBlank())
                messages.add(mapper.readValue(line, new TypeReference<>() {}));
        }
        Files.writeString(inbox, "");  // drain
        return messages;
    }
}
  1. 各チームメイトは call() 呼び出し間でインボックスをチェックし、メッセージをコンテキストに注入する。ChatClient の call() は Python の完全なツールループ(stop_reason != "tool_use" まで繰り返す)に相当する。
// Python のチームメイトは毎回の LLM 呼び出し前にインボックスをチェック、Java は毎回の call() 呼び出し間でチェック
protected void teammateLoop(String name, String role, String initialPrompt) {
    String sysPrompt = String.format(
            "You are '%s', role: %s. Use send_message to communicate.",
            name, role);

    var messageTool = new TeammateMessageTool(bus, name);
    ChatClient client = ChatClient.builder(chatModel)
            .defaultSystem(sysPrompt)
            .defaultTools(new BashTool(), new ReadFileTool(),
                    new WriteFileTool(), new EditFileTool(), messageTool)
            .build();

    // 初期作業(call() = 完全なツールチェーン、Python の stop_reason != "tool_use" までのループに相当)
    String response = client.prompt(initialPrompt).call().content();

    // 毎回の call() 間でインボックスをチェック(Python の毎回の LLM 呼び出し間ではなく)
    for (int round = 0; round < 50; round++) {
        Thread.sleep(2000);
        var inbox = bus.readInbox(name);
        if (inbox.isEmpty()) break;
        String inboxJson = mapper.writeValueAsString(inbox);
        response = client.prompt("<inbox>" + inboxJson + "</inbox>").call().content();
    }
    setStatus(name, "idle");
}

s08 からの変更点

コンポーネント 変更前 (s08) 変更後 (s09)
Tools 6 9 (+spawn/send/read_inbox)
エージェント数 単一 リーダー + N チームメイト
永続化 なし config.json + JSONL インボックス
スレッド バックグラウンドコマンド 各スレッドで完全なエージェントループ
ライフサイクル 使い捨て idle -> working -> idle
通信 なし message + broadcast

試してみる

cd learn-claude-code
mvn exec:java -Dexec.mainClass=io.mybatis.learn.s09.S09AgentTeams

以下のプロンプトを試してみよう (英語プロンプトの方が LLM に効果的だが、日本語でも可):

  1. Spawn alice (coder) and bob (tester). Have alice send bob a message.
  2. Broadcast "status update: phase 1 complete" to all teammates
  3. Check the lead inbox for any messages
  4. /team と入力してチーム名簿とステータスを確認する
  5. /inbox と入力してリーダーのインボックスを手動確認する