import { serializeIntermediateSteps } from '../serializeIntermediateSteps'; describe('serializeIntermediateSteps', () => { it('should convert class instances with toJSON to plain objects', () => { const fakeAIMessage = { content: 'I need to call a tool', tool_calls: [{ name: 'TestTool', args: { input: 'test' }, id: 'call_123' }], additional_kwargs: {}, response_metadata: { model: 'gpt-4' }, id: 'msg_abc', name: undefined, toJSON() { return { lc: 1, type: 'constructor', id: ['langchain_core', 'messages', 'AIMessage'], kwargs: { content: this.content, tool_calls: this.tool_calls, additional_kwargs: this.additional_kwargs, }, }; }, }; const steps = [ { action: { tool: 'TestTool', toolInput: { input: 'test' }, log: 'Calling TestTool', messageLog: [fakeAIMessage], toolCallId: 'call_123', type: 'function', }, observation: 'Tool result', }, ]; serializeIntermediateSteps(steps); const serializedMsg = steps[0].action.messageLog[0] as Record; // Should be a plain object, not the original class instance expect(serializedMsg).not.toBe(fakeAIMessage); expect(typeof serializedMsg.toJSON).toBe('undefined'); // Direct property access should work expect(serializedMsg.content).toBe('I need to call a tool'); expect(serializedMsg.tool_calls).toEqual([ { name: 'TestTool', args: { input: 'test' }, id: 'call_123' }, ]); expect(serializedMsg.additional_kwargs).toEqual({}); expect(serializedMsg.response_metadata).toEqual({ model: 'gpt-4' }); expect(serializedMsg.id).toBe('msg_abc'); }); it('should leave plain objects unchanged', () => { const plainMsg = { content: 'Hello', tool_calls: [], }; const steps = [ { action: { tool: 'TestTool', toolInput: {}, log: '', messageLog: [plainMsg], toolCallId: 'call_1', type: 'function', }, }, ]; serializeIntermediateSteps(steps); // Should be the same reference since it has no toJSON expect(steps[0].action.messageLog[0]).toBe(plainMsg); }); it('should handle steps without messageLog', () => { const steps = [ { action: { tool: 'TestTool', toolInput: {}, log: '', toolCallId: 'call_1', type: 'function', }, }, ]; // Should not throw expect(() => serializeIntermediateSteps(steps as Array<{ action: { messageLog?: unknown[] } }>), ).not.toThrow(); }); it('should handle empty steps array', () => { const steps: Array<{ action: { messageLog?: unknown[] } }> = []; expect(() => serializeIntermediateSteps(steps)).not.toThrow(); }); it('should include type from _getType when not an own property', () => { const proto = { _getType() { return 'ai'; }, }; const fakeMsg = Object.create(proto) as Record; fakeMsg.content = 'test'; fakeMsg.toJSON = () => ({ lc: 1, type: 'constructor', kwargs: {} }); const steps = [ { action: { messageLog: [fakeMsg], }, }, ]; serializeIntermediateSteps(steps as Array<{ action: { messageLog?: unknown[] } }>); const serialized = steps[0].action.messageLog[0] as Record; expect(serialized.type).toBe('ai'); expect(serialized.content).toBe('test'); }); it('should handle mixed messageLog entries', () => { const classInstance = { content: 'from class', toJSON() { return { kwargs: { content: this.content } }; }, }; const plainObject = { content: 'from plain' }; const primitiveValue = 'just a string'; const steps = [ { action: { messageLog: [classInstance, plainObject, primitiveValue], }, }, ]; serializeIntermediateSteps(steps as Array<{ action: { messageLog?: unknown[] } }>); const [serializedClass, unchangedPlain, unchangedPrimitive] = steps[0].action.messageLog; // Class instance should be serialized expect(serializedClass).not.toBe(classInstance); expect((serializedClass as Record).content).toBe('from class'); expect(typeof (serializedClass as Record).toJSON).toBe('undefined'); // Plain object should be unchanged expect(unchangedPlain).toBe(plainObject); // Primitive should be unchanged expect(unchangedPrimitive).toBe(primitiveValue); }); });