fix(agent): fix agent hardAbortX
This commit is contained in:
parent
23d9b653f8
commit
bfa8c88565
5 changed files with 42 additions and 29 deletions
|
|
@ -48,24 +48,6 @@ func (al *AgentLoop) emitEvent(kind EventKind, meta EventMeta, payload any) {
|
||||||
al.eventBus.Emit(evt)
|
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) {
|
func (al *AgentLoop) logEvent(evt Event) {
|
||||||
fields := map[string]any{
|
fields := map[string]any{
|
||||||
"event_kind": evt.Kind.String(),
|
"event_kind": evt.Kind.String(),
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ func (p *Pipeline) ExecuteTools(
|
||||||
toolLoop:
|
toolLoop:
|
||||||
for i, tc := range normalizedToolCalls {
|
for i, tc := range normalizedToolCalls {
|
||||||
if ts.hardAbortRequested() {
|
if ts.hardAbortRequested() {
|
||||||
|
exec.abortedByHardAbort = true
|
||||||
return ToolControlBreak
|
return ToolControlBreak
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -288,10 +289,11 @@ toolLoop:
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
case HookActionAbortTurn:
|
case HookActionAbortTurn:
|
||||||
_ = ts.requestHardAbort()
|
exec.abortedByHook = true
|
||||||
return ToolControlBreak
|
return ToolControlBreak
|
||||||
case HookActionHardAbort:
|
case HookActionHardAbort:
|
||||||
_ = ts.requestHardAbort()
|
_ = ts.requestHardAbort()
|
||||||
|
exec.abortedByHardAbort = true
|
||||||
return ToolControlBreak
|
return ToolControlBreak
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -426,6 +428,7 @@ toolLoop:
|
||||||
toolDuration := time.Since(toolStart)
|
toolDuration := time.Since(toolStart)
|
||||||
|
|
||||||
if ts.hardAbortRequested() {
|
if ts.hardAbortRequested() {
|
||||||
|
exec.abortedByHardAbort = true
|
||||||
return ToolControlBreak
|
return ToolControlBreak
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -449,10 +452,11 @@ toolLoop:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case HookActionAbortTurn:
|
case HookActionAbortTurn:
|
||||||
_ = ts.requestHardAbort()
|
exec.abortedByHook = true
|
||||||
return ToolControlBreak
|
return ToolControlBreak
|
||||||
case HookActionHardAbort:
|
case HookActionHardAbort:
|
||||||
_ = ts.requestHardAbort()
|
_ = ts.requestHardAbort()
|
||||||
|
exec.abortedByHardAbort = true
|
||||||
return ToolControlBreak
|
return ToolControlBreak
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -633,6 +637,7 @@ toolLoop:
|
||||||
"pending_count": len(exec.pendingMessages),
|
"pending_count": len(exec.pendingMessages),
|
||||||
"allResponsesHandled": exec.allResponsesHandled,
|
"allResponsesHandled": exec.allResponsesHandled,
|
||||||
})
|
})
|
||||||
|
exec.allResponsesHandled = false
|
||||||
return ToolControlContinue
|
return ToolControlContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -644,6 +649,7 @@ toolLoop:
|
||||||
"steering_count": len(steerMsgs),
|
"steering_count": len(steerMsgs),
|
||||||
})
|
})
|
||||||
exec.pendingMessages = append(exec.pendingMessages, steerMsgs...)
|
exec.pendingMessages = append(exec.pendingMessages, steerMsgs...)
|
||||||
|
exec.allResponsesHandled = false
|
||||||
return ToolControlContinue
|
return ToolControlContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,9 +104,11 @@ func (p *Pipeline) CallLLM(
|
||||||
exec.llmOpts = llmReq.Options
|
exec.llmOpts = llmReq.Options
|
||||||
}
|
}
|
||||||
case HookActionAbortTurn:
|
case HookActionAbortTurn:
|
||||||
return ControlBreak, al.hookAbortError(ts, "before_llm", decision)
|
exec.abortedByHook = true
|
||||||
|
return ControlBreak, nil
|
||||||
case HookActionHardAbort:
|
case HookActionHardAbort:
|
||||||
_ = ts.requestHardAbort()
|
_ = ts.requestHardAbort()
|
||||||
|
exec.abortedByHardAbort = true
|
||||||
return ControlBreak, nil
|
return ControlBreak, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -191,6 +193,7 @@ func (p *Pipeline) CallLLM(
|
||||||
}
|
}
|
||||||
if ts.hardAbortRequested() && errors.Is(err, context.Canceled) {
|
if ts.hardAbortRequested() && errors.Is(err, context.Canceled) {
|
||||||
_ = ts.requestHardAbort()
|
_ = ts.requestHardAbort()
|
||||||
|
exec.abortedByHardAbort = true
|
||||||
return ControlBreak, nil
|
return ControlBreak, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -364,9 +367,11 @@ func (p *Pipeline) CallLLM(
|
||||||
exec.response = llmResp.Response
|
exec.response = llmResp.Response
|
||||||
}
|
}
|
||||||
case HookActionAbortTurn:
|
case HookActionAbortTurn:
|
||||||
return ControlBreak, al.hookAbortError(ts, "after_llm", decision)
|
exec.abortedByHook = true
|
||||||
|
return ControlBreak, nil
|
||||||
case HookActionHardAbort:
|
case HookActionHardAbort:
|
||||||
_ = ts.requestHardAbort()
|
_ = ts.requestHardAbort()
|
||||||
|
exec.abortedByHardAbort = true
|
||||||
return ControlBreak, nil
|
return ControlBreak, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,9 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState, pipeline *Pipel
|
||||||
TotalContentLen: totalContentLen,
|
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
|
// Always sync messages into exec.messages so CallLLM sees the updated state
|
||||||
exec.messages = messages
|
exec.messages = messages
|
||||||
|
|
@ -169,12 +172,20 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState, pipeline *Pipel
|
||||||
pendingMessages = exec.pendingMessages
|
pendingMessages = exec.pendingMessages
|
||||||
finalContent = exec.finalContent
|
finalContent = exec.finalContent
|
||||||
|
|
||||||
logger.InfoCF("agent", "SWITCH ctrl", map[string]any{"ctrl": int(ctrl), "iter": iteration})
|
|
||||||
switch ctrl {
|
switch ctrl {
|
||||||
case ControlContinue:
|
case ControlContinue:
|
||||||
logger.InfoCF("agent", "CASE ControlContinue", map[string]any{"iter": iteration})
|
|
||||||
continue
|
continue
|
||||||
case ControlBreak:
|
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
|
// Ensure empty response falls back to DefaultResponse
|
||||||
if finalContent == "" {
|
if finalContent == "" {
|
||||||
finalContent = ts.opts.DefaultResponse
|
finalContent = ts.opts.DefaultResponse
|
||||||
|
|
@ -190,17 +201,22 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState, pipeline *Pipel
|
||||||
messages = exec.messages
|
messages = exec.messages
|
||||||
continue
|
continue
|
||||||
case ToolControlBreak:
|
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:
|
// ExecuteTools returned ControlBreak:
|
||||||
// - allResponsesHandled=true: finalize without DefaultResponse (exec.finalContent empty)
|
// - allResponsesHandled=true: finalize without DefaultResponse (exec.finalContent empty)
|
||||||
// - allResponsesHandled=false: coordinator applies DefaultResponse before finalize
|
// - allResponsesHandled=false: coordinator applies DefaultResponse before finalize
|
||||||
if exec.allResponsesHandled {
|
if exec.allResponsesHandled {
|
||||||
finalContent = ""
|
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)
|
return pipeline.Finalize(ctx, turnCtx, ts, exec, turnStatus, finalContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,10 @@ type turnExecution struct {
|
||||||
|
|
||||||
// Phase tracking
|
// Phase tracking
|
||||||
phase LLMPhase
|
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.
|
// newTurnExecution creates a turnExecution initialized from turnState and options.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue