fix(agent): fix subturn panic result, hard abort rollback, and drain bus exit

- spawnSubTurn: set result=nil on panic instead of constructing a non-nil ToolResult
- HardAbort: roll back session history to initialHistoryLength after Finish()
- drainBusToSteering: switch to non-blocking reads after first message so function
  returns promptly when the inbound channel is empty
- remove obsolete documentation files
This commit is contained in:
Administrator
2026-03-22 20:35:14 +08:00
parent 7ba8682ac5
commit 7868c5811a
6 changed files with 36 additions and 1247 deletions
+27 -9
View File
@@ -509,21 +509,39 @@ func (al *AgentLoop) Run(ctx context.Context) error {
return nil
}
// drainBusToSteering continuously consumes inbound messages and redirects
// messages from the active scope into the steering queue. Messages from other
// scopes are requeued so they can be processed normally after the active turn.
// drainBusToSteering consumes inbound messages and redirects messages from the
// active scope into the steering queue. Messages from other scopes are requeued
// so they can be processed normally after the active turn. It drains all
// immediately available messages, blocking for the first one until ctx is done.
func (al *AgentLoop) drainBusToSteering(ctx context.Context, activeScope, activeAgentID string) {
blocking := true
for {
var msg bus.InboundMessage
select {
case <-ctx.Done():
return
case m, ok := <-al.bus.InboundChan():
if !ok {
if blocking {
// Block waiting for the first available message or ctx cancellation.
select {
case <-ctx.Done():
return
case m, ok := <-al.bus.InboundChan():
if !ok {
return
}
msg = m
}
} else {
// Non-blocking: drain any remaining queued messages, return when empty.
select {
case m, ok := <-al.bus.InboundChan():
if !ok {
return
}
msg = m
default:
return
}
msg = m
}
blocking = false
msgScope, _, scopeOK := al.resolveSteeringTarget(msg)
if !scopeOK || msgScope != activeScope {
+8
View File
@@ -460,6 +460,14 @@ func (al *AgentLoop) HardAbort(sessionKey string) error {
// Use isHardAbort=true for hard abort to immediately cancel all children.
ts.Finish(true)
// Roll back session history to the state before the turn started.
if ts.session != nil {
history := ts.session.GetHistory(sessionKey)
if ts.initialHistoryLength < len(history) {
ts.session.SetHistory(sessionKey, history[:ts.initialHistoryLength])
}
}
return nil
}
+1 -8
View File
@@ -428,19 +428,12 @@ func spawnSubTurn(
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("subturn panicked: %v", r)
result = nil
logger.ErrorCF("subturn", "SubTurn panicked", map[string]any{
"child_id": childID,
"parent_id": parentTS.turnID,
"panic": r,
})
// Ensure result is not nil to prevent panic during event emission
if result == nil {
result = &tools.ToolResult{
Err: err,
ForLLM: fmt.Sprintf("SubTurn panicked: %v", r),
}
}
}
// Result Delivery Strategy (Async vs Sync)