@falai/agent 2.2.4 → 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 -102
- package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
- package/dist/cjs/core/ResponseModal.js +178 -1194
- 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 +11 -26
- package/dist/cjs/core/SignalEvaluator.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 +12 -2
- 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 -102
- package/dist/core/ResponseModal.d.ts.map +1 -1
- package/dist/core/ResponseModal.js +171 -1186
- 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 +11 -26
- package/dist/core/SignalEvaluator.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 +12 -2
- 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 +2 -0
- 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 +11 -3
- 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 +233 -1482
- 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 +12 -29
- 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 +14 -2
- 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({
|
|
@@ -370,18 +400,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
370
400
|
return this.agent.tool;
|
|
371
401
|
}
|
|
372
402
|
|
|
373
|
-
/**
|
|
374
|
-
* Post-phase signal replies replace the user-visible message after the
|
|
375
|
-
* response has otherwise completed. Undefined means "leave it unchanged";
|
|
376
|
-
* an empty string is an explicit replacement.
|
|
377
|
-
*/
|
|
378
|
-
private applyPostSignalReply(
|
|
379
|
-
message: string,
|
|
380
|
-
directive: Directive<TContext, TData> | undefined,
|
|
381
|
-
): string {
|
|
382
|
-
return directive?.reply !== undefined ? directive.reply : message;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
403
|
/**
|
|
386
404
|
* Collect scoped instructions from agent, flow, and step into a ScopedInstructions value.
|
|
387
405
|
* @private
|
|
@@ -423,52 +441,53 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
423
441
|
// Keep original HistoryItem[] format for external APIs
|
|
424
442
|
const history = simpleHistory;
|
|
425
443
|
|
|
426
|
-
// Use ResponsePipeline for
|
|
427
|
-
//
|
|
444
|
+
// Use ResponsePipeline for context and session preparation; context
|
|
445
|
+
// and session are passed explicitly — the pipeline holds no state
|
|
428
446
|
let responseContext: {
|
|
429
447
|
effectiveContext: TContext;
|
|
430
448
|
session: SessionState<TData>;
|
|
449
|
+
contextAfterHook?: TContext;
|
|
431
450
|
};
|
|
432
451
|
try {
|
|
433
|
-
// Set current context and session in pipeline for consistency
|
|
434
|
-
this.responsePipeline.setContext(await this.agent.getContext());
|
|
435
|
-
this.responsePipeline.setCurrentSession(this.agent.currentSession);
|
|
436
|
-
|
|
437
452
|
responseContext = await this.responsePipeline.prepareResponseContext({
|
|
438
453
|
contextOverride,
|
|
439
454
|
session: params.session ? cloneDeep(params.session) : undefined,
|
|
455
|
+
currentContext: await this.agent.getContext(),
|
|
456
|
+
currentSession: this.agent.currentSession,
|
|
440
457
|
});
|
|
441
458
|
} catch (error) {
|
|
442
459
|
throw ResponseGenerationError.fromError(error, 'pipeline_context_preparation', params);
|
|
443
460
|
}
|
|
444
461
|
|
|
445
|
-
const { effectiveContext } = responseContext;
|
|
462
|
+
const { effectiveContext, contextAfterHook } = responseContext;
|
|
446
463
|
let session = responseContext.session;
|
|
447
464
|
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
if (storedContext !== undefined) {
|
|
465
|
+
// Sync the beforeRespond hook's context result back to the agent
|
|
466
|
+
if (contextAfterHook !== undefined) {
|
|
451
467
|
try {
|
|
452
|
-
await this.agent.updateContext(
|
|
468
|
+
await this.agent.updateContext(contextAfterHook as Partial<TContext>);
|
|
453
469
|
} catch (error) {
|
|
454
|
-
throw ResponseGenerationError.fromError(error, 'context_update_from_pipeline', params, {
|
|
470
|
+
throw ResponseGenerationError.fromError(error, 'context_update_from_pipeline', params, { contextAfterHook });
|
|
455
471
|
}
|
|
456
472
|
}
|
|
457
473
|
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
|
|
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) {
|
|
461
480
|
try {
|
|
462
|
-
session = mergeCollected(session,
|
|
463
|
-
logger.debug("[ResponseModal] Merged agent
|
|
481
|
+
session = mergeCollected(session, stagedData);
|
|
482
|
+
logger.debug("[ResponseModal] Merged staged agent data into session:", stagedData);
|
|
464
483
|
} catch (error) {
|
|
465
|
-
throw ResponseGenerationError.fromError(error, 'data_merging', params, {
|
|
484
|
+
throw ResponseGenerationError.fromError(error, 'data_merging', params, { stagedData });
|
|
466
485
|
}
|
|
467
486
|
}
|
|
468
487
|
|
|
469
488
|
// PHASE 1: PREPARE - Execute prepare function if current step has one
|
|
470
489
|
try {
|
|
471
|
-
await this.
|
|
490
|
+
await this.stepLifecycle.runPrepare(session, effectiveContext);
|
|
472
491
|
} catch (error) {
|
|
473
492
|
throw ResponseGenerationError.fromError(error, 'step_preparation', params, { session, effectiveContext });
|
|
474
493
|
}
|
|
@@ -487,7 +506,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
487
506
|
signalHaltReply?: string;
|
|
488
507
|
};
|
|
489
508
|
try {
|
|
490
|
-
routingResult = await this.
|
|
509
|
+
routingResult = await this.responsePipeline.routeAndSelectStep({
|
|
491
510
|
session,
|
|
492
511
|
history: historyEvents,
|
|
493
512
|
context: effectiveContext,
|
|
@@ -520,564 +539,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
520
539
|
}
|
|
521
540
|
}
|
|
522
541
|
|
|
523
|
-
/**
|
|
524
|
-
* Unified routing and step selection logic using ResponsePipeline for optimization
|
|
525
|
-
* @private
|
|
526
|
-
*/
|
|
527
|
-
private async handleUnifiedRoutingAndStepSelection(params: {
|
|
528
|
-
session: SessionState<TData>;
|
|
529
|
-
history: Event[]; // Use Event[] for internal processing
|
|
530
|
-
context: TContext;
|
|
531
|
-
signal?: AbortSignal;
|
|
532
|
-
}): Promise<{
|
|
533
|
-
selectedFlow?: Flow<TContext, TData>;
|
|
534
|
-
selectedStep?: Step<TContext, TData>;
|
|
535
|
-
responseDirectives?: string[];
|
|
536
|
-
session: SessionState<TData>;
|
|
537
|
-
isFlowComplete: boolean;
|
|
538
|
-
/** Signal firings from the pre-phase (threaded through for response surface). */
|
|
539
|
-
signalFirings?: SignalFiring<TContext, TData>[];
|
|
540
|
-
/** Non-position signal directive for pre-LLM augmentation (appendPrompt, injectTools, etc). */
|
|
541
|
-
signalPreDirective?: Directive<TContext, TData>;
|
|
542
|
-
/** Pre-signal phase halted the turn. */
|
|
543
|
-
signalHalted?: boolean;
|
|
544
|
-
/** Reply text from the halt directive. */
|
|
545
|
-
signalHaltReply?: string;
|
|
546
|
-
}> {
|
|
547
|
-
try {
|
|
548
|
-
// Create a fresh chain tracker for this turn (Requirement 22.1)
|
|
549
|
-
this.responsePipeline.createChainTracker();
|
|
550
|
-
|
|
551
|
-
// ROUTING SKIP OPTIMIZATION (Requirements 20.1, 20.2, 20.3):
|
|
552
|
-
// When the current step has collect fields AND pre-extraction populates at least
|
|
553
|
-
// one of those fields, skip FlowRouter.decideFlowAndStep for this turn.
|
|
554
|
-
const routingSkipResult = await this.attemptRoutingSkipForCollect(params);
|
|
555
|
-
if (routingSkipResult) {
|
|
556
|
-
// Even when routing is skipped, run pre-signal phase if processor is present
|
|
557
|
-
if (this.agent.signalProcessor) {
|
|
558
|
-
const signalResult = await this.responsePipeline.runPreSignalPhase(
|
|
559
|
-
params.session, params.context, params.history,
|
|
560
|
-
);
|
|
561
|
-
// If signal halts, override the routing skip result
|
|
562
|
-
if (signalResult.mergedDirective?.halt) {
|
|
563
|
-
return {
|
|
564
|
-
...routingSkipResult,
|
|
565
|
-
session: signalResult.updatedSession,
|
|
566
|
-
signalFirings: signalResult.firings,
|
|
567
|
-
signalHalted: true,
|
|
568
|
-
signalHaltReply: signalResult.mergedDirective.reply,
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
// If signal has position fields, override routing skip result
|
|
572
|
-
if (hasDirectivePositionField(signalResult.mergedDirective)) {
|
|
573
|
-
return this.applySignalPositionDirective(
|
|
574
|
-
signalResult, params,
|
|
575
|
-
);
|
|
576
|
-
}
|
|
577
|
-
// Non-position directive: propagate for pre-LLM augmentation
|
|
578
|
-
return {
|
|
579
|
-
...routingSkipResult,
|
|
580
|
-
session: signalResult.updatedSession,
|
|
581
|
-
signalFirings: signalResult.firings,
|
|
582
|
-
signalPreDirective: signalResult.mergedDirective || undefined,
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
return routingSkipResult;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// ── PARALLEL PRE-SIGNAL PHASE + ROUTING (Algorithm 5) ────────────────
|
|
589
|
-
// When signalProcessor is present, run pre-signals in parallel with routing.
|
|
590
|
-
// When absent, call the router directly (zero overhead, preserve current behavior).
|
|
591
|
-
if (this.agent.signalProcessor) {
|
|
592
|
-
// Run pre-signal phase in parallel with routing (Requirement 8.1)
|
|
593
|
-
const [signalResult, routingResult] = await Promise.all([
|
|
594
|
-
this.responsePipeline.runPreSignalPhase(
|
|
595
|
-
params.session, params.context, params.history,
|
|
596
|
-
),
|
|
597
|
-
this.responsePipeline.handleRoutingAndStepSelection({
|
|
598
|
-
session: params.session,
|
|
599
|
-
history: params.history,
|
|
600
|
-
context: params.context,
|
|
601
|
-
signal: params.signal,
|
|
602
|
-
}),
|
|
603
|
-
]);
|
|
604
|
-
|
|
605
|
-
// ── Requirement 8.2: halt → discard routing, skip LLM ────────────
|
|
606
|
-
if (signalResult.mergedDirective?.halt) {
|
|
607
|
-
return {
|
|
608
|
-
selectedFlow: undefined,
|
|
609
|
-
selectedStep: undefined,
|
|
610
|
-
session: signalResult.updatedSession,
|
|
611
|
-
isFlowComplete: false,
|
|
612
|
-
signalFirings: signalResult.firings,
|
|
613
|
-
signalHalted: true,
|
|
614
|
-
signalHaltReply: signalResult.mergedDirective.reply,
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// ── Requirement 8.3: position directive → discard routing, apply signal position ──
|
|
619
|
-
if (hasDirectivePositionField(signalResult.mergedDirective)) {
|
|
620
|
-
return this.applySignalPositionDirective(
|
|
621
|
-
signalResult, params,
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// ── Requirement 8.4: non-position directive → use routing, propagate augmentation ──
|
|
626
|
-
// ── Requirement 8.5: no directive → use routing as-is ─────────────
|
|
627
|
-
let updatedSession = signalResult.updatedSession;
|
|
628
|
-
|
|
629
|
-
// Apply data/context updates from signal to the routed session
|
|
630
|
-
if (signalResult.mergedDirective?.dataUpdate) {
|
|
631
|
-
updatedSession = mergeCollected(updatedSession, signalResult.mergedDirective.dataUpdate);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// Use routing result for flow/step, but carry signal session state
|
|
635
|
-
// Merge routing session changes on top of signal session
|
|
636
|
-
const routingSession = routingResult.session;
|
|
637
|
-
updatedSession = {
|
|
638
|
-
...updatedSession,
|
|
639
|
-
currentFlow: routingSession.currentFlow,
|
|
640
|
-
currentStep: routingSession.currentStep,
|
|
641
|
-
flowHistory: routingSession.flowHistory,
|
|
642
|
-
pendingDirective: routingSession.pendingDirective,
|
|
643
|
-
};
|
|
644
|
-
|
|
645
|
-
const isFlowComplete = routingResult.isFlowComplete;
|
|
646
|
-
|
|
647
|
-
// PRE-EXTRACTION: same logic as below — extract data from user message
|
|
648
|
-
if (routingResult.selectedFlow && !isFlowComplete) {
|
|
649
|
-
if (this.shouldPreExtractData(routingResult.selectedFlow)) {
|
|
650
|
-
logger.debug(
|
|
651
|
-
`[ResponseModal] Pre-extracting data for flow: ${routingResult.selectedFlow.title}`
|
|
652
|
-
);
|
|
653
|
-
const extractedData = await this.preExtractFlowData({
|
|
654
|
-
route: routingResult.selectedFlow,
|
|
655
|
-
history: params.history,
|
|
656
|
-
context: params.context,
|
|
657
|
-
session: updatedSession,
|
|
658
|
-
signal: params.signal,
|
|
659
|
-
});
|
|
660
|
-
if (extractedData && Object.keys(extractedData).length > 0) {
|
|
661
|
-
logger.debug(`[ResponseModal] Pre-extracted data:`, extractedData);
|
|
662
|
-
updatedSession = mergeCollected(updatedSession, extractedData);
|
|
663
|
-
await this.agent.updateCollectedData(extractedData);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// Determine next step
|
|
669
|
-
const stepResult = await this.responsePipeline.determineNextStep({
|
|
670
|
-
selectedFlow: routingResult.selectedFlow,
|
|
671
|
-
selectedStep: routingResult.selectedStep,
|
|
672
|
-
session: updatedSession,
|
|
673
|
-
isFlowComplete,
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
return {
|
|
677
|
-
selectedFlow: stepResult.flowChanged || routingResult.selectedFlow,
|
|
678
|
-
selectedStep: stepResult.nextStep,
|
|
679
|
-
responseDirectives: routingResult.responseDirectives,
|
|
680
|
-
session: stepResult.session,
|
|
681
|
-
isFlowComplete: stepResult.flowChanged ? false : isFlowComplete,
|
|
682
|
-
signalFirings: signalResult.firings,
|
|
683
|
-
signalPreDirective: signalResult.mergedDirective || undefined,
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// ── No signal processor: existing behavior (zero overhead) ────────────
|
|
688
|
-
const routingResult = await this.responsePipeline.handleRoutingAndStepSelection({
|
|
689
|
-
session: params.session,
|
|
690
|
-
history: params.history,
|
|
691
|
-
context: params.context,
|
|
692
|
-
signal: params.signal,
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
let updatedSession = routingResult.session;
|
|
696
|
-
const isFlowComplete = routingResult.isFlowComplete;
|
|
697
|
-
|
|
698
|
-
// PRE-EXTRACTION: If entering a flow that collects data, extract data from user message first
|
|
699
|
-
// This allows us to skip steps whose data is already provided
|
|
700
|
-
if (routingResult.selectedFlow && !isFlowComplete) {
|
|
701
|
-
// Always pre-extract when flow collects data (not just on new flow entry)
|
|
702
|
-
// This ensures step selection has the most up-to-date data
|
|
703
|
-
if (this.shouldPreExtractData(routingResult.selectedFlow)) {
|
|
704
|
-
logger.debug(
|
|
705
|
-
`[ResponseModal] Pre-extracting data for flow: ${routingResult.selectedFlow.title}`
|
|
706
|
-
);
|
|
707
|
-
|
|
708
|
-
const extractedData = await this.preExtractFlowData({
|
|
709
|
-
route: routingResult.selectedFlow,
|
|
710
|
-
history: params.history,
|
|
711
|
-
context: params.context,
|
|
712
|
-
session: updatedSession,
|
|
713
|
-
signal: params.signal,
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
if (extractedData && Object.keys(extractedData).length > 0) {
|
|
717
|
-
logger.debug(
|
|
718
|
-
`[ResponseModal] Pre-extracted data:`,
|
|
719
|
-
extractedData
|
|
720
|
-
);
|
|
721
|
-
// Merge pre-extracted data into session before step selection
|
|
722
|
-
updatedSession = mergeCollected(updatedSession, extractedData);
|
|
723
|
-
// Also update agent's collected data
|
|
724
|
-
await this.agent.updateCollectedData(extractedData);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// Determine next step using pipeline method for consistency
|
|
730
|
-
const stepResult = await this.responsePipeline.determineNextStep({
|
|
731
|
-
selectedFlow: routingResult.selectedFlow,
|
|
732
|
-
selectedStep: routingResult.selectedStep,
|
|
733
|
-
session: updatedSession, // Use updated session with pre-extracted data
|
|
734
|
-
isFlowComplete, // Use updated completion status
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
return {
|
|
738
|
-
selectedFlow: stepResult.flowChanged || routingResult.selectedFlow,
|
|
739
|
-
selectedStep: stepResult.nextStep, // Use the determined next step
|
|
740
|
-
responseDirectives: routingResult.responseDirectives,
|
|
741
|
-
session: stepResult.session,
|
|
742
|
-
// If a branch changed the flow, the original isFlowComplete no longer applies
|
|
743
|
-
isFlowComplete: stepResult.flowChanged ? false : isFlowComplete,
|
|
744
|
-
};
|
|
745
|
-
} catch (error) {
|
|
746
|
-
throw ResponseGenerationError.fromError(error, 'routing_optimization', params);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
/**
|
|
751
|
-
* Apply a signal's position directive (goTo, goToStep, complete, abort, reset).
|
|
752
|
-
* Discards routing result and uses the signal's position decision.
|
|
753
|
-
* @private
|
|
754
|
-
* @requirements 8.3
|
|
755
|
-
*/
|
|
756
|
-
private applySignalPositionDirective(
|
|
757
|
-
signalResult: {
|
|
758
|
-
firings: SignalFiring<TContext, TData>[];
|
|
759
|
-
updatedSession: SessionState<TData>;
|
|
760
|
-
mergedDirective: Directive<TContext, TData> | undefined;
|
|
761
|
-
},
|
|
762
|
-
_params: { session: SessionState<TData>; history: Event[]; context: TContext },
|
|
763
|
-
): {
|
|
764
|
-
selectedFlow?: Flow<TContext, TData>;
|
|
765
|
-
selectedStep?: Step<TContext, TData>;
|
|
766
|
-
responseDirectives?: string[];
|
|
767
|
-
session: SessionState<TData>;
|
|
768
|
-
isFlowComplete: boolean;
|
|
769
|
-
signalFirings?: SignalFiring<TContext, TData>[];
|
|
770
|
-
signalPreDirective?: Directive<TContext, TData>;
|
|
771
|
-
signalHalted?: boolean;
|
|
772
|
-
signalHaltReply?: string;
|
|
773
|
-
} {
|
|
774
|
-
const directive = signalResult.mergedDirective!;
|
|
775
|
-
let session = signalResult.updatedSession;
|
|
776
|
-
const flows = this.agent.getFlows();
|
|
777
|
-
let selectedFlow: Flow<TContext, TData> | undefined;
|
|
778
|
-
let selectedStep: Step<TContext, TData> | undefined;
|
|
779
|
-
let isFlowComplete = false;
|
|
780
|
-
|
|
781
|
-
// Apply data updates if present alongside position
|
|
782
|
-
if (directive.dataUpdate) {
|
|
783
|
-
session = mergeCollected(session, directive.dataUpdate);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
if (directive.goTo) {
|
|
787
|
-
const flowTarget = typeof directive.goTo === 'string'
|
|
788
|
-
? directive.goTo
|
|
789
|
-
: directive.goTo.flow ?? directive.goTo.step;
|
|
790
|
-
|
|
791
|
-
if (flowTarget) {
|
|
792
|
-
const targetFlow = flows.find(f => f.id === flowTarget || f.title === flowTarget);
|
|
793
|
-
if (targetFlow) {
|
|
794
|
-
session = enterFlow(session, targetFlow.id, targetFlow.title);
|
|
795
|
-
selectedFlow = targetFlow;
|
|
796
|
-
|
|
797
|
-
if (typeof directive.goTo === 'object' && directive.goTo.step) {
|
|
798
|
-
const targetStep = targetFlow.getStep(directive.goTo.step);
|
|
799
|
-
if (targetStep) {
|
|
800
|
-
session = enterStep(session, targetStep.id, targetStep.description);
|
|
801
|
-
selectedStep = targetStep;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
} else {
|
|
805
|
-
logger.warn(`[Signals] Pre-phase goTo target not found: "${flowTarget}". Falling back to no flow.`);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
} else if (directive.goToStep) {
|
|
809
|
-
const stepTarget = typeof directive.goToStep === 'string'
|
|
810
|
-
? directive.goToStep
|
|
811
|
-
: directive.goToStep.step;
|
|
812
|
-
const flowTarget = typeof directive.goToStep === 'object'
|
|
813
|
-
? directive.goToStep.flow
|
|
814
|
-
: undefined;
|
|
815
|
-
|
|
816
|
-
if (flowTarget) {
|
|
817
|
-
const targetFlow = flows.find(f => f.id === flowTarget || f.title === flowTarget);
|
|
818
|
-
if (targetFlow) {
|
|
819
|
-
session = enterFlow(session, targetFlow.id, targetFlow.title);
|
|
820
|
-
selectedFlow = targetFlow;
|
|
821
|
-
const targetStep = targetFlow.getStep(stepTarget);
|
|
822
|
-
if (targetStep) {
|
|
823
|
-
session = enterStep(session, targetStep.id, targetStep.description);
|
|
824
|
-
selectedStep = targetStep;
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
} else if (session.currentFlow) {
|
|
828
|
-
const currentFlow = flows.find(f => f.id === session.currentFlow?.id);
|
|
829
|
-
if (currentFlow) {
|
|
830
|
-
selectedFlow = currentFlow;
|
|
831
|
-
const targetStep = currentFlow.getStep(stepTarget);
|
|
832
|
-
if (targetStep) {
|
|
833
|
-
session = enterStep(session, targetStep.id, targetStep.description);
|
|
834
|
-
selectedStep = targetStep;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
} else if (directive.complete) {
|
|
839
|
-
isFlowComplete = true;
|
|
840
|
-
} else if (directive.abort) {
|
|
841
|
-
// Abort — no flow, session cleared or marked
|
|
842
|
-
isFlowComplete = true;
|
|
843
|
-
} else if (directive.reset) {
|
|
844
|
-
if (session.currentFlow) {
|
|
845
|
-
const currentFlow = flows.find(f => f.id === session.currentFlow?.id);
|
|
846
|
-
if (currentFlow) {
|
|
847
|
-
selectedFlow = currentFlow;
|
|
848
|
-
const resetStep = typeof directive.reset === 'object' && directive.reset.step
|
|
849
|
-
? directive.reset.step
|
|
850
|
-
: undefined;
|
|
851
|
-
if (resetStep) {
|
|
852
|
-
const targetStep = currentFlow.getStep(resetStep);
|
|
853
|
-
if (targetStep) {
|
|
854
|
-
session = enterStep(session, targetStep.id, targetStep.description);
|
|
855
|
-
selectedStep = targetStep;
|
|
856
|
-
}
|
|
857
|
-
} else {
|
|
858
|
-
const initialStep = currentFlow.initialStep;
|
|
859
|
-
session = enterStep(session, initialStep.id, initialStep.description);
|
|
860
|
-
selectedStep = initialStep;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
return {
|
|
867
|
-
selectedFlow,
|
|
868
|
-
selectedStep,
|
|
869
|
-
session,
|
|
870
|
-
isFlowComplete,
|
|
871
|
-
signalFirings: signalResult.firings,
|
|
872
|
-
signalPreDirective: signalResult.mergedDirective || undefined,
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
/**
|
|
877
|
-
* Routing skip optimization (Requirements 20.1, 20.2, 20.3):
|
|
878
|
-
* When the current step declares `collect` fields AND pre-extraction populates
|
|
879
|
-
* at least one of those fields from the user's message, skip routing for this turn.
|
|
880
|
-
*
|
|
881
|
-
* Returns the routing result if the skip applies, or undefined to fall through
|
|
882
|
-
* to normal routing.
|
|
883
|
-
* @private
|
|
884
|
-
*/
|
|
885
|
-
private async attemptRoutingSkipForCollect(params: {
|
|
886
|
-
session: SessionState<TData>;
|
|
887
|
-
history: Event[];
|
|
888
|
-
context: TContext;
|
|
889
|
-
signal?: AbortSignal;
|
|
890
|
-
}): Promise<{
|
|
891
|
-
selectedFlow?: Flow<TContext, TData>;
|
|
892
|
-
selectedStep?: Step<TContext, TData>;
|
|
893
|
-
responseDirectives?: string[];
|
|
894
|
-
session: SessionState<TData>;
|
|
895
|
-
isFlowComplete: boolean;
|
|
896
|
-
} | undefined> {
|
|
897
|
-
const { session } = params;
|
|
898
|
-
|
|
899
|
-
// Only applies when we already have a current flow and step
|
|
900
|
-
if (!session.currentFlow || !session.currentStep) {
|
|
901
|
-
return undefined;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
// Also skip this optimization if there's a pending directive (it takes priority)
|
|
905
|
-
if (session.pendingDirective) {
|
|
906
|
-
return undefined;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
// Look up the actual Flow and Step objects to access `collect`
|
|
910
|
-
const currentFlow = this.agent.getFlows().find(
|
|
911
|
-
(f) => f.id === session.currentFlow?.id
|
|
912
|
-
);
|
|
913
|
-
if (!currentFlow) {
|
|
914
|
-
return undefined;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
const currentStep = currentFlow.getStep(session.currentStep.id);
|
|
918
|
-
if (!currentStep || !currentStep.collect || currentStep.collect.length === 0) {
|
|
919
|
-
return undefined;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
// We have a step with collect fields. Run pre-extraction to see if the
|
|
923
|
-
// user's message populates any of them.
|
|
924
|
-
const collectFields = currentStep.collect;
|
|
925
|
-
|
|
926
|
-
// Snapshot current data for comparison
|
|
927
|
-
const dataBefore = { ...session.data };
|
|
928
|
-
|
|
929
|
-
// Run pre-extraction against the current flow
|
|
930
|
-
const extractedData = await this.preExtractFlowData({
|
|
931
|
-
route: currentFlow,
|
|
932
|
-
history: params.history,
|
|
933
|
-
context: params.context,
|
|
934
|
-
session,
|
|
935
|
-
signal: params.signal,
|
|
936
|
-
});
|
|
937
|
-
|
|
938
|
-
if (!extractedData || Object.keys(extractedData).length === 0) {
|
|
939
|
-
return undefined;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
// Determine which collect fields were newly populated by pre-extraction
|
|
943
|
-
const populatedCollectFields: string[] = [];
|
|
944
|
-
for (const field of collectFields) {
|
|
945
|
-
const key = field as string;
|
|
946
|
-
const hadValue = dataBefore[field] !== undefined && dataBefore[field] !== null;
|
|
947
|
-
const hasNewValue = extractedData[field] !== undefined && extractedData[field] !== null;
|
|
948
|
-
if (hasNewValue && !hadValue) {
|
|
949
|
-
populatedCollectFields.push(key);
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
if (populatedCollectFields.length === 0) {
|
|
954
|
-
// Pre-extraction didn't populate any declared collect field — no skip
|
|
955
|
-
return undefined;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// ROUTING SKIP: pre-extraction populated collect fields → retain current flow/step
|
|
959
|
-
logger.debug(
|
|
960
|
-
`[ResponseModal] Routing skip: pre-extraction populated collect fields [${populatedCollectFields.join(', ')}] for step "${currentStep.id}" — skipping FlowRouter`
|
|
961
|
-
);
|
|
962
|
-
|
|
963
|
-
// Merge extracted data into session
|
|
964
|
-
const updatedSession = mergeCollected(session, extractedData);
|
|
965
|
-
await this.agent.updateCollectedData(extractedData);
|
|
966
|
-
|
|
967
|
-
// Determine next step using pipeline method for consistency
|
|
968
|
-
// Pass the current flow/step as the routing result (retained)
|
|
969
|
-
const stepResult = await this.responsePipeline.determineNextStep({
|
|
970
|
-
selectedFlow: currentFlow,
|
|
971
|
-
selectedStep: currentStep,
|
|
972
|
-
session: updatedSession,
|
|
973
|
-
isFlowComplete: false,
|
|
974
|
-
});
|
|
975
|
-
|
|
976
|
-
return {
|
|
977
|
-
selectedFlow: stepResult.flowChanged || currentFlow,
|
|
978
|
-
selectedStep: stepResult.nextStep,
|
|
979
|
-
responseDirectives: undefined,
|
|
980
|
-
session: stepResult.session,
|
|
981
|
-
isFlowComplete: stepResult.flowChanged ? false : false,
|
|
982
|
-
};
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
/**
|
|
986
|
-
* Check if a flow should pre-extract data before determining the initial step
|
|
987
|
-
* @private
|
|
988
|
-
*/
|
|
989
|
-
private shouldPreExtractData(flow: Flow<TContext, TData>): boolean {
|
|
990
|
-
// Pre-extract if flow has declared required or optional fields
|
|
991
|
-
if (flow.requiredFields && flow.requiredFields.length > 0) {
|
|
992
|
-
return true;
|
|
993
|
-
}
|
|
994
|
-
if (flow.optionalFields && flow.optionalFields.length > 0) {
|
|
995
|
-
return true;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
// Pre-extract if any step in the flow collects data
|
|
999
|
-
const steps = flow.getAllSteps();
|
|
1000
|
-
const hasDataCollectionSteps = steps.some(
|
|
1001
|
-
step => step.collect && step.collect.length > 0
|
|
1002
|
-
);
|
|
1003
|
-
|
|
1004
|
-
return hasDataCollectionSteps;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
/**
|
|
1008
|
-
* Pre-extract data from user message when entering a flow
|
|
1009
|
-
* This allows skipping steps whose data is already provided
|
|
1010
|
-
* @private
|
|
1011
|
-
*/
|
|
1012
|
-
private async preExtractFlowData(params: {
|
|
1013
|
-
route: Flow<TContext, TData>;
|
|
1014
|
-
history: Event[];
|
|
1015
|
-
context: TContext;
|
|
1016
|
-
session: SessionState<TData>;
|
|
1017
|
-
signal?: AbortSignal;
|
|
1018
|
-
}): Promise<Partial<TData>> {
|
|
1019
|
-
const { route: flow, history, signal } = params;
|
|
1020
|
-
|
|
1021
|
-
// Build a schema for data extraction based on flow's fields
|
|
1022
|
-
const extractionSchema = this.agent.schema;
|
|
1023
|
-
if (!extractionSchema) {
|
|
1024
|
-
logger.warn(`[ResponseModal] No schema available for pre-extraction`);
|
|
1025
|
-
return {};
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
// Get last user message
|
|
1029
|
-
const lastMessage = getLastMessageFromHistory(history);
|
|
1030
|
-
|
|
1031
|
-
// Build extraction prompt
|
|
1032
|
-
const extractionPrompt = [
|
|
1033
|
-
`Extract any relevant information from the user's message that matches the following data fields.`,
|
|
1034
|
-
`Only extract information that is explicitly stated or clearly implied.`,
|
|
1035
|
-
``,
|
|
1036
|
-
`User's message: "${lastMessage}"`,
|
|
1037
|
-
``,
|
|
1038
|
-
`Extract data for these fields if present:`,
|
|
1039
|
-
];
|
|
1040
|
-
|
|
1041
|
-
// Add field descriptions
|
|
1042
|
-
if (flow.requiredFields) {
|
|
1043
|
-
extractionPrompt.push(`Required fields: ${flow.requiredFields.join(', ')}`);
|
|
1044
|
-
}
|
|
1045
|
-
if (flow.optionalFields) {
|
|
1046
|
-
extractionPrompt.push(`Optional fields: ${flow.optionalFields.join(', ')}`);
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
extractionPrompt.push(
|
|
1050
|
-
``,
|
|
1051
|
-
`Return ONLY the extracted data as JSON. If no data can be extracted, return an empty object {}.`
|
|
1052
|
-
);
|
|
1053
|
-
|
|
1054
|
-
// Convert Event[] to HistoryItem[] for provider call
|
|
1055
|
-
const historyItems = eventsToHistory(history);
|
|
1056
|
-
|
|
1057
|
-
// Call AI to extract data
|
|
1058
|
-
const agentOptions = this.agent.getAgentOptions();
|
|
1059
|
-
try {
|
|
1060
|
-
const result = await agentOptions.provider.generateMessage<TContext, Partial<TData>>({
|
|
1061
|
-
prompt: extractionPrompt.join('\n'),
|
|
1062
|
-
history: historyItems,
|
|
1063
|
-
context: {} as TContext, // Passed as empty object so AI doesn't "extract" from context
|
|
1064
|
-
// NOTE: context is intentionally NOT passed here.
|
|
1065
|
-
// Passing context caused the AI to "extract" data from the lead's context
|
|
1066
|
-
// (e.g., name, sector, city) instead of from what the user actually said.
|
|
1067
|
-
signal,
|
|
1068
|
-
parameters: {
|
|
1069
|
-
jsonSchema: extractionSchema,
|
|
1070
|
-
schemaName: 'data_extraction',
|
|
1071
|
-
},
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
return result.structured || {};
|
|
1075
|
-
} catch (error) {
|
|
1076
|
-
logger.error(`[ResponseModal] Pre-extraction failed:`, error);
|
|
1077
|
-
return {};
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
542
|
/**
|
|
1082
543
|
* Unified response generation for non-streaming responses
|
|
1083
544
|
* @private
|
|
@@ -1113,20 +574,13 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1113
574
|
if (signalHalted) {
|
|
1114
575
|
const haltMessage = signalHaltReply || '';
|
|
1115
576
|
// Run post-signal phase even on halt (post-phase sees complete turn context)
|
|
1116
|
-
const
|
|
1117
|
-
session, effectiveContext, historyEvents,
|
|
1118
|
-
);
|
|
1119
|
-
session =
|
|
1120
|
-
signalFirings.push(...
|
|
1121
|
-
|
|
1122
|
-
// Apply post-phase position directive as pendingDirective (Requirement 9.3)
|
|
1123
|
-
if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
|
|
1124
|
-
session = { ...session, pendingDirective: postResult.mergedDirective };
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
const message = this.applyPostSignalReply(haltMessage, postResult.mergedDirective);
|
|
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;
|
|
1128
583
|
|
|
1129
|
-
await this.finalizeSession(session, effectiveContext);
|
|
1130
584
|
return {
|
|
1131
585
|
message,
|
|
1132
586
|
session,
|
|
@@ -1134,7 +588,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1134
588
|
isFlowComplete: false,
|
|
1135
589
|
executedSteps: [],
|
|
1136
590
|
stoppedReason: haltMessage ? 'reply' : 'halt',
|
|
1137
|
-
triggeredSignals: signalFirings.length > 0 ? signalFirings
|
|
591
|
+
triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
|
|
1138
592
|
};
|
|
1139
593
|
}
|
|
1140
594
|
|
|
@@ -1144,8 +598,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1144
598
|
let stoppedReason: StoppedReason | undefined;
|
|
1145
599
|
let appliedInstructions: AppliedInstruction[] | undefined;
|
|
1146
600
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
601
|
if (selectedFlow && !isFlowComplete) {
|
|
1150
602
|
// AUTO-CHAIN: Walk consecutive auto-steps before any LLM work.
|
|
1151
603
|
// If the current step is auto, the executor advances through it (and any
|
|
@@ -1167,13 +619,13 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1167
619
|
|
|
1168
620
|
session = autoResult.session;
|
|
1169
621
|
|
|
1170
|
-
// Handle halt: emit verbatim reply,
|
|
622
|
+
// Handle halt: emit verbatim reply, return — no LLM call.
|
|
623
|
+
// respond() finalizes the returned session exactly once.
|
|
1171
624
|
if (autoResult.stoppedReason === 'halt') {
|
|
1172
625
|
message = autoResult.mergedDirective?.reply || '';
|
|
1173
626
|
stoppedReason = 'halt';
|
|
1174
627
|
executedSteps = [];
|
|
1175
628
|
|
|
1176
|
-
await this.finalizeSession(session, effectiveContext);
|
|
1177
629
|
return {
|
|
1178
630
|
message,
|
|
1179
631
|
session,
|
|
@@ -1197,7 +649,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1197
649
|
history,
|
|
1198
650
|
});
|
|
1199
651
|
|
|
1200
|
-
await this.finalizeSession(session, effectiveContext);
|
|
1201
652
|
return {
|
|
1202
653
|
message: '',
|
|
1203
654
|
session,
|
|
@@ -1286,22 +737,13 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1286
737
|
// Runs after finalize/onComplete and before session persistence.
|
|
1287
738
|
// Post-phase signals see the complete turn result: assistant message in
|
|
1288
739
|
// history, collected data, tool results.
|
|
1289
|
-
const
|
|
1290
|
-
session, effectiveContext, historyEvents,
|
|
1291
|
-
);
|
|
1292
|
-
session =
|
|
1293
|
-
|
|
740
|
+
const post = await this.signalCoordinator.applyPostPhase({
|
|
741
|
+
session, context: effectiveContext, historyEvents, message,
|
|
742
|
+
});
|
|
743
|
+
session = post.session;
|
|
1294
744
|
// Append post-phase firings to the accumulator (preserves fire order)
|
|
1295
|
-
signalFirings.push(...
|
|
1296
|
-
|
|
1297
|
-
// Requirement 9.3: Post-phase position directive sets session.pendingDirective
|
|
1298
|
-
// (no mid-turn re-entry per D6 decision). Pre-LLM-only fields are already
|
|
1299
|
-
// dropped inside runPostSignalPhase per Phase 4.5.
|
|
1300
|
-
if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
|
|
1301
|
-
session = { ...session, pendingDirective: postResult.mergedDirective };
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
message = this.applyPostSignalReply(message, postResult.mergedDirective);
|
|
745
|
+
signalFirings.push(...post.firings);
|
|
746
|
+
message = post.message;
|
|
1305
747
|
|
|
1306
748
|
// Ensure response structure completeness (Requirement 8.1, 8.2, 8.3)
|
|
1307
749
|
// - executedSteps: array of steps executed (empty array if none)
|
|
@@ -1315,67 +757,10 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1315
757
|
executedSteps: executedSteps || [],
|
|
1316
758
|
stoppedReason,
|
|
1317
759
|
appliedInstructions,
|
|
1318
|
-
triggeredSignals: signalFirings.length > 0 ? signalFirings
|
|
760
|
+
triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
|
|
1319
761
|
};
|
|
1320
762
|
}
|
|
1321
763
|
|
|
1322
|
-
/**
|
|
1323
|
-
* Execute prepare function for current step if available
|
|
1324
|
-
* @private
|
|
1325
|
-
*/
|
|
1326
|
-
private async executeStepPrepare(session: SessionState<TData>, context: TContext): Promise<void> {
|
|
1327
|
-
if (session.currentFlow && session.currentStep) {
|
|
1328
|
-
const currentFlow = this.agent.getFlows().find(
|
|
1329
|
-
(r) => r.id === session.currentFlow?.id
|
|
1330
|
-
);
|
|
1331
|
-
if (currentFlow) {
|
|
1332
|
-
const currentStep = currentFlow.getStep(session.currentStep.id);
|
|
1333
|
-
// Skip auto-steps — their prepare is handled by AutoChainExecutor
|
|
1334
|
-
if (currentStep?.auto) {
|
|
1335
|
-
logger.debug(`[ResponseModal] Skipping pre-routing prepare for auto-step: ${currentStep.id}`);
|
|
1336
|
-
return;
|
|
1337
|
-
}
|
|
1338
|
-
if (currentStep?.prepare) {
|
|
1339
|
-
logger.debug(`[ResponseModal] Executing prepare for step: ${currentStep.id}`);
|
|
1340
|
-
await this.executePrepareFinalize(
|
|
1341
|
-
currentStep.prepare,
|
|
1342
|
-
context,
|
|
1343
|
-
session.data,
|
|
1344
|
-
currentFlow,
|
|
1345
|
-
currentStep
|
|
1346
|
-
);
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
/**
|
|
1353
|
-
* Execute finalize function for current step if available
|
|
1354
|
-
* @private
|
|
1355
|
-
*/
|
|
1356
|
-
private async executeStepFinalize(session: SessionState<TData>, context: TContext): Promise<void> {
|
|
1357
|
-
if (session.currentFlow && session.currentStep) {
|
|
1358
|
-
const currentFlow = this.agent.getFlows().find(
|
|
1359
|
-
(r) => r.id === session.currentFlow?.id
|
|
1360
|
-
);
|
|
1361
|
-
if (currentFlow) {
|
|
1362
|
-
const currentStep = currentFlow.getStep(session.currentStep.id);
|
|
1363
|
-
if (currentStep?.finalize) {
|
|
1364
|
-
logger.debug(
|
|
1365
|
-
`[ResponseModal] Executing finalize for step: ${currentStep.id}`
|
|
1366
|
-
);
|
|
1367
|
-
await this.executePrepareFinalize(
|
|
1368
|
-
currentStep.finalize,
|
|
1369
|
-
context,
|
|
1370
|
-
session.data,
|
|
1371
|
-
currentFlow,
|
|
1372
|
-
currentStep
|
|
1373
|
-
);
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
764
|
/**
|
|
1380
765
|
* Process flow response with unified tool execution and data collection
|
|
1381
766
|
* @private
|
|
@@ -1409,87 +794,20 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1409
794
|
const { selectedFlow, selectedStep, responseDirectives, history, context, historyEvents, signal, transientAppendage, mergedPreDirective } = params;
|
|
1410
795
|
let session = params.session;
|
|
1411
796
|
|
|
1412
|
-
//
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
logger.debug(`[ResponseModal] Step determination: flow match=${isInSameFlow}, currentFlow=${session.currentFlow?.id}, selectedFlow=${selectedFlow.id}, currentStep=${currentStep?.id || 'none'}`);
|
|
1424
|
-
|
|
1425
|
-
// STEP 1 (Algorithm 1): branches win over linear chain
|
|
1426
|
-
if (currentStep?.branches && currentStep.branches.length > 0) {
|
|
1427
|
-
const branchResult = await this.responsePipeline.evaluateStepBranches(
|
|
1428
|
-
currentStep, selectedFlow, session, context
|
|
1429
|
-
);
|
|
1430
|
-
if (branchResult) {
|
|
1431
|
-
if (branchResult.nextStep) {
|
|
1432
|
-
nextStep = branchResult.nextStep;
|
|
1433
|
-
session = branchResult.session;
|
|
1434
|
-
} else {
|
|
1435
|
-
// Flow transition or completion — no local step to render
|
|
1436
|
-
// Return empty message with updated session; caller handles flow transition
|
|
1437
|
-
return { message: '', session: branchResult.session };
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
if (!nextStep!) {
|
|
1443
|
-
// Get candidate steps based on current position in the flow
|
|
1444
|
-
const flowRouter = this.agent.getFlowRouter();
|
|
1445
|
-
const candidates = await flowRouter.getCandidateStepsWithConditions(
|
|
1446
|
-
selectedFlow,
|
|
1447
|
-
currentStep, // Pass current step instead of undefined to maintain progression
|
|
1448
|
-
createTemplateContext({ data: session.data, session, context })
|
|
1449
|
-
);
|
|
1450
|
-
|
|
1451
|
-
logger.debug(`[ResponseModal] Found ${candidates.length} candidate steps${currentStep ? ' from current step ' + currentStep.id : ' (new flow entry)'}`);
|
|
1452
|
-
|
|
1453
|
-
if (candidates.length > 0) {
|
|
1454
|
-
nextStep = candidates[0].step;
|
|
1455
|
-
logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new flow'}`);
|
|
1456
|
-
} else {
|
|
1457
|
-
// Fallback to initial step even if it should be skipped
|
|
1458
|
-
nextStep = selectedFlow.initialStep;
|
|
1459
|
-
logger.warn(`[FlowConfigurationError] No valid steps found: all candidates were skipped in flow. Falling back to initial step "${nextStep.id}". Review step skip conditions.`);
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
// Update session with next step
|
|
1465
|
-
// If the next step has requires fields that are missing, stay at the previous step
|
|
1466
|
-
if (nextStep.requires && nextStep.requires.length > 0) {
|
|
1467
|
-
const sessionData = session.data || {};
|
|
1468
|
-
const missingRequires = nextStep.requires.filter(
|
|
1469
|
-
field => (sessionData as Record<string, unknown>)[String(field)] === undefined
|
|
1470
|
-
);
|
|
1471
|
-
if (missingRequires.length > 0) {
|
|
1472
|
-
const warning = `[FlowConfigurationError] Cannot advance to step "${nextStep.description || nextStep.id}": ` +
|
|
1473
|
-
`missing required fields [${missingRequires.join(', ')}]. Staying at current step. Ensure preceding steps collect these fields.`;
|
|
1474
|
-
logger.warn(warning);
|
|
1475
|
-
console.warn(warning);
|
|
1476
|
-
// Stay at the current step - don't enter the next one
|
|
1477
|
-
const currentStepId = session.currentStep?.id;
|
|
1478
|
-
if (currentStepId) {
|
|
1479
|
-
const currentStepInstance = selectedFlow.getStep(currentStepId);
|
|
1480
|
-
if (currentStepInstance) {
|
|
1481
|
-
nextStep = currentStepInstance;
|
|
1482
|
-
logger.debug(`[ResponseModal] Staying at current step: ${nextStep.id} due to missing requires`);
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
} else {
|
|
1486
|
-
session = enterStep(session, nextStep.id, nextStep.description);
|
|
1487
|
-
logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
|
|
1488
|
-
}
|
|
1489
|
-
} else {
|
|
1490
|
-
session = enterStep(session, nextStep.id, nextStep.description);
|
|
1491
|
-
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 };
|
|
1492
808
|
}
|
|
809
|
+
const nextStep = stepResolution.nextStep!;
|
|
810
|
+
session = stepResolution.session;
|
|
1493
811
|
|
|
1494
812
|
// Build response schema for this flow (with collect fields from step)
|
|
1495
813
|
const responseSchema = this.responseEngine.responseSchemaForFlow(selectedFlow, nextStep, this.agent.schema);
|
|
@@ -1573,7 +891,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1573
891
|
});
|
|
1574
892
|
|
|
1575
893
|
// Execute tools with unified loop handling
|
|
1576
|
-
const toolResult = await this.
|
|
894
|
+
const toolResult = await this.toolLoopExecutor.runLoop({
|
|
1577
895
|
toolCalls,
|
|
1578
896
|
context,
|
|
1579
897
|
session,
|
|
@@ -1640,19 +958,14 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1640
958
|
if (signalHalted) {
|
|
1641
959
|
const haltMessage = signalHaltReply || '';
|
|
1642
960
|
// Run post-signal phase even on halt
|
|
1643
|
-
const
|
|
1644
|
-
session, effectiveContext, historyEvents,
|
|
1645
|
-
);
|
|
1646
|
-
session =
|
|
1647
|
-
signalFirings.push(...
|
|
1648
|
-
|
|
1649
|
-
if (postResult.mergedDirective && hasDirectivePositionField(postResult.mergedDirective)) {
|
|
1650
|
-
session = { ...session, pendingDirective: postResult.mergedDirective };
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
const message = this.applyPostSignalReply(haltMessage, postResult.mergedDirective);
|
|
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;
|
|
1654
967
|
|
|
1655
|
-
await this.
|
|
968
|
+
await this.sessionFinalizer.finalize(session, effectiveContext);
|
|
1656
969
|
yield {
|
|
1657
970
|
delta: message,
|
|
1658
971
|
accumulated: message,
|
|
@@ -1660,7 +973,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1660
973
|
session,
|
|
1661
974
|
stoppedReason: haltMessage ? 'reply' : 'halt',
|
|
1662
975
|
executedSteps: [],
|
|
1663
|
-
triggeredSignals: signalFirings.length > 0 ? signalFirings
|
|
976
|
+
triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
|
|
1664
977
|
} as AgentResponseStreamChunk<TData>;
|
|
1665
978
|
return;
|
|
1666
979
|
}
|
|
@@ -1690,7 +1003,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1690
1003
|
// Handle halt: emit verbatim reply as a single chunk, done.
|
|
1691
1004
|
if (autoResult.stoppedReason === 'halt') {
|
|
1692
1005
|
const reply = autoResult.mergedDirective?.reply || '';
|
|
1693
|
-
await this.
|
|
1006
|
+
await this.sessionFinalizer.finalize(session, effectiveContext);
|
|
1694
1007
|
yield {
|
|
1695
1008
|
delta: reply,
|
|
1696
1009
|
accumulated: reply,
|
|
@@ -1698,7 +1011,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1698
1011
|
session,
|
|
1699
1012
|
stoppedReason: 'halt',
|
|
1700
1013
|
executedSteps: [],
|
|
1701
|
-
triggeredSignals: signalFirings.length > 0 ? signalFirings
|
|
1014
|
+
triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
|
|
1702
1015
|
} as AgentResponseStreamChunk<TData>;
|
|
1703
1016
|
return;
|
|
1704
1017
|
}
|
|
@@ -1767,35 +1080,35 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1767
1080
|
});
|
|
1768
1081
|
}
|
|
1769
1082
|
|
|
1770
|
-
// ── Intercept the inner stream
|
|
1771
|
-
//
|
|
1772
|
-
//
|
|
1773
|
-
// (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).
|
|
1774
1087
|
for await (const chunk of innerStream!) {
|
|
1775
1088
|
if (chunk.done) {
|
|
1776
1089
|
// Run post-signal phase on final chunk (Requirement 9.1, 9.2)
|
|
1777
|
-
const
|
|
1778
|
-
chunk.session || session,
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
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);
|
|
1782
1098
|
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
finalSession = { ...finalSession, pendingDirective: postResult.mergedDirective };
|
|
1786
|
-
}
|
|
1099
|
+
const accumulated = post.message;
|
|
1100
|
+
const delta = post.replyOverridden ? accumulated : chunk.delta;
|
|
1787
1101
|
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
: chunk.delta;
|
|
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);
|
|
1792
1105
|
|
|
1793
1106
|
yield {
|
|
1794
1107
|
...chunk,
|
|
1795
1108
|
delta,
|
|
1796
1109
|
accumulated,
|
|
1797
1110
|
session: finalSession,
|
|
1798
|
-
triggeredSignals: signalFirings.length > 0 ? signalFirings
|
|
1111
|
+
triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
|
|
1799
1112
|
} as AgentResponseStreamChunk<TData>;
|
|
1800
1113
|
} else {
|
|
1801
1114
|
yield chunk;
|
|
@@ -1830,86 +1143,25 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1830
1143
|
const { selectedFlow, selectedStep, responseDirectives, history, context, historyEvents, signal, transientAppendage, mergedPreDirective } = params;
|
|
1831
1144
|
let session = params.session;
|
|
1832
1145
|
|
|
1833
|
-
//
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
// Branch resolved — yield a final chunk with the updated session and return
|
|
1850
|
-
if (branchResult.nextStep) {
|
|
1851
|
-
session = branchResult.session;
|
|
1852
|
-
nextStep = branchResult.nextStep;
|
|
1853
|
-
} else {
|
|
1854
|
-
// Flow transition or completion — no step to render
|
|
1855
|
-
yield {
|
|
1856
|
-
delta: '',
|
|
1857
|
-
accumulated: '',
|
|
1858
|
-
done: true,
|
|
1859
|
-
session: branchResult.session,
|
|
1860
|
-
} as AgentResponseStreamChunk<TData>;
|
|
1861
|
-
return;
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
if (!nextStep!) {
|
|
1867
|
-
// Get candidate steps based on current position in the flow
|
|
1868
|
-
const flowRouter = this.agent.getFlowRouter();
|
|
1869
|
-
const candidates = await flowRouter.getCandidateStepsWithConditions(
|
|
1870
|
-
selectedFlow,
|
|
1871
|
-
currentStep, // Pass current step instead of undefined to maintain progression
|
|
1872
|
-
createTemplateContext({ data: session.data, session, context })
|
|
1873
|
-
);
|
|
1874
|
-
|
|
1875
|
-
if (candidates.length > 0) {
|
|
1876
|
-
nextStep = candidates[0].step;
|
|
1877
|
-
logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new flow'}`);
|
|
1878
|
-
} else {
|
|
1879
|
-
nextStep = selectedFlow.initialStep;
|
|
1880
|
-
logger.warn(`[FlowConfigurationError] No valid steps found: all candidates were skipped in flow. Falling back to initial step "${nextStep.id}". Review step skip conditions.`);
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
// Update session with next step
|
|
1886
|
-
// If the next step has requires fields that are missing, stay at the previous step
|
|
1887
|
-
if (nextStep.requires && nextStep.requires.length > 0) {
|
|
1888
|
-
const sessionData = session.data || {};
|
|
1889
|
-
const missingRequires = nextStep.requires.filter(
|
|
1890
|
-
field => (sessionData as Record<string, unknown>)[String(field)] === undefined
|
|
1891
|
-
);
|
|
1892
|
-
if (missingRequires.length > 0) {
|
|
1893
|
-
const warning = `[FlowConfigurationError] Cannot advance to step "${nextStep.description || nextStep.id}": ` +
|
|
1894
|
-
`missing required fields [${missingRequires.join(', ')}]. Staying at current step. Ensure preceding steps collect these fields.`;
|
|
1895
|
-
logger.warn(warning);
|
|
1896
|
-
console.warn(warning);
|
|
1897
|
-
const currentStepId = session.currentStep?.id;
|
|
1898
|
-
if (currentStepId) {
|
|
1899
|
-
const currentStepInstance = selectedFlow.getStep(currentStepId);
|
|
1900
|
-
if (currentStepInstance) {
|
|
1901
|
-
nextStep = currentStepInstance;
|
|
1902
|
-
logger.debug(`[ResponseModal] Staying at current step: ${nextStep.id} due to missing requires`);
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
} else {
|
|
1906
|
-
session = enterStep(session, nextStep.id, nextStep.description);
|
|
1907
|
-
logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
|
|
1908
|
-
}
|
|
1909
|
-
} else {
|
|
1910
|
-
session = enterStep(session, nextStep.id, nextStep.description);
|
|
1911
|
-
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;
|
|
1912
1162
|
}
|
|
1163
|
+
const nextStep = stepResolution.nextStep!;
|
|
1164
|
+
session = stepResolution.session;
|
|
1913
1165
|
|
|
1914
1166
|
// Build response schema and prompt (same as non-streaming)
|
|
1915
1167
|
const responseSchema = this.responseEngine.responseSchemaForFlow(selectedFlow, nextStep, this.agent.schema);
|
|
@@ -1921,7 +1173,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1921
1173
|
const reply = mergedPreDirective.reply || '';
|
|
1922
1174
|
const reason: StoppedReason = mergedPreDirective.reply ? 'reply' : 'halt';
|
|
1923
1175
|
logger.debug(`[ResponseModal] Halt (streaming) — skipping LLM call for step ${nextStep.id}, stoppedReason: ${reason}`);
|
|
1924
|
-
await this.finalizeSession(session, context);
|
|
1925
1176
|
yield {
|
|
1926
1177
|
delta: reply,
|
|
1927
1178
|
accumulated: reply,
|
|
@@ -1943,13 +1194,12 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1943
1194
|
createTemplateContext({ data: session.data || {}, context, session })
|
|
1944
1195
|
);
|
|
1945
1196
|
logger.debug(`[ResponseModal] Step.reply (streaming) — skipping LLM call for step ${nextStep.id}`);
|
|
1946
|
-
await this.finalizeSession(session, context);
|
|
1947
1197
|
yield {
|
|
1948
1198
|
delta: effectiveReply,
|
|
1949
1199
|
accumulated: effectiveReply,
|
|
1950
1200
|
done: true,
|
|
1951
1201
|
session,
|
|
1952
|
-
stoppedReason: 'reply'
|
|
1202
|
+
stoppedReason: 'reply',
|
|
1953
1203
|
executedSteps: [{ id: nextStep.id, flowId: selectedFlow.id }],
|
|
1954
1204
|
} as AgentResponseStreamChunk<TData>;
|
|
1955
1205
|
return;
|
|
@@ -1998,73 +1248,23 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1998
1248
|
if (chunk.done && chunk.structured?.toolCalls) {
|
|
1999
1249
|
toolCalls = chunk.structured.toolCalls;
|
|
2000
1250
|
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
history: historyEvents,
|
|
2019
|
-
signal,
|
|
2020
|
-
flow: selectedFlow,
|
|
2021
|
-
step: nextStep,
|
|
2022
|
-
})) {
|
|
2023
|
-
// Apply context updates
|
|
2024
|
-
if (update.contextUpdate) {
|
|
2025
|
-
try {
|
|
2026
|
-
await this.agent.updateContext(update.contextUpdate as Partial<TContext>);
|
|
2027
|
-
} catch (error) {
|
|
2028
|
-
logger.error(`[ResponseModal] Failed to update context from concurrent tool:`, error);
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
// Apply data updates
|
|
2033
|
-
if (update.dataUpdate) {
|
|
2034
|
-
try {
|
|
2035
|
-
const updateDataMethod = this.agent.getUpdateDataMethod();
|
|
2036
|
-
session = await updateDataMethod(session, update.dataUpdate);
|
|
2037
|
-
} catch (error) {
|
|
2038
|
-
logger.error(`[ResponseModal] Failed to update data from concurrent tool:`, error);
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
|
|
2042
|
-
// Yield progress updates immediately
|
|
2043
|
-
if (update.progress) {
|
|
2044
|
-
yield {
|
|
2045
|
-
delta: '',
|
|
2046
|
-
accumulated: chunk.accumulated,
|
|
2047
|
-
done: false,
|
|
2048
|
-
session,
|
|
2049
|
-
toolCalls: undefined,
|
|
2050
|
-
isFlowComplete: false,
|
|
2051
|
-
metadata: { toolProgress: update.progress, toolCallId: update.toolCallId },
|
|
2052
|
-
};
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
|
-
logger.debug(`[ResponseModal] Concurrent tool execution completed for ${toolCallRequests.length} tools`);
|
|
2057
|
-
} catch (error) {
|
|
2058
|
-
logger.error(`[ResponseModal] Concurrent tool execution failed, falling back to sequential:`, error);
|
|
2059
|
-
// Fall back to the unified tool loop on failure
|
|
2060
|
-
const toolResult = await this.executeUnifiedToolLoop({
|
|
2061
|
-
toolCalls, context, session, history, selectedFlow,
|
|
2062
|
-
responsePrompt, availableTools, responseSchema, signal,
|
|
2063
|
-
});
|
|
2064
|
-
session = toolResult.session;
|
|
2065
|
-
toolCalls = toolResult.finalToolCalls;
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
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;
|
|
2068
1268
|
}
|
|
2069
1269
|
|
|
2070
1270
|
// Extract collected data on final chunk
|
|
@@ -2077,11 +1277,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
2077
1277
|
});
|
|
2078
1278
|
}
|
|
2079
1279
|
|
|
2080
|
-
// Handle session finalization on final chunk
|
|
2081
|
-
if (chunk.done) {
|
|
2082
|
-
await this.finalizeSession(session, context);
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
1280
|
// Response structure completeness (Requirement 8.1, 8.2, 8.3)
|
|
2086
1281
|
// - executedSteps: single step executed in this response
|
|
2087
1282
|
// - stoppedReason: 'needs_input' for single-step execution (waiting for user input)
|
|
@@ -2109,341 +1304,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
2109
1304
|
}
|
|
2110
1305
|
|
|
2111
1306
|
/**
|
|
2112
|
-
* Unified tool execution logic with loop handling
|
|
2113
|
-
* Consolidates the complex tool execution logic from both streaming and non-streaming responses
|
|
2114
|
-
* @private
|
|
2115
|
-
*/
|
|
2116
|
-
private async executeUnifiedToolLoop(params: {
|
|
2117
|
-
toolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
|
|
2118
|
-
context: TContext;
|
|
2119
|
-
session: SessionState<TData>;
|
|
2120
|
-
history: HistoryItem[];
|
|
2121
|
-
selectedFlow?: Flow<TContext, TData>;
|
|
2122
|
-
responsePrompt: string;
|
|
2123
|
-
availableTools: Array<{
|
|
2124
|
-
id: string;
|
|
2125
|
-
name: string;
|
|
2126
|
-
description?: string;
|
|
2127
|
-
parameters?: unknown;
|
|
2128
|
-
}>;
|
|
2129
|
-
responseSchema?: Record<string, unknown>;
|
|
2130
|
-
signal?: AbortSignal;
|
|
2131
|
-
}): Promise<{
|
|
2132
|
-
session: SessionState<TData>;
|
|
2133
|
-
finalToolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
|
|
2134
|
-
finalMessage?: string;
|
|
2135
|
-
structured?: AgentStructuredResponse;
|
|
2136
|
-
}> {
|
|
2137
|
-
try {
|
|
2138
|
-
const { context, history, selectedFlow, responsePrompt, availableTools, responseSchema, signal } = params;
|
|
2139
|
-
let { toolCalls, session } = params;
|
|
2140
|
-
|
|
2141
|
-
// Convert HistoryItem[] to Event[] for internal processing
|
|
2142
|
-
const historyEvents = historyToEvents(history);
|
|
2143
|
-
|
|
2144
|
-
// Map to store tool execution results for history
|
|
2145
|
-
const toolResultsMap = new Map<string, string>();
|
|
2146
|
-
// Map to store tool call arguments for history reconstruction
|
|
2147
|
-
const toolArgsMap = new Map<string, Record<string, unknown>>();
|
|
2148
|
-
|
|
2149
|
-
// Execute initial dynamic tool calls
|
|
2150
|
-
if (toolCalls && toolCalls.length > 0) {
|
|
2151
|
-
logger.debug(`[ResponseModal] Executing ${toolCalls.length} dynamic tool calls:`, toolCalls.map(tc => tc.toolName));
|
|
2152
|
-
|
|
2153
|
-
for (const toolCall of toolCalls) {
|
|
2154
|
-
const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
|
|
2155
|
-
if (!tool) {
|
|
2156
|
-
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.`);
|
|
2157
|
-
continue;
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
try {
|
|
2161
|
-
// Use ToolManager for unified tool execution
|
|
2162
|
-
const toolManager = this.getToolManager();
|
|
2163
|
-
const toolResult = await toolManager.executeTool({
|
|
2164
|
-
tool: tool,
|
|
2165
|
-
context,
|
|
2166
|
-
updateContext: this.agent.updateContext.bind(this.agent),
|
|
2167
|
-
updateData: this.agent.updateCollectedData.bind(this.agent),
|
|
2168
|
-
history: historyEvents, // Use Event[] for tool execution
|
|
2169
|
-
data: session.data,
|
|
2170
|
-
toolArguments: toolCall.arguments,
|
|
2171
|
-
});
|
|
2172
|
-
|
|
2173
|
-
// Store the actual tool result data for history
|
|
2174
|
-
toolResultsMap.set(toolCall.toolName, serializeToolResult(toolResult));
|
|
2175
|
-
toolArgsMap.set(toolCall.toolName, toolCall.arguments);
|
|
2176
|
-
|
|
2177
|
-
// Check if tool execution was successful
|
|
2178
|
-
if (!toolResult.success) {
|
|
2179
|
-
logger.error(`[ResponseModal] Tool execution failed: ${toolCall.toolName} - ${toolResult.error}`);
|
|
2180
|
-
// Continue with other tools rather than failing completely
|
|
2181
|
-
continue;
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
// Update context with tool results
|
|
2185
|
-
if (toolResult.contextUpdate) {
|
|
2186
|
-
try {
|
|
2187
|
-
await this.agent.updateContext(toolResult.contextUpdate as Partial<TContext>);
|
|
2188
|
-
} catch (error) {
|
|
2189
|
-
logger.error(`[ResponseModal] Failed to update context from tool ${toolCall.toolName}:`, error);
|
|
2190
|
-
// Continue execution but log the error
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
|
-
// Update collected data with tool results
|
|
2195
|
-
if (toolResult.dataUpdate) {
|
|
2196
|
-
try {
|
|
2197
|
-
const updateDataMethod = this.agent.getUpdateDataMethod();
|
|
2198
|
-
session = await updateDataMethod(session, toolResult.dataUpdate as Partial<TData>);
|
|
2199
|
-
logger.debug(`[ResponseModal] Tool updated collected data:`, toolResult.dataUpdate);
|
|
2200
|
-
} catch (error) {
|
|
2201
|
-
logger.error(`[ResponseModal] Failed to update data from tool ${toolCall.toolName}:`, error);
|
|
2202
|
-
// Continue execution but log the error
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
logger.debug(`[ResponseModal] Executed dynamic tool: ${toolCall.toolName} (success: ${toolResult.success})`);
|
|
2207
|
-
} catch (error) {
|
|
2208
|
-
logger.error(`[ResponseModal] Tool execution error for ${toolCall.toolName}:`, error);
|
|
2209
|
-
// Continue with other tools rather than failing the entire response
|
|
2210
|
-
continue;
|
|
2211
|
-
}
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
|
-
// TOOL LOOP: Allow AI to make follow-up tool calls after initial tool execution
|
|
2216
|
-
const MAX_TOOL_LOOPS = this.options?.maxToolLoops || 5;
|
|
2217
|
-
let toolLoopCount = 0;
|
|
2218
|
-
let hasToolCalls = toolCalls && toolCalls.length > 0;
|
|
2219
|
-
let finalMessage: string | undefined;
|
|
2220
|
-
let followUpStructured: AgentStructuredResponse | undefined;
|
|
2221
|
-
|
|
2222
|
-
while (hasToolCalls && toolLoopCount < MAX_TOOL_LOOPS) {
|
|
2223
|
-
toolLoopCount++;
|
|
2224
|
-
logger.debug(`[ResponseModal] Starting tool loop ${toolLoopCount}/${MAX_TOOL_LOOPS} with ${toolCalls?.length || 0} tool calls`);
|
|
2225
|
-
|
|
2226
|
-
// Create tool result history items
|
|
2227
|
-
const toolResultHistoryItems: HistoryItem[] = [];
|
|
2228
|
-
for (const toolCall of toolCalls || []) {
|
|
2229
|
-
const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
|
|
2230
|
-
if (tool) {
|
|
2231
|
-
// Create HistoryItem format for tool results
|
|
2232
|
-
// Add assistant message with tool_calls
|
|
2233
|
-
toolResultHistoryItems.push({
|
|
2234
|
-
role: "assistant" as const,
|
|
2235
|
-
content: null,
|
|
2236
|
-
tool_calls: [{
|
|
2237
|
-
id: toolCall.toolName,
|
|
2238
|
-
name: toolCall.toolName,
|
|
2239
|
-
arguments: toolCall.arguments,
|
|
2240
|
-
}],
|
|
2241
|
-
});
|
|
2242
|
-
// Add tool result
|
|
2243
|
-
toolResultHistoryItems.push({
|
|
2244
|
-
role: "tool" as const,
|
|
2245
|
-
tool_call_id: toolCall.toolName,
|
|
2246
|
-
name: toolCall.toolName,
|
|
2247
|
-
content: toolResultsMap.get(toolCall.toolName) || "Tool executed successfully",
|
|
2248
|
-
});
|
|
2249
|
-
}
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
|
-
// Create updated history with tool results
|
|
2253
|
-
const updatedHistory = [...history, ...toolResultHistoryItems];
|
|
2254
|
-
|
|
2255
|
-
// Make follow-up AI call to see if more tools are needed
|
|
2256
|
-
// After first iteration, don't provide tools to force a text response
|
|
2257
|
-
const agentOptions = this.agent.getAgentOptions();
|
|
2258
|
-
const shouldProvideTools = toolLoopCount === 1;
|
|
2259
|
-
|
|
2260
|
-
logger.debug(`[ResponseModal] Making follow-up AI call (loop ${toolLoopCount}):`, {
|
|
2261
|
-
providingTools: shouldProvideTools,
|
|
2262
|
-
toolsCount: shouldProvideTools ? availableTools.length : 0,
|
|
2263
|
-
addingTextInstruction: toolLoopCount > 1,
|
|
2264
|
-
});
|
|
2265
|
-
|
|
2266
|
-
const followUpResult = await agentOptions.provider.generateMessage({
|
|
2267
|
-
prompt: responsePrompt + (toolLoopCount > 1 ? "\n\nProvide a text response to the user based on the tool results." : ""),
|
|
2268
|
-
history: updatedHistory, // Use HistoryItem[] for AI provider
|
|
2269
|
-
context,
|
|
2270
|
-
tools: shouldProvideTools ? availableTools : [], // Only provide tools on first iteration
|
|
2271
|
-
parameters: responseSchema ? {
|
|
2272
|
-
jsonSchema: responseSchema,
|
|
2273
|
-
schemaName: "tool_followup",
|
|
2274
|
-
} : undefined,
|
|
2275
|
-
signal,
|
|
2276
|
-
});
|
|
2277
|
-
|
|
2278
|
-
// Check if follow-up call has more tool calls
|
|
2279
|
-
const followUpToolCalls = followUpResult.structured?.toolCalls;
|
|
2280
|
-
hasToolCalls = followUpToolCalls && followUpToolCalls.length > 0;
|
|
2281
|
-
|
|
2282
|
-
logger.debug(`[ResponseModal] Follow-up AI response (loop ${toolLoopCount}):`, {
|
|
2283
|
-
hasMessage: !!followUpResult.message,
|
|
2284
|
-
messageLength: followUpResult.message?.length || 0,
|
|
2285
|
-
hasToolCalls,
|
|
2286
|
-
toolCallsCount: followUpToolCalls?.length || 0,
|
|
2287
|
-
toolNames: followUpToolCalls?.map(tc => tc.toolName) || [],
|
|
2288
|
-
});
|
|
2289
|
-
|
|
2290
|
-
if (hasToolCalls) {
|
|
2291
|
-
logger.debug(`[ResponseModal] Follow-up call produced ${followUpToolCalls!.length} additional tool calls`);
|
|
2292
|
-
|
|
2293
|
-
// Execute the follow-up tool calls
|
|
2294
|
-
for (const toolCall of followUpToolCalls!) {
|
|
2295
|
-
const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
|
|
2296
|
-
if (!tool) {
|
|
2297
|
-
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.`);
|
|
2298
|
-
continue;
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
|
-
try {
|
|
2302
|
-
// Use ToolManager for unified tool execution
|
|
2303
|
-
const toolManager = this.getToolManager();
|
|
2304
|
-
const toolResult = await toolManager.executeTool({
|
|
2305
|
-
tool: tool,
|
|
2306
|
-
context,
|
|
2307
|
-
updateContext: this.agent.updateContext.bind(this.agent),
|
|
2308
|
-
updateData: this.agent.updateCollectedData.bind(this.agent),
|
|
2309
|
-
history: historyToEvents(updatedHistory), // Convert to Event[] for tool execution
|
|
2310
|
-
data: session.data,
|
|
2311
|
-
toolArguments: toolCall.arguments,
|
|
2312
|
-
});
|
|
2313
|
-
|
|
2314
|
-
// Check if tool execution was successful
|
|
2315
|
-
if (!toolResult.success) {
|
|
2316
|
-
logger.error(`[ResponseModal] Follow-up tool execution failed: ${toolCall.toolName} - ${toolResult.error}`);
|
|
2317
|
-
continue;
|
|
2318
|
-
}
|
|
2319
|
-
|
|
2320
|
-
// Update context with follow-up tool results
|
|
2321
|
-
if (toolResult.contextUpdate) {
|
|
2322
|
-
try {
|
|
2323
|
-
await this.agent.updateContext(toolResult.contextUpdate as Partial<TContext>);
|
|
2324
|
-
} catch (error) {
|
|
2325
|
-
logger.error(`[ResponseModal] Failed to update context from follow-up tool ${toolCall.toolName}:`, error);
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
|
-
if (toolResult.dataUpdate) {
|
|
2330
|
-
try {
|
|
2331
|
-
const updateDataMethod = this.agent.getUpdateDataMethod();
|
|
2332
|
-
session = await updateDataMethod(session, toolResult.dataUpdate as Partial<TData>);
|
|
2333
|
-
logger.debug(`[ResponseModal] Follow-up tool updated collected data:`, toolResult.dataUpdate);
|
|
2334
|
-
} catch (error) {
|
|
2335
|
-
logger.error(`[ResponseModal] Failed to update data from follow-up tool ${toolCall.toolName}:`, error);
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
|
|
2339
|
-
// Store the follow-up tool result for potential next loop iteration
|
|
2340
|
-
toolResultsMap.set(toolCall.toolName, serializeToolResult(toolResult));
|
|
2341
|
-
toolArgsMap.set(toolCall.toolName, toolCall.arguments);
|
|
2342
|
-
|
|
2343
|
-
logger.debug(`[ResponseModal] Executed follow-up tool: ${toolCall.toolName} (success: ${toolResult.success})`);
|
|
2344
|
-
} catch (error) {
|
|
2345
|
-
logger.error(`[ResponseModal] Follow-up tool execution error for ${toolCall.toolName}:`, error);
|
|
2346
|
-
continue;
|
|
2347
|
-
}
|
|
2348
|
-
}
|
|
2349
|
-
|
|
2350
|
-
// Update toolCalls for next iteration or final response
|
|
2351
|
-
toolCalls = followUpToolCalls;
|
|
2352
|
-
} else {
|
|
2353
|
-
logger.debug(`[ResponseModal] Tool loop completed after ${toolLoopCount} iterations`);
|
|
2354
|
-
// Update final message and toolCalls from follow-up result if no more tools
|
|
2355
|
-
finalMessage = followUpResult.structured?.message || followUpResult.message;
|
|
2356
|
-
followUpStructured = followUpResult.structured;
|
|
2357
|
-
toolCalls = followUpToolCalls || [];
|
|
2358
|
-
break;
|
|
2359
|
-
}
|
|
2360
|
-
}
|
|
2361
|
-
|
|
2362
|
-
if (toolLoopCount >= MAX_TOOL_LOOPS) {
|
|
2363
|
-
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.`);
|
|
2364
|
-
}
|
|
2365
|
-
|
|
2366
|
-
// If tools were executed but no final text message was produced,
|
|
2367
|
-
// make one more LLM call to generate a proper text response from tool results.
|
|
2368
|
-
// This prevents the original tool-invocation message (e.g. "Let me check...")
|
|
2369
|
-
// from being returned as the final user-facing response.
|
|
2370
|
-
if (!finalMessage && toolLoopCount > 0) {
|
|
2371
|
-
logger.debug(`[ResponseModal] No final message after tool loop, making additional LLM call for text response`);
|
|
2372
|
-
|
|
2373
|
-
// Build tool result history from toolResultsMap which contains ALL
|
|
2374
|
-
// tool executions (initial + follow-up). We can't use `toolCalls` here
|
|
2375
|
-
// because it was reassigned to the (empty) follow-up tool calls when
|
|
2376
|
-
// the while loop broke out.
|
|
2377
|
-
const finalToolResultHistoryItems: HistoryItem[] = [];
|
|
2378
|
-
for (const [toolName, toolResult] of toolResultsMap) {
|
|
2379
|
-
finalToolResultHistoryItems.push({
|
|
2380
|
-
role: "assistant" as const,
|
|
2381
|
-
content: null,
|
|
2382
|
-
tool_calls: [{
|
|
2383
|
-
id: toolName,
|
|
2384
|
-
name: toolName,
|
|
2385
|
-
arguments: toolArgsMap.get(toolName) || {},
|
|
2386
|
-
}],
|
|
2387
|
-
});
|
|
2388
|
-
finalToolResultHistoryItems.push({
|
|
2389
|
-
role: "tool" as const,
|
|
2390
|
-
tool_call_id: toolName,
|
|
2391
|
-
name: toolName,
|
|
2392
|
-
content: toolResult,
|
|
2393
|
-
});
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
const finalHistory = [...history, ...finalToolResultHistoryItems];
|
|
2397
|
-
const agentOptions = this.agent.getAgentOptions();
|
|
2398
|
-
|
|
2399
|
-
try {
|
|
2400
|
-
const textResult = await agentOptions.provider.generateMessage({
|
|
2401
|
-
prompt: responsePrompt + "\n\nProvide a text response to the user based on the tool results. Do not call any tools.",
|
|
2402
|
-
history: finalHistory,
|
|
2403
|
-
context,
|
|
2404
|
-
tools: [], // No tools - force text response
|
|
2405
|
-
parameters: responseSchema ? {
|
|
2406
|
-
jsonSchema: responseSchema,
|
|
2407
|
-
schemaName: "tool_final_text",
|
|
2408
|
-
} : undefined,
|
|
2409
|
-
signal,
|
|
2410
|
-
});
|
|
2411
|
-
|
|
2412
|
-
finalMessage = textResult.structured?.message || textResult.message;
|
|
2413
|
-
if (textResult.structured) {
|
|
2414
|
-
followUpStructured = textResult.structured;
|
|
2415
|
-
}
|
|
2416
|
-
|
|
2417
|
-
logger.debug(`[ResponseModal] Generated final text response after tool loop:`, {
|
|
2418
|
-
hasMessage: !!finalMessage,
|
|
2419
|
-
messageLength: finalMessage?.length || 0,
|
|
2420
|
-
});
|
|
2421
|
-
} catch (error) {
|
|
2422
|
-
logger.error(`[ResponseModal] Failed to generate final text response after tool loop:`, error);
|
|
2423
|
-
// finalMessage remains undefined; caller will use original message as fallback
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
|
|
2427
|
-
logger.debug(`[ResponseModal] Tool loop completed:`, {
|
|
2428
|
-
totalIterations: toolLoopCount,
|
|
2429
|
-
hasFinalMessage: !!finalMessage,
|
|
2430
|
-
finalMessageLength: finalMessage?.length || 0,
|
|
2431
|
-
finalToolCallsCount: toolCalls?.length || 0,
|
|
2432
|
-
});
|
|
2433
|
-
|
|
2434
|
-
return {
|
|
2435
|
-
session,
|
|
2436
|
-
finalToolCalls: toolCalls,
|
|
2437
|
-
finalMessage,
|
|
2438
|
-
structured: followUpStructured,
|
|
2439
|
-
};
|
|
2440
|
-
} catch (error) {
|
|
2441
|
-
throw ResponseGenerationError.fromError(error, 'tool_execution', params, {
|
|
2442
|
-
toolCallsCount: params.toolCalls?.length || 0,
|
|
2443
|
-
availableToolsCount: params.availableTools.length
|
|
2444
|
-
});
|
|
2445
|
-
}
|
|
2446
|
-
} /**
|
|
2447
1307
|
* Unified data collection from AI response
|
|
2448
1308
|
* @private
|
|
2449
1309
|
*/
|
|
@@ -2624,10 +1484,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
2624
1484
|
return nextSession;
|
|
2625
1485
|
}
|
|
2626
1486
|
|
|
2627
|
-
/**
|
|
2628
|
-
* Stream flow completion response
|
|
2629
|
-
* @private
|
|
2630
|
-
*/
|
|
2631
1487
|
/**
|
|
2632
1488
|
* Stream a flow completion as a single terminal chunk.
|
|
2633
1489
|
*
|
|
@@ -2659,8 +1515,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
2659
1515
|
history,
|
|
2660
1516
|
});
|
|
2661
1517
|
|
|
2662
|
-
await this.finalizeSession(session, context);
|
|
2663
|
-
|
|
2664
1518
|
yield {
|
|
2665
1519
|
delta: '',
|
|
2666
1520
|
accumulated: '',
|
|
@@ -2754,11 +1608,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
2754
1608
|
});
|
|
2755
1609
|
|
|
2756
1610
|
for await (const chunk of stream) {
|
|
2757
|
-
// Update current session if we have one
|
|
2758
|
-
if (chunk.done) {
|
|
2759
|
-
await this.finalizeSession(session, context);
|
|
2760
|
-
}
|
|
2761
|
-
|
|
2762
1611
|
// Response structure completeness (Requirement 8.1, 8.2, 8.3)
|
|
2763
1612
|
// - executedSteps: empty for fallback (no flow/step execution)
|
|
2764
1613
|
// - stoppedReason: undefined for fallback (no flow context)
|
|
@@ -2779,49 +1628,11 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
2779
1628
|
}
|
|
2780
1629
|
}
|
|
2781
1630
|
|
|
2782
|
-
/**
|
|
2783
|
-
* Handle session persistence and finalization
|
|
2784
|
-
* @private
|
|
2785
|
-
*/
|
|
2786
|
-
private async finalizeSession(session: SessionState<TData>, context: TContext): Promise<void> {
|
|
2787
|
-
// Auto-save session step to persistence if configured
|
|
2788
|
-
const persistenceManager = this.agent.getPersistenceManager();
|
|
2789
|
-
const agentOptions = this.agent.getAgentOptions();
|
|
2790
|
-
if (
|
|
2791
|
-
persistenceManager &&
|
|
2792
|
-
session.id &&
|
|
2793
|
-
(this.options?.enableAutoSave !== false && agentOptions.persistence?.autoSave !== false)
|
|
2794
|
-
) {
|
|
2795
|
-
await persistenceManager.saveSessionState(session.id, session);
|
|
2796
|
-
logger.debug(`[ResponseModal] Auto-saved session step to persistence: ${session.id}`);
|
|
2797
|
-
}
|
|
2798
|
-
|
|
2799
|
-
// Execute finalize function
|
|
2800
|
-
await this.executeStepFinalize(session, context);
|
|
2801
|
-
|
|
2802
|
-
// Update current session if we have one
|
|
2803
|
-
const currentSession = this.agent.currentSession;
|
|
2804
|
-
if (currentSession) {
|
|
2805
|
-
this.agent.currentSession = session;
|
|
2806
|
-
}
|
|
2807
|
-
}
|
|
2808
1631
|
// ============================================================================
|
|
2809
1632
|
// UTILITY METHODS - Helper methods for tool management and other utilities
|
|
2810
1633
|
// ============================================================================
|
|
2811
1634
|
|
|
2812
1635
|
|
|
2813
|
-
/**
|
|
2814
|
-
* Find an available tool by name for the given flow.
|
|
2815
|
-
* Delegates to ToolManager for unified tool resolution.
|
|
2816
|
-
* @private
|
|
2817
|
-
*/
|
|
2818
|
-
private findAvailableTool(
|
|
2819
|
-
toolName: string,
|
|
2820
|
-
flow?: Flow<TContext, TData>
|
|
2821
|
-
): Tool<TContext, TData> | undefined {
|
|
2822
|
-
return this.getToolManager().find(toolName, undefined, undefined, flow);
|
|
2823
|
-
}
|
|
2824
|
-
|
|
2825
1636
|
/**
|
|
2826
1637
|
* Collect all available tools for the given flow and step context.
|
|
2827
1638
|
* Delegates to ToolManager for unified tool resolution and deduplication.
|
|
@@ -2845,64 +1656,4 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
2845
1656
|
}));
|
|
2846
1657
|
}
|
|
2847
1658
|
|
|
2848
|
-
/**
|
|
2849
|
-
* Execute a prepare or finalize function/tool
|
|
2850
|
-
* @private
|
|
2851
|
-
*/
|
|
2852
|
-
private async executePrepareFinalize(
|
|
2853
|
-
prepareOrFinalize:
|
|
2854
|
-
| string
|
|
2855
|
-
| Tool<TContext, TData>
|
|
2856
|
-
| ((context: TContext, data?: Partial<TData>) => void | PrepareResult | Promise<void | PrepareResult>)
|
|
2857
|
-
| undefined,
|
|
2858
|
-
context: TContext,
|
|
2859
|
-
data?: Partial<TData>,
|
|
2860
|
-
flow?: Flow<TContext, TData>,
|
|
2861
|
-
step?: Step<TContext, TData>
|
|
2862
|
-
): Promise<void> {
|
|
2863
|
-
if (!prepareOrFinalize) return;
|
|
2864
|
-
|
|
2865
|
-
if (typeof prepareOrFinalize === "function") {
|
|
2866
|
-
// It's a function - call it directly
|
|
2867
|
-
await prepareOrFinalize(context, data);
|
|
2868
|
-
} else {
|
|
2869
|
-
// It's a tool reference - find and execute the tool
|
|
2870
|
-
let tool: Tool<TContext, TData> | undefined;
|
|
2871
|
-
|
|
2872
|
-
if (typeof prepareOrFinalize === "string") {
|
|
2873
|
-
// Tool ID - use ToolManager for unified resolution
|
|
2874
|
-
tool = this.getToolManager().find(prepareOrFinalize, undefined, step, flow);
|
|
2875
|
-
} else {
|
|
2876
|
-
// Tool object - use directly
|
|
2877
|
-
tool = prepareOrFinalize;
|
|
2878
|
-
}
|
|
2879
|
-
|
|
2880
|
-
if (tool) {
|
|
2881
|
-
// Use ToolManager for unified tool execution
|
|
2882
|
-
const result = await this.getToolManager().executeTool({
|
|
2883
|
-
tool,
|
|
2884
|
-
context,
|
|
2885
|
-
updateContext: this.agent.updateContext.bind(this.agent),
|
|
2886
|
-
updateData: this.agent.updateCollectedData.bind(this.agent),
|
|
2887
|
-
history: [], // Empty history for prepare/finalize
|
|
2888
|
-
data,
|
|
2889
|
-
});
|
|
2890
|
-
|
|
2891
|
-
if (!result.success) {
|
|
2892
|
-
logger.error(
|
|
2893
|
-
`[ResponseModal] Tool execution failed in prepare/finalize: ${result.error}`
|
|
2894
|
-
);
|
|
2895
|
-
throw new Error(`Tool execution failed: ${result.error}`);
|
|
2896
|
-
}
|
|
2897
|
-
} else {
|
|
2898
|
-
logger.warn(
|
|
2899
|
-
`[ResponseModal] Tool not found for prepare/finalize: ${typeof prepareOrFinalize === "string"
|
|
2900
|
-
? prepareOrFinalize
|
|
2901
|
-
: "inline tool"
|
|
2902
|
-
}`
|
|
2903
|
-
);
|
|
2904
|
-
}
|
|
2905
|
-
}
|
|
2906
|
-
}
|
|
2907
|
-
|
|
2908
1659
|
}
|