@falai/agent 2.2.3 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/adapters/MemoryAdapter.d.ts.map +1 -1
- package/dist/adapters/MemoryAdapter.js +10 -1
- package/dist/adapters/MemoryAdapter.js.map +1 -1
- package/dist/adapters/MongoAdapter.d.ts.map +1 -1
- package/dist/adapters/MongoAdapter.js +33 -2
- package/dist/adapters/MongoAdapter.js.map +1 -1
- package/dist/adapters/OpenSearchAdapter.d.ts.map +1 -1
- package/dist/adapters/OpenSearchAdapter.js +15 -1
- package/dist/adapters/OpenSearchAdapter.js.map +1 -1
- package/dist/adapters/PostgreSQLAdapter.d.ts.map +1 -1
- package/dist/adapters/PostgreSQLAdapter.js +35 -5
- package/dist/adapters/PostgreSQLAdapter.js.map +1 -1
- package/dist/adapters/PrismaAdapter.d.ts.map +1 -1
- package/dist/adapters/PrismaAdapter.js +58 -15
- package/dist/adapters/PrismaAdapter.js.map +1 -1
- package/dist/adapters/RedisAdapter.d.ts.map +1 -1
- package/dist/adapters/RedisAdapter.js +10 -1
- package/dist/adapters/RedisAdapter.js.map +1 -1
- package/dist/adapters/SQLiteAdapter.d.ts.map +1 -1
- package/dist/adapters/SQLiteAdapter.js +38 -6
- package/dist/adapters/SQLiteAdapter.js.map +1 -1
- package/dist/cjs/adapters/MemoryAdapter.d.ts.map +1 -1
- package/dist/cjs/adapters/MemoryAdapter.js +10 -1
- package/dist/cjs/adapters/MemoryAdapter.js.map +1 -1
- package/dist/cjs/adapters/MongoAdapter.d.ts.map +1 -1
- package/dist/cjs/adapters/MongoAdapter.js +33 -2
- package/dist/cjs/adapters/MongoAdapter.js.map +1 -1
- package/dist/cjs/adapters/OpenSearchAdapter.d.ts.map +1 -1
- package/dist/cjs/adapters/OpenSearchAdapter.js +15 -1
- package/dist/cjs/adapters/OpenSearchAdapter.js.map +1 -1
- package/dist/cjs/adapters/PostgreSQLAdapter.d.ts.map +1 -1
- package/dist/cjs/adapters/PostgreSQLAdapter.js +35 -5
- package/dist/cjs/adapters/PostgreSQLAdapter.js.map +1 -1
- package/dist/cjs/adapters/PrismaAdapter.d.ts.map +1 -1
- package/dist/cjs/adapters/PrismaAdapter.js +58 -15
- package/dist/cjs/adapters/PrismaAdapter.js.map +1 -1
- package/dist/cjs/adapters/RedisAdapter.d.ts.map +1 -1
- package/dist/cjs/adapters/RedisAdapter.js +10 -1
- package/dist/cjs/adapters/RedisAdapter.js.map +1 -1
- package/dist/cjs/adapters/SQLiteAdapter.d.ts.map +1 -1
- package/dist/cjs/adapters/SQLiteAdapter.js +38 -6
- package/dist/cjs/adapters/SQLiteAdapter.js.map +1 -1
- package/dist/cjs/core/Agent.d.ts +28 -18
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +58 -68
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/AutoChainExecutor.d.ts.map +1 -1
- package/dist/cjs/core/AutoChainExecutor.js +14 -20
- package/dist/cjs/core/AutoChainExecutor.js.map +1 -1
- package/dist/cjs/core/BranchEvaluator.d.ts +4 -3
- package/dist/cjs/core/BranchEvaluator.d.ts.map +1 -1
- package/dist/cjs/core/BranchEvaluator.js +18 -23
- package/dist/cjs/core/BranchEvaluator.js.map +1 -1
- package/dist/cjs/core/Flow.d.ts +2 -1
- package/dist/cjs/core/Flow.d.ts.map +1 -1
- package/dist/cjs/core/Flow.js +10 -3
- package/dist/cjs/core/Flow.js.map +1 -1
- package/dist/cjs/core/FlowRouter.d.ts +1 -0
- package/dist/cjs/core/FlowRouter.d.ts.map +1 -1
- package/dist/cjs/core/FlowRouter.js +28 -5
- package/dist/cjs/core/FlowRouter.js.map +1 -1
- package/dist/cjs/core/PersistenceManager.d.ts +3 -0
- package/dist/cjs/core/PersistenceManager.d.ts.map +1 -1
- package/dist/cjs/core/PersistenceManager.js +57 -5
- package/dist/cjs/core/PersistenceManager.js.map +1 -1
- package/dist/cjs/core/PromptComposer.d.ts.map +1 -1
- package/dist/cjs/core/PromptComposer.js +24 -10
- package/dist/cjs/core/PromptComposer.js.map +1 -1
- package/dist/cjs/core/ResponseGenerationError.d.ts +30 -0
- package/dist/cjs/core/ResponseGenerationError.d.ts.map +1 -0
- package/dist/cjs/core/ResponseGenerationError.js +37 -0
- package/dist/cjs/core/ResponseGenerationError.js.map +1 -0
- package/dist/cjs/core/ResponseModal.d.ts +43 -96
- package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
- package/dist/cjs/core/ResponseModal.js +184 -1183
- package/dist/cjs/core/ResponseModal.js.map +1 -1
- package/dist/cjs/core/ResponsePipeline.d.ts +58 -152
- package/dist/cjs/core/ResponsePipeline.d.ts.map +1 -1
- package/dist/cjs/core/ResponsePipeline.js +408 -457
- package/dist/cjs/core/ResponsePipeline.js.map +1 -1
- package/dist/cjs/core/SessionFinalizer.d.ts +34 -0
- package/dist/cjs/core/SessionFinalizer.d.ts.map +1 -0
- package/dist/cjs/core/SessionFinalizer.js +61 -0
- package/dist/cjs/core/SessionFinalizer.js.map +1 -0
- package/dist/cjs/core/SessionManager.d.ts +1 -1
- package/dist/cjs/core/SessionManager.d.ts.map +1 -1
- package/dist/cjs/core/SessionManager.js +18 -7
- package/dist/cjs/core/SessionManager.js.map +1 -1
- package/dist/cjs/core/SignalCoordinator.d.ts +103 -0
- package/dist/cjs/core/SignalCoordinator.d.ts.map +1 -0
- package/dist/cjs/core/SignalCoordinator.js +207 -0
- package/dist/cjs/core/SignalCoordinator.js.map +1 -0
- package/dist/cjs/core/SignalEvaluator.d.ts +2 -2
- package/dist/cjs/core/SignalEvaluator.d.ts.map +1 -1
- package/dist/cjs/core/SignalEvaluator.js +13 -27
- package/dist/cjs/core/SignalEvaluator.js.map +1 -1
- package/dist/cjs/core/SignalProcessor.d.ts.map +1 -1
- package/dist/cjs/core/SignalProcessor.js +1 -3
- package/dist/cjs/core/SignalProcessor.js.map +1 -1
- package/dist/cjs/core/Step.d.ts +3 -1
- package/dist/cjs/core/Step.d.ts.map +1 -1
- package/dist/cjs/core/Step.js +10 -3
- package/dist/cjs/core/Step.js.map +1 -1
- package/dist/cjs/core/StepLifecycle.d.ts +33 -0
- package/dist/cjs/core/StepLifecycle.d.ts.map +1 -0
- package/dist/cjs/core/StepLifecycle.js +97 -0
- package/dist/cjs/core/StepLifecycle.js.map +1 -0
- package/dist/cjs/core/ToolLoopExecutor.d.ts +104 -0
- package/dist/cjs/core/ToolLoopExecutor.d.ts.map +1 -0
- package/dist/cjs/core/ToolLoopExecutor.js +391 -0
- package/dist/cjs/core/ToolLoopExecutor.js.map +1 -0
- package/dist/cjs/core/ToolManager.d.ts +1 -1
- package/dist/cjs/core/ToolManager.d.ts.map +1 -1
- package/dist/cjs/core/ToolManager.js.map +1 -1
- package/dist/cjs/index.d.ts +4 -5
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +7 -6
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.d.ts +2 -0
- package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.js +28 -56
- package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.d.ts +25 -21
- package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.js +48 -407
- package/dist/cjs/providers/DeepSeekProvider.js.map +1 -1
- package/dist/cjs/providers/GeminiProvider.d.ts +2 -0
- package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/GeminiProvider.js +27 -56
- package/dist/cjs/providers/GeminiProvider.js.map +1 -1
- package/dist/cjs/providers/OpenAICompatibleProvider.d.ts +129 -0
- package/dist/cjs/providers/OpenAICompatibleProvider.d.ts.map +1 -0
- package/dist/cjs/providers/OpenAICompatibleProvider.js +485 -0
- package/dist/cjs/providers/OpenAICompatibleProvider.js.map +1 -0
- package/dist/cjs/providers/OpenAIProvider.d.ts +9 -28
- package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.js +23 -417
- package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.d.ts +10 -27
- package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.js +28 -417
- package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
- package/dist/cjs/providers/errorClassification.d.ts +61 -0
- package/dist/cjs/providers/errorClassification.d.ts.map +1 -0
- package/dist/cjs/providers/errorClassification.js +123 -0
- package/dist/cjs/providers/errorClassification.js.map +1 -0
- package/dist/cjs/providers/index.d.ts +4 -0
- package/dist/cjs/providers/index.d.ts.map +1 -1
- package/dist/cjs/providers/index.js +7 -1
- package/dist/cjs/providers/index.js.map +1 -1
- package/dist/cjs/types/agent.d.ts +2 -1
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/ai.d.ts +20 -0
- package/dist/cjs/types/ai.d.ts.map +1 -1
- package/dist/cjs/types/errors.d.ts +33 -0
- package/dist/cjs/types/errors.d.ts.map +1 -1
- package/dist/cjs/types/errors.js +37 -1
- package/dist/cjs/types/errors.js.map +1 -1
- package/dist/cjs/types/flow.d.ts +10 -5
- package/dist/cjs/types/flow.d.ts.map +1 -1
- package/dist/cjs/types/history.d.ts +1 -1
- package/dist/cjs/types/history.d.ts.map +1 -1
- package/dist/cjs/types/index.d.ts +5 -4
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/types/index.js +3 -1
- package/dist/cjs/types/index.js.map +1 -1
- package/dist/cjs/types/persistence.d.ts +43 -2
- package/dist/cjs/types/persistence.d.ts.map +1 -1
- package/dist/cjs/types/session.d.ts +8 -0
- package/dist/cjs/types/session.d.ts.map +1 -1
- package/dist/cjs/types/signals.d.ts +21 -11
- package/dist/cjs/types/signals.d.ts.map +1 -1
- package/dist/cjs/types/template.d.ts +3 -1
- package/dist/cjs/types/template.d.ts.map +1 -1
- package/dist/cjs/types/tool.d.ts +4 -4
- package/dist/cjs/types/tool.d.ts.map +1 -1
- package/dist/cjs/utils/condition.d.ts +19 -0
- package/dist/cjs/utils/condition.d.ts.map +1 -1
- package/dist/cjs/utils/condition.js +86 -15
- package/dist/cjs/utils/condition.js.map +1 -1
- package/dist/cjs/utils/session.d.ts +1 -0
- package/dist/cjs/utils/session.d.ts.map +1 -1
- package/dist/cjs/utils/session.js +1 -0
- package/dist/cjs/utils/session.js.map +1 -1
- package/dist/core/Agent.d.ts +28 -18
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +58 -68
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/AutoChainExecutor.d.ts.map +1 -1
- package/dist/core/AutoChainExecutor.js +14 -20
- package/dist/core/AutoChainExecutor.js.map +1 -1
- package/dist/core/BranchEvaluator.d.ts +4 -3
- package/dist/core/BranchEvaluator.d.ts.map +1 -1
- package/dist/core/BranchEvaluator.js +18 -23
- package/dist/core/BranchEvaluator.js.map +1 -1
- package/dist/core/Flow.d.ts +2 -1
- package/dist/core/Flow.d.ts.map +1 -1
- package/dist/core/Flow.js +10 -3
- package/dist/core/Flow.js.map +1 -1
- package/dist/core/FlowRouter.d.ts +1 -0
- package/dist/core/FlowRouter.d.ts.map +1 -1
- package/dist/core/FlowRouter.js +28 -5
- package/dist/core/FlowRouter.js.map +1 -1
- package/dist/core/PersistenceManager.d.ts +3 -0
- package/dist/core/PersistenceManager.d.ts.map +1 -1
- package/dist/core/PersistenceManager.js +58 -6
- package/dist/core/PersistenceManager.js.map +1 -1
- package/dist/core/PromptComposer.d.ts.map +1 -1
- package/dist/core/PromptComposer.js +24 -10
- package/dist/core/PromptComposer.js.map +1 -1
- package/dist/core/ResponseGenerationError.d.ts +30 -0
- package/dist/core/ResponseGenerationError.d.ts.map +1 -0
- package/dist/core/ResponseGenerationError.js +33 -0
- package/dist/core/ResponseGenerationError.js.map +1 -0
- package/dist/core/ResponseModal.d.ts +43 -96
- package/dist/core/ResponseModal.d.ts.map +1 -1
- package/dist/core/ResponseModal.js +177 -1175
- package/dist/core/ResponseModal.js.map +1 -1
- package/dist/core/ResponsePipeline.d.ts +58 -152
- package/dist/core/ResponsePipeline.d.ts.map +1 -1
- package/dist/core/ResponsePipeline.js +409 -458
- package/dist/core/ResponsePipeline.js.map +1 -1
- package/dist/core/SessionFinalizer.d.ts +34 -0
- package/dist/core/SessionFinalizer.d.ts.map +1 -0
- package/dist/core/SessionFinalizer.js +57 -0
- package/dist/core/SessionFinalizer.js.map +1 -0
- package/dist/core/SessionManager.d.ts +1 -1
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +18 -7
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/SignalCoordinator.d.ts +103 -0
- package/dist/core/SignalCoordinator.d.ts.map +1 -0
- package/dist/core/SignalCoordinator.js +203 -0
- package/dist/core/SignalCoordinator.js.map +1 -0
- package/dist/core/SignalEvaluator.d.ts +2 -2
- package/dist/core/SignalEvaluator.d.ts.map +1 -1
- package/dist/core/SignalEvaluator.js +13 -27
- package/dist/core/SignalEvaluator.js.map +1 -1
- package/dist/core/SignalProcessor.d.ts.map +1 -1
- package/dist/core/SignalProcessor.js +1 -3
- package/dist/core/SignalProcessor.js.map +1 -1
- package/dist/core/Step.d.ts +3 -1
- package/dist/core/Step.d.ts.map +1 -1
- package/dist/core/Step.js +10 -3
- package/dist/core/Step.js.map +1 -1
- package/dist/core/StepLifecycle.d.ts +33 -0
- package/dist/core/StepLifecycle.d.ts.map +1 -0
- package/dist/core/StepLifecycle.js +93 -0
- package/dist/core/StepLifecycle.js.map +1 -0
- package/dist/core/ToolLoopExecutor.d.ts +104 -0
- package/dist/core/ToolLoopExecutor.d.ts.map +1 -0
- package/dist/core/ToolLoopExecutor.js +387 -0
- package/dist/core/ToolLoopExecutor.js.map +1 -0
- package/dist/core/ToolManager.d.ts +1 -1
- package/dist/core/ToolManager.d.ts.map +1 -1
- package/dist/core/ToolManager.js.map +1 -1
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/providers/AnthropicProvider.d.ts +2 -0
- package/dist/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.js +22 -50
- package/dist/providers/AnthropicProvider.js.map +1 -1
- package/dist/providers/DeepSeekProvider.d.ts +25 -21
- package/dist/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/providers/DeepSeekProvider.js +49 -408
- package/dist/providers/DeepSeekProvider.js.map +1 -1
- package/dist/providers/GeminiProvider.d.ts +2 -0
- package/dist/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/providers/GeminiProvider.js +21 -50
- package/dist/providers/GeminiProvider.js.map +1 -1
- package/dist/providers/OpenAICompatibleProvider.d.ts +129 -0
- package/dist/providers/OpenAICompatibleProvider.d.ts.map +1 -0
- package/dist/providers/OpenAICompatibleProvider.js +481 -0
- package/dist/providers/OpenAICompatibleProvider.js.map +1 -0
- package/dist/providers/OpenAIProvider.d.ts +9 -28
- package/dist/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/providers/OpenAIProvider.js +23 -417
- package/dist/providers/OpenAIProvider.js.map +1 -1
- package/dist/providers/OpenRouterProvider.d.ts +10 -27
- package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/providers/OpenRouterProvider.js +28 -417
- package/dist/providers/OpenRouterProvider.js.map +1 -1
- package/dist/providers/errorClassification.d.ts +61 -0
- package/dist/providers/errorClassification.d.ts.map +1 -0
- package/dist/providers/errorClassification.js +116 -0
- package/dist/providers/errorClassification.js.map +1 -0
- package/dist/providers/index.d.ts +4 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +2 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/types/agent.d.ts +2 -1
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/ai.d.ts +20 -0
- package/dist/types/ai.d.ts.map +1 -1
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +34 -0
- package/dist/types/errors.js.map +1 -1
- package/dist/types/flow.d.ts +10 -5
- package/dist/types/flow.d.ts.map +1 -1
- package/dist/types/history.d.ts +1 -1
- package/dist/types/history.d.ts.map +1 -1
- package/dist/types/index.d.ts +5 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/persistence.d.ts +43 -2
- package/dist/types/persistence.d.ts.map +1 -1
- package/dist/types/session.d.ts +8 -0
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/signals.d.ts +21 -11
- package/dist/types/signals.d.ts.map +1 -1
- package/dist/types/template.d.ts +3 -1
- package/dist/types/template.d.ts.map +1 -1
- package/dist/types/tool.d.ts +4 -4
- package/dist/types/tool.d.ts.map +1 -1
- package/dist/utils/condition.d.ts +19 -0
- package/dist/utils/condition.d.ts.map +1 -1
- package/dist/utils/condition.js +84 -15
- package/dist/utils/condition.js.map +1 -1
- package/dist/utils/session.d.ts +1 -0
- package/dist/utils/session.d.ts.map +1 -1
- package/dist/utils/session.js +1 -0
- package/dist/utils/session.js.map +1 -1
- package/docs/README.md +1 -1
- package/docs/guides/branching.md +1 -1
- package/docs/guides/compaction.md +12 -5
- package/docs/guides/conditions.md +15 -2
- package/docs/guides/error-handling.md +59 -9
- package/docs/guides/instructions.md +2 -2
- package/docs/guides/persistence.md +69 -2
- package/docs/guides/streaming.md +3 -1
- package/docs/migration/README.md +5 -1
- package/docs/migration/v2-3-to-v2-4.md +316 -0
- package/docs/reference/adapters.md +55 -7
- package/docs/reference/branches.md +2 -2
- package/docs/reference/create-agent.md +3 -3
- package/docs/reference/errors.md +66 -1
- package/docs/reference/flow.md +1 -1
- package/docs/reference/instruction.md +4 -4
- package/docs/reference/providers.md +100 -3
- package/docs/reference/signals.md +18 -8
- package/docs/reference/step.md +2 -2
- package/docs/reference/tool.md +3 -1
- package/docs/start/05-go-to-production.md +3 -0
- package/package.json +1 -1
- package/src/adapters/MemoryAdapter.ts +15 -1
- package/src/adapters/MongoAdapter.ts +41 -2
- package/src/adapters/OpenSearchAdapter.ts +24 -1
- package/src/adapters/PostgreSQLAdapter.ts +45 -5
- package/src/adapters/PrismaAdapter.ts +82 -16
- package/src/adapters/RedisAdapter.ts +19 -1
- package/src/adapters/SQLiteAdapter.ts +47 -5
- package/src/core/Agent.ts +70 -85
- package/src/core/AutoChainExecutor.ts +27 -57
- package/src/core/BranchEvaluator.ts +24 -30
- package/src/core/Flow.ts +10 -3
- package/src/core/FlowRouter.ts +36 -4
- package/src/core/PersistenceManager.ts +79 -10
- package/src/core/PromptComposer.ts +25 -12
- package/src/core/ResponseGenerationError.ts +56 -0
- package/src/core/ResponseModal.ts +241 -1465
- package/src/core/ResponsePipeline.ts +492 -662
- package/src/core/SessionFinalizer.ts +78 -0
- package/src/core/SessionManager.ts +21 -9
- package/src/core/SignalCoordinator.ts +263 -0
- package/src/core/SignalEvaluator.ts +14 -30
- package/src/core/SignalProcessor.ts +1 -4
- package/src/core/Step.ts +11 -3
- package/src/core/StepLifecycle.ts +139 -0
- package/src/core/ToolLoopExecutor.ts +492 -0
- package/src/core/ToolManager.ts +2 -1
- package/src/index.ts +7 -5
- package/src/providers/AnthropicProvider.ts +30 -72
- package/src/providers/DeepSeekProvider.ts +74 -586
- package/src/providers/GeminiProvider.ts +29 -70
- package/src/providers/OpenAICompatibleProvider.ts +738 -0
- package/src/providers/OpenAIProvider.ts +29 -602
- package/src/providers/OpenRouterProvider.ts +38 -596
- package/src/providers/errorClassification.ts +172 -0
- package/src/providers/index.ts +13 -0
- package/src/types/agent.ts +2 -1
- package/src/types/ai.ts +22 -0
- package/src/types/errors.ts +57 -0
- package/src/types/flow.ts +10 -5
- package/src/types/history.ts +1 -2
- package/src/types/index.ts +5 -1
- package/src/types/persistence.ts +50 -2
- package/src/types/session.ts +9 -0
- package/src/types/signals.ts +23 -11
- package/src/types/template.ts +3 -1
- package/src/types/tool.ts +10 -10
- package/src/utils/condition.ts +115 -18
- package/src/utils/session.ts +2 -0
|
@@ -4,33 +4,78 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type {
|
|
7
|
+
AgentOptions,
|
|
7
8
|
AgentResponse,
|
|
8
9
|
AgentResponseStreamChunk,
|
|
9
10
|
History,
|
|
10
11
|
SessionState,
|
|
11
12
|
StepRef,
|
|
12
13
|
HistoryItem,
|
|
13
|
-
Tool,
|
|
14
14
|
Event,
|
|
15
15
|
AgentStructuredResponse,
|
|
16
16
|
StoppedReason,
|
|
17
|
-
ToolCallRequest,
|
|
18
17
|
ScopedInstructions,
|
|
19
18
|
AppliedInstruction,
|
|
20
|
-
PrepareResult,
|
|
21
19
|
Directive,
|
|
20
|
+
Instruction,
|
|
21
|
+
Term,
|
|
22
|
+
StructuredSchema,
|
|
23
|
+
CompactionOptions,
|
|
22
24
|
} from "../types";
|
|
23
25
|
import type { SignalFiring } from "../types/signals";
|
|
24
|
-
import type {
|
|
26
|
+
import type { SessionManager } from "./SessionManager";
|
|
27
|
+
import type { SignalProcessor } from "./SignalProcessor";
|
|
28
|
+
import type { PromptSectionCache } from "./PromptSectionCache";
|
|
29
|
+
import type { FlowRouter } from "./FlowRouter";
|
|
30
|
+
import type { PersistenceManager } from "./PersistenceManager";
|
|
25
31
|
import type { Flow } from "./Flow";
|
|
26
32
|
import { Step } from "./Step";
|
|
27
33
|
import { ResponseEngine } from "./ResponseEngine";
|
|
28
|
-
import { ResponsePipeline
|
|
34
|
+
import { ResponsePipeline } from "./ResponsePipeline";
|
|
29
35
|
import { AutoChainExecutor, type AutoChainResult } from "./AutoChainExecutor";
|
|
30
|
-
import {
|
|
36
|
+
import { StepLifecycle } from "./StepLifecycle";
|
|
37
|
+
import { SessionFinalizer } from "./SessionFinalizer";
|
|
38
|
+
import { ToolLoopExecutor } from "./ToolLoopExecutor";
|
|
39
|
+
import { SignalCoordinator } from "./SignalCoordinator";
|
|
40
|
+
import { ResponseGenerationError } from "./ResponseGenerationError";
|
|
41
|
+
import { cloneDeep, mergeCollected, logger, historyToEvents, completeCurrentFlow, render } from "../utils";
|
|
31
42
|
import { createTemplateContext } from "../utils/template";
|
|
32
43
|
import type { ToolManager } from "./ToolManager";
|
|
33
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The narrow surface ResponseModal (and its collaborators) need from the
|
|
47
|
+
* Agent. Agent implements this; the response layer is constructible and
|
|
48
|
+
* testable against this interface without a full Agent.
|
|
49
|
+
*/
|
|
50
|
+
export interface ResponseModalDeps<TContext = unknown, TData = unknown> {
|
|
51
|
+
/** Session manager (history, live session, sync). */
|
|
52
|
+
readonly session: SessionManager<TData>;
|
|
53
|
+
/** Tool registry/resolver and single-tool executor. */
|
|
54
|
+
readonly tool: ToolManager<TContext, TData>;
|
|
55
|
+
readonly signalProcessor: SignalProcessor<TContext, TData> | undefined;
|
|
56
|
+
readonly promptSectionCache: PromptSectionCache;
|
|
57
|
+
readonly instructions: Instruction<TContext, TData>[];
|
|
58
|
+
readonly schema: StructuredSchema | undefined;
|
|
59
|
+
readonly maxAutoStepsPerTurn: number;
|
|
60
|
+
/** The agent's live session reference (read and replaced at finalize). */
|
|
61
|
+
currentSession: SessionState<TData> | undefined;
|
|
62
|
+
getAgentOptions(): AgentOptions<TContext, TData>;
|
|
63
|
+
getFlows(): Flow<TContext, TData>[];
|
|
64
|
+
getTerms(): Term<TContext, TData>[];
|
|
65
|
+
getFlowRouter(): FlowRouter<TContext, TData>;
|
|
66
|
+
getContext(): Promise<TContext | undefined>;
|
|
67
|
+
getCompactionOptions(): CompactionOptions | undefined;
|
|
68
|
+
getPersistenceManager(): PersistenceManager<TData> | undefined;
|
|
69
|
+
getUpdateDataMethod(): (
|
|
70
|
+
session: SessionState<TData>,
|
|
71
|
+
dataUpdate: Partial<TData>
|
|
72
|
+
) => Promise<SessionState<TData>>;
|
|
73
|
+
updateContext(updates: Partial<TContext>): Promise<void>;
|
|
74
|
+
updateCollectedData(updates: Partial<TData>): Promise<void>;
|
|
75
|
+
/** Drain data staged before any session existed. */
|
|
76
|
+
consumePendingData(): Partial<TData>;
|
|
77
|
+
}
|
|
78
|
+
|
|
34
79
|
/**
|
|
35
80
|
* Configuration options for ResponseModal
|
|
36
81
|
*/
|
|
@@ -72,58 +117,6 @@ export interface GenerateOptions<TContext = unknown> {
|
|
|
72
117
|
history?: History; // Optional: override session history
|
|
73
118
|
}
|
|
74
119
|
|
|
75
|
-
/**
|
|
76
|
-
* Error details for response generation failures
|
|
77
|
-
*/
|
|
78
|
-
interface ResponseGenerationErrorDetails {
|
|
79
|
-
originalError?: unknown;
|
|
80
|
-
params?: Record<string, unknown>;
|
|
81
|
-
phase?: string;
|
|
82
|
-
context?: Record<string, unknown>;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Error class for response generation failures
|
|
87
|
-
*/
|
|
88
|
-
export class ResponseGenerationError extends Error {
|
|
89
|
-
constructor(
|
|
90
|
-
message: string,
|
|
91
|
-
public readonly details?: ResponseGenerationErrorDetails
|
|
92
|
-
) {
|
|
93
|
-
super(message);
|
|
94
|
-
this.name = 'ResponseGenerationError';
|
|
95
|
-
|
|
96
|
-
// Preserve stack trace from original error if available
|
|
97
|
-
if (details?.originalError instanceof Error && details.originalError.stack) {
|
|
98
|
-
this.stack = `${this.stack}\nCaused by: ${details.originalError.stack}`;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Create a ResponseGenerationError from an unknown error
|
|
104
|
-
*/
|
|
105
|
-
static fromError(
|
|
106
|
-
error: unknown,
|
|
107
|
-
phase: string,
|
|
108
|
-
params?: Record<string, unknown>,
|
|
109
|
-
context?: Record<string, unknown>
|
|
110
|
-
): ResponseGenerationError {
|
|
111
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
112
|
-
return new ResponseGenerationError(
|
|
113
|
-
`[ResponseGenerationError] Response generation failed in ${phase}: ${message}. ` +
|
|
114
|
-
`Check provider configuration and the ${phase} phase handler.`,
|
|
115
|
-
{ originalError: error, params, phase, context }
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Check if an error is a ResponseGenerationError
|
|
121
|
-
*/
|
|
122
|
-
static isResponseGenerationError(error: unknown): error is ResponseGenerationError {
|
|
123
|
-
return error instanceof ResponseGenerationError;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
120
|
/**
|
|
128
121
|
* Common response context used across all response methods
|
|
129
122
|
*/
|
|
@@ -154,44 +147,90 @@ interface ResponseContext<TContext = unknown, TData = unknown> {
|
|
|
154
147
|
export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
155
148
|
private readonly responseEngine: ResponseEngine<TContext, TData>;
|
|
156
149
|
private readonly responsePipeline: ResponsePipeline<TContext, TData>;
|
|
150
|
+
private readonly stepLifecycle: StepLifecycle<TContext, TData>;
|
|
151
|
+
private readonly sessionFinalizer: SessionFinalizer<TContext, TData>;
|
|
152
|
+
private readonly toolLoopExecutor: ToolLoopExecutor<TContext, TData>;
|
|
153
|
+
private readonly signalCoordinator: SignalCoordinator<TContext, TData>;
|
|
157
154
|
|
|
158
155
|
constructor(
|
|
159
|
-
private readonly agent:
|
|
156
|
+
private readonly agent: ResponseModalDeps<TContext, TData>,
|
|
160
157
|
private readonly options?: ResponseModalOptions
|
|
161
158
|
) {
|
|
162
159
|
// Initialize response engine
|
|
163
160
|
this.responseEngine = new ResponseEngine<TContext, TData>(this.agent.promptSectionCache);
|
|
164
161
|
|
|
162
|
+
// Signal pre/post phase orchestration
|
|
163
|
+
this.signalCoordinator = new SignalCoordinator<TContext, TData>({
|
|
164
|
+
getFlows: () => this.agent.getFlows(),
|
|
165
|
+
signalProcessor: this.agent.signalProcessor,
|
|
166
|
+
});
|
|
167
|
+
|
|
165
168
|
// Initialize response pipeline with agent dependencies
|
|
166
169
|
this.responsePipeline = new ResponsePipeline<TContext, TData>(
|
|
167
170
|
this.agent.getAgentOptions(),
|
|
168
171
|
() => this.agent.getFlows(), // Pass a function to get flows dynamically
|
|
169
|
-
this.agent.getTools(),
|
|
170
172
|
this.agent.getFlowRouter(),
|
|
171
|
-
this.
|
|
172
|
-
this.agent.getUpdateDataMethod(),
|
|
173
|
+
this.signalCoordinator,
|
|
173
174
|
this.agent.updateCollectedData.bind(this.agent),
|
|
174
|
-
this.
|
|
175
|
-
this.agent.signalProcessor
|
|
175
|
+
() => this.agent.schema
|
|
176
176
|
);
|
|
177
|
+
|
|
178
|
+
// Step prepare/finalize execution, shared by the prepare phase and finalizer
|
|
179
|
+
this.stepLifecycle = new StepLifecycle<TContext, TData>({
|
|
180
|
+
getFlows: () => this.agent.getFlows(),
|
|
181
|
+
toolManager: this.getToolManager(),
|
|
182
|
+
updateContext: this.agent.updateContext.bind(this.agent),
|
|
183
|
+
updateData: this.agent.updateCollectedData.bind(this.agent),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Single owner of end-of-turn finalization (compaction + persistence + sync)
|
|
187
|
+
this.sessionFinalizer = new SessionFinalizer<TContext, TData>({
|
|
188
|
+
getCompactionOptions: () => this.agent.getCompactionOptions(),
|
|
189
|
+
getPersistenceManager: () => this.agent.getPersistenceManager(),
|
|
190
|
+
getAgentOptions: () => this.agent.getAgentOptions(),
|
|
191
|
+
getCurrentSession: () => this.agent.currentSession,
|
|
192
|
+
setCurrentSession: (session) => { this.agent.currentSession = session; },
|
|
193
|
+
stepLifecycle: this.stepLifecycle,
|
|
194
|
+
enableAutoSave: this.options?.enableAutoSave,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Tool follow-up loop (run tools, ask the LLM again) + streaming batch execution
|
|
198
|
+
this.toolLoopExecutor = new ToolLoopExecutor<TContext, TData>({
|
|
199
|
+
toolManager: this.getToolManager(),
|
|
200
|
+
getAgentOptions: () => this.agent.getAgentOptions(),
|
|
201
|
+
updateContext: this.agent.updateContext.bind(this.agent),
|
|
202
|
+
updateCollectedData: this.agent.updateCollectedData.bind(this.agent),
|
|
203
|
+
updateSessionData: this.agent.getUpdateDataMethod(),
|
|
204
|
+
maxToolLoops: this.options?.maxToolLoops,
|
|
205
|
+
});
|
|
206
|
+
|
|
177
207
|
}
|
|
178
208
|
|
|
179
209
|
/**
|
|
180
210
|
* Generate a non-streaming response using unified logic
|
|
181
211
|
*/
|
|
182
212
|
async respond(params: RespondParams<TContext, TData>): Promise<AgentResponse<TData>> {
|
|
213
|
+
// Snapshot the managed session so a failed turn has no in-memory effect:
|
|
214
|
+
// without this, mutations made before the failure leave the live session
|
|
215
|
+
// diverged from persisted state
|
|
216
|
+
const preTurnSession = this.agent.session.current
|
|
217
|
+
? cloneDeep(this.agent.session.current)
|
|
218
|
+
: undefined;
|
|
183
219
|
try {
|
|
184
220
|
// Use unified response preparation and routing
|
|
185
221
|
const responseContext = await this.prepareUnifiedResponseContext(params);
|
|
186
222
|
// Generate response using unified logic
|
|
187
223
|
const result = await this.generateUnifiedResponse(responseContext);
|
|
188
224
|
|
|
189
|
-
// Finalize session
|
|
190
|
-
await this.
|
|
225
|
+
// Finalize session — the non-streaming turn's single finalize
|
|
226
|
+
await this.sessionFinalizer.finalize(result.session!, responseContext.effectiveContext);
|
|
191
227
|
|
|
192
228
|
return result;
|
|
193
229
|
|
|
194
230
|
} catch (error) {
|
|
231
|
+
if (preTurnSession) {
|
|
232
|
+
this.agent.session.syncSession(preTurnSession);
|
|
233
|
+
}
|
|
195
234
|
throw new ResponseGenerationError(
|
|
196
235
|
`[ResponseGenerationError] Response generation failed: ${error instanceof Error ? error.message : String(error)}. ` +
|
|
197
236
|
`Check provider configuration and network connectivity.`,
|
|
@@ -204,6 +243,10 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
204
243
|
* Generate a streaming response using unified logic
|
|
205
244
|
*/
|
|
206
245
|
async *respondStream(params: RespondParams<TContext, TData>): AsyncGenerator<AgentResponseStreamChunk<TData>> {
|
|
246
|
+
// Same failed-turn rollback semantics as respond()
|
|
247
|
+
const preTurnSession = this.agent.session.current
|
|
248
|
+
? cloneDeep(this.agent.session.current)
|
|
249
|
+
: undefined;
|
|
207
250
|
try {
|
|
208
251
|
// Use unified response preparation and routing
|
|
209
252
|
const responseContext = await this.prepareUnifiedResponseContext(params);
|
|
@@ -212,6 +255,9 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
212
255
|
yield* this.generateUnifiedStreamingResponse(responseContext);
|
|
213
256
|
|
|
214
257
|
} catch (error) {
|
|
258
|
+
if (preTurnSession) {
|
|
259
|
+
this.agent.session.syncSession(preTurnSession);
|
|
260
|
+
}
|
|
215
261
|
// Stream error to caller
|
|
216
262
|
yield {
|
|
217
263
|
delta: "",
|
|
@@ -246,17 +292,9 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
246
292
|
history = this.agent.session.getHistory();
|
|
247
293
|
}
|
|
248
294
|
|
|
249
|
-
// Get or create session
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// Merge agent's collected data into session (agent data takes precedence)
|
|
253
|
-
const collectedData = this.agent.getCollectedData();
|
|
254
|
-
if (Object.keys(collectedData).length > 0) {
|
|
255
|
-
session = mergeCollected(session, collectedData);
|
|
256
|
-
// Update the session manager with the merged data
|
|
257
|
-
await this.agent.session.setData(collectedData);
|
|
258
|
-
logger.debug("[ResponseModal] Merged agent collected data into stream session:", collectedData);
|
|
259
|
-
}
|
|
295
|
+
// Get or create session — session.data is the single source of truth,
|
|
296
|
+
// so no agent-side data merge is needed
|
|
297
|
+
const session = await this.agent.session.getOrCreate();
|
|
260
298
|
|
|
261
299
|
// Stream response using existing respondStream method
|
|
262
300
|
let finalMessage = "";
|
|
@@ -308,17 +346,9 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
308
346
|
history = this.agent.session.getHistory();
|
|
309
347
|
}
|
|
310
348
|
|
|
311
|
-
// Get or create session
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
// Merge agent's collected data into session (agent data takes precedence)
|
|
315
|
-
const collectedData = this.agent.getCollectedData();
|
|
316
|
-
if (Object.keys(collectedData).length > 0) {
|
|
317
|
-
session = mergeCollected(session, collectedData);
|
|
318
|
-
// Update the session manager with the merged data
|
|
319
|
-
await this.agent.session.setData(collectedData);
|
|
320
|
-
logger.debug("[ResponseModal] Merged agent collected data into generate session:", collectedData);
|
|
321
|
-
}
|
|
349
|
+
// Get or create session — session.data is the single source of truth,
|
|
350
|
+
// so no agent-side data merge is needed
|
|
351
|
+
const session = await this.agent.session.getOrCreate();
|
|
322
352
|
|
|
323
353
|
// Generate response using existing respond method
|
|
324
354
|
const result = await this.respond({
|
|
@@ -411,52 +441,53 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
411
441
|
// Keep original HistoryItem[] format for external APIs
|
|
412
442
|
const history = simpleHistory;
|
|
413
443
|
|
|
414
|
-
// Use ResponsePipeline for
|
|
415
|
-
//
|
|
444
|
+
// Use ResponsePipeline for context and session preparation; context
|
|
445
|
+
// and session are passed explicitly — the pipeline holds no state
|
|
416
446
|
let responseContext: {
|
|
417
447
|
effectiveContext: TContext;
|
|
418
448
|
session: SessionState<TData>;
|
|
449
|
+
contextAfterHook?: TContext;
|
|
419
450
|
};
|
|
420
451
|
try {
|
|
421
|
-
// Set current context and session in pipeline for consistency
|
|
422
|
-
this.responsePipeline.setContext(await this.agent.getContext());
|
|
423
|
-
this.responsePipeline.setCurrentSession(this.agent.currentSession);
|
|
424
|
-
|
|
425
452
|
responseContext = await this.responsePipeline.prepareResponseContext({
|
|
426
453
|
contextOverride,
|
|
427
454
|
session: params.session ? cloneDeep(params.session) : undefined,
|
|
455
|
+
currentContext: await this.agent.getContext(),
|
|
456
|
+
currentSession: this.agent.currentSession,
|
|
428
457
|
});
|
|
429
458
|
} catch (error) {
|
|
430
459
|
throw ResponseGenerationError.fromError(error, 'pipeline_context_preparation', params);
|
|
431
460
|
}
|
|
432
461
|
|
|
433
|
-
const { effectiveContext } = responseContext;
|
|
462
|
+
const { effectiveContext, contextAfterHook } = responseContext;
|
|
434
463
|
let session = responseContext.session;
|
|
435
464
|
|
|
436
|
-
//
|
|
437
|
-
|
|
438
|
-
if (storedContext !== undefined) {
|
|
465
|
+
// Sync the beforeRespond hook's context result back to the agent
|
|
466
|
+
if (contextAfterHook !== undefined) {
|
|
439
467
|
try {
|
|
440
|
-
await this.agent.updateContext(
|
|
468
|
+
await this.agent.updateContext(contextAfterHook as Partial<TContext>);
|
|
441
469
|
} catch (error) {
|
|
442
|
-
throw ResponseGenerationError.fromError(error, 'context_update_from_pipeline', params, {
|
|
470
|
+
throw ResponseGenerationError.fromError(error, 'context_update_from_pipeline', params, { contextAfterHook });
|
|
443
471
|
}
|
|
444
472
|
}
|
|
445
473
|
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
474
|
+
// Apply data staged before any session existed (initialData,
|
|
475
|
+
// pre-session updateCollectedData calls). Reading the live session's
|
|
476
|
+
// data here would leak state across sessions when an explicit
|
|
477
|
+
// session is passed, so only the staging buffer is merged.
|
|
478
|
+
const stagedData = this.agent.consumePendingData();
|
|
479
|
+
if (Object.keys(stagedData).length > 0) {
|
|
449
480
|
try {
|
|
450
|
-
session = mergeCollected(session,
|
|
451
|
-
logger.debug("[ResponseModal] Merged agent
|
|
481
|
+
session = mergeCollected(session, stagedData);
|
|
482
|
+
logger.debug("[ResponseModal] Merged staged agent data into session:", stagedData);
|
|
452
483
|
} catch (error) {
|
|
453
|
-
throw ResponseGenerationError.fromError(error, 'data_merging', params, {
|
|
484
|
+
throw ResponseGenerationError.fromError(error, 'data_merging', params, { stagedData });
|
|
454
485
|
}
|
|
455
486
|
}
|
|
456
487
|
|
|
457
488
|
// PHASE 1: PREPARE - Execute prepare function if current step has one
|
|
458
489
|
try {
|
|
459
|
-
await this.
|
|
490
|
+
await this.stepLifecycle.runPrepare(session, effectiveContext);
|
|
460
491
|
} catch (error) {
|
|
461
492
|
throw ResponseGenerationError.fromError(error, 'step_preparation', params, { session, effectiveContext });
|
|
462
493
|
}
|
|
@@ -475,7 +506,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
475
506
|
signalHaltReply?: string;
|
|
476
507
|
};
|
|
477
508
|
try {
|
|
478
|
-
routingResult = await this.
|
|
509
|
+
routingResult = await this.responsePipeline.routeAndSelectStep({
|
|
479
510
|
session,
|
|
480
511
|
history: historyEvents,
|
|
481
512
|
context: effectiveContext,
|
|
@@ -508,564 +539,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
508
539
|
}
|
|
509
540
|
}
|
|
510
541
|
|
|
511
|
-
/**
|
|
512
|
-
* Unified routing and step selection logic using ResponsePipeline for optimization
|
|
513
|
-
* @private
|
|
514
|
-
*/
|
|
515
|
-
private async handleUnifiedRoutingAndStepSelection(params: {
|
|
516
|
-
session: SessionState<TData>;
|
|
517
|
-
history: Event[]; // Use Event[] for internal processing
|
|
518
|
-
context: TContext;
|
|
519
|
-
signal?: AbortSignal;
|
|
520
|
-
}): Promise<{
|
|
521
|
-
selectedFlow?: Flow<TContext, TData>;
|
|
522
|
-
selectedStep?: Step<TContext, TData>;
|
|
523
|
-
responseDirectives?: string[];
|
|
524
|
-
session: SessionState<TData>;
|
|
525
|
-
isFlowComplete: boolean;
|
|
526
|
-
/** Signal firings from the pre-phase (threaded through for response surface). */
|
|
527
|
-
signalFirings?: SignalFiring<TContext, TData>[];
|
|
528
|
-
/** Non-position signal directive for pre-LLM augmentation (appendPrompt, injectTools, etc). */
|
|
529
|
-
signalPreDirective?: Directive<TContext, TData>;
|
|
530
|
-
/** Pre-signal phase halted the turn. */
|
|
531
|
-
signalHalted?: boolean;
|
|
532
|
-
/** Reply text from the halt directive. */
|
|
533
|
-
signalHaltReply?: string;
|
|
534
|
-
}> {
|
|
535
|
-
try {
|
|
536
|
-
// Create a fresh chain tracker for this turn (Requirement 22.1)
|
|
537
|
-
this.responsePipeline.createChainTracker();
|
|
538
|
-
|
|
539
|
-
// ROUTING SKIP OPTIMIZATION (Requirements 20.1, 20.2, 20.3):
|
|
540
|
-
// When the current step has collect fields AND pre-extraction populates at least
|
|
541
|
-
// one of those fields, skip FlowRouter.decideFlowAndStep for this turn.
|
|
542
|
-
const routingSkipResult = await this.attemptRoutingSkipForCollect(params);
|
|
543
|
-
if (routingSkipResult) {
|
|
544
|
-
// Even when routing is skipped, run pre-signal phase if processor is present
|
|
545
|
-
if (this.agent.signalProcessor) {
|
|
546
|
-
const signalResult = await this.responsePipeline.runPreSignalPhase(
|
|
547
|
-
params.session, params.context, params.history,
|
|
548
|
-
);
|
|
549
|
-
// If signal halts, override the routing skip result
|
|
550
|
-
if (signalResult.mergedDirective?.halt) {
|
|
551
|
-
return {
|
|
552
|
-
...routingSkipResult,
|
|
553
|
-
session: signalResult.updatedSession,
|
|
554
|
-
signalFirings: signalResult.firings,
|
|
555
|
-
signalHalted: true,
|
|
556
|
-
signalHaltReply: signalResult.mergedDirective.reply,
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
// If signal has position fields, override routing skip result
|
|
560
|
-
if (hasDirectivePositionField(signalResult.mergedDirective)) {
|
|
561
|
-
return this.applySignalPositionDirective(
|
|
562
|
-
signalResult, params,
|
|
563
|
-
);
|
|
564
|
-
}
|
|
565
|
-
// Non-position directive: propagate for pre-LLM augmentation
|
|
566
|
-
return {
|
|
567
|
-
...routingSkipResult,
|
|
568
|
-
session: signalResult.updatedSession,
|
|
569
|
-
signalFirings: signalResult.firings,
|
|
570
|
-
signalPreDirective: signalResult.mergedDirective || undefined,
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
return routingSkipResult;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// ── PARALLEL PRE-SIGNAL PHASE + ROUTING (Algorithm 5) ────────────────
|
|
577
|
-
// When signalProcessor is present, run pre-signals in parallel with routing.
|
|
578
|
-
// When absent, call the router directly (zero overhead, preserve current behavior).
|
|
579
|
-
if (this.agent.signalProcessor) {
|
|
580
|
-
// Run pre-signal phase in parallel with routing (Requirement 8.1)
|
|
581
|
-
const [signalResult, routingResult] = await Promise.all([
|
|
582
|
-
this.responsePipeline.runPreSignalPhase(
|
|
583
|
-
params.session, params.context, params.history,
|
|
584
|
-
),
|
|
585
|
-
this.responsePipeline.handleRoutingAndStepSelection({
|
|
586
|
-
session: params.session,
|
|
587
|
-
history: params.history,
|
|
588
|
-
context: params.context,
|
|
589
|
-
signal: params.signal,
|
|
590
|
-
}),
|
|
591
|
-
]);
|
|
592
|
-
|
|
593
|
-
// ── Requirement 8.2: halt → discard routing, skip LLM ────────────
|
|
594
|
-
if (signalResult.mergedDirective?.halt) {
|
|
595
|
-
return {
|
|
596
|
-
selectedFlow: undefined,
|
|
597
|
-
selectedStep: undefined,
|
|
598
|
-
session: signalResult.updatedSession,
|
|
599
|
-
isFlowComplete: false,
|
|
600
|
-
signalFirings: signalResult.firings,
|
|
601
|
-
signalHalted: true,
|
|
602
|
-
signalHaltReply: signalResult.mergedDirective.reply,
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// ── Requirement 8.3: position directive → discard routing, apply signal position ──
|
|
607
|
-
if (hasDirectivePositionField(signalResult.mergedDirective)) {
|
|
608
|
-
return this.applySignalPositionDirective(
|
|
609
|
-
signalResult, params,
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// ── Requirement 8.4: non-position directive → use routing, propagate augmentation ──
|
|
614
|
-
// ── Requirement 8.5: no directive → use routing as-is ─────────────
|
|
615
|
-
let updatedSession = signalResult.updatedSession;
|
|
616
|
-
|
|
617
|
-
// Apply data/context updates from signal to the routed session
|
|
618
|
-
if (signalResult.mergedDirective?.dataUpdate) {
|
|
619
|
-
updatedSession = mergeCollected(updatedSession, signalResult.mergedDirective.dataUpdate);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Use routing result for flow/step, but carry signal session state
|
|
623
|
-
// Merge routing session changes on top of signal session
|
|
624
|
-
const routingSession = routingResult.session;
|
|
625
|
-
updatedSession = {
|
|
626
|
-
...updatedSession,
|
|
627
|
-
currentFlow: routingSession.currentFlow,
|
|
628
|
-
currentStep: routingSession.currentStep,
|
|
629
|
-
flowHistory: routingSession.flowHistory,
|
|
630
|
-
pendingDirective: routingSession.pendingDirective,
|
|
631
|
-
};
|
|
632
|
-
|
|
633
|
-
const isFlowComplete = routingResult.isFlowComplete;
|
|
634
|
-
|
|
635
|
-
// PRE-EXTRACTION: same logic as below — extract data from user message
|
|
636
|
-
if (routingResult.selectedFlow && !isFlowComplete) {
|
|
637
|
-
if (this.shouldPreExtractData(routingResult.selectedFlow)) {
|
|
638
|
-
logger.debug(
|
|
639
|
-
`[ResponseModal] Pre-extracting data for flow: ${routingResult.selectedFlow.title}`
|
|
640
|
-
);
|
|
641
|
-
const extractedData = await this.preExtractFlowData({
|
|
642
|
-
route: routingResult.selectedFlow,
|
|
643
|
-
history: params.history,
|
|
644
|
-
context: params.context,
|
|
645
|
-
session: updatedSession,
|
|
646
|
-
signal: params.signal,
|
|
647
|
-
});
|
|
648
|
-
if (extractedData && Object.keys(extractedData).length > 0) {
|
|
649
|
-
logger.debug(`[ResponseModal] Pre-extracted data:`, extractedData);
|
|
650
|
-
updatedSession = mergeCollected(updatedSession, extractedData);
|
|
651
|
-
await this.agent.updateCollectedData(extractedData);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// Determine next step
|
|
657
|
-
const stepResult = await this.responsePipeline.determineNextStep({
|
|
658
|
-
selectedFlow: routingResult.selectedFlow,
|
|
659
|
-
selectedStep: routingResult.selectedStep,
|
|
660
|
-
session: updatedSession,
|
|
661
|
-
isFlowComplete,
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
return {
|
|
665
|
-
selectedFlow: stepResult.flowChanged || routingResult.selectedFlow,
|
|
666
|
-
selectedStep: stepResult.nextStep,
|
|
667
|
-
responseDirectives: routingResult.responseDirectives,
|
|
668
|
-
session: stepResult.session,
|
|
669
|
-
isFlowComplete: stepResult.flowChanged ? false : isFlowComplete,
|
|
670
|
-
signalFirings: signalResult.firings,
|
|
671
|
-
signalPreDirective: signalResult.mergedDirective || undefined,
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// ── No signal processor: existing behavior (zero overhead) ────────────
|
|
676
|
-
const routingResult = await this.responsePipeline.handleRoutingAndStepSelection({
|
|
677
|
-
session: params.session,
|
|
678
|
-
history: params.history,
|
|
679
|
-
context: params.context,
|
|
680
|
-
signal: params.signal,
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
let updatedSession = routingResult.session;
|
|
684
|
-
const isFlowComplete = routingResult.isFlowComplete;
|
|
685
|
-
|
|
686
|
-
// PRE-EXTRACTION: If entering a flow that collects data, extract data from user message first
|
|
687
|
-
// This allows us to skip steps whose data is already provided
|
|
688
|
-
if (routingResult.selectedFlow && !isFlowComplete) {
|
|
689
|
-
// Always pre-extract when flow collects data (not just on new flow entry)
|
|
690
|
-
// This ensures step selection has the most up-to-date data
|
|
691
|
-
if (this.shouldPreExtractData(routingResult.selectedFlow)) {
|
|
692
|
-
logger.debug(
|
|
693
|
-
`[ResponseModal] Pre-extracting data for flow: ${routingResult.selectedFlow.title}`
|
|
694
|
-
);
|
|
695
|
-
|
|
696
|
-
const extractedData = await this.preExtractFlowData({
|
|
697
|
-
route: routingResult.selectedFlow,
|
|
698
|
-
history: params.history,
|
|
699
|
-
context: params.context,
|
|
700
|
-
session: updatedSession,
|
|
701
|
-
signal: params.signal,
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
if (extractedData && Object.keys(extractedData).length > 0) {
|
|
705
|
-
logger.debug(
|
|
706
|
-
`[ResponseModal] Pre-extracted data:`,
|
|
707
|
-
extractedData
|
|
708
|
-
);
|
|
709
|
-
// Merge pre-extracted data into session before step selection
|
|
710
|
-
updatedSession = mergeCollected(updatedSession, extractedData);
|
|
711
|
-
// Also update agent's collected data
|
|
712
|
-
await this.agent.updateCollectedData(extractedData);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
// Determine next step using pipeline method for consistency
|
|
718
|
-
const stepResult = await this.responsePipeline.determineNextStep({
|
|
719
|
-
selectedFlow: routingResult.selectedFlow,
|
|
720
|
-
selectedStep: routingResult.selectedStep,
|
|
721
|
-
session: updatedSession, // Use updated session with pre-extracted data
|
|
722
|
-
isFlowComplete, // Use updated completion status
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
return {
|
|
726
|
-
selectedFlow: stepResult.flowChanged || routingResult.selectedFlow,
|
|
727
|
-
selectedStep: stepResult.nextStep, // Use the determined next step
|
|
728
|
-
responseDirectives: routingResult.responseDirectives,
|
|
729
|
-
session: stepResult.session,
|
|
730
|
-
// If a branch changed the flow, the original isFlowComplete no longer applies
|
|
731
|
-
isFlowComplete: stepResult.flowChanged ? false : isFlowComplete,
|
|
732
|
-
};
|
|
733
|
-
} catch (error) {
|
|
734
|
-
throw ResponseGenerationError.fromError(error, 'routing_optimization', params);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
/**
|
|
739
|
-
* Apply a signal's position directive (goTo, goToStep, complete, abort, reset).
|
|
740
|
-
* Discards routing result and uses the signal's position decision.
|
|
741
|
-
* @private
|
|
742
|
-
* @requirements 8.3
|
|
743
|
-
*/
|
|
744
|
-
private applySignalPositionDirective(
|
|
745
|
-
signalResult: {
|
|
746
|
-
firings: SignalFiring<TContext, TData>[];
|
|
747
|
-
updatedSession: SessionState<TData>;
|
|
748
|
-
mergedDirective: Directive<TContext, TData> | undefined;
|
|
749
|
-
},
|
|
750
|
-
_params: { session: SessionState<TData>; history: Event[]; context: TContext },
|
|
751
|
-
): {
|
|
752
|
-
selectedFlow?: Flow<TContext, TData>;
|
|
753
|
-
selectedStep?: Step<TContext, TData>;
|
|
754
|
-
responseDirectives?: string[];
|
|
755
|
-
session: SessionState<TData>;
|
|
756
|
-
isFlowComplete: boolean;
|
|
757
|
-
signalFirings?: SignalFiring<TContext, TData>[];
|
|
758
|
-
signalPreDirective?: Directive<TContext, TData>;
|
|
759
|
-
signalHalted?: boolean;
|
|
760
|
-
signalHaltReply?: string;
|
|
761
|
-
} {
|
|
762
|
-
const directive = signalResult.mergedDirective!;
|
|
763
|
-
let session = signalResult.updatedSession;
|
|
764
|
-
const flows = this.agent.getFlows();
|
|
765
|
-
let selectedFlow: Flow<TContext, TData> | undefined;
|
|
766
|
-
let selectedStep: Step<TContext, TData> | undefined;
|
|
767
|
-
let isFlowComplete = false;
|
|
768
|
-
|
|
769
|
-
// Apply data updates if present alongside position
|
|
770
|
-
if (directive.dataUpdate) {
|
|
771
|
-
session = mergeCollected(session, directive.dataUpdate);
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
if (directive.goTo) {
|
|
775
|
-
const flowTarget = typeof directive.goTo === 'string'
|
|
776
|
-
? directive.goTo
|
|
777
|
-
: directive.goTo.flow ?? directive.goTo.step;
|
|
778
|
-
|
|
779
|
-
if (flowTarget) {
|
|
780
|
-
const targetFlow = flows.find(f => f.id === flowTarget || f.title === flowTarget);
|
|
781
|
-
if (targetFlow) {
|
|
782
|
-
session = enterFlow(session, targetFlow.id, targetFlow.title);
|
|
783
|
-
selectedFlow = targetFlow;
|
|
784
|
-
|
|
785
|
-
if (typeof directive.goTo === 'object' && directive.goTo.step) {
|
|
786
|
-
const targetStep = targetFlow.getStep(directive.goTo.step);
|
|
787
|
-
if (targetStep) {
|
|
788
|
-
session = enterStep(session, targetStep.id, targetStep.description);
|
|
789
|
-
selectedStep = targetStep;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
} else {
|
|
793
|
-
logger.warn(`[Signals] Pre-phase goTo target not found: "${flowTarget}". Falling back to no flow.`);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
} else if (directive.goToStep) {
|
|
797
|
-
const stepTarget = typeof directive.goToStep === 'string'
|
|
798
|
-
? directive.goToStep
|
|
799
|
-
: directive.goToStep.step;
|
|
800
|
-
const flowTarget = typeof directive.goToStep === 'object'
|
|
801
|
-
? directive.goToStep.flow
|
|
802
|
-
: undefined;
|
|
803
|
-
|
|
804
|
-
if (flowTarget) {
|
|
805
|
-
const targetFlow = flows.find(f => f.id === flowTarget || f.title === flowTarget);
|
|
806
|
-
if (targetFlow) {
|
|
807
|
-
session = enterFlow(session, targetFlow.id, targetFlow.title);
|
|
808
|
-
selectedFlow = targetFlow;
|
|
809
|
-
const targetStep = targetFlow.getStep(stepTarget);
|
|
810
|
-
if (targetStep) {
|
|
811
|
-
session = enterStep(session, targetStep.id, targetStep.description);
|
|
812
|
-
selectedStep = targetStep;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
} else if (session.currentFlow) {
|
|
816
|
-
const currentFlow = flows.find(f => f.id === session.currentFlow?.id);
|
|
817
|
-
if (currentFlow) {
|
|
818
|
-
selectedFlow = currentFlow;
|
|
819
|
-
const targetStep = currentFlow.getStep(stepTarget);
|
|
820
|
-
if (targetStep) {
|
|
821
|
-
session = enterStep(session, targetStep.id, targetStep.description);
|
|
822
|
-
selectedStep = targetStep;
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
} else if (directive.complete) {
|
|
827
|
-
isFlowComplete = true;
|
|
828
|
-
} else if (directive.abort) {
|
|
829
|
-
// Abort — no flow, session cleared or marked
|
|
830
|
-
isFlowComplete = true;
|
|
831
|
-
} else if (directive.reset) {
|
|
832
|
-
if (session.currentFlow) {
|
|
833
|
-
const currentFlow = flows.find(f => f.id === session.currentFlow?.id);
|
|
834
|
-
if (currentFlow) {
|
|
835
|
-
selectedFlow = currentFlow;
|
|
836
|
-
const resetStep = typeof directive.reset === 'object' && directive.reset.step
|
|
837
|
-
? directive.reset.step
|
|
838
|
-
: undefined;
|
|
839
|
-
if (resetStep) {
|
|
840
|
-
const targetStep = currentFlow.getStep(resetStep);
|
|
841
|
-
if (targetStep) {
|
|
842
|
-
session = enterStep(session, targetStep.id, targetStep.description);
|
|
843
|
-
selectedStep = targetStep;
|
|
844
|
-
}
|
|
845
|
-
} else {
|
|
846
|
-
const initialStep = currentFlow.initialStep;
|
|
847
|
-
session = enterStep(session, initialStep.id, initialStep.description);
|
|
848
|
-
selectedStep = initialStep;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
return {
|
|
855
|
-
selectedFlow,
|
|
856
|
-
selectedStep,
|
|
857
|
-
session,
|
|
858
|
-
isFlowComplete,
|
|
859
|
-
signalFirings: signalResult.firings,
|
|
860
|
-
signalPreDirective: signalResult.mergedDirective || undefined,
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
/**
|
|
865
|
-
* Routing skip optimization (Requirements 20.1, 20.2, 20.3):
|
|
866
|
-
* When the current step declares `collect` fields AND pre-extraction populates
|
|
867
|
-
* at least one of those fields from the user's message, skip routing for this turn.
|
|
868
|
-
*
|
|
869
|
-
* Returns the routing result if the skip applies, or undefined to fall through
|
|
870
|
-
* to normal routing.
|
|
871
|
-
* @private
|
|
872
|
-
*/
|
|
873
|
-
private async attemptRoutingSkipForCollect(params: {
|
|
874
|
-
session: SessionState<TData>;
|
|
875
|
-
history: Event[];
|
|
876
|
-
context: TContext;
|
|
877
|
-
signal?: AbortSignal;
|
|
878
|
-
}): Promise<{
|
|
879
|
-
selectedFlow?: Flow<TContext, TData>;
|
|
880
|
-
selectedStep?: Step<TContext, TData>;
|
|
881
|
-
responseDirectives?: string[];
|
|
882
|
-
session: SessionState<TData>;
|
|
883
|
-
isFlowComplete: boolean;
|
|
884
|
-
} | undefined> {
|
|
885
|
-
const { session } = params;
|
|
886
|
-
|
|
887
|
-
// Only applies when we already have a current flow and step
|
|
888
|
-
if (!session.currentFlow || !session.currentStep) {
|
|
889
|
-
return undefined;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
// Also skip this optimization if there's a pending directive (it takes priority)
|
|
893
|
-
if (session.pendingDirective) {
|
|
894
|
-
return undefined;
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
// Look up the actual Flow and Step objects to access `collect`
|
|
898
|
-
const currentFlow = this.agent.getFlows().find(
|
|
899
|
-
(f) => f.id === session.currentFlow?.id
|
|
900
|
-
);
|
|
901
|
-
if (!currentFlow) {
|
|
902
|
-
return undefined;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
const currentStep = currentFlow.getStep(session.currentStep.id);
|
|
906
|
-
if (!currentStep || !currentStep.collect || currentStep.collect.length === 0) {
|
|
907
|
-
return undefined;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// We have a step with collect fields. Run pre-extraction to see if the
|
|
911
|
-
// user's message populates any of them.
|
|
912
|
-
const collectFields = currentStep.collect;
|
|
913
|
-
|
|
914
|
-
// Snapshot current data for comparison
|
|
915
|
-
const dataBefore = { ...session.data };
|
|
916
|
-
|
|
917
|
-
// Run pre-extraction against the current flow
|
|
918
|
-
const extractedData = await this.preExtractFlowData({
|
|
919
|
-
route: currentFlow,
|
|
920
|
-
history: params.history,
|
|
921
|
-
context: params.context,
|
|
922
|
-
session,
|
|
923
|
-
signal: params.signal,
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
if (!extractedData || Object.keys(extractedData).length === 0) {
|
|
927
|
-
return undefined;
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// Determine which collect fields were newly populated by pre-extraction
|
|
931
|
-
const populatedCollectFields: string[] = [];
|
|
932
|
-
for (const field of collectFields) {
|
|
933
|
-
const key = field as string;
|
|
934
|
-
const hadValue = dataBefore[field] !== undefined && dataBefore[field] !== null;
|
|
935
|
-
const hasNewValue = extractedData[field] !== undefined && extractedData[field] !== null;
|
|
936
|
-
if (hasNewValue && !hadValue) {
|
|
937
|
-
populatedCollectFields.push(key);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
if (populatedCollectFields.length === 0) {
|
|
942
|
-
// Pre-extraction didn't populate any declared collect field — no skip
|
|
943
|
-
return undefined;
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
// ROUTING SKIP: pre-extraction populated collect fields → retain current flow/step
|
|
947
|
-
logger.debug(
|
|
948
|
-
`[ResponseModal] Routing skip: pre-extraction populated collect fields [${populatedCollectFields.join(', ')}] for step "${currentStep.id}" — skipping FlowRouter`
|
|
949
|
-
);
|
|
950
|
-
|
|
951
|
-
// Merge extracted data into session
|
|
952
|
-
const updatedSession = mergeCollected(session, extractedData);
|
|
953
|
-
await this.agent.updateCollectedData(extractedData);
|
|
954
|
-
|
|
955
|
-
// Determine next step using pipeline method for consistency
|
|
956
|
-
// Pass the current flow/step as the routing result (retained)
|
|
957
|
-
const stepResult = await this.responsePipeline.determineNextStep({
|
|
958
|
-
selectedFlow: currentFlow,
|
|
959
|
-
selectedStep: currentStep,
|
|
960
|
-
session: updatedSession,
|
|
961
|
-
isFlowComplete: false,
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
return {
|
|
965
|
-
selectedFlow: stepResult.flowChanged || currentFlow,
|
|
966
|
-
selectedStep: stepResult.nextStep,
|
|
967
|
-
responseDirectives: undefined,
|
|
968
|
-
session: stepResult.session,
|
|
969
|
-
isFlowComplete: stepResult.flowChanged ? false : false,
|
|
970
|
-
};
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
/**
|
|
974
|
-
* Check if a flow should pre-extract data before determining the initial step
|
|
975
|
-
* @private
|
|
976
|
-
*/
|
|
977
|
-
private shouldPreExtractData(flow: Flow<TContext, TData>): boolean {
|
|
978
|
-
// Pre-extract if flow has declared required or optional fields
|
|
979
|
-
if (flow.requiredFields && flow.requiredFields.length > 0) {
|
|
980
|
-
return true;
|
|
981
|
-
}
|
|
982
|
-
if (flow.optionalFields && flow.optionalFields.length > 0) {
|
|
983
|
-
return true;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
// Pre-extract if any step in the flow collects data
|
|
987
|
-
const steps = flow.getAllSteps();
|
|
988
|
-
const hasDataCollectionSteps = steps.some(
|
|
989
|
-
step => step.collect && step.collect.length > 0
|
|
990
|
-
);
|
|
991
|
-
|
|
992
|
-
return hasDataCollectionSteps;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
/**
|
|
996
|
-
* Pre-extract data from user message when entering a flow
|
|
997
|
-
* This allows skipping steps whose data is already provided
|
|
998
|
-
* @private
|
|
999
|
-
*/
|
|
1000
|
-
private async preExtractFlowData(params: {
|
|
1001
|
-
route: Flow<TContext, TData>;
|
|
1002
|
-
history: Event[];
|
|
1003
|
-
context: TContext;
|
|
1004
|
-
session: SessionState<TData>;
|
|
1005
|
-
signal?: AbortSignal;
|
|
1006
|
-
}): Promise<Partial<TData>> {
|
|
1007
|
-
const { route: flow, history, signal } = params;
|
|
1008
|
-
|
|
1009
|
-
// Build a schema for data extraction based on flow's fields
|
|
1010
|
-
const extractionSchema = this.agent.schema;
|
|
1011
|
-
if (!extractionSchema) {
|
|
1012
|
-
logger.warn(`[ResponseModal] No schema available for pre-extraction`);
|
|
1013
|
-
return {};
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
// Get last user message
|
|
1017
|
-
const lastMessage = getLastMessageFromHistory(history);
|
|
1018
|
-
|
|
1019
|
-
// Build extraction prompt
|
|
1020
|
-
const extractionPrompt = [
|
|
1021
|
-
`Extract any relevant information from the user's message that matches the following data fields.`,
|
|
1022
|
-
`Only extract information that is explicitly stated or clearly implied.`,
|
|
1023
|
-
``,
|
|
1024
|
-
`User's message: "${lastMessage}"`,
|
|
1025
|
-
``,
|
|
1026
|
-
`Extract data for these fields if present:`,
|
|
1027
|
-
];
|
|
1028
|
-
|
|
1029
|
-
// Add field descriptions
|
|
1030
|
-
if (flow.requiredFields) {
|
|
1031
|
-
extractionPrompt.push(`Required fields: ${flow.requiredFields.join(', ')}`);
|
|
1032
|
-
}
|
|
1033
|
-
if (flow.optionalFields) {
|
|
1034
|
-
extractionPrompt.push(`Optional fields: ${flow.optionalFields.join(', ')}`);
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
extractionPrompt.push(
|
|
1038
|
-
``,
|
|
1039
|
-
`Return ONLY the extracted data as JSON. If no data can be extracted, return an empty object {}.`
|
|
1040
|
-
);
|
|
1041
|
-
|
|
1042
|
-
// Convert Event[] to HistoryItem[] for provider call
|
|
1043
|
-
const historyItems = eventsToHistory(history);
|
|
1044
|
-
|
|
1045
|
-
// Call AI to extract data
|
|
1046
|
-
const agentOptions = this.agent.getAgentOptions();
|
|
1047
|
-
try {
|
|
1048
|
-
const result = await agentOptions.provider.generateMessage<TContext, Partial<TData>>({
|
|
1049
|
-
prompt: extractionPrompt.join('\n'),
|
|
1050
|
-
history: historyItems,
|
|
1051
|
-
context: {} as TContext, // Passed as empty object so AI doesn't "extract" from context
|
|
1052
|
-
// NOTE: context is intentionally NOT passed here.
|
|
1053
|
-
// Passing context caused the AI to "extract" data from the lead's context
|
|
1054
|
-
// (e.g., name, sector, city) instead of from what the user actually said.
|
|
1055
|
-
signal,
|
|
1056
|
-
parameters: {
|
|
1057
|
-
jsonSchema: extractionSchema,
|
|
1058
|
-
schemaName: 'data_extraction',
|
|
1059
|
-
},
|
|
1060
|
-
});
|
|
1061
|
-
|
|
1062
|
-
return result.structured || {};
|
|
1063
|
-
} catch (error) {
|
|
1064
|
-
logger.error(`[ResponseModal] Pre-extraction failed:`, error);
|
|
1065
|
-
return {};
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
542
|
/**
|
|
1070
543
|
* Unified response generation for non-streaming responses
|
|
1071
544
|
* @private
|
|
@@ -1101,26 +574,21 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1101
574
|
if (signalHalted) {
|
|
1102
575
|
const haltMessage = signalHaltReply || '';
|
|
1103
576
|
// Run post-signal phase even on halt (post-phase sees complete turn context)
|
|
1104
|
-
const
|
|
1105
|
-
session, effectiveContext, historyEvents,
|
|
1106
|
-
);
|
|
1107
|
-
session =
|
|
1108
|
-
signalFirings.push(...
|
|
1109
|
-
|
|
1110
|
-
// Apply post-phase position directive as pendingDirective (Requirement 9.3)
|
|
1111
|
-
if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
|
|
1112
|
-
session = { ...session, pendingDirective: postResult.mergedDirective };
|
|
1113
|
-
}
|
|
577
|
+
const post = await this.signalCoordinator.applyPostPhase({
|
|
578
|
+
session, context: effectiveContext, historyEvents, message: haltMessage,
|
|
579
|
+
});
|
|
580
|
+
session = post.session;
|
|
581
|
+
signalFirings.push(...post.firings);
|
|
582
|
+
const message = post.message;
|
|
1114
583
|
|
|
1115
|
-
await this.finalizeSession(session, effectiveContext);
|
|
1116
584
|
return {
|
|
1117
|
-
message
|
|
585
|
+
message,
|
|
1118
586
|
session,
|
|
1119
587
|
toolCalls: undefined,
|
|
1120
588
|
isFlowComplete: false,
|
|
1121
589
|
executedSteps: [],
|
|
1122
|
-
stoppedReason:
|
|
1123
|
-
triggeredSignals: signalFirings.length > 0 ? signalFirings
|
|
590
|
+
stoppedReason: haltMessage ? 'reply' : 'halt',
|
|
591
|
+
triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
|
|
1124
592
|
};
|
|
1125
593
|
}
|
|
1126
594
|
|
|
@@ -1130,8 +598,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1130
598
|
let stoppedReason: StoppedReason | undefined;
|
|
1131
599
|
let appliedInstructions: AppliedInstruction[] | undefined;
|
|
1132
600
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
601
|
if (selectedFlow && !isFlowComplete) {
|
|
1136
602
|
// AUTO-CHAIN: Walk consecutive auto-steps before any LLM work.
|
|
1137
603
|
// If the current step is auto, the executor advances through it (and any
|
|
@@ -1153,13 +619,13 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1153
619
|
|
|
1154
620
|
session = autoResult.session;
|
|
1155
621
|
|
|
1156
|
-
// Handle halt: emit verbatim reply,
|
|
622
|
+
// Handle halt: emit verbatim reply, return — no LLM call.
|
|
623
|
+
// respond() finalizes the returned session exactly once.
|
|
1157
624
|
if (autoResult.stoppedReason === 'halt') {
|
|
1158
625
|
message = autoResult.mergedDirective?.reply || '';
|
|
1159
626
|
stoppedReason = 'halt';
|
|
1160
627
|
executedSteps = [];
|
|
1161
628
|
|
|
1162
|
-
await this.finalizeSession(session, effectiveContext);
|
|
1163
629
|
return {
|
|
1164
630
|
message,
|
|
1165
631
|
session,
|
|
@@ -1183,7 +649,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1183
649
|
history,
|
|
1184
650
|
});
|
|
1185
651
|
|
|
1186
|
-
await this.finalizeSession(session, effectiveContext);
|
|
1187
652
|
return {
|
|
1188
653
|
message: '',
|
|
1189
654
|
session,
|
|
@@ -1272,20 +737,13 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1272
737
|
// Runs after finalize/onComplete and before session persistence.
|
|
1273
738
|
// Post-phase signals see the complete turn result: assistant message in
|
|
1274
739
|
// history, collected data, tool results.
|
|
1275
|
-
const
|
|
1276
|
-
session, effectiveContext, historyEvents,
|
|
1277
|
-
);
|
|
1278
|
-
session =
|
|
1279
|
-
|
|
740
|
+
const post = await this.signalCoordinator.applyPostPhase({
|
|
741
|
+
session, context: effectiveContext, historyEvents, message,
|
|
742
|
+
});
|
|
743
|
+
session = post.session;
|
|
1280
744
|
// Append post-phase firings to the accumulator (preserves fire order)
|
|
1281
|
-
signalFirings.push(...
|
|
1282
|
-
|
|
1283
|
-
// Requirement 9.3: Post-phase position directive sets session.pendingDirective
|
|
1284
|
-
// (no mid-turn re-entry per D6 decision). Pre-LLM-only fields are already
|
|
1285
|
-
// dropped inside runPostSignalPhase per Phase 4.5.
|
|
1286
|
-
if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
|
|
1287
|
-
session = { ...session, pendingDirective: postResult.mergedDirective };
|
|
1288
|
-
}
|
|
745
|
+
signalFirings.push(...post.firings);
|
|
746
|
+
message = post.message;
|
|
1289
747
|
|
|
1290
748
|
// Ensure response structure completeness (Requirement 8.1, 8.2, 8.3)
|
|
1291
749
|
// - executedSteps: array of steps executed (empty array if none)
|
|
@@ -1299,67 +757,10 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1299
757
|
executedSteps: executedSteps || [],
|
|
1300
758
|
stoppedReason,
|
|
1301
759
|
appliedInstructions,
|
|
1302
|
-
triggeredSignals: signalFirings.length > 0 ? signalFirings
|
|
760
|
+
triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
|
|
1303
761
|
};
|
|
1304
762
|
}
|
|
1305
763
|
|
|
1306
|
-
/**
|
|
1307
|
-
* Execute prepare function for current step if available
|
|
1308
|
-
* @private
|
|
1309
|
-
*/
|
|
1310
|
-
private async executeStepPrepare(session: SessionState<TData>, context: TContext): Promise<void> {
|
|
1311
|
-
if (session.currentFlow && session.currentStep) {
|
|
1312
|
-
const currentFlow = this.agent.getFlows().find(
|
|
1313
|
-
(r) => r.id === session.currentFlow?.id
|
|
1314
|
-
);
|
|
1315
|
-
if (currentFlow) {
|
|
1316
|
-
const currentStep = currentFlow.getStep(session.currentStep.id);
|
|
1317
|
-
// Skip auto-steps — their prepare is handled by AutoChainExecutor
|
|
1318
|
-
if (currentStep?.auto) {
|
|
1319
|
-
logger.debug(`[ResponseModal] Skipping pre-routing prepare for auto-step: ${currentStep.id}`);
|
|
1320
|
-
return;
|
|
1321
|
-
}
|
|
1322
|
-
if (currentStep?.prepare) {
|
|
1323
|
-
logger.debug(`[ResponseModal] Executing prepare for step: ${currentStep.id}`);
|
|
1324
|
-
await this.executePrepareFinalize(
|
|
1325
|
-
currentStep.prepare,
|
|
1326
|
-
context,
|
|
1327
|
-
session.data,
|
|
1328
|
-
currentFlow,
|
|
1329
|
-
currentStep
|
|
1330
|
-
);
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
/**
|
|
1337
|
-
* Execute finalize function for current step if available
|
|
1338
|
-
* @private
|
|
1339
|
-
*/
|
|
1340
|
-
private async executeStepFinalize(session: SessionState<TData>, context: TContext): Promise<void> {
|
|
1341
|
-
if (session.currentFlow && session.currentStep) {
|
|
1342
|
-
const currentFlow = this.agent.getFlows().find(
|
|
1343
|
-
(r) => r.id === session.currentFlow?.id
|
|
1344
|
-
);
|
|
1345
|
-
if (currentFlow) {
|
|
1346
|
-
const currentStep = currentFlow.getStep(session.currentStep.id);
|
|
1347
|
-
if (currentStep?.finalize) {
|
|
1348
|
-
logger.debug(
|
|
1349
|
-
`[ResponseModal] Executing finalize for step: ${currentStep.id}`
|
|
1350
|
-
);
|
|
1351
|
-
await this.executePrepareFinalize(
|
|
1352
|
-
currentStep.finalize,
|
|
1353
|
-
context,
|
|
1354
|
-
session.data,
|
|
1355
|
-
currentFlow,
|
|
1356
|
-
currentStep
|
|
1357
|
-
);
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
764
|
/**
|
|
1364
765
|
* Process flow response with unified tool execution and data collection
|
|
1365
766
|
* @private
|
|
@@ -1393,87 +794,20 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1393
794
|
const { selectedFlow, selectedStep, responseDirectives, history, context, historyEvents, signal, transientAppendage, mergedPreDirective } = params;
|
|
1394
795
|
let session = params.session;
|
|
1395
796
|
|
|
1396
|
-
//
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
logger.debug(`[ResponseModal] Step determination: flow match=${isInSameFlow}, currentFlow=${session.currentFlow?.id}, selectedFlow=${selectedFlow.id}, currentStep=${currentStep?.id || 'none'}`);
|
|
1408
|
-
|
|
1409
|
-
// STEP 1 (Algorithm 1): branches win over linear chain
|
|
1410
|
-
if (currentStep?.branches && currentStep.branches.length > 0) {
|
|
1411
|
-
const branchResult = await this.responsePipeline.evaluateStepBranches(
|
|
1412
|
-
currentStep, selectedFlow, session, context
|
|
1413
|
-
);
|
|
1414
|
-
if (branchResult) {
|
|
1415
|
-
if (branchResult.nextStep) {
|
|
1416
|
-
nextStep = branchResult.nextStep;
|
|
1417
|
-
session = branchResult.session;
|
|
1418
|
-
} else {
|
|
1419
|
-
// Flow transition or completion — no local step to render
|
|
1420
|
-
// Return empty message with updated session; caller handles flow transition
|
|
1421
|
-
return { message: '', session: branchResult.session };
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
if (!nextStep!) {
|
|
1427
|
-
// Get candidate steps based on current position in the flow
|
|
1428
|
-
const flowRouter = this.agent.getFlowRouter();
|
|
1429
|
-
const candidates = await flowRouter.getCandidateStepsWithConditions(
|
|
1430
|
-
selectedFlow,
|
|
1431
|
-
currentStep, // Pass current step instead of undefined to maintain progression
|
|
1432
|
-
createTemplateContext({ data: session.data, session, context })
|
|
1433
|
-
);
|
|
1434
|
-
|
|
1435
|
-
logger.debug(`[ResponseModal] Found ${candidates.length} candidate steps${currentStep ? ' from current step ' + currentStep.id : ' (new flow entry)'}`);
|
|
1436
|
-
|
|
1437
|
-
if (candidates.length > 0) {
|
|
1438
|
-
nextStep = candidates[0].step;
|
|
1439
|
-
logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new flow'}`);
|
|
1440
|
-
} else {
|
|
1441
|
-
// Fallback to initial step even if it should be skipped
|
|
1442
|
-
nextStep = selectedFlow.initialStep;
|
|
1443
|
-
logger.warn(`[FlowConfigurationError] No valid steps found: all candidates were skipped in flow. Falling back to initial step "${nextStep.id}". Review step skip conditions.`);
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
// Update session with next step
|
|
1449
|
-
// If the next step has requires fields that are missing, stay at the previous step
|
|
1450
|
-
if (nextStep.requires && nextStep.requires.length > 0) {
|
|
1451
|
-
const sessionData = session.data || {};
|
|
1452
|
-
const missingRequires = nextStep.requires.filter(
|
|
1453
|
-
field => (sessionData as Record<string, unknown>)[String(field)] === undefined
|
|
1454
|
-
);
|
|
1455
|
-
if (missingRequires.length > 0) {
|
|
1456
|
-
const warning = `[FlowConfigurationError] Cannot advance to step "${nextStep.description || nextStep.id}": ` +
|
|
1457
|
-
`missing required fields [${missingRequires.join(', ')}]. Staying at current step. Ensure preceding steps collect these fields.`;
|
|
1458
|
-
logger.warn(warning);
|
|
1459
|
-
console.warn(warning);
|
|
1460
|
-
// Stay at the current step - don't enter the next one
|
|
1461
|
-
const currentStepId = session.currentStep?.id;
|
|
1462
|
-
if (currentStepId) {
|
|
1463
|
-
const currentStepInstance = selectedFlow.getStep(currentStepId);
|
|
1464
|
-
if (currentStepInstance) {
|
|
1465
|
-
nextStep = currentStepInstance;
|
|
1466
|
-
logger.debug(`[ResponseModal] Staying at current step: ${nextStep.id} due to missing requires`);
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
} else {
|
|
1470
|
-
session = enterStep(session, nextStep.id, nextStep.description);
|
|
1471
|
-
logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
|
|
1472
|
-
}
|
|
1473
|
-
} else {
|
|
1474
|
-
session = enterStep(session, nextStep.id, nextStep.description);
|
|
1475
|
-
logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
|
|
797
|
+
// Resolve the step to render (branches win over linear chain; requires enforced)
|
|
798
|
+
const stepResolution = await this.responsePipeline.resolveRenderStep({
|
|
799
|
+
selectedFlow,
|
|
800
|
+
selectedStep,
|
|
801
|
+
session,
|
|
802
|
+
context,
|
|
803
|
+
});
|
|
804
|
+
if (stepResolution.flowTransition) {
|
|
805
|
+
// Flow transition or completion — no local step to render
|
|
806
|
+
// Return empty message with updated session; caller handles flow transition
|
|
807
|
+
return { message: '', session: stepResolution.session };
|
|
1476
808
|
}
|
|
809
|
+
const nextStep = stepResolution.nextStep!;
|
|
810
|
+
session = stepResolution.session;
|
|
1477
811
|
|
|
1478
812
|
// Build response schema for this flow (with collect fields from step)
|
|
1479
813
|
const responseSchema = this.responseEngine.responseSchemaForFlow(selectedFlow, nextStep, this.agent.schema);
|
|
@@ -1557,7 +891,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1557
891
|
});
|
|
1558
892
|
|
|
1559
893
|
// Execute tools with unified loop handling
|
|
1560
|
-
const toolResult = await this.
|
|
894
|
+
const toolResult = await this.toolLoopExecutor.runLoop({
|
|
1561
895
|
toolCalls,
|
|
1562
896
|
context,
|
|
1563
897
|
session,
|
|
@@ -1624,25 +958,22 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1624
958
|
if (signalHalted) {
|
|
1625
959
|
const haltMessage = signalHaltReply || '';
|
|
1626
960
|
// Run post-signal phase even on halt
|
|
1627
|
-
const
|
|
1628
|
-
session, effectiveContext, historyEvents,
|
|
1629
|
-
);
|
|
1630
|
-
session =
|
|
1631
|
-
signalFirings.push(...
|
|
1632
|
-
|
|
1633
|
-
if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
|
|
1634
|
-
session = { ...session, pendingDirective: postResult.mergedDirective };
|
|
1635
|
-
}
|
|
961
|
+
const post = await this.signalCoordinator.applyPostPhase({
|
|
962
|
+
session, context: effectiveContext, historyEvents, message: haltMessage,
|
|
963
|
+
});
|
|
964
|
+
session = post.session;
|
|
965
|
+
signalFirings.push(...post.firings);
|
|
966
|
+
const message = post.message;
|
|
1636
967
|
|
|
1637
|
-
await this.
|
|
968
|
+
await this.sessionFinalizer.finalize(session, effectiveContext);
|
|
1638
969
|
yield {
|
|
1639
|
-
delta:
|
|
1640
|
-
accumulated:
|
|
970
|
+
delta: message,
|
|
971
|
+
accumulated: message,
|
|
1641
972
|
done: true,
|
|
1642
973
|
session,
|
|
1643
974
|
stoppedReason: haltMessage ? 'reply' : 'halt',
|
|
1644
975
|
executedSteps: [],
|
|
1645
|
-
triggeredSignals: signalFirings.length > 0 ? signalFirings
|
|
976
|
+
triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
|
|
1646
977
|
} as AgentResponseStreamChunk<TData>;
|
|
1647
978
|
return;
|
|
1648
979
|
}
|
|
@@ -1672,7 +1003,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1672
1003
|
// Handle halt: emit verbatim reply as a single chunk, done.
|
|
1673
1004
|
if (autoResult.stoppedReason === 'halt') {
|
|
1674
1005
|
const reply = autoResult.mergedDirective?.reply || '';
|
|
1675
|
-
await this.
|
|
1006
|
+
await this.sessionFinalizer.finalize(session, effectiveContext);
|
|
1676
1007
|
yield {
|
|
1677
1008
|
delta: reply,
|
|
1678
1009
|
accumulated: reply,
|
|
@@ -1680,7 +1011,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1680
1011
|
session,
|
|
1681
1012
|
stoppedReason: 'halt',
|
|
1682
1013
|
executedSteps: [],
|
|
1683
|
-
triggeredSignals: signalFirings.length > 0 ? signalFirings
|
|
1014
|
+
triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
|
|
1684
1015
|
} as AgentResponseStreamChunk<TData>;
|
|
1685
1016
|
return;
|
|
1686
1017
|
}
|
|
@@ -1749,28 +1080,35 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1749
1080
|
});
|
|
1750
1081
|
}
|
|
1751
1082
|
|
|
1752
|
-
// ── Intercept the inner stream
|
|
1753
|
-
//
|
|
1754
|
-
//
|
|
1755
|
-
// (Requirement 11.2).
|
|
1083
|
+
// ── Intercept the inner stream on the final chunk ──────────────────────
|
|
1084
|
+
// Mirrors the non-streaming path: post-signal phase runs first, then the
|
|
1085
|
+
// session (including post-phase mutations) is finalized exactly once,
|
|
1086
|
+
// attaching triggeredSignals to the final chunk (Requirement 11.2).
|
|
1756
1087
|
for await (const chunk of innerStream!) {
|
|
1757
1088
|
if (chunk.done) {
|
|
1758
1089
|
// Run post-signal phase on final chunk (Requirement 9.1, 9.2)
|
|
1759
|
-
const
|
|
1760
|
-
chunk.session || session,
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1090
|
+
const post = await this.signalCoordinator.applyPostPhase({
|
|
1091
|
+
session: chunk.session || session,
|
|
1092
|
+
context: effectiveContext,
|
|
1093
|
+
historyEvents,
|
|
1094
|
+
message: chunk.accumulated,
|
|
1095
|
+
});
|
|
1096
|
+
const finalSession = post.session;
|
|
1097
|
+
signalFirings.push(...post.firings);
|
|
1764
1098
|
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1099
|
+
const accumulated = post.message;
|
|
1100
|
+
const delta = post.replyOverridden ? accumulated : chunk.delta;
|
|
1101
|
+
|
|
1102
|
+
// Single streaming exit: finalize the post-phase session so
|
|
1103
|
+
// post-signal mutations (e.g. pendingDirective) are persisted
|
|
1104
|
+
await this.sessionFinalizer.finalize(finalSession, effectiveContext);
|
|
1769
1105
|
|
|
1770
1106
|
yield {
|
|
1771
1107
|
...chunk,
|
|
1108
|
+
delta,
|
|
1109
|
+
accumulated,
|
|
1772
1110
|
session: finalSession,
|
|
1773
|
-
triggeredSignals: signalFirings.length > 0 ? signalFirings
|
|
1111
|
+
triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
|
|
1774
1112
|
} as AgentResponseStreamChunk<TData>;
|
|
1775
1113
|
} else {
|
|
1776
1114
|
yield chunk;
|
|
@@ -1805,86 +1143,25 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1805
1143
|
const { selectedFlow, selectedStep, responseDirectives, history, context, historyEvents, signal, transientAppendage, mergedPreDirective } = params;
|
|
1806
1144
|
let session = params.session;
|
|
1807
1145
|
|
|
1808
|
-
//
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
// Branch resolved — yield a final chunk with the updated session and return
|
|
1825
|
-
if (branchResult.nextStep) {
|
|
1826
|
-
session = branchResult.session;
|
|
1827
|
-
nextStep = branchResult.nextStep;
|
|
1828
|
-
} else {
|
|
1829
|
-
// Flow transition or completion — no step to render
|
|
1830
|
-
yield {
|
|
1831
|
-
delta: '',
|
|
1832
|
-
accumulated: '',
|
|
1833
|
-
done: true,
|
|
1834
|
-
session: branchResult.session,
|
|
1835
|
-
} as AgentResponseStreamChunk<TData>;
|
|
1836
|
-
return;
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
if (!nextStep!) {
|
|
1842
|
-
// Get candidate steps based on current position in the flow
|
|
1843
|
-
const flowRouter = this.agent.getFlowRouter();
|
|
1844
|
-
const candidates = await flowRouter.getCandidateStepsWithConditions(
|
|
1845
|
-
selectedFlow,
|
|
1846
|
-
currentStep, // Pass current step instead of undefined to maintain progression
|
|
1847
|
-
createTemplateContext({ data: session.data, session, context })
|
|
1848
|
-
);
|
|
1849
|
-
|
|
1850
|
-
if (candidates.length > 0) {
|
|
1851
|
-
nextStep = candidates[0].step;
|
|
1852
|
-
logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new flow'}`);
|
|
1853
|
-
} else {
|
|
1854
|
-
nextStep = selectedFlow.initialStep;
|
|
1855
|
-
logger.warn(`[FlowConfigurationError] No valid steps found: all candidates were skipped in flow. Falling back to initial step "${nextStep.id}". Review step skip conditions.`);
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
// Update session with next step
|
|
1861
|
-
// If the next step has requires fields that are missing, stay at the previous step
|
|
1862
|
-
if (nextStep.requires && nextStep.requires.length > 0) {
|
|
1863
|
-
const sessionData = session.data || {};
|
|
1864
|
-
const missingRequires = nextStep.requires.filter(
|
|
1865
|
-
field => (sessionData as Record<string, unknown>)[String(field)] === undefined
|
|
1866
|
-
);
|
|
1867
|
-
if (missingRequires.length > 0) {
|
|
1868
|
-
const warning = `[FlowConfigurationError] Cannot advance to step "${nextStep.description || nextStep.id}": ` +
|
|
1869
|
-
`missing required fields [${missingRequires.join(', ')}]. Staying at current step. Ensure preceding steps collect these fields.`;
|
|
1870
|
-
logger.warn(warning);
|
|
1871
|
-
console.warn(warning);
|
|
1872
|
-
const currentStepId = session.currentStep?.id;
|
|
1873
|
-
if (currentStepId) {
|
|
1874
|
-
const currentStepInstance = selectedFlow.getStep(currentStepId);
|
|
1875
|
-
if (currentStepInstance) {
|
|
1876
|
-
nextStep = currentStepInstance;
|
|
1877
|
-
logger.debug(`[ResponseModal] Staying at current step: ${nextStep.id} due to missing requires`);
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
} else {
|
|
1881
|
-
session = enterStep(session, nextStep.id, nextStep.description);
|
|
1882
|
-
logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
|
|
1883
|
-
}
|
|
1884
|
-
} else {
|
|
1885
|
-
session = enterStep(session, nextStep.id, nextStep.description);
|
|
1886
|
-
logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
|
|
1146
|
+
// Resolve the step to render (same logic as non-streaming)
|
|
1147
|
+
const stepResolution = await this.responsePipeline.resolveRenderStep({
|
|
1148
|
+
selectedFlow,
|
|
1149
|
+
selectedStep,
|
|
1150
|
+
session,
|
|
1151
|
+
context,
|
|
1152
|
+
});
|
|
1153
|
+
if (stepResolution.flowTransition) {
|
|
1154
|
+
// Flow transition or completion — no step to render
|
|
1155
|
+
yield {
|
|
1156
|
+
delta: '',
|
|
1157
|
+
accumulated: '',
|
|
1158
|
+
done: true,
|
|
1159
|
+
session: stepResolution.session,
|
|
1160
|
+
} as AgentResponseStreamChunk<TData>;
|
|
1161
|
+
return;
|
|
1887
1162
|
}
|
|
1163
|
+
const nextStep = stepResolution.nextStep!;
|
|
1164
|
+
session = stepResolution.session;
|
|
1888
1165
|
|
|
1889
1166
|
// Build response schema and prompt (same as non-streaming)
|
|
1890
1167
|
const responseSchema = this.responseEngine.responseSchemaForFlow(selectedFlow, nextStep, this.agent.schema);
|
|
@@ -1896,7 +1173,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1896
1173
|
const reply = mergedPreDirective.reply || '';
|
|
1897
1174
|
const reason: StoppedReason = mergedPreDirective.reply ? 'reply' : 'halt';
|
|
1898
1175
|
logger.debug(`[ResponseModal] Halt (streaming) — skipping LLM call for step ${nextStep.id}, stoppedReason: ${reason}`);
|
|
1899
|
-
await this.finalizeSession(session, context);
|
|
1900
1176
|
yield {
|
|
1901
1177
|
delta: reply,
|
|
1902
1178
|
accumulated: reply,
|
|
@@ -1918,13 +1194,12 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1918
1194
|
createTemplateContext({ data: session.data || {}, context, session })
|
|
1919
1195
|
);
|
|
1920
1196
|
logger.debug(`[ResponseModal] Step.reply (streaming) — skipping LLM call for step ${nextStep.id}`);
|
|
1921
|
-
await this.finalizeSession(session, context);
|
|
1922
1197
|
yield {
|
|
1923
1198
|
delta: effectiveReply,
|
|
1924
1199
|
accumulated: effectiveReply,
|
|
1925
1200
|
done: true,
|
|
1926
1201
|
session,
|
|
1927
|
-
stoppedReason: 'reply'
|
|
1202
|
+
stoppedReason: 'reply',
|
|
1928
1203
|
executedSteps: [{ id: nextStep.id, flowId: selectedFlow.id }],
|
|
1929
1204
|
} as AgentResponseStreamChunk<TData>;
|
|
1930
1205
|
return;
|
|
@@ -1973,73 +1248,23 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1973
1248
|
if (chunk.done && chunk.structured?.toolCalls) {
|
|
1974
1249
|
toolCalls = chunk.structured.toolCalls;
|
|
1975
1250
|
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
history: historyEvents,
|
|
1994
|
-
signal,
|
|
1995
|
-
flow: selectedFlow,
|
|
1996
|
-
step: nextStep,
|
|
1997
|
-
})) {
|
|
1998
|
-
// Apply context updates
|
|
1999
|
-
if (update.contextUpdate) {
|
|
2000
|
-
try {
|
|
2001
|
-
await this.agent.updateContext(update.contextUpdate as Partial<TContext>);
|
|
2002
|
-
} catch (error) {
|
|
2003
|
-
logger.error(`[ResponseModal] Failed to update context from concurrent tool:`, error);
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
// Apply data updates
|
|
2008
|
-
if (update.dataUpdate) {
|
|
2009
|
-
try {
|
|
2010
|
-
const updateDataMethod = this.agent.getUpdateDataMethod();
|
|
2011
|
-
session = await updateDataMethod(session, update.dataUpdate);
|
|
2012
|
-
} catch (error) {
|
|
2013
|
-
logger.error(`[ResponseModal] Failed to update data from concurrent tool:`, error);
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
// Yield progress updates immediately
|
|
2018
|
-
if (update.progress) {
|
|
2019
|
-
yield {
|
|
2020
|
-
delta: '',
|
|
2021
|
-
accumulated: chunk.accumulated,
|
|
2022
|
-
done: false,
|
|
2023
|
-
session,
|
|
2024
|
-
toolCalls: undefined,
|
|
2025
|
-
isFlowComplete: false,
|
|
2026
|
-
metadata: { toolProgress: update.progress, toolCallId: update.toolCallId },
|
|
2027
|
-
};
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
logger.debug(`[ResponseModal] Concurrent tool execution completed for ${toolCallRequests.length} tools`);
|
|
2032
|
-
} catch (error) {
|
|
2033
|
-
logger.error(`[ResponseModal] Concurrent tool execution failed, falling back to sequential:`, error);
|
|
2034
|
-
// Fall back to the unified tool loop on failure
|
|
2035
|
-
const toolResult = await this.executeUnifiedToolLoop({
|
|
2036
|
-
toolCalls, context, session, history, selectedFlow,
|
|
2037
|
-
responsePrompt, availableTools, responseSchema, signal,
|
|
2038
|
-
});
|
|
2039
|
-
session = toolResult.session;
|
|
2040
|
-
toolCalls = toolResult.finalToolCalls;
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
1251
|
+
// Concurrent execution for the initial batch of tool calls,
|
|
1252
|
+
// yielding tool-progress chunks as they arrive
|
|
1253
|
+
const batchResult = yield* this.toolLoopExecutor.runStreamingBatch({
|
|
1254
|
+
toolCalls,
|
|
1255
|
+
context,
|
|
1256
|
+
session,
|
|
1257
|
+
history,
|
|
1258
|
+
selectedFlow,
|
|
1259
|
+
step: nextStep,
|
|
1260
|
+
accumulated: chunk.accumulated,
|
|
1261
|
+
responsePrompt,
|
|
1262
|
+
availableTools,
|
|
1263
|
+
responseSchema,
|
|
1264
|
+
signal,
|
|
1265
|
+
});
|
|
1266
|
+
session = batchResult.session;
|
|
1267
|
+
toolCalls = batchResult.toolCalls;
|
|
2043
1268
|
}
|
|
2044
1269
|
|
|
2045
1270
|
// Extract collected data on final chunk
|
|
@@ -2052,11 +1277,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
2052
1277
|
});
|
|
2053
1278
|
}
|
|
2054
1279
|
|
|
2055
|
-
// Handle session finalization on final chunk
|
|
2056
|
-
if (chunk.done) {
|
|
2057
|
-
await this.finalizeSession(session, context);
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
1280
|
// Response structure completeness (Requirement 8.1, 8.2, 8.3)
|
|
2061
1281
|
// - executedSteps: single step executed in this response
|
|
2062
1282
|
// - stoppedReason: 'needs_input' for single-step execution (waiting for user input)
|
|
@@ -2084,341 +1304,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
2084
1304
|
}
|
|
2085
1305
|
|
|
2086
1306
|
/**
|
|
2087
|
-
* Unified tool execution logic with loop handling
|
|
2088
|
-
* Consolidates the complex tool execution logic from both streaming and non-streaming responses
|
|
2089
|
-
* @private
|
|
2090
|
-
*/
|
|
2091
|
-
private async executeUnifiedToolLoop(params: {
|
|
2092
|
-
toolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
|
|
2093
|
-
context: TContext;
|
|
2094
|
-
session: SessionState<TData>;
|
|
2095
|
-
history: HistoryItem[];
|
|
2096
|
-
selectedFlow?: Flow<TContext, TData>;
|
|
2097
|
-
responsePrompt: string;
|
|
2098
|
-
availableTools: Array<{
|
|
2099
|
-
id: string;
|
|
2100
|
-
name: string;
|
|
2101
|
-
description?: string;
|
|
2102
|
-
parameters?: unknown;
|
|
2103
|
-
}>;
|
|
2104
|
-
responseSchema?: Record<string, unknown>;
|
|
2105
|
-
signal?: AbortSignal;
|
|
2106
|
-
}): Promise<{
|
|
2107
|
-
session: SessionState<TData>;
|
|
2108
|
-
finalToolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
|
|
2109
|
-
finalMessage?: string;
|
|
2110
|
-
structured?: AgentStructuredResponse;
|
|
2111
|
-
}> {
|
|
2112
|
-
try {
|
|
2113
|
-
const { context, history, selectedFlow, responsePrompt, availableTools, responseSchema, signal } = params;
|
|
2114
|
-
let { toolCalls, session } = params;
|
|
2115
|
-
|
|
2116
|
-
// Convert HistoryItem[] to Event[] for internal processing
|
|
2117
|
-
const historyEvents = historyToEvents(history);
|
|
2118
|
-
|
|
2119
|
-
// Map to store tool execution results for history
|
|
2120
|
-
const toolResultsMap = new Map<string, string>();
|
|
2121
|
-
// Map to store tool call arguments for history reconstruction
|
|
2122
|
-
const toolArgsMap = new Map<string, Record<string, unknown>>();
|
|
2123
|
-
|
|
2124
|
-
// Execute initial dynamic tool calls
|
|
2125
|
-
if (toolCalls && toolCalls.length > 0) {
|
|
2126
|
-
logger.debug(`[ResponseModal] Executing ${toolCalls.length} dynamic tool calls:`, toolCalls.map(tc => tc.toolName));
|
|
2127
|
-
|
|
2128
|
-
for (const toolCall of toolCalls) {
|
|
2129
|
-
const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
|
|
2130
|
-
if (!tool) {
|
|
2131
|
-
logger.warn(`[ToolExecutionError] Tool not found: "${toolCall.toolName}" is not registered in any scope. Skipping this tool call. Register the tool or check the tool name.`);
|
|
2132
|
-
continue;
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
try {
|
|
2136
|
-
// Use ToolManager for unified tool execution
|
|
2137
|
-
const toolManager = this.getToolManager();
|
|
2138
|
-
const toolResult = await toolManager.executeTool({
|
|
2139
|
-
tool: tool,
|
|
2140
|
-
context,
|
|
2141
|
-
updateContext: this.agent.updateContext.bind(this.agent),
|
|
2142
|
-
updateData: this.agent.updateCollectedData.bind(this.agent),
|
|
2143
|
-
history: historyEvents, // Use Event[] for tool execution
|
|
2144
|
-
data: session.data,
|
|
2145
|
-
toolArguments: toolCall.arguments,
|
|
2146
|
-
});
|
|
2147
|
-
|
|
2148
|
-
// Store the actual tool result data for history
|
|
2149
|
-
toolResultsMap.set(toolCall.toolName, serializeToolResult(toolResult));
|
|
2150
|
-
toolArgsMap.set(toolCall.toolName, toolCall.arguments);
|
|
2151
|
-
|
|
2152
|
-
// Check if tool execution was successful
|
|
2153
|
-
if (!toolResult.success) {
|
|
2154
|
-
logger.error(`[ResponseModal] Tool execution failed: ${toolCall.toolName} - ${toolResult.error}`);
|
|
2155
|
-
// Continue with other tools rather than failing completely
|
|
2156
|
-
continue;
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
// Update context with tool results
|
|
2160
|
-
if (toolResult.contextUpdate) {
|
|
2161
|
-
try {
|
|
2162
|
-
await this.agent.updateContext(toolResult.contextUpdate as Partial<TContext>);
|
|
2163
|
-
} catch (error) {
|
|
2164
|
-
logger.error(`[ResponseModal] Failed to update context from tool ${toolCall.toolName}:`, error);
|
|
2165
|
-
// Continue execution but log the error
|
|
2166
|
-
}
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
|
-
// Update collected data with tool results
|
|
2170
|
-
if (toolResult.dataUpdate) {
|
|
2171
|
-
try {
|
|
2172
|
-
const updateDataMethod = this.agent.getUpdateDataMethod();
|
|
2173
|
-
session = await updateDataMethod(session, toolResult.dataUpdate as Partial<TData>);
|
|
2174
|
-
logger.debug(`[ResponseModal] Tool updated collected data:`, toolResult.dataUpdate);
|
|
2175
|
-
} catch (error) {
|
|
2176
|
-
logger.error(`[ResponseModal] Failed to update data from tool ${toolCall.toolName}:`, error);
|
|
2177
|
-
// Continue execution but log the error
|
|
2178
|
-
}
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
logger.debug(`[ResponseModal] Executed dynamic tool: ${toolCall.toolName} (success: ${toolResult.success})`);
|
|
2182
|
-
} catch (error) {
|
|
2183
|
-
logger.error(`[ResponseModal] Tool execution error for ${toolCall.toolName}:`, error);
|
|
2184
|
-
// Continue with other tools rather than failing the entire response
|
|
2185
|
-
continue;
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
// TOOL LOOP: Allow AI to make follow-up tool calls after initial tool execution
|
|
2191
|
-
const MAX_TOOL_LOOPS = this.options?.maxToolLoops || 5;
|
|
2192
|
-
let toolLoopCount = 0;
|
|
2193
|
-
let hasToolCalls = toolCalls && toolCalls.length > 0;
|
|
2194
|
-
let finalMessage: string | undefined;
|
|
2195
|
-
let followUpStructured: AgentStructuredResponse | undefined;
|
|
2196
|
-
|
|
2197
|
-
while (hasToolCalls && toolLoopCount < MAX_TOOL_LOOPS) {
|
|
2198
|
-
toolLoopCount++;
|
|
2199
|
-
logger.debug(`[ResponseModal] Starting tool loop ${toolLoopCount}/${MAX_TOOL_LOOPS} with ${toolCalls?.length || 0} tool calls`);
|
|
2200
|
-
|
|
2201
|
-
// Create tool result history items
|
|
2202
|
-
const toolResultHistoryItems: HistoryItem[] = [];
|
|
2203
|
-
for (const toolCall of toolCalls || []) {
|
|
2204
|
-
const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
|
|
2205
|
-
if (tool) {
|
|
2206
|
-
// Create HistoryItem format for tool results
|
|
2207
|
-
// Add assistant message with tool_calls
|
|
2208
|
-
toolResultHistoryItems.push({
|
|
2209
|
-
role: "assistant" as const,
|
|
2210
|
-
content: null,
|
|
2211
|
-
tool_calls: [{
|
|
2212
|
-
id: toolCall.toolName,
|
|
2213
|
-
name: toolCall.toolName,
|
|
2214
|
-
arguments: toolCall.arguments,
|
|
2215
|
-
}],
|
|
2216
|
-
});
|
|
2217
|
-
// Add tool result
|
|
2218
|
-
toolResultHistoryItems.push({
|
|
2219
|
-
role: "tool" as const,
|
|
2220
|
-
tool_call_id: toolCall.toolName,
|
|
2221
|
-
name: toolCall.toolName,
|
|
2222
|
-
content: toolResultsMap.get(toolCall.toolName) || "Tool executed successfully",
|
|
2223
|
-
});
|
|
2224
|
-
}
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
// Create updated history with tool results
|
|
2228
|
-
const updatedHistory = [...history, ...toolResultHistoryItems];
|
|
2229
|
-
|
|
2230
|
-
// Make follow-up AI call to see if more tools are needed
|
|
2231
|
-
// After first iteration, don't provide tools to force a text response
|
|
2232
|
-
const agentOptions = this.agent.getAgentOptions();
|
|
2233
|
-
const shouldProvideTools = toolLoopCount === 1;
|
|
2234
|
-
|
|
2235
|
-
logger.debug(`[ResponseModal] Making follow-up AI call (loop ${toolLoopCount}):`, {
|
|
2236
|
-
providingTools: shouldProvideTools,
|
|
2237
|
-
toolsCount: shouldProvideTools ? availableTools.length : 0,
|
|
2238
|
-
addingTextInstruction: toolLoopCount > 1,
|
|
2239
|
-
});
|
|
2240
|
-
|
|
2241
|
-
const followUpResult = await agentOptions.provider.generateMessage({
|
|
2242
|
-
prompt: responsePrompt + (toolLoopCount > 1 ? "\n\nProvide a text response to the user based on the tool results." : ""),
|
|
2243
|
-
history: updatedHistory, // Use HistoryItem[] for AI provider
|
|
2244
|
-
context,
|
|
2245
|
-
tools: shouldProvideTools ? availableTools : [], // Only provide tools on first iteration
|
|
2246
|
-
parameters: responseSchema ? {
|
|
2247
|
-
jsonSchema: responseSchema,
|
|
2248
|
-
schemaName: "tool_followup",
|
|
2249
|
-
} : undefined,
|
|
2250
|
-
signal,
|
|
2251
|
-
});
|
|
2252
|
-
|
|
2253
|
-
// Check if follow-up call has more tool calls
|
|
2254
|
-
const followUpToolCalls = followUpResult.structured?.toolCalls;
|
|
2255
|
-
hasToolCalls = followUpToolCalls && followUpToolCalls.length > 0;
|
|
2256
|
-
|
|
2257
|
-
logger.debug(`[ResponseModal] Follow-up AI response (loop ${toolLoopCount}):`, {
|
|
2258
|
-
hasMessage: !!followUpResult.message,
|
|
2259
|
-
messageLength: followUpResult.message?.length || 0,
|
|
2260
|
-
hasToolCalls,
|
|
2261
|
-
toolCallsCount: followUpToolCalls?.length || 0,
|
|
2262
|
-
toolNames: followUpToolCalls?.map(tc => tc.toolName) || [],
|
|
2263
|
-
});
|
|
2264
|
-
|
|
2265
|
-
if (hasToolCalls) {
|
|
2266
|
-
logger.debug(`[ResponseModal] Follow-up call produced ${followUpToolCalls!.length} additional tool calls`);
|
|
2267
|
-
|
|
2268
|
-
// Execute the follow-up tool calls
|
|
2269
|
-
for (const toolCall of followUpToolCalls!) {
|
|
2270
|
-
const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
|
|
2271
|
-
if (!tool) {
|
|
2272
|
-
logger.warn(`[ToolExecutionError] Tool not found in follow-up: "${toolCall.toolName}" is not registered in any scope. Skipping this tool call. Register the tool or check the tool name.`);
|
|
2273
|
-
continue;
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
try {
|
|
2277
|
-
// Use ToolManager for unified tool execution
|
|
2278
|
-
const toolManager = this.getToolManager();
|
|
2279
|
-
const toolResult = await toolManager.executeTool({
|
|
2280
|
-
tool: tool,
|
|
2281
|
-
context,
|
|
2282
|
-
updateContext: this.agent.updateContext.bind(this.agent),
|
|
2283
|
-
updateData: this.agent.updateCollectedData.bind(this.agent),
|
|
2284
|
-
history: historyToEvents(updatedHistory), // Convert to Event[] for tool execution
|
|
2285
|
-
data: session.data,
|
|
2286
|
-
toolArguments: toolCall.arguments,
|
|
2287
|
-
});
|
|
2288
|
-
|
|
2289
|
-
// Check if tool execution was successful
|
|
2290
|
-
if (!toolResult.success) {
|
|
2291
|
-
logger.error(`[ResponseModal] Follow-up tool execution failed: ${toolCall.toolName} - ${toolResult.error}`);
|
|
2292
|
-
continue;
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
// Update context with follow-up tool results
|
|
2296
|
-
if (toolResult.contextUpdate) {
|
|
2297
|
-
try {
|
|
2298
|
-
await this.agent.updateContext(toolResult.contextUpdate as Partial<TContext>);
|
|
2299
|
-
} catch (error) {
|
|
2300
|
-
logger.error(`[ResponseModal] Failed to update context from follow-up tool ${toolCall.toolName}:`, error);
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
if (toolResult.dataUpdate) {
|
|
2305
|
-
try {
|
|
2306
|
-
const updateDataMethod = this.agent.getUpdateDataMethod();
|
|
2307
|
-
session = await updateDataMethod(session, toolResult.dataUpdate as Partial<TData>);
|
|
2308
|
-
logger.debug(`[ResponseModal] Follow-up tool updated collected data:`, toolResult.dataUpdate);
|
|
2309
|
-
} catch (error) {
|
|
2310
|
-
logger.error(`[ResponseModal] Failed to update data from follow-up tool ${toolCall.toolName}:`, error);
|
|
2311
|
-
}
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
// Store the follow-up tool result for potential next loop iteration
|
|
2315
|
-
toolResultsMap.set(toolCall.toolName, serializeToolResult(toolResult));
|
|
2316
|
-
toolArgsMap.set(toolCall.toolName, toolCall.arguments);
|
|
2317
|
-
|
|
2318
|
-
logger.debug(`[ResponseModal] Executed follow-up tool: ${toolCall.toolName} (success: ${toolResult.success})`);
|
|
2319
|
-
} catch (error) {
|
|
2320
|
-
logger.error(`[ResponseModal] Follow-up tool execution error for ${toolCall.toolName}:`, error);
|
|
2321
|
-
continue;
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
|
|
2325
|
-
// Update toolCalls for next iteration or final response
|
|
2326
|
-
toolCalls = followUpToolCalls;
|
|
2327
|
-
} else {
|
|
2328
|
-
logger.debug(`[ResponseModal] Tool loop completed after ${toolLoopCount} iterations`);
|
|
2329
|
-
// Update final message and toolCalls from follow-up result if no more tools
|
|
2330
|
-
finalMessage = followUpResult.structured?.message || followUpResult.message;
|
|
2331
|
-
followUpStructured = followUpResult.structured;
|
|
2332
|
-
toolCalls = followUpToolCalls || [];
|
|
2333
|
-
break;
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
if (toolLoopCount >= MAX_TOOL_LOOPS) {
|
|
2338
|
-
logger.warn(`[ResponseGenerationError] Tool loop limit reached: ${toolLoopCount} iterations hit the cap (${MAX_TOOL_LOOPS}). Stopping tool execution. Increase MAX_TOOL_LOOPS or reduce recursive tool calls.`);
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
|
-
// If tools were executed but no final text message was produced,
|
|
2342
|
-
// make one more LLM call to generate a proper text response from tool results.
|
|
2343
|
-
// This prevents the original tool-invocation message (e.g. "Let me check...")
|
|
2344
|
-
// from being returned as the final user-facing response.
|
|
2345
|
-
if (!finalMessage && toolLoopCount > 0) {
|
|
2346
|
-
logger.debug(`[ResponseModal] No final message after tool loop, making additional LLM call for text response`);
|
|
2347
|
-
|
|
2348
|
-
// Build tool result history from toolResultsMap which contains ALL
|
|
2349
|
-
// tool executions (initial + follow-up). We can't use `toolCalls` here
|
|
2350
|
-
// because it was reassigned to the (empty) follow-up tool calls when
|
|
2351
|
-
// the while loop broke out.
|
|
2352
|
-
const finalToolResultHistoryItems: HistoryItem[] = [];
|
|
2353
|
-
for (const [toolName, toolResult] of toolResultsMap) {
|
|
2354
|
-
finalToolResultHistoryItems.push({
|
|
2355
|
-
role: "assistant" as const,
|
|
2356
|
-
content: null,
|
|
2357
|
-
tool_calls: [{
|
|
2358
|
-
id: toolName,
|
|
2359
|
-
name: toolName,
|
|
2360
|
-
arguments: toolArgsMap.get(toolName) || {},
|
|
2361
|
-
}],
|
|
2362
|
-
});
|
|
2363
|
-
finalToolResultHistoryItems.push({
|
|
2364
|
-
role: "tool" as const,
|
|
2365
|
-
tool_call_id: toolName,
|
|
2366
|
-
name: toolName,
|
|
2367
|
-
content: toolResult,
|
|
2368
|
-
});
|
|
2369
|
-
}
|
|
2370
|
-
|
|
2371
|
-
const finalHistory = [...history, ...finalToolResultHistoryItems];
|
|
2372
|
-
const agentOptions = this.agent.getAgentOptions();
|
|
2373
|
-
|
|
2374
|
-
try {
|
|
2375
|
-
const textResult = await agentOptions.provider.generateMessage({
|
|
2376
|
-
prompt: responsePrompt + "\n\nProvide a text response to the user based on the tool results. Do not call any tools.",
|
|
2377
|
-
history: finalHistory,
|
|
2378
|
-
context,
|
|
2379
|
-
tools: [], // No tools - force text response
|
|
2380
|
-
parameters: responseSchema ? {
|
|
2381
|
-
jsonSchema: responseSchema,
|
|
2382
|
-
schemaName: "tool_final_text",
|
|
2383
|
-
} : undefined,
|
|
2384
|
-
signal,
|
|
2385
|
-
});
|
|
2386
|
-
|
|
2387
|
-
finalMessage = textResult.structured?.message || textResult.message;
|
|
2388
|
-
if (textResult.structured) {
|
|
2389
|
-
followUpStructured = textResult.structured;
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
logger.debug(`[ResponseModal] Generated final text response after tool loop:`, {
|
|
2393
|
-
hasMessage: !!finalMessage,
|
|
2394
|
-
messageLength: finalMessage?.length || 0,
|
|
2395
|
-
});
|
|
2396
|
-
} catch (error) {
|
|
2397
|
-
logger.error(`[ResponseModal] Failed to generate final text response after tool loop:`, error);
|
|
2398
|
-
// finalMessage remains undefined; caller will use original message as fallback
|
|
2399
|
-
}
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
logger.debug(`[ResponseModal] Tool loop completed:`, {
|
|
2403
|
-
totalIterations: toolLoopCount,
|
|
2404
|
-
hasFinalMessage: !!finalMessage,
|
|
2405
|
-
finalMessageLength: finalMessage?.length || 0,
|
|
2406
|
-
finalToolCallsCount: toolCalls?.length || 0,
|
|
2407
|
-
});
|
|
2408
|
-
|
|
2409
|
-
return {
|
|
2410
|
-
session,
|
|
2411
|
-
finalToolCalls: toolCalls,
|
|
2412
|
-
finalMessage,
|
|
2413
|
-
structured: followUpStructured,
|
|
2414
|
-
};
|
|
2415
|
-
} catch (error) {
|
|
2416
|
-
throw ResponseGenerationError.fromError(error, 'tool_execution', params, {
|
|
2417
|
-
toolCallsCount: params.toolCalls?.length || 0,
|
|
2418
|
-
availableToolsCount: params.availableTools.length
|
|
2419
|
-
});
|
|
2420
|
-
}
|
|
2421
|
-
} /**
|
|
2422
1307
|
* Unified data collection from AI response
|
|
2423
1308
|
* @private
|
|
2424
1309
|
*/
|
|
@@ -2599,10 +1484,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
2599
1484
|
return nextSession;
|
|
2600
1485
|
}
|
|
2601
1486
|
|
|
2602
|
-
/**
|
|
2603
|
-
* Stream flow completion response
|
|
2604
|
-
* @private
|
|
2605
|
-
*/
|
|
2606
1487
|
/**
|
|
2607
1488
|
* Stream a flow completion as a single terminal chunk.
|
|
2608
1489
|
*
|
|
@@ -2634,8 +1515,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
2634
1515
|
history,
|
|
2635
1516
|
});
|
|
2636
1517
|
|
|
2637
|
-
await this.finalizeSession(session, context);
|
|
2638
|
-
|
|
2639
1518
|
yield {
|
|
2640
1519
|
delta: '',
|
|
2641
1520
|
accumulated: '',
|
|
@@ -2729,11 +1608,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
2729
1608
|
});
|
|
2730
1609
|
|
|
2731
1610
|
for await (const chunk of stream) {
|
|
2732
|
-
// Update current session if we have one
|
|
2733
|
-
if (chunk.done) {
|
|
2734
|
-
await this.finalizeSession(session, context);
|
|
2735
|
-
}
|
|
2736
|
-
|
|
2737
1611
|
// Response structure completeness (Requirement 8.1, 8.2, 8.3)
|
|
2738
1612
|
// - executedSteps: empty for fallback (no flow/step execution)
|
|
2739
1613
|
// - stoppedReason: undefined for fallback (no flow context)
|
|
@@ -2754,49 +1628,11 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
2754
1628
|
}
|
|
2755
1629
|
}
|
|
2756
1630
|
|
|
2757
|
-
/**
|
|
2758
|
-
* Handle session persistence and finalization
|
|
2759
|
-
* @private
|
|
2760
|
-
*/
|
|
2761
|
-
private async finalizeSession(session: SessionState<TData>, context: TContext): Promise<void> {
|
|
2762
|
-
// Auto-save session step to persistence if configured
|
|
2763
|
-
const persistenceManager = this.agent.getPersistenceManager();
|
|
2764
|
-
const agentOptions = this.agent.getAgentOptions();
|
|
2765
|
-
if (
|
|
2766
|
-
persistenceManager &&
|
|
2767
|
-
session.id &&
|
|
2768
|
-
(this.options?.enableAutoSave !== false && agentOptions.persistence?.autoSave !== false)
|
|
2769
|
-
) {
|
|
2770
|
-
await persistenceManager.saveSessionState(session.id, session);
|
|
2771
|
-
logger.debug(`[ResponseModal] Auto-saved session step to persistence: ${session.id}`);
|
|
2772
|
-
}
|
|
2773
|
-
|
|
2774
|
-
// Execute finalize function
|
|
2775
|
-
await this.executeStepFinalize(session, context);
|
|
2776
|
-
|
|
2777
|
-
// Update current session if we have one
|
|
2778
|
-
const currentSession = this.agent.currentSession;
|
|
2779
|
-
if (currentSession) {
|
|
2780
|
-
this.agent.currentSession = session;
|
|
2781
|
-
}
|
|
2782
|
-
}
|
|
2783
1631
|
// ============================================================================
|
|
2784
1632
|
// UTILITY METHODS - Helper methods for tool management and other utilities
|
|
2785
1633
|
// ============================================================================
|
|
2786
1634
|
|
|
2787
1635
|
|
|
2788
|
-
/**
|
|
2789
|
-
* Find an available tool by name for the given flow.
|
|
2790
|
-
* Delegates to ToolManager for unified tool resolution.
|
|
2791
|
-
* @private
|
|
2792
|
-
*/
|
|
2793
|
-
private findAvailableTool(
|
|
2794
|
-
toolName: string,
|
|
2795
|
-
flow?: Flow<TContext, TData>
|
|
2796
|
-
): Tool<TContext, TData> | undefined {
|
|
2797
|
-
return this.getToolManager().find(toolName, undefined, undefined, flow);
|
|
2798
|
-
}
|
|
2799
|
-
|
|
2800
1636
|
/**
|
|
2801
1637
|
* Collect all available tools for the given flow and step context.
|
|
2802
1638
|
* Delegates to ToolManager for unified tool resolution and deduplication.
|
|
@@ -2820,64 +1656,4 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
2820
1656
|
}));
|
|
2821
1657
|
}
|
|
2822
1658
|
|
|
2823
|
-
|
|
2824
|
-
* Execute a prepare or finalize function/tool
|
|
2825
|
-
* @private
|
|
2826
|
-
*/
|
|
2827
|
-
private async executePrepareFinalize(
|
|
2828
|
-
prepareOrFinalize:
|
|
2829
|
-
| string
|
|
2830
|
-
| Tool<TContext, TData>
|
|
2831
|
-
| ((context: TContext, data?: Partial<TData>) => void | PrepareResult | Promise<void | PrepareResult>)
|
|
2832
|
-
| undefined,
|
|
2833
|
-
context: TContext,
|
|
2834
|
-
data?: Partial<TData>,
|
|
2835
|
-
flow?: Flow<TContext, TData>,
|
|
2836
|
-
step?: Step<TContext, TData>
|
|
2837
|
-
): Promise<void> {
|
|
2838
|
-
if (!prepareOrFinalize) return;
|
|
2839
|
-
|
|
2840
|
-
if (typeof prepareOrFinalize === "function") {
|
|
2841
|
-
// It's a function - call it directly
|
|
2842
|
-
await prepareOrFinalize(context, data);
|
|
2843
|
-
} else {
|
|
2844
|
-
// It's a tool reference - find and execute the tool
|
|
2845
|
-
let tool: Tool<TContext, TData> | undefined;
|
|
2846
|
-
|
|
2847
|
-
if (typeof prepareOrFinalize === "string") {
|
|
2848
|
-
// Tool ID - use ToolManager for unified resolution
|
|
2849
|
-
tool = this.getToolManager().find(prepareOrFinalize, undefined, step, flow);
|
|
2850
|
-
} else {
|
|
2851
|
-
// Tool object - use directly
|
|
2852
|
-
tool = prepareOrFinalize;
|
|
2853
|
-
}
|
|
2854
|
-
|
|
2855
|
-
if (tool) {
|
|
2856
|
-
// Use ToolManager for unified tool execution
|
|
2857
|
-
const result = await this.getToolManager().executeTool({
|
|
2858
|
-
tool,
|
|
2859
|
-
context,
|
|
2860
|
-
updateContext: this.agent.updateContext.bind(this.agent),
|
|
2861
|
-
updateData: this.agent.updateCollectedData.bind(this.agent),
|
|
2862
|
-
history: [], // Empty history for prepare/finalize
|
|
2863
|
-
data,
|
|
2864
|
-
});
|
|
2865
|
-
|
|
2866
|
-
if (!result.success) {
|
|
2867
|
-
logger.error(
|
|
2868
|
-
`[ResponseModal] Tool execution failed in prepare/finalize: ${result.error}`
|
|
2869
|
-
);
|
|
2870
|
-
throw new Error(`Tool execution failed: ${result.error}`);
|
|
2871
|
-
}
|
|
2872
|
-
} else {
|
|
2873
|
-
logger.warn(
|
|
2874
|
-
`[ResponseModal] Tool not found for prepare/finalize: ${typeof prepareOrFinalize === "string"
|
|
2875
|
-
? prepareOrFinalize
|
|
2876
|
-
: "inline tool"
|
|
2877
|
-
}`
|
|
2878
|
-
);
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
|
-
}
|
|
2882
|
-
|
|
2883
|
-
}
|
|
1659
|
+
}
|