Skip to content

Commit f5e7b6e

Browse files
committed
refactor: Simplify truncation detection in Claude stream service
- Remove complex truncation logic and replace with simpler checks - Check for unclosed code blocks - Check if generated code contains the required '..' terminator - Export CLAUDE_MODELS constant for reuse across modules - Remove unused consecutiveShortResponses tracking The simplified approach focuses on the two key indicators of incomplete Graffiticode programs: unclosed code blocks and missing '..' terminators.
1 parent 8ad1634 commit f5e7b6e

2 files changed

Lines changed: 26 additions & 119 deletions

File tree

src/lib/claude-stream-service.ts

Lines changed: 25 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010

1111
import axios from "axios";
12+
import { CLAUDE_MODELS } from "./code-generation-service";
1213

1314
interface StreamOptions {
1415
model?: string;
@@ -98,95 +99,6 @@ class ClaudeStreamParser {
9899
}
99100
}
100101

101-
/**
102-
* Check if the generated content appears to be truncated mid-generation
103-
* For Graffiticode:
104-
* - All valid programs end with .. (double dots)
105-
* - Code blocks must be properly closed with matching ```
106-
*/
107-
function appearsTruncated(content: string): boolean {
108-
if (!content || content.length === 0) return false;
109-
110-
const trimmed = content.trim();
111-
112-
// Check for incomplete code blocks (odd number of ```)
113-
const codeBlockCount = (content.match(/```/g) || []).length;
114-
if (codeBlockCount % 2 !== 0) {
115-
// Unclosed code block - definitely truncated
116-
return true;
117-
}
118-
119-
// If we have complete code blocks, check if ANY substantial code block ends with ..
120-
if (codeBlockCount > 0) {
121-
// Extract ALL code blocks
122-
const codeBlocks = content.split('```');
123-
if (codeBlocks.length >= 3 && codeBlocks.length % 2 === 1) {
124-
// Check each code block (they're at even indices: 1, 3, 5, etc.)
125-
for (let i = 1; i < codeBlocks.length; i += 2) {
126-
const blockContent = codeBlocks[i];
127-
// Remove language identifier if present (e.g., "javascript\n")
128-
const codeContent = blockContent.replace(/^[\w]*\n/, '').trim();
129-
130-
// Skip empty or very short code blocks (likely just snippets)
131-
if (codeContent.length < 10) continue;
132-
133-
// Check if this looks like a complete Graffiticode program
134-
// A complete program should have substantial content and end with ..
135-
const hasSubstantialContent =
136-
codeContent.includes('{') ||
137-
codeContent.includes('let') ||
138-
codeContent.includes('map') ||
139-
codeContent.includes('cells') ||
140-
codeContent.includes('columns');
141-
142-
if (hasSubstantialContent && codeContent.endsWith('..')) {
143-
// Found a complete Graffiticode program
144-
return false;
145-
} else if (hasSubstantialContent && !codeContent.endsWith('..')) {
146-
// Found a substantial code block that doesn't end with ..
147-
return true;
148-
}
149-
}
150-
151-
// If we only found small snippets or fragments, check the longest one
152-
let longestBlock = '';
153-
for (let i = 1; i < codeBlocks.length; i += 2) {
154-
const blockContent = codeBlocks[i].replace(/^[\w]*\n/, '').trim();
155-
if (blockContent.length > longestBlock.length) {
156-
longestBlock = blockContent;
157-
}
158-
}
159-
160-
if (longestBlock && !longestBlock.endsWith('..')) {
161-
return true;
162-
}
163-
}
164-
// If all code blocks are properly terminated, consider it complete
165-
return false;
166-
}
167-
168-
// Only check for truncation in non-code content if there are no code blocks at all
169-
// This handles cases where the response is pure text without code
170-
const lastLine = trimmed.split('\n').pop() || '';
171-
if (lastLine.length > 200) {
172-
return true;
173-
}
174-
175-
// Check for obvious truncation indicators at the very end
176-
const truncationIndicators = [
177-
/[^\\]["']$/, // Ends with unescaped quote
178-
/[\[{(,]$/, // Ends with opening bracket or comma
179-
/[=+\-*/<>!&|:]$/, // Ends with operator or colon
180-
];
181-
182-
for (const pattern of truncationIndicators) {
183-
if (pattern.test(trimmed)) {
184-
return true;
185-
}
186-
}
187-
188-
return false;
189-
}
190102

191103
/**
192104
* Stream code generation from Claude with automatic continuation
@@ -219,7 +131,6 @@ export async function* streamClaudeCode({
219131
let conversationHistory = [...messages];
220132
let fullContent = "";
221133
let totalUsage = { inputTokens: 0, outputTokens: 0 };
222-
let consecutiveShortResponses = 0; // Track short responses that might indicate completion
223134

224135
// Add initial user message if prompt provided
225136
if (prompt) {
@@ -236,7 +147,7 @@ export async function* streamClaudeCode({
236147
const response = await axios.post(
237148
"https://api.anthropic.com/v1/messages",
238149
{
239-
model: options.model || "claude-3-5-sonnet-20241022",
150+
model: options.model || CLAUDE_MODELS.DEFAULT,
240151
system: systemPrompt,
241152
messages: conversationHistory,
242153
max_tokens: options.maxTokens || 4096,
@@ -294,41 +205,37 @@ export async function* streamClaudeCode({
294205
return;
295206
}
296207

297-
// Primary check: Did the API signal completion naturally?
298-
const isTruncated = appearsTruncated(chunkContent);
208+
// Check for truncation:
209+
// 1. Odd number of ``` (unclosed block) OR
210+
// 2. No .. anywhere in the code
211+
let isTruncated = false;
212+
213+
const codeBlockCount = (fullContent.match(/```/g) || []).length;
214+
215+
if (codeBlockCount % 2 !== 0) {
216+
// Odd number of ``` means unclosed code block - definitely truncated
217+
isTruncated = true;
218+
} else if (codeBlockCount > 0) {
219+
// Even number of ``` - check if any code contains ..
220+
isTruncated = !fullContent.includes('..');
221+
}
222+
299223
console.log(
300224
"streamClaudeCode()",
301-
"isTruncated=" + isTruncated,
225+
"fullContent=" + fullContent,
226+
"codeBlockCount=" + codeBlockCount,
302227
"isComplete=" + isComplete,
228+
"chunkLength=" + chunkContent.length,
229+
"isTruncated=" + isTruncated,
303230
);
231+
232+
// If API says complete AND code ends with .., we're done
304233
if (isComplete && !isTruncated) {
305-
console.log(`Response completed naturally after ${continuationCount + 1} chunk(s)`);
234+
console.log(`Response completed after ${continuationCount + 1} chunk(s)`);
306235
break;
307236
}
308237

309-
// Debug logging
310-
if (isComplete && isTruncated) {
311-
console.log(`API signaled complete but content appears truncated`);
312-
}
313-
314-
// Check if response was cut off (likely hit token limit)
315-
const likelyHitTokenLimit = chunkContent.length >= (options.maxTokens || 4096) * 3;
316-
317-
// If response is very short and API signals complete, trust it
318-
if (chunkContent.length < 500 && isComplete && !isTruncated) {
319-
consecutiveShortResponses++;
320-
if (consecutiveShortResponses >= 2) {
321-
console.log(`Response appears complete (consecutive short responses)`);
322-
break;
323-
}
324-
} else {
325-
consecutiveShortResponses = 0;
326-
}
327-
328-
// Decide if we need continuation
329-
// Continue if:
330-
// 1. API didn't signal completion (!isComplete)
331-
// 2. Content appears truncated (isTruncated), regardless of why
238+
// Continue if API says not complete OR if code doesn't end with ..
332239
const needsContinuation = !isComplete || isTruncated;
333240

334241
if (needsContinuation) {

src/lib/code-generation-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
import { generateCodeWithContinuation } from "./claude-stream-service";
2525

2626
// Define available Claude models with best practices
27-
const CLAUDE_MODELS = {
27+
export const CLAUDE_MODELS = {
2828
OPUS: "claude-opus-4-20250514",
2929
SONNET: "claude-sonnet-4-20250514",
3030
HAIKU: "claude-3-5-haiku-20241022",

0 commit comments

Comments
 (0)