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
6210eaae4c
commit
6904cada56
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)
|
||||
|
||||
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
|
||||
│ ContainerRequest (task, preset, environment)
|
||||
▼
|
||||
Container goroutine: provision → run → finalize
|
||||
│ ContainerMessage (question / result / status)
|
||||
▼
|
||||
Conductor goroutine
|
||||
│ answer (question への回答)
|
||||
▼ (わからなければ human に escalate)
|
||||
Container goroutine (再開)
|
||||
Conductor (main loop)
|
||||
│ SpawnTool.Execute({"task":..., "preset":"scout"})
|
||||
│
|
||||
├─ manager.Spawn() → goroutine 起動 → AsyncResult 即返却
|
||||
│
|
||||
│ [goroutine]
|
||||
│ ├─ buildPresetRegistry(preset) ← 隔離された ToolRegistry
|
||||
│ ├─ RunToolLoop(messages, tools) ← LLM + tool 実行ループ
|
||||
│ ├─ recorder.RecordCompletion()
|
||||
│ ├─ 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
|
||||
|
||||
```
|
||||
subagent (clarifying) → question → conductor
|
||||
→ conductor が答えられる: inCh に回答
|
||||
→ conductor もわからない: message tool で human に投げ、回答を転送
|
||||
subagent (clarifying) → outCh: question → conductor 受信
|
||||
→ conductor が答えられる → inCh: answer → subagent 再開
|
||||
→ conductor もわからない → message tool で human に投げ → 回答を転送 → inCh
|
||||
```
|
||||
|
||||
### Presets (5種)
|
||||
### Presets (5種、定義済み)
|
||||
|
||||
| preset | 性格 | write | exec | search | spawn |
|
||||
|---|---|---|---|---|---|
|
||||
|
|
@ -38,190 +93,581 @@ subagent (clarifying) → question → conductor
|
|||
| `worker` | Deliberate | o sandbox | build/package manager 系 | o | x |
|
||||
| `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
|
||||
// 既存の 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 string // "question" | "result" | "status"
|
||||
Type string // "question" | "status"
|
||||
Content string
|
||||
}
|
||||
|
||||
type SubagentContainer struct {
|
||||
inCh chan string // conductor → subagent (回答)
|
||||
outCh chan ContainerMessage // subagent → conductor (質問・結果・進捗)
|
||||
cancel context.CancelFunc
|
||||
TaskID string // 送信元タスクID
|
||||
}
|
||||
```
|
||||
|
||||
- spawn (async): outCh を返して即リターン
|
||||
- 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 に渡すコンテキストの構造化。
|
||||
2. `Spawn()` で Deliberate preset の場合のみチャネル生成:
|
||||
|
||||
```go
|
||||
type SubagentEnvironment struct {
|
||||
Workspace string
|
||||
WorktreeDir string
|
||||
PlanTask string // MEMORY.md > Task: から自動抽出
|
||||
PlanContext string // MEMORY.md ## Context から自動抽出
|
||||
Commands string // MEMORY.md ## Commands から自動抽出
|
||||
CurrentPhase string
|
||||
Background string // conductor が明示的に追加
|
||||
Constraints string
|
||||
ContextFiles []string
|
||||
func (sm *SubagentManager) Spawn(...) (string, error) {
|
||||
// ... existing code ...
|
||||
if isDeliberatePreset(preset) {
|
||||
task.inCh = make(chan string, 1)
|
||||
task.outCh = make(chan ContainerMessage, 4)
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
`inject_plan_context: true` で MEMORY.md からの自動注入を有効化。
|
||||
|
||||
**対象ファイル:** `pkg/tools/container.go` または `pkg/tools/environment.go` (新規)
|
||||
|
||||
---
|
||||
|
||||
### 4. SandboxConfig enforcement
|
||||
|
||||
`ToolRegistry.Execute()` の入口で一括 enforcement。subagent は透過的に sandboxed になる。
|
||||
3. `PendingQuestions()` メソッド追加 — conductor が polling で question を回収:
|
||||
|
||||
```go
|
||||
type SandboxConfig struct {
|
||||
Preset string
|
||||
WriteRoot string
|
||||
AllowedTools map[string]bool
|
||||
ExecPolicy *ExecPolicy
|
||||
SpawnablePresets []string
|
||||
// PendingQuestions returns all pending questions from active subagents.
|
||||
// Non-blocking: returns nil if no questions.
|
||||
func (sm *SubagentManager) PendingQuestions() []ContainerMessage {
|
||||
sm.mu.RLock()
|
||||
defer sm.mu.RUnlock()
|
||||
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。
|
||||
`pkg/tools/sandbox.go` に既存の定義があるが、ToolRegistry.Execute() での一括 enforcement がまだ。
|
||||
**目的:** subagent goroutine 内で conductor に質問を送り、回答をブロック待ちする tool。
|
||||
|
||||
**対象ファイル:** `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
|
||||
type SubagentPlanState int
|
||||
|
||||
const (
|
||||
PlanStateNone SubagentPlanState = iota // Exploratory preset
|
||||
PlanStateNone SubagentPlanState = iota // Exploratory — 即実行
|
||||
PlanStateClarifying // 目的・制約を確認中
|
||||
PlanStateReview // conductor の承認待ち
|
||||
PlanStateExecuting // 実行中
|
||||
PlanStateCompleted // 完了
|
||||
)
|
||||
|
||||
type SubagentPlan struct {
|
||||
State SubagentPlanState
|
||||
Goal string
|
||||
Approach []string
|
||||
QA []QAItem
|
||||
mu sync.Mutex
|
||||
// SubagentTask に追加
|
||||
type SubagentTask struct {
|
||||
// ... existing ...
|
||||
PlanState SubagentPlanState
|
||||
PlanGoal string // clarifying で確定した目標
|
||||
PlanSteps []string // review で提出したステップ
|
||||
}
|
||||
```
|
||||
|
||||
SubagentContainer がフィールドとして保持。goroutine 終了とともに消える。
|
||||
2. `runTask()` の Deliberate preset フロー変更:
|
||||
|
||||
**System prompt 使い分け:**
|
||||
- Deliberate: clarifying → review → executing の3段階
|
||||
- Exploratory: 即座に探索開始、判断は自律的に
|
||||
```go
|
||||
func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask, preset string, callback AsyncCallback) {
|
||||
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 セクションの使い方を追記。
|
||||
|
||||
```markdown
|
||||
## Orchestration
|
||||
|
||||
### Delegated
|
||||
- coder-1 (coder): rate limiter 実装 → Phase 2 Step 1
|
||||
|
||||
### Findings
|
||||
- pkg/auth は middleware パターン (scout-1)
|
||||
|
||||
### Decisions
|
||||
- auth: JWT を選択 (外部依存なし、scout-2 推奨)
|
||||
// Phase C: Executing — 承認済み計画を実行
|
||||
task.PlanState = PlanStateExecuting
|
||||
sm.runExecutingPhase(ctx, task, p, callback)
|
||||
} else {
|
||||
// Exploratory — 既存フロー(即実行)
|
||||
sm.runExploratoryTask(ctx, task, p, callback)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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 ./...` 全パス
|
||||
- scout/analyst/coder preset で spawn → 結果受信 → conductor 応答の E2E フロー動作
|
||||
- Deliberate preset の clarifying → review → executing フロー動作
|
||||
- SandboxConfig による exec 制限が全 preset で正しく enforcement
|
||||
- escalation chain (subagent → conductor → human) が動作
|
||||
- Exploratory preset (scout/analyst): 既存フロー変更なし、即実行
|
||||
- Deliberate preset (coder/worker): clarifying → review → executing の 3 段階動作
|
||||
- `ask_conductor` → conductor LLM 回答 → subagent 再開 のラウンドトリップ
|
||||
- conductor が回答不可 → message tool で human escalate → 回答転送
|
||||
- MEMORY.md Orchestration セクションが conductor guidance に含まれる
|
||||
- 既存の spawn/subagent E2E フロー(Exploratory)に regression なし
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue