diff --git a/packages/context-engine/src/engine/tools/ToolArgumentsRepairer.ts b/packages/context-engine/src/engine/tools/ToolArgumentsRepairer.ts index aa7c104d3e..8c3a6160ab 100644 --- a/packages/context-engine/src/engine/tools/ToolArgumentsRepairer.ts +++ b/packages/context-engine/src/engine/tools/ToolArgumentsRepairer.ts @@ -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 = >(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; + } } }; diff --git a/packages/context-engine/src/engine/tools/__tests__/ToolArgumentsRepairer.test.ts b/packages/context-engine/src/engine/tools/__tests__/ToolArgumentsRepairer.test.ts index 4a06527356..3d9c149b9c 100644 --- a/packages/context-engine/src/engine/tools/__tests__/ToolArgumentsRepairer.test.ts +++ b/packages/context-engine/src/engine/tools/__tests__/ToolArgumentsRepairer.test.ts @@ -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'); + }); + }); });