Commit 1b55c515da44
Changed files (1)
dots
pi
agent
extensions
vertex-claude
dots/pi/agent/extensions/vertex-claude/index.ts
@@ -344,16 +344,63 @@ export function mapStopReason(reason: string): StopReason {
}
}
+// Escape control characters that are invalid inside JSON string literals.
+// The Anthropic API sometimes emits literal tabs/newlines inside tool call
+// argument strings (e.g. when the model copies Go or Makefile indentation).
+// JSON.parse rejects these with "Bad control character in string literal".
+function escapeControlCharsInJsonStrings(json: string): string {
+ let result = "";
+ let inString = false;
+ let escape = false;
+ for (let i = 0; i < json.length; i++) {
+ const ch = json[i];
+ if (escape) {
+ result += ch;
+ escape = false;
+ continue;
+ }
+ if (ch === "\\" && inString) {
+ result += ch;
+ escape = true;
+ continue;
+ }
+ if (ch === '"') {
+ inString = !inString;
+ result += ch;
+ continue;
+ }
+ if (inString) {
+ const code = ch.charCodeAt(0);
+ if (code < 0x20) {
+ // Replace control chars with their JSON escape sequences
+ switch (code) {
+ case 0x09: result += "\\t"; break;
+ case 0x0a: result += "\\n"; break;
+ case 0x0d: result += "\\r"; break;
+ case 0x08: result += "\\b"; break;
+ case 0x0c: result += "\\f"; break;
+ default: result += "\\u" + code.toString(16).padStart(4, "0"); break;
+ }
+ continue;
+ }
+ }
+ result += ch;
+ }
+ return result;
+}
+
// Streaming JSON parser for tool arguments
export function parseStreamingJson(partialJson: string): Record<string, any> {
if (!partialJson || partialJson.trim() === "") {
return {};
}
+ // Escape bare control characters that the model may emit inside strings
+ const sanitized = escapeControlCharsInJsonStrings(partialJson);
try {
- return JSON.parse(partialJson);
+ return JSON.parse(sanitized);
} catch {
try {
- return partialParse(partialJson) ?? {};
+ return partialParse(sanitized) ?? {};
} catch {
return {};
}