From bfa8c88565abccff02892cbfb59e0db51f4db49a Mon Sep 17 00:00:00 2001 From: sky5454 Date: Mon, 20 Apr 2026 23:10:08 +0800 Subject: [PATCH] fix(agent): fix agent hardAbortX --- pkg/agent/agent_event.go | 18 ------------------ pkg/agent/pipeline_execute.go | 10 ++++++++-- pkg/agent/pipeline_llm.go | 9 +++++++-- pkg/agent/turn_coord.go | 30 +++++++++++++++++++++++------- pkg/agent/turn_state.go | 4 ++++ 5 files changed, 42 insertions(+), 29 deletions(-) diff --git a/pkg/agent/agent_event.go b/pkg/agent/agent_event.go index 510c339c1..9b8625df1 100644 --- a/pkg/agent/agent_event.go +++ b/pkg/agent/agent_event.go @@ -48,24 +48,6 @@ func (al *AgentLoop) emitEvent(kind EventKind, meta EventMeta, payload any) { al.eventBus.Emit(evt) } -func (al *AgentLoop) hookAbortError(ts *turnState, stage string, decision HookDecision) error { - reason := decision.Reason - if reason == "" { - reason = "hook requested turn abort" - } - - err := fmt.Errorf("hook aborted turn during %s: %s", stage, reason) - al.emitEvent( - EventKindError, - ts.eventMeta("hooks", "turn.error"), - ErrorPayload{ - Stage: "hook." + stage, - Message: err.Error(), - }, - ) - return err -} - func (al *AgentLoop) logEvent(evt Event) { fields := map[string]any{ "event_kind": evt.Kind.String(), diff --git a/pkg/agent/pipeline_execute.go b/pkg/agent/pipeline_execute.go index 0cf622971..76ada0e64 100644 --- a/pkg/agent/pipeline_execute.go +++ b/pkg/agent/pipeline_execute.go @@ -37,6 +37,7 @@ func (p *Pipeline) ExecuteTools( toolLoop: for i, tc := range normalizedToolCalls { if ts.hardAbortRequested() { + exec.abortedByHardAbort = true return ToolControlBreak } @@ -288,10 +289,11 @@ toolLoop: } continue case HookActionAbortTurn: - _ = ts.requestHardAbort() + exec.abortedByHook = true return ToolControlBreak case HookActionHardAbort: _ = ts.requestHardAbort() + exec.abortedByHardAbort = true return ToolControlBreak } } @@ -426,6 +428,7 @@ toolLoop: toolDuration := time.Since(toolStart) if ts.hardAbortRequested() { + exec.abortedByHardAbort = true return ToolControlBreak } @@ -449,10 +452,11 @@ toolLoop: } } case HookActionAbortTurn: - _ = ts.requestHardAbort() + exec.abortedByHook = true return ToolControlBreak case HookActionHardAbort: _ = ts.requestHardAbort() + exec.abortedByHardAbort = true return ToolControlBreak } } @@ -633,6 +637,7 @@ toolLoop: "pending_count": len(exec.pendingMessages), "allResponsesHandled": exec.allResponsesHandled, }) + exec.allResponsesHandled = false return ToolControlContinue } @@ -644,6 +649,7 @@ toolLoop: "steering_count": len(steerMsgs), }) exec.pendingMessages = append(exec.pendingMessages, steerMsgs...) + exec.allResponsesHandled = false return ToolControlContinue } diff --git a/pkg/agent/pipeline_llm.go b/pkg/agent/pipeline_llm.go index c7ef3353c..c426c25c9 100644 --- a/pkg/agent/pipeline_llm.go +++ b/pkg/agent/pipeline_llm.go @@ -104,9 +104,11 @@ func (p *Pipeline) CallLLM( exec.llmOpts = llmReq.Options } case HookActionAbortTurn: - return ControlBreak, al.hookAbortError(ts, "before_llm", decision) + exec.abortedByHook = true + return ControlBreak, nil case HookActionHardAbort: _ = ts.requestHardAbort() + exec.abortedByHardAbort = true return ControlBreak, nil } } @@ -191,6 +193,7 @@ func (p *Pipeline) CallLLM( } if ts.hardAbortRequested() && errors.Is(err, context.Canceled) { _ = ts.requestHardAbort() + exec.abortedByHardAbort = true return ControlBreak, nil } @@ -364,9 +367,11 @@ func (p *Pipeline) CallLLM( exec.response = llmResp.Response } case HookActionAbortTurn: - return ControlBreak, al.hookAbortError(ts, "after_llm", decision) + exec.abortedByHook = true + return ControlBreak, nil case HookActionHardAbort: _ = ts.requestHardAbort() + exec.abortedByHardAbort = true return ControlBreak, nil } } diff --git a/pkg/agent/turn_coord.go b/pkg/agent/turn_coord.go index 8a9ca82d3..4c8335933 100644 --- a/pkg/agent/turn_coord.go +++ b/pkg/agent/turn_coord.go @@ -147,6 +147,9 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState, pipeline *Pipel TotalContentLen: totalContentLen, }, ) + // Clear exec.pendingMessages after injection so InitialSteeringMessages + // are not re-injected on subsequent iterations (Issue 2 fix). + exec.pendingMessages = nil } // Always sync messages into exec.messages so CallLLM sees the updated state exec.messages = messages @@ -169,12 +172,20 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState, pipeline *Pipel pendingMessages = exec.pendingMessages finalContent = exec.finalContent - logger.InfoCF("agent", "SWITCH ctrl", map[string]any{"ctrl": int(ctrl), "iter": iteration}) switch ctrl { case ControlContinue: - logger.InfoCF("agent", "CASE ControlContinue", map[string]any{"iter": iteration}) continue case ControlBreak: + // Hard abort: delegate to abortTurn (sets TurnEndStatusAborted) + if exec.abortedByHardAbort { + turnStatus = TurnEndStatusAborted + return al.abortTurn(ts) + } + // Hook abort (HookActionAbortTurn): sets TurnEndStatusError, returns error + if exec.abortedByHook { + turnStatus = TurnEndStatusError + return turnResult{}, fmt.Errorf("hook requested turn abort") + } // Ensure empty response falls back to DefaultResponse if finalContent == "" { finalContent = ts.opts.DefaultResponse @@ -190,17 +201,22 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState, pipeline *Pipel messages = exec.messages continue case ToolControlBreak: + // Hard abort: delegate to abortTurn (sets TurnEndStatusAborted) + if exec.abortedByHardAbort { + turnStatus = TurnEndStatusAborted + return al.abortTurn(ts) + } + // Hook abort (HookActionAbortTurn): sets TurnEndStatusError, returns error + if exec.abortedByHook { + turnStatus = TurnEndStatusError + return turnResult{}, fmt.Errorf("hook requested turn abort") + } // ExecuteTools returned ControlBreak: // - allResponsesHandled=true: finalize without DefaultResponse (exec.finalContent empty) // - allResponsesHandled=false: coordinator applies DefaultResponse before finalize if exec.allResponsesHandled { finalContent = "" } - // Check hard abort after tool execution (may have been set during ExecuteTools) - if ts.hardAbortRequested() { - turnStatus = TurnEndStatusAborted - return al.abortTurn(ts) - } return pipeline.Finalize(ctx, turnCtx, ts, exec, turnStatus, finalContent) } } diff --git a/pkg/agent/turn_state.go b/pkg/agent/turn_state.go index f2616cda1..edf8654b5 100644 --- a/pkg/agent/turn_state.go +++ b/pkg/agent/turn_state.go @@ -141,6 +141,10 @@ type turnExecution struct { // Phase tracking phase LLMPhase + + // Abort signaling for coordinator (set by Pipeline methods) + abortedByHardAbort bool // true when hard abort triggered during LLM/tools + abortedByHook bool // true when HookActionAbortTurn triggered } // newTurnExecution creates a turnExecution initialized from turnState and options.