picoclaw/hybrid_implementation_guide.md

564 lines
15 KiB
Markdown
Raw Normal View History

# 混合方案落地指南
## 目标
结合 Incoming 的事件驱动架构和 HEAD 的并发能力,实现:
- ✅ 保留 `activeTurnStates` map支持并发 Session
- ✅ 采用 `EventBus``HookManager`(事件驱动 + 扩展性)
- ✅ 保留 SubTurn 并发支持
- ✅ 统一使用 `runTurn` 函数(简化代码)
---
## 实施步骤
### 步骤 1: 合并 AgentLoop 结构体 (30 分钟)
**目标**: 结合两边的字段
```go
type AgentLoop struct {
// ===== Incoming 的字段 (保留) =====
bus *bus.MessageBus
cfg *config.Config
registry *AgentRegistry
state *state.Manager
eventBus *EventBus // ✅ 新增:事件系统
hooks *HookManager // ✅ 新增Hook 系统
running atomic.Bool
summarizing sync.Map
fallback *providers.FallbackChain
channelManager *channels.Manager
mediaStore media.MediaStore
transcriber voice.Transcriber
cmdRegistry *commands.Registry
mcp mcpRuntime
hookRuntime hookRuntime // ✅ 新增Hook 运行时
steering *steeringQueue
mu sync.RWMutex
// ===== HEAD 的字段 (保留) =====
activeTurnStates sync.Map // ✅ 保留:支持并发 Session
subTurnCounter atomic.Int64 // ✅ 保留SubTurn ID 生成
// ===== Incoming 的字段 (调整) =====
turnSeq atomic.Uint64 // ✅ 保留:全局 Turn 序列号
activeRequests sync.WaitGroup // ✅ 保留:请求跟踪
reloadFunc func() error
}
```
**操作**:
1. 找到 AgentLoop 结构体定义38-77 行的冲突)
2. 采用上面的合并版本
3. 删除 Incoming 的 `activeTurn *turnState``activeTurnMu`(不需要了)
---
### 步骤 2: 合并 processOptions 结构体 (10 分钟)
**目标**: 采用 Incoming 的版本,移除 HEAD 的 `SkipAddUserMessage`
```go
type processOptions struct {
SessionKey string
Channel string
ChatID string
SenderID string
SenderDisplayName string
UserMessage string
SystemPromptOverride string
Media []string
InitialSteeringMessages []providers.Message // ✅ Incoming 的方式
DefaultResponse string
EnableSummary bool
SendResponse bool
NoHistory bool
SkipInitialSteeringPoll bool
}
type continuationTarget struct {
SessionKey string
Channel string
ChatID string
}
```
**操作**:
1. 找到 processOptions 结构体92-112 行的冲突)
2. 采用上面的版本
3. 添加 `continuationTarget` 结构体
---
### 步骤 3: 更新 turnState 结构体 (20 分钟)
**目标**: 在 Incoming 的 turnState 基础上添加 SubTurn 支持
需要检查 `turn.go``turn_state.go` 文件,确保 turnState 有这些字段:
```go
type turnState struct {
mu sync.RWMutex
// ===== Incoming 的字段 (保留) =====
agent *AgentInstance
opts processOptions
scope turnEventScope
turnID string
agentID string
sessionKey string
channel string
chatID string
userMessage string
media []string
phase TurnPhase
iteration int
startedAt time.Time
finalContent string
followUps []bus.InboundMessage
gracefulInterrupt bool
gracefulInterruptHint string
gracefulTerminalUsed bool
hardAbort bool
providerCancel context.CancelFunc
turnCancel context.CancelFunc
restorePointHistory []providers.Message
restorePointSummary string
persistedMessages []providers.Message
// ===== HEAD 的字段 (新增SubTurn 支持) =====
depth int // ✅ SubTurn 深度
parentTurnID string // ✅ 父 Turn ID
childTurnIDs []string // ✅ 子 Turn IDs
pendingResults chan *tools.ToolResult // ✅ SubTurn 结果 channel
concurrencySem chan struct{} // ✅ 并发信号量
isFinished atomic.Bool // ✅ 是否已完成
}
```
**操作**:
1. 查找 `turnState` 结构体定义
2. 如果有冲突,采用 Incoming 的基础版本
3. 添加 SubTurn 相关字段depth, parentTurnID 等)
---
### 步骤 4: 重写 runAgentLoop 函数 (1 小时)
**目标**: 简化为调用 runTurn但保留 SubTurn 检测
```go
func (al *AgentLoop) runAgentLoop(
ctx context.Context,
agent *AgentInstance,
opts processOptions,
) (string, error) {
// 1. 检查是否在 SubTurn 中
existingTS := turnStateFromContext(ctx)
var ts *turnState
var isRootTurn bool
if existingTS != nil {
// 在 SubTurn 中 - 创建子 turnState
ts = newSubTurnState(agent, opts, existingTS, al.newTurnEventScope(agent.ID, opts.SessionKey))
isRootTurn = false
} else {
// 根 Turn - 创建新的 turnState
ts = newTurnState(agent, opts, al.newTurnEventScope(agent.ID, opts.SessionKey))
isRootTurn = true
// 注册到 activeTurnStates支持并发
al.activeTurnStates.Store(opts.SessionKey, ts)
defer al.activeTurnStates.Delete(opts.SessionKey)
}
// 2. 记录 last channel
if opts.Channel != "" && opts.ChatID != "" && !constants.IsInternalChannel(opts.Channel) {
channelKey := fmt.Sprintf("%s:%s", opts.Channel, opts.ChatID)
if err := al.RecordLastChannel(channelKey); err != nil {
logger.WarnCF("agent", "Failed to record last channel",
map[string]any{"error": err.Error()})
}
}
// 3. 执行 Turn带事件和 Hook
result, err := al.runTurn(ctx, ts)
if err != nil {
return "", err
}
if result.status == TurnEndStatusAborted {
return "", nil
}
// 4. 处理 SubTurn 结果(仅根 Turn
if isRootTurn && ts.pendingResults != nil {
finalResults := al.drainPendingSubTurnResults(ts)
for _, r := range finalResults {
if r != nil && r.ForLLM != "" {
result.finalContent += fmt.Sprintf("\n\n[SubTurn Result] %s", r.ForLLM)
}
}
}
// 5. 处理 follow-up 消息
for _, followUp := range result.followUps {
if pubErr := al.bus.PublishInbound(ctx, followUp); pubErr != nil {
logger.WarnCF("agent", "Failed to publish follow-up after turn",
map[string]any{"turn_id": ts.turnID, "error": pubErr.Error()})
}
}
// 6. 发送响应
if opts.SendResponse && result.finalContent != "" {
al.bus.PublishOutbound(ctx, bus.OutboundMessage{
Channel: opts.Channel,
ChatID: opts.ChatID,
Content: result.finalContent,
})
}
return result.finalContent, nil
}
```
**操作**:
1. 找到 runAgentLoop 函数1439-1581 行的冲突)
2. 替换为上面的简化版本
3. 保留 SubTurn 检测逻辑(`turnStateFromContext`
4. 保留 `activeTurnStates` 注册逻辑
---
### 步骤 5: 采用 Incoming 的 runTurn 函数 (30 分钟)
**目标**: 使用 Incoming 的 runTurn但添加 SubTurn 结果轮询
```go
func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState) (turnResult, error) {
turnCtx, turnCancel := context.WithCancel(ctx)
defer turnCancel()
ts.setTurnCancel(turnCancel)
// ===== 不使用单例 activeTurn因为我们有 activeTurnStates =====
// al.registerActiveTurn(ts) ← 删除这行
// defer al.clearActiveTurn(ts) ← 删除这行
turnStatus := TurnEndStatusCompleted
defer func() {
al.emitEvent(
EventKindTurnEnd,
ts.eventMeta("runTurn", "turn.end"),
TurnEndPayload{
Status: turnStatus,
Iterations: ts.currentIteration(),
Duration: time.Since(ts.startedAt),
FinalContentLen: ts.finalContentLen(),
},
)
}()
al.emitEvent(
EventKindTurnStart,
ts.eventMeta("runTurn", "turn.start"),
TurnStartPayload{
Channel: ts.channel,
ChatID: ts.chatID,
UserMessage: ts.userMessage,
MediaCount: len(ts.media),
},
)
// ... 保留 Incoming 的其余逻辑 ...
// ===== 在 Turn Loop 中添加 SubTurn 结果轮询 =====
turnLoop:
for ts.currentIteration() < ts.agent.MaxIterations || len(pendingMessages) > 0 {
// ... LLM 调用 ...
// ... Tool 执行 ...
// ✅ 新增:轮询 SubTurn 结果
if ts.pendingResults != nil {
subTurnResults := al.pollSubTurnResults(ts)
for _, result := range subTurnResults {
if result.ForLLM != "" {
// 将 SubTurn 结果作为 steering message 注入
pendingMessages = append(pendingMessages, providers.Message{
Role: "user",
Content: fmt.Sprintf("[SubTurn Result] %s", result.ForLLM),
})
}
}
}
// ... 继续迭代 ...
}
// ... 返回结果 ...
}
```
**操作**:
1. 找到 runTurn 函数1672-1689 行开始的冲突)
2. 采用 Incoming 的完整实现
3. 删除 `registerActiveTurn``clearActiveTurn` 调用
4. 在 Turn Loop 中添加 SubTurn 结果轮询逻辑
---
### 步骤 6: 实现辅助函数 (30 分钟)
需要实现以下辅助函数:
#### 6.1 newSubTurnState
```go
func newSubTurnState(
agent *AgentInstance,
opts processOptions,
parent *turnState,
scope turnEventScope,
) *turnState {
ts := newTurnState(agent, opts, scope)
// 设置 SubTurn 关系
ts.depth = parent.depth + 1
ts.parentTurnID = parent.turnID
ts.pendingResults = parent.pendingResults // 共享结果 channel
ts.concurrencySem = parent.concurrencySem // 共享信号量
// 记录父子关系
parent.mu.Lock()
parent.childTurnIDs = append(parent.childTurnIDs, ts.turnID)
parent.mu.Unlock()
return ts
}
```
#### 6.2 pollSubTurnResults
```go
func (al *AgentLoop) pollSubTurnResults(ts *turnState) []*tools.ToolResult {
if ts.pendingResults == nil {
return nil
}
var results []*tools.ToolResult
for {
select {
case result := <-ts.pendingResults:
results = append(results, result)
default:
return results
}
}
}
```
#### 6.3 drainPendingSubTurnResults
```go
func (al *AgentLoop) drainPendingSubTurnResults(ts *turnState) []*tools.ToolResult {
if ts.pendingResults == nil {
return nil
}
// 等待一小段时间,确保所有 SubTurn 结果都到达
time.Sleep(100 * time.Millisecond)
return al.pollSubTurnResults(ts)
}
```
#### 6.4 更新 GetActiveTurn
```go
func (al *AgentLoop) GetActiveTurn(sessionKey string) *ActiveTurnInfo {
val, ok := al.activeTurnStates.Load(sessionKey)
if !ok {
return nil
}
ts, ok := val.(*turnState)
if !ok {
return nil
}
info := ts.snapshot()
return &info
}
```
---
### 步骤 7: 更新 SpawnSubTurn 实现 (30 分钟)
确保 spawn tool 能正确创建 SubTurn
```go
func (spawner *subTurnSpawner) SpawnSubTurn(
ctx context.Context,
config SubTurnConfig,
) (*tools.ToolResult, error) {
// 1. 获取父 turnState
parentTS := turnStateFromContext(ctx)
if parentTS == nil {
return nil, fmt.Errorf("no parent turn state in context")
}
// 2. 检查深度限制
maxDepth := spawner.loop.getSubTurnConfig().maxDepth
if parentTS.depth >= maxDepth {
return tools.ErrorResult(fmt.Sprintf(
"SubTurn depth limit reached (%d)", maxDepth)), nil
}
// 3. 获取并发信号量
select {
case <-parentTS.concurrencySem:
defer func() { parentTS.concurrencySem <- struct{}{} }()
case <-ctx.Done():
return tools.ErrorResult("SubTurn cancelled"), nil
}
// 4. 生成 SubTurn ID
subTurnID := spawner.loop.subTurnCounter.Add(1)
turnID := fmt.Sprintf("%s-sub-%d", parentTS.turnID, subTurnID)
// 5. 创建 SubTurn context
subCtx := withTurnState(ctx, parentTS) // 继承父 context
// 6. 启动 SubTurn goroutine
go func() {
opts := processOptions{
SessionKey: parentTS.sessionKey,
Channel: parentTS.channel,
ChatID: parentTS.chatID,
UserMessage: config.SystemPrompt,
SystemPromptOverride: config.SystemPrompt,
NoHistory: true, // SubTurn 不加载历史
SendResponse: false, // SubTurn 不发送响应
}
result, err := spawner.loop.runAgentLoop(subCtx, spawner.agent, opts)
// 7. 发送结果到父 Turn
toolResult := &tools.ToolResult{
ForLLM: result,
Error: err,
}
select {
case parentTS.pendingResults <- toolResult:
case <-subCtx.Done():
}
}()
// 8. 立即返回(异步执行)
return tools.AsyncResult(fmt.Sprintf("SubTurn %d started", subTurnID)), nil
}
```
---
### 步骤 8: 解决其他小冲突 (1 小时)
处理剩余的 7 个冲突点:
1. **变量命名冲突** (2179-2183 行等)
- 统一使用 `ts.channel`, `ts.chatID` 而不是 `opts.Channel`
2. **Tool feedback** (2469-2494 行)
- 采用 HEAD 的实现(发送 tool feedback 到 chat
3. **其他小差异**
- 逐个检查,优先采用 Incoming 的实现
- 确保 EventBus 事件正确触发
---
## 验证步骤
### 1. 编译验证
```bash
go build ./pkg/agent/
```
### 2. 单元测试
```bash
go test ./pkg/agent/ -v
```
### 3. 功能测试
创建测试用例验证:
```go
func TestMixedArchitecture_ConcurrentSessions(t *testing.T) {
// 测试多个 session 并发执行
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
sessionKey := fmt.Sprintf("session-%d", id)
// 执行 agent loop
}(i)
}
wg.Wait()
}
func TestMixedArchitecture_SubTurnExecution(t *testing.T) {
// 测试 SubTurn 执行
// 1. 启动主 Turn
// 2. 调用 spawn tool
// 3. 验证 SubTurn 结果返回
}
func TestMixedArchitecture_EventBusIntegration(t *testing.T) {
// 测试事件系统
// 1. 订阅事件
// 2. 执行 Turn
// 3. 验证事件触发
}
```
---
## 预期结果
完成后,系统应该:
✅ 支持多个 Session 并发执行
✅ 支持 SubTurn 并发和嵌套
✅ 所有操作都触发 EventBus 事件
✅ Hook 系统正常工作
✅ 代码结构清晰,易于维护
---
## 时间估算
- 步骤 1-2: 结构体合并 (40 分钟)
- 步骤 3: turnState 更新 (20 分钟)
- 步骤 4: runAgentLoop 重写 (1 小时)
- 步骤 5: runTurn 调整 (30 分钟)
- 步骤 6: 辅助函数 (30 分钟)
- 步骤 7: SpawnSubTurn (30 分钟)
- 步骤 8: 其他冲突 (1 小时)
- 测试验证 (1 小时)
**总计: 约 5-6 小时**
---
## 风险和注意事项
1. **Context 传递**: 确保 SubTurn 的 context 正确继承父 context
2. **Channel 关闭**: 确保 `pendingResults` channel 在合适的时机关闭
3. **并发安全**: 所有对 turnState 的访问都要加锁
4. **事件顺序**: 确保事件按正确顺序触发
5. **测试覆盖**: 重点测试并发场景和 SubTurn 场景