(
tc: ToolCall,
transaction: Transaction,
sessionId: string,
options: AgentOptions,
)
| 2203 | for (const sm of skipped) newMessages.push(sm); |
| 2204 | sessionStore.recordTurn(sessionId, skipped, { input: 0, output: 0, costUsd: 0 }); |
| 2205 | } |
| 2206 | newMessages.push(stuckMsg); |
| 2207 | sessionStore.recordTurn(sessionId, [stuckMsg], { input: 0, output: 0, costUsd: 0 }); |
| 2208 | // CRITICAL: also clear recentCalls so the model gets one clean shot after the advice |
| 2209 | recentCalls.length = 0; |
| 2210 | continue; |
| 2211 | } |
| 2212 | |
| 2213 | // Error-guessing loop: same tool, same error kind, different args each time (e.g. reading |
| 2214 | // Header.tsx / App.tsx / Navbar.jsx — all FILE_NOT_FOUND — on a .jsx project). detectStuckLoop |
| 2215 | // misses this because the args differ. Nudge the model to STOP guessing and learn the real paths. |
| 2216 | // Scope guard: if the user never asked to run/install/test and the model is now starting a |
| 2217 | // dev server or installing packages on its own, nudge it once to finish the edits and ask |
| 2218 | // first. Advisory only (doesn't skip execution); the soft-failure loop detector is the real |
| 2219 | // circuit-breaker if the model ignores this and starts thrashing. |
| 2220 | if (!userAskedExecution && !scopeNudged) { |
| 2221 | const wandering = toolCalls.find(tc => |
| 2222 | isExecutionAction(tc.function.name, tc.function.arguments ?? ''), |
| 2223 | ); |
| 2224 | if (wandering) { |
| 2225 | scopeNudged = true; |
| 2226 | const smsg: Message = { |
| 2227 | role: 'user', |
| 2228 | content: |
| 2229 | '[SYSTEM] The user asked you to write/redesign code, not to run a dev server or install ' + |
| 2230 | 'packages. Finish and save the file changes, then report what you changed in a short ' + |
| 2231 | 'summary. Do NOT start dev servers, install dependencies, or debug the environment unless ' + |
| 2232 | 'the user explicitly asks — those steps are out of scope and waste time.', |
| 2233 | }; |
| 2234 | newMessages.push(smsg); |
| 2235 | sessionStore.recordTurn(sessionId, [smsg], { input: 0, output: 0, costUsd: 0 }); |
| 2236 | } |
| 2237 | } |
| 2238 | |
| 2239 | const errLoop = detectErrorLoop(recentErrors); |
| 2240 | if (errLoop) { |
| 2241 | let advice = ' Change approach instead of repeating the same kind of call.'; |
| 2242 | if (errLoop.code === 'FILE_NOT_FOUND') { |
| 2243 | advice = |
| 2244 | ' STOP guessing file paths. The "did you mean …" suggestions in the errors show the REAL ' + |
| 2245 | 'names — this project likely uses different extensions than you assume (e.g. .jsx, not .tsx). ' + |
| 2246 | 'Run `glob` or `ls` to get exact paths, then read those. Do not keep trying filename variations.'; |
| 2247 | } |
| 2248 | const msg: Message = { |
| 2249 | role: 'user', |
| 2250 | content: |
| 2251 | `[SYSTEM] \`${errLoop.name}\` has returned ${errLoop.code} ${errLoop.count} times with different ` + |
| 2252 | `arguments.${advice} If you genuinely can't proceed, say so in ONE sentence and stop. Do not loop.`, |
| 2253 | }; |
| 2254 | // Answer the just-emitted tool_calls before skipping execution, or the assistant's |
| 2255 | // tool_calls are orphaned and OpenAI-format providers 400 on the next request. |
| 2256 | if (toolCalls.length > 0) { |
| 2257 | const skipped = skippedToolResults(toolCalls, 'loop guard: same error kind repeated'); |
| 2258 | for (const sm of skipped) newMessages.push(sm); |
| 2259 | sessionStore.recordTurn(sessionId, skipped, { input: 0, output: 0, costUsd: 0 }); |
| 2260 | } |
| 2261 | newMessages.push(msg); |
| 2262 | sessionStore.recordTurn(sessionId, [msg], { input: 0, output: 0, costUsd: 0 }); |
no test coverage detected