agent: convert Submit recursion to explicit loop

Post-turn hook continue, unconsumed pendingInject, and FIFO drain all
previously re-entered Submit recursively, growing the stack with each
continuation. Replace with an explicit loop in Submit and extract the
single-turn logic into executeTurn, which returns the next prompt or "".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-04-21 10:07:14 +02:00
parent 5569b3917b
commit 5ffda77972
1 changed files with 24 additions and 18 deletions

View File

@ -707,6 +707,9 @@ func firstSentence(s string) string {
// and shell shortcuts are dispatched immediately via handler; any other input
// starts an agent turn that streams events to handler. If a turn is already
// in progress the prompt is queued as an in-stream interruption instead.
//
// Continuations (post-turn hook context, unconsumed inject, FIFO drain) are
// handled via an explicit loop rather than recursion to avoid stack growth.
func (s *agentCore) Submit(ctx context.Context, input string, handler EventHandler) {
clog.Debug("Submit() input_len=%d running=%v", len(input), s.IsRunning())
if input == "" {
@ -715,12 +718,19 @@ func (s *agentCore) Submit(ctx context.Context, input string, handler EventHandl
if s.handleCommand(ctx, input, handler) {
return
}
if s.IsRunning() {
s.Inject(input)
return
}
for input != "" && ctx.Err() == nil {
input = s.executeTurn(ctx, input, handler)
}
}
// executeTurn runs a single agent turn and returns the next prompt to execute,
// or "" if there is nothing more to do.
func (s *agentCore) executeTurn(ctx context.Context, input string, handler EventHandler) string {
handler(Event{Role: "user", Content: input})
hookResult := s.hooks.Run(ctx, HookPreTurn, map[string]string{
@ -730,7 +740,7 @@ func (s *agentCore) Submit(ctx context.Context, input string, handler EventHandl
})
if hookResult.Blocked {
handler(infoEvent("hook blocked prompt"))
return
return ""
}
if hookResult.Context != "" {
input += "\n" + hookResult.Context
@ -825,27 +835,23 @@ func (s *agentCore) Submit(ctx context.Context, input string, handler EventHandl
s.saveSession()
// Stop hook said "continue" — re-enter with the hook's context as the prompt.
if stopResult.Blocked && stopResult.Context != "" && ctx.Err() == nil {
s.Submit(ctx, stopResult.Context, handler)
return
// Post-turn hook said "continue" — its context becomes the next prompt.
if stopResult.Blocked && stopResult.Context != "" {
return stopResult.Context
}
// If an inject was pending but never consumed (e.g. text-only response
// with no tool calls), treat it as the next user message.
if p := s.pendingInject.Swap(nil); p != nil && ctx.Err() == nil {
s.Submit(ctx, *p, handler)
return
// Inject that was pending but never consumed (text-only response with no
// tool calls) — treat it as the next user message.
if p := s.pendingInject.Swap(nil); p != nil {
return *p
}
// Drain the FIFO: run queued prompts sequentially.
for ctx.Err() == nil {
next, ok := s.fifo.Pop()
if !ok {
break
}
s.Submit(ctx, next, handler)
// Drain one item from the FIFO; the outer loop handles the rest.
if next, ok := s.fifo.Pop(); ok {
return next
}
return ""
}
func (s *agentCore) handleCommand(ctx context.Context, input string, handler EventHandler) bool {