toolTargetKey is the stable identity used to detect a repeated-failure loop: tool name + its target (the path for file tools, the command's first line for bash). Deliberately NOT the full argument set: a full-args key is defeated by any cosmetic change between retries (a regenerated file body, a rew
(call chmctx.ToolCall)
| 1084 | // deterministically swallows the call can't loop. If it persists, surface it |
| 1085 | // rather than dying silently: the prior behaviour left a half-done artifact |
| 1086 | // with no banner at all. |
| 1087 | outcome := outcomeDone // a clean, non-empty finish; stall/leak below downgrade it |
| 1088 | if newestAssistantEmpty(m.history) { |
| 1089 | if !m.emptyNudged { |
| 1090 | m.emptyNudged = true |
| 1091 | dbgWritef("nudge", "empty-reply nudge injected (turn ended with no content and no tool call)") |
| 1092 | m.history = append(m.history, chmctx.Message{ |
| 1093 | Role: chmctx.RoleSystem, |
| 1094 | Content: nudgeOrigin + "Your last turn ended with no reply and no tool call. If you meant to call a tool and it did not run, issue it again now as a proper tool call. If you are still working, continue. If the task is done, check it against the original request - actually run or drive what proves it works - then reply with a one-line summary.", |
| 1095 | }) |
| 1096 | m.phase = phaseThinking |
| 1097 | return m, m.startChat() |
| 1098 | } |
| 1099 | m.appendLine(styleError.Render("⚠ the model ended its turn with no reply and no tool call - it stalled, or your server dropped the call. If thinking is on, its reasoning parser may be swallowing calls - enable one (e.g. vLLM `--reasoning-parser`) or disable thinking for tool turns.")) |
| 1100 | dbgWritef("leak", "turn ended with an empty assistant message after a re-prompt (model stalled or the call was swallowed server-side)") |
| 1101 | outcome = outcomeStopped |
| 1102 | } else if w := toolCallLeakWarning(m.history); w != "" { |
no outgoing calls