picoclaw/hybrid_implementation_guide.md

563 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 混合方案落地指南
## 目标
结合 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 场景