fix(agent): fix agent hardAbortX

This commit is contained in:
sky5454 2026-04-20 23:10:08 +08:00
parent 23d9b653f8
commit bfa8c88565
5 changed files with 42 additions and 29 deletions

View file

@ -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(),

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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.