@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
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentExecuteStage — Stage 4 of the turn pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Absorbs the full runLoop from Runtime: handoff loop, flow agent execution,
|
|
5
|
+
* LLM agent execution (structured triage, streamText, tool loop),
|
|
6
|
+
* tool enforcement, output processing, result handling, and handoff delegation.
|
|
7
|
+
*/
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
import { streamText, streamObject, stepCountIs } from 'ai';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { runContextGatherStage, emitGatherEvents } from './ContextGatherStage.js';
|
|
12
|
+
import { runContextAssembleStage, buildStructuredTriagePrompt } from './ContextAssembleStage.js';
|
|
13
|
+
import { isFlowAgent, isTriageAgent } from './agentTypeGuards.js';
|
|
14
|
+
import { getFlowState, setFlowState, clearFlowState, updateFlowStateSnapshot, appendSessionMessage, appendSessionMessages, } from './sessionUtils.js';
|
|
15
|
+
import { runOutputProcessing, postProcessPersistedAssistantMessages, getAgentOutputProcessors, } from './outputProcessing.js';
|
|
16
|
+
import { checkStopConditions } from '../../guards/StopConditions.js';
|
|
17
|
+
import { createHandoffTool, isHandoffResult } from '../../tools/handoff.js';
|
|
18
|
+
import { isFinalResult } from '../../tools/final.js';
|
|
19
|
+
import { runInputProcessors } from '../../processors/ProcessorRunner.js';
|
|
20
|
+
import { getChunkArgs, getChunkErrorMessage, getChunkResult, getChunkToolCallId } from '../../utils/streamChunk.js';
|
|
21
|
+
import { isRecord } from '../../utils/isRecord.js';
|
|
22
|
+
import { estimateTokenCount } from '../ContextBudget.js';
|
|
23
|
+
/**
|
|
24
|
+
* Runs the full agent execution loop (Stage 4).
|
|
25
|
+
* Handles handoffs, flow agents, LLM agents, structured triage, tool loops.
|
|
26
|
+
*/
|
|
27
|
+
export async function* runAgentExecuteStage(execInput, services) {
|
|
28
|
+
const { context, injectionQueue, abortController } = execInput;
|
|
29
|
+
let currentInput = execInput.input;
|
|
30
|
+
const abortSignal = abortController?.signal;
|
|
31
|
+
let interruptionEmitted = false;
|
|
32
|
+
let circularRecoveryActive = false;
|
|
33
|
+
let circularRecoveryAttempts = 0;
|
|
34
|
+
while (context.handoffStack.length < services.maxHandoffs) {
|
|
35
|
+
if (abortSignal?.aborted) {
|
|
36
|
+
if (!interruptionEmitted) {
|
|
37
|
+
interruptionEmitted = true;
|
|
38
|
+
yield* services.streamEmitter.emit(context, {
|
|
39
|
+
type: 'interrupted',
|
|
40
|
+
sessionId: context.session.id,
|
|
41
|
+
reason: abortSignal.reason ?? 'Operation cancelled',
|
|
42
|
+
timestamp: new Date(),
|
|
43
|
+
lastAgentId: context.agentId,
|
|
44
|
+
lastStep: context.stepCount,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Circular handoff detection
|
|
50
|
+
if (services.agentStateController.isCircularHandoff(context.handoffStack, context.agentId) && !circularRecoveryActive) {
|
|
51
|
+
const err = `Circular handoff detected: ${context.handoffStack.join(' -> ')} -> ${context.agentId}`;
|
|
52
|
+
yield* services.streamEmitter.emit(context, { type: 'error', error: err });
|
|
53
|
+
circularRecoveryAttempts += 1;
|
|
54
|
+
if (circularRecoveryAttempts > 1) {
|
|
55
|
+
yield* emitCircularHandoffFallback(context, services);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
circularRecoveryActive = true;
|
|
59
|
+
context.handoffStack = [];
|
|
60
|
+
}
|
|
61
|
+
context.handoffStack.push(context.agentId);
|
|
62
|
+
const agent = services.agents.get(context.agentId);
|
|
63
|
+
if (!agent) {
|
|
64
|
+
yield* services.streamEmitter.emit(context, { type: 'error', error: `Agent "${context.agentId}" not found` });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Agent-specific input processors
|
|
68
|
+
if (agent.inputProcessors && agent.inputProcessors.length > 0) {
|
|
69
|
+
const procCtx = {
|
|
70
|
+
session: context.session,
|
|
71
|
+
agentId: agent.id,
|
|
72
|
+
toolCallHistory: context.toolCallHistory,
|
|
73
|
+
abortSignal,
|
|
74
|
+
};
|
|
75
|
+
const outcome = await runInputProcessors({
|
|
76
|
+
processors: agent.inputProcessors,
|
|
77
|
+
input: currentInput,
|
|
78
|
+
messages: context.session.messages,
|
|
79
|
+
context: procCtx,
|
|
80
|
+
});
|
|
81
|
+
if (outcome.blocked) {
|
|
82
|
+
yield* services.streamEmitter.emit(context, {
|
|
83
|
+
type: 'tripwire',
|
|
84
|
+
phase: 'input',
|
|
85
|
+
processorId: outcome.processorId,
|
|
86
|
+
reason: outcome.reason,
|
|
87
|
+
message: outcome.message,
|
|
88
|
+
});
|
|
89
|
+
yield* services.streamEmitter.emit(context, { type: 'text-delta', text: outcome.message });
|
|
90
|
+
yield* services.streamEmitter.emit(context, { type: 'turn-end' });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (outcome.input !== currentInput) {
|
|
94
|
+
currentInput = outcome.input;
|
|
95
|
+
const last = context.session.messages[context.session.messages.length - 1];
|
|
96
|
+
if (last && last.role === 'user' && typeof last.content === 'string') {
|
|
97
|
+
last.content = currentInput;
|
|
98
|
+
services.conversationState.touchSession(context.session);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
yield* services.streamEmitter.emit(context, { type: 'agent-start', agentId: agent.id });
|
|
103
|
+
await services.hookRunner.onAgentStart(context, agent.id);
|
|
104
|
+
// Stage 2: Context Gather
|
|
105
|
+
const gatherResult = await runContextGatherStage(agent, context, currentInput, services, abortSignal);
|
|
106
|
+
yield* emitGatherEvents(gatherResult.autoRetrieveEvents, context, services);
|
|
107
|
+
const { extractionSnapshot, activeRoutes } = gatherResult;
|
|
108
|
+
// Stage 3: Context Assemble
|
|
109
|
+
const turnToolErrors = [];
|
|
110
|
+
const assembleGen = runContextAssembleStage({ agent, context, injectionQueue, gatherResult, currentInput, circularRecoveryActive }, services);
|
|
111
|
+
let assembleNext = await assembleGen.next();
|
|
112
|
+
while (!assembleNext.done) {
|
|
113
|
+
yield assembleNext.value;
|
|
114
|
+
assembleNext = await assembleGen.next();
|
|
115
|
+
}
|
|
116
|
+
let system = assembleNext.value.systemPrompt;
|
|
117
|
+
const handoffCandidates = circularRecoveryActive ? [] : getHandoffCandidates(agent, services, activeRoutes);
|
|
118
|
+
const handoffTool = handoffCandidates.length > 0
|
|
119
|
+
? createHandoffTool(handoffCandidates, context.agentId)
|
|
120
|
+
: null;
|
|
121
|
+
let handoffTo = null;
|
|
122
|
+
let handoffReason = 'No reason provided';
|
|
123
|
+
if (isFlowAgent(agent)) {
|
|
124
|
+
yield* runFlowAgentStep(agent, context, currentInput, system, handoffTool, abortSignal, services, turnToolErrors, interruptionEmitted, (to, reason) => { handoffTo = to; handoffReason = reason ?? 'No reason provided'; }, (v) => { interruptionEmitted = v; });
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const result = yield* runLLMAgentSteps(agent, context, currentInput, system, handoffTool, abortSignal, services, turnToolErrors, interruptionEmitted, activeRoutes, (to, reason) => { handoffTo = to; handoffReason = reason ?? 'No reason provided'; }, (v) => { interruptionEmitted = v; });
|
|
128
|
+
if (result === 'return')
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
yield* services.streamEmitter.emit(context, { type: 'agent-end', agentId: agent.id });
|
|
132
|
+
await services.hookRunner.onAgentEnd(context, agent.id);
|
|
133
|
+
if (circularRecoveryActive && handoffTo) {
|
|
134
|
+
yield* services.streamEmitter.emit(context, {
|
|
135
|
+
type: 'error',
|
|
136
|
+
error: `Circular handoff recovery failed: attempted handoff ${context.agentId} -> ${handoffTo}`,
|
|
137
|
+
});
|
|
138
|
+
yield* emitCircularHandoffFallback(context, services);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (circularRecoveryActive) {
|
|
142
|
+
circularRecoveryActive = false;
|
|
143
|
+
}
|
|
144
|
+
if (!handoffTo)
|
|
145
|
+
break;
|
|
146
|
+
await services.hookRunner.onHandoff(context, context.agentId, handoffTo, handoffReason);
|
|
147
|
+
yield* services.streamEmitter.emit(context, {
|
|
148
|
+
type: 'handoff',
|
|
149
|
+
from: context.agentId,
|
|
150
|
+
to: handoffTo,
|
|
151
|
+
reason: handoffReason,
|
|
152
|
+
});
|
|
153
|
+
services.agentStateController.recordHandoff({
|
|
154
|
+
session: context.session,
|
|
155
|
+
fromAgentId: context.agentId,
|
|
156
|
+
toAgentId: handoffTo,
|
|
157
|
+
reason: handoffReason,
|
|
158
|
+
});
|
|
159
|
+
// Apply handoff input filter if configured
|
|
160
|
+
const handoffAgent = services.agents.get(context.agentId);
|
|
161
|
+
const agentRoutes = handoffAgent?.routes;
|
|
162
|
+
const matchedRoute = agentRoutes?.find((r) => r.agentId === handoffTo);
|
|
163
|
+
if (matchedRoute?.inputFilter) {
|
|
164
|
+
try {
|
|
165
|
+
const filterInput = {
|
|
166
|
+
messages: [...context.session.messages],
|
|
167
|
+
workingMemory: { ...context.session.workingMemory },
|
|
168
|
+
sourceAgentId: context.agentId,
|
|
169
|
+
targetAgentId: handoffTo,
|
|
170
|
+
reason: handoffReason,
|
|
171
|
+
};
|
|
172
|
+
const filterResult = await matchedRoute.inputFilter(filterInput);
|
|
173
|
+
context.session.messages = filterResult.messages;
|
|
174
|
+
const internalState = {};
|
|
175
|
+
const internalKeys = [
|
|
176
|
+
'runtimeEventLog',
|
|
177
|
+
'__ariaSessionTurn',
|
|
178
|
+
'__ariaRedactCarry',
|
|
179
|
+
'flowStateByAgent',
|
|
180
|
+
'__ariaAssistantText',
|
|
181
|
+
'__ariaContextBudget',
|
|
182
|
+
];
|
|
183
|
+
for (const key of internalKeys) {
|
|
184
|
+
if (key in context.session.workingMemory) {
|
|
185
|
+
internalState[key] = context.session.workingMemory[key];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
context.session.workingMemory = { ...filterResult.workingMemory, ...internalState };
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
console.warn('[AriaFlow] Handoff input filter failed, using full context:', err);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
services.agentStateController.setActiveAgent(context.session, handoffTo);
|
|
195
|
+
await services.eventLog.checkpoint(context.session);
|
|
196
|
+
if (services.sessionCache) {
|
|
197
|
+
services.sessionCache.put(context.session);
|
|
198
|
+
}
|
|
199
|
+
context.agentId = handoffTo;
|
|
200
|
+
}
|
|
201
|
+
if (context.handoffStack.length >= services.maxHandoffs) {
|
|
202
|
+
yield* services.streamEmitter.emit(context, { type: 'error', error: `Maximum handoffs (${services.maxHandoffs}) exceeded` });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// --- Flow agent step ---
|
|
206
|
+
async function* runFlowAgentStep(agent, context, currentInput, system, handoffTool, abortSignal, services, turnToolErrors, interruptionEmitted, onHandoff, setInterruptionEmitted) {
|
|
207
|
+
const stopResult = checkStopConditions(context, services.stopConditions);
|
|
208
|
+
if (stopResult.shouldStop) {
|
|
209
|
+
yield* services.streamEmitter.emit(context, { type: 'error', error: `Stopped: ${stopResult.reason}` });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const agentMaxSteps = agent.maxSteps ?? agent.maxTurns ?? services.maxSteps;
|
|
213
|
+
if (agentMaxSteps <= 0) {
|
|
214
|
+
yield* services.streamEmitter.emit(context, { type: 'error', error: `Max steps exceeded for agent "${agent.id}"` });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
context.stepCount += 1;
|
|
218
|
+
yield* services.streamEmitter.emit(context, { type: 'step-start', step: context.stepCount, agentId: agent.id });
|
|
219
|
+
await services.hookRunner.onStepStart(context, context.stepCount);
|
|
220
|
+
const toolCalls = [];
|
|
221
|
+
let handoffTo = null;
|
|
222
|
+
let handoffReason = 'No reason provided';
|
|
223
|
+
try {
|
|
224
|
+
const helpers = buildFlowExecutorHelpers(services);
|
|
225
|
+
for await (const part of services.flowExecutor.runFlowAgent(agent, context, currentInput, system, handoffTool ?? undefined, (target, reason) => {
|
|
226
|
+
handoffTo = target;
|
|
227
|
+
handoffReason = reason ?? 'No reason provided';
|
|
228
|
+
onHandoff(target, reason);
|
|
229
|
+
}, toolCalls, helpers, abortSignal)) {
|
|
230
|
+
yield part;
|
|
231
|
+
}
|
|
232
|
+
context.consecutiveErrors = 0;
|
|
233
|
+
if (context.session.metadata) {
|
|
234
|
+
context.session.metadata.totalSteps += 1;
|
|
235
|
+
}
|
|
236
|
+
yield* services.streamEmitter.emit(context, { type: 'step-end', step: context.stepCount, agentId: agent.id });
|
|
237
|
+
await services.hookRunner.onStepEnd(context, context.stepCount, {
|
|
238
|
+
toolCalls,
|
|
239
|
+
finishReason: 'flow',
|
|
240
|
+
tokensUsed: 0,
|
|
241
|
+
handoffTo: handoffTo ?? undefined,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (abortSignal?.aborted) {
|
|
246
|
+
if (!interruptionEmitted) {
|
|
247
|
+
setInterruptionEmitted(true);
|
|
248
|
+
yield* services.streamEmitter.emit(context, {
|
|
249
|
+
type: 'interrupted',
|
|
250
|
+
sessionId: context.session.id,
|
|
251
|
+
reason: abortSignal.reason ?? 'Operation cancelled',
|
|
252
|
+
timestamp: new Date(),
|
|
253
|
+
lastAgentId: context.agentId,
|
|
254
|
+
lastStep: context.stepCount,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
context.consecutiveErrors += 1;
|
|
260
|
+
await services.hookRunner.onError(context, error);
|
|
261
|
+
yield* services.streamEmitter.emit(context, { type: 'error', error: error.message });
|
|
262
|
+
if (context.consecutiveErrors >= 3) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// --- LLM agent step loop ---
|
|
268
|
+
async function* runLLMAgentSteps(agent, context, currentInput, system, handoffTool, abortSignal, services, turnToolErrors, interruptionEmitted, activeRoutes, onHandoff, setInterruptionEmitted) {
|
|
269
|
+
const tools = wrapToolsWithEnforcement(context, handoffTool ? { ...agent.tools, handoff: handoffTool } : { ...agent.tools }, services);
|
|
270
|
+
let agentSteps = 0;
|
|
271
|
+
const agentMaxSteps = agent.maxSteps ?? agent.maxTurns ?? services.maxSteps;
|
|
272
|
+
let handoffTo = null;
|
|
273
|
+
let handoffReason = 'No reason provided';
|
|
274
|
+
while (agentSteps < agentMaxSteps) {
|
|
275
|
+
const stopResult = checkStopConditions(context, services.stopConditions);
|
|
276
|
+
if (stopResult.shouldStop) {
|
|
277
|
+
yield* services.streamEmitter.emit(context, { type: 'error', error: `Stopped: ${stopResult.reason}` });
|
|
278
|
+
return 'return';
|
|
279
|
+
}
|
|
280
|
+
agentSteps += 1;
|
|
281
|
+
context.stepCount += 1;
|
|
282
|
+
yield* services.streamEmitter.emit(context, { type: 'step-start', step: context.stepCount, agentId: agent.id });
|
|
283
|
+
await services.hookRunner.onStepStart(context, context.stepCount);
|
|
284
|
+
try {
|
|
285
|
+
const model = agent.model ?? services.defaultModel;
|
|
286
|
+
if (!model) {
|
|
287
|
+
throw new Error(`Agent "${agent.id}" is missing a model`);
|
|
288
|
+
}
|
|
289
|
+
// Structured triage
|
|
290
|
+
if (isTriageAgent(agent) && agent.triageMode === 'structured') {
|
|
291
|
+
const triageResult = yield* runStructuredTriage(agent, context, system, abortSignal, services, activeRoutes);
|
|
292
|
+
handoffTo = triageResult.handoffTo;
|
|
293
|
+
handoffReason = triageResult.handoffReason;
|
|
294
|
+
onHandoff(handoffTo, handoffReason);
|
|
295
|
+
yield* services.streamEmitter.emit(context, { type: 'step-end', step: context.stepCount, agentId: agent.id });
|
|
296
|
+
await services.hookRunner.onStepEnd(context, context.stepCount, {
|
|
297
|
+
toolCalls: [],
|
|
298
|
+
finishReason: 'handoff',
|
|
299
|
+
tokensUsed: 0,
|
|
300
|
+
handoffTo: handoffTo ?? undefined,
|
|
301
|
+
});
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
// onBeforeModelCall hook
|
|
305
|
+
if (services.config.hooks?.onBeforeModelCall) {
|
|
306
|
+
system = await runBeforeModelCallHook(agent, context, system, services);
|
|
307
|
+
}
|
|
308
|
+
const result = streamText({
|
|
309
|
+
model: model,
|
|
310
|
+
system,
|
|
311
|
+
messages: context.session.messages,
|
|
312
|
+
tools,
|
|
313
|
+
abortSignal,
|
|
314
|
+
stopWhen: stepCountIs(agent.toolMaxSteps ?? 5),
|
|
315
|
+
experimental_context: {
|
|
316
|
+
session: context.session,
|
|
317
|
+
agentId: agent.id,
|
|
318
|
+
},
|
|
319
|
+
experimental_telemetry: agent.telemetry ?? services.config.telemetry,
|
|
320
|
+
});
|
|
321
|
+
const toolCalls = [];
|
|
322
|
+
let finalResult = null;
|
|
323
|
+
let finalEmitted = false;
|
|
324
|
+
const outputProcessors = getAgentOutputProcessors(agent, services.outputProcessors);
|
|
325
|
+
const bufferOutput = services.outputProcessorMode === 'buffer' && outputProcessors.length > 0;
|
|
326
|
+
const bufferedParts = [];
|
|
327
|
+
let stoppedByGuard = false;
|
|
328
|
+
// Process stream chunks
|
|
329
|
+
for await (const chunk of result.fullStream) {
|
|
330
|
+
const chunkResult = yield* processStreamChunk(chunk, context, agent, services, toolCalls, turnToolErrors, finalResult, finalEmitted, bufferOutput, bufferedParts);
|
|
331
|
+
if (chunkResult.finalResult)
|
|
332
|
+
finalResult = chunkResult.finalResult;
|
|
333
|
+
if (chunkResult.finalEmitted)
|
|
334
|
+
finalEmitted = chunkResult.finalEmitted;
|
|
335
|
+
if (chunkResult.handoffTo) {
|
|
336
|
+
handoffTo = chunkResult.handoffTo;
|
|
337
|
+
handoffReason = chunkResult.handoffReason ?? 'No reason provided';
|
|
338
|
+
onHandoff(handoffTo, handoffReason);
|
|
339
|
+
}
|
|
340
|
+
if (chunkResult.stoppedByGuard) {
|
|
341
|
+
stoppedByGuard = true;
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (stoppedByGuard)
|
|
346
|
+
return 'return';
|
|
347
|
+
const response = await result.response;
|
|
348
|
+
const finishReason = finalResult ? 'final' : await result.finishReason;
|
|
349
|
+
// Finalize buffered output
|
|
350
|
+
if (finalResult || (bufferOutput && finishReason !== 'tool-calls') || turnToolErrors.length > 0) {
|
|
351
|
+
let rawText = finalResult ? finalResult.text : bufferedParts.join('');
|
|
352
|
+
if (turnToolErrors.length > 0) {
|
|
353
|
+
const failedTools = turnToolErrors.map(e => `\`${e.toolName}\``).join(', ');
|
|
354
|
+
rawText = `I encountered an error while using the following tools: ${failedTools}. I cannot proceed with the requested action at this time.`;
|
|
355
|
+
yield* services.streamEmitter.emit(context, {
|
|
356
|
+
type: 'error',
|
|
357
|
+
error: `Turn blocked by critical tool failures: ${failedTools}`,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
const getAbortSignal = () => abortSignal;
|
|
361
|
+
const processed = await runOutputProcessing(agent, context, rawText, services.outputProcessors, services.outputRedactions, getAbortSignal);
|
|
362
|
+
if (processed.tripwire) {
|
|
363
|
+
yield* services.streamEmitter.emit(context, {
|
|
364
|
+
type: 'tripwire',
|
|
365
|
+
phase: 'output',
|
|
366
|
+
processorId: processed.tripwire.processorId,
|
|
367
|
+
reason: processed.tripwire.reason,
|
|
368
|
+
message: processed.tripwire.message,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
if (bufferOutput || turnToolErrors.length > 0) {
|
|
372
|
+
yield* services.streamEmitter.emit(context, { type: 'text-delta', text: processed.text });
|
|
373
|
+
}
|
|
374
|
+
appendSessionMessage(context.session, { role: 'assistant', content: processed.text }, services.conversationState);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
const beforeLen = context.session.messages.length;
|
|
378
|
+
appendSessionMessages(context.session, response.messages, services.conversationState);
|
|
379
|
+
const getAbortSignal = () => abortSignal;
|
|
380
|
+
const tripwires = await postProcessPersistedAssistantMessages(agent, context, beforeLen, services.outputProcessors, services.outputRedactions, getAbortSignal);
|
|
381
|
+
for (const t of tripwires) {
|
|
382
|
+
yield* services.streamEmitter.emit(context, {
|
|
383
|
+
type: 'tripwire',
|
|
384
|
+
phase: 'output',
|
|
385
|
+
processorId: t.processorId,
|
|
386
|
+
reason: t.reason,
|
|
387
|
+
message: t.message,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const usage = await result.usage;
|
|
392
|
+
const totalTokens = usage.totalTokens ?? 0;
|
|
393
|
+
context.totalTokens += totalTokens;
|
|
394
|
+
if (context.session.metadata) {
|
|
395
|
+
context.session.metadata.totalTokens += totalTokens;
|
|
396
|
+
context.session.metadata.totalSteps += 1;
|
|
397
|
+
}
|
|
398
|
+
context.consecutiveErrors = 0;
|
|
399
|
+
yield* services.streamEmitter.emit(context, { type: 'step-end', step: context.stepCount, agentId: agent.id });
|
|
400
|
+
await services.hookRunner.onStepEnd(context, context.stepCount, {
|
|
401
|
+
toolCalls,
|
|
402
|
+
finishReason,
|
|
403
|
+
tokensUsed: totalTokens,
|
|
404
|
+
handoffTo: handoffTo ?? undefined,
|
|
405
|
+
});
|
|
406
|
+
if (finalResult || finishReason !== 'tool-calls' || handoffTo) {
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
console.error('RUNTIME_LOOP_ERROR:', error);
|
|
412
|
+
if (abortSignal?.aborted) {
|
|
413
|
+
if (!interruptionEmitted) {
|
|
414
|
+
setInterruptionEmitted(true);
|
|
415
|
+
yield* services.streamEmitter.emit(context, {
|
|
416
|
+
type: 'interrupted',
|
|
417
|
+
sessionId: context.session.id,
|
|
418
|
+
reason: abortSignal.reason ?? 'Operation cancelled',
|
|
419
|
+
timestamp: new Date(),
|
|
420
|
+
lastAgentId: context.agentId,
|
|
421
|
+
lastStep: context.stepCount,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return 'return';
|
|
425
|
+
}
|
|
426
|
+
context.consecutiveErrors += 1;
|
|
427
|
+
await services.hookRunner.onError(context, error);
|
|
428
|
+
yield* services.streamEmitter.emit(context, { type: 'error', error: error.message });
|
|
429
|
+
if (context.consecutiveErrors >= 3) {
|
|
430
|
+
return 'return';
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return 'break';
|
|
435
|
+
}
|
|
436
|
+
async function* processStreamChunk(chunk, context, agent, services, toolCalls, turnToolErrors, finalResult, finalEmitted, bufferOutput, bufferedParts) {
|
|
437
|
+
const result = {};
|
|
438
|
+
if (chunk.type === 'text-delta') {
|
|
439
|
+
if (finalResult)
|
|
440
|
+
return result;
|
|
441
|
+
if (turnToolErrors.length > 0)
|
|
442
|
+
return result;
|
|
443
|
+
if (bufferOutput) {
|
|
444
|
+
bufferedParts.push(chunk.text);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
yield* services.streamEmitter.emit(context, { type: 'text-delta', text: chunk.text });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (chunk.type === 'tool-call') {
|
|
451
|
+
const args = getChunkArgs(chunk);
|
|
452
|
+
const idempotencyKey = services.toolExecutor.buildIdempotencyKey({
|
|
453
|
+
sessionId: context.session.id,
|
|
454
|
+
agentId: context.agentId,
|
|
455
|
+
step: context.stepCount,
|
|
456
|
+
toolName: chunk.toolName,
|
|
457
|
+
toolCallId: chunk.toolCallId,
|
|
458
|
+
});
|
|
459
|
+
const callRecord = {
|
|
460
|
+
toolCallId: chunk.toolCallId,
|
|
461
|
+
toolName: chunk.toolName,
|
|
462
|
+
args,
|
|
463
|
+
idempotencyKey,
|
|
464
|
+
success: true,
|
|
465
|
+
timestamp: Date.now(),
|
|
466
|
+
};
|
|
467
|
+
toolCalls.push(callRecord);
|
|
468
|
+
await services.hookRunner.onToolCall(context, callRecord);
|
|
469
|
+
const toolConfig = services.agents.get(context.agentId)?.tools;
|
|
470
|
+
const toolDef = toolConfig?.[chunk.toolName];
|
|
471
|
+
const filler = toolDef?.filler ?? `Let me check ${chunk.toolName}...`;
|
|
472
|
+
yield* services.streamEmitter.emit(context, {
|
|
473
|
+
type: 'tool-start',
|
|
474
|
+
toolCallId: chunk.toolCallId,
|
|
475
|
+
toolName: chunk.toolName,
|
|
476
|
+
message: filler,
|
|
477
|
+
});
|
|
478
|
+
yield* services.streamEmitter.emit(context, {
|
|
479
|
+
type: 'tool-call',
|
|
480
|
+
toolCallId: chunk.toolCallId,
|
|
481
|
+
toolName: chunk.toolName,
|
|
482
|
+
args,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
if (chunk.type === 'tool-error') {
|
|
486
|
+
const errText = getChunkErrorMessage(chunk);
|
|
487
|
+
const args = getChunkArgs(chunk);
|
|
488
|
+
const toolCallId = getChunkToolCallId(chunk);
|
|
489
|
+
const callRecord = toolCalls.find(call => call.toolCallId === toolCallId);
|
|
490
|
+
if (callRecord) {
|
|
491
|
+
callRecord.success = false;
|
|
492
|
+
callRecord.error = new Error(errText);
|
|
493
|
+
if (args !== undefined)
|
|
494
|
+
callRecord.args = args;
|
|
495
|
+
context.toolCallHistory.push(callRecord);
|
|
496
|
+
await services.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
497
|
+
const toolPolicy = agent.toolPolicies?.[callRecord.toolName];
|
|
498
|
+
const toolDef = agent.tools?.[callRecord.toolName];
|
|
499
|
+
const isCritical = toolPolicy?.critical ?? toolDef?.critical ?? true;
|
|
500
|
+
if (isCritical) {
|
|
501
|
+
turnToolErrors.push(callRecord);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
yield* services.streamEmitter.emit(context, {
|
|
505
|
+
type: 'tool-error',
|
|
506
|
+
toolCallId: toolCallId ?? chunk.toolCallId,
|
|
507
|
+
toolName: chunk.toolName,
|
|
508
|
+
error: errText,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
if (chunk.type === 'tool-result') {
|
|
512
|
+
const startTime = toolCalls.find(call => call.toolCallId === chunk.toolCallId)?.timestamp ?? Date.now();
|
|
513
|
+
const toolResult = getChunkResult(chunk);
|
|
514
|
+
const callRecord = toolCalls.find(call => call.toolCallId === chunk.toolCallId);
|
|
515
|
+
if (callRecord) {
|
|
516
|
+
callRecord.result = toolResult;
|
|
517
|
+
callRecord.durationMs = Date.now() - callRecord.timestamp;
|
|
518
|
+
context.toolCallHistory.push(callRecord);
|
|
519
|
+
await services.hookRunner.onToolResult(context, callRecord);
|
|
520
|
+
}
|
|
521
|
+
const durationMs = Date.now() - startTime;
|
|
522
|
+
yield* services.streamEmitter.emit(context, {
|
|
523
|
+
type: 'tool-done',
|
|
524
|
+
toolCallId: chunk.toolCallId,
|
|
525
|
+
toolName: chunk.toolName,
|
|
526
|
+
durationMs,
|
|
527
|
+
});
|
|
528
|
+
if (callRecord) {
|
|
529
|
+
const enforcement = await services.enforcer.checkResult(callRecord, {
|
|
530
|
+
previousCalls: context.toolCallHistory,
|
|
531
|
+
currentStep: context.stepCount,
|
|
532
|
+
sessionState: context.session.state ?? {},
|
|
533
|
+
});
|
|
534
|
+
if (!enforcement.allowed) {
|
|
535
|
+
const reason = enforcement.reason ?? 'Tool result blocked by enforcement';
|
|
536
|
+
callRecord.success = false;
|
|
537
|
+
callRecord.error = new Error(reason);
|
|
538
|
+
await services.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
539
|
+
turnToolErrors.push(callRecord);
|
|
540
|
+
yield* services.streamEmitter.emit(context, {
|
|
541
|
+
type: 'tool-error',
|
|
542
|
+
toolCallId: chunk.toolCallId,
|
|
543
|
+
toolName: chunk.toolName,
|
|
544
|
+
error: reason,
|
|
545
|
+
});
|
|
546
|
+
result.finalResult = { type: 'final', text: reason };
|
|
547
|
+
if (!finalEmitted) {
|
|
548
|
+
result.finalEmitted = true;
|
|
549
|
+
if (bufferOutput) {
|
|
550
|
+
bufferedParts.push(reason);
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
yield* services.streamEmitter.emit(context, { type: 'text-delta', text: reason });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return result;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
yield* services.streamEmitter.emit(context, {
|
|
560
|
+
type: 'tool-result',
|
|
561
|
+
toolCallId: chunk.toolCallId,
|
|
562
|
+
toolName: chunk.toolName,
|
|
563
|
+
result: toolResult,
|
|
564
|
+
});
|
|
565
|
+
const stopCheck = checkStopConditions(context, services.stopConditions);
|
|
566
|
+
if (stopCheck.shouldStop) {
|
|
567
|
+
yield* services.streamEmitter.emit(context, { type: 'error', error: `Stopped: ${stopCheck.reason}` });
|
|
568
|
+
result.stoppedByGuard = true;
|
|
569
|
+
return result;
|
|
570
|
+
}
|
|
571
|
+
if (isFinalResult(toolResult)) {
|
|
572
|
+
result.finalResult = toolResult;
|
|
573
|
+
if (!finalEmitted) {
|
|
574
|
+
result.finalEmitted = true;
|
|
575
|
+
if (bufferOutput) {
|
|
576
|
+
bufferedParts.push(toolResult.text);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
yield* services.streamEmitter.emit(context, { type: 'text-delta', text: toolResult.text });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return result;
|
|
583
|
+
}
|
|
584
|
+
if (isHandoffResult(toolResult)) {
|
|
585
|
+
const targetAgent = toolResult.targetAgent ?? toolResult.targetAgentId;
|
|
586
|
+
result.handoffTo = targetAgent;
|
|
587
|
+
result.handoffReason = toolResult.reason ?? 'No reason provided';
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return result;
|
|
591
|
+
}
|
|
592
|
+
// --- Structured triage ---
|
|
593
|
+
async function* runStructuredTriage(agent, context, system, abortSignal, services, activeRoutes) {
|
|
594
|
+
const allowed = new Set((activeRoutes ?? agent.routes).map(route => route.agentId));
|
|
595
|
+
// Single-candidate short-circuit
|
|
596
|
+
if (allowed.size === 1) {
|
|
597
|
+
const [onlyAgent] = allowed;
|
|
598
|
+
return { handoffTo: onlyAgent, handoffReason: 'Single active route — direct handoff' };
|
|
599
|
+
}
|
|
600
|
+
const model = agent.model ?? services.defaultModel;
|
|
601
|
+
const schema = z.object({
|
|
602
|
+
agentId: z.string().describe('Target agent ID for this request.'),
|
|
603
|
+
reason: z.string().describe('Short, concrete reason grounded in the user request.'),
|
|
604
|
+
confidence: z.number().min(0).max(1).describe('Routing confidence from 0 to 1.'),
|
|
605
|
+
stayWithCurrent: z.boolean().describe('True only if the current agent is best fit.'),
|
|
606
|
+
});
|
|
607
|
+
const result = streamObject({
|
|
608
|
+
model: model,
|
|
609
|
+
schema,
|
|
610
|
+
system: buildStructuredTriagePrompt(agent, activeRoutes),
|
|
611
|
+
messages: context.session.messages,
|
|
612
|
+
abortSignal,
|
|
613
|
+
experimental_telemetry: agent.telemetry ?? services.config.telemetry,
|
|
614
|
+
});
|
|
615
|
+
let handoffTo;
|
|
616
|
+
let handoffReason = 'Routed by triage';
|
|
617
|
+
let resolved = false;
|
|
618
|
+
for await (const partial of result.partialObjectStream) {
|
|
619
|
+
if (partial.agentId && allowed.has(partial.agentId)) {
|
|
620
|
+
handoffTo = partial.agentId;
|
|
621
|
+
handoffReason = partial.reason ?? 'Routed by triage';
|
|
622
|
+
resolved = true;
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (!resolved) {
|
|
627
|
+
const fullObject = await result.object;
|
|
628
|
+
let target = fullObject.agentId;
|
|
629
|
+
if (!allowed.has(target)) {
|
|
630
|
+
const fallback = agent.defaultAgent ?? agent.routes[0]?.agentId;
|
|
631
|
+
if (fallback && fallback !== agent.id) {
|
|
632
|
+
target = fallback;
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
// No valid target -- respond directly from triage
|
|
636
|
+
handoffTo = agent.id;
|
|
637
|
+
handoffReason = 'No valid routing target. Responding directly.';
|
|
638
|
+
return { handoffTo: handoffTo, handoffReason };
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
handoffTo = target;
|
|
642
|
+
handoffReason = fullObject.reason ?? 'Routed by triage';
|
|
643
|
+
}
|
|
644
|
+
return { handoffTo: handoffTo, handoffReason };
|
|
645
|
+
}
|
|
646
|
+
// --- Tool enforcement wrapper ---
|
|
647
|
+
function wrapToolsWithEnforcement(context, tools, services) {
|
|
648
|
+
const wrapped = {};
|
|
649
|
+
for (const [toolName, toolDef] of Object.entries(tools ?? {})) {
|
|
650
|
+
if (!('execute' in toolDef) || typeof toolDef.execute !== 'function') {
|
|
651
|
+
wrapped[toolName] = toolDef;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const exec = toolDef.execute;
|
|
655
|
+
wrapped[toolName] = {
|
|
656
|
+
...toolDef,
|
|
657
|
+
execute: async (args, options) => {
|
|
658
|
+
const toolCallId = typeof options?.toolCallId === 'string'
|
|
659
|
+
? options.toolCallId
|
|
660
|
+
: crypto.randomUUID();
|
|
661
|
+
const idempotencyKey = services.toolExecutor.buildIdempotencyKey({
|
|
662
|
+
sessionId: context.session.id,
|
|
663
|
+
agentId: context.agentId,
|
|
664
|
+
step: context.stepCount,
|
|
665
|
+
toolName,
|
|
666
|
+
toolCallId,
|
|
667
|
+
});
|
|
668
|
+
const callRecord = {
|
|
669
|
+
toolCallId,
|
|
670
|
+
toolName,
|
|
671
|
+
args,
|
|
672
|
+
idempotencyKey,
|
|
673
|
+
success: true,
|
|
674
|
+
timestamp: Date.now(),
|
|
675
|
+
};
|
|
676
|
+
const enforcement = await services.enforcer.check(callRecord, {
|
|
677
|
+
previousCalls: context.toolCallHistory,
|
|
678
|
+
currentStep: context.stepCount,
|
|
679
|
+
sessionState: context.session.state ?? {},
|
|
680
|
+
});
|
|
681
|
+
if (!enforcement.allowed) {
|
|
682
|
+
const reason = enforcement.reason ?? 'Tool call blocked by enforcement';
|
|
683
|
+
callRecord.success = false;
|
|
684
|
+
callRecord.error = new Error(reason);
|
|
685
|
+
context.toolCallHistory.push(callRecord);
|
|
686
|
+
await services.hookRunner.onToolError(context, callRecord, callRecord.error);
|
|
687
|
+
throw callRecord.error;
|
|
688
|
+
}
|
|
689
|
+
const enrichedOptions = withToolExecutionMetadata(options, context, toolName, toolCallId, idempotencyKey, services);
|
|
690
|
+
try {
|
|
691
|
+
return await exec(args, enrichedOptions);
|
|
692
|
+
}
|
|
693
|
+
catch (error) {
|
|
694
|
+
console.error(`[Runtime] Tool execution failed for ${toolName}:`, error);
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
return wrapped;
|
|
701
|
+
}
|
|
702
|
+
function withToolExecutionMetadata(options, context, toolName, toolCallId, idempotencyKey, services) {
|
|
703
|
+
const baseOptions = options ?? {};
|
|
704
|
+
const existingContext = isRecord(baseOptions.experimental_context)
|
|
705
|
+
? baseOptions.experimental_context
|
|
706
|
+
: {};
|
|
707
|
+
return {
|
|
708
|
+
...baseOptions,
|
|
709
|
+
toolCallId,
|
|
710
|
+
experimental_context: {
|
|
711
|
+
...existingContext,
|
|
712
|
+
session: context.session,
|
|
713
|
+
sessionId: context.session.id,
|
|
714
|
+
agentId: context.agentId,
|
|
715
|
+
step: context.stepCount,
|
|
716
|
+
turn: services.conversationState.getSessionTurn(context.session),
|
|
717
|
+
toolName,
|
|
718
|
+
toolCallId,
|
|
719
|
+
idempotencyKey,
|
|
720
|
+
memoryService: services.memoryService,
|
|
721
|
+
},
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
// --- Handoff candidates ---
|
|
725
|
+
function getHandoffCandidates(agent, services, activeRoutes) {
|
|
726
|
+
if (isTriageAgent(agent)) {
|
|
727
|
+
const routes = activeRoutes ?? agent.routes;
|
|
728
|
+
return routes
|
|
729
|
+
.map(route => services.agents.get(route.agentId))
|
|
730
|
+
.filter((candidate) => Boolean(candidate));
|
|
731
|
+
}
|
|
732
|
+
const targets = agent.canHandoffTo;
|
|
733
|
+
if (!targets || targets.length === 0)
|
|
734
|
+
return [];
|
|
735
|
+
return targets
|
|
736
|
+
.map(id => services.agents.get(id))
|
|
737
|
+
.filter((candidate) => Boolean(candidate));
|
|
738
|
+
}
|
|
739
|
+
// --- Circular handoff fallback ---
|
|
740
|
+
function getCircularHandoffFallbackMessage(services) {
|
|
741
|
+
const configured = services.config.circularHandoffFallbackMessage?.trim();
|
|
742
|
+
if (configured)
|
|
743
|
+
return configured;
|
|
744
|
+
return 'I hit a routing issue between agents. I will continue here. Tell me the exact outcome you need in one sentence.';
|
|
745
|
+
}
|
|
746
|
+
async function* emitCircularHandoffFallback(context, services) {
|
|
747
|
+
const fallback = getCircularHandoffFallbackMessage(services);
|
|
748
|
+
appendSessionMessage(context.session, { role: 'assistant', content: fallback }, services.conversationState);
|
|
749
|
+
yield* services.streamEmitter.emit(context, { type: 'text-delta', text: fallback });
|
|
750
|
+
}
|
|
751
|
+
// --- onBeforeModelCall hook ---
|
|
752
|
+
async function runBeforeModelCallHook(agent, context, system, services) {
|
|
753
|
+
try {
|
|
754
|
+
const messageTokens = context.session.messages.reduce((sum, m) => {
|
|
755
|
+
const txt = typeof m.content === 'string'
|
|
756
|
+
? m.content
|
|
757
|
+
: Array.isArray(m.content)
|
|
758
|
+
? m.content
|
|
759
|
+
.filter((p) => p.type === 'text')
|
|
760
|
+
.map((p) => p.text)
|
|
761
|
+
.join('')
|
|
762
|
+
: '';
|
|
763
|
+
return sum + estimateTokenCount(txt);
|
|
764
|
+
}, 0);
|
|
765
|
+
const systemTokens = estimateTokenCount(system);
|
|
766
|
+
const budgetData = (context.session.workingMemory['__ariaContextBudget'] ?? {});
|
|
767
|
+
const allocations = (budgetData.allocations ?? {});
|
|
768
|
+
const hookData = {
|
|
769
|
+
systemPrompt: system,
|
|
770
|
+
messages: context.session.messages,
|
|
771
|
+
estimatedTokens: systemTokens + messageTokens,
|
|
772
|
+
agentId: agent.id,
|
|
773
|
+
tokenBreakdown: {
|
|
774
|
+
basePrompt: allocations.basePrompt ?? 0,
|
|
775
|
+
autoRetrieve: allocations.autoRetrieve ?? 0,
|
|
776
|
+
workingMemory: allocations.workingMemory ?? 0,
|
|
777
|
+
extraction: 0,
|
|
778
|
+
longTermMemory: allocations.longTermMemory ?? 0,
|
|
779
|
+
policyInjections: allocations.policyInjections ?? 0,
|
|
780
|
+
messageHistory: messageTokens,
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
const hookResult = await services.config.hooks.onBeforeModelCall(context, hookData);
|
|
784
|
+
if (hookResult?.systemPrompt !== undefined) {
|
|
785
|
+
system = hookResult.systemPrompt;
|
|
786
|
+
}
|
|
787
|
+
if (hookResult?.messages !== undefined) {
|
|
788
|
+
context.session.messages = hookResult.messages;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
catch (err) {
|
|
792
|
+
console.warn('[AriaFlow] onBeforeModelCall hook failed, using originals:', err);
|
|
793
|
+
}
|
|
794
|
+
return system;
|
|
795
|
+
}
|
|
796
|
+
// --- FlowExecutorHelpers builder ---
|
|
797
|
+
function matchesDetourRule(input, patterns) {
|
|
798
|
+
if (!patterns || patterns.length === 0)
|
|
799
|
+
return false;
|
|
800
|
+
for (const pattern of patterns) {
|
|
801
|
+
try {
|
|
802
|
+
const regex = new RegExp(pattern, 'i');
|
|
803
|
+
if (regex.test(input))
|
|
804
|
+
return true;
|
|
805
|
+
}
|
|
806
|
+
catch {
|
|
807
|
+
if (input.includes(pattern.toLowerCase()))
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
function buildFlowExecutorHelpers(services) {
|
|
814
|
+
return {
|
|
815
|
+
emit: (ctx, part) => services.streamEmitter.emit(ctx, part),
|
|
816
|
+
runOutputProcessing: (agent, ctx, text) => runOutputProcessing(agent, ctx, text, services.outputProcessors, services.outputRedactions),
|
|
817
|
+
appendSessionMessage: (s, m) => appendSessionMessage(s, m, services.conversationState),
|
|
818
|
+
appendSessionMessages: (s, ms) => appendSessionMessages(s, ms, services.conversationState),
|
|
819
|
+
postProcessPersistedAssistantMessages: (agent, ctx, start) => postProcessPersistedAssistantMessages(agent, ctx, start, services.outputProcessors, services.outputRedactions),
|
|
820
|
+
getAgentOutputProcessors: (agent) => getAgentOutputProcessors(agent, services.outputProcessors),
|
|
821
|
+
getSessionTurn: (s) => services.conversationState.getSessionTurn(s),
|
|
822
|
+
buildToolIdempotencyKey: (ctx, tn, tcid) => services.toolExecutor.buildIdempotencyKey({
|
|
823
|
+
sessionId: ctx.session.id,
|
|
824
|
+
agentId: ctx.agentId,
|
|
825
|
+
step: ctx.stepCount,
|
|
826
|
+
toolName: tn,
|
|
827
|
+
toolCallId: tcid,
|
|
828
|
+
}),
|
|
829
|
+
setFlowState: (s, id, st) => setFlowState(s, id, st, services.conversationState),
|
|
830
|
+
getFlowState: (s, id) => getFlowState(s, id),
|
|
831
|
+
updateFlowStateSnapshot: (s, id, st) => updateFlowStateSnapshot(s, id, st),
|
|
832
|
+
clearFlowState: (s, id) => clearFlowState(s, id, services.conversationState),
|
|
833
|
+
buildFlowWithHandoff: (a, ht, suppress) => buildFlowWithHandoff(a, ht, suppress),
|
|
834
|
+
getFlowNode: (a, nid) => a.flow.nodes.find(node => node.id === nid),
|
|
835
|
+
touchSession: (s) => services.conversationState.touchSession(s),
|
|
836
|
+
matchesDetourRule: (i, p) => matchesDetourRule(i, p),
|
|
837
|
+
enforcecheck: (call, ctx) => services.enforcer.check(call, {
|
|
838
|
+
previousCalls: ctx.toolCallHistory,
|
|
839
|
+
currentStep: ctx.stepCount,
|
|
840
|
+
sessionState: ctx.session.state ?? {},
|
|
841
|
+
}),
|
|
842
|
+
enforcecheckResult: (call, ctx) => services.enforcer.checkResult(call, {
|
|
843
|
+
previousCalls: ctx.toolCallHistory,
|
|
844
|
+
currentStep: ctx.stepCount,
|
|
845
|
+
sessionState: ctx.session.state ?? {},
|
|
846
|
+
}),
|
|
847
|
+
onToolCallHook: (ctx, call) => services.hookRunner.onToolCall(ctx, call),
|
|
848
|
+
onToolResultHook: (ctx, call) => services.hookRunner.onToolResult(ctx, call),
|
|
849
|
+
onToolErrorHook: (ctx, call, error) => services.hookRunner.onToolError(ctx, call, error),
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
// --- buildFlowWithHandoff (moved from Runtime) ---
|
|
853
|
+
function buildFlowWithHandoff(agent, handoffTool, suppressAutoRespond) {
|
|
854
|
+
const needsInitialSuppression = suppressAutoRespond &&
|
|
855
|
+
agent.flow.nodes.some(n => n.id === agent.initialNode && n.autoRespond === undefined);
|
|
856
|
+
if (!handoffTool && !needsInitialSuppression)
|
|
857
|
+
return agent.flow;
|
|
858
|
+
return {
|
|
859
|
+
...agent.flow,
|
|
860
|
+
nodes: agent.flow.nodes.map(node => {
|
|
861
|
+
const shouldSuppress = suppressAutoRespond
|
|
862
|
+
&& node.id === agent.initialNode
|
|
863
|
+
&& node.autoRespond === undefined;
|
|
864
|
+
const existingTools = node.tools;
|
|
865
|
+
if (typeof existingTools === 'function') {
|
|
866
|
+
return {
|
|
867
|
+
...node,
|
|
868
|
+
tools: (ctx) => {
|
|
869
|
+
const resolved = existingTools(ctx) ?? {};
|
|
870
|
+
if (!handoffTool)
|
|
871
|
+
return resolved;
|
|
872
|
+
return resolved.handoff ? resolved : { ...resolved, handoff: handoffTool };
|
|
873
|
+
},
|
|
874
|
+
...(shouldSuppress ? { autoRespond: false } : {}),
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
const toolSet = existingTools ?? {};
|
|
878
|
+
if (!handoffTool || toolSet.handoff) {
|
|
879
|
+
return shouldSuppress ? { ...node, autoRespond: false } : node;
|
|
880
|
+
}
|
|
881
|
+
return {
|
|
882
|
+
...node,
|
|
883
|
+
tools: { ...toolSet, handoff: handoffTool },
|
|
884
|
+
...(shouldSuppress ? { autoRespond: false } : {}),
|
|
885
|
+
};
|
|
886
|
+
}),
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
//# sourceMappingURL=AgentExecuteStage.js.map
|