@ariaflowagents/core 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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/flows/FlowManager.d.ts +8 -0
- package/dist/flows/FlowManager.d.ts.map +1 -1
- package/dist/flows/FlowManager.js +32 -2
- package/dist/flows/FlowManager.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 +2 -0
- package/dist/hooks/HookRunner.d.ts.map +1 -1
- package/dist/hooks/HookRunner.js +4 -0
- package/dist/hooks/HookRunner.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- 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/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/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/FlowExecutor.d.ts +16 -11
- package/dist/runtime/FlowExecutor.d.ts.map +1 -1
- package/dist/runtime/FlowExecutor.js +32 -138
- package/dist/runtime/FlowExecutor.js.map +1 -1
- package/dist/runtime/Runtime.d.ts +31 -78
- package/dist/runtime/Runtime.d.ts.map +1 -1
- package/dist/runtime/Runtime.js +225 -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 +889 -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 +177 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/guides/AGENTS.md +173 -0
- package/guides/README.md +12 -0
- package/guides/TOOLS.md +93 -27
- package/package.json +12 -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,177 @@ 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';
|
|
24
22
|
export class Runtime {
|
|
25
23
|
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
24
|
abortControllers = new Map();
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
runtimeEventLogMaxEntries = 2000;
|
|
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
|
-
}
|
|
25
|
+
_userIdWarned = new Set();
|
|
26
|
+
sessionMutex = new SessionMutex();
|
|
27
|
+
turnServices;
|
|
28
|
+
// Keep references for public API getters
|
|
29
|
+
_sessionStore;
|
|
30
|
+
_memoryService;
|
|
31
|
+
_toolExecutor;
|
|
32
|
+
_conversationState;
|
|
33
|
+
_eventLog;
|
|
34
|
+
_agentStateController;
|
|
110
35
|
constructor(config) {
|
|
111
36
|
this.config = config;
|
|
37
|
+
// Build agent map
|
|
38
|
+
const agents = new Map();
|
|
112
39
|
for (const agent of config.agents) {
|
|
113
|
-
|
|
40
|
+
agents.set(agent.id, agent);
|
|
41
|
+
}
|
|
42
|
+
// Config scalars
|
|
43
|
+
const defaultAgentId = config.defaultAgentId;
|
|
44
|
+
const defaultModel = config.defaultModel;
|
|
45
|
+
const maxSteps = config.maxSteps ?? 20;
|
|
46
|
+
const maxHandoffs = config.maxHandoffs ?? 10;
|
|
47
|
+
const stopConditions = config.stopConditions ?? defaultStopConditions;
|
|
48
|
+
const sessionStore = config.sessionStore ?? new MemoryStore();
|
|
49
|
+
const inputProcessors = config.inputProcessors ?? [];
|
|
50
|
+
const outputProcessors = config.outputProcessors ?? [];
|
|
51
|
+
const outputProcessorMode = config.outputProcessorMode ?? 'stream';
|
|
52
|
+
const alwaysRouteThroughTriage = config.alwaysRouteThroughTriage ?? false;
|
|
53
|
+
const triageAgentId = config.triageAgentId;
|
|
54
|
+
// Output redactions
|
|
55
|
+
let outputRedactions;
|
|
56
|
+
if (config.outputRedaction && config.outputRedaction.length > 0) {
|
|
57
|
+
outputRedactions = config.outputRedaction.map(r => {
|
|
58
|
+
const base = compileSanitizePattern(r.pattern);
|
|
59
|
+
const flags = base.flags.includes('g') ? base.flags : `${base.flags}g`;
|
|
60
|
+
const re = new RegExp(base.source, flags);
|
|
61
|
+
return { re, replacement: r.replacement };
|
|
62
|
+
});
|
|
114
63
|
}
|
|
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();
|
|
64
|
+
// Hooks setup
|
|
121
65
|
const hooks = { ...config.hooks };
|
|
122
66
|
const appendOnStreamPart = (fn) => {
|
|
123
67
|
const originalHook = hooks.onStreamPart;
|
|
124
68
|
hooks.onStreamPart = async (context, part) => {
|
|
125
|
-
if (originalHook)
|
|
69
|
+
if (originalHook)
|
|
126
70
|
await originalHook(context, part);
|
|
127
|
-
}
|
|
128
71
|
await fn(context, part);
|
|
129
72
|
};
|
|
130
73
|
};
|
|
131
74
|
if (config.callback) {
|
|
132
|
-
|
|
133
|
-
appendOnStreamPart(httpCallback);
|
|
75
|
+
appendOnStreamPart(createHttpCallback(config.callback));
|
|
134
76
|
}
|
|
135
77
|
if (config.streamCallback) {
|
|
136
78
|
const adapter = createStreamCallbackAdapter(config.streamCallback);
|
|
137
79
|
appendOnStreamPart(adapter.onStreamPart);
|
|
138
80
|
const originalOnEnd = hooks.onEnd;
|
|
139
81
|
hooks.onEnd = async (context, result) => {
|
|
140
|
-
if (originalOnEnd)
|
|
82
|
+
if (originalOnEnd)
|
|
141
83
|
await originalOnEnd(context, result);
|
|
142
|
-
}
|
|
143
84
|
if (config.streamCallback?.flushOnEnd) {
|
|
144
85
|
await adapter.flush(config.streamCallback.flushTimeoutMs ?? 2000);
|
|
145
86
|
}
|
|
146
87
|
};
|
|
147
88
|
}
|
|
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,
|
|
89
|
+
const hookRunner = new HookRunner(hooks);
|
|
90
|
+
const enforcer = new ToolEnforcer(config.enforcementRules ?? defaultEnforcementRules);
|
|
91
|
+
const contextManager = config.contextManager;
|
|
92
|
+
// Service instances
|
|
93
|
+
const extractionEngine = new ExtractionEngine({ defaultModel, telemetry: config.telemetry });
|
|
94
|
+
const suggestionManager = new SuggestionManager(config);
|
|
95
|
+
const flowExecutor = new FlowExecutor(config);
|
|
96
|
+
const memoryService = config.memoryService;
|
|
97
|
+
const conversationState = new DefaultConversationState({
|
|
98
|
+
sessionStore,
|
|
99
|
+
defaultAgentId,
|
|
167
100
|
});
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
101
|
+
const eventLog = new DefaultConversationEventLog({ sessionStore });
|
|
102
|
+
const agentStateController = new DefaultAgentStateController();
|
|
103
|
+
const toolExecutor = new DefaultToolExecutor({
|
|
104
|
+
enforcer,
|
|
105
|
+
hookRunner,
|
|
106
|
+
memoryService,
|
|
171
107
|
});
|
|
172
|
-
|
|
173
|
-
|
|
108
|
+
let sessionCache;
|
|
109
|
+
if (config.sessionCache) {
|
|
110
|
+
sessionCache = new SessionCache(config.sessionCache);
|
|
111
|
+
}
|
|
112
|
+
const streamEmitter = new StreamEmitter({
|
|
113
|
+
hookRunner,
|
|
114
|
+
eventLog,
|
|
115
|
+
hasStreamPartHook: Boolean(hooks.onStreamPart),
|
|
116
|
+
outputRedactions,
|
|
117
|
+
redactCarryKey: '__ariaRedactCarry',
|
|
118
|
+
redactLookbehind: 64,
|
|
119
|
+
saveSessionCheckpoint: async (session) => {
|
|
120
|
+
await eventLog.checkpoint(session);
|
|
121
|
+
if (sessionCache)
|
|
122
|
+
sessionCache.put(session);
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
// Store references for public API
|
|
126
|
+
this._sessionStore = sessionStore;
|
|
127
|
+
this._memoryService = memoryService;
|
|
128
|
+
this._toolExecutor = toolExecutor;
|
|
129
|
+
this._conversationState = conversationState;
|
|
130
|
+
this._eventLog = eventLog;
|
|
131
|
+
this._agentStateController = agentStateController;
|
|
132
|
+
// Build TurnServices — one plain struct, no closures
|
|
133
|
+
this.turnServices = {
|
|
134
|
+
conversationState,
|
|
135
|
+
agentStateController,
|
|
136
|
+
eventLog,
|
|
137
|
+
toolExecutor,
|
|
138
|
+
streamEmitter,
|
|
139
|
+
hookRunner,
|
|
140
|
+
enforcer,
|
|
141
|
+
extractionEngine,
|
|
142
|
+
suggestionManager,
|
|
143
|
+
flowExecutor,
|
|
144
|
+
contextManager,
|
|
145
|
+
memoryService,
|
|
146
|
+
sessionCache,
|
|
147
|
+
sessionStore,
|
|
148
|
+
agents,
|
|
149
|
+
config,
|
|
150
|
+
defaultAgentId,
|
|
151
|
+
defaultModel,
|
|
152
|
+
maxSteps,
|
|
153
|
+
maxHandoffs,
|
|
154
|
+
stopConditions,
|
|
155
|
+
inputProcessors,
|
|
156
|
+
outputProcessors,
|
|
157
|
+
outputProcessorMode,
|
|
158
|
+
outputRedactions,
|
|
159
|
+
alwaysRouteThroughTriage,
|
|
160
|
+
triageAgentId,
|
|
161
|
+
};
|
|
174
162
|
}
|
|
163
|
+
// --- Public API ---
|
|
175
164
|
get sessionStore() {
|
|
176
165
|
return this._sessionStore;
|
|
177
166
|
}
|
|
167
|
+
get memoryService() {
|
|
168
|
+
return this._memoryService;
|
|
169
|
+
}
|
|
170
|
+
/** Foundation services — shared primitives reusable by VoiceEngine etc. */
|
|
171
|
+
get foundation() {
|
|
172
|
+
return {
|
|
173
|
+
toolExecutor: this._toolExecutor,
|
|
174
|
+
conversationState: this._conversationState,
|
|
175
|
+
eventLog: this._eventLog,
|
|
176
|
+
agentState: this._agentStateController,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
178
179
|
getActiveAbortController(sessionId) {
|
|
179
180
|
return this.abortControllers.get(sessionId);
|
|
180
181
|
}
|
|
@@ -185,144 +186,84 @@ export class Runtime {
|
|
|
185
186
|
this.abortControllers.delete(sessionId);
|
|
186
187
|
}
|
|
187
188
|
}
|
|
189
|
+
async getSession(id) {
|
|
190
|
+
return this._sessionStore.get(id);
|
|
191
|
+
}
|
|
192
|
+
async deleteSession(id) {
|
|
193
|
+
await this._sessionStore.delete(id);
|
|
194
|
+
}
|
|
195
|
+
getAgent(id) {
|
|
196
|
+
return this.turnServices.agents.get(id);
|
|
197
|
+
}
|
|
198
|
+
getAllAgents() {
|
|
199
|
+
return Array.from(this.turnServices.agents.values());
|
|
200
|
+
}
|
|
201
|
+
async *chat(sessionId, input, userId) {
|
|
202
|
+
yield* this.stream({ input, sessionId, userId });
|
|
203
|
+
}
|
|
204
|
+
// --- Main orchestration ---
|
|
188
205
|
async *stream(options) {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
206
|
+
const svc = this.turnServices;
|
|
207
|
+
// Warn when memoryService is configured but userId is missing
|
|
208
|
+
if (svc.memoryService && !options.userId) {
|
|
209
|
+
const sessionId = options.sessionId ?? 'new';
|
|
210
|
+
if (!this._userIdWarned.has(sessionId)) {
|
|
211
|
+
this._userIdWarned.add(sessionId);
|
|
212
|
+
console.warn('[AriaFlow] memoryService is configured but session has no userId. ' +
|
|
213
|
+
'Memory operations will be skipped. Pass userId via stream({ input, sessionId, userId }).');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Acquire per-session mutex to prevent concurrent read-modify-write races.
|
|
217
|
+
// Only lock when a sessionId is provided (existing session). New sessions
|
|
218
|
+
// get a random UUID and cannot have concurrent callers.
|
|
219
|
+
let releaseMutex;
|
|
196
220
|
if (options.sessionId) {
|
|
197
|
-
|
|
198
|
-
session = existing ?? this.createSession(effectiveSessionId, userId);
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
session = this.createSession(effectiveSessionId, userId);
|
|
221
|
+
releaseMutex = await this.sessionMutex.acquire(options.sessionId);
|
|
202
222
|
}
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
}
|
|
223
|
+
try {
|
|
224
|
+
yield* this._streamInner(options, svc);
|
|
220
225
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
226
|
+
finally {
|
|
227
|
+
releaseMutex?.();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Inner stream implementation — separated so the mutex can wrap the
|
|
232
|
+
* entire async generator lifecycle from IntakeStage through PostStreamStage.
|
|
233
|
+
*/
|
|
234
|
+
async *_streamInner(options, svc) {
|
|
235
|
+
// Stage 1: Intake
|
|
236
|
+
const intakeGen = runIntakeStage(options, svc);
|
|
237
|
+
let intakeNext = await intakeGen.next();
|
|
238
|
+
while (!intakeNext.done) {
|
|
239
|
+
yield intakeNext.value;
|
|
240
|
+
intakeNext = await intakeGen.next();
|
|
241
|
+
}
|
|
242
|
+
const intake = intakeNext.value;
|
|
243
|
+
if (!intake)
|
|
244
|
+
return;
|
|
245
|
+
const { session, context, processedInput, injectionQueue, controller } = intake;
|
|
240
246
|
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;
|
|
247
|
+
const cleanup = this.setupAbortHandlers(session.id, controller, options.abortSignal);
|
|
260
248
|
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
|
-
}
|
|
249
|
+
// Stages 2-4: Agent execute (includes gather, assemble, handoff loop)
|
|
250
|
+
yield* runAgentExecuteStage({
|
|
251
|
+
context,
|
|
252
|
+
input: processedInput,
|
|
253
|
+
injectionQueue,
|
|
254
|
+
abortController: controller,
|
|
255
|
+
}, svc);
|
|
256
|
+
if (svc.config.suggestionModel) {
|
|
257
|
+
yield* svc.suggestionManager.generateSuggestions(context);
|
|
316
258
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
yield* this.generateSuggestions(context);
|
|
259
|
+
if (session.metadata) {
|
|
260
|
+
session.metadata.lastTurnHadToolCalls = context.toolCallHistory.length > 0;
|
|
320
261
|
}
|
|
321
|
-
await
|
|
262
|
+
await svc.hookRunner.onEnd(context, { success: true });
|
|
322
263
|
}
|
|
323
264
|
catch (error) {
|
|
324
265
|
if (controller.signal.aborted) {
|
|
325
|
-
yield*
|
|
266
|
+
yield* svc.streamEmitter.emit(context, {
|
|
326
267
|
type: 'interrupted',
|
|
327
268
|
sessionId: context.session.id,
|
|
328
269
|
reason: controller.signal.reason ?? 'Operation cancelled',
|
|
@@ -330,1167 +271,45 @@ export class Runtime {
|
|
|
330
271
|
lastAgentId: context.agentId,
|
|
331
272
|
lastStep: context.stepCount,
|
|
332
273
|
});
|
|
333
|
-
await
|
|
274
|
+
await svc.hookRunner.onEnd(context, { success: false });
|
|
334
275
|
}
|
|
335
276
|
else {
|
|
336
|
-
await
|
|
337
|
-
await
|
|
338
|
-
yield*
|
|
277
|
+
await svc.hookRunner.onError(context, error);
|
|
278
|
+
await svc.hookRunner.onEnd(context, { success: false, error: error });
|
|
279
|
+
yield* svc.streamEmitter.emit(context, { type: 'error', error: error.message });
|
|
339
280
|
}
|
|
340
281
|
}
|
|
341
282
|
finally {
|
|
342
|
-
|
|
343
|
-
if (abortSignal && externalAbortHandler) {
|
|
344
|
-
abortSignal.removeEventListener('abort', externalAbortHandler);
|
|
345
|
-
}
|
|
283
|
+
cleanup();
|
|
346
284
|
this.abortControllers.delete(session.id);
|
|
347
|
-
|
|
348
|
-
yield* this.emit(context, { type: 'done', sessionId: session.id });
|
|
349
|
-
await this.saveSessionCheckpoint(session);
|
|
285
|
+
yield* runPostStreamStage(context, session, svc);
|
|
350
286
|
}
|
|
351
287
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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;
|
|
974
|
-
}
|
|
975
|
-
return 'I hit a routing issue between agents. I will continue here. Tell me the exact outcome you need in one sentence.';
|
|
976
|
-
}
|
|
977
|
-
async *emitCircularHandoffFallback(context) {
|
|
978
|
-
const fallback = this.getCircularHandoffFallbackMessage();
|
|
979
|
-
this.appendSessionMessage(context.session, { role: 'assistant', content: fallback });
|
|
980
|
-
yield* this.emit(context, { type: 'text-delta', text: fallback });
|
|
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),
|
|
288
|
+
// --- Private helpers ---
|
|
289
|
+
setupAbortHandlers(sessionId, controller, externalSignal) {
|
|
290
|
+
const abortHandler = () => {
|
|
291
|
+
this.abortControllers.delete(sessionId);
|
|
1269
292
|
};
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
if (
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
if (regex.test(input)) {
|
|
1279
|
-
return true;
|
|
1280
|
-
}
|
|
293
|
+
controller.signal.addEventListener('abort', abortHandler);
|
|
294
|
+
let externalAbortHandler;
|
|
295
|
+
if (externalSignal) {
|
|
296
|
+
externalAbortHandler = () => {
|
|
297
|
+
controller.abort(externalSignal.reason ?? 'External abort');
|
|
298
|
+
};
|
|
299
|
+
if (externalSignal.aborted) {
|
|
300
|
+
controller.abort(externalSignal.reason ?? 'External abort');
|
|
1281
301
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
return true;
|
|
1285
|
-
}
|
|
302
|
+
else {
|
|
303
|
+
externalSignal.addEventListener('abort', externalAbortHandler);
|
|
1286
304
|
}
|
|
1287
305
|
}
|
|
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);
|
|
306
|
+
return () => {
|
|
307
|
+
controller.signal.removeEventListener('abort', abortHandler);
|
|
308
|
+
if (externalSignal && externalAbortHandler) {
|
|
309
|
+
externalSignal.removeEventListener('abort', externalAbortHandler);
|
|
1311
310
|
}
|
|
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
311
|
};
|
|
1378
312
|
}
|
|
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
313
|
}
|
|
1495
314
|
export function createRuntime(config) {
|
|
1496
315
|
return new Runtime(config);
|