Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 42 additions & 12 deletions src/__tests__/bugfixes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,30 +86,60 @@ describe('Session timeout fix', () => {
// Fix 3: Agent loop continuation nudge
// ---------------------------------------------------------------------------
describe('Agent loop continuation nudge', () => {
test('query.ts has continuation signal detection', async () => {
test('continuation logic has been moved to utility', async () => {
const content = await file('query.ts').text()

expect(content).toContain('continuationSignals')
expect(content).toContain('Continuation nudge triggered')
expect(content).toContain('continuation_nudge')
// query.ts should now call the utility
expect(content).toContain('analyzeContinuationIntent')
})

test('continuation signals include tightened patterns', async () => {
const content = await file('query.ts').text()
test('continuation.ts has robust patterns', async () => {
const content = await file('utils/continuation.ts').text()

expect(content).toContain('CONTINUATION_SIGNALS')
expect(content).toContain('COMPLETION_MARKERS')
// Should detect tightened patterns requiring explicit action verbs
expect(content).toMatch(/so now \(i\|let me\|we\)/)
expect(content).toContain('completionMarkers')
expect(content).toContain('MAX_CONTINUATION_NUDGES')
// Verify the nudge counter guard exists
expect(content).toMatch(/continuationNudgeCount\s*<\s*MAX_CONTINUATION_NUDGES/)
})

test('analyzeContinuationIntent behavior follows project standards', async () => {
const { analyzeContinuationIntent } = await import('../utils/continuation.js')

// Transition intent detected (requires explicit action verb or transition phrase)
expect(analyzeContinuationIntent("So now I will start task 2").shouldNudge).toBe(true)
expect(analyzeContinuationIntent("I will now do the following").shouldNudge).toBe(true)

// Completion marker suppresses nudge
expect(analyzeContinuationIntent("Task finished").shouldNudge).toBe(false)

// Punctuation-less completion suppresses nudge (Reviewer Feedback)
expect(analyzeContinuationIntent("The analysis is complete and no code changes are needed here").shouldNudge).toBe(false)
expect(analyzeContinuationIntent("I changed package.json and src/query.ts and added tests").shouldNudge).toBe(false)
expect(analyzeContinuationIntent("Updated src/query.ts and added coverage in bugfixes.test.ts").shouldNudge).toBe(false)
expect(analyzeContinuationIntent("This should be ready after the latest test updates").shouldNudge).toBe(false)

// Mixed Intent: Late continuation survives earlier completion (Reviewer Feedback)
expect(analyzeContinuationIntent("Task 1 is done. Let me update the status.").shouldNudge).toBe(true)
expect(analyzeContinuationIntent("Task 1 finished. I will now run tests.").shouldNudge).toBe(true)
expect(analyzeContinuationIntent("Analysis complete. Now I will edit src/query.ts").shouldNudge).toBe(true)
expect(analyzeContinuationIntent("No issues in the first file. I will now inspect the next one.").shouldNudge).toBe(true)

// Structural truncation survives earlier completion (Reviewer Feedback)
expect(analyzeContinuationIntent("Setup is complete. Here is the code:\n```typescript\nfunction run() {").shouldNudge).toBe(true)
expect(analyzeContinuationIntent("Task complete. Please inspect (src/query.ts").shouldNudge).toBe(true)
expect(analyzeContinuationIntent("The analysis is done and now I am editing files and").shouldNudge).toBe(true)

// Structural truncation detection (Supreme Logic)
expect(analyzeContinuationIntent("I am currently updating the following files and").shouldNudge).toBe(true)
expect(analyzeContinuationIntent("Please check the results in (src/query.ts").shouldNudge).toBe(true)
expect(analyzeContinuationIntent("The plan is as follows:").shouldNudge).toBe(true)
expect(analyzeContinuationIntent("Here is the code:\n```typescript\nfunction test() {").shouldNudge).toBe(true)
})

test('nudge creates a meta user message to continue', async () => {
const content = await file('query.ts').text()

expect(content).toContain(
'Continue with the task. Use the appropriate tools to proceed.',
'Continue with the task. If you were interrupted, resume your thought. Otherwise, use the appropriate tools to proceed to the next step.',
)
})
})
Expand Down
2 changes: 2 additions & 0 deletions src/integrations/models/openai-compatible-alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const aliasModels: readonly AliasModel[] = [
['google/gemma-3-27b-it', 'Google Gemma 3 27B IT', 131_072, 16_384],
['google/gemma-3-12b-it', 'Google Gemma 3 12B IT', 131_072, 16_384],
['google/gemma-3-4b-it', 'Google Gemma 3 4B IT', 131_072],
['google/gemma-4-31b-it', 'Google Gemma 4 31B IT', 262_144, 16_384],
['deepseek-ai/deepseek-r1', 'DeepSeek R1', 128_000, 32_768],
['deepseek-ai/deepseek-v3', 'DeepSeek V3', 128_000, 32_768],
['deepseek-ai/deepseek-v3.2', 'DeepSeek V3.2', 128_000, 32_768],
Expand Down Expand Up @@ -115,6 +116,7 @@ const aliasModels: readonly AliasModel[] = [
['deepseek-r1:14b', 'DeepSeek R1 14B', 65_536, 8_192],
['phi4:14b', 'Phi 4 14B', 16_384, 4_096],
['gemma2:27b', 'Gemma 2 27B', 8_192, 4_096],
['gemma4:31b', 'Gemma 4 31B', 262_144, 16_384],
['codellama:13b', 'Code Llama 13B', 16_384, 4_096],
['qwen3:8b', 'Qwen 3 8B', 128_000, 8_192],
['qwen3-max-2026-01-23', 'Qwen 3 Max 2026-01-23', 262_144, 32_768],
Expand Down
37 changes: 9 additions & 28 deletions src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
createToolUseSummaryMessage,
createMicrocompactBoundaryMessage,
} from './utils/messages.js'
import { analyzeContinuationIntent } from './utils/continuation.js'
import { generateToolUseSummary } from './services/toolUseSummary/toolUseSummaryGenerator.js'
import { prependUserContext, appendSystemContext } from './utils/api.js'
import {
Expand Down Expand Up @@ -1459,37 +1460,17 @@ async function* queryLoop(
.join(' ')
.toLowerCase()

// Tightened patterns: require explicit action verbs and exclude
// common explanatory phrasing to reduce false positives.
const continuationSignals = [
// Only match "so now I/let me/we" followed by an action verb
/\bso now (i|let me|we) (need to|have to|should|must|will) (do|create|write|edit|update|fix|implement|add|run|check|make|build|set up)\b/,
// "now I'll" + action (not "now I'll explain" etc.)
/\bnow i('ll| will) (do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|go|proceed)\b/,
// "let me" + action (not "let me think/explain/show")
/\blet me (go ahead and |now )?(do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|proceed)\b/,
// "I'll/I need to/I have to" + action, only if message is short (<80 chars)
...(lastText.length < 80
? [/\b(i('ll| will| need to| have to| must) (now )?(do|create|write|edit|update|fix|implement|add|run|check|make|build|set up))\b/]
: []),
// "time to" + action
/\btime to (do|create|write|edit|update|fix|implement|add|run|check|make|build|get started|begin)\b/,
// "next, I'll/let me" + action, only if message is short
...(lastText.length < 80
? [/\bnext,?\s+(i('ll| will)|let me|i need to) (do|create|write|edit|update|fix|implement|add|run|check|make|build)\b/]
: []),
]

// Don't nudge if the text contains completion markers
const completionMarkers = /\b(done|finished|completed|complete|summary|that's all|that is all|all set|hope this helps|let me know if)\b/
if (completionMarkers.test(lastText)) {
// Model signaled completion — don't nudge
} else if (continuationSignals.some(re => re.test(lastText))) {
const { shouldNudge, reason: nudgeReason } = analyzeContinuationIntent(
lastText,
)

if (shouldNudge) {
logForDebugging(
`Continuation nudge triggered (${state.continuationNudgeCount + 1}/${MAX_CONTINUATION_NUDGES}): model said "${lastText.slice(-120)}" without tool calls`,
`Continuation nudge triggered (${state.continuationNudgeCount + 1}/${MAX_CONTINUATION_NUDGES}): ${nudgeReason} detected in "${lastText.slice(-120)}" without tool calls`,
)
const nudge = createUserMessage({
content: 'Continue with the task. Use the appropriate tools to proceed.',
content:
'Continue with the task. If you were interrupted, resume your thought. Otherwise, use the appropriate tools to proceed to the next step.',
isMeta: true,
})
const next: State = {
Expand Down
128 changes: 128 additions & 0 deletions src/utils/continuation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { tokenCountWithEstimation } from './tokens.js'

/**
* Heuristics to detect if the agent intends to continue its task
* but stopped (potentially due to truncation or missed tool calls).
*/

export const CONTINUATION_SIGNALS = [
// English: Action-transition phrases (requires intent + action)
/\bso now (i|let me|we) (need to|have to|should|must|will) (do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|start|begin|apply|identify|inspect|analyze|review|search)\b/i,
/\bnow i('ll| will) (do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|go|proceed|start|begin|apply|identify|inspect|analyze|review|search)\b/i,
/\bi (will|shall|now|need to|have to|must|should) (now )?(do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|go|proceed|start|begin|apply|identify|inspect|analyze|review|search)\b/i,
/\blet me (go ahead and |now )?(do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|proceed|start|begin|apply|update|create|identify|inspect|analyze|review|search|summarize)\b/i,
/\btime to (do|create|write|edit|update|fix|implement|add|run|check|make|build|get started|begin|start|inspect|analyze|review|search)\b/i,
/\b(moving on to|next step is to|starting to|proceeding to|applying (the|these) changes|inspecting|analyzing|reviewing|searching)\b/i,
// French: Support for common continuation phrasing (relaxed boundaries for accents and apostrophes)
/(^|\s)(je passe (à|au)|ensuite|l'étape suivante est de|je continue avec|au suivant|passons à|je reviens vers vous|je suis en train d'|je vais maintenant)(\s|$|[a-zà-ÿ])/i,
/(^|\s)(je (vais|dois|dois maintenant|vais maintenant) (faire|créer|écrire|modifier|ajouter|tester|vérifier|lancer|exécuter|procéder|démarrer|commencer|identifier|analyser|inspecter|revoir|chercher))(\s|$|[a-zà-ÿ])/i,
/(^|\s)((lancement|exécution|vérification|modification|mise à jour|analyse|inspection|recherche) de)(\s|$|[a-zà-ÿ])/i,
// Universal: Sentence ending with a colon indicates intent to list/act
/:\s*$/,
// Universal: Open task marker indicates pending work
/◻/,
]

export const COMPLETION_MARKERS = /\b(done|finished|completed|complete|summary|that's all|that is all|all set|hope this helps|let me know if|no issues|lgtm)\b/i

export type ContinuationResult = {
shouldNudge: boolean
reason?: 'possible_truncation' | 'continuation_signal'
}

export const UNFINISHED_SENTIMENT_SIGNALS = [
// English trailing connectors
/\b(and|with|the|to|of|for|at|by|in|on|a|an|is|are|was|were|my|your|his|her|its|our|their|if|as|but|or|so|which|that)\s*$/i,
// French trailing connectors
/\b(et|avec|le|la|les|un|une|de|du|des|pour|au|aux|dans|sur|par|à|en|si|car|mais|ou|donc|ni|que|ce|ma|ta|sa|mes|tes|ses|notre|votre|leur|nos|vos|leurs)\s*$/i,
// Trailing non-terminal punctuation
/[,;]\s*$/,
// Unclosed code block starter
/```[a-z]*\s*$/i,
]

/**
* Analyzes assistant text to determine if a continuation nudge is required.
*/
export function analyzeContinuationIntent(
text: string,
): ContinuationResult {
const lastText = text.trim()
if (lastText.length === 0) return { shouldNudge: false }

const lowerText = lastText.toLowerCase()

// 1. High-Confidence Structural Truncation signals (Strongest - Ignore completion markers)

// Check for unclosed markdown code blocks
const codeBlockCount = (lastText.match(/```/g) || []).length
const hasUnclosedCodeBlock = codeBlockCount % 2 !== 0

// Check for unclosed structural elements (brackets, parens, braces)
const unclosedPairs = [['(', ')'], ['[', ']'], ['{', '}']]
const hasUnclosedPair = unclosedPairs.some(([open, close]) => {
const openCount = (lastText.match(new RegExp('\\' + open, 'g')) || []).length
const closeCount = (lastText.match(new RegExp('\\' + close, 'g')) || []).length
return openCount > closeCount
})

// Check for trailing connectors (e.g., "... and", "... with")
const hasUnfinishedSuffix = UNFINISHED_SENTIMENT_SIGNALS.some(re => re.test(lastText))

if (hasUnclosedCodeBlock || hasUnclosedPair || hasUnfinishedSuffix) {
// Structural cut-offs always trigger a nudge, even if "done" was said earlier.
return { shouldNudge: true, reason: 'possible_truncation' }
}

// 2. Late Intent-based signals (Overriding earlier completion markers)

// Check if continuation signals match in the last 120 characters
const lateWindowSize = 120
const lateText = lowerText.slice(-lateWindowSize)

const hasLateContinuationSignal = CONTINUATION_SIGNALS.some(re => {
const match = lateText.match(re)
if (!match) return false

// Check if any completion marker follows THIS specific continuation signal in the late window
const afterMatch = lateText.slice(match.index! + match[0].length)
const hasLaterCompletion = COMPLETION_MARKERS.test(afterMatch)

// Very strong action intents (I will now, Let me, Je vais) override any later markers
const strongAction = /\b(let me|i will|i'll|je vais|je suis en train)\b/i.test(match[0])

return strongAction || !hasLaterCompletion
})

if (hasLateContinuationSignal) {
// If the sentence is punctuated but has a transition word, only nudge if
// it's a strong 1st person intent or open tasks are present.
const hasTerminalPunctuation = /[.!??"'`)\]]\s*$/.test(lastText) || lastText.endsWith('`')
if (hasTerminalPunctuation) {
const strongIntent = /\b(i (will|shall|need to|must|should|now)|let (me|us)|je (vais|reviens)|passons à|moving on to|next step is to)\b/i.test(lowerText) ||
/je suis en train d'/i.test(lowerText) || /◻/.test(lastText)
const endsWithColon = /:\s*$/.test(lastText)
if (strongIntent || endsWithColon) {
return { shouldNudge: true, reason: 'continuation_signal' }
}
} else {
return { shouldNudge: true, reason: 'continuation_signal' }
}
}

// 3. Completion Marker Guard (Final check for sound, completed messages)
if (COMPLETION_MARKERS.test(lowerText)) {
return { shouldNudge: false }
}

// Global fallback for unpunctuated signals (must be a clear transition)
const hasTerminalPunctuation = /[.!??"'`)\]]\s*$/.test(lastText) || lastText.endsWith('`')
if (
CONTINUATION_SIGNALS.some(re => re.test(lowerText)) &&
!hasTerminalPunctuation
) {
return { shouldNudge: true, reason: 'continuation_signal' }
}

return { shouldNudge: false }
}