import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' process.env['LEON_NODE_ENV'] = 'testing' process.env['LEON_LLM'] = 'openai/gpt-5.4' /** * Hoisted mocks let the imported ReAct module capture the fake phase functions * and shared runtime singletons during module initialization. */ const phaseMocks = vi.hoisted(() => ({ buildCatalog: vi.fn(() => ({ text: 'mock catalog', mode: 'function' as const })), runPlanningPhase: vi.fn(), runRecoveryPlanningPhase: vi.fn(), runExecutionSelfObservationPhase: vi.fn(), runExecutionStep: vi.fn(), runFinalAnswerPhase: vi.fn() })) const coreMocks = vi.hoisted(() => ({ persona: { getCompactDutySystemPrompt: vi.fn((prompt: string) => prompt) }, toolkitRegistry: { isLoaded: true, load: vi.fn(), getFlattenedTools: vi.fn(() => [ { toolkitId: 'operating_system_control', toolkitName: 'Operating System Control', toolkitDescription: 'Control the operating system.', toolkitIconName: 'terminal', toolId: 'shell', toolName: 'Shell', toolDescription: 'Execute shell commands.', toolIconName: 'terminal' } ]), getToolFunctions: vi.fn((toolkitId: string, toolId: string) => { if (toolkitId !== 'operating_system_control' || toolId !== 'shell') { return null } return { executeCommand: { description: 'Execute a command in the active platform shell and return the result.', parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] } } } }), resolveToolById: vi.fn((toolId: string, toolkitId?: string) => { if ( toolId !== 'shell' || (toolkitId && toolkitId !== 'operating_system_control') ) { return null } return { toolkitId: 'operating_system_control', toolkitName: 'Operating System Control', toolkitIconName: 'terminal', toolId: 'shell', toolName: 'Shell', toolDescription: 'Execute shell commands.', toolIconName: 'terminal' } }), getToolkitContextFiles: vi.fn(() => []) }, contextManager: { isLoaded: true, load: vi.fn(), getContextFileContent: vi.fn(() => null), getManifest: vi.fn(() => '') }, selfModelManager: { getSnapshot: vi.fn(() => '') }, conversationLogger: { loadAll: vi.fn(async () => []) }, toolCallLogger: { getRecentArtifactManifest: vi.fn(() => '') }, postTurnMaintenanceQueue: { enqueue: vi.fn() }, llmProvider: { consumeLastProviderErrorMessage: vi.fn(() => null), prompt: vi.fn(), promptText: vi.fn(), promptWithTools: vi.fn() }, brain: { talk: vi.fn(async () => undefined), wernicke: vi.fn(() => ''), isMuted: true }, socket: { emit: vi.fn() } })) const widgetMocks = vi.hoisted(() => ({ emitPlanWidget: vi.fn(), widgetId: vi.fn(() => 'plan_test_widget') })) /** * The unit suite stays on the remote-provider path, so a lightweight session * stub is enough to satisfy the local-provider import surface. */ vi.mock('node-llama-cpp', () => ({ LlamaChatSession: class MockLlamaChatSession { setChatHistory(): void {} dispose(): void {} } })) vi.mock('@/helpers/log-helper', () => ({ LogHelper: { title: vi.fn(), success: vi.fn(), info: vi.fn(), debug: vi.fn(), warning: vi.fn(), error: vi.fn() } })) vi.mock('@/core', () => ({ LLM_MANAGER: { model: { createContext: vi.fn() } }, LLM_PROVIDER: coreMocks.llmProvider, PERSONA: coreMocks.persona, TOOLKIT_REGISTRY: coreMocks.toolkitRegistry, TOOL_EXECUTOR: { execute: vi.fn() }, CONTEXT_MANAGER: coreMocks.contextManager, SELF_MODEL_MANAGER: coreMocks.selfModelManager, CONVERSATION_LOGGER: coreMocks.conversationLogger, TOOL_CALL_LOGGER: coreMocks.toolCallLogger, BRAIN: coreMocks.brain, POST_TURN_MAINTENANCE_QUEUE: coreMocks.postTurnMaintenanceQueue, SOCKET_SERVER: { socket: coreMocks.socket, emitToChatClients: coreMocks.socket.emit, emitAnswerToChatClients: coreMocks.socket.emit, clearLiveWidgets: vi.fn() } })) vi.mock('@/core/llm-manager/llm-duties/react-llm-duty/phases', () => ({ buildCatalog: phaseMocks.buildCatalog, runPlanningPhase: phaseMocks.runPlanningPhase, runRecoveryPlanningPhase: phaseMocks.runRecoveryPlanningPhase, runExecutionSelfObservationPhase: phaseMocks.runExecutionSelfObservationPhase, runExecutionStep: phaseMocks.runExecutionStep, runFinalAnswerPhase: phaseMocks.runFinalAnswerPhase })) vi.mock('@/core/llm-manager/llm-duties/react-llm-duty/plan-widget', () => ({ emitPlanWidget: widgetMocks.emitPlanWidget, widgetId: widgetMocks.widgetId })) let ReActLLMDuty: typeof import('@/core/llm-manager/llm-duties/react-llm-duty').ReActLLMDuty let runFinalAnswerPhase: typeof import('@/core/llm-manager/llm-duties/react-llm-duty/final-answer').runFinalAnswerPhase let runPlanningPhaseDirect: typeof import('@/core/llm-manager/llm-duties/react-llm-duty/planning').runPlanningPhase let runExecutionStepDirect: typeof import('@/core/llm-manager/llm-duties/react-llm-duty/execution').runExecutionStep let runExecutionSelfObservationPhaseDirect: typeof import('@/core/llm-manager/llm-duties/react-llm-duty/execution').runExecutionSelfObservationPhase function createMessageLog( who: 'owner' | 'leon', message: string, sentAt: number ): { who: 'owner' | 'leon' message: string sentAt: number isAddedToHistory: true } { return { who, message, sentAt, isAddedToHistory: true } } function createCompactionState( overrides: Partial<{ summary: string | null summarySentAt: number | null tail: ReturnType[] newMessagesSinceCompaction: number }> = {} ): { summary: string | null summarySentAt: number | null tail: ReturnType[] newMessagesSinceCompaction: number } { return { summary: '- Previous topic', summarySentAt: 1, tail: [], newMessagesSinceCompaction: 0, ...overrides } } function logUnitProgress(message: string, data?: Record): void { const serializedData = data ? ` ${JSON.stringify(data)}` : '' console.info(`[agent:unit] ${message}${serializedData}`) } async function createDuty(input: string): Promise> { const duty = new ReActLLMDuty({ input }) /** * The suite focuses on loop orchestration, so history loading and post-answer * compaction are stubbed out to keep each case deterministic. */ vi.spyOn(duty as never, 'loadPreparedHistory' as never).mockResolvedValue({ messageLogs: [], localChatHistory: undefined }) vi.spyOn( duty as never, 'maybeCompactHistoryAfterAnswer' as never ).mockResolvedValue(undefined) await duty.init({ force: true }) return duty } beforeAll(async () => { ;({ ReActLLMDuty } = await import('@/core/llm-manager/llm-duties/react-llm-duty')) ;({ runFinalAnswerPhase } = await import( '@/core/llm-manager/llm-duties/react-llm-duty/final-answer' )) ;({ runPlanningPhase: runPlanningPhaseDirect } = await import( '@/core/llm-manager/llm-duties/react-llm-duty/planning' )) ;({ runExecutionStep: runExecutionStepDirect, runExecutionSelfObservationPhase: runExecutionSelfObservationPhaseDirect } = await import('@/core/llm-manager/llm-duties/react-llm-duty/execution')) }) beforeEach(() => { vi.clearAllMocks() phaseMocks.buildCatalog.mockReturnValue({ text: 'mock catalog', mode: 'function' }) phaseMocks.runRecoveryPlanningPhase.mockResolvedValue(null) phaseMocks.runExecutionSelfObservationPhase.mockResolvedValue(null) coreMocks.persona.getCompactDutySystemPrompt.mockImplementation( (prompt: string) => prompt ) coreMocks.contextManager.getContextFileContent.mockReturnValue(null) coreMocks.contextManager.getManifest.mockReturnValue('') coreMocks.selfModelManager.getSnapshot.mockReturnValue('') coreMocks.toolCallLogger.getRecentArtifactManifest.mockReturnValue('') coreMocks.llmProvider.consumeLastProviderErrorMessage.mockReturnValue(null) }) describe('ReActLLMDuty agent loop', () => { it('passes conversation history into execution tool argument selection', async () => { const history = [ createMessageLog( 'leon', 'Install a local Obsidian smooth cursor plugin named leon-smooth-cursor across the detected vaults.', 1 ), createMessageLog('owner', 'Then do it', 2) ] const callLLMWithTools = vi.fn(async () => ({ textContent: JSON.stringify({ type: 'handoff', intent: 'answer', draft: 'Execution context retained.' }) })) const caller = { callLLM: vi.fn(), callLLMText: vi.fn(), callLLMWithTools, supportsNativeTools: true, isLocalProvider: false, input: 'Then do it', history, agentSkillCatalog: '', setAgentSkillContext: vi.fn(), getAgentSkillContext: vi.fn(), getContextFileContent: vi.fn(() => null), getContextManifest: vi.fn(() => ''), getSelfModelSnapshot: vi.fn(() => ''), consumeProviderErrorMessage: vi.fn(() => null) } const result = await runExecutionStepDirect( caller, { function: 'functions.executeCommand', label: 'Install smooth cursor plugin' }, [], { text: 'mock catalog', mode: 'function' } ) expect(result).toEqual({ type: 'handoff', signal: { intent: 'answer', draft: 'Execution context retained.', source: 'execution' } }) expect(callLLMWithTools).toHaveBeenCalledOnce() expect(callLLMWithTools.mock.calls[0]?.[4]).toBe(history) }) it('passes conversation history into execution self-observation', async () => { const history = [ createMessageLog('leon', 'The next step is to write plugin files.', 1), createMessageLog('owner', 'Then do it', 2) ] const callLLM = vi.fn(async () => ({ output: { type: 'handoff', intent: 'answer', draft: 'No more steps needed.', functions: null, steps: null, reason: null } })) const caller = { callLLM, callLLMText: vi.fn(), callLLMWithTools: vi.fn(), supportsNativeTools: true, isLocalProvider: false, input: 'Then do it', history, agentSkillCatalog: '', setAgentSkillContext: vi.fn(), getAgentSkillContext: vi.fn(), getContextFileContent: vi.fn(() => null), getContextManifest: vi.fn(() => ''), getSelfModelSnapshot: vi.fn(() => ''), consumeProviderErrorMessage: vi.fn(() => null) } const result = await runExecutionSelfObservationPhaseDirect(caller, []) expect(result).toEqual({ type: 'handoff', signal: { intent: 'answer', draft: 'No more steps needed.', source: 'self_observation' } }) expect(callLLM).toHaveBeenCalledOnce() expect(callLLM.mock.calls[0]?.[3]).toBe(history) expect(callLLM.mock.calls[0]?.[5]).toMatchObject({ reasoningMode: 'off', streamToProvider: false }) }) it('lets final answer synthesis continue when self-observation fails', async () => { const consumeProviderErrorMessage = vi.fn( () => 'Provider timed out during self-observation.' ) const caller = { callLLM: vi.fn(async () => null), callLLMText: vi.fn(), callLLMWithTools: vi.fn(), supportsNativeTools: true, isLocalProvider: false, input: 'Summarize the transcript', history: [], agentSkillCatalog: '', setAgentSkillContext: vi.fn(), getAgentSkillContext: vi.fn(), getContextFileContent: vi.fn(() => null), getContextManifest: vi.fn(() => ''), getSelfModelSnapshot: vi.fn(() => ''), consumeProviderErrorMessage } const result = await runExecutionSelfObservationPhaseDirect(caller, [ { function: 'operating_system_control.file.read', status: 'success', observation: 'Transcript content was read.', stepLabel: 'Read transcript' } ]) expect(result).toBeNull() expect(consumeProviderErrorMessage).toHaveBeenCalledOnce() }) it('skips Agent Skill selection when no name or metadata match is present', async () => { const callLLMWithTools = vi.fn(async () => ({ toolCall: { functionName: 'create_plan', arguments: JSON.stringify({ type: 'final', answer: 'Acknowledge the thanks.', intent: 'answer' }) } })) const caller = { callLLM: vi.fn(), callLLMText: vi.fn(), callLLMWithTools, supportsNativeTools: true, isLocalProvider: false, input: 'Awesome, thanks', history: [], agentSkillCatalog: '1. tiny-web-crawler: Crawl from starting web pages, fetch readable content, search within pages, and follow relevant links.', setAgentSkillContext: vi.fn(), getAgentSkillContext: vi.fn(), getContextFileContent: vi.fn(() => null), getContextManifest: vi.fn(() => ''), getSelfModelSnapshot: vi.fn(() => ''), consumeProviderErrorMessage: vi.fn(() => null) } const result = await runPlanningPhaseDirect( caller, { text: 'mock catalog', mode: 'function' }, [] ) expect(callLLMWithTools).toHaveBeenCalledOnce() expect(callLLMWithTools.mock.calls[0]?.[2]?.[0]?.function.name).toBe( 'create_plan' ) expect(result).toEqual({ type: 'handoff', signal: { intent: 'answer', draft: 'Acknowledge the thanks.', source: 'planning' } }) }) it('routes local plain-text planning output to final answer handoff', async () => { const callLLM = vi.fn(async () => ({ output: 'Good morning. I am here and ready.' })) const caller = { callLLM, callLLMText: vi.fn(), callLLMWithTools: vi.fn(), supportsNativeTools: false, isLocalProvider: true, input: 'Good morning Leon', history: [], agentSkillCatalog: '', setAgentSkillContext: vi.fn(), getAgentSkillContext: vi.fn(), getContextFileContent: vi.fn(() => null), getContextManifest: vi.fn(() => ''), getSelfModelSnapshot: vi.fn(() => ''), consumeProviderErrorMessage: vi.fn(() => null) } const result = await runPlanningPhaseDirect( caller, { text: 'mock catalog', mode: 'function' }, [] ) expect(callLLM).toHaveBeenCalledOnce() expect(result).toEqual({ type: 'handoff', signal: { intent: 'answer', draft: 'Good morning. I am here and ready.', source: 'planning' } }) }) it('routes remote no-tool planning text to final answer handoff', async () => { const callLLMWithTools = vi.fn(async () => ({ textContent: 'Hey. I am here. What do you need?' })) const caller = { callLLM: vi.fn(), callLLMText: vi.fn(), callLLMWithTools, supportsNativeTools: true, isLocalProvider: false, input: 'Hi there', history: [], agentSkillCatalog: '', setAgentSkillContext: vi.fn(), getAgentSkillContext: vi.fn(), getContextFileContent: vi.fn(() => null), getContextManifest: vi.fn(() => ''), getSelfModelSnapshot: vi.fn(() => ''), consumeProviderErrorMessage: vi.fn(() => null) } const result = await runPlanningPhaseDirect( caller, { text: 'mock catalog', mode: 'function' }, [] ) expect(caller.callLLM).not.toHaveBeenCalled() expect(result).toEqual({ type: 'handoff', signal: { intent: 'answer', draft: 'Hey. I am here. What do you need?', source: 'planning' } }) }) it('recovers a local plan step that uses function_name', async () => { const callLLM = vi.fn(async () => ({ output: { type: 'plan', steps: [ { function_name: 'operating_system_control.shell.executeCommand', label: 'Run shell command' } ], summary: 'Running the shell command...', answer: null, intent: null } })) const caller = { callLLM, callLLMText: vi.fn(), callLLMWithTools: vi.fn(), supportsNativeTools: false, isLocalProvider: true, input: 'Run a command', history: [], agentSkillCatalog: '', setAgentSkillContext: vi.fn(), getAgentSkillContext: vi.fn(), getContextFileContent: vi.fn(() => null), getContextManifest: vi.fn(() => ''), getSelfModelSnapshot: vi.fn(() => ''), consumeProviderErrorMessage: vi.fn(() => null) } const result = await runPlanningPhaseDirect( caller, { text: '- operating_system_control.shell.executeCommand (command): Execute a command.', mode: 'function' }, [] ) expect(result).toEqual({ type: 'plan', steps: [ { function: 'operating_system_control.shell.executeCommand', label: 'Run shell command' } ], summary: 'Running the shell command...' }) }) it('recovers exact catalog function names from local planning text', async () => { const callLLM = vi.fn(async () => ({ output: 'I will use search_web.grok.search first, then operating_system_control.shell.executeCommand.' })) const caller = { callLLM, callLLMText: vi.fn(), callLLMWithTools: vi.fn(), supportsNativeTools: false, isLocalProvider: true, input: 'Search and run a command', history: [], agentSkillCatalog: '', setAgentSkillContext: vi.fn(), getAgentSkillContext: vi.fn(), getContextFileContent: vi.fn(() => null), getContextManifest: vi.fn(() => ''), getSelfModelSnapshot: vi.fn(() => ''), consumeProviderErrorMessage: vi.fn(() => null) } const result = await runPlanningPhaseDirect( caller, { text: [ '- search_web.grok.search (query): Search both web and X.', '- operating_system_control.shell.executeCommand (command): Execute a command.' ].join('\n'), mode: 'function' }, [] ) expect(result).toEqual({ type: 'plan', steps: [ { function: 'search_web.grok.search', label: 'Run search' }, { function: 'operating_system_control.shell.executeCommand', label: 'Run executeCommand' } ], summary: 'Running the recovered tool plan...' }) }) it('preserves an Agent Skill id selected in a plan step', async () => { const agentSkillContext = { id: 'tiny-web-crawler', name: 'tiny-web-crawler', description: 'Crawl from starting web pages.', rootPath: '/tmp/tiny-web-crawler', skillPath: '/tmp/tiny-web-crawler/SKILL.md', instructions: '# Tiny Web Crawler' } const callLLMWithTools = vi.fn().mockResolvedValueOnce({ toolCall: { functionName: 'create_plan', arguments: JSON.stringify({ type: 'plan', steps: [ { function: 'operating_system_control.shell.executeCommand', label: 'Fetch target page', agent_skill_id: 'tiny-web-crawler' } ], summary: 'Fetching the target page...', answer: null, intent: null }) } }) const caller = { callLLM: vi.fn(), callLLMText: vi.fn(), callLLMWithTools, supportsNativeTools: true, isLocalProvider: false, input: 'Please crawl from https://example.com, fetch readable content, and follow relevant links to find pricing.', history: [], agentSkillCatalog: '1. tiny-web-crawler: Crawl from starting web pages, fetch readable content, search within pages, and follow relevant links.', setAgentSkillContext: vi.fn(), getAgentSkillContext: vi.fn(async () => agentSkillContext), getContextFileContent: vi.fn(() => null), getContextManifest: vi.fn(() => ''), getSelfModelSnapshot: vi.fn(() => ''), consumeProviderErrorMessage: vi.fn(() => null) } const result = await runPlanningPhaseDirect( caller, { text: 'mock catalog', mode: 'function' }, [] ) expect(callLLMWithTools).toHaveBeenCalledOnce() expect(callLLMWithTools.mock.calls[0]?.[2]?.[0]?.function.name).toBe( 'create_plan' ) expect(caller.setAgentSkillContext).not.toHaveBeenCalled() expect(result).toEqual({ type: 'plan', steps: [ { function: 'operating_system_control.shell.executeCommand', label: 'Fetch target page', agentSkillId: 'tiny-web-crawler' } ], summary: 'Fetching the target page...' }) }) it('uses the handoff draft when final answer synthesis fails', async () => { const callLLMText = vi.fn(async () => null) const callLLMWithTools = vi.fn() const caller = { callLLM: vi.fn(), callLLMText, callLLMWithTools, supportsNativeTools: true, isLocalProvider: false, input: 'Summarize the completed research.', history: [], agentSkillCatalog: '', setAgentSkillContext: vi.fn(), getAgentSkillContext: vi.fn(), getContextFileContent: vi.fn(() => null), getContextManifest: vi.fn(() => ''), getSelfModelSnapshot: vi.fn(() => ''), consumeProviderErrorMessage: vi.fn(() => null) } const result = await runFinalAnswerPhase( caller, [], { intent: 'answer', draft: 'Research complete. The useful summary is already here.', source: 'execution' } ) expect(result).toBe( 'Research complete. The useful summary is already here.' ) expect(callLLMText).toHaveBeenCalledOnce() expect(callLLMWithTools).not.toHaveBeenCalled() }) it('finalizes directly when planning returns a handoff', async () => { logUnitProgress('planning handoff scenario', { input: 'Hi there, what do you reply if I tell you "ping"?', expectedIntent: 'answer' }) phaseMocks.runPlanningPhase.mockResolvedValue({ type: 'handoff', signal: { intent: 'answer', draft: 'Reply with pong.', source: 'planning' } }) phaseMocks.runFinalAnswerPhase.mockResolvedValue('Pong.') const duty = await createDuty( 'Hi there, what do you reply if I tell you "ping"?' ) const result = await duty.execute() logUnitProgress('planning handoff result', { output: result?.output, finalIntent: result?.data.finalIntent }) expect(phaseMocks.runPlanningPhase).toHaveBeenCalledOnce() expect(phaseMocks.runExecutionStep).not.toHaveBeenCalled() expect(phaseMocks.runFinalAnswerPhase).toHaveBeenCalledOnce() expect(result?.output).toBe('Pong.') expect(result?.data.finalIntent).toBe('answer') expect(result?.data.executionHistory).toEqual([]) }) it('executes a planned step and synthesizes the final answer', async () => { logUnitProgress('planned execution scenario', { input: 'What\'s the weather like today in Shenzhen?', stepFunction: 'weather.openmeteo.getCurrentConditions' }) phaseMocks.runPlanningPhase.mockResolvedValue({ type: 'plan', steps: [ { function: 'weather.openmeteo.getCurrentConditions', label: 'Check weather' } ], summary: 'Checking the weather...' }) phaseMocks.runExecutionStep.mockResolvedValue({ type: 'executed', execution: { function: 'weather.openmeteo.getCurrentConditions', status: 'success', observation: 'Current weather is 24C and sunny.', stepLabel: 'Check weather', requestedToolInput: '{"location":"Shenzhen"}' } }) phaseMocks.runExecutionSelfObservationPhase.mockResolvedValue(null) phaseMocks.runFinalAnswerPhase.mockResolvedValue('It is 24C and sunny in Shenzhen.') const duty = await createDuty('What\'s the weather like today in Shenzhen?') const result = await duty.execute() logUnitProgress('planned execution result', { output: result?.output, executionHistory: result?.data.executionHistory }) expect(coreMocks.socket.emit).toHaveBeenCalledWith( expect.objectContaining({ answer: 'Checking the weather...', fallbackText: 'Checking the weather...', historyMode: 'system_widget' }) ) expect(phaseMocks.runExecutionStep).toHaveBeenCalledOnce() expect(phaseMocks.runExecutionSelfObservationPhase).toHaveBeenCalledOnce() expect(result?.output).toBe('It is 24C and sunny in Shenzhen.') expect(result?.data.finalIntent).toBe('answer') expect(result?.data.executionHistory).toEqual([ { function: 'weather.openmeteo.getCurrentConditions', status: 'success', observation: 'Current weather is 24C and sunny.', stepLabel: 'Check weather', requestedToolInput: '{"location":"Shenzhen"}' } ]) }) it('finalizes an observed tool result when self-observation says it satisfies the request', async () => { phaseMocks.runPlanningPhase.mockResolvedValue({ type: 'plan', steps: [ { function: 'operating_system_control.shell.executeCommand', label: 'Run scanner' } ], summary: 'Running the scanner...' }) phaseMocks.runExecutionStep.mockResolvedValue({ type: 'executed', execution: { function: 'operating_system_control.shell.executeCommand', status: 'observed', observation: JSON.stringify({ status: 'observed', data: { output: { result: { success: false, stdout: 'Scan completed with useful findings.', returncode: 5 } } } }), stepLabel: 'Run scanner', requestedToolInput: '{"command":"scanner --target example.test"}' } }) phaseMocks.runExecutionSelfObservationPhase.mockResolvedValue({ type: 'handoff', signal: { intent: 'answer', draft: 'Report the scan findings.', source: 'self_observation' } }) phaseMocks.runFinalAnswerPhase.mockResolvedValue( 'The scan completed and returned useful findings.' ) const duty = await createDuty('Run the scanner and tell me what it finds.') const result = await duty.execute() expect(phaseMocks.runExecutionSelfObservationPhase).toHaveBeenCalledOnce() expect(phaseMocks.runRecoveryPlanningPhase).not.toHaveBeenCalled() expect(result?.output).toBe( 'The scan completed and returned useful findings.' ) expect(result?.data.executionHistory).toEqual([ expect.objectContaining({ function: 'operating_system_control.shell.executeCommand', status: 'observed', stepLabel: 'Run scanner' }) ]) }) it('replans from an observed tool result when self-observation says more work is needed', async () => { phaseMocks.runPlanningPhase.mockResolvedValue({ type: 'plan', steps: [ { function: 'operating_system_control.shell.executeCommand', label: 'Run invalid command' } ], summary: 'Running the command...' }) phaseMocks.runExecutionStep .mockResolvedValueOnce({ type: 'executed', execution: { function: 'operating_system_control.shell.executeCommand', status: 'observed', observation: JSON.stringify({ status: 'observed', observed_tool_failure: { success: false, error: 'Invalid option' } }), stepLabel: 'Run invalid command', requestedToolInput: '{"command":"scanner --bad-option"}' } }) .mockResolvedValueOnce({ type: 'executed', execution: { function: 'operating_system_control.shell.executeCommand', status: 'success', observation: 'Command completed with corrected options.', stepLabel: 'Run corrected command', requestedToolInput: '{"command":"scanner --good-option"}' } }) phaseMocks.runExecutionSelfObservationPhase .mockResolvedValueOnce({ type: 'replan', reason: 'Correcting the command options...', steps: [ { function: 'operating_system_control.shell.executeCommand', label: 'Run corrected command' } ] }) .mockResolvedValueOnce(null) phaseMocks.runFinalAnswerPhase.mockResolvedValue( 'The corrected command completed.' ) const duty = await createDuty('Run the scanner.') const result = await duty.execute() expect(phaseMocks.runExecutionStep).toHaveBeenCalledTimes(2) expect(phaseMocks.runExecutionSelfObservationPhase).toHaveBeenCalledTimes(2) expect(result?.output).toBe('The corrected command completed.') expect(result?.data.executionHistory).toEqual([ expect.objectContaining({ status: 'error', stepLabel: 'Run invalid command' }), expect.objectContaining({ status: 'success', stepLabel: 'Run corrected command' }) ]) }) it('keeps a selected Agent Skill active during execution', async () => { const agentSkillContext = { id: 'tiny-web-crawler', name: 'tiny-web-crawler', description: 'Fetch and crawl web pages.', rootPath: '/tmp/tiny-web-crawler', skillPath: '/tmp/tiny-web-crawler/SKILL.md', instructions: '# Tiny Web Crawler' } phaseMocks.runPlanningPhase.mockImplementation(async (caller) => { caller.setAgentSkillContext(agentSkillContext) return { type: 'plan', steps: [ { function: 'operating_system_control.shell.executeCommand', label: 'Fetch target page' } ], summary: 'Fetching the target page...' } }) phaseMocks.runExecutionStep.mockImplementation(async (caller) => { expect(caller.agentSkillContext).toEqual(agentSkillContext) return { type: 'executed', execution: { function: 'operating_system_control.shell.executeCommand', status: 'success', observation: 'Fetched with skill script.', stepLabel: 'Fetch target page' } } }) phaseMocks.runFinalAnswerPhase.mockResolvedValue('Fetched with skill script.') const duty = await createDuty( 'Use the tiny-web-crawler skill to inspect a page.' ) const result = await duty.execute() expect(phaseMocks.runExecutionStep).toHaveBeenCalledOnce() expect(result?.output).toBe('Fetched with skill script.') }) it('short-circuits to final synthesis when a tool returns a handoff signal', async () => { // This covers the path where a tool result already contains the semantic // handoff Leon should forward into the final-answer phase. logUnitProgress('tool handoff scenario', { input: 'There is a file waiting for you. Do what it asks you to do.', stepFunction: 'operating_system_control.shell.executeCommand' }) phaseMocks.runPlanningPhase.mockResolvedValue({ type: 'plan', steps: [ { function: 'operating_system_control.shell.executeCommand', label: 'List project root' } ], summary: 'Listing the project root...' }) phaseMocks.runExecutionStep.mockResolvedValue({ type: 'executed', execution: { function: 'operating_system_control.shell.executeCommand', status: 'success', observation: 'package.json\nserver\nbridges', stepLabel: 'List project root', requestedToolInput: '{"command":"ls -1"}' }, handoffSignal: { intent: 'answer', draft: 'Report the listed project root files.', source: 'tool' } }) phaseMocks.runFinalAnswerPhase.mockResolvedValue( 'The project root contains package.json, server, and bridges.' ) const duty = await createDuty( 'There is a file waiting for you. Do what it asks you to do.' ) const result = await duty.execute() logUnitProgress('tool handoff result', { output: result?.output, finalIntent: result?.data.finalIntent }) expect(coreMocks.socket.emit).toHaveBeenCalledWith( expect.objectContaining({ answer: 'Listing the project root...', fallbackText: 'Listing the project root...', historyMode: 'system_widget' }) ) expect(phaseMocks.runExecutionStep).toHaveBeenCalledOnce() expect(phaseMocks.runExecutionSelfObservationPhase).not.toHaveBeenCalled() expect(result?.output).toBe( 'The project root contains package.json, server, and bridges.' ) expect(result?.data.executionHistory).toEqual([ { function: 'operating_system_control.shell.executeCommand', status: 'success', observation: 'package.json\nserver\nbridges', stepLabel: 'List project root', requestedToolInput: '{"command":"ls -1"}' } ]) }) it('pauses for clarification during execution and resumes pending steps', async () => { interface SavedContinuationState { originalInput: string clarificationQuestion: string pendingSteps: Array<{ function: string, label: string }> } let savedContinuation: SavedContinuationState | null = null const fileCreationStep = { function: 'operating_system_control.shell.executeCommand', label: 'Create file' } phaseMocks.runPlanningPhase.mockResolvedValue({ type: 'plan', steps: [fileCreationStep], summary: 'Preparing the file creation...' }) phaseMocks.runExecutionStep .mockResolvedValueOnce({ type: 'handoff', signal: { intent: 'clarification', draft: 'What filename should I use?', source: 'execution' } }) .mockImplementationOnce(async (caller, currentStep) => { expect(caller.input).toContain( 'Previous clarification request: "What filename should I use?"' ) expect(caller.input).toContain('Clarification reply: "test.txt"') expect(currentStep).toEqual(fileCreationStep) return { type: 'executed', execution: { function: 'operating_system_control.shell.executeCommand', status: 'success', observation: 'Created /home/louis/Downloads/test.txt', stepLabel: 'Create file', requestedToolInput: '{"command":"touch /home/louis/Downloads/test.txt"}' } } }) phaseMocks.runFinalAnswerPhase .mockResolvedValueOnce('What filename should I use?') .mockResolvedValueOnce( 'Done. I created [FILE_PATH]/home/louis/Downloads/test.txt[/FILE_PATH].' ) const initialDuty = await createDuty( 'Create a text file in Downloads, but ask me for the filename first.' ) vi.spyOn( initialDuty as never, 'saveExecutionContinuation' as never ).mockImplementation((state: SavedContinuationState) => { savedContinuation = state }) const clarificationResult = await initialDuty.execute() expect(clarificationResult?.output).toBe('What filename should I use?') expect(clarificationResult?.data.finalIntent).toBe('clarification') expect(savedContinuation?.pendingSteps).toEqual([fileCreationStep]) const resumeDuty = await createDuty('test.txt') vi.spyOn( resumeDuty as never, 'loadExecutionContinuation' as never ).mockReturnValue(savedContinuation) vi.spyOn( resumeDuty as never, 'clearExecutionContinuation' as never ).mockImplementation(() => undefined) const resumedResult = await resumeDuty.execute() expect(phaseMocks.runPlanningPhase).toHaveBeenCalledOnce() expect(phaseMocks.runExecutionStep).toHaveBeenCalledTimes(2) expect(resumedResult?.output).toBe( 'Done. I created [FILE_PATH]/home/louis/Downloads/test.txt[/FILE_PATH].' ) expect(resumedResult?.data.finalIntent).toBe('answer') }) describe('history compaction', () => { it('tracks only newly appended history entries when the saved tail still matches', () => { const duty = new ReActLLMDuty({ input: 'Hi there' }) const currentState = createCompactionState({ tail: [ createMessageLog('owner', 'Earlier owner turn', 10), createMessageLog('leon', 'Earlier Leon turn', 11) ] }) const synchronized = ( duty as never ).synchronizeHistoryCompactionState( [ ...currentState.tail, createMessageLog('owner', 'New owner turn', 12), createMessageLog('leon', 'New Leon turn', 13) ], currentState ) expect(synchronized.shouldPersist).toBe(true) expect(synchronized.state.summary).toBe('- Previous topic') expect(synchronized.state.tail).toHaveLength(4) expect(synchronized.state.newMessagesSinceCompaction).toBe(2) }) it('rebuilds the tail from the saved compaction boundary when the stored tail no longer matches', () => { const duty = new ReActLLMDuty({ input: 'Hi there' }) const conversationLogs = [ createMessageLog('owner', 'Before summary', 10), createMessageLog('leon', 'Last compacted reply', 20), createMessageLog('owner', 'Fresh owner turn', 30), createMessageLog('leon', 'Fresh Leon turn', 31) ] const currentState = createCompactionState({ summarySentAt: 20, tail: [createMessageLog('owner', 'Stale tail entry', 21)] }) const synchronized = ( duty as never ).synchronizeHistoryCompactionState(conversationLogs, currentState) expect(synchronized.shouldPersist).toBe(true) expect(synchronized.state.summary).toBe('- Previous topic') expect(synchronized.state.summarySentAt).toBe(20) expect(synchronized.state.tail).toEqual(conversationLogs.slice(2)) expect(synchronized.state.newMessagesSinceCompaction).toBe(2) }) it('rebuilds the tail from the saved compaction boundary when the stored tail is empty', () => { const duty = new ReActLLMDuty({ input: 'Hi there' }) const conversationLogs = [ createMessageLog('owner', 'Before summary', 10), createMessageLog('leon', 'Last compacted reply', 20), createMessageLog('owner', 'Fresh owner turn', 30) ] const currentState = createCompactionState({ summarySentAt: 20, tail: [] }) const synchronized = ( duty as never ).synchronizeHistoryCompactionState(conversationLogs, currentState) expect(synchronized.shouldPersist).toBe(true) expect(synchronized.state.summary).toBe('- Previous topic') expect(synchronized.state.tail).toEqual([conversationLogs[2]]) expect(synchronized.state.newMessagesSinceCompaction).toBe(1) }) it('keeps the compacted summary in prompt history while clipping the raw tail window', () => { const duty = new ReActLLMDuty({ input: 'Hi there' }) const state = createCompactionState({ summarySentAt: 50, tail: Array.from({ length: 60 }, (_, index) => createMessageLog( index % 2 === 0 ? 'owner' : 'leon', `Message ${index + 1}`, 100 + index ) ), newMessagesSinceCompaction: 24 }) const history = (duty as never).buildHistoryForCurrentTurn([], state, { historyLimit: 48, compactionBatchSize: 36 }) expect(history).toHaveLength(48) expect(history[0]?.message).toBe( 'Earlier conversation summary:\n- Previous topic' ) expect(history[1]?.message).toBe('Message 14') expect(history[47]?.message).toBe('Message 60') }) it('does not compact again when the fresh-entry counter is still below the threshold', async () => { const duty = new ReActLLMDuty({ input: 'Set a timer for 3 seconds' }) const state = createCompactionState({ summary: '- Timer discussion already compacted', summarySentAt: 100, tail: Array.from({ length: 38 }, (_, index) => createMessageLog( index % 2 === 0 ? 'owner' : 'leon', `Tail message ${index + 1}`, 200 + index ) ), newMessagesSinceCompaction: 2 }) const loadStateSpy = vi .spyOn(duty as never, 'loadHistoryCompactionProviderState' as never) .mockReturnValue(state) const rollSpy = vi .spyOn(duty as never, 'rollHistoryCompactionState' as never) .mockResolvedValue(state) coreMocks.conversationLogger.loadAll.mockResolvedValue(state.tail) await (duty as never).maybeCompactHistoryAfterAnswer('plan_test_widget', []) expect(loadStateSpy).toHaveBeenCalledOnce() expect(rollSpy).not.toHaveBeenCalled() expect(widgetMocks.emitPlanWidget).not.toHaveBeenCalled() }) }) })