🐛 fix: use partial-json fallback in ToolArgumentsRepairer to recover incomplete args (#13239)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arvin Xu
2026-03-25 13:50:34 +08:00
committed by GitHub
parent 9b9949befa
commit 056f390abc
2 changed files with 91 additions and 2 deletions

View File

@@ -1,3 +1,5 @@
import { parse as parsePartialJSON } from 'partial-json';
import type { LobeToolManifest } from './types';
/**
@@ -10,14 +12,20 @@ export interface ToolParameterSchema {
}
/**
* Safe JSON parse utility
* Safe JSON parse with partial JSON fallback.
* When strict JSON.parse fails (e.g. stream interrupted mid-arguments),
* falls back to partial-json to recover as many fields as possible.
*/
const safeParseJSON = <T = Record<string, unknown>>(text?: string): T | undefined => {
if (typeof text !== 'string') return undefined;
try {
return JSON.parse(text) as T;
} catch {
return undefined;
try {
return parsePartialJSON(text) as T;
} catch {
return undefined;
}
}
};

View File

@@ -183,4 +183,85 @@ describe('ToolArgumentsRepairer', () => {
expect(result).toEqual({});
});
});
describe('parse - partial JSON recovery (stream interruption)', () => {
it('should recover fields from incomplete JSON when stream is interrupted', () => {
const repairer = new ToolArgumentsRepairer();
// Simulates stream dropping after title, description, type were sent but before content
const incompleteArgs =
'{"title": "My Document", "description": "A brief summary", "type": "report"';
const result = repairer.parse('createDocument', incompleteArgs);
expect(result).toEqual({
title: 'My Document',
description: 'A brief summary',
type: 'report',
});
});
it('should recover fields when stream drops mid-value', () => {
const repairer = new ToolArgumentsRepairer();
// Stream drops in the middle of the content value
const incompleteArgs = '{"title": "My Document", "content": "This is the beginning of';
const result = repairer.parse('createDocument', incompleteArgs);
expect(result.title).toBe('My Document');
expect(result.content).toBe('This is the beginning of');
});
it('should recover when stream drops after first field', () => {
const repairer = new ToolArgumentsRepairer();
const incompleteArgs = '{"title": "My Document"';
const result = repairer.parse('createDocument', incompleteArgs);
expect(result).toEqual({ title: 'My Document' });
});
it('should return empty object when stream drops before any field value', () => {
const repairer = new ToolArgumentsRepairer();
const result = repairer.parse('createDocument', '{');
expect(result).toEqual({});
});
it('should recover partial JSON and still apply repair if needed', () => {
const manifest: LobeToolManifest = {
identifier: 'lobe-notebook',
api: [
{
name: 'createDocument',
description: 'Create a document',
parameters: {
type: 'object',
required: ['title', 'description', 'content'],
properties: {
title: { type: 'string' },
description: { type: 'string' },
content: { type: 'string' },
},
},
},
],
type: 'builtin',
} as unknown as LobeToolManifest;
const repairer = new ToolArgumentsRepairer(manifest);
// Stream interrupted - has title and description but no content
const incompleteArgs = '{"title": "Test", "description": "Summary"';
const result = repairer.parse('createDocument', incompleteArgs);
// Should recover available fields instead of returning {}
expect(result.title).toBe('Test');
expect(result.description).toBe('Summary');
});
});
});