maybeVerifyNudge appends one re-grounding system note when a substantial turn (>= verifyNudgeMinRounds tool calls) is about to finish with a clean, non-empty reply, then latches so it fires at most once per turn. Returns true when it nudged, so the caller re-prompts and the model can verify before i
()
| 1225 | const maxToolFailStreak = 5 |
| 1226 | |
| 1227 | // toolTargetKey is the stable identity used to detect a repeated-failure loop: |
| 1228 | // tool name + its target (the path for file tools, the command's first line for |
| 1229 | // bash). Deliberately NOT the full argument set: a full-args key is defeated by |
| 1230 | // any cosmetic change between retries (a regenerated file body, a reworded |
| 1231 | // command). Keying on the target catches a model hammering the same operation |
| 1232 | // while leaving varied exploration alone. |
| 1233 | func toolTargetKey(call chmctx.ToolCall) string { |
| 1234 | switch call.Name { |
| 1235 | case tools.WriteFileName, tools.EditFileName, tools.ReadFileName: |
| 1236 | path, _ := call.Arguments["path"].(string) |
| 1237 | return call.Name + "|" + path |
| 1238 | case tools.BashName: |
| 1239 | cmd, _ := call.Arguments["cmd"].(string) |
| 1240 | if i := strings.IndexByte(cmd, '\n'); i >= 0 { |
| 1241 | cmd = cmd[:i] |
| 1242 | } |
| 1243 | return call.Name + "|" + strings.TrimSpace(cmd) |
| 1244 | } |
| 1245 | return call.Name |
| 1246 | } |
| 1247 | |
| 1248 | // toolResultFailed reports whether a tool result is an error the model should |
| 1249 | // react to. File tools wrap errors in parens ("(write error: ...)", "(not |
| 1250 | // found: ...)") and report success as plain text ("wrote N bytes"); bash |
| 1251 | // appends "(exit: N)" / "(timeout after ...)" on failure. A user Ctrl+C |
| 1252 | // ("(cancelled)") never counts as a failure. |