@ariaflowagents/core 0.7.1 → 0.9.0
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 +90 -1
- package/dist/agents/Agent.d.ts +188 -9
- package/dist/agents/Agent.d.ts.map +1 -1
- package/dist/agents/Agent.js +246 -24
- package/dist/agents/Agent.js.map +1 -1
- package/dist/agents/CompositeAgent.d.ts +4 -3
- package/dist/agents/CompositeAgent.d.ts.map +1 -1
- package/dist/agents/CompositeAgent.js +19 -9
- package/dist/agents/CompositeAgent.js.map +1 -1
- package/dist/agents/FlowAgent.d.ts +3 -2
- package/dist/agents/FlowAgent.d.ts.map +1 -1
- package/dist/agents/FlowAgent.js +16 -6
- package/dist/agents/FlowAgent.js.map +1 -1
- package/dist/agents/TriageAgent.d.ts +8 -2
- package/dist/agents/TriageAgent.d.ts.map +1 -1
- package/dist/agents/TriageAgent.js +39 -6
- package/dist/agents/TriageAgent.js.map +1 -1
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +0 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/capabilities/AutoRetrieveCapability.d.ts +30 -0
- package/dist/capabilities/AutoRetrieveCapability.d.ts.map +1 -0
- package/dist/capabilities/AutoRetrieveCapability.js +36 -0
- package/dist/capabilities/AutoRetrieveCapability.js.map +1 -0
- package/dist/capabilities/ExtractionCapability.d.ts +25 -0
- package/dist/capabilities/ExtractionCapability.d.ts.map +1 -0
- package/dist/capabilities/ExtractionCapability.js +74 -0
- package/dist/capabilities/ExtractionCapability.js.map +1 -0
- package/dist/capabilities/FlowCapability.d.ts +81 -0
- package/dist/capabilities/FlowCapability.d.ts.map +1 -0
- package/dist/capabilities/FlowCapability.js +482 -0
- package/dist/capabilities/FlowCapability.js.map +1 -0
- package/dist/capabilities/GuardrailCapability.d.ts +30 -0
- package/dist/capabilities/GuardrailCapability.d.ts.map +1 -0
- package/dist/capabilities/GuardrailCapability.js +38 -0
- package/dist/capabilities/GuardrailCapability.js.map +1 -0
- package/dist/capabilities/HandoffCapability.d.ts +19 -0
- package/dist/capabilities/HandoffCapability.d.ts.map +1 -0
- package/dist/capabilities/HandoffCapability.js +58 -0
- package/dist/capabilities/HandoffCapability.js.map +1 -0
- package/dist/capabilities/LivePromptAssembler.d.ts +108 -0
- package/dist/capabilities/LivePromptAssembler.d.ts.map +1 -0
- package/dist/capabilities/LivePromptAssembler.js +157 -0
- package/dist/capabilities/LivePromptAssembler.js.map +1 -0
- package/dist/capabilities/TriageCapability.d.ts +16 -0
- package/dist/capabilities/TriageCapability.d.ts.map +1 -0
- package/dist/capabilities/TriageCapability.js +61 -0
- package/dist/capabilities/TriageCapability.js.map +1 -0
- package/dist/capabilities/adapters/ai-sdk.d.ts +14 -0
- package/dist/capabilities/adapters/ai-sdk.d.ts.map +1 -0
- package/dist/capabilities/adapters/ai-sdk.js +29 -0
- package/dist/capabilities/adapters/ai-sdk.js.map +1 -0
- package/dist/capabilities/adapters/gemini.d.ts +15 -0
- package/dist/capabilities/adapters/gemini.d.ts.map +1 -0
- package/dist/capabilities/adapters/gemini.js +40 -0
- package/dist/capabilities/adapters/gemini.js.map +1 -0
- package/dist/capabilities/index.d.ts +154 -0
- package/dist/capabilities/index.d.ts.map +1 -0
- package/dist/capabilities/index.js +128 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/eval/EvalRunner.d.ts +12 -0
- package/dist/eval/EvalRunner.d.ts.map +1 -0
- package/dist/eval/EvalRunner.js +64 -0
- package/dist/eval/EvalRunner.js.map +1 -0
- package/dist/eval/scoring.d.ts +15 -0
- package/dist/eval/scoring.d.ts.map +1 -0
- package/dist/eval/scoring.js +152 -0
- package/dist/eval/scoring.js.map +1 -0
- package/dist/eval/types.d.ts +59 -0
- package/dist/eval/types.d.ts.map +1 -0
- package/dist/eval/types.js +2 -0
- package/dist/eval/types.js.map +1 -0
- package/dist/flows/FlowGraph.d.ts +3 -1
- package/dist/flows/FlowGraph.d.ts.map +1 -1
- package/dist/flows/FlowGraph.js +5 -0
- package/dist/flows/FlowGraph.js.map +1 -1
- package/dist/flows/FlowManager.d.ts +68 -1
- package/dist/flows/FlowManager.d.ts.map +1 -1
- package/dist/flows/FlowManager.js +499 -36
- package/dist/flows/FlowManager.js.map +1 -1
- package/dist/flows/extraction.d.ts +16 -1
- package/dist/flows/extraction.d.ts.map +1 -1
- package/dist/flows/extraction.js +34 -0
- package/dist/flows/extraction.js.map +1 -1
- package/dist/flows/index.d.ts +2 -0
- package/dist/flows/index.d.ts.map +1 -1
- package/dist/flows/index.js +1 -0
- package/dist/flows/index.js.map +1 -1
- package/dist/flows/validation.d.ts +1 -1
- package/dist/flows/validation.d.ts.map +1 -1
- package/dist/flows/validation.js +13 -1
- package/dist/flows/validation.js.map +1 -1
- package/dist/foundation/AgentDefinition.d.ts +18 -0
- package/dist/foundation/AgentDefinition.d.ts.map +1 -0
- package/dist/foundation/AgentDefinition.js +2 -0
- package/dist/foundation/AgentDefinition.js.map +1 -0
- package/dist/foundation/AgentStateController.d.ts +26 -0
- package/dist/foundation/AgentStateController.d.ts.map +1 -0
- package/dist/foundation/AgentStateController.js +2 -0
- package/dist/foundation/AgentStateController.js.map +1 -0
- package/dist/foundation/ConversationEventLog.d.ts +72 -0
- package/dist/foundation/ConversationEventLog.d.ts.map +1 -0
- package/dist/foundation/ConversationEventLog.js +2 -0
- package/dist/foundation/ConversationEventLog.js.map +1 -0
- package/dist/foundation/ConversationState.d.ts +31 -0
- package/dist/foundation/ConversationState.d.ts.map +1 -0
- package/dist/foundation/ConversationState.js +2 -0
- package/dist/foundation/ConversationState.js.map +1 -0
- package/dist/foundation/DefaultAgentStateController.d.ts +24 -0
- package/dist/foundation/DefaultAgentStateController.d.ts.map +1 -0
- package/dist/foundation/DefaultAgentStateController.js +49 -0
- package/dist/foundation/DefaultAgentStateController.js.map +1 -0
- package/dist/foundation/DefaultConversationEventLog.d.ts +28 -0
- package/dist/foundation/DefaultConversationEventLog.d.ts.map +1 -0
- package/dist/foundation/DefaultConversationEventLog.js +195 -0
- package/dist/foundation/DefaultConversationEventLog.js.map +1 -0
- package/dist/foundation/DefaultConversationState.d.ts +34 -0
- package/dist/foundation/DefaultConversationState.d.ts.map +1 -0
- package/dist/foundation/DefaultConversationState.js +100 -0
- package/dist/foundation/DefaultConversationState.js.map +1 -0
- package/dist/foundation/DefaultToolExecutor.d.ts +58 -0
- package/dist/foundation/DefaultToolExecutor.d.ts.map +1 -0
- package/dist/foundation/DefaultToolExecutor.js +128 -0
- package/dist/foundation/DefaultToolExecutor.js.map +1 -0
- package/dist/foundation/ToolExecutor.d.ts +44 -0
- package/dist/foundation/ToolExecutor.d.ts.map +1 -0
- package/dist/foundation/ToolExecutor.js +2 -0
- package/dist/foundation/ToolExecutor.js.map +1 -0
- package/dist/foundation/createFoundation.d.ts +33 -0
- package/dist/foundation/createFoundation.d.ts.map +1 -0
- package/dist/foundation/createFoundation.js +34 -0
- package/dist/foundation/createFoundation.js.map +1 -0
- package/dist/foundation/index.d.ts +15 -0
- package/dist/foundation/index.d.ts.map +1 -0
- package/dist/foundation/index.js +8 -0
- package/dist/foundation/index.js.map +1 -0
- package/dist/hooks/HookRunner.d.ts +5 -1
- package/dist/hooks/HookRunner.d.ts.map +1 -1
- package/dist/hooks/HookRunner.js +7 -0
- package/dist/hooks/HookRunner.js.map +1 -1
- package/dist/hooks/builtin/metrics.d.ts.map +1 -1
- package/dist/hooks/builtin/metrics.js +12 -0
- package/dist/hooks/builtin/metrics.js.map +1 -1
- package/dist/hooks/builtin/observability.d.ts +21 -0
- package/dist/hooks/builtin/observability.d.ts.map +1 -0
- package/dist/hooks/builtin/observability.js +535 -0
- package/dist/hooks/builtin/observability.js.map +1 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/dist/memory/MemoryService.d.ts +40 -0
- package/dist/memory/MemoryService.d.ts.map +1 -0
- package/dist/memory/MemoryService.js +2 -0
- package/dist/memory/MemoryService.js.map +1 -0
- package/dist/memory/index.d.ts +5 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +3 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/preloadMemory.d.ts +17 -0
- package/dist/memory/preloadMemory.d.ts.map +1 -0
- package/dist/memory/preloadMemory.js +62 -0
- package/dist/memory/preloadMemory.js.map +1 -0
- package/dist/memory/stores/InMemoryMemoryService.d.ts +20 -0
- package/dist/memory/stores/InMemoryMemoryService.d.ts.map +1 -0
- package/dist/memory/stores/InMemoryMemoryService.js +92 -0
- package/dist/memory/stores/InMemoryMemoryService.js.map +1 -0
- package/dist/memory/types.d.ts +49 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +8 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/orchestration/DefaultOrchestrationAuthority.d.ts +91 -0
- package/dist/orchestration/DefaultOrchestrationAuthority.d.ts.map +1 -0
- package/dist/orchestration/DefaultOrchestrationAuthority.js +786 -0
- package/dist/orchestration/DefaultOrchestrationAuthority.js.map +1 -0
- package/dist/orchestration/OrchestrationAuthority.d.ts +119 -0
- package/dist/orchestration/OrchestrationAuthority.d.ts.map +1 -0
- package/dist/orchestration/OrchestrationAuthority.js +2 -0
- package/dist/orchestration/OrchestrationAuthority.js.map +1 -0
- package/dist/orchestration/RealtimeExtractionRunner.d.ts +25 -0
- package/dist/orchestration/RealtimeExtractionRunner.d.ts.map +1 -0
- package/dist/orchestration/RealtimeExtractionRunner.js +62 -0
- package/dist/orchestration/RealtimeExtractionRunner.js.map +1 -0
- package/dist/orchestration/index.d.ts +5 -0
- package/dist/orchestration/index.d.ts.map +1 -0
- package/dist/orchestration/index.js +4 -0
- package/dist/orchestration/index.js.map +1 -0
- package/dist/orchestration/types.d.ts +134 -0
- package/dist/orchestration/types.d.ts.map +1 -0
- package/dist/orchestration/types.js +2 -0
- package/dist/orchestration/types.js.map +1 -0
- package/dist/prompts/AgentPrompt.d.ts +110 -0
- package/dist/prompts/AgentPrompt.d.ts.map +1 -0
- package/dist/prompts/AgentPrompt.js +373 -0
- package/dist/prompts/AgentPrompt.js.map +1 -0
- package/dist/prompts/PromptAssembly.d.ts +119 -0
- package/dist/prompts/PromptAssembly.d.ts.map +1 -0
- package/dist/prompts/PromptAssembly.js +150 -0
- package/dist/prompts/PromptAssembly.js.map +1 -0
- package/dist/prompts/PromptBuilder.d.ts +22 -3
- package/dist/prompts/PromptBuilder.d.ts.map +1 -1
- package/dist/prompts/PromptBuilder.js +242 -13
- package/dist/prompts/PromptBuilder.js.map +1 -1
- package/dist/prompts/PromptRenderer.d.ts +43 -0
- package/dist/prompts/PromptRenderer.d.ts.map +1 -0
- package/dist/prompts/PromptRenderer.js +114 -0
- package/dist/prompts/PromptRenderer.js.map +1 -0
- package/dist/prompts/brandVoice.d.ts +10 -0
- package/dist/prompts/brandVoice.d.ts.map +1 -0
- package/dist/prompts/brandVoice.js +87 -0
- package/dist/prompts/brandVoice.js.map +1 -0
- package/dist/prompts/index.d.ts +11 -4
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +7 -2
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/security.d.ts +5 -0
- package/dist/prompts/security.d.ts.map +1 -0
- package/dist/prompts/security.js +52 -0
- package/dist/prompts/security.js.map +1 -0
- package/dist/prompts/types.d.ts +65 -1
- package/dist/prompts/types.d.ts.map +1 -1
- package/dist/prompts/types.js +26 -0
- package/dist/prompts/types.js.map +1 -1
- package/dist/realtime/RealtimeAudioClient.d.ts +105 -0
- package/dist/realtime/RealtimeAudioClient.d.ts.map +1 -0
- package/dist/realtime/RealtimeAudioClient.js +15 -0
- package/dist/realtime/RealtimeAudioClient.js.map +1 -0
- package/dist/realtime/RealtimeRuntime.d.ts +136 -0
- package/dist/realtime/RealtimeRuntime.d.ts.map +1 -0
- package/dist/realtime/RealtimeRuntime.js +270 -0
- package/dist/realtime/RealtimeRuntime.js.map +1 -0
- package/dist/realtime/index.d.ts +4 -0
- package/dist/realtime/index.d.ts.map +1 -0
- package/dist/realtime/index.js +2 -0
- package/dist/realtime/index.js.map +1 -0
- package/dist/runtime/ContextBudget.d.ts +57 -0
- package/dist/runtime/ContextBudget.d.ts.map +1 -0
- package/dist/runtime/ContextBudget.js +103 -0
- package/dist/runtime/ContextBudget.js.map +1 -0
- package/dist/runtime/ContextManager.d.ts +8 -5
- package/dist/runtime/ContextManager.d.ts.map +1 -1
- package/dist/runtime/ContextManager.js +47 -14
- package/dist/runtime/ContextManager.js.map +1 -1
- package/dist/runtime/ExtractionEngine.d.ts +2 -1
- package/dist/runtime/ExtractionEngine.d.ts.map +1 -1
- package/dist/runtime/ExtractionEngine.js +11 -0
- package/dist/runtime/ExtractionEngine.js.map +1 -1
- package/dist/runtime/FlowExecutor.d.ts +22 -15
- package/dist/runtime/FlowExecutor.d.ts.map +1 -1
- package/dist/runtime/FlowExecutor.js +102 -149
- package/dist/runtime/FlowExecutor.js.map +1 -1
- package/dist/runtime/Runtime.d.ts +53 -78
- package/dist/runtime/Runtime.d.ts.map +1 -1
- package/dist/runtime/Runtime.js +272 -1406
- package/dist/runtime/Runtime.js.map +1 -1
- package/dist/runtime/SessionCache.d.ts +16 -0
- package/dist/runtime/SessionCache.d.ts.map +1 -0
- package/dist/runtime/SessionCache.js +49 -0
- package/dist/runtime/SessionCache.js.map +1 -0
- package/dist/runtime/SessionMutex.d.ts +37 -0
- package/dist/runtime/SessionMutex.d.ts.map +1 -0
- package/dist/runtime/SessionMutex.js +59 -0
- package/dist/runtime/SessionMutex.js.map +1 -0
- package/dist/runtime/StreamEmitter.d.ts +34 -0
- package/dist/runtime/StreamEmitter.d.ts.map +1 -0
- package/dist/runtime/StreamEmitter.js +91 -0
- package/dist/runtime/StreamEmitter.js.map +1 -0
- package/dist/runtime/handoffFilters.d.ts +60 -0
- package/dist/runtime/handoffFilters.d.ts.map +1 -0
- package/dist/runtime/handoffFilters.js +95 -0
- package/dist/runtime/handoffFilters.js.map +1 -0
- package/dist/runtime/pipeline/AgentExecuteStage.d.ts +22 -0
- package/dist/runtime/pipeline/AgentExecuteStage.d.ts.map +1 -0
- package/dist/runtime/pipeline/AgentExecuteStage.js +958 -0
- package/dist/runtime/pipeline/AgentExecuteStage.js.map +1 -0
- package/dist/runtime/pipeline/ContextAssembleStage.d.ts +26 -0
- package/dist/runtime/pipeline/ContextAssembleStage.d.ts.map +1 -0
- package/dist/runtime/pipeline/ContextAssembleStage.js +253 -0
- package/dist/runtime/pipeline/ContextAssembleStage.js.map +1 -0
- package/dist/runtime/pipeline/ContextGatherStage.d.ts +21 -0
- package/dist/runtime/pipeline/ContextGatherStage.d.ts.map +1 -0
- package/dist/runtime/pipeline/ContextGatherStage.js +161 -0
- package/dist/runtime/pipeline/ContextGatherStage.js.map +1 -0
- package/dist/runtime/pipeline/IntakeStage.d.ts +25 -0
- package/dist/runtime/pipeline/IntakeStage.d.ts.map +1 -0
- package/dist/runtime/pipeline/IntakeStage.js +126 -0
- package/dist/runtime/pipeline/IntakeStage.js.map +1 -0
- package/dist/runtime/pipeline/PostStreamStage.d.ts +26 -0
- package/dist/runtime/pipeline/PostStreamStage.d.ts.map +1 -0
- package/dist/runtime/pipeline/PostStreamStage.js +129 -0
- package/dist/runtime/pipeline/PostStreamStage.js.map +1 -0
- package/dist/runtime/pipeline/TurnPipeline.d.ts +54 -0
- package/dist/runtime/pipeline/TurnPipeline.d.ts.map +1 -0
- package/dist/runtime/pipeline/TurnPipeline.js +15 -0
- package/dist/runtime/pipeline/TurnPipeline.js.map +1 -0
- package/dist/runtime/pipeline/TurnServices.d.ts +48 -0
- package/dist/runtime/pipeline/TurnServices.d.ts.map +1 -0
- package/dist/runtime/pipeline/TurnServices.js +2 -0
- package/dist/runtime/pipeline/TurnServices.js.map +1 -0
- package/dist/runtime/pipeline/agentTypeGuards.d.ts +4 -0
- package/dist/runtime/pipeline/agentTypeGuards.d.ts.map +1 -0
- package/dist/runtime/pipeline/agentTypeGuards.js +7 -0
- package/dist/runtime/pipeline/agentTypeGuards.js.map +1 -0
- package/dist/runtime/pipeline/index.d.ts +11 -0
- package/dist/runtime/pipeline/index.d.ts.map +1 -0
- package/dist/runtime/pipeline/index.js +13 -0
- package/dist/runtime/pipeline/index.js.map +1 -0
- package/dist/runtime/pipeline/outputProcessing.d.ts +23 -0
- package/dist/runtime/pipeline/outputProcessing.d.ts.map +1 -0
- package/dist/runtime/pipeline/outputProcessing.js +63 -0
- package/dist/runtime/pipeline/outputProcessing.js.map +1 -0
- package/dist/runtime/pipeline/sessionUtils.d.ts +12 -0
- package/dist/runtime/pipeline/sessionUtils.d.ts.map +1 -0
- package/dist/runtime/pipeline/sessionUtils.js +73 -0
- package/dist/runtime/pipeline/sessionUtils.js.map +1 -0
- package/dist/tools/Tool.d.ts +7 -0
- package/dist/tools/Tool.d.ts.map +1 -1
- package/dist/tools/Tool.js +12 -3
- package/dist/tools/Tool.js.map +1 -1
- package/dist/tools/memory.d.ts +26 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +51 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/types/index.d.ts +238 -9
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +4 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/telemetry.d.ts +107 -0
- package/dist/types/telemetry.d.ts.map +1 -1
- package/guides/AGENTS.md +173 -0
- package/guides/README.md +12 -0
- package/guides/TOOLS.md +93 -27
- package/package.json +25 -4
- package/dist/agents/LLMAgent.d.ts +0 -11
- package/dist/agents/LLMAgent.d.ts.map +0 -1
- package/dist/agents/LLMAgent.js +0 -31
- package/dist/agents/LLMAgent.js.map +0 -1
package/dist/runtime/Runtime.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
import { streamText, generateText, Output, stepCountIs } from 'ai';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import { createHandoffTool, isHandoffResult } from '../tools/handoff.js';
|
|
5
|
-
import { isFinalResult } from '../tools/final.js';
|
|
6
|
-
import { InjectionQueue, getPolicyProfileInjections } from './InjectionQueue.js';
|
|
7
|
-
import { checkStopConditions, defaultStopConditions } from '../guards/StopConditions.js';
|
|
8
1
|
import { HookRunner } from '../hooks/HookRunner.js';
|
|
9
2
|
import { ToolEnforcer } from '../guards/ToolEnforcer.js';
|
|
10
3
|
import { defaultEnforcementRules } from '../guards/rules.js';
|
|
@@ -12,169 +5,224 @@ import { MemoryStore } from '../session/stores/MemoryStore.js';
|
|
|
12
5
|
import { createHttpCallback } from '../callbacks/httpCallback.js';
|
|
13
6
|
import { createStreamCallbackAdapter } from '../callbacks/streamCallback.js';
|
|
14
7
|
import { compileSanitizePattern } from '../flows/template.js';
|
|
15
|
-
import {
|
|
16
|
-
import { getChunkArgs, getChunkErrorMessage, getChunkResult, getChunkToolCallId } from '../utils/streamChunk.js';
|
|
17
|
-
import { normalizeModelMessage } from '../utils/messageNormalization.js';
|
|
18
|
-
import { isRecord } from '../utils/isRecord.js';
|
|
8
|
+
import { defaultStopConditions } from '../guards/StopConditions.js';
|
|
19
9
|
import { ExtractionEngine } from './ExtractionEngine.js';
|
|
20
|
-
import { SessionEventManager } from './SessionEventManager.js';
|
|
21
10
|
import { SuggestionManager } from './SuggestionManager.js';
|
|
22
11
|
import { FlowExecutor } from './FlowExecutor.js';
|
|
23
|
-
import {
|
|
12
|
+
import { DefaultToolExecutor } from '../foundation/DefaultToolExecutor.js';
|
|
13
|
+
import { DefaultConversationState } from '../foundation/DefaultConversationState.js';
|
|
14
|
+
import { DefaultConversationEventLog } from '../foundation/DefaultConversationEventLog.js';
|
|
15
|
+
import { DefaultAgentStateController } from '../foundation/DefaultAgentStateController.js';
|
|
16
|
+
import { SessionCache } from './SessionCache.js';
|
|
17
|
+
import { StreamEmitter } from './StreamEmitter.js';
|
|
18
|
+
import { SessionMutex } from './SessionMutex.js';
|
|
19
|
+
import { runIntakeStage } from './pipeline/IntakeStage.js';
|
|
20
|
+
import { runAgentExecuteStage } from './pipeline/AgentExecuteStage.js';
|
|
21
|
+
import { runPostStreamStage } from './pipeline/PostStreamStage.js';
|
|
22
|
+
import { DefaultOrchestrationAuthority } from '../orchestration/DefaultOrchestrationAuthority.js';
|
|
24
23
|
export class Runtime {
|
|
25
24
|
config;
|
|
26
|
-
agents = new Map();
|
|
27
|
-
defaultAgentId;
|
|
28
|
-
defaultModel;
|
|
29
|
-
maxSteps;
|
|
30
|
-
maxHandoffs;
|
|
31
|
-
stopConditions;
|
|
32
|
-
_sessionStore;
|
|
33
|
-
hookRunner;
|
|
34
|
-
enforcer;
|
|
35
|
-
contextManager;
|
|
36
|
-
turnCount = 0;
|
|
37
25
|
abortControllers = new Map();
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
checkpointEventTypes = new Set([
|
|
50
|
-
'tool-result',
|
|
51
|
-
'tool-error',
|
|
52
|
-
'flow-transition',
|
|
53
|
-
]);
|
|
54
|
-
extractionEngine;
|
|
55
|
-
sessionEventManager;
|
|
56
|
-
suggestionManager;
|
|
57
|
-
flowExecutor;
|
|
58
|
-
wrapToolsWithEnforcement(context, tools) {
|
|
59
|
-
console.log('[Runtime] Wrapping tools:', Object.keys(tools));
|
|
60
|
-
const wrapped = {};
|
|
61
|
-
for (const [toolName, toolDef] of Object.entries(tools ?? {})) {
|
|
62
|
-
const exec = toolDef?.execute;
|
|
63
|
-
if (typeof exec !== 'function') {
|
|
64
|
-
wrapped[toolName] = toolDef;
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
wrapped[toolName] = {
|
|
68
|
-
...toolDef,
|
|
69
|
-
execute: async (args, options) => {
|
|
70
|
-
const toolCallId = typeof options?.toolCallId === 'string'
|
|
71
|
-
? options.toolCallId
|
|
72
|
-
: crypto.randomUUID();
|
|
73
|
-
const idempotencyKey = this.buildToolIdempotencyKey(context, toolName, toolCallId);
|
|
74
|
-
const callRecord = {
|
|
75
|
-
toolCallId,
|
|
76
|
-
toolName,
|
|
77
|
-
args,
|
|
78
|
-
idempotencyKey,
|
|
79
|
-
success: true,
|
|
80
|
-
timestamp: Date.now(),
|
|
81
|
-
};
|
|
82
|
-
const enforcement = await this.enforcer.check(callRecord, {
|
|
83
|
-
previousCalls: context.toolCallHistory,
|
|
84
|
-
currentStep: context.stepCount,
|
|
85
|
-
sessionState: context.session.state ?? {},
|
|
86
|
-
});
|
|
87
|
-
if (!enforcement.allowed) {
|
|
88
|
-
const reason = enforcement.reason ?? 'Tool call blocked by enforcement';
|
|
89
|
-
callRecord.success = false;
|
|
90
|
-
callRecord.error = new Error(reason);
|
|
91
|
-
context.toolCallHistory.push(callRecord);
|
|
92
|
-
await this.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
93
|
-
throw callRecord.error;
|
|
94
|
-
}
|
|
95
|
-
// Preserve AI SDK tool execution context (toolCallId, messages, experimental_context, etc.).
|
|
96
|
-
const enrichedOptions = this.withToolExecutionMetadata(options, context, toolName, toolCallId, idempotencyKey);
|
|
97
|
-
try {
|
|
98
|
-
console.log(`[Runtime] Executing tool ${toolName}...`);
|
|
99
|
-
return await exec(args, enrichedOptions);
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
console.error(`[Runtime] Tool execution failed for ${toolName}:`, error);
|
|
103
|
-
throw error;
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
return wrapped;
|
|
109
|
-
}
|
|
26
|
+
_userIdWarned = new Set();
|
|
27
|
+
sessionMutex = new SessionMutex();
|
|
28
|
+
turnServices;
|
|
29
|
+
_authority;
|
|
30
|
+
// Keep references for public API getters
|
|
31
|
+
_sessionStore;
|
|
32
|
+
_memoryService;
|
|
33
|
+
_toolExecutor;
|
|
34
|
+
_conversationState;
|
|
35
|
+
_eventLog;
|
|
36
|
+
_agentStateController;
|
|
110
37
|
constructor(config) {
|
|
111
38
|
this.config = config;
|
|
39
|
+
// Build agent map
|
|
40
|
+
const agents = new Map();
|
|
112
41
|
for (const agent of config.agents) {
|
|
113
|
-
|
|
42
|
+
agents.set(agent.id, agent);
|
|
43
|
+
}
|
|
44
|
+
// Config scalars
|
|
45
|
+
const defaultAgentId = config.defaultAgentId;
|
|
46
|
+
const defaultModel = config.defaultModel;
|
|
47
|
+
const maxSteps = config.maxSteps ?? 20;
|
|
48
|
+
const maxHandoffs = config.maxHandoffs ?? 10;
|
|
49
|
+
const stopConditions = config.stopConditions ?? defaultStopConditions;
|
|
50
|
+
const sessionStore = config.sessionStore ?? new MemoryStore();
|
|
51
|
+
const inputProcessors = config.inputProcessors ?? [];
|
|
52
|
+
const outputProcessors = config.outputProcessors ?? [];
|
|
53
|
+
const outputProcessorMode = config.outputProcessorMode ?? 'stream';
|
|
54
|
+
const alwaysRouteThroughTriage = config.alwaysRouteThroughTriage ?? false;
|
|
55
|
+
const triageAgentId = config.triageAgentId;
|
|
56
|
+
// Output redactions
|
|
57
|
+
let outputRedactions;
|
|
58
|
+
if (config.outputRedaction && config.outputRedaction.length > 0) {
|
|
59
|
+
outputRedactions = config.outputRedaction.map(r => {
|
|
60
|
+
const base = compileSanitizePattern(r.pattern);
|
|
61
|
+
const flags = base.flags.includes('g') ? base.flags : `${base.flags}g`;
|
|
62
|
+
const re = new RegExp(base.source, flags);
|
|
63
|
+
return { re, replacement: r.replacement };
|
|
64
|
+
});
|
|
114
65
|
}
|
|
115
|
-
|
|
116
|
-
this.defaultModel = config.defaultModel;
|
|
117
|
-
this.maxSteps = config.maxSteps ?? 20;
|
|
118
|
-
this.maxHandoffs = config.maxHandoffs ?? 10;
|
|
119
|
-
this.stopConditions = config.stopConditions ?? defaultStopConditions;
|
|
120
|
-
this._sessionStore = config.sessionStore ?? new MemoryStore();
|
|
66
|
+
// Hooks setup
|
|
121
67
|
const hooks = { ...config.hooks };
|
|
122
68
|
const appendOnStreamPart = (fn) => {
|
|
123
69
|
const originalHook = hooks.onStreamPart;
|
|
124
70
|
hooks.onStreamPart = async (context, part) => {
|
|
125
|
-
if (originalHook)
|
|
71
|
+
if (originalHook)
|
|
126
72
|
await originalHook(context, part);
|
|
127
|
-
}
|
|
128
73
|
await fn(context, part);
|
|
129
74
|
};
|
|
130
75
|
};
|
|
131
76
|
if (config.callback) {
|
|
132
|
-
|
|
133
|
-
appendOnStreamPart(httpCallback);
|
|
77
|
+
appendOnStreamPart(createHttpCallback(config.callback));
|
|
134
78
|
}
|
|
135
79
|
if (config.streamCallback) {
|
|
136
80
|
const adapter = createStreamCallbackAdapter(config.streamCallback);
|
|
137
81
|
appendOnStreamPart(adapter.onStreamPart);
|
|
138
82
|
const originalOnEnd = hooks.onEnd;
|
|
139
83
|
hooks.onEnd = async (context, result) => {
|
|
140
|
-
if (originalOnEnd)
|
|
84
|
+
if (originalOnEnd)
|
|
141
85
|
await originalOnEnd(context, result);
|
|
142
|
-
}
|
|
143
86
|
if (config.streamCallback?.flushOnEnd) {
|
|
144
87
|
await adapter.flush(config.streamCallback.flushTimeoutMs ?? 2000);
|
|
145
88
|
}
|
|
146
89
|
};
|
|
147
90
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const flags = base.flags.includes('g') ? base.flags : `${base.flags}g`;
|
|
160
|
-
const re = new RegExp(base.source, flags);
|
|
161
|
-
return { re, replacement: r.replacement };
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
this.extractionEngine = new ExtractionEngine({
|
|
165
|
-
defaultModel: this.defaultModel,
|
|
166
|
-
telemetry: config.telemetry,
|
|
91
|
+
const hookRunner = new HookRunner(hooks);
|
|
92
|
+
const enforcer = new ToolEnforcer(config.enforcementRules ?? defaultEnforcementRules);
|
|
93
|
+
const contextManager = config.contextManager;
|
|
94
|
+
// Service instances
|
|
95
|
+
const extractionEngine = new ExtractionEngine({ defaultModel, telemetry: config.telemetry });
|
|
96
|
+
const suggestionManager = new SuggestionManager(config);
|
|
97
|
+
const flowExecutor = new FlowExecutor(config);
|
|
98
|
+
const memoryService = config.memoryService;
|
|
99
|
+
const conversationState = new DefaultConversationState({
|
|
100
|
+
sessionStore,
|
|
101
|
+
defaultAgentId,
|
|
167
102
|
});
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
103
|
+
const eventLog = new DefaultConversationEventLog({ sessionStore });
|
|
104
|
+
const agentStateController = new DefaultAgentStateController();
|
|
105
|
+
const toolExecutor = new DefaultToolExecutor({
|
|
106
|
+
enforcer,
|
|
107
|
+
hookRunner,
|
|
108
|
+
memoryService,
|
|
109
|
+
});
|
|
110
|
+
let sessionCache;
|
|
111
|
+
if (config.sessionCache) {
|
|
112
|
+
sessionCache = new SessionCache(config.sessionCache);
|
|
113
|
+
}
|
|
114
|
+
const streamEmitter = new StreamEmitter({
|
|
115
|
+
hookRunner,
|
|
116
|
+
eventLog,
|
|
117
|
+
hasStreamPartHook: Boolean(hooks.onStreamPart),
|
|
118
|
+
outputRedactions,
|
|
119
|
+
redactCarryKey: '__ariaRedactCarry',
|
|
120
|
+
redactLookbehind: 64,
|
|
121
|
+
saveSessionCheckpoint: async (session) => {
|
|
122
|
+
await eventLog.checkpoint(session);
|
|
123
|
+
if (sessionCache)
|
|
124
|
+
sessionCache.put(session);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
// Store references for public API
|
|
128
|
+
this._sessionStore = sessionStore;
|
|
129
|
+
this._memoryService = memoryService;
|
|
130
|
+
this._toolExecutor = toolExecutor;
|
|
131
|
+
this._conversationState = conversationState;
|
|
132
|
+
this._eventLog = eventLog;
|
|
133
|
+
this._agentStateController = agentStateController;
|
|
134
|
+
// Build TurnServices — one plain struct, no closures
|
|
135
|
+
this.turnServices = {
|
|
136
|
+
conversationState,
|
|
137
|
+
agentStateController,
|
|
138
|
+
eventLog,
|
|
139
|
+
toolExecutor,
|
|
140
|
+
streamEmitter,
|
|
141
|
+
hookRunner,
|
|
142
|
+
enforcer,
|
|
143
|
+
extractionEngine,
|
|
144
|
+
suggestionManager,
|
|
145
|
+
flowExecutor,
|
|
146
|
+
contextManager,
|
|
147
|
+
memoryService,
|
|
148
|
+
sessionCache,
|
|
149
|
+
sessionStore,
|
|
150
|
+
agents,
|
|
151
|
+
config,
|
|
152
|
+
defaultAgentId,
|
|
153
|
+
defaultModel,
|
|
154
|
+
maxSteps,
|
|
155
|
+
maxHandoffs,
|
|
156
|
+
stopConditions,
|
|
157
|
+
inputProcessors,
|
|
158
|
+
outputProcessors,
|
|
159
|
+
outputProcessorMode,
|
|
160
|
+
outputRedactions,
|
|
161
|
+
alwaysRouteThroughTriage,
|
|
162
|
+
triageAgentId,
|
|
163
|
+
};
|
|
164
|
+
// Build OrchestrationAuthority that shares the same backing services.
|
|
165
|
+
// This establishes the architectural connection: both text (this Runtime)
|
|
166
|
+
// and realtime (RealtimeRuntime) can compose the same authority, sharing
|
|
167
|
+
// session store, hooks, tool executor, and all foundation services.
|
|
168
|
+
this._authority = new DefaultOrchestrationAuthority({
|
|
169
|
+
agents: config.agents,
|
|
170
|
+
defaultAgentId,
|
|
171
|
+
hooks,
|
|
172
|
+
sessionStore,
|
|
173
|
+
memoryService,
|
|
174
|
+
memoryIngestion: config.memoryIngestion,
|
|
175
|
+
defaultModel,
|
|
176
|
+
extractionModel: config.extractionModel,
|
|
177
|
+
enforcementRules: config.enforcementRules,
|
|
178
|
+
telemetry: config.telemetry,
|
|
179
|
+
injectedServices: {
|
|
180
|
+
toolExecutor,
|
|
181
|
+
conversationState,
|
|
182
|
+
eventLog,
|
|
183
|
+
agentState: agentStateController,
|
|
184
|
+
hookRunner,
|
|
185
|
+
},
|
|
171
186
|
});
|
|
172
|
-
this.suggestionManager = new SuggestionManager(config);
|
|
173
|
-
this.flowExecutor = new FlowExecutor(config);
|
|
174
187
|
}
|
|
188
|
+
// --- Public API ---
|
|
175
189
|
get sessionStore() {
|
|
176
190
|
return this._sessionStore;
|
|
177
191
|
}
|
|
192
|
+
get memoryService() {
|
|
193
|
+
return this._memoryService;
|
|
194
|
+
}
|
|
195
|
+
/** Foundation services — shared primitives reusable by VoiceEngine etc. */
|
|
196
|
+
get foundation() {
|
|
197
|
+
return {
|
|
198
|
+
toolExecutor: this._toolExecutor,
|
|
199
|
+
conversationState: this._conversationState,
|
|
200
|
+
eventLog: this._eventLog,
|
|
201
|
+
agentState: this._agentStateController,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The core orchestration authority shared by this Runtime.
|
|
206
|
+
*
|
|
207
|
+
* This authority uses the same backing services (session store, hook runner,
|
|
208
|
+
* tool executor, etc.) as the text pipeline. Use it to construct a
|
|
209
|
+
* RealtimeRuntime that shares state with this Runtime:
|
|
210
|
+
*
|
|
211
|
+
* ```typescript
|
|
212
|
+
* const runtime = createRuntime(config);
|
|
213
|
+
* const realtimeRuntime = new RealtimeRuntime({
|
|
214
|
+
* agents: config.agents,
|
|
215
|
+
* defaultAgentId: config.defaultAgentId,
|
|
216
|
+
* authority: runtime.authority,
|
|
217
|
+
* });
|
|
218
|
+
* ```
|
|
219
|
+
*
|
|
220
|
+
* This ensures text and realtime sessions share the same session store,
|
|
221
|
+
* the same hooks fire for both paths, and agent state is consistent.
|
|
222
|
+
*/
|
|
223
|
+
get authority() {
|
|
224
|
+
return this._authority;
|
|
225
|
+
}
|
|
178
226
|
getActiveAbortController(sessionId) {
|
|
179
227
|
return this.abortControllers.get(sessionId);
|
|
180
228
|
}
|
|
@@ -185,144 +233,84 @@ export class Runtime {
|
|
|
185
233
|
this.abortControllers.delete(sessionId);
|
|
186
234
|
}
|
|
187
235
|
}
|
|
236
|
+
async getSession(id) {
|
|
237
|
+
return this._sessionStore.get(id);
|
|
238
|
+
}
|
|
239
|
+
async deleteSession(id) {
|
|
240
|
+
await this._sessionStore.delete(id);
|
|
241
|
+
}
|
|
242
|
+
getAgent(id) {
|
|
243
|
+
return this.turnServices.agents.get(id);
|
|
244
|
+
}
|
|
245
|
+
getAllAgents() {
|
|
246
|
+
return Array.from(this.turnServices.agents.values());
|
|
247
|
+
}
|
|
248
|
+
async *chat(sessionId, input, userId) {
|
|
249
|
+
yield* this.stream({ input, sessionId, userId });
|
|
250
|
+
}
|
|
251
|
+
// --- Main orchestration ---
|
|
188
252
|
async *stream(options) {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
253
|
+
const svc = this.turnServices;
|
|
254
|
+
// Warn when memoryService is configured but userId is missing
|
|
255
|
+
if (svc.memoryService && !options.userId) {
|
|
256
|
+
const sessionId = options.sessionId ?? 'new';
|
|
257
|
+
if (!this._userIdWarned.has(sessionId)) {
|
|
258
|
+
this._userIdWarned.add(sessionId);
|
|
259
|
+
console.warn('[AriaFlow] memoryService is configured but session has no userId. ' +
|
|
260
|
+
'Memory operations will be skipped. Pass userId via stream({ input, sessionId, userId }).');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Acquire per-session mutex to prevent concurrent read-modify-write races.
|
|
264
|
+
// Only lock when a sessionId is provided (existing session). New sessions
|
|
265
|
+
// get a random UUID and cannot have concurrent callers.
|
|
266
|
+
let releaseMutex;
|
|
196
267
|
if (options.sessionId) {
|
|
197
|
-
|
|
198
|
-
session = existing ?? this.createSession(effectiveSessionId, userId);
|
|
268
|
+
releaseMutex = await this.sessionMutex.acquire(options.sessionId);
|
|
199
269
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
if (this.alwaysRouteThroughTriage) {
|
|
204
|
-
// If a flow agent is currently active and has an initialized, non-ended flow state,
|
|
205
|
-
// keep routing to it so multi-turn flows can complete without triage interference.
|
|
206
|
-
const currentAgentId = session.activeAgentId ?? session.currentAgent;
|
|
207
|
-
const currentAgent = currentAgentId ? this.agents.get(currentAgentId) : undefined;
|
|
208
|
-
const hasActiveFlow = Boolean(currentAgent) &&
|
|
209
|
-
this.isFlowAgent(currentAgent) &&
|
|
210
|
-
Boolean(this.getFlowState(session, currentAgent.id)?.initialized) &&
|
|
211
|
-
!Boolean(this.getFlowState(session, currentAgent.id)?.flowEnded);
|
|
212
|
-
if (!hasActiveFlow) {
|
|
213
|
-
const triageId = this.triageAgentId ?? this.defaultAgentId;
|
|
214
|
-
const triageAgent = this.agents.get(triageId);
|
|
215
|
-
if (triageAgent && this.isTriageAgent(triageAgent)) {
|
|
216
|
-
session.activeAgentId = triageId;
|
|
217
|
-
session.currentAgent = triageId;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
270
|
+
try {
|
|
271
|
+
yield* this._streamInner(options, svc);
|
|
220
272
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
273
|
+
finally {
|
|
274
|
+
releaseMutex?.();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Inner stream implementation — separated so the mutex can wrap the
|
|
279
|
+
* entire async generator lifecycle from IntakeStage through PostStreamStage.
|
|
280
|
+
*/
|
|
281
|
+
async *_streamInner(options, svc) {
|
|
282
|
+
// Stage 1: Intake
|
|
283
|
+
const intakeGen = runIntakeStage(options, svc);
|
|
284
|
+
let intakeNext = await intakeGen.next();
|
|
285
|
+
while (!intakeNext.done) {
|
|
286
|
+
yield intakeNext.value;
|
|
287
|
+
intakeNext = await intakeGen.next();
|
|
288
|
+
}
|
|
289
|
+
const intake = intakeNext.value;
|
|
290
|
+
if (!intake)
|
|
291
|
+
return;
|
|
292
|
+
const { session, context, processedInput, injectionQueue, controller } = intake;
|
|
240
293
|
this.abortControllers.set(session.id, controller);
|
|
241
|
-
const
|
|
242
|
-
this.abortControllers.delete(session.id);
|
|
243
|
-
};
|
|
244
|
-
controller.signal.addEventListener('abort', abortHandler);
|
|
245
|
-
let externalAbortHandler;
|
|
246
|
-
if (abortSignal) {
|
|
247
|
-
externalAbortHandler = () => {
|
|
248
|
-
controller.abort(abortSignal.reason ?? 'External abort');
|
|
249
|
-
};
|
|
250
|
-
if (abortSignal.aborted) {
|
|
251
|
-
controller.abort(abortSignal.reason ?? 'External abort');
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
abortSignal.addEventListener('abort', externalAbortHandler);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
await this.hookRunner.onStart(context);
|
|
258
|
-
// Run input processors BEFORE persisting the user message.
|
|
259
|
-
let processedInput = input;
|
|
294
|
+
const cleanup = this.setupAbortHandlers(session.id, controller, options.abortSignal);
|
|
260
295
|
try {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
toolCallHistory: context.toolCallHistory,
|
|
271
|
-
abortSignal: controller.signal,
|
|
272
|
-
};
|
|
273
|
-
const outcome = await runInputProcessors({
|
|
274
|
-
processors: turnInputProcessors,
|
|
275
|
-
input: processedInput,
|
|
276
|
-
messages: candidateMessages,
|
|
277
|
-
context: procCtx,
|
|
278
|
-
});
|
|
279
|
-
if (outcome.blocked) {
|
|
280
|
-
yield* this.emit(context, {
|
|
281
|
-
type: 'tripwire',
|
|
282
|
-
phase: 'input',
|
|
283
|
-
processorId: outcome.processorId,
|
|
284
|
-
reason: outcome.reason,
|
|
285
|
-
message: outcome.message,
|
|
286
|
-
});
|
|
287
|
-
yield* this.emit(context, { type: 'text-delta', text: outcome.message });
|
|
288
|
-
yield* this.emit(context, { type: 'turn-end' });
|
|
289
|
-
await this.hookRunner.onEnd(context, { success: true });
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
processedInput = outcome.input;
|
|
293
|
-
}
|
|
294
|
-
// Persist the (possibly modified) user message after input processors.
|
|
295
|
-
this.appendSessionMessage(session, { role: 'user', content: processedInput });
|
|
296
|
-
if (this.contextManager) {
|
|
297
|
-
const messagesBefore = session.messages.length;
|
|
298
|
-
const totalTokens = session.messages.reduce((sum, m) => {
|
|
299
|
-
const text = typeof m.content === 'string' ? m.content : '';
|
|
300
|
-
return sum + Math.ceil(text.length / 4);
|
|
301
|
-
}, 0);
|
|
302
|
-
const compacted = await this.contextManager.beforeTurn(session.messages, {
|
|
303
|
-
turnCount: this.turnCount,
|
|
304
|
-
totalTokens,
|
|
305
|
-
sessionId: session.id,
|
|
306
|
-
});
|
|
307
|
-
session.messages = this.normalizeSessionHistory(compacted);
|
|
308
|
-
const messagesAfter = session.messages.length;
|
|
309
|
-
if (messagesBefore !== messagesAfter) {
|
|
310
|
-
yield* this.emit(context, {
|
|
311
|
-
type: 'context-compacted',
|
|
312
|
-
messagesBefore,
|
|
313
|
-
messagesAfter,
|
|
314
|
-
});
|
|
315
|
-
}
|
|
296
|
+
// Stages 2-4: Agent execute (includes gather, assemble, handoff loop)
|
|
297
|
+
yield* runAgentExecuteStage({
|
|
298
|
+
context,
|
|
299
|
+
input: processedInput,
|
|
300
|
+
injectionQueue,
|
|
301
|
+
abortController: controller,
|
|
302
|
+
}, svc);
|
|
303
|
+
if (svc.config.suggestionModel) {
|
|
304
|
+
yield* svc.suggestionManager.generateSuggestions(context);
|
|
316
305
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
yield* this.generateSuggestions(context);
|
|
306
|
+
if (session.metadata) {
|
|
307
|
+
session.metadata.lastTurnHadToolCalls = context.toolCallHistory.length > 0;
|
|
320
308
|
}
|
|
321
|
-
await
|
|
309
|
+
await svc.hookRunner.onEnd(context, { success: true });
|
|
322
310
|
}
|
|
323
311
|
catch (error) {
|
|
324
312
|
if (controller.signal.aborted) {
|
|
325
|
-
yield*
|
|
313
|
+
yield* svc.streamEmitter.emit(context, {
|
|
326
314
|
type: 'interrupted',
|
|
327
315
|
sessionId: context.session.id,
|
|
328
316
|
reason: controller.signal.reason ?? 'Operation cancelled',
|
|
@@ -330,1167 +318,45 @@ export class Runtime {
|
|
|
330
318
|
lastAgentId: context.agentId,
|
|
331
319
|
lastStep: context.stepCount,
|
|
332
320
|
});
|
|
333
|
-
await
|
|
321
|
+
await svc.hookRunner.onEnd(context, { success: false });
|
|
334
322
|
}
|
|
335
323
|
else {
|
|
336
|
-
await
|
|
337
|
-
await
|
|
338
|
-
yield*
|
|
324
|
+
await svc.hookRunner.onError(context, error);
|
|
325
|
+
await svc.hookRunner.onEnd(context, { success: false, error: error });
|
|
326
|
+
yield* svc.streamEmitter.emit(context, { type: 'error', error: error.message });
|
|
339
327
|
}
|
|
340
328
|
}
|
|
341
329
|
finally {
|
|
342
|
-
|
|
343
|
-
if (abortSignal && externalAbortHandler) {
|
|
344
|
-
abortSignal.removeEventListener('abort', externalAbortHandler);
|
|
345
|
-
}
|
|
330
|
+
cleanup();
|
|
346
331
|
this.abortControllers.delete(session.id);
|
|
347
|
-
|
|
348
|
-
yield* this.emit(context, { type: 'done', sessionId: session.id });
|
|
349
|
-
await this.saveSessionCheckpoint(session);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
async *generateSuggestions(context) {
|
|
353
|
-
yield* this.suggestionManager.generateSuggestions(context);
|
|
354
|
-
}
|
|
355
|
-
async *chat(sessionId, input, userId) {
|
|
356
|
-
yield* this.stream({ input, sessionId, userId });
|
|
357
|
-
}
|
|
358
|
-
async *runLoop(context, injectionQueue, input, abortController) {
|
|
359
|
-
let currentInput = input;
|
|
360
|
-
const abortSignal = abortController?.signal;
|
|
361
|
-
let interruptionEmitted = false;
|
|
362
|
-
let circularRecoveryActive = false;
|
|
363
|
-
let circularRecoveryAttempts = 0;
|
|
364
|
-
while (context.handoffStack.length < this.maxHandoffs) {
|
|
365
|
-
if (abortSignal?.aborted) {
|
|
366
|
-
if (!interruptionEmitted) {
|
|
367
|
-
interruptionEmitted = true;
|
|
368
|
-
yield* this.emit(context, {
|
|
369
|
-
type: 'interrupted',
|
|
370
|
-
sessionId: context.session.id,
|
|
371
|
-
reason: abortSignal.reason ?? 'Operation cancelled',
|
|
372
|
-
timestamp: new Date(),
|
|
373
|
-
lastAgentId: context.agentId,
|
|
374
|
-
lastStep: context.stepCount,
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
// Allow a single "bounce back" (A -> B -> A) for detours and multi-intent turns.
|
|
380
|
-
// Stop only when an agent would be visited a third time in the same user turn.
|
|
381
|
-
const priorVisits = context.handoffStack.filter(id => id === context.agentId).length;
|
|
382
|
-
if (priorVisits >= 2 && !circularRecoveryActive) {
|
|
383
|
-
const err = `Circular handoff detected: ${context.handoffStack.join(' -> ')} -> ${context.agentId}`;
|
|
384
|
-
yield* this.emit(context, { type: 'error', error: err });
|
|
385
|
-
circularRecoveryAttempts += 1;
|
|
386
|
-
if (circularRecoveryAttempts > 1) {
|
|
387
|
-
yield* this.emitCircularHandoffFallback(context);
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
circularRecoveryActive = true;
|
|
391
|
-
// Reset path tracking for this turn and force a single in-agent recovery pass.
|
|
392
|
-
context.handoffStack = [];
|
|
393
|
-
}
|
|
394
|
-
context.handoffStack.push(context.agentId);
|
|
395
|
-
const agent = this.agents.get(context.agentId);
|
|
396
|
-
if (!agent) {
|
|
397
|
-
yield* this.emit(context, { type: 'error', error: `Agent "${context.agentId}" not found` });
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
// Agent-specific input processors. These run after the user message is in history and
|
|
401
|
-
// before the agent gets a chance to call the model/tools.
|
|
402
|
-
if (agent.inputProcessors && agent.inputProcessors.length > 0) {
|
|
403
|
-
const procCtx = {
|
|
404
|
-
session: context.session,
|
|
405
|
-
agentId: agent.id,
|
|
406
|
-
toolCallHistory: context.toolCallHistory,
|
|
407
|
-
abortSignal,
|
|
408
|
-
};
|
|
409
|
-
const outcome = await runInputProcessors({
|
|
410
|
-
processors: agent.inputProcessors,
|
|
411
|
-
input: currentInput,
|
|
412
|
-
messages: context.session.messages,
|
|
413
|
-
context: procCtx,
|
|
414
|
-
});
|
|
415
|
-
if (outcome.blocked) {
|
|
416
|
-
yield* this.emit(context, {
|
|
417
|
-
type: 'tripwire',
|
|
418
|
-
phase: 'input',
|
|
419
|
-
processorId: outcome.processorId,
|
|
420
|
-
reason: outcome.reason,
|
|
421
|
-
message: outcome.message,
|
|
422
|
-
});
|
|
423
|
-
yield* this.emit(context, { type: 'text-delta', text: outcome.message });
|
|
424
|
-
yield* this.emit(context, { type: 'turn-end' });
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
if (outcome.input !== currentInput) {
|
|
428
|
-
currentInput = outcome.input;
|
|
429
|
-
const last = context.session.messages[context.session.messages.length - 1];
|
|
430
|
-
if (last && last.role === 'user' && typeof last.content === 'string') {
|
|
431
|
-
last.content = currentInput;
|
|
432
|
-
this.touchSession(context.session);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
yield* this.emit(context, { type: 'agent-start', agentId: agent.id });
|
|
437
|
-
await this.hookRunner.onAgentStart(context, agent.id);
|
|
438
|
-
let autoContext;
|
|
439
|
-
if (agent.autoRetrieve) {
|
|
440
|
-
const toolName = agent.autoRetrieve.toolName ?? 'auto_retrieve';
|
|
441
|
-
const toolCallId = crypto.randomUUID();
|
|
442
|
-
const idempotencyKey = this.buildToolIdempotencyKey(context, toolName, toolCallId);
|
|
443
|
-
const callRecord = {
|
|
444
|
-
toolCallId,
|
|
445
|
-
toolName,
|
|
446
|
-
args: { input: currentInput },
|
|
447
|
-
idempotencyKey,
|
|
448
|
-
success: true,
|
|
449
|
-
timestamp: Date.now(),
|
|
450
|
-
};
|
|
451
|
-
await this.hookRunner.onToolCall(context, callRecord);
|
|
452
|
-
yield* this.emit(context, {
|
|
453
|
-
type: 'tool-start',
|
|
454
|
-
toolCallId,
|
|
455
|
-
toolName,
|
|
456
|
-
message: agent.autoRetrieve.message ?? 'Retrieving context...',
|
|
457
|
-
});
|
|
458
|
-
yield* this.emit(context, {
|
|
459
|
-
type: 'tool-call',
|
|
460
|
-
toolCallId,
|
|
461
|
-
toolName,
|
|
462
|
-
args: callRecord.args,
|
|
463
|
-
});
|
|
464
|
-
let result = null;
|
|
465
|
-
try {
|
|
466
|
-
result = await agent.autoRetrieve.run({ input: currentInput, context, abortSignal });
|
|
467
|
-
callRecord.result = result;
|
|
468
|
-
}
|
|
469
|
-
catch (error) {
|
|
470
|
-
callRecord.success = false;
|
|
471
|
-
callRecord.error = error;
|
|
472
|
-
await this.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
473
|
-
yield* this.emit(context, {
|
|
474
|
-
type: 'tool-error',
|
|
475
|
-
toolCallId,
|
|
476
|
-
toolName,
|
|
477
|
-
error: callRecord.error.message,
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
finally {
|
|
481
|
-
callRecord.durationMs = Date.now() - callRecord.timestamp;
|
|
482
|
-
}
|
|
483
|
-
if (callRecord.result) {
|
|
484
|
-
await this.hookRunner.onToolResult(context, callRecord);
|
|
485
|
-
yield* this.emit(context, {
|
|
486
|
-
type: 'tool-result',
|
|
487
|
-
toolCallId,
|
|
488
|
-
toolName,
|
|
489
|
-
result: callRecord.result,
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
yield* this.emit(context, {
|
|
493
|
-
type: 'tool-done',
|
|
494
|
-
toolCallId,
|
|
495
|
-
toolName,
|
|
496
|
-
durationMs: callRecord.durationMs ?? 0,
|
|
497
|
-
});
|
|
498
|
-
if (result?.text?.trim()) {
|
|
499
|
-
autoContext = {
|
|
500
|
-
label: agent.autoRetrieve.label ?? 'Relevant Context',
|
|
501
|
-
text: result.text,
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
const extractionSnapshot = await this.runTurnExtraction(agent, context, currentInput, abortSignal);
|
|
506
|
-
let activeRoutes;
|
|
507
|
-
if (this.isTriageAgent(agent)) {
|
|
508
|
-
const agentContext = this.buildAgentContext(currentInput, context, abortSignal);
|
|
509
|
-
const results = await Promise.all(agent.routes.map(async (route) => {
|
|
510
|
-
if (!route.condition)
|
|
511
|
-
return { route, active: true };
|
|
512
|
-
try {
|
|
513
|
-
const active = await route.condition(currentInput, agentContext);
|
|
514
|
-
return { route, active };
|
|
515
|
-
}
|
|
516
|
-
catch (error) {
|
|
517
|
-
console.error(`Error evaluating condition for route ${route.agentId}:`, error);
|
|
518
|
-
return { route, active: false }; // Fail-closed on error
|
|
519
|
-
}
|
|
520
|
-
}));
|
|
521
|
-
activeRoutes = results.filter(r => r.active).map(r => r.route);
|
|
522
|
-
}
|
|
523
|
-
let system = this.buildSystemPrompt(agent, injectionQueue, autoContext, context, activeRoutes);
|
|
524
|
-
const turnToolErrors = [];
|
|
525
|
-
if (extractionSnapshot?.includeInSystemPrompt) {
|
|
526
|
-
system += this.extractionEngine.buildExtractionSystemBlock(extractionSnapshot);
|
|
527
|
-
}
|
|
528
|
-
if (circularRecoveryActive) {
|
|
529
|
-
system = `${system}\n\n## Routing Recovery
|
|
530
|
-
A circular handoff was detected in this turn.
|
|
531
|
-
Stay with the current agent and provide the best direct answer.
|
|
532
|
-
Do not call or suggest handoff/transfer tools in this response.`;
|
|
533
|
-
}
|
|
534
|
-
const handoffCandidates = circularRecoveryActive ? [] : this.getHandoffCandidates(agent, activeRoutes);
|
|
535
|
-
const handoffTool = handoffCandidates.length > 0
|
|
536
|
-
? createHandoffTool(handoffCandidates, context.agentId)
|
|
537
|
-
: null;
|
|
538
|
-
let handoffTo = null;
|
|
539
|
-
let handoffReason = 'No reason provided';
|
|
540
|
-
if (this.isFlowAgent(agent)) {
|
|
541
|
-
const stopResult = checkStopConditions(context, this.stopConditions);
|
|
542
|
-
if (stopResult.shouldStop) {
|
|
543
|
-
yield* this.emit(context, { type: 'error', error: `Stopped: ${stopResult.reason}` });
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
const agentMaxSteps = agent.maxSteps ?? agent.maxTurns ?? this.maxSteps;
|
|
547
|
-
if (agentMaxSteps <= 0) {
|
|
548
|
-
yield* this.emit(context, { type: 'error', error: `Max steps exceeded for agent "${agent.id}"` });
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
context.stepCount += 1;
|
|
552
|
-
yield* this.emit(context, { type: 'step-start', step: context.stepCount, agentId: agent.id });
|
|
553
|
-
await this.hookRunner.onStepStart(context, context.stepCount);
|
|
554
|
-
const toolCalls = [];
|
|
555
|
-
try {
|
|
556
|
-
for await (const part of this.runFlowAgent(agent, context, currentInput, system, handoffTool ?? undefined, (target, reason) => {
|
|
557
|
-
handoffTo = target;
|
|
558
|
-
handoffReason = reason ?? 'No reason provided';
|
|
559
|
-
}, toolCalls, abortSignal)) {
|
|
560
|
-
yield part;
|
|
561
|
-
}
|
|
562
|
-
context.consecutiveErrors = 0;
|
|
563
|
-
if (context.session.metadata) {
|
|
564
|
-
context.session.metadata.totalSteps += 1;
|
|
565
|
-
}
|
|
566
|
-
yield* this.emit(context, { type: 'step-end', step: context.stepCount, agentId: agent.id });
|
|
567
|
-
await this.hookRunner.onStepEnd(context, context.stepCount, {
|
|
568
|
-
toolCalls,
|
|
569
|
-
finishReason: 'flow',
|
|
570
|
-
tokensUsed: 0,
|
|
571
|
-
handoffTo: handoffTo ?? undefined,
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
catch (error) {
|
|
575
|
-
if (abortSignal?.aborted) {
|
|
576
|
-
if (!interruptionEmitted) {
|
|
577
|
-
interruptionEmitted = true;
|
|
578
|
-
yield* this.emit(context, {
|
|
579
|
-
type: 'interrupted',
|
|
580
|
-
sessionId: context.session.id,
|
|
581
|
-
reason: abortSignal.reason ?? 'Operation cancelled',
|
|
582
|
-
timestamp: new Date(),
|
|
583
|
-
lastAgentId: context.agentId,
|
|
584
|
-
lastStep: context.stepCount,
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
context.consecutiveErrors += 1;
|
|
590
|
-
await this.hookRunner.onError(context, error);
|
|
591
|
-
yield* this.emit(context, { type: 'error', error: error.message });
|
|
592
|
-
if (context.consecutiveErrors >= 3) {
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
else {
|
|
598
|
-
const tools = this.wrapToolsWithEnforcement(context, handoffTool ? { ...agent.tools, handoff: handoffTool } : { ...agent.tools });
|
|
599
|
-
let agentSteps = 0;
|
|
600
|
-
const agentMaxSteps = agent.maxSteps ?? agent.maxTurns ?? this.maxSteps;
|
|
601
|
-
while (agentSteps < agentMaxSteps) {
|
|
602
|
-
const stopResult = checkStopConditions(context, this.stopConditions);
|
|
603
|
-
if (stopResult.shouldStop) {
|
|
604
|
-
yield* this.emit(context, { type: 'error', error: `Stopped: ${stopResult.reason}` });
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
agentSteps += 1;
|
|
608
|
-
context.stepCount += 1;
|
|
609
|
-
yield* this.emit(context, { type: 'step-start', step: context.stepCount, agentId: agent.id });
|
|
610
|
-
await this.hookRunner.onStepStart(context, context.stepCount);
|
|
611
|
-
try {
|
|
612
|
-
const model = agent.model ?? this.defaultModel;
|
|
613
|
-
if (!model) {
|
|
614
|
-
throw new Error(`Agent "${agent.id}" is missing a model`);
|
|
615
|
-
}
|
|
616
|
-
if (this.isTriageAgent(agent) && agent.triageMode === 'structured') {
|
|
617
|
-
const schema = z.object({
|
|
618
|
-
agentId: z.string().describe('Target agent ID for this request.'),
|
|
619
|
-
reason: z.string().describe('Short, concrete reason grounded in the user request.'),
|
|
620
|
-
confidence: z.number().min(0).max(1).describe('Routing confidence from 0 to 1.'),
|
|
621
|
-
stayWithCurrent: z.boolean().describe('True only if the current agent is best fit.'),
|
|
622
|
-
});
|
|
623
|
-
try {
|
|
624
|
-
// AI SDK v6+ recommended structured output approach.
|
|
625
|
-
const { output: decision } = await generateText({
|
|
626
|
-
model: model,
|
|
627
|
-
output: Output.object({ schema }),
|
|
628
|
-
system: this.buildStructuredTriagePrompt(agent, activeRoutes),
|
|
629
|
-
messages: context.session.messages,
|
|
630
|
-
abortSignal,
|
|
631
|
-
experimental_telemetry: agent.telemetry ?? this.config.telemetry,
|
|
632
|
-
});
|
|
633
|
-
const allowed = new Set(agent.routes.map(route => route.agentId));
|
|
634
|
-
let target = decision.agentId;
|
|
635
|
-
if (!allowed.has(target)) {
|
|
636
|
-
target = agent.defaultAgent ?? agent.routes[0]?.agentId ?? agent.id;
|
|
637
|
-
}
|
|
638
|
-
handoffTo = target;
|
|
639
|
-
handoffReason = decision.reason ?? 'Routed by triage';
|
|
640
|
-
}
|
|
641
|
-
catch (err) {
|
|
642
|
-
throw err;
|
|
643
|
-
}
|
|
644
|
-
yield* this.emit(context, { type: 'step-end', step: context.stepCount, agentId: agent.id });
|
|
645
|
-
await this.hookRunner.onStepEnd(context, context.stepCount, {
|
|
646
|
-
toolCalls: [],
|
|
647
|
-
finishReason: 'handoff',
|
|
648
|
-
tokensUsed: 0,
|
|
649
|
-
handoffTo,
|
|
650
|
-
});
|
|
651
|
-
break;
|
|
652
|
-
}
|
|
653
|
-
const result = streamText({
|
|
654
|
-
model: model,
|
|
655
|
-
system,
|
|
656
|
-
messages: context.session.messages,
|
|
657
|
-
tools,
|
|
658
|
-
abortSignal,
|
|
659
|
-
// Let the AI SDK handle tool-calling steps internally.
|
|
660
|
-
// Default is stepCountIs(1), which ends right after tool-calls.
|
|
661
|
-
stopWhen: stepCountIs(agent.toolMaxSteps ?? 5),
|
|
662
|
-
// Tool execution can read this via options.experimental_context.
|
|
663
|
-
experimental_context: {
|
|
664
|
-
session: context.session,
|
|
665
|
-
agentId: agent.id,
|
|
666
|
-
},
|
|
667
|
-
experimental_telemetry: agent.telemetry ?? this.config.telemetry,
|
|
668
|
-
});
|
|
669
|
-
const toolCalls = [];
|
|
670
|
-
let finalResult = null;
|
|
671
|
-
let finalEmitted = false;
|
|
672
|
-
const outputProcessors = this.getAgentOutputProcessors(agent);
|
|
673
|
-
const bufferOutput = this.outputProcessorMode === 'buffer' && outputProcessors.length > 0;
|
|
674
|
-
let bufferedText = '';
|
|
675
|
-
let stoppedByGuard = false;
|
|
676
|
-
for await (const chunk of result.fullStream) {
|
|
677
|
-
if (chunk.type === 'text-delta') {
|
|
678
|
-
if (finalResult) {
|
|
679
|
-
continue;
|
|
680
|
-
}
|
|
681
|
-
// Fail-closed: Suppress text delta if critical tool errors have occurred in this turn.
|
|
682
|
-
if (turnToolErrors.length > 0) {
|
|
683
|
-
continue;
|
|
684
|
-
}
|
|
685
|
-
if (bufferOutput) {
|
|
686
|
-
bufferedText += chunk.text;
|
|
687
|
-
}
|
|
688
|
-
else {
|
|
689
|
-
yield* this.emit(context, { type: 'text-delta', text: chunk.text });
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
if (chunk.type === 'tool-call') {
|
|
693
|
-
const args = getChunkArgs(chunk);
|
|
694
|
-
const callRecord = {
|
|
695
|
-
toolCallId: chunk.toolCallId,
|
|
696
|
-
toolName: chunk.toolName,
|
|
697
|
-
args,
|
|
698
|
-
idempotencyKey: this.buildToolIdempotencyKey(context, chunk.toolName, chunk.toolCallId),
|
|
699
|
-
success: true,
|
|
700
|
-
timestamp: Date.now(),
|
|
701
|
-
};
|
|
702
|
-
toolCalls.push(callRecord);
|
|
703
|
-
await this.hookRunner.onToolCall(context, callRecord);
|
|
704
|
-
// Emit tool-start with optional filler from tool config
|
|
705
|
-
const toolConfig = this.agents.get(context.agentId)?.tools;
|
|
706
|
-
const toolDef = toolConfig?.[chunk.toolName];
|
|
707
|
-
const filler = toolDef?.filler ?? `Let me check ${chunk.toolName}...`;
|
|
708
|
-
yield* this.emit(context, {
|
|
709
|
-
type: 'tool-start',
|
|
710
|
-
toolCallId: chunk.toolCallId,
|
|
711
|
-
toolName: chunk.toolName,
|
|
712
|
-
message: filler,
|
|
713
|
-
});
|
|
714
|
-
yield* this.emit(context, {
|
|
715
|
-
type: 'tool-call',
|
|
716
|
-
toolCallId: chunk.toolCallId,
|
|
717
|
-
toolName: chunk.toolName,
|
|
718
|
-
args,
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
if (chunk.type === 'tool-error') {
|
|
722
|
-
const errText = getChunkErrorMessage(chunk);
|
|
723
|
-
const args = getChunkArgs(chunk);
|
|
724
|
-
const toolCallId = getChunkToolCallId(chunk);
|
|
725
|
-
const callRecord = toolCalls.find(call => call.toolCallId === toolCallId);
|
|
726
|
-
if (callRecord) {
|
|
727
|
-
callRecord.success = false;
|
|
728
|
-
callRecord.error = new Error(errText);
|
|
729
|
-
if (args !== undefined)
|
|
730
|
-
callRecord.args = args;
|
|
731
|
-
context.toolCallHistory.push(callRecord);
|
|
732
|
-
await this.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
733
|
-
// Fail-closed: Track critical tool errors to block turn finalization.
|
|
734
|
-
const toolPolicy = agent.toolPolicies?.[callRecord.toolName];
|
|
735
|
-
const toolDef = agent.tools?.[callRecord.toolName];
|
|
736
|
-
const isCritical = toolPolicy?.critical ?? toolDef?.critical ?? true;
|
|
737
|
-
if (isCritical) {
|
|
738
|
-
turnToolErrors.push(callRecord);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
yield* this.emit(context, {
|
|
742
|
-
type: 'tool-error',
|
|
743
|
-
toolCallId: toolCallId ?? chunk.toolCallId,
|
|
744
|
-
toolName: chunk.toolName,
|
|
745
|
-
error: errText,
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
if (chunk.type === 'tool-result') {
|
|
749
|
-
const startTime = toolCalls.find(call => call.toolCallId === chunk.toolCallId)?.timestamp ?? Date.now();
|
|
750
|
-
const toolResult = getChunkResult(chunk);
|
|
751
|
-
const callRecord = toolCalls.find(call => call.toolCallId === chunk.toolCallId);
|
|
752
|
-
if (callRecord) {
|
|
753
|
-
callRecord.result = toolResult;
|
|
754
|
-
callRecord.durationMs = Date.now() - callRecord.timestamp;
|
|
755
|
-
context.toolCallHistory.push(callRecord);
|
|
756
|
-
await this.hookRunner.onToolResult(context, callRecord);
|
|
757
|
-
}
|
|
758
|
-
// Emit tool-done with duration
|
|
759
|
-
const durationMs = Date.now() - startTime;
|
|
760
|
-
yield* this.emit(context, {
|
|
761
|
-
type: 'tool-done',
|
|
762
|
-
toolCallId: chunk.toolCallId,
|
|
763
|
-
toolName: chunk.toolName,
|
|
764
|
-
durationMs,
|
|
765
|
-
});
|
|
766
|
-
if (callRecord) {
|
|
767
|
-
const enforcement = await this.enforcer.checkResult(callRecord, {
|
|
768
|
-
previousCalls: context.toolCallHistory,
|
|
769
|
-
currentStep: context.stepCount,
|
|
770
|
-
sessionState: context.session.state ?? {},
|
|
771
|
-
});
|
|
772
|
-
if (!enforcement.allowed) {
|
|
773
|
-
const reason = enforcement.reason ?? 'Tool result blocked by enforcement';
|
|
774
|
-
callRecord.success = false;
|
|
775
|
-
callRecord.error = new Error(reason);
|
|
776
|
-
await this.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
777
|
-
// Fail-closed: Track enforcement failures as critical errors.
|
|
778
|
-
turnToolErrors.push(callRecord);
|
|
779
|
-
yield* this.emit(context, {
|
|
780
|
-
type: 'tool-error',
|
|
781
|
-
toolCallId: chunk.toolCallId,
|
|
782
|
-
toolName: chunk.toolName,
|
|
783
|
-
error: reason,
|
|
784
|
-
});
|
|
785
|
-
finalResult = { type: 'final', text: reason };
|
|
786
|
-
if (!finalEmitted) {
|
|
787
|
-
finalEmitted = true;
|
|
788
|
-
if (bufferOutput) {
|
|
789
|
-
bufferedText += reason;
|
|
790
|
-
}
|
|
791
|
-
else {
|
|
792
|
-
yield* this.emit(context, { type: 'text-delta', text: reason });
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
continue;
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
yield* this.emit(context, {
|
|
799
|
-
type: 'tool-result',
|
|
800
|
-
toolCallId: chunk.toolCallId,
|
|
801
|
-
toolName: chunk.toolName,
|
|
802
|
-
result: toolResult,
|
|
803
|
-
});
|
|
804
|
-
// Stop as soon as a stop condition triggers, even mid-stream.
|
|
805
|
-
const stopResult = checkStopConditions(context, this.stopConditions);
|
|
806
|
-
if (stopResult.shouldStop) {
|
|
807
|
-
yield* this.emit(context, { type: 'error', error: `Stopped: ${stopResult.reason}` });
|
|
808
|
-
stoppedByGuard = true;
|
|
809
|
-
break;
|
|
810
|
-
}
|
|
811
|
-
if (isFinalResult(toolResult)) {
|
|
812
|
-
finalResult = toolResult;
|
|
813
|
-
if (!finalEmitted) {
|
|
814
|
-
finalEmitted = true;
|
|
815
|
-
if (bufferOutput) {
|
|
816
|
-
bufferedText += toolResult.text;
|
|
817
|
-
}
|
|
818
|
-
else {
|
|
819
|
-
yield* this.emit(context, { type: 'text-delta', text: toolResult.text });
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
continue;
|
|
823
|
-
}
|
|
824
|
-
if (isHandoffResult(toolResult)) {
|
|
825
|
-
const targetAgent = toolResult.targetAgent ?? toolResult.targetAgentId;
|
|
826
|
-
handoffTo = targetAgent;
|
|
827
|
-
handoffReason = toolResult.reason ?? 'No reason provided';
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
if (stoppedByGuard) {
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
const response = await result.response;
|
|
835
|
-
const finishReason = finalResult ? 'final' : await result.finishReason;
|
|
836
|
-
// Only finalize buffered output when the model has completed a non-tool finish.
|
|
837
|
-
// If the model ended on tool-calls, we must persist the tool messages and
|
|
838
|
-
// continue the loop (or let AI SDK continue in maxSteps) instead of emitting
|
|
839
|
-
// synthetic assistant text, which can duplicate responses.
|
|
840
|
-
if (finalResult || (bufferOutput && finishReason !== 'tool-calls') || turnToolErrors.length > 0) {
|
|
841
|
-
let rawText = finalResult ? finalResult.text : bufferedText;
|
|
842
|
-
// Fail-closed: Suppress success message if critical tool failed.
|
|
843
|
-
if (turnToolErrors.length > 0) {
|
|
844
|
-
const failedTools = turnToolErrors.map(e => `\`${e.toolName}\``).join(', ');
|
|
845
|
-
rawText = `I encountered an error while using the following tools: ${failedTools}. I cannot proceed with the requested action at this time.`;
|
|
846
|
-
yield* this.emit(context, {
|
|
847
|
-
type: 'error',
|
|
848
|
-
error: `Turn blocked by critical tool failures: ${failedTools}`,
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
const processed = await this.runOutputProcessing(agent, context, rawText);
|
|
852
|
-
if (processed.tripwire) {
|
|
853
|
-
yield* this.emit(context, {
|
|
854
|
-
type: 'tripwire',
|
|
855
|
-
phase: 'output',
|
|
856
|
-
processorId: processed.tripwire.processorId,
|
|
857
|
-
reason: processed.tripwire.reason,
|
|
858
|
-
message: processed.tripwire.message,
|
|
859
|
-
});
|
|
860
|
-
}
|
|
861
|
-
if (bufferOutput || turnToolErrors.length > 0) {
|
|
862
|
-
yield* this.emit(context, { type: 'text-delta', text: processed.text });
|
|
863
|
-
}
|
|
864
|
-
this.appendSessionMessage(context.session, { role: 'assistant', content: processed.text });
|
|
865
|
-
}
|
|
866
|
-
else {
|
|
867
|
-
const beforeLen = context.session.messages.length;
|
|
868
|
-
this.appendSessionMessages(context.session, response.messages);
|
|
869
|
-
const tripwires = await this.postProcessPersistedAssistantMessages(agent, context, beforeLen);
|
|
870
|
-
for (const t of tripwires) {
|
|
871
|
-
yield* this.emit(context, {
|
|
872
|
-
type: 'tripwire',
|
|
873
|
-
phase: 'output',
|
|
874
|
-
processorId: t.processorId,
|
|
875
|
-
reason: t.reason,
|
|
876
|
-
message: t.message,
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
const usage = await result.usage;
|
|
881
|
-
const totalTokens = usage.totalTokens ?? 0;
|
|
882
|
-
context.totalTokens += totalTokens;
|
|
883
|
-
if (context.session.metadata) {
|
|
884
|
-
context.session.metadata.totalTokens += totalTokens;
|
|
885
|
-
context.session.metadata.totalSteps += 1;
|
|
886
|
-
}
|
|
887
|
-
context.consecutiveErrors = 0;
|
|
888
|
-
yield* this.emit(context, { type: 'step-end', step: context.stepCount, agentId: agent.id });
|
|
889
|
-
await this.hookRunner.onStepEnd(context, context.stepCount, {
|
|
890
|
-
toolCalls,
|
|
891
|
-
finishReason,
|
|
892
|
-
tokensUsed: totalTokens,
|
|
893
|
-
handoffTo: handoffTo ?? undefined,
|
|
894
|
-
});
|
|
895
|
-
if (finalResult || finishReason !== 'tool-calls' || handoffTo) {
|
|
896
|
-
break;
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
catch (error) {
|
|
900
|
-
console.error('RUNTIME_LOOP_ERROR:', error);
|
|
901
|
-
if (abortSignal?.aborted) {
|
|
902
|
-
if (!interruptionEmitted) {
|
|
903
|
-
interruptionEmitted = true;
|
|
904
|
-
yield* this.emit(context, {
|
|
905
|
-
type: 'interrupted',
|
|
906
|
-
sessionId: context.session.id,
|
|
907
|
-
reason: abortSignal.reason ?? 'Operation cancelled',
|
|
908
|
-
timestamp: new Date(),
|
|
909
|
-
lastAgentId: context.agentId,
|
|
910
|
-
lastStep: context.stepCount,
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
context.consecutiveErrors += 1;
|
|
916
|
-
await this.hookRunner.onError(context, error);
|
|
917
|
-
yield* this.emit(context, { type: 'error', error: error.message });
|
|
918
|
-
if (context.consecutiveErrors >= 3) {
|
|
919
|
-
return;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
yield* this.emit(context, { type: 'agent-end', agentId: agent.id });
|
|
925
|
-
await this.hookRunner.onAgentEnd(context, agent.id);
|
|
926
|
-
if (circularRecoveryActive && handoffTo) {
|
|
927
|
-
yield* this.emit(context, {
|
|
928
|
-
type: 'error',
|
|
929
|
-
error: `Circular handoff recovery failed: attempted handoff ${context.agentId} -> ${handoffTo}`,
|
|
930
|
-
});
|
|
931
|
-
yield* this.emitCircularHandoffFallback(context);
|
|
932
|
-
return;
|
|
933
|
-
}
|
|
934
|
-
if (circularRecoveryActive) {
|
|
935
|
-
circularRecoveryActive = false;
|
|
936
|
-
}
|
|
937
|
-
if (!handoffTo) {
|
|
938
|
-
break;
|
|
939
|
-
}
|
|
940
|
-
await this.hookRunner.onHandoff(context, context.agentId, handoffTo, handoffReason);
|
|
941
|
-
yield* this.emit(context, {
|
|
942
|
-
type: 'handoff',
|
|
943
|
-
from: context.agentId,
|
|
944
|
-
to: handoffTo,
|
|
945
|
-
reason: handoffReason,
|
|
946
|
-
});
|
|
947
|
-
context.session.handoffHistory.push({
|
|
948
|
-
from: context.agentId,
|
|
949
|
-
to: handoffTo,
|
|
950
|
-
reason: handoffReason,
|
|
951
|
-
timestamp: new Date(),
|
|
952
|
-
});
|
|
953
|
-
if (context.session.metadata) {
|
|
954
|
-
context.session.metadata.handoffHistory.push({
|
|
955
|
-
from: context.agentId,
|
|
956
|
-
to: handoffTo,
|
|
957
|
-
reason: handoffReason,
|
|
958
|
-
timestamp: new Date(),
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
context.session.activeAgentId = handoffTo;
|
|
962
|
-
context.session.currentAgent = handoffTo;
|
|
963
|
-
await this.saveSessionCheckpoint(context.session);
|
|
964
|
-
context.agentId = handoffTo;
|
|
965
|
-
}
|
|
966
|
-
if (context.handoffStack.length >= this.maxHandoffs) {
|
|
967
|
-
yield* this.emit(context, { type: 'error', error: `Maximum handoffs (${this.maxHandoffs}) exceeded` });
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
getCircularHandoffFallbackMessage() {
|
|
971
|
-
const configured = this.config.circularHandoffFallbackMessage?.trim();
|
|
972
|
-
if (configured) {
|
|
973
|
-
return configured;
|
|
332
|
+
yield* runPostStreamStage(context, session, svc);
|
|
974
333
|
}
|
|
975
|
-
return 'I hit a routing issue between agents. I will continue here. Tell me the exact outcome you need in one sentence.';
|
|
976
334
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
}
|
|
982
|
-
buildPolicyInjectionQueue() {
|
|
983
|
-
const queue = new InjectionQueue();
|
|
984
|
-
const profile = this.config.policyProfile ?? 'minimal';
|
|
985
|
-
queue.addBatch(getPolicyProfileInjections(profile));
|
|
986
|
-
if (this.config.policyInjections && this.config.policyInjections.length > 0) {
|
|
987
|
-
queue.addBatch(this.config.policyInjections);
|
|
988
|
-
}
|
|
989
|
-
return queue;
|
|
990
|
-
}
|
|
991
|
-
createSession(id, userId) {
|
|
992
|
-
const now = new Date();
|
|
993
|
-
return {
|
|
994
|
-
id: id ?? crypto.randomUUID(),
|
|
995
|
-
userId,
|
|
996
|
-
messages: [],
|
|
997
|
-
createdAt: now,
|
|
998
|
-
updatedAt: now,
|
|
999
|
-
workingMemory: {},
|
|
1000
|
-
currentAgent: this.defaultAgentId,
|
|
1001
|
-
activeAgentId: this.defaultAgentId,
|
|
1002
|
-
state: {},
|
|
1003
|
-
metadata: {
|
|
1004
|
-
createdAt: now,
|
|
1005
|
-
lastActiveAt: now,
|
|
1006
|
-
totalTokens: 0,
|
|
1007
|
-
totalSteps: 0,
|
|
1008
|
-
handoffHistory: [],
|
|
1009
|
-
},
|
|
1010
|
-
agentStates: {},
|
|
1011
|
-
handoffHistory: [],
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
1014
|
-
isFlowAgent(agent) {
|
|
1015
|
-
return agent.type === 'flow';
|
|
1016
|
-
}
|
|
1017
|
-
isTriageAgent(agent) {
|
|
1018
|
-
return agent.type === 'triage';
|
|
1019
|
-
}
|
|
1020
|
-
buildPromptMemoryView(session, agent) {
|
|
1021
|
-
const memory = session.workingMemory ?? {};
|
|
1022
|
-
const allowlist = agent.promptMemoryAllowlist ?? this.config.promptMemoryAllowlist;
|
|
1023
|
-
const internalKeys = [
|
|
1024
|
-
this.runtimeEventLogKey,
|
|
1025
|
-
this.runtimeSessionTurnKey,
|
|
1026
|
-
this.redactCarryKey,
|
|
1027
|
-
];
|
|
1028
|
-
const filtered = {};
|
|
1029
|
-
for (const [key, value] of Object.entries(memory)) {
|
|
1030
|
-
if (internalKeys.includes(key)) {
|
|
1031
|
-
continue;
|
|
1032
|
-
}
|
|
1033
|
-
if (allowlist && !allowlist.includes(key)) {
|
|
1034
|
-
continue;
|
|
1035
|
-
}
|
|
1036
|
-
filtered[key] = value;
|
|
1037
|
-
}
|
|
1038
|
-
return filtered;
|
|
1039
|
-
}
|
|
1040
|
-
buildSystemPrompt(agent, injectionQueue, autoContext, context, activeRoutes) {
|
|
1041
|
-
const basePrompt = this.isTriageAgent(agent)
|
|
1042
|
-
? this.buildTriagePrompt(agent, activeRoutes)
|
|
1043
|
-
: agent.systemPrompt;
|
|
1044
|
-
const autoBlock = autoContext?.text?.trim()
|
|
1045
|
-
? `\n\n## ${autoContext.label}\n${autoContext.text}`
|
|
1046
|
-
: '';
|
|
1047
|
-
const systemInjections = injectionQueue.getFor('system');
|
|
1048
|
-
const memory = context?.session ? this.buildPromptMemoryView(context.session, agent) : {};
|
|
1049
|
-
const memoryBlock = Object.keys(memory).length > 0
|
|
1050
|
-
? `\n\n## Known Information\n${JSON.stringify(memory, null, 2)}`
|
|
1051
|
-
: '';
|
|
1052
|
-
const merged = `${basePrompt}${autoBlock}${memoryBlock}`;
|
|
1053
|
-
return systemInjections ? `${merged}\n\n${systemInjections}` : merged;
|
|
1054
|
-
}
|
|
1055
|
-
buildStructuredTriagePrompt(agent, activeRoutes) {
|
|
1056
|
-
const routes = activeRoutes ?? agent.routes;
|
|
1057
|
-
const routeDescriptions = routes
|
|
1058
|
-
.map(route => `- ${route.agentId}: ${route.description}`)
|
|
1059
|
-
.join('\n');
|
|
1060
|
-
const allowed = routes.map(route => route.agentId);
|
|
1061
|
-
const defaultNote = agent.defaultAgent ? `Default: ${agent.defaultAgent}` : 'Default: none';
|
|
1062
|
-
return `${agent.systemPrompt}
|
|
1063
|
-
|
|
1064
|
-
## Available Specialists
|
|
1065
|
-
${routeDescriptions}
|
|
1066
|
-
|
|
1067
|
-
## Output (JSON)
|
|
1068
|
-
Return a JSON object with:
|
|
1069
|
-
- agentId: one of [${allowed.join(', ')}]
|
|
1070
|
-
- reason: short, concrete reason grounded in the user request
|
|
1071
|
-
- confidence: number 0-1 (0 = weak match, 1 = perfect match)
|
|
1072
|
-
- stayWithCurrent: boolean (true only if current agent is best fit)
|
|
1073
|
-
${defaultNote}`;
|
|
1074
|
-
}
|
|
1075
|
-
buildTriagePrompt(agent, activeRoutes) {
|
|
1076
|
-
const routes = activeRoutes ?? agent.routes;
|
|
1077
|
-
const routeDescriptions = routes
|
|
1078
|
-
.map(route => `- **${route.agentId}**: ${route.description}`)
|
|
1079
|
-
.join('\n');
|
|
1080
|
-
const defaultNote = agent.defaultAgent
|
|
1081
|
-
? `\n- Use "${agent.defaultAgent}" when no specialist applies.`
|
|
1082
|
-
: '';
|
|
1083
|
-
return `${agent.systemPrompt}
|
|
1084
|
-
|
|
1085
|
-
## Available Specialists
|
|
1086
|
-
${routeDescriptions}
|
|
1087
|
-
|
|
1088
|
-
## Instructions
|
|
1089
|
-
- For general questions, answer directly
|
|
1090
|
-
- When the customer needs specialized help, use the handoff tool
|
|
1091
|
-
- Always provide a brief reason for the handoff${defaultNote}`;
|
|
1092
|
-
}
|
|
1093
|
-
getHandoffCandidates(agent, activeRoutes) {
|
|
1094
|
-
if (this.isTriageAgent(agent)) {
|
|
1095
|
-
const routes = activeRoutes ?? agent.routes;
|
|
1096
|
-
return routes
|
|
1097
|
-
.map(route => this.agents.get(route.agentId))
|
|
1098
|
-
.filter((candidate) => Boolean(candidate));
|
|
1099
|
-
}
|
|
1100
|
-
// Production default: only triage routes by default. Other agents can handoff only if explicitly configured.
|
|
1101
|
-
const targets = agent.canHandoffTo;
|
|
1102
|
-
if (!targets || targets.length === 0) {
|
|
1103
|
-
return [];
|
|
1104
|
-
}
|
|
1105
|
-
return targets
|
|
1106
|
-
.map(id => this.agents.get(id))
|
|
1107
|
-
.filter((candidate) => Boolean(candidate));
|
|
1108
|
-
}
|
|
1109
|
-
getFlowState(session, agentId) {
|
|
1110
|
-
const stored = session.agentStates?.[agentId]?.state;
|
|
1111
|
-
if (!stored) {
|
|
1112
|
-
return undefined;
|
|
1113
|
-
}
|
|
1114
|
-
return stored;
|
|
1115
|
-
}
|
|
1116
|
-
buildAgentContext(input, context, abortSignal) {
|
|
1117
|
-
return {
|
|
1118
|
-
session: context.session,
|
|
1119
|
-
messages: context.session.messages,
|
|
1120
|
-
workingMemory: new SessionWorkingMemory(context.session),
|
|
1121
|
-
currentAgent: context.agentId,
|
|
1122
|
-
turnCount: this.turnCount,
|
|
1123
|
-
metadata: context.session.metadata,
|
|
1124
|
-
abortSignal,
|
|
1125
|
-
};
|
|
1126
|
-
}
|
|
1127
|
-
setFlowState(session, agentId, state) {
|
|
1128
|
-
session.agentStates[agentId] = {
|
|
1129
|
-
agentId,
|
|
1130
|
-
state: state,
|
|
1131
|
-
lastActive: new Date(),
|
|
1132
|
-
};
|
|
1133
|
-
this.updateFlowStateSnapshot(session, agentId, state);
|
|
1134
|
-
this.touchSession(session);
|
|
1135
|
-
}
|
|
1136
|
-
clearFlowState(session, agentId) {
|
|
1137
|
-
delete session.agentStates[agentId];
|
|
1138
|
-
this.touchSession(session);
|
|
1139
|
-
}
|
|
1140
|
-
updateFlowStateSnapshot(session, agentId, state) {
|
|
1141
|
-
const existing = session.workingMemory.flowStateByAgent;
|
|
1142
|
-
const byAgent = typeof existing === 'object' && existing !== null
|
|
1143
|
-
? { ...existing }
|
|
1144
|
-
: {};
|
|
1145
|
-
byAgent[agentId] = {
|
|
1146
|
-
currentNode: state.context.currentNode,
|
|
1147
|
-
collectedData: state.context.collectedData,
|
|
1148
|
-
nodeHistory: state.context.nodeHistory,
|
|
1149
|
-
initialized: state.initialized,
|
|
1150
|
-
flowEnded: state.flowEnded,
|
|
1151
|
-
updatedAt: new Date().toISOString(),
|
|
1152
|
-
};
|
|
1153
|
-
session.workingMemory.flowStateByAgent = byAgent;
|
|
1154
|
-
this.touchSession(session);
|
|
1155
|
-
}
|
|
1156
|
-
buildFlowWithHandoff(agent, handoffTool, suppressAutoRespond) {
|
|
1157
|
-
// If we don't need to inject a handoff tool and we don't need to suppress the initial autoRespond,
|
|
1158
|
-
// we can return the flow as-is. Otherwise we must clone nodes to apply changes.
|
|
1159
|
-
const needsInitialSuppression = suppressAutoRespond &&
|
|
1160
|
-
agent.flow.nodes.some(n => n.id === agent.initialNode && n.autoRespond === undefined);
|
|
1161
|
-
if (!handoffTool && !needsInitialSuppression)
|
|
1162
|
-
return agent.flow;
|
|
1163
|
-
return {
|
|
1164
|
-
...agent.flow,
|
|
1165
|
-
nodes: agent.flow.nodes.map(node => {
|
|
1166
|
-
const shouldSuppress = suppressAutoRespond
|
|
1167
|
-
&& node.id === agent.initialNode
|
|
1168
|
-
&& node.autoRespond === undefined;
|
|
1169
|
-
const existingTools = node.tools;
|
|
1170
|
-
// Support tool factories (tools: (ctx) => ToolSet) as well as static ToolSets.
|
|
1171
|
-
if (typeof existingTools === 'function') {
|
|
1172
|
-
return {
|
|
1173
|
-
...node,
|
|
1174
|
-
tools: (ctx) => {
|
|
1175
|
-
const resolved = existingTools(ctx) ?? {};
|
|
1176
|
-
if (!handoffTool)
|
|
1177
|
-
return resolved;
|
|
1178
|
-
return resolved.handoff ? resolved : { ...resolved, handoff: handoffTool };
|
|
1179
|
-
},
|
|
1180
|
-
...(shouldSuppress ? { autoRespond: false } : {}),
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
const toolSet = existingTools ?? {};
|
|
1184
|
-
if (!handoffTool || toolSet.handoff) {
|
|
1185
|
-
return shouldSuppress ? { ...node, autoRespond: false } : node;
|
|
1186
|
-
}
|
|
1187
|
-
return {
|
|
1188
|
-
...node,
|
|
1189
|
-
tools: {
|
|
1190
|
-
...toolSet,
|
|
1191
|
-
handoff: handoffTool,
|
|
1192
|
-
},
|
|
1193
|
-
...(shouldSuppress ? { autoRespond: false } : {}),
|
|
1194
|
-
};
|
|
1195
|
-
}),
|
|
1196
|
-
};
|
|
1197
|
-
}
|
|
1198
|
-
getFlowNode(agent, nodeId) {
|
|
1199
|
-
if (!nodeId) {
|
|
1200
|
-
return undefined;
|
|
1201
|
-
}
|
|
1202
|
-
return agent.flow.nodes.find(node => node.id === nodeId);
|
|
1203
|
-
}
|
|
1204
|
-
getExtractionConfig(agent, session) {
|
|
1205
|
-
if (this.isFlowAgent(agent)) {
|
|
1206
|
-
const flowState = this.getFlowState(session, agent.id);
|
|
1207
|
-
const nodeId = flowState?.context.currentNode ?? agent.initialNode;
|
|
1208
|
-
const node = this.getFlowNode(agent, nodeId);
|
|
1209
|
-
const config = node?.extraction ?? agent.extraction;
|
|
1210
|
-
if (!config) {
|
|
1211
|
-
return null;
|
|
1212
|
-
}
|
|
1213
|
-
return { config, nodeId };
|
|
1214
|
-
}
|
|
1215
|
-
if (!agent.extraction) {
|
|
1216
|
-
return null;
|
|
1217
|
-
}
|
|
1218
|
-
return { config: agent.extraction };
|
|
1219
|
-
}
|
|
1220
|
-
async runTurnExtraction(agent, context, input, abortSignal) {
|
|
1221
|
-
return this.extractionEngine.runTurnExtraction(agent, context, input, {
|
|
1222
|
-
getFlowState: (s, id) => this.getFlowState(s, id),
|
|
1223
|
-
setFlowState: (s, id, st) => this.setFlowState(s, id, st),
|
|
1224
|
-
getFlowNode: (a, nid) => this.getFlowNode(a, nid),
|
|
1225
|
-
isFlowAgent: (a) => this.isFlowAgent(a),
|
|
1226
|
-
touchSession: (s) => this.touchSession(s),
|
|
1227
|
-
}, abortSignal);
|
|
1228
|
-
}
|
|
1229
|
-
async shouldHandleFlowInput(agent, input, flowState, abortSignal) {
|
|
1230
|
-
return this.flowExecutor.shouldHandleFlowInput(agent, input, flowState, this.getFlowExecutorHelpers(), abortSignal);
|
|
1231
|
-
}
|
|
1232
|
-
async *runDetourResponse(agent, context, input, flowState, handoffTool, onHandoff, abortSignal) {
|
|
1233
|
-
yield* this.flowExecutor.runDetourResponse(agent, context, input, flowState, handoffTool, onHandoff, this.getFlowExecutorHelpers(), abortSignal);
|
|
1234
|
-
}
|
|
1235
|
-
async *runFlowAgent(agent, context, input, systemPrompt, handoffTool, onHandoff, toolCalls, abortSignal) {
|
|
1236
|
-
yield* this.flowExecutor.runFlowAgent(agent, context, input, systemPrompt, handoffTool, onHandoff, toolCalls, this.getFlowExecutorHelpers(), abortSignal);
|
|
1237
|
-
}
|
|
1238
|
-
getFlowExecutorHelpers() {
|
|
1239
|
-
return {
|
|
1240
|
-
emit: (ctx, part) => this.emit(ctx, part),
|
|
1241
|
-
runOutputProcessing: (agent, ctx, text) => this.runOutputProcessing(agent, ctx, text),
|
|
1242
|
-
appendSessionMessage: (s, m) => this.appendSessionMessage(s, m),
|
|
1243
|
-
appendSessionMessages: (s, ms) => this.appendSessionMessages(s, ms),
|
|
1244
|
-
postProcessPersistedAssistantMessages: (agent, ctx, start) => this.postProcessPersistedAssistantMessages(agent, ctx, start),
|
|
1245
|
-
getAgentOutputProcessors: (agent) => this.getAgentOutputProcessors(agent),
|
|
1246
|
-
getSessionTurn: (s) => this.getSessionTurn(s),
|
|
1247
|
-
buildToolIdempotencyKey: (ctx, tn, tcid) => this.buildToolIdempotencyKey(ctx, tn, tcid),
|
|
1248
|
-
setFlowState: (s, id, st) => this.setFlowState(s, id, st),
|
|
1249
|
-
getFlowState: (s, id) => this.getFlowState(s, id),
|
|
1250
|
-
updateFlowStateSnapshot: (s, id, st) => this.updateFlowStateSnapshot(s, id, st),
|
|
1251
|
-
clearFlowState: (s, id) => this.clearFlowState(s, id),
|
|
1252
|
-
buildFlowWithHandoff: (a, ht, s) => this.buildFlowWithHandoff(a, ht, s),
|
|
1253
|
-
getFlowNode: (a, ni) => this.getFlowNode(a, ni),
|
|
1254
|
-
touchSession: (s) => this.touchSession(s),
|
|
1255
|
-
matchesDetourRule: (i, p) => this.matchesDetourRuleHelper(i, p),
|
|
1256
|
-
enforcecheck: (call, ctx) => this.enforcer.check(call, {
|
|
1257
|
-
previousCalls: ctx.toolCallHistory,
|
|
1258
|
-
currentStep: ctx.stepCount,
|
|
1259
|
-
sessionState: ctx.session.state ?? {},
|
|
1260
|
-
}),
|
|
1261
|
-
enforcecheckResult: (call, ctx) => this.enforcer.checkResult(call, {
|
|
1262
|
-
previousCalls: ctx.toolCallHistory,
|
|
1263
|
-
currentStep: ctx.stepCount,
|
|
1264
|
-
sessionState: ctx.session.state ?? {},
|
|
1265
|
-
}),
|
|
1266
|
-
onToolCallHook: (ctx, call) => this.hookRunner.onToolCall(ctx, call),
|
|
1267
|
-
onToolResultHook: (ctx, call) => this.hookRunner.onToolResult(ctx, call),
|
|
1268
|
-
onToolErrorHook: (ctx, call, error) => this.hookRunner.onToolError(ctx, call, error),
|
|
335
|
+
// --- Private helpers ---
|
|
336
|
+
setupAbortHandlers(sessionId, controller, externalSignal) {
|
|
337
|
+
const abortHandler = () => {
|
|
338
|
+
this.abortControllers.delete(sessionId);
|
|
1269
339
|
};
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
if (
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
if (regex.test(input)) {
|
|
1279
|
-
return true;
|
|
1280
|
-
}
|
|
340
|
+
controller.signal.addEventListener('abort', abortHandler);
|
|
341
|
+
let externalAbortHandler;
|
|
342
|
+
if (externalSignal) {
|
|
343
|
+
externalAbortHandler = () => {
|
|
344
|
+
controller.abort(externalSignal.reason ?? 'External abort');
|
|
345
|
+
};
|
|
346
|
+
if (externalSignal.aborted) {
|
|
347
|
+
controller.abort(externalSignal.reason ?? 'External abort');
|
|
1281
348
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
return true;
|
|
1285
|
-
}
|
|
349
|
+
else {
|
|
350
|
+
externalSignal.addEventListener('abort', externalAbortHandler);
|
|
1286
351
|
}
|
|
1287
352
|
}
|
|
1288
|
-
return
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
}
|
|
1293
|
-
async deleteSession(id) {
|
|
1294
|
-
await this.sessionStore.delete(id);
|
|
1295
|
-
}
|
|
1296
|
-
getAgent(id) {
|
|
1297
|
-
return this.agents.get(id);
|
|
1298
|
-
}
|
|
1299
|
-
getAllAgents() {
|
|
1300
|
-
return Array.from(this.agents.values());
|
|
1301
|
-
}
|
|
1302
|
-
normalizeSessionMessage(message) {
|
|
1303
|
-
return normalizeModelMessage(message);
|
|
1304
|
-
}
|
|
1305
|
-
normalizeSessionHistory(messages) {
|
|
1306
|
-
const normalized = [];
|
|
1307
|
-
for (const message of messages) {
|
|
1308
|
-
const next = this.normalizeSessionMessage(message);
|
|
1309
|
-
if (next) {
|
|
1310
|
-
normalized.push(next);
|
|
353
|
+
return () => {
|
|
354
|
+
controller.signal.removeEventListener('abort', abortHandler);
|
|
355
|
+
if (externalSignal && externalAbortHandler) {
|
|
356
|
+
externalSignal.removeEventListener('abort', externalAbortHandler);
|
|
1311
357
|
}
|
|
1312
|
-
}
|
|
1313
|
-
return normalized;
|
|
1314
|
-
}
|
|
1315
|
-
appendSessionMessage(session, message) {
|
|
1316
|
-
const normalized = this.normalizeSessionMessage(message);
|
|
1317
|
-
if (!normalized) {
|
|
1318
|
-
return;
|
|
1319
|
-
}
|
|
1320
|
-
session.messages.push(normalized);
|
|
1321
|
-
this.touchSession(session);
|
|
1322
|
-
}
|
|
1323
|
-
appendSessionMessages(session, messages) {
|
|
1324
|
-
for (const message of messages) {
|
|
1325
|
-
this.appendSessionMessage(session, message);
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
touchSession(session) {
|
|
1329
|
-
const now = new Date();
|
|
1330
|
-
session.updatedAt = now;
|
|
1331
|
-
if (session.metadata) {
|
|
1332
|
-
session.metadata.lastActiveAt = now;
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
getSessionTurn(session) {
|
|
1336
|
-
const value = session.workingMemory[this.runtimeSessionTurnKey];
|
|
1337
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
1338
|
-
}
|
|
1339
|
-
bumpSessionTurn(session) {
|
|
1340
|
-
const next = this.getSessionTurn(session) + 1;
|
|
1341
|
-
session.workingMemory[this.runtimeSessionTurnKey] = next;
|
|
1342
|
-
this.touchSession(session);
|
|
1343
|
-
return next;
|
|
1344
|
-
}
|
|
1345
|
-
async saveSessionCheckpoint(session) {
|
|
1346
|
-
this.touchSession(session);
|
|
1347
|
-
await this._sessionStore.save(session);
|
|
1348
|
-
}
|
|
1349
|
-
shouldCheckpointAfterPart(part) {
|
|
1350
|
-
return this.checkpointEventTypes.has(part.type);
|
|
1351
|
-
}
|
|
1352
|
-
recordRuntimeEvent(context, part) {
|
|
1353
|
-
this.sessionEventManager.recordRuntimeEvent(context, part);
|
|
1354
|
-
}
|
|
1355
|
-
buildToolIdempotencyKey(context, toolName, toolCallId) {
|
|
1356
|
-
return `${context.session.id}:${context.agentId}:${context.stepCount}:${toolName}:${toolCallId}`;
|
|
1357
|
-
}
|
|
1358
|
-
withToolExecutionMetadata(options, context, toolName, toolCallId, idempotencyKey) {
|
|
1359
|
-
const baseOptions = isRecord(options) ? options : {};
|
|
1360
|
-
const existingContext = isRecord(baseOptions.experimental_context)
|
|
1361
|
-
? baseOptions.experimental_context
|
|
1362
|
-
: {};
|
|
1363
|
-
return {
|
|
1364
|
-
...baseOptions,
|
|
1365
|
-
toolCallId,
|
|
1366
|
-
experimental_context: {
|
|
1367
|
-
...existingContext,
|
|
1368
|
-
session: context.session,
|
|
1369
|
-
sessionId: context.session.id,
|
|
1370
|
-
agentId: context.agentId,
|
|
1371
|
-
step: context.stepCount,
|
|
1372
|
-
turn: this.getSessionTurn(context.session),
|
|
1373
|
-
toolName,
|
|
1374
|
-
toolCallId,
|
|
1375
|
-
idempotencyKey,
|
|
1376
|
-
},
|
|
1377
358
|
};
|
|
1378
359
|
}
|
|
1379
|
-
buildProcessorContext(context, abortSignal) {
|
|
1380
|
-
return {
|
|
1381
|
-
session: context.session,
|
|
1382
|
-
agentId: context.agentId,
|
|
1383
|
-
toolCallHistory: context.toolCallHistory,
|
|
1384
|
-
abortSignal,
|
|
1385
|
-
};
|
|
1386
|
-
}
|
|
1387
|
-
getAgentOutputProcessors(agent) {
|
|
1388
|
-
return [
|
|
1389
|
-
...this.outputProcessors,
|
|
1390
|
-
...(agent.outputProcessors ?? []),
|
|
1391
|
-
];
|
|
1392
|
-
}
|
|
1393
|
-
applyRedactionsToText(text) {
|
|
1394
|
-
if (!this.outputRedactions || this.outputRedactions.length === 0)
|
|
1395
|
-
return text;
|
|
1396
|
-
let out = text;
|
|
1397
|
-
for (const r of this.outputRedactions) {
|
|
1398
|
-
out = out.replace(r.re, r.replacement);
|
|
1399
|
-
}
|
|
1400
|
-
return out;
|
|
1401
|
-
}
|
|
1402
|
-
async runOutputProcessing(agent, context, text) {
|
|
1403
|
-
const processors = this.getAgentOutputProcessors(agent);
|
|
1404
|
-
let cur = text;
|
|
1405
|
-
if (processors.length > 0) {
|
|
1406
|
-
const abortSignal = this.getActiveAbortController(context.session.id)?.signal;
|
|
1407
|
-
const outcome = await runOutputProcessors({
|
|
1408
|
-
processors,
|
|
1409
|
-
text: cur,
|
|
1410
|
-
messages: context.session.messages,
|
|
1411
|
-
context: this.buildProcessorContext(context, abortSignal),
|
|
1412
|
-
});
|
|
1413
|
-
if (outcome.blocked) {
|
|
1414
|
-
const msg = this.applyRedactionsToText(outcome.message);
|
|
1415
|
-
return {
|
|
1416
|
-
text: msg,
|
|
1417
|
-
tripwire: { processorId: outcome.processorId, reason: outcome.reason, message: msg },
|
|
1418
|
-
};
|
|
1419
|
-
}
|
|
1420
|
-
cur = outcome.text;
|
|
1421
|
-
}
|
|
1422
|
-
cur = this.applyRedactionsToText(cur);
|
|
1423
|
-
return { text: cur };
|
|
1424
|
-
}
|
|
1425
|
-
async postProcessPersistedAssistantMessages(agent, context, startIndex) {
|
|
1426
|
-
const tripwires = [];
|
|
1427
|
-
const msgs = context.session.messages;
|
|
1428
|
-
for (let i = startIndex; i < msgs.length; i++) {
|
|
1429
|
-
const m = msgs[i];
|
|
1430
|
-
if (!m || m.role !== 'assistant' || typeof m.content !== 'string')
|
|
1431
|
-
continue;
|
|
1432
|
-
const res = await this.runOutputProcessing(agent, context, m.content);
|
|
1433
|
-
msgs[i] = { ...m, content: res.text };
|
|
1434
|
-
if (res.tripwire) {
|
|
1435
|
-
tripwires.push(res.tripwire);
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
return tripwires;
|
|
1439
|
-
}
|
|
1440
|
-
async *emit(context, part) {
|
|
1441
|
-
// Defense-in-depth redaction of streamed assistant output.
|
|
1442
|
-
if (this.outputRedactions && this.outputRedactions.length > 0) {
|
|
1443
|
-
const sessionId = context.session.id;
|
|
1444
|
-
if (part.type === 'text-delta') {
|
|
1445
|
-
const next = this.applyOutputRedactions(context.session, part.text, false);
|
|
1446
|
-
if (next) {
|
|
1447
|
-
const redacted = { ...part, text: next };
|
|
1448
|
-
yield* this.emitWithHooks(context, redacted);
|
|
1449
|
-
}
|
|
1450
|
-
return;
|
|
1451
|
-
}
|
|
1452
|
-
if (part.type === 'turn-end' || part.type === 'done') {
|
|
1453
|
-
const flushed = this.applyOutputRedactions(context.session, '', true);
|
|
1454
|
-
if (flushed) {
|
|
1455
|
-
const carryPart = { type: 'text-delta', text: flushed };
|
|
1456
|
-
yield* this.emitWithHooks(context, carryPart);
|
|
1457
|
-
}
|
|
1458
|
-
delete context.session.workingMemory[this.redactCarryKey];
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
yield* this.emitWithHooks(context, part);
|
|
1462
|
-
}
|
|
1463
|
-
async *emitWithHooks(context, part) {
|
|
1464
|
-
this.recordRuntimeEvent(context, part);
|
|
1465
|
-
await this.hookRunner.onStreamPart(context, part);
|
|
1466
|
-
if (this.shouldCheckpointAfterPart(part)) {
|
|
1467
|
-
await this.saveSessionCheckpoint(context.session);
|
|
1468
|
-
}
|
|
1469
|
-
yield part;
|
|
1470
|
-
}
|
|
1471
|
-
applyOutputRedactions(session, text, flush) {
|
|
1472
|
-
if (!this.outputRedactions || this.outputRedactions.length === 0)
|
|
1473
|
-
return text;
|
|
1474
|
-
const carry = typeof session.workingMemory[this.redactCarryKey] === 'string'
|
|
1475
|
-
? session.workingMemory[this.redactCarryKey]
|
|
1476
|
-
: '';
|
|
1477
|
-
let combined = `${carry}${text}`;
|
|
1478
|
-
for (const r of this.outputRedactions) {
|
|
1479
|
-
combined = combined.replace(r.re, r.replacement);
|
|
1480
|
-
}
|
|
1481
|
-
const keep = flush ? 0 : this.redactLookbehind;
|
|
1482
|
-
if (keep === 0) {
|
|
1483
|
-
session.workingMemory[this.redactCarryKey] = '';
|
|
1484
|
-
return combined;
|
|
1485
|
-
}
|
|
1486
|
-
if (combined.length <= keep) {
|
|
1487
|
-
session.workingMemory[this.redactCarryKey] = combined;
|
|
1488
|
-
return '';
|
|
1489
|
-
}
|
|
1490
|
-
const out = combined.slice(0, combined.length - keep);
|
|
1491
|
-
session.workingMemory[this.redactCarryKey] = combined.slice(-keep);
|
|
1492
|
-
return out;
|
|
1493
|
-
}
|
|
1494
360
|
}
|
|
1495
361
|
export function createRuntime(config) {
|
|
1496
362
|
return new Runtime(config);
|