docs: split CLAUDE.md tasks into parallel todo tracks
Extract all unimplemented design plans and tasks from CLAUDE.md into 5 independent todo files, each targeting a separate branch for parallel implementation: - TASKS-1: Memory & Performance Optimization - TASKS-2: Subagent Orchestration (Container Model) - TASKS-3: Session DAG (SQLite Store) - TASKS-4: Mini App & Static Serving - TASKS-5: Heartbeat Worktree Management CLAUDE.md now retains only implemented references, coding guidelines, and a summary table linking to each todo file. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5c046cecfc
commit
81a8abf1f2
6 changed files with 683 additions and 844 deletions
859
CLAUDE.md
859
CLAUDE.md
|
|
@ -19,471 +19,14 @@ Lint: `golangci-lint run`
|
||||||
- **Interview tool filtering**: `interviewAllowedTools` in `pkg/agent/loop.go` is the single source of truth for tools available during interview/review phases. Both `filterInterviewTools` (strips definitions before LLM call) and `isToolAllowedDuringInterview` (argument-level gating) reference this map.
|
- **Interview tool filtering**: `interviewAllowedTools` in `pkg/agent/loop.go` is the single source of truth for tools available during interview/review phases. Both `filterInterviewTools` (strips definitions before LLM call) and `isToolAllowedDuringInterview` (argument-level gating) reference this map.
|
||||||
- **History clear**: `/plan start clear` wipes session history and summary on transition to executing. The Mini App review UI offers two sliders: standard approve and approve-with-clear.
|
- **History clear**: `/plan start clear` wipes session history and summary on transition to executing. The Mini App review UI offers two sliders: standard approve and approve-with-clear.
|
||||||
|
|
||||||
## Mini App — Static File Serving
|
## Subagent Orchestration (実装済み部分)
|
||||||
|
|
||||||
現状は `pkg/miniapp/static/` 以下のファイルを `//go:embed` でバイナリに焼いて個別ルートで配信している。
|
- **Startup flag**: `--orchestration` で on/off。`SubagentsConfig.Enabled` で gate。
|
||||||
|
- **Conductor identity**: orchestration 有効時に conductor identity + spawn/subagent guidance を system prompt へ注入。
|
||||||
|
- **Sandbox/Spawn**: `pkg/tools/sandbox.go`, `pkg/tools/spawn.go` 実装済み。
|
||||||
|
- **AgentReporter**: `orch.AgentReporter` / `orch.Noop` / `orch.Broadcaster` で統一。main/heartbeat/subagent 全セッションが同一 Broadcaster に発火。Mini App は `agentLoop.GetOrchBroadcaster()` → `handler.SetOrchBroadcaster()` で受信。
|
||||||
|
|
||||||
```
|
## コードの匂い — チェックリスト
|
||||||
pkg/miniapp/static/
|
|
||||||
index.html ← すべての JS/CSS をインライン
|
|
||||||
map.js ← オーケストレーションルーム描画
|
|
||||||
```
|
|
||||||
|
|
||||||
Go 側: `//go:embed static/index.html static/map.js` + `/miniapp/map.js` ルート。
|
|
||||||
|
|
||||||
### バンドラ導入の検討事項
|
|
||||||
|
|
||||||
- ビルドステップ(Vite / esbuild 等)を挟む場合、成果物ディレクトリ(`static/dist/` 等)を embed する形になる
|
|
||||||
- Go 側は `//go:embed static` でディレクトリごと embed し、`http.FS` で一括配信すれば個別ルートが不要になる
|
|
||||||
- `serveMapJS` など個別ルートは汎用 static ファイルサーバーに統合できる
|
|
||||||
- `serveIndex` での `ORCH_ENABLED` 注入は、テンプレートエンジンまたはビルド時の環境変数注入に移行する必要がある
|
|
||||||
|
|
||||||
### 現時点でやっておける布石(任意)
|
|
||||||
|
|
||||||
- `/miniapp/` 以下を `http.FileServer(http.FS(staticFS))` で一括配信するよう `serveStatic` ハンドラを汎用化する
|
|
||||||
- これにより `map.js`・将来の `assets/*.js` もルート追加なしに自動配信される
|
|
||||||
|
|
||||||
## Known Gaps
|
|
||||||
|
|
||||||
- **Mini App log viewer has no frontend tests**: `renderLogs()` in `pkg/miniapp/static/index.html` is inline vanilla JS with no unit/E2E test coverage. Backend (Go) tests cover `RecentLogs`, `SanitizeFields`, and JSON serialization, but nothing verifies the JS rendering. This allowed the Fields display bug (fields sent but not rendered) to ship undetected.
|
|
||||||
- **No human intervention for heartbeat worktrees**: Heartbeat sessions create git worktrees (`.worktrees/heartbeat-YYYYMMDD/`) but there is no CLI or Mini App command to list, inspect, or manually dispose them. Need a `/plan worktrees` command (or similar) that shows active worktrees with branch/commit info and allows manual merge/dispose. `PruneOrphaned` on startup only removes directories without auto-committing first, so uncommitted changes in orphaned worktrees are silently lost.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Subagent Orchestration Design
|
|
||||||
|
|
||||||
> Designed 2026-02-25 on branch `sub-agent-technical-breakdown`.
|
|
||||||
|
|
||||||
### なぜオーケストレーションか
|
|
||||||
|
|
||||||
単純な指示から可能性の木を広げることが目的。conductor は一人でやり遂げるのではなく、探索・深化・fork をサブエージェントに委ねながら大局観を保つ。
|
|
||||||
|
|
||||||
```
|
|
||||||
without orchestration:
|
|
||||||
human → conductor → (全部自分でやる) → result
|
|
||||||
常にボトルネック、逐次処理
|
|
||||||
|
|
||||||
with orchestration:
|
|
||||||
human → conductor ─┬─ scout A ─┐
|
|
||||||
├─ scout B ─┼─ synthesize → deeper insight
|
|
||||||
└─ scout C ─┘
|
|
||||||
conductor は次を考えながら並走
|
|
||||||
```
|
|
||||||
|
|
||||||
**3つの核心原則:**
|
|
||||||
|
|
||||||
1. **Fork** — 同じ問いに複数の切り口で同時探索。sequential queue ではなく tree の展開。
|
|
||||||
2. **管理職の原則** — conductor は subagent の完了を待たない。spawn したら即座に次を設計する。人員を遊ばせないことがポイント。
|
|
||||||
3. **会話の fork** — main thread (conductor ↔ human) は高レベル・戦略的に保つ。subagent への細かい指示出しは branch thread で行い、main thread を汚染しない。subagent の進捗はサマリーだけ main thread に上げる。
|
|
||||||
|
|
||||||
**spawn がデフォルト、subagent は例外:**
|
|
||||||
```
|
|
||||||
spawn = conductor が次を考え続けられる (正しい姿)
|
|
||||||
subagent = conductor が止まる (結果が絶対必要な時だけ)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Architecture Overview
|
|
||||||
|
|
||||||
```
|
|
||||||
Conductor goroutine
|
|
||||||
│ ContainerRequest (task, preset, environment)
|
|
||||||
▼
|
|
||||||
Container goroutine: provision → run → finalize
|
|
||||||
│ ContainerMessage (question / result / status)
|
|
||||||
▼
|
|
||||||
Conductor goroutine
|
|
||||||
│ answer (question への回答)
|
|
||||||
▼ (わからなければ human に escalate)
|
|
||||||
Container goroutine (再開)
|
|
||||||
```
|
|
||||||
|
|
||||||
**escalation chain:**
|
|
||||||
```
|
|
||||||
subagent (clarifying) → question → conductor
|
|
||||||
→ conductor が答えられる: inCh に回答
|
|
||||||
→ conductor もわからない: message tool で human に投げ、回答を転送
|
|
||||||
```
|
|
||||||
|
|
||||||
### SubagentContainer
|
|
||||||
|
|
||||||
goroutine と channel でライフサイクルを表現。goroutine がブロックしている場所が現在の状態。
|
|
||||||
|
|
||||||
```go
|
|
||||||
type ContainerMessage struct {
|
|
||||||
Type string // "question" | "result" | "status"
|
|
||||||
Content string
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubagentContainer struct {
|
|
||||||
inCh chan string // conductor → subagent (回答)
|
|
||||||
outCh chan ContainerMessage // subagent → conductor (質問・結果・進捗)
|
|
||||||
cancel context.CancelFunc
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
spawn (async) は outCh を返して即リターン。subagent (sync) はその場で result を待つ。
|
|
||||||
|
|
||||||
**tasks map 問題の解消:** goroutine 終了時に `defer orchestrator.active.Delete(id)` + `defer close(outCh)` で自動 GC。
|
|
||||||
|
|
||||||
### SubagentEnvironment (Context Injection)
|
|
||||||
|
|
||||||
conductor は subagent に必要なコンテキストを明示的に渡す。MEMORY.md からの自動注入で冗長な手動記述を排除。
|
|
||||||
|
|
||||||
```go
|
|
||||||
type SubagentEnvironment struct {
|
|
||||||
// 自動注入 (harness が埋める)
|
|
||||||
Workspace string // workspace パス
|
|
||||||
WorktreeDir string // write先、workDir として透過的に機能
|
|
||||||
|
|
||||||
// MEMORY.md から自動抽出 (inject_plan_context: true の場合)
|
|
||||||
PlanTask string // > Task: の内容
|
|
||||||
PlanContext string // ## Context セクション
|
|
||||||
Commands string // ## Commands セクション (build/test/lint)
|
|
||||||
CurrentPhase string // 対象 Phase の内容
|
|
||||||
|
|
||||||
// conductor が明示的に追加
|
|
||||||
Background string // 追加の背景・意図
|
|
||||||
Constraints string // 制約
|
|
||||||
ContextFiles []string // 参照すべきファイルリスト
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
spawn パラメータ例:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"task": "Phase 2 Step 1: implement the rate limiter",
|
|
||||||
"preset": "coder",
|
|
||||||
"context_files": ["pkg/ratelimit/ratelimit.go"],
|
|
||||||
"inject_plan_context": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### SandboxConfig
|
|
||||||
|
|
||||||
ToolRegistry.Execute() の入口で一括 enforcement。subagent は普通に tool call するつもりで透過的に sandboxed になる。
|
|
||||||
|
|
||||||
```go
|
|
||||||
type SandboxConfig struct {
|
|
||||||
Preset string
|
|
||||||
WriteRoot string // write 系ツールのパス制限
|
|
||||||
AllowedTools map[string]bool
|
|
||||||
ExecPolicy *ExecPolicy // nil = exec 不可
|
|
||||||
SpawnablePresets []string // nil = spawn 不可
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExecPolicy struct {
|
|
||||||
AllowPattern string // 先頭一致 regex; マッチしたコマンドだけ実行可
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**透過的隔離:** workDir = worktreeDir として設定することで、AI は自分が隔離されていることに気づかずに振る舞う。picoclaw 側で CoW 的にファイルを引き渡せる。
|
|
||||||
|
|
||||||
### Presets (5種)
|
|
||||||
|
|
||||||
| preset | 性格 | write | exec | search | spawn |
|
|
||||||
|---|---|---|---|---|---|
|
|
||||||
| `scout` | Exploratory | ✗ | ✗ | ✓ | ✗ |
|
|
||||||
| `analyst` | Exploratory | ✗ | go test/vet, git log/diff, grep | ✓ | ✗ |
|
|
||||||
| `coder` | Deliberate | ✓ sandbox | test/lint/fmt 系 | ✓ | ✗ |
|
|
||||||
| `worker` | Deliberate | ✓ sandbox | build/package manager 系 | ✓ | ✗ |
|
|
||||||
| `coordinator` | Deliberate | ✓ sandbox | go/pnpm/bun/curl 系 | ✓ | scout〜worker のみ |
|
|
||||||
|
|
||||||
**性格の分類:**
|
|
||||||
- **Exploratory** (scout/analyst): open-ended、見てきて報告。clarifying フェーズなし。
|
|
||||||
- **Deliberate** (coder/worker/coordinator): 成果物を作る。目標があいまいだと失敗する。clarifying フェーズあり。
|
|
||||||
|
|
||||||
**境界:**
|
|
||||||
- `coder` = 書いて自分で検証できる (package 追加・deploy 不可)
|
|
||||||
- `worker` = インフラも含めてやりきる (package install, CI pipeline 等)
|
|
||||||
- `coordinator` = coordinator を spawn できない (深さ自然制限)
|
|
||||||
|
|
||||||
**npm は全 preset で禁止:** git worktree に node_modules が作られると大量ファイルが生じるため。pnpm は symbolic link で済む、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`,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Subagent Plan Mode
|
|
||||||
|
|
||||||
Deliberate な preset (coder/worker/coordinator) は in-memory のミニ plan mode を持つ。MEMORY.md には一切触れない (ファイル参照・編集を避けるため)。
|
|
||||||
|
|
||||||
```go
|
|
||||||
type SubagentPlanState int
|
|
||||||
|
|
||||||
const (
|
|
||||||
PlanStateNone SubagentPlanState = iota // Exploratory preset
|
|
||||||
PlanStateClarifying // 目的・制約を確認中
|
|
||||||
PlanStateReview // conductor の承認待ち
|
|
||||||
PlanStateExecuting // 実行中
|
|
||||||
)
|
|
||||||
|
|
||||||
type SubagentPlan struct {
|
|
||||||
State SubagentPlanState
|
|
||||||
Goal string // clarifying で合意した目的
|
|
||||||
Approach []string // proposed なステップ
|
|
||||||
QA []QAItem // 質問・回答の履歴
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
SubagentContainer がフィールドとして保持。goroutine 終了とともに消える。
|
|
||||||
|
|
||||||
**Deliberate preset の system prompt:**
|
|
||||||
```
|
|
||||||
You are in clarifying mode. Before executing, confirm with the conductor:
|
|
||||||
1. What is the exact goal?
|
|
||||||
2. What are the constraints and acceptance criteria?
|
|
||||||
3. Are there relevant files I should know about?
|
|
||||||
Use the `message` tool to ask questions.
|
|
||||||
When you have clear answers, propose your approach (steps) for review.
|
|
||||||
Do NOT start executing until the conductor approves.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Exploratory preset の system prompt:**
|
|
||||||
```
|
|
||||||
Explore and return findings. Use your best judgment when encountering ambiguity.
|
|
||||||
```
|
|
||||||
|
|
||||||
**fractal 構造:**
|
|
||||||
```
|
|
||||||
human
|
|
||||||
↕ plan mode (MEMORY.md, file-based, 永続)
|
|
||||||
conductor
|
|
||||||
↕ subagent plan mode (in-memory, 揮発)
|
|
||||||
subagent (deliberate)
|
|
||||||
```
|
|
||||||
|
|
||||||
### MEMORY.md Orchestration Section
|
|
||||||
|
|
||||||
executing 中に conductor が自由に書き込める専用エリア。システムはパースしない。
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## Orchestration
|
|
||||||
|
|
||||||
### Delegated
|
|
||||||
<!-- subagent に委譲したタスクの記録 (spawn 直後に書く) -->
|
|
||||||
- coder-1 (coder): rate limiter 実装 → Phase 2 Step 1
|
|
||||||
- scout-1 (scout): pkg/auth の構造調査
|
|
||||||
|
|
||||||
### Findings
|
|
||||||
<!-- subagent の結果から蓄積した知見 (結果受信後に書く) -->
|
|
||||||
- pkg/auth は middleware パターン、入口は middleware.go (scout-1)
|
|
||||||
- セッションストアは存在しない、JWT が有効 (scout-2)
|
|
||||||
|
|
||||||
### Decisions
|
|
||||||
<!-- fork の意思決定ログ (方向選択時に書く) -->
|
|
||||||
- auth: OAuth2 より JWT を選択 (外部依存なし、scout-2 推奨)
|
|
||||||
```
|
|
||||||
|
|
||||||
conductor の guidance に追記:
|
|
||||||
```
|
|
||||||
After spawning a subagent, record the assignment in ## Orchestration > Delegated.
|
|
||||||
When a subagent reports back, move key findings to ## Orchestration > Findings.
|
|
||||||
When you choose one direction over another, log the rationale in ## Orchestration > Decisions.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Conductor Identity (System Prompt)
|
|
||||||
|
|
||||||
`pkg/agent/context.go` の `getIdentity()` に追加予定:
|
|
||||||
|
|
||||||
```
|
|
||||||
You are picoclaw, a conductor AI agent. Your role is to orchestrate:
|
|
||||||
break work into tasks, delegate them to subagents, and synthesize results —
|
|
||||||
rather than doing everything inline yourself.
|
|
||||||
|
|
||||||
## Orchestration
|
|
||||||
|
|
||||||
You are the conductor, not the performer. Prefer delegation over doing everything inline.
|
|
||||||
|
|
||||||
Use `spawn` (non-blocking) when:
|
|
||||||
- Tasks can run in parallel or in the background
|
|
||||||
- Multiple independent tasks can run simultaneously (spawn each one)
|
|
||||||
- You don't need the result to decide the next step
|
|
||||||
- The operation is long-running (builds, fetches, analysis, file processing)
|
|
||||||
|
|
||||||
Use `subagent` (blocking) when:
|
|
||||||
- You need the result before you can continue
|
|
||||||
- Correctness of next steps depends on the outcome
|
|
||||||
|
|
||||||
Do inline only when:
|
|
||||||
- It's a single fast tool call (read a file, quick search)
|
|
||||||
- Delegation overhead clearly outweighs the benefit
|
|
||||||
|
|
||||||
Default bias: if a task involves more than 2-3 tool calls or can run
|
|
||||||
independently, delegate it. When you spawn, immediately plan what comes next —
|
|
||||||
blocking means you've stopped thinking.
|
|
||||||
|
|
||||||
Fork aggressively: explore multiple directions simultaneously.
|
|
||||||
After spawning a subagent, record the assignment in ## Orchestration > Delegated.
|
|
||||||
When results come back, synthesize and decide the next fork.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Startup Flag
|
|
||||||
|
|
||||||
テスト用途で起動時にオーケストレーション機能を on/off できるようにする。
|
|
||||||
|
|
||||||
**変更箇所:**
|
|
||||||
1. `pkg/config/config.go` — `SubagentsConfig` に `Enabled bool` を追加
|
|
||||||
2. `cmd/picoclaw/cmd_agent.go` — `--orchestration` フラグを追加 (default: false for now)
|
|
||||||
3. `pkg/agent/loop.go` — `registerSharedTools()` で spawn tool 登録を `Enabled` で gate
|
|
||||||
|
|
||||||
```go
|
|
||||||
// pkg/config/config.go
|
|
||||||
type SubagentsConfig struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
AllowAgents []string `json:"allow_agents,omitempty"`
|
|
||||||
Model *AgentModelConfig `json:"model,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// cmd/picoclaw/cmd_agent.go
|
|
||||||
case "--orchestration":
|
|
||||||
cfg.Agents.Defaults.Subagents.Enabled = true // or toggle
|
|
||||||
```
|
|
||||||
|
|
||||||
### Implementation Files (予定)
|
|
||||||
|
|
||||||
```
|
|
||||||
pkg/tools/
|
|
||||||
container.go — SubagentContainer, ContainerRequest, ContainerMessage,
|
|
||||||
SubagentEnvironment, SubagentPlan
|
|
||||||
sandbox.go — SandboxConfig, ExecPolicy, preset 定義
|
|
||||||
orchestrator.go — Orchestrator (SubagentManager を置き換え)
|
|
||||||
spawn.go — preset / inject_plan_context パラメータ追加
|
|
||||||
pkg/agent/
|
|
||||||
context.go — conductor identity + orchestration guidance 追加
|
|
||||||
```
|
|
||||||
|
|
||||||
### AgentReporter 抽象化 (実装済み 2026-02-25)
|
|
||||||
|
|
||||||
> branch `sub-agent-technical-breakdown`
|
|
||||||
|
|
||||||
`Broadcaster` を `SubagentManager` 内部で生成する密結合を解消し、
|
|
||||||
`orch.AgentReporter` インターフェースを中心に置くリファクタリングを実施。
|
|
||||||
|
|
||||||
#### オーナーシップ
|
|
||||||
|
|
||||||
```
|
|
||||||
AgentLoop
|
|
||||||
├─ owns: *orch.Broadcaster (orchBroadcaster — nil when disabled)
|
|
||||||
└─ holds: orch.AgentReporter (orchReporter = Broadcaster or Noop)
|
|
||||||
├─ passes to → SubagentManager.reporter
|
|
||||||
│ └─ passes to → ToolLoopConfig.Reporter
|
|
||||||
└─ calls directly for main/heartbeat sessions
|
|
||||||
├─ runAgentLoop: ReportSpawn / ReportGC
|
|
||||||
└─ runLLMIteration: ReportStateChange
|
|
||||||
|
|
||||||
cmd_gateway.go
|
|
||||||
└─ agentLoop.GetOrchBroadcaster() → handler.SetOrchBroadcaster()
|
|
||||||
|
|
||||||
miniapp.Handler
|
|
||||||
└─ borrows *orch.Broadcaster for Subscribe/Snapshot (WS 配信)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### インターフェース (`pkg/orch/reporter.go`)
|
|
||||||
|
|
||||||
```go
|
|
||||||
type AgentReporter interface {
|
|
||||||
ReportSpawn(id, label, task string)
|
|
||||||
ReportStateChange(id, state, tool string)
|
|
||||||
ReportConversation(from, to, text string)
|
|
||||||
ReportGC(id, reason string)
|
|
||||||
}
|
|
||||||
var Noop AgentReporter = &noopReporter{} // nil-free; 全メソッドが no-op
|
|
||||||
```
|
|
||||||
|
|
||||||
`Broadcaster` は `AgentReporter` を満たす (`ReportSpawn` 等が `Publish` のラッパー)。
|
|
||||||
|
|
||||||
#### Noop パターン
|
|
||||||
|
|
||||||
```
|
|
||||||
--orchestration なし: orchReporter = orch.Noop → 全 Report* が空振り (panic なし)
|
|
||||||
--orchestration あり: orchReporter = *Broadcaster → WS 配信
|
|
||||||
```
|
|
||||||
|
|
||||||
呼び出し側は `if reporter != nil` チェック不要。
|
|
||||||
|
|
||||||
#### イベント発火の責任分担
|
|
||||||
|
|
||||||
| 発火元 | イベント | 経由 |
|
|
||||||
|--------|---------|------|
|
|
||||||
| `runAgentLoop` | `ReportSpawn` / `ReportGC` | `al.reporter()` |
|
|
||||||
| `runLLMIteration` | `ReportStateChange("waiting"/"toolcall")` | `al.reporter()` |
|
|
||||||
| `SubagentManager.Spawn` | `ReportSpawn` | `sm.reporter` |
|
|
||||||
| `SubagentManager.runTask` | `ReportConversation` / `ReportGC` | `sm.reporter` |
|
|
||||||
| `RunToolLoop` | `ReportStateChange` | `config.Reporter` |
|
|
||||||
|
|
||||||
main / heartbeat / subagent の全セッションが同一 Broadcaster に発火するため、
|
|
||||||
canvas には全エージェントが統一して表示される。
|
|
||||||
|
|
||||||
#### 変更ファイル
|
|
||||||
|
|
||||||
- `pkg/orch/reporter.go` — **新規** インターフェース + Noop
|
|
||||||
- `pkg/orch/broadcaster.go` — `ReportSpawn/StateChange/Conversation/GC` 追加
|
|
||||||
- `pkg/tools/toolloop.go` — `OnStateChange func` → `Reporter AgentReporter + AgentID`
|
|
||||||
- `pkg/tools/subagent.go` — constructor に `reporter` 受け取り、内部 broadcaster 廃止、`GetBroadcaster()` 削除
|
|
||||||
- `pkg/agent/loop.go` — `orchBroadcaster`/`orchReporter` フィールド追加、`SetOrchReporter`/`GetOrchBroadcaster` 追加、`registerSharedTools` シグネチャに `al *AgentLoop` 追加
|
|
||||||
- `cmd/picoclaw/cmd_gateway.go` — `GetOrchBroadcaster()` → `handler.SetOrchBroadcaster()`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Memory Optimization Notes
|
|
||||||
|
|
||||||
> Reviewed 2026-02-24 on branch `memory-optimization-review`.
|
|
||||||
|
|
||||||
### 設計レベルの根本原因
|
|
||||||
|
|
||||||
#### D-1. MemoryStore が「ファイル = 正」でパース済み表現をキャッシュできない
|
|
||||||
|
|
||||||
`GetMemoryContext()` 1回で `ReadLongTerm()` が5回以上呼ばれる連鎖。MEMORY.md を外部エディタが直接編集できる設計上、インメモリキャッシュを自然に導入できない。
|
|
||||||
|
|
||||||
対策: (a) content パススルー方式 — 高レベルメソッドだけが1回 ReadLongTerm() を呼び、content を private ヘルパーに渡す。(b) `*ParsedPlan` 常駐 — RAM が潤沢なので MemoryStore にパース済み構造体を持たせる。edit_file 後に `InvalidateCache()` を呼ぶ。
|
|
||||||
|
|
||||||
#### D-2. `FunctionCall.Arguments` が JSON 文字列のままドメイン型に
|
|
||||||
|
|
||||||
ストリーミングループ内の重複 Unmarshal の根本原因。`ToolCall.Arguments map[string]any` のパース済みフィールドも存在するが中途半端に共存している。
|
|
||||||
|
|
||||||
#### D-3. `ToolFunctionDefinition.Parameters` が `map[string]any`
|
|
||||||
|
|
||||||
プロバイダーへ送るたびに Marshal が必要。`json.RawMessage` にすれば一度の marshal で済む。
|
|
||||||
|
|
||||||
#### D-4. 検索プロバイダーに共通フォーマット抽象がない
|
|
||||||
|
|
||||||
`[]string + strings.Join` パターンが3箇所に複製。`Search()` 戻り値を `string` でなく構造体にすれば1箇所で済む。
|
|
||||||
|
|
||||||
#### D-5. `Session.Messages` が可変スライスで全コピーが必要
|
|
||||||
|
|
||||||
`GetHistory()` / `Save()` での防衛的コピーは意図的設計。COW または append-only immutable 構造で解消できる。
|
|
||||||
|
|
||||||
#### D-6. `MemoryStore` のメソッド境界が「ファイル操作単位」
|
|
||||||
|
|
||||||
呼び出し側は複数の値が必要でも複数回呼ぶしかない。D-1 の解決策 (ParsedPlan 常駐) と合わせて解消。
|
|
||||||
|
|
||||||
### コードの匂い — チェックリスト
|
|
||||||
|
|
||||||
新しいコードを書くとき・レビューするときの確認事項:
|
新しいコードを書くとき・レビューするときの確認事項:
|
||||||
|
|
||||||
|
|
@ -495,388 +38,16 @@ canvas には全エージェントが統一して表示される。
|
||||||
6. **`var x []T` から始まる容量なし append** → ソース長が既知なら `make([]T, 0, n)`
|
6. **`var x []T` から始まる容量なし append** → ソース長が既知なら `make([]T, 0, n)`
|
||||||
7. **`[]rune(s)` 変換前に長さチェックなし** → `len(s) <= max` で ASCII fast path を先に
|
7. **`[]rune(s)` 変換前に長さチェックなし** → `len(s) <= max` で ASCII fast path を先に
|
||||||
|
|
||||||
### ストレージ保護設計 (microSD 寿命)
|
|
||||||
|
|
||||||
| データ | 現状 | 推奨戦略 | 削減率 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `sessions/*.json` | メッセージ毎書き込み | write-behind (dirty flag + 5分タイマー) | 80% |
|
|
||||||
| `state/stats.json` | LLM呼び出し毎 | 定期フラッシュのみ (5分) | 98% |
|
|
||||||
| `memory/MEMORY.md` | 即時 (変えない) | ターンスコープキャッシュ (読み取りのみ最適化) | — |
|
|
||||||
|
|
||||||
**実装ポイント:**
|
|
||||||
- `SessionManager` に `dirtyKeys map[string]bool` + バックグラウンドフラッシャー goroutine
|
|
||||||
- `stats.Tracker` に `Close()` メソッド追加 (タイマー停止 + 最終 save)
|
|
||||||
- SIGTERM/SIGINT でシャットダウンフック必須
|
|
||||||
- MEMORY.md の edit_file 書き込み後にターンキャッシュを無効化 (`InvalidateCache()`)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Session Management (Future)
|
## 未実装タスク
|
||||||
|
|
||||||
現状の設計は「正確性」は成熟しているが「ライフサイクル」が欠落している。
|
以下の `todo/` ファイルに分割。各ファイルは互いに依存関係がなく、別ブランチで並列実装可能。
|
||||||
|
|
||||||
**近期:**
|
| ファイル | 概要 |
|
||||||
- `SessionManager.Delete(key)` + TTL エビクション
|
|---|---|
|
||||||
- `sessionLocks sync.Map` (loop.go) の GC
|
| [`todo/TASKS-1.md`](todo/TASKS-1.md) | **Memory & Performance Optimization** — MemoryStore キャッシュ、FunctionCall/ToolDefinition 型整理、stats フラッシュ最適化 |
|
||||||
- 起動時の `loadSessions()` を遅延ロード化
|
| [`todo/TASKS-2.md`](todo/TASKS-2.md) | **Subagent Orchestration (Container Model)** — SubagentContainer、Orchestrator、Presets enforcement、Subagent Plan Mode |
|
||||||
|
| [`todo/TASKS-3.md`](todo/TASKS-3.md) | **Session DAG (SQLite Store)** — セッション管理の SQLite 移行、Turn ベース線形+セッション間 DAG、Fork/Report フロー |
|
||||||
**中期:**
|
| [`todo/TASKS-4.md`](todo/TASKS-4.md) | **Mini App & Static Serving** — 静的配信の汎用化、バンドラ導入、フロントエンドテスト追加 |
|
||||||
- チェックポイント / ロールバック (`Session.Messages` を append-only immutable に)
|
| [`todo/TASKS-5.md`](todo/TASKS-5.md) | **Heartbeat Worktree Management** — worktree 一覧・点検・手動 merge/dispose の CLI/Mini App UI |
|
||||||
- 名前付きセッション (`/new-session`, `/switch-session`, `/list-sessions`)
|
|
||||||
|
|
||||||
**長期:**
|
|
||||||
- `Session.ParentKey` でサブエージェントセッションをグラフ化
|
|
||||||
- クロスセッション検索 (MEMORY.md の補完として)
|
|
||||||
|
|
||||||
設計の哲学: `Session.Messages` (履歴中心) と `MEMORY.md` (知識中心) が現状共存している。どちらを主軸にするかで発展方向が変わる。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Session DAG Migration Plan (Branch/Merge for Subagent Orchestration)
|
|
||||||
|
|
||||||
> Drafted 2026-03-04. Goal: make branch/fork/merge first-class in session history,
|
|
||||||
> instead of injecting subagent completion as ad-hoc system text.
|
|
||||||
|
|
||||||
### 背景
|
|
||||||
|
|
||||||
thread の有無に依存せず、subagent 運用の本質は「会話コンテキストの分岐と合流」。
|
|
||||||
現行の `sessionKey -> linear []Message` は分離には強いが、
|
|
||||||
「どの branch から何を merge したか」を構造化して保持できない。
|
|
||||||
|
|
||||||
### 目標
|
|
||||||
|
|
||||||
1. `sessionKey` の value を head 参照 (`SessionRef`) に変更し、履歴実体を DAG ノード化する
|
|
||||||
2. merge を「1回分の記憶」として会話ログに追加(git merge commit 相当)
|
|
||||||
3. チャネル/プロバイダ差分は capability interface で吸収する
|
|
||||||
4. 既存線形セッションとの後方互換を維持しながら段階移行する
|
|
||||||
|
|
||||||
### 提案データモデル(v1)
|
|
||||||
|
|
||||||
```go
|
|
||||||
type SessionRef struct {
|
|
||||||
HeadNodeID string
|
|
||||||
GraphID string // optional shard namespace
|
|
||||||
Version uint64 // optimistic concurrency (CAS)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConversationNode struct {
|
|
||||||
NodeID string
|
|
||||||
SessionKey string
|
|
||||||
Kind NodeKind // Message | Merge
|
|
||||||
Parents []string // 1 parent = append, 2 parents = merge
|
|
||||||
Events []LogEvent // one-turn memory bundle
|
|
||||||
SummaryDelta string
|
|
||||||
CreatedAt time.Time
|
|
||||||
Author string
|
|
||||||
Meta map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogEvent struct {
|
|
||||||
Role string // user|assistant|tool|system
|
|
||||||
Content string
|
|
||||||
ToolCallID string
|
|
||||||
ToolCalls []providers.ToolCall
|
|
||||||
Usage *providers.UsageInfo
|
|
||||||
Meta map[string]string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 提案インターフェース(v1)
|
|
||||||
|
|
||||||
```go
|
|
||||||
type SessionGraphStore interface {
|
|
||||||
Resolve(sessionKey string) (SessionRef, bool, error)
|
|
||||||
CompareAndSwapHead(sessionKey string, expectVersion uint64, next SessionRef) error
|
|
||||||
|
|
||||||
AppendNode(node ConversationNode) error
|
|
||||||
GetNode(nodeID string) (ConversationNode, bool, error)
|
|
||||||
|
|
||||||
ForkSession(parentKey, childKey string, atHead bool) error
|
|
||||||
MergeSession(baseKey, branchKey string, policy MergePolicy) (mergeNodeID string, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type MergePolicy interface {
|
|
||||||
BuildMergeEvents(basePath []ConversationNode, branchPath []ConversationNode) ([]LogEvent, error)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Merge 方針
|
|
||||||
|
|
||||||
- subagent 結果は `processSystemMessage` で plain text 注入するのではなく、
|
|
||||||
`NodeKind=Merge` のノードを base セッションに append する。
|
|
||||||
- `Events` は「1回分の記憶」として扱う(例: merge summary, adopted decisions, artifacts)。
|
|
||||||
- merge conflict は `MergePolicy` で deterministic ルール優先、必要に応じて LLM 補助。
|
|
||||||
|
|
||||||
### Capability 型の整理方針
|
|
||||||
|
|
||||||
既存の interface ベース設計を継続し、分岐/合流にも適用する:
|
|
||||||
|
|
||||||
- Channel capability (thread, draft, edit, status update)
|
|
||||||
- Provider capability (streaming, structured output, tool-call fidelity)
|
|
||||||
|
|
||||||
capability は type assertion で解決し、非対応時は deterministic fallback。
|
|
||||||
|
|
||||||
### 互換移行ステップ
|
|
||||||
|
|
||||||
1. 新規 `session/graph` 実装を追加(in-memory + file persistence)
|
|
||||||
2. adapter で既存 `GetHistory/SetHistory/AddMessage` を DAG replay に接続
|
|
||||||
3. 新規 fork/merge API を miniapp/command 層へ段階公開
|
|
||||||
4. 既存 linear JSON は lazy migration(read old -> write new on mutation)
|
|
||||||
5. 安定後に old-only path を read-compat へ縮退
|
|
||||||
|
|
||||||
### 並列作業可能タスク
|
|
||||||
|
|
||||||
#### Track A: Core Data Model / Storage
|
|
||||||
- `SessionRef`, `ConversationNode`, `LogEvent` 型追加
|
|
||||||
- `SessionGraphStore` と CAS 更新実装
|
|
||||||
- node persistence + compaction + GC (unreachable branch TTL)
|
|
||||||
|
|
||||||
#### Track B: Replay / Prompt Integration
|
|
||||||
- DAG -> linear message replay 実装
|
|
||||||
- merge node の replay 表現(1-turn memory bundle)
|
|
||||||
- context overflow 時の compression 戦略を DAG 前提に再設計
|
|
||||||
|
|
||||||
#### Track C: Orchestration Integration
|
|
||||||
- subagent completion を merge node 化
|
|
||||||
- task metadata (`task_id`, `source_session`, `duration`, `tool_calls`) を node meta へ格納
|
|
||||||
- reporter/broadcaster のイベントと node lifecycle の対応付け
|
|
||||||
|
|
||||||
#### Track D: Channel & Provider Capabilities
|
|
||||||
- channel capability matrix を型化(thread/draft/edit/task-status)
|
|
||||||
- provider capability matrix を型化(streaming/structured/tool fidelity)
|
|
||||||
- merge policy の capability-aware fallback 追加
|
|
||||||
|
|
||||||
#### Track E: API / UI / Commands
|
|
||||||
- fork/merge/list-branches API 追加
|
|
||||||
- Mini App: branch graph 可視化、merge preview、conflict explanation
|
|
||||||
- CLI/command: `/session fork`, `/session merge`, `/session graph`
|
|
||||||
|
|
||||||
#### Track F: Migration / Safety / Ops
|
|
||||||
- old sessions から DAG への lazy migration
|
|
||||||
- observability: merge success rate, conflict rate, replay latency
|
|
||||||
- rollback switch(feature flag)とデータ整合性チェック
|
|
||||||
|
|
||||||
#### Track G: Tests / Benchmarks
|
|
||||||
- replay determinism tests
|
|
||||||
- merge policy golden tests
|
|
||||||
- concurrency tests (CAS conflict / retry)
|
|
||||||
- memory & latency benchmark(long-history / many-branch scenarios)
|
|
||||||
|
|
||||||
### 依存関係(高レベル)
|
|
||||||
|
|
||||||
- A は全 Track の前提
|
|
||||||
- B/C は A 完了後に並列可能
|
|
||||||
- D は B/C と並列可能(インターフェース先行)
|
|
||||||
- E は B/C/D の API 固定後に進める
|
|
||||||
- F/G は全フェーズ横断で継続実施
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Session DAG Design (Revised 2026-03-04)
|
|
||||||
|
|
||||||
> 上記 Session DAG Migration Plan を SBC制約と設計レビューを踏まえて改訂したもの。
|
|
||||||
|
|
||||||
### 設計原則
|
|
||||||
|
|
||||||
1. **セッション内は線形、セッション間がDAG** — per-message DAGは過剰。ターン間の因果は順序で十分。
|
|
||||||
2. **SQLite single-file backend** — microSD書き込み最小化、WALモードでクラッシュ耐性。
|
|
||||||
3. **サブエージェント報告は user role** — system roleの権威性バイアスを回避。conductorが評価・反論できる。
|
|
||||||
4. **"merge" は特別な操作ではない** — 報告を受けて会話を続ける通常のターン。
|
|
||||||
|
|
||||||
### 初期提案からの変更点
|
|
||||||
|
|
||||||
| 初期提案 | 改訂 | 理由 |
|
|
||||||
|---|---|---|
|
|
||||||
| `ConversationNode` per-message | `Turn` per-turn (複数メッセージをバンドル) | ノード爆発回避、microSD保護 |
|
|
||||||
| CAS + `Version uint64` | `sync.RWMutex` (single process) | SBCはシングルプロセス、CASは過剰 |
|
|
||||||
| `NodeKind=Merge` + system role注入 | `TurnReport` + user role会話 | LLMの権威性バイアス回避 |
|
|
||||||
| per-node JSON files | SQLite single file | 書き込み回数削減、インデックス付きクエリ |
|
|
||||||
| LLM-assisted merge | deterministic (不要に) | mergeという概念自体が消えた |
|
|
||||||
| 毎回DAG→linear replay | cached linear view + dirty flag | ARM CPU負荷軽減 |
|
|
||||||
|
|
||||||
### DAG構造
|
|
||||||
|
|
||||||
```
|
|
||||||
Conductor session: turn1 → turn2 → turn3 → report(scout-1) → turn4 → report(coder-1) → turn5
|
|
||||||
↓ fork ↑ report
|
|
||||||
Scout-1 session: turn1 → turn2 → turn3 ──────────┘
|
|
||||||
↓ fork
|
|
||||||
Coder-1 session: turn1 → turn2 → turn3 ──────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
セッション内は `seq INTEGER` で順序管理。DAGの辺はセッション間の `parent_key` / `origin_key`。
|
|
||||||
|
|
||||||
### SQLite Schema
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE sessions (
|
|
||||||
key TEXT PRIMARY KEY,
|
|
||||||
parent_key TEXT REFERENCES sessions(key),
|
|
||||||
fork_turn_id TEXT, -- 親のどのturnで分岐したか
|
|
||||||
status TEXT NOT NULL DEFAULT 'active', -- active|completed|archived
|
|
||||||
label TEXT NOT NULL DEFAULT '', -- "scout-1", "heartbeat" etc.
|
|
||||||
summary TEXT NOT NULL DEFAULT '',
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE turns (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
session_key TEXT NOT NULL REFERENCES sessions(key) ON DELETE CASCADE,
|
|
||||||
seq INTEGER NOT NULL,
|
|
||||||
kind INTEGER NOT NULL DEFAULT 0, -- 0=normal 1=report 2=fork_point
|
|
||||||
messages TEXT NOT NULL, -- JSON []providers.Message
|
|
||||||
origin_key TEXT, -- report: どのセッションの報告か
|
|
||||||
summary TEXT, -- compaction後。非NULLならmessagesは空
|
|
||||||
author TEXT NOT NULL DEFAULT '',
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
meta TEXT, -- JSON object, nullable
|
|
||||||
UNIQUE(session_key, seq)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_turns_session_seq ON turns(session_key, seq);
|
|
||||||
CREATE INDEX idx_sessions_parent ON sessions(parent_key);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go Interface
|
|
||||||
|
|
||||||
```go
|
|
||||||
package session
|
|
||||||
|
|
||||||
type TurnKind int
|
|
||||||
|
|
||||||
const (
|
|
||||||
TurnNormal TurnKind = iota // user↔assistant 通常ターン
|
|
||||||
TurnReport // サブエージェント報告 + conductor評価
|
|
||||||
TurnForkPoint // マーカー: ここで子セッションが分岐した
|
|
||||||
)
|
|
||||||
|
|
||||||
type Turn struct {
|
|
||||||
ID string
|
|
||||||
Seq int
|
|
||||||
Kind TurnKind
|
|
||||||
Messages []providers.Message
|
|
||||||
OriginKey string // TurnReport時: 報告元セッションkey
|
|
||||||
Summary string // compaction済みなら非空、Messagesは空
|
|
||||||
Author string
|
|
||||||
CreatedAt time.Time
|
|
||||||
Meta map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
type SessionInfo struct {
|
|
||||||
Key string
|
|
||||||
ParentKey string // "" = root
|
|
||||||
ForkTurnID string
|
|
||||||
Status string // active, completed, archived
|
|
||||||
Label string
|
|
||||||
Summary string
|
|
||||||
TurnCount int
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateOpts struct {
|
|
||||||
ParentKey string
|
|
||||||
ForkTurnID string
|
|
||||||
Label string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListFilter struct {
|
|
||||||
ParentKey string
|
|
||||||
Status string
|
|
||||||
}
|
|
||||||
|
|
||||||
// SessionStore — SQLite実装が唯一の実装
|
|
||||||
type SessionStore interface {
|
|
||||||
// セッション
|
|
||||||
Create(key string, opts *CreateOpts) error
|
|
||||||
Get(key string) (*SessionInfo, error)
|
|
||||||
List(filter *ListFilter) ([]*SessionInfo, error)
|
|
||||||
SetStatus(key, status string) error
|
|
||||||
SetSummary(key, summary string) error
|
|
||||||
Delete(key string) error
|
|
||||||
Children(key string) ([]*SessionInfo, error)
|
|
||||||
|
|
||||||
// ターン (セッション内は線形)
|
|
||||||
Append(sessionKey string, turn *Turn) error
|
|
||||||
Turns(sessionKey string, sinceSeq int) ([]*Turn, error)
|
|
||||||
LastTurn(sessionKey string) (*Turn, error)
|
|
||||||
TurnCount(sessionKey string) (int, error)
|
|
||||||
|
|
||||||
// compaction: seq以前のturnsをsummaryで置換
|
|
||||||
Compact(sessionKey string, upToSeq int, summary string) error
|
|
||||||
|
|
||||||
// fork: 親セッションの現在headから子セッションを作る
|
|
||||||
Fork(parentKey, childKey string, opts *CreateOpts) error
|
|
||||||
|
|
||||||
// 管理
|
|
||||||
Prune(olderThan time.Duration) (int, error)
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 高レベルラッパー
|
|
||||||
|
|
||||||
```go
|
|
||||||
// SessionGraph — AgentLoopが直接使う層
|
|
||||||
// SessionStoreをラップし、ターンバッファとlinear viewキャッシュを持つ
|
|
||||||
type SessionGraph struct {
|
|
||||||
store SessionStore
|
|
||||||
buffers sync.Map // sessionKey → *turnBuffer (書き込み中ターン)
|
|
||||||
views sync.Map // sessionKey → *cachedView (LLM用メッセージ列)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LLM用: 全turnsをフラットな[]Messageに展開
|
|
||||||
// compaction済みturnsはsummaryをsystem messageとして先頭に置く
|
|
||||||
func (g *SessionGraph) Messages(sessionKey string) ([]providers.Message, error)
|
|
||||||
|
|
||||||
// ターン開始 (user message受信時)
|
|
||||||
func (g *SessionGraph) BeginTurn(sessionKey string, kind TurnKind) *TurnWriter
|
|
||||||
|
|
||||||
// TurnWriter — 1ターン内でメッセージを逐次追加
|
|
||||||
type TurnWriter struct { ... }
|
|
||||||
func (tw *TurnWriter) Add(msg providers.Message)
|
|
||||||
func (tw *TurnWriter) SetOrigin(sessionKey string) // report元を設定
|
|
||||||
func (tw *TurnWriter) Commit() error // store.Appendして確定
|
|
||||||
func (tw *TurnWriter) Discard() // 破棄 (エラー時)
|
|
||||||
```
|
|
||||||
|
|
||||||
### サブエージェント報告フロー
|
|
||||||
|
|
||||||
```
|
|
||||||
1. conductor が spawn → store.Fork(conductorSession, subagentSession)
|
|
||||||
2. subagent 実行中 → store.Append(subagentSession, Turn{Kind: TurnNormal, ...})
|
|
||||||
3. subagent 完了 → store.SetStatus(subagentSession, "completed")
|
|
||||||
4. conductor 側に report ターン:
|
|
||||||
tw := graph.BeginTurn(conductorSession, TurnReport)
|
|
||||||
tw.SetOrigin(subagentSession)
|
|
||||||
tw.Add(Message{Role: "user", Content: "[scout-1] 調査結果..."})
|
|
||||||
// conductorのLLMループが応答を追加してから tw.Commit()
|
|
||||||
5. conductor が応答:
|
|
||||||
tw.Add(Message{Role: "assistant", Content: "なるほど、JWTで十分..."})
|
|
||||||
tw.Commit()
|
|
||||||
```
|
|
||||||
|
|
||||||
conductorは各報告を個別に評価・反論できる。system roleではないので鵜呑みにしにくい。
|
|
||||||
|
|
||||||
### 既存コードとの互換アダプター
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 移行期間中、既存の SessionManager interface を満たす
|
|
||||||
type LegacyAdapter struct {
|
|
||||||
graph *SessionGraph
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *LegacyAdapter) GetHistory(key string) ([]providers.Message, error) {
|
|
||||||
return a.graph.Messages(key)
|
|
||||||
}
|
|
||||||
func (a *LegacyAdapter) AddMessage(key, role, content string) error { ... }
|
|
||||||
func (a *LegacyAdapter) SetHistory(key string, msgs []providers.Message) error { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
### 移行フェーズ
|
|
||||||
|
|
||||||
1. **Phase 0**: SQLite SessionStore 実装 + LegacyAdapter。既存動作を維持したまま裏側を差し替え
|
|
||||||
2. **Phase 1**: サブエージェントセッション永続化 + Fork/Report ターン導入
|
|
||||||
3. **Phase 2**: AgentLoop を SessionGraph 直接呼び出しに移行。LegacyAdapter 廃止
|
|
||||||
4. **Phase 3**: Mini App graph 可視化 + `/session` コマンド群
|
|
||||||
|
|
|
||||||
75
todo/TASKS-1.md
Normal file
75
todo/TASKS-1.md
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
# TASKS-1: Memory & Performance Optimization
|
||||||
|
|
||||||
|
内部リファクタリング。外部APIの変更なし。他トラックへの依存なし。
|
||||||
|
|
||||||
|
## タスク一覧
|
||||||
|
|
||||||
|
### D-1. MemoryStore のパース済みキャッシュ導入
|
||||||
|
|
||||||
|
`GetMemoryContext()` 1回で `ReadLongTerm()` が5回以上呼ばれる連鎖を解消。
|
||||||
|
|
||||||
|
**方針 (いずれか):**
|
||||||
|
- (a) content パススルー — 高レベルメソッドだけが1回 `ReadLongTerm()` を呼び、content を private ヘルパーに渡す
|
||||||
|
- (b) `*ParsedPlan` 常駐 — MemoryStore にパース済み構造体を持たせ、`edit_file` 後に `InvalidateCache()` を呼ぶ
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/agent/memory.go`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### D-2. FunctionCall.Arguments の型整理
|
||||||
|
|
||||||
|
`FunctionCall.Arguments` が JSON 文字列のまま。ストリーミングループ内で重複 Unmarshal が発生。
|
||||||
|
`ToolCall.Arguments map[string]any` のパース済みフィールドと中途半端に共存している。
|
||||||
|
|
||||||
|
**やること:** パース済みフィールドに一本化し、JSON 文字列フィールドを内部に隠蔽。
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/providers/protocoltypes/types.go`, ストリーミング処理周辺
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### D-3. ToolFunctionDefinition.Parameters → json.RawMessage
|
||||||
|
|
||||||
|
`Parameters` が `map[string]any` のため、プロバイダーへ送るたびに Marshal が必要。
|
||||||
|
`json.RawMessage` にすれば一度の marshal で済む。
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/providers/protocoltypes/types.go`, 各プロバイダー実装
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### D-4. 検索プロバイダーの共通フォーマット抽象
|
||||||
|
|
||||||
|
`[]string + strings.Join` パターンが3箇所に複製。`Search()` 戻り値を構造体にすれば1箇所で済む。
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/search/` 配下
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### D-6. MemoryStore メソッド境界の再設計
|
||||||
|
|
||||||
|
呼び出し側は複数の値が必要でも複数回 `ReadLongTerm()` を呼ぶしかない。
|
||||||
|
D-1 の解決策と合わせて、メソッド境界を「必要な情報の単位」に再編成。
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/agent/memory.go`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### stats.Tracker 定期フラッシュ
|
||||||
|
|
||||||
|
`state/stats.json` が LLM 呼び出し毎に書き込まれる。定期フラッシュ (5分) に変更して microSD 寿命を保護。
|
||||||
|
|
||||||
|
**やること:**
|
||||||
|
- `stats.Tracker` にインメモリバッファ + バックグラウンドフラッシャー goroutine を追加
|
||||||
|
- `Close()` メソッド追加 (タイマー停止 + 最終 save)
|
||||||
|
- SIGTERM/SIGINT でシャットダウンフック
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/state/state.go`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完了基準
|
||||||
|
|
||||||
|
- `go test ./...` 全パス
|
||||||
|
- MEMORY.md 読み取りが1ターンあたり1回以下に削減 (D-1)
|
||||||
|
- FunctionCall の Unmarshal が1回に集約 (D-2)
|
||||||
|
- ToolFunctionDefinition.Parameters の Marshal がプロバイダー初期化時のみ (D-3)
|
||||||
|
- stats.json の書き込み頻度が 98% 削減 (stats.Tracker)
|
||||||
227
todo/TASKS-2.md
Normal file
227
todo/TASKS-2.md
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
# TASKS-2: Subagent Orchestration (Container Model)
|
||||||
|
|
||||||
|
SubagentManager を Container ベースの Orchestrator に進化させる。
|
||||||
|
セッション管理の内部実装 (TASKS-3) には依存しない — 現行の SessionManager 上で動作させ、後から SessionStore に差し替える。
|
||||||
|
|
||||||
|
## 設計コンテキスト
|
||||||
|
|
||||||
|
### アーキテクチャ
|
||||||
|
|
||||||
|
```
|
||||||
|
Conductor goroutine
|
||||||
|
│ ContainerRequest (task, preset, environment)
|
||||||
|
▼
|
||||||
|
Container goroutine: provision → run → finalize
|
||||||
|
│ ContainerMessage (question / result / status)
|
||||||
|
▼
|
||||||
|
Conductor goroutine
|
||||||
|
│ answer (question への回答)
|
||||||
|
▼ (わからなければ human に escalate)
|
||||||
|
Container goroutine (再開)
|
||||||
|
```
|
||||||
|
|
||||||
|
### escalation chain
|
||||||
|
|
||||||
|
```
|
||||||
|
subagent (clarifying) → question → conductor
|
||||||
|
→ conductor が答えられる: inCh に回答
|
||||||
|
→ conductor もわからない: message tool で human に投げ、回答を転送
|
||||||
|
```
|
||||||
|
|
||||||
|
### Presets (5種)
|
||||||
|
|
||||||
|
| preset | 性格 | write | exec | search | spawn |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `scout` | Exploratory | x | x | o | x |
|
||||||
|
| `analyst` | Exploratory | x | go test/vet, git log/diff, grep | o | x |
|
||||||
|
| `coder` | Deliberate | o sandbox | test/lint/fmt 系 | o | x |
|
||||||
|
| `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`,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## タスク一覧
|
||||||
|
|
||||||
|
### 1. SubagentContainer 実装
|
||||||
|
|
||||||
|
goroutine + channel でサブエージェントのライフサイクルを表現。
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ContainerMessage struct {
|
||||||
|
Type string // "question" | "result" | "status"
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubagentContainer struct {
|
||||||
|
inCh chan string // conductor → subagent (回答)
|
||||||
|
outCh chan ContainerMessage // subagent → conductor (質問・結果・進捗)
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 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 に渡すコンテキストの構造化。
|
||||||
|
|
||||||
|
```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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`inject_plan_context: true` で MEMORY.md からの自動注入を有効化。
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/tools/container.go` または `pkg/tools/environment.go` (新規)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. SandboxConfig enforcement
|
||||||
|
|
||||||
|
`ToolRegistry.Execute()` の入口で一括 enforcement。subagent は透過的に sandboxed になる。
|
||||||
|
|
||||||
|
```go
|
||||||
|
type SandboxConfig struct {
|
||||||
|
Preset string
|
||||||
|
WriteRoot string
|
||||||
|
AllowedTools map[string]bool
|
||||||
|
ExecPolicy *ExecPolicy
|
||||||
|
SpawnablePresets []string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
workDir = worktreeDir として設定し、AI が隔離に気づかず振る舞う透過的隔離を実現。
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/tools/sandbox.go` (既存拡張), `pkg/tools/registry.go`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Preset enforcement
|
||||||
|
|
||||||
|
preset 定義と exec allowlist regex を SandboxConfig 経由で enforce。
|
||||||
|
`pkg/tools/sandbox.go` に既存の定義があるが、ToolRegistry.Execute() での一括 enforcement がまだ。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Subagent Plan Mode (in-memory)
|
||||||
|
|
||||||
|
Deliberate preset 用のミニ plan mode。MEMORY.md には触れない。
|
||||||
|
|
||||||
|
```go
|
||||||
|
type SubagentPlanState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlanStateNone SubagentPlanState = iota // Exploratory preset
|
||||||
|
PlanStateClarifying // 目的・制約を確認中
|
||||||
|
PlanStateReview // conductor の承認待ち
|
||||||
|
PlanStateExecuting // 実行中
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubagentPlan struct {
|
||||||
|
State SubagentPlanState
|
||||||
|
Goal string
|
||||||
|
Approach []string
|
||||||
|
QA []QAItem
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
SubagentContainer がフィールドとして保持。goroutine 終了とともに消える。
|
||||||
|
|
||||||
|
**System prompt 使い分け:**
|
||||||
|
- Deliberate: clarifying → review → executing の3段階
|
||||||
|
- Exploratory: 即座に探索開始、判断は自律的に
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. MEMORY.md Orchestration Section
|
||||||
|
|
||||||
|
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 推奨)
|
||||||
|
```
|
||||||
|
|
||||||
|
conductor は spawn 後に Delegated に記録、結果受信後に Findings に追記、方向選択時に Decisions に記録。
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/agent/context.go` (guidance 追記)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完了基準
|
||||||
|
|
||||||
|
- `go test ./...` 全パス
|
||||||
|
- scout/analyst/coder preset で spawn → 結果受信 → conductor 応答の E2E フロー動作
|
||||||
|
- Deliberate preset の clarifying → review → executing フロー動作
|
||||||
|
- SandboxConfig による exec 制限が全 preset で正しく enforcement
|
||||||
|
- escalation chain (subagent → conductor → human) が動作
|
||||||
230
todo/TASKS-3.md
Normal file
230
todo/TASKS-3.md
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
# TASKS-3: Session DAG (SQLite Store)
|
||||||
|
|
||||||
|
セッション管理を JSON ファイルベースの線形スライスから SQLite ベースの Turn DAG に移行する。
|
||||||
|
他トラックへの依存なし — LegacyAdapter で既存コードとの後方互換を維持しながら段階移行。
|
||||||
|
|
||||||
|
## 設計原則
|
||||||
|
|
||||||
|
1. **セッション内は線形、セッション間が DAG** — per-message DAG は過剰。ターン間の因果は順序で十分。
|
||||||
|
2. **SQLite single-file backend** — microSD 書き込み最小化、WAL モードでクラッシュ耐性。
|
||||||
|
3. **サブエージェント報告は user role** — system role の権威性バイアスを回避。conductor が評価・反論できる。
|
||||||
|
4. **"merge" は特別な操作ではない** — 報告を受けて会話を続ける通常のターン。
|
||||||
|
|
||||||
|
## DAG 構造
|
||||||
|
|
||||||
|
```
|
||||||
|
Conductor session: turn1 → turn2 → turn3 → report(scout-1) → turn4 → report(coder-1) → turn5
|
||||||
|
↓ fork ↑ report
|
||||||
|
Scout-1 session: turn1 → turn2 → turn3 ──────────┘
|
||||||
|
↓ fork
|
||||||
|
Coder-1 session: turn1 → turn2 → turn3 ──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## SQLite Schema
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE sessions (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
parent_key TEXT REFERENCES sessions(key),
|
||||||
|
fork_turn_id TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active',
|
||||||
|
label TEXT NOT NULL DEFAULT '',
|
||||||
|
summary TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE turns (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
session_key TEXT NOT NULL REFERENCES sessions(key) ON DELETE CASCADE,
|
||||||
|
seq INTEGER NOT NULL,
|
||||||
|
kind INTEGER NOT NULL DEFAULT 0,
|
||||||
|
messages TEXT NOT NULL,
|
||||||
|
origin_key TEXT,
|
||||||
|
summary TEXT,
|
||||||
|
author TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
meta TEXT,
|
||||||
|
UNIQUE(session_key, seq)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_turns_session_seq ON turns(session_key, seq);
|
||||||
|
CREATE INDEX idx_sessions_parent ON sessions(parent_key);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Go Interface
|
||||||
|
|
||||||
|
```go
|
||||||
|
type TurnKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TurnNormal TurnKind = iota
|
||||||
|
TurnReport
|
||||||
|
TurnForkPoint
|
||||||
|
)
|
||||||
|
|
||||||
|
type Turn struct {
|
||||||
|
ID string
|
||||||
|
Seq int
|
||||||
|
Kind TurnKind
|
||||||
|
Messages []providers.Message
|
||||||
|
OriginKey string
|
||||||
|
Summary string
|
||||||
|
Author string
|
||||||
|
CreatedAt time.Time
|
||||||
|
Meta map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionInfo struct {
|
||||||
|
Key string
|
||||||
|
ParentKey string
|
||||||
|
ForkTurnID string
|
||||||
|
Status string
|
||||||
|
Label string
|
||||||
|
Summary string
|
||||||
|
TurnCount int
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionStore interface {
|
||||||
|
Create(key string, opts *CreateOpts) error
|
||||||
|
Get(key string) (*SessionInfo, error)
|
||||||
|
List(filter *ListFilter) ([]*SessionInfo, error)
|
||||||
|
SetStatus(key, status string) error
|
||||||
|
SetSummary(key, summary string) error
|
||||||
|
Delete(key string) error
|
||||||
|
Children(key string) ([]*SessionInfo, error)
|
||||||
|
|
||||||
|
Append(sessionKey string, turn *Turn) error
|
||||||
|
Turns(sessionKey string, sinceSeq int) ([]*Turn, error)
|
||||||
|
LastTurn(sessionKey string) (*Turn, error)
|
||||||
|
TurnCount(sessionKey string) (int, error)
|
||||||
|
|
||||||
|
Compact(sessionKey string, upToSeq int, summary string) error
|
||||||
|
Fork(parentKey, childKey string, opts *CreateOpts) error
|
||||||
|
|
||||||
|
Prune(olderThan time.Duration) (int, error)
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 高レベルラッパー
|
||||||
|
|
||||||
|
```go
|
||||||
|
type SessionGraph struct {
|
||||||
|
store SessionStore
|
||||||
|
buffers sync.Map // sessionKey → *turnBuffer
|
||||||
|
views sync.Map // sessionKey → *cachedView
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *SessionGraph) Messages(sessionKey string) ([]providers.Message, error)
|
||||||
|
func (g *SessionGraph) BeginTurn(sessionKey string, kind TurnKind) *TurnWriter
|
||||||
|
|
||||||
|
type TurnWriter struct { ... }
|
||||||
|
func (tw *TurnWriter) Add(msg providers.Message)
|
||||||
|
func (tw *TurnWriter) SetOrigin(sessionKey string)
|
||||||
|
func (tw *TurnWriter) Commit() error
|
||||||
|
func (tw *TurnWriter) Discard()
|
||||||
|
```
|
||||||
|
|
||||||
|
## サブエージェント報告フロー
|
||||||
|
|
||||||
|
```
|
||||||
|
1. conductor が spawn → store.Fork(conductorSession, subagentSession)
|
||||||
|
2. subagent 実行中 → store.Append(subagentSession, Turn{Kind: TurnNormal, ...})
|
||||||
|
3. subagent 完了 → store.SetStatus(subagentSession, "completed")
|
||||||
|
4. conductor 側に report ターン:
|
||||||
|
tw := graph.BeginTurn(conductorSession, TurnReport)
|
||||||
|
tw.SetOrigin(subagentSession)
|
||||||
|
tw.Add(Message{Role: "user", Content: "[scout-1] 調査結果..."})
|
||||||
|
5. conductor が応答:
|
||||||
|
tw.Add(Message{Role: "assistant", Content: "なるほど、JWTで十分..."})
|
||||||
|
tw.Commit()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## タスク一覧
|
||||||
|
|
||||||
|
### Phase 0: SQLite SessionStore + LegacyAdapter
|
||||||
|
|
||||||
|
既存動作を維持したまま裏側を差し替える。
|
||||||
|
|
||||||
|
1. **SQLite SessionStore 実装**
|
||||||
|
- `pkg/session/sqlite.go` (新規): SessionStore interface の SQLite 実装
|
||||||
|
- WAL モード、`modernc.org/sqlite` (CGO なし、ARM クロスコンパイル容易)
|
||||||
|
- schema migration (CREATE TABLE IF NOT EXISTS)
|
||||||
|
- `go:build` タグなしで常に利用可能
|
||||||
|
|
||||||
|
2. **LegacyAdapter 実装**
|
||||||
|
- `pkg/session/legacy_adapter.go` (新規)
|
||||||
|
- 既存の `GetHistory` / `SetHistory` / `AddMessage` / `MarkDirty` を SessionGraph 経由で実装
|
||||||
|
- 既存テストが全パスすること
|
||||||
|
|
||||||
|
3. **JSON → SQLite lazy migration**
|
||||||
|
- 起動時に `sessions/*.json` を検出 → SQLite に import → JSON ファイルをリネーム (.migrated)
|
||||||
|
- エラー時は JSON にフォールバック
|
||||||
|
|
||||||
|
4. **AgentLoop 配線**
|
||||||
|
- `AgentInstance` が `SessionStore` を保持、`LegacyAdapter` 経由で既存コードに注入
|
||||||
|
- 既存の `SessionManager` は Phase 2 で廃止
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 1: Fork/Report ターン導入
|
||||||
|
|
||||||
|
5. **Fork 操作**
|
||||||
|
- `SessionStore.Fork()` 実装
|
||||||
|
- 親セッションに `TurnForkPoint` を追記、子セッションを `parent_key` 付きで作成
|
||||||
|
|
||||||
|
6. **Report ターン**
|
||||||
|
- `TurnReport` の Append/Turns/Messages 対応
|
||||||
|
- `origin_key` でどのセッションの報告かを追跡
|
||||||
|
- user role でメッセージ格納 (system role 禁止)
|
||||||
|
|
||||||
|
7. **サブエージェントセッション永続化**
|
||||||
|
- サブエージェント実行中のターンを SQLite に記録
|
||||||
|
- 完了後にセッション status を "completed" に変更
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: AgentLoop 直接移行
|
||||||
|
|
||||||
|
8. **SessionGraph 直接呼び出し**
|
||||||
|
- `runAgentLoop()` を `SessionGraph.BeginTurn()` / `TurnWriter` 経由に変更
|
||||||
|
- `processSystemMessage()` を Report ターン生成に変更
|
||||||
|
- LegacyAdapter 廃止
|
||||||
|
|
||||||
|
9. **Compaction**
|
||||||
|
- 古いターンの messages を空にして summary で置換
|
||||||
|
- context window 管理と連動
|
||||||
|
|
||||||
|
10. **セッションライフサイクル**
|
||||||
|
- `Delete(key)` + TTL エビクション (Prune)
|
||||||
|
- `sessionLocks sync.Map` の GC
|
||||||
|
- 起動時の遅延ロード (SQLite なので自然に実現)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: UI & Commands
|
||||||
|
|
||||||
|
11. **Mini App セッショングラフ可視化**
|
||||||
|
- WebSocket で session DAG 構造を配信
|
||||||
|
- fork/report 関係をグラフとして描画
|
||||||
|
|
||||||
|
12. **CLI コマンド**
|
||||||
|
- `/session list` — アクティブセッション一覧
|
||||||
|
- `/session fork` — 現在のセッションを fork
|
||||||
|
- `/session graph` — DAG 構造をテキスト表示
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完了基準
|
||||||
|
|
||||||
|
- `go test ./...` 全パス
|
||||||
|
- 既存の JSON セッションが SQLite に自動マイグレーション
|
||||||
|
- Phase 0 完了時点で既存動作に変化なし (LegacyAdapter 透過)
|
||||||
|
- サブエージェント報告が user role ターンとして記録
|
||||||
|
- conductor が報告を個別に評価・応答するフロー動作
|
||||||
|
- microSD 書き込み頻度が既存比で削減
|
||||||
74
todo/TASKS-4.md
Normal file
74
todo/TASKS-4.md
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# TASKS-4: Mini App & Static Serving
|
||||||
|
|
||||||
|
Mini App のフロントエンド改善。バックエンドの変更に依存しない。
|
||||||
|
|
||||||
|
## 現状
|
||||||
|
|
||||||
|
```
|
||||||
|
pkg/miniapp/static/
|
||||||
|
index.html ← すべての JS/CSS をインライン
|
||||||
|
map.js ← オーケストレーションルーム描画
|
||||||
|
```
|
||||||
|
|
||||||
|
Go 側: `//go:embed static/index.html static/map.js` + 個別ルートで配信。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## タスク一覧
|
||||||
|
|
||||||
|
### 1. Static file serving の汎用化
|
||||||
|
|
||||||
|
個別ルート (`serveIndex`, `serveMapJS`) を `http.FileServer(http.FS(staticFS))` に統合。
|
||||||
|
|
||||||
|
**やること:**
|
||||||
|
- `//go:embed static` でディレクトリごと embed
|
||||||
|
- `/miniapp/` 以下を `http.FileServer` で一括配信
|
||||||
|
- 新規ファイル追加時にルート登録が不要になる
|
||||||
|
- `serveIndex` の `ORCH_ENABLED` 注入は `text/template` に移行
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/miniapp/handler.go`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. バンドラ導入 (Vite / esbuild)
|
||||||
|
|
||||||
|
inline JS/CSS を外部ファイルに分離し、ビルドステップで bundle する。
|
||||||
|
|
||||||
|
**やること:**
|
||||||
|
- `pkg/miniapp/frontend/` に source を配置
|
||||||
|
- esbuild (軽量、Go 製) でビルド → `pkg/miniapp/static/dist/` に出力
|
||||||
|
- Go 側は `//go:embed static/dist` で embed
|
||||||
|
- `ORCH_ENABLED` はビルド時の環境変数注入 (`define`) で対応
|
||||||
|
- `Makefile` / `go:generate` でビルドステップを統合
|
||||||
|
|
||||||
|
**検討事項:**
|
||||||
|
- esbuild は Go 製でクロスコンパイル環境に影響しない
|
||||||
|
- Vite は機能豊富だが Node.js 依存が増える
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Log viewer のフロントエンドテスト
|
||||||
|
|
||||||
|
`renderLogs()` (index.html 内の inline JS) のテストが皆無。
|
||||||
|
Fields 表示バグが検出されずに ship された前科あり。
|
||||||
|
|
||||||
|
**やること:**
|
||||||
|
- index.html から JS を外部ファイルに抽出 (タスク 2 と連動)
|
||||||
|
- `renderLogs()`, `renderFields()` 等の unit test を追加
|
||||||
|
- DOM 操作のテストは jsdom (vitest) or happy-dom
|
||||||
|
- CI で `pnpm test` を実行
|
||||||
|
|
||||||
|
**最低限のテストケース:**
|
||||||
|
- メッセージの正常レンダリング
|
||||||
|
- Fields の表示 (空/1件/複数件)
|
||||||
|
- サニタイズ (XSS 防止)
|
||||||
|
- ログのフィルタリング・ページネーション
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完了基準
|
||||||
|
|
||||||
|
- 新規 JS/CSS ファイルの追加がルート登録なしで配信される
|
||||||
|
- `ORCH_ENABLED` の注入がテンプレートまたはビルド時変数で動作
|
||||||
|
- `renderLogs()` のユニットテストが CI で実行される
|
||||||
|
- `go build ./...` が変わらず動作 (embed パスの整合性)
|
||||||
62
todo/TASKS-5.md
Normal file
62
todo/TASKS-5.md
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
# TASKS-5: Heartbeat Worktree Management
|
||||||
|
|
||||||
|
ハートビートで作られる git worktree の管理 CLI/UI。他トラックへの依存なし。
|
||||||
|
|
||||||
|
## 現状の問題
|
||||||
|
|
||||||
|
- ハートビートセッションが `.worktrees/heartbeat-YYYYMMDD/` を作るが、人手で管理する手段がない
|
||||||
|
- `PruneOrphaned()` は起動時にディレクトリを削除するが、**未コミット変更は通知なく消失**する
|
||||||
|
- active worktree の一覧・点検・手動 merge/dispose ができない
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## タスク一覧
|
||||||
|
|
||||||
|
### 1. `/plan worktrees` コマンド
|
||||||
|
|
||||||
|
worktree の一覧表示と操作を提供する CLI コマンド。
|
||||||
|
|
||||||
|
**サブコマンド:**
|
||||||
|
- `/plan worktrees list` — active worktree の一覧 (branch, last commit, status, uncommitted changes の有無)
|
||||||
|
- `/plan worktrees inspect <name>` — 特定 worktree の詳細 (diff, log)
|
||||||
|
- `/plan worktrees merge <name>` — main branch に merge を試行
|
||||||
|
- `/plan worktrees dispose <name>` — worktree を削除 (未コミット変更がある場合は確認)
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/agent/loop.go` (コマンドハンドラ追加), `pkg/git/worktree.go`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. PruneOrphaned の安全化
|
||||||
|
|
||||||
|
起動時の orphan prune で未コミット変更を保護する。
|
||||||
|
|
||||||
|
**やること:**
|
||||||
|
- prune 前に `git status` で未コミット変更を検出
|
||||||
|
- 未コミット変更がある場合: auto-commit (メッセージ: "auto-save before prune") してから dispose
|
||||||
|
- auto-commit 結果をログに出力
|
||||||
|
- commit 不可の場合 (conflict 等) はスキップしてログ警告
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/git/worktree.go`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Mini App worktree UI
|
||||||
|
|
||||||
|
Mini App から worktree を閲覧・操作できる画面。
|
||||||
|
|
||||||
|
**やること:**
|
||||||
|
- `/api/worktrees` エンドポイント (GET: list, POST: merge/dispose)
|
||||||
|
- Mini App に worktree 一覧パネルを追加
|
||||||
|
- 各 worktree: branch 名、最終コミット日時、uncommitted changes badge
|
||||||
|
- merge/dispose ボタン (確認ダイアログ付き)
|
||||||
|
|
||||||
|
**対象ファイル:** `pkg/miniapp/handler.go`, `pkg/miniapp/static/index.html`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完了基準
|
||||||
|
|
||||||
|
- `go test ./...` 全パス
|
||||||
|
- `/plan worktrees list` で active worktree が表示される
|
||||||
|
- orphan prune 時に未コミット変更が auto-commit で保護される
|
||||||
|
- Mini App から worktree の一覧と dispose が操作できる
|
||||||
Loading…
Add table
Reference in a new issue