(userMessage, config)
| 509 | } |
| 510 | |
| 511 | async function runAgentLoop(userMessage, config) { |
| 512 | // Reset early-stop state for new turn |
| 513 | earlyStop.newTurn(); |
| 514 | // Reset quality monitor's consecutive-correction window for the new turn. |
| 515 | qualityMonitor.reset(); |
| 516 | |
| 517 | // Reset per-turn idempotent-write dedup. PURE_TOOLS dedup uses a sliding |
| 518 | // window across turns; this set is *per-turn* and stops `memory_remember` |
| 519 | // (and friends) from being called with identical args repeatedly in a |
| 520 | // single turn. See src/tools/dedup.js for the full rationale. |
| 521 | try { |
| 522 | const { newTurnIdempotentWriteSet } = require('../src/tools/dedup'); |
| 523 | newTurnIdempotentWriteSet(); |
| 524 | } catch {} |
| 525 | |
| 526 | // Start trace recording for this turn |
| 527 | traceRecorder.start(userMessage, config.model.name); |
| 528 | |
| 529 | // Mark new turn in token monitor (next recordCall will start a new turn entry) |
| 530 | tokenMonitor._nextCallIsNewTurn = true; |
| 531 | |
| 532 | // Feature 3: rate limiting — assert within budget before starting turn |
| 533 | try { |
| 534 | if (assertWithinBudget) assertWithinBudget('run_turn', {}); |
| 535 | } catch (e) { |
| 536 | const msg = e.message || String(e); |
| 537 | if (_fullscreenRef) _fullscreenRef.addTool('policy', 'err', msg); |
| 538 | else console.log(` \x1b[33m⚠ ${msg}\x1b[0m`); |
| 539 | // Still proceed — rate limiting is advisory for local use |
| 540 | } |
| 541 | |
| 542 | // Clarification loop — detect vague prompts before wasting tool calls. |
| 543 | // MarrowScript Feature #1: uses compiled intent_clarifier (LLM-based, cached 30m) |
| 544 | // with automatic fallback to regex when the model is unavailable. |
| 545 | // Only fires on short messages (< 80 chars) — long messages are almost never vague |
| 546 | // and we don't want to add 2s latency to every detailed task description. |
| 547 | const { getClarificationInstruction } = require('../src/session/clarify'); |
| 548 | let _needsClarification = false; |
| 549 | // Skip clarifier when the message is clearly actionable even if short: |
| 550 | // - Looks like a file path (quoted, contains slash/backslash, has extension) |
| 551 | // - Pure number or "option N" / "work on N" — context-reference to prior options |
| 552 | // - Affirmation in continuation context (yes/ok/sure/proceed) |
| 553 | // - ROOT CAUSE FIX: assistant's last turn ended with a question — user reply is an answer, |
| 554 | // not a new task. Evaluate messages in context, not in isolation. |
| 555 | const lastAssistantMsg = [...conversationHistory].reverse().find(m => m.role === 'assistant'); |
| 556 | const assistantAskedQuestion = typeof lastAssistantMsg?.content === 'string' && lastAssistantMsg.content.trimEnd().endsWith('?'); |
| 557 | const looksLikePath = /[\\\/]|\.\w{1,5}\s*$|^["'].*["']$/.test(userMessage.trim()); |
| 558 | const looksLikeOptionRef = /^(option\s+\d|work\s+on\s+\d|do\s+\d|start\s+with\s+\d|^\d+\.?\s*$|first|second|third|fourth)\b/i.test(userMessage.trim()); |
| 559 | // Multi-number selection (e.g. "1 and 2", "1, 2", "both 1 and 2") |
| 560 | const looksLikeMultiSelect = /^(both\s+)?\d+(\s*,\s*|\s+and\s+)\d+$/i.test(userMessage.trim()); |
| 561 | const looksLikeAffirmation = ( |
| 562 | /^(yes|y|yep|yeah|sure|ok|okay|go|proceed|do it|continue|please|alright|👍|✅)\b\s*\.?\s*$/i.test(userMessage.trim()) || |
| 563 | // Multi-word continuations: "go ahead", "go ahead and read it", "read it", "do that", "that one" |
| 564 | /^(go ahead|go for it|just do it|do that|do both|read it|show me|that one|sounds good|let's do it|let's go|that works)\b/i.test(userMessage.trim()) |
| 565 | ); |
| 566 | if (userMessage.length < 80 && !assistantAskedQuestion && !looksLikePath && !looksLikeOptionRef && !looksLikeMultiSelect && !looksLikeAffirmation) { |
| 567 | try { |
| 568 | const { checkNeedsClarification } = require('./features_adapter'); |
no test coverage detected