docs(tasks-2): rewrite with concrete implementation plan based on existing code
Analyze current SubagentManager/SpawnTool/Sandbox implementation and restructure TASKS-2 into 3 phases with 7 concrete tasks: - Phase 1: ContainerMessage channels + AskConductorTool + escalation - Phase 2: Deliberate Plan Mode (clarifying→review→executing) - Phase 3: SubagentEnvironment MEMORY.md injection + guidance Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c95dee28ef
commit
3233752715
1 changed files with 593 additions and 147 deletions
740
todo/TASKS-2.md
740
todo/TASKS-2.md
|
|
@ -1,34 +1,89 @@
|
||||||
# TASKS-2: Subagent Orchestration (Container Model)
|
# TASKS-2: Subagent Orchestration (Container Model)
|
||||||
|
|
||||||
SubagentManager を Container ベースの Orchestrator に進化させる。
|
SubagentManager を Container ベースの Orchestrator に進化させる。
|
||||||
セッション管理の内部実装 (TASKS-3) には依存しない — 現行の SessionManager 上で動作させ、後から SessionStore に差し替える。
|
escalation chain(質問→回答)と Deliberate preset の plan mode を追加。
|
||||||
|
|
||||||
|
## 既存実装の現状
|
||||||
|
|
||||||
|
TASKS-2 の下地はかなり実装済み。以下を前提として差分のみ実装する。
|
||||||
|
|
||||||
|
### 実装済み ✅
|
||||||
|
|
||||||
|
| 要素 | ファイル | 状態 |
|
||||||
|
|---|---|---|
|
||||||
|
| **SubagentManager** | `pkg/tools/subagent.go` | Spawn/SubagentTool、タスク管理、goroutine lifecycle、WaitAll |
|
||||||
|
| **SpawnTool / SubagentTool** | `pkg/tools/spawn.go`, `subagent.go` | async/blocking 両パターン |
|
||||||
|
| **Preset 定義** | `pkg/tools/sandbox.go` | 5 preset、`SandboxConfig`、`ExecPolicy`、`AllowedToolsForPreset()` |
|
||||||
|
| **Registry 隔離** | `subagent.go` `buildPresetRegistry()` | preset 毎に専用 ToolRegistry + ExecTool インスタンス |
|
||||||
|
| **orch.Broadcaster** | `pkg/orch/broadcaster.go` | Spawn/StateChange/Conversation/GC イベント配信 |
|
||||||
|
| **SessionRecorder** | `pkg/tools/session_recorder.go` + `pkg/agent/session_recorder.go` | Fork/Turn/Completion/Report の DAG 記録 |
|
||||||
|
| **SubagentEnvironment 型** | `pkg/tools/sandbox.go` | Workspace/WorktreeDir/Background/Constraints/ContextFiles |
|
||||||
|
| **Async Callback** | `pkg/agent/loop.go` `processRequest` | spawn 完了 → MessageBus → conductor に結果注入 |
|
||||||
|
| **Orchestration Nudge** | `pkg/agent/loop.go` `buildOrchReminder()` | plan 実行中に spawn/subagent 使用を促すリマインダ |
|
||||||
|
|
||||||
|
### 未実装 ❌ → TASKS-2 スコープ
|
||||||
|
|
||||||
|
| # | 要素 | 概要 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | **ContainerMessage channel** | subagent→conductor の question/status/result 双方向通信 |
|
||||||
|
| 2 | **Escalation chain** | subagent question → conductor 回答 or → human escalate |
|
||||||
|
| 3 | **Deliberate Plan Mode** | coder/worker/coordinator の clarifying→review→executing 状態遷移 |
|
||||||
|
| 4 | **SubagentEnvironment injection** | MEMORY.md からの自動コンテキスト注入 |
|
||||||
|
| 5 | **MEMORY.md Orchestration section** | conductor guidance に delegated/findings/decisions 追記 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 設計コンテキスト
|
## 設計コンテキスト
|
||||||
|
|
||||||
### アーキテクチャ
|
### 現行アーキテクチャ(spawn 非同期フロー)
|
||||||
|
|
||||||
```
|
```
|
||||||
Conductor goroutine
|
Conductor (main loop)
|
||||||
│ ContainerRequest (task, preset, environment)
|
│ SpawnTool.Execute({"task":..., "preset":"scout"})
|
||||||
▼
|
│
|
||||||
Container goroutine: provision → run → finalize
|
├─ manager.Spawn() → goroutine 起動 → AsyncResult 即返却
|
||||||
│ ContainerMessage (question / result / status)
|
│
|
||||||
▼
|
│ [goroutine]
|
||||||
Conductor goroutine
|
│ ├─ buildPresetRegistry(preset) ← 隔離された ToolRegistry
|
||||||
│ answer (question への回答)
|
│ ├─ RunToolLoop(messages, tools) ← LLM + tool 実行ループ
|
||||||
▼ (わからなければ human に escalate)
|
│ ├─ recorder.RecordCompletion()
|
||||||
Container goroutine (再開)
|
│ ├─ reporter.ReportGC()
|
||||||
|
│ └─ callback() → msgBus.PublishInbound() → conductor に結果到着
|
||||||
|
│
|
||||||
|
▼ conductor 次イテレーションで結果を受信
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container 追加後のアーキテクチャ
|
||||||
|
|
||||||
|
```
|
||||||
|
Conductor (main loop)
|
||||||
|
│ SpawnTool.Execute({"task":..., "preset":"coder"})
|
||||||
|
│
|
||||||
|
├─ manager.Spawn() → goroutine 起動 → AsyncResult 即返却
|
||||||
|
│
|
||||||
|
│ [goroutine: Container]
|
||||||
|
│ ├─ buildPresetRegistry(preset)
|
||||||
|
│ ├─ RunToolLoop() — subagent が ask_conductor tool を呼ぶ
|
||||||
|
│ │ └─ AskConductorTool.Execute()
|
||||||
|
│ │ ├─ outCh ← ContainerMessage{Type:"question", Content:...}
|
||||||
|
│ │ ├─ ← inCh (ブロック: conductor の回答を待つ)
|
||||||
|
│ │ └─ return ToolResult{ForLLM: answer}
|
||||||
|
│ │
|
||||||
|
│ ├─ recorder.RecordCompletion()
|
||||||
|
│ └─ callback() → conductor に結果到着
|
||||||
|
│
|
||||||
|
▼ conductor は outCh からの question を受信し回答 or escalate
|
||||||
```
|
```
|
||||||
|
|
||||||
### escalation chain
|
### escalation chain
|
||||||
|
|
||||||
```
|
```
|
||||||
subagent (clarifying) → question → conductor
|
subagent (clarifying) → outCh: question → conductor 受信
|
||||||
→ conductor が答えられる: inCh に回答
|
→ conductor が答えられる → inCh: answer → subagent 再開
|
||||||
→ conductor もわからない: message tool で human に投げ、回答を転送
|
→ conductor もわからない → message tool で human に投げ → 回答を転送 → inCh
|
||||||
```
|
```
|
||||||
|
|
||||||
### Presets (5種)
|
### Presets (5種、定義済み)
|
||||||
|
|
||||||
| preset | 性格 | write | exec | search | spawn |
|
| preset | 性格 | write | exec | search | spawn |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
|
|
@ -38,190 +93,581 @@ subagent (clarifying) → question → conductor
|
||||||
| `worker` | Deliberate | o sandbox | build/package manager 系 | o | x |
|
| `worker` | Deliberate | o sandbox | build/package manager 系 | o | x |
|
||||||
| `coordinator` | Deliberate | o sandbox | go/pnpm/bun/curl 系 | o | scout-worker のみ |
|
| `coordinator` | Deliberate | o sandbox | go/pnpm/bun/curl 系 | o | scout-worker のみ |
|
||||||
|
|
||||||
**性格の分類:**
|
|
||||||
- **Exploratory** (scout/analyst): open-ended、見てきて報告。clarifying フェーズなし。
|
|
||||||
- **Deliberate** (coder/worker/coordinator): 成果物を作る。clarifying フェーズあり。
|
|
||||||
|
|
||||||
**npm は全 preset で禁止** (git worktree に node_modules が生じるため)。pnpm/bun は可。
|
|
||||||
**websearch/webfetch は全 preset で許可** (read-only のため)。
|
|
||||||
|
|
||||||
### exec allowlist regex
|
|
||||||
|
|
||||||
```go
|
|
||||||
var presetExecPatterns = map[string]string{
|
|
||||||
"scout": ``,
|
|
||||||
"analyst": `^(go\s+(test|vet)|git\s+(log|diff|status)|curl|wget|grep|find)\b`,
|
|
||||||
"coder": `^(` +
|
|
||||||
`go\s+(test|vet|fmt)|gofmt|goimports|golangci-lint|` +
|
|
||||||
`prettier|eslint|` +
|
|
||||||
`black|ruff|` +
|
|
||||||
`cargo\s+(test|fmt|clippy)|` +
|
|
||||||
`pnpm\s+(test|run\s+(test|lint|format))|` +
|
|
||||||
`bun\s+(test|run\s+(test|lint|format))|` +
|
|
||||||
`uv\s+run\s+` +
|
|
||||||
`)\b`,
|
|
||||||
"worker": `^(` +
|
|
||||||
`go\s+|` +
|
|
||||||
`pnpm\s+(install|add|run|test|build)|` +
|
|
||||||
`bun\s+(install|add|run|test|build)|` +
|
|
||||||
`uv\s+(run|sync|add|pip\s+install)|` +
|
|
||||||
`pip\s+install|` +
|
|
||||||
`cargo\s+` +
|
|
||||||
`)\b`,
|
|
||||||
"coordinator": `^(go\s+|pnpm\s+|bun\s+|curl|wget)\b`,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## タスク一覧
|
## Phase 1: Container + Escalation
|
||||||
|
|
||||||
### 1. SubagentContainer 実装
|
### Task 1: ContainerMessage channel 追加
|
||||||
|
|
||||||
goroutine + channel でサブエージェントのライフサイクルを表現。
|
**目的:** subagent goroutine と conductor 間の双方向通信を既存の SubagentManager に追加。
|
||||||
|
新規ファイルは作らず SubagentTask を拡張する。
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/tools/subagent.go`
|
||||||
|
|
||||||
|
**変更内容:**
|
||||||
|
|
||||||
|
1. SubagentTask にチャネルフィールド追加:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
// 既存の SubagentTask に追加
|
||||||
|
type SubagentTask struct {
|
||||||
|
// ... existing fields ...
|
||||||
|
|
||||||
|
// Container channels — nil for Exploratory presets
|
||||||
|
inCh chan string // conductor → subagent (回答)
|
||||||
|
outCh chan ContainerMessage // subagent → conductor (質問・結果)
|
||||||
|
}
|
||||||
|
|
||||||
type ContainerMessage struct {
|
type ContainerMessage struct {
|
||||||
Type string // "question" | "result" | "status"
|
Type string // "question" | "status"
|
||||||
Content string
|
Content string
|
||||||
}
|
TaskID string // 送信元タスクID
|
||||||
|
|
||||||
type SubagentContainer struct {
|
|
||||||
inCh chan string // conductor → subagent (回答)
|
|
||||||
outCh chan ContainerMessage // subagent → conductor (質問・結果・進捗)
|
|
||||||
cancel context.CancelFunc
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- spawn (async): outCh を返して即リターン
|
2. `Spawn()` で Deliberate preset の場合のみチャネル生成:
|
||||||
- subagent (sync): その場で result を待つ
|
|
||||||
- goroutine 終了時に `defer orchestrator.active.Delete(id)` + `defer close(outCh)` で自動 GC
|
|
||||||
|
|
||||||
**対象ファイル:** `pkg/tools/container.go` (新規)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Orchestrator 実装
|
|
||||||
|
|
||||||
SubagentManager を置き換える上位構造。
|
|
||||||
|
|
||||||
- Container の生成・管理
|
|
||||||
- escalation chain の実装 (subagent → conductor → human)
|
|
||||||
- spawn/subagent の使い分けロジック
|
|
||||||
|
|
||||||
**対象ファイル:** `pkg/tools/orchestrator.go` (新規)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. SubagentEnvironment (Context Injection)
|
|
||||||
|
|
||||||
conductor が subagent に渡すコンテキストの構造化。
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type SubagentEnvironment struct {
|
func (sm *SubagentManager) Spawn(...) (string, error) {
|
||||||
Workspace string
|
// ... existing code ...
|
||||||
WorktreeDir string
|
if isDeliberatePreset(preset) {
|
||||||
PlanTask string // MEMORY.md > Task: から自動抽出
|
task.inCh = make(chan string, 1)
|
||||||
PlanContext string // MEMORY.md ## Context から自動抽出
|
task.outCh = make(chan ContainerMessage, 4)
|
||||||
Commands string // MEMORY.md ## Commands から自動抽出
|
}
|
||||||
CurrentPhase string
|
// ...
|
||||||
Background string // conductor が明示的に追加
|
|
||||||
Constraints string
|
|
||||||
ContextFiles []string
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`inject_plan_context: true` で MEMORY.md からの自動注入を有効化。
|
3. `PendingQuestions()` メソッド追加 — conductor が polling で question を回収:
|
||||||
|
|
||||||
**対象ファイル:** `pkg/tools/container.go` または `pkg/tools/environment.go` (新規)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. SandboxConfig enforcement
|
|
||||||
|
|
||||||
`ToolRegistry.Execute()` の入口で一括 enforcement。subagent は透過的に sandboxed になる。
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type SandboxConfig struct {
|
// PendingQuestions returns all pending questions from active subagents.
|
||||||
Preset string
|
// Non-blocking: returns nil if no questions.
|
||||||
WriteRoot string
|
func (sm *SubagentManager) PendingQuestions() []ContainerMessage {
|
||||||
AllowedTools map[string]bool
|
sm.mu.RLock()
|
||||||
ExecPolicy *ExecPolicy
|
defer sm.mu.RUnlock()
|
||||||
SpawnablePresets []string
|
var questions []ContainerMessage
|
||||||
|
for _, t := range sm.tasks {
|
||||||
|
if t.outCh == nil { continue }
|
||||||
|
select {
|
||||||
|
case msg := <-t.outCh:
|
||||||
|
questions = append(questions, msg)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return questions
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
workDir = worktreeDir として設定し、AI が隔離に気づかず振る舞う透過的隔離を実現。
|
4. `AnswerQuestion()` メソッド追加:
|
||||||
|
|
||||||
**対象ファイル:** `pkg/tools/sandbox.go` (既存拡張), `pkg/tools/registry.go`
|
```go
|
||||||
|
func (sm *SubagentManager) AnswerQuestion(taskID, answer string) error {
|
||||||
|
sm.mu.RLock()
|
||||||
|
t, ok := sm.tasks[taskID]
|
||||||
|
sm.mu.RUnlock()
|
||||||
|
if !ok { return fmt.Errorf("task %s not found", taskID) }
|
||||||
|
if t.inCh == nil { return fmt.Errorf("task %s has no question channel", taskID) }
|
||||||
|
select {
|
||||||
|
case t.inCh <- answer:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("task %s answer channel full", taskID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**テスト:** `pkg/tools/subagent_container_test.go`
|
||||||
|
- Deliberate preset → outCh/inCh 生成確認
|
||||||
|
- Exploratory preset → outCh/inCh = nil 確認
|
||||||
|
- PendingQuestions の non-blocking 動作
|
||||||
|
- AnswerQuestion の正常系/異常系
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5. Preset enforcement
|
### Task 2: AskConductorTool
|
||||||
|
|
||||||
preset 定義と exec allowlist regex を SandboxConfig 経由で enforce。
|
**目的:** subagent goroutine 内で conductor に質問を送り、回答をブロック待ちする tool。
|
||||||
`pkg/tools/sandbox.go` に既存の定義があるが、ToolRegistry.Execute() での一括 enforcement がまだ。
|
|
||||||
|
**対象ファイル:** `pkg/tools/ask_conductor.go` (新規)
|
||||||
|
|
||||||
|
**実装:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
type AskConductorTool struct {
|
||||||
|
taskID string
|
||||||
|
outCh chan<- ContainerMessage
|
||||||
|
inCh <-chan string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAskConductorTool(taskID string, outCh chan<- ContainerMessage, inCh <-chan string) *AskConductorTool
|
||||||
|
|
||||||
|
func (t *AskConductorTool) Name() string { return "ask_conductor" }
|
||||||
|
|
||||||
|
func (t *AskConductorTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
|
question := args["question"].(string)
|
||||||
|
|
||||||
|
// 1. Send question to conductor
|
||||||
|
select {
|
||||||
|
case t.outCh <- ContainerMessage{Type: "question", Content: question, TaskID: t.taskID}:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ErrorResult("canceled while sending question")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Block waiting for answer (with context cancellation)
|
||||||
|
select {
|
||||||
|
case answer := <-t.inCh:
|
||||||
|
return SuccessResult(answer)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ErrorResult("canceled while waiting for answer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"question": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A clarifying question for the conductor about the task requirements, constraints, or approach."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["question"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Registry 登録:** `buildPresetRegistry()` で Deliberate preset のみ登録:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// In buildPresetRegistry():
|
||||||
|
if task.inCh != nil && task.outCh != nil {
|
||||||
|
reg.Register(NewAskConductorTool(task.ID, task.outCh, task.inCh))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**テスト:** `pkg/tools/ask_conductor_test.go`
|
||||||
|
- question 送信 → answer 受信の正常フロー
|
||||||
|
- context キャンセルでの中断
|
||||||
|
- channel 閉鎖時の graceful error
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 6. Subagent Plan Mode (in-memory)
|
### Task 3: Conductor 側の question 処理
|
||||||
|
|
||||||
Deliberate preset 用のミニ plan mode。MEMORY.md には触れない。
|
**目的:** conductor の main loop が subagent question を検出し、LLM に判断させる。
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/agent/loop.go`
|
||||||
|
|
||||||
|
**方式:** processRequest のイテレーションごとに pending questions をチェック。
|
||||||
|
questions があれば system message として LLM に注入。
|
||||||
|
|
||||||
|
**変更内容:**
|
||||||
|
|
||||||
|
1. `runAgentLoop()` のイテレーション先頭に question チェック追加:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// In runAgentLoop(), after getting LLM response:
|
||||||
|
if agent.SubagentMgr != nil {
|
||||||
|
questions := agent.SubagentMgr.PendingQuestions()
|
||||||
|
for _, q := range questions {
|
||||||
|
// LLM の次の応答で answer_subagent tool call を期待
|
||||||
|
messages = append(messages, providers.Message{
|
||||||
|
Role: "user",
|
||||||
|
Content: fmt.Sprintf("[Subagent %s asks]: %s\n\nRespond using the answer_subagent tool.",
|
||||||
|
q.TaskID, q.Content),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. `AnswerSubagentTool` 追加 (`pkg/tools/answer_subagent.go`、新規):
|
||||||
|
|
||||||
|
```go
|
||||||
|
type AnswerSubagentTool struct {
|
||||||
|
manager *SubagentManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *AnswerSubagentTool) Name() string { return "answer_subagent" }
|
||||||
|
|
||||||
|
func (t *AnswerSubagentTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
|
taskID := args["task_id"].(string)
|
||||||
|
answer := args["answer"].(string)
|
||||||
|
if err := t.manager.AnswerQuestion(taskID, answer); err != nil {
|
||||||
|
return ErrorResult(err.Error())
|
||||||
|
}
|
||||||
|
return SuccessResult("Answer delivered to " + taskID)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"task_id": {"type": "string", "description": "The subagent task ID that asked the question"},
|
||||||
|
"answer": {"type": "string", "description": "Your answer to the subagent's question"}
|
||||||
|
},
|
||||||
|
"required": ["task_id", "answer"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. conductor の ToolRegistry に `answer_subagent` 登録:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// In registerSharedTools(), after creating SubagentManager:
|
||||||
|
agent.Tools.Register(tools.NewAnswerSubagentTool(subagentManager))
|
||||||
|
```
|
||||||
|
|
||||||
|
**escalation (conductor → human):**
|
||||||
|
conductor LLM が answer_subagent で答えられないと判断した場合:
|
||||||
|
- message tool で human に質問を転送
|
||||||
|
- human 回答後に conductor が answer_subagent を呼ぶ
|
||||||
|
- **追加実装不要** — 既存の message tool + LLM 判断で自然に発生
|
||||||
|
|
||||||
|
**テスト:** `pkg/agent/loop_escalation_test.go`
|
||||||
|
- question 注入 → LLM が answer_subagent を呼ぶフロー(mock LLM)
|
||||||
|
- 複数 subagent から同時に question が来るケース
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Deliberate Plan Mode
|
||||||
|
|
||||||
|
### Task 4: SubagentPlanState
|
||||||
|
|
||||||
|
**目的:** Deliberate preset (coder/worker/coordinator) の subagent に clarifying→review→executing の状態遷移を追加。
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/tools/subagent.go` (SubagentTask に追加)
|
||||||
|
|
||||||
|
**変更内容:**
|
||||||
|
|
||||||
|
1. 状態定義:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type SubagentPlanState int
|
type SubagentPlanState int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PlanStateNone SubagentPlanState = iota // Exploratory preset
|
PlanStateNone SubagentPlanState = iota // Exploratory — 即実行
|
||||||
PlanStateClarifying // 目的・制約を確認中
|
PlanStateClarifying // 目的・制約を確認中
|
||||||
PlanStateReview // conductor の承認待ち
|
PlanStateReview // conductor の承認待ち
|
||||||
PlanStateExecuting // 実行中
|
PlanStateExecuting // 実行中
|
||||||
|
PlanStateCompleted // 完了
|
||||||
)
|
)
|
||||||
|
|
||||||
type SubagentPlan struct {
|
// SubagentTask に追加
|
||||||
State SubagentPlanState
|
type SubagentTask struct {
|
||||||
Goal string
|
// ... existing ...
|
||||||
Approach []string
|
PlanState SubagentPlanState
|
||||||
QA []QAItem
|
PlanGoal string // clarifying で確定した目標
|
||||||
mu sync.Mutex
|
PlanSteps []string // review で提出したステップ
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
SubagentContainer がフィールドとして保持。goroutine 終了とともに消える。
|
2. `runTask()` の Deliberate preset フロー変更:
|
||||||
|
|
||||||
**System prompt 使い分け:**
|
```go
|
||||||
- Deliberate: clarifying → review → executing の3段階
|
func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask, preset string, callback AsyncCallback) {
|
||||||
- Exploratory: 即座に探索開始、判断は自律的に
|
p := Preset(preset)
|
||||||
|
|
||||||
---
|
if isDeliberatePreset(p) {
|
||||||
|
// Phase A: Clarifying — ask_conductor で不明点を解消
|
||||||
|
task.PlanState = PlanStateClarifying
|
||||||
|
clarifyResult := sm.runClarifyingPhase(ctx, task, p)
|
||||||
|
if ctx.Err() != nil { return }
|
||||||
|
|
||||||
### 7. MEMORY.md Orchestration Section
|
// Phase B: Review — conductor に計画を提出、承認待ち
|
||||||
|
task.PlanState = PlanStateReview
|
||||||
|
approved := sm.submitPlanForReview(ctx, task, clarifyResult)
|
||||||
|
if !approved || ctx.Err() != nil { return }
|
||||||
|
|
||||||
conductor の guidance に Orchestration セクションの使い方を追記。
|
// Phase C: Executing — 承認済み計画を実行
|
||||||
|
task.PlanState = PlanStateExecuting
|
||||||
```markdown
|
sm.runExecutingPhase(ctx, task, p, callback)
|
||||||
## Orchestration
|
} else {
|
||||||
|
// Exploratory — 既存フロー(即実行)
|
||||||
### Delegated
|
sm.runExploratoryTask(ctx, task, p, callback)
|
||||||
- coder-1 (coder): rate limiter 実装 → Phase 2 Step 1
|
}
|
||||||
|
}
|
||||||
### Findings
|
|
||||||
- pkg/auth は middleware パターン (scout-1)
|
|
||||||
|
|
||||||
### Decisions
|
|
||||||
- auth: JWT を選択 (外部依存なし、scout-2 推奨)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
conductor は spawn 後に Delegated に記録、結果受信後に Findings に追記、方向選択時に Decisions に記録。
|
3. 各フェーズの system prompt テンプレート:
|
||||||
|
|
||||||
**対象ファイル:** `pkg/agent/context.go` (guidance 追記)
|
```go
|
||||||
|
func clarifyingSystemPrompt(task, preset string) string {
|
||||||
|
return fmt.Sprintf(`You are a %s subagent. Your task:
|
||||||
|
%s
|
||||||
|
|
||||||
|
PHASE: CLARIFYING
|
||||||
|
You must understand the task fully before executing.
|
||||||
|
- If anything is unclear, use ask_conductor to ask questions.
|
||||||
|
- When you are confident you understand the task, call submit_plan with your goal and approach.
|
||||||
|
- Do NOT write code or make changes yet.`, preset, task)
|
||||||
|
}
|
||||||
|
|
||||||
|
func executingSystemPrompt(task, preset, plan string) string {
|
||||||
|
return fmt.Sprintf(`You are a %s subagent. Your task:
|
||||||
|
%s
|
||||||
|
|
||||||
|
APPROVED PLAN:
|
||||||
|
%s
|
||||||
|
|
||||||
|
PHASE: EXECUTING
|
||||||
|
Execute the approved plan step by step. You may use your tools to implement.
|
||||||
|
Do not deviate from the plan without asking via ask_conductor.`, preset, task, plan)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**テスト:** `pkg/tools/subagent_plan_test.go`
|
||||||
|
- Deliberate preset → clarifying→review→executing 遷移
|
||||||
|
- Exploratory preset → PlanStateNone のまま即実行
|
||||||
|
- clarifying 中に context cancel → graceful exit
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Task 5: SubmitPlanTool + review フロー
|
||||||
|
|
||||||
|
**目的:** subagent が clarifying 完了後に計画を conductor に提出する tool。
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/tools/submit_plan.go` (新規)
|
||||||
|
|
||||||
|
**実装:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
type SubmitPlanTool struct {
|
||||||
|
taskID string
|
||||||
|
outCh chan<- ContainerMessage
|
||||||
|
inCh <-chan string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SubmitPlanTool) Name() string { return "submit_plan" }
|
||||||
|
|
||||||
|
func (t *SubmitPlanTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
|
goal := args["goal"].(string)
|
||||||
|
steps := args["steps"] // []any → []string
|
||||||
|
|
||||||
|
planText := fmt.Sprintf("Goal: %s\nSteps:\n", goal)
|
||||||
|
for i, s := range steps.([]any) {
|
||||||
|
planText += fmt.Sprintf(" %d. %s\n", i+1, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send plan to conductor for review
|
||||||
|
select {
|
||||||
|
case t.outCh <- ContainerMessage{Type: "plan_review", Content: planText, TaskID: t.taskID}:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ErrorResult("canceled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for approval/rejection
|
||||||
|
select {
|
||||||
|
case response := <-t.inCh:
|
||||||
|
if response == "approved" || strings.HasPrefix(response, "approved") {
|
||||||
|
return SuccessResult("Plan approved. Proceed with execution.")
|
||||||
|
}
|
||||||
|
return SuccessResult("Plan rejected: " + response + "\nRevise your plan and resubmit.")
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ErrorResult("canceled while waiting for review")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"goal": {"type": "string", "description": "One-sentence goal of the task"},
|
||||||
|
"steps": {"type": "array", "items": {"type": "string"}, "description": "Ordered implementation steps"}
|
||||||
|
},
|
||||||
|
"required": ["goal", "steps"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Conductor 側:** `review_subagent_plan` tool 追加 (`pkg/tools/answer_subagent.go` に同居):
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ReviewSubagentPlanTool struct {
|
||||||
|
manager *SubagentManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ReviewSubagentPlanTool) Name() string { return "review_subagent_plan" }
|
||||||
|
|
||||||
|
func (t *ReviewSubagentPlanTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
|
taskID := args["task_id"].(string)
|
||||||
|
decision := args["decision"].(string) // "approved" or "rejected: reason"
|
||||||
|
if err := t.manager.AnswerQuestion(taskID, decision); err != nil {
|
||||||
|
return ErrorResult(err.Error())
|
||||||
|
}
|
||||||
|
return SuccessResult("Review delivered to " + taskID)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Question injection:** conductor の LLM には plan_review も question と同じ経路で注入:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// PendingQuestions already returns plan_review messages
|
||||||
|
// Conductor LLM sees:
|
||||||
|
// "[Subagent coder-1 submits plan for review]:
|
||||||
|
// Goal: ...
|
||||||
|
// Steps: ...
|
||||||
|
// Use review_subagent_plan to approve or reject."
|
||||||
|
```
|
||||||
|
|
||||||
|
**テスト:** `pkg/tools/submit_plan_test.go`
|
||||||
|
- submit → approved フロー
|
||||||
|
- submit → rejected → 再 submit → approved フロー
|
||||||
|
- context cancel 中断
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Context Injection + Guidance
|
||||||
|
|
||||||
|
### Task 6: SubagentEnvironment injection
|
||||||
|
|
||||||
|
**目的:** `SubagentEnvironment` (既存型) に MEMORY.md からの自動コンテキスト注入を実装。
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/tools/subagent.go` (runTask 内)
|
||||||
|
|
||||||
|
**変更内容:**
|
||||||
|
|
||||||
|
1. `extractPlanContext()` ヘルパー追加:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// extractPlanContext reads MEMORY.md and extracts Task/Context/Commands sections.
|
||||||
|
func extractPlanContext(workspace string) (task, context, commands string) {
|
||||||
|
data, err := os.ReadFile(filepath.Join(workspace, "MEMORY.md"))
|
||||||
|
if err != nil { return }
|
||||||
|
// Parse markdown sections:
|
||||||
|
// - "Task:" or "# Task" line → task
|
||||||
|
// - "## Context" section → context
|
||||||
|
// - "## Commands" section → commands
|
||||||
|
// Simple line-based parser, no external deps
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. `runTask()` で Environment を system prompt に注入:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (sm *SubagentManager) runTask(...) {
|
||||||
|
// ... existing ...
|
||||||
|
env := SubagentEnvironment{
|
||||||
|
Workspace: sm.workspace,
|
||||||
|
WorktreeDir: sm.workspace, // TODO: worktree 有効時は worktree path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-inject from MEMORY.md if available
|
||||||
|
env.PlanTask, env.PlanContext, env.Commands = extractPlanContext(sm.workspace)
|
||||||
|
|
||||||
|
// Build system prompt with environment
|
||||||
|
systemPrompt := buildSubagentSystemPrompt(preset, task.Task, env)
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. `buildSubagentSystemPrompt()` に environment セクション追加:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func buildSubagentSystemPrompt(preset, task string, env SubagentEnvironment) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
// ... existing preset-based prompt ...
|
||||||
|
|
||||||
|
if env.PlanTask != "" || env.PlanContext != "" {
|
||||||
|
sb.WriteString("\n## Project Context\n")
|
||||||
|
if env.PlanTask != "" {
|
||||||
|
sb.WriteString("Current task: " + env.PlanTask + "\n")
|
||||||
|
}
|
||||||
|
if env.PlanContext != "" {
|
||||||
|
sb.WriteString(env.PlanContext + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if env.Commands != "" {
|
||||||
|
sb.WriteString("\n## Build Commands\n" + env.Commands + "\n")
|
||||||
|
}
|
||||||
|
if env.Constraints != "" {
|
||||||
|
sb.WriteString("\n## Constraints\n" + env.Constraints + "\n")
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**テスト:** `pkg/tools/subagent_env_test.go`
|
||||||
|
- MEMORY.md ありの場合にセクション抽出確認
|
||||||
|
- MEMORY.md なし → 空文字列、エラーなし
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: MEMORY.md Orchestration section guidance
|
||||||
|
|
||||||
|
**目的:** conductor が spawn/結果受信/方向決定時に MEMORY.md の Orchestration セクションを更新するよう guidance を追加。
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/agent/context.go`
|
||||||
|
|
||||||
|
**変更内容:**
|
||||||
|
|
||||||
|
既存の `buildOrchGuidance()` に Orchestration セクションの使い方を追記:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func buildOrchGuidance() string {
|
||||||
|
return `## Orchestration Memory
|
||||||
|
|
||||||
|
When orchestrating subagents, maintain an "## Orchestration" section in MEMORY.md:
|
||||||
|
|
||||||
|
### Delegated
|
||||||
|
Track active delegations:
|
||||||
|
- <label> (<preset>): <task summary> → <plan phase/step>
|
||||||
|
|
||||||
|
### Findings
|
||||||
|
Record discoveries from subagent reports:
|
||||||
|
- <finding> (<source subagent>)
|
||||||
|
|
||||||
|
### Decisions
|
||||||
|
Record architectural/design decisions:
|
||||||
|
- <decision> (<rationale>, <recommending subagent>)
|
||||||
|
|
||||||
|
Update this section after each spawn and after receiving each subagent report.`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**テスト:** 既存の context_test.go で guidance 文字列に "Orchestration" が含まれることを確認。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ファイル一覧
|
||||||
|
|
||||||
|
| ファイル | Action | Phase |
|
||||||
|
|---|---|---|
|
||||||
|
| `pkg/tools/subagent.go` | 拡張: ContainerMessage, inCh/outCh, PendingQuestions, AnswerQuestion, PlanState | 1, 2 |
|
||||||
|
| `pkg/tools/ask_conductor.go` | 新規: AskConductorTool | 1 |
|
||||||
|
| `pkg/tools/answer_subagent.go` | 新規: AnswerSubagentTool, ReviewSubagentPlanTool | 1, 2 |
|
||||||
|
| `pkg/tools/submit_plan.go` | 新規: SubmitPlanTool | 2 |
|
||||||
|
| `pkg/agent/loop.go` | 拡張: question polling + 注入, answer/review tool 登録 | 1, 2 |
|
||||||
|
| `pkg/agent/context.go` | 拡張: Orchestration guidance | 3 |
|
||||||
|
| `pkg/tools/subagent_container_test.go` | 新規 | 1 |
|
||||||
|
| `pkg/tools/ask_conductor_test.go` | 新規 | 1 |
|
||||||
|
| `pkg/tools/subagent_plan_test.go` | 新規 | 2 |
|
||||||
|
| `pkg/tools/submit_plan_test.go` | 新規 | 2 |
|
||||||
|
| `pkg/tools/subagent_env_test.go` | 新規 | 3 |
|
||||||
|
|
||||||
|
## Phase 間の依存
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1: Container + Escalation (Task 1-3)
|
||||||
|
↓ (inCh/outCh が前提)
|
||||||
|
Phase 2: Deliberate Plan Mode (Task 4-5)
|
||||||
|
↓ (subagent の system prompt 拡張が前提)
|
||||||
|
Phase 3: Context Injection + Guidance (Task 6-7) ← 独立して先行も可
|
||||||
|
```
|
||||||
|
|
||||||
## 完了基準
|
## 完了基準
|
||||||
|
|
||||||
- `go test ./...` 全パス
|
- `go test ./...` 全パス
|
||||||
- scout/analyst/coder preset で spawn → 結果受信 → conductor 応答の E2E フロー動作
|
- Exploratory preset (scout/analyst): 既存フロー変更なし、即実行
|
||||||
- Deliberate preset の clarifying → review → executing フロー動作
|
- Deliberate preset (coder/worker): clarifying → review → executing の 3 段階動作
|
||||||
- SandboxConfig による exec 制限が全 preset で正しく enforcement
|
- `ask_conductor` → conductor LLM 回答 → subagent 再開 のラウンドトリップ
|
||||||
- escalation chain (subagent → conductor → human) が動作
|
- conductor が回答不可 → message tool で human escalate → 回答転送
|
||||||
|
- MEMORY.md Orchestration セクションが conductor guidance に含まれる
|
||||||
|
- 既存の spawn/subagent E2E フロー(Exploratory)に regression なし
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue