@falai/agent 0.9.0-alpha-2 → 0.9.2
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 +42 -34
- package/dist/cjs/src/core/Agent.d.ts +48 -44
- package/dist/cjs/src/core/Agent.d.ts.map +1 -1
- package/dist/cjs/src/core/Agent.js +151 -1110
- package/dist/cjs/src/core/Agent.js.map +1 -1
- package/dist/cjs/src/core/ResponseModal.d.ts +211 -0
- package/dist/cjs/src/core/ResponseModal.d.ts.map +1 -0
- package/dist/cjs/src/core/ResponseModal.js +1394 -0
- package/dist/cjs/src/core/ResponseModal.js.map +1 -0
- package/dist/cjs/src/core/ResponsePipeline.d.ts +8 -4
- package/dist/cjs/src/core/ResponsePipeline.d.ts.map +1 -1
- package/dist/cjs/src/core/ResponsePipeline.js +48 -20
- package/dist/cjs/src/core/ResponsePipeline.js.map +1 -1
- package/dist/cjs/src/core/Route.d.ts +12 -5
- package/dist/cjs/src/core/Route.d.ts.map +1 -1
- package/dist/cjs/src/core/Route.js +26 -5
- package/dist/cjs/src/core/Route.js.map +1 -1
- package/dist/cjs/src/core/RoutingEngine.d.ts +5 -0
- package/dist/cjs/src/core/RoutingEngine.d.ts.map +1 -1
- package/dist/cjs/src/core/RoutingEngine.js +37 -25
- package/dist/cjs/src/core/RoutingEngine.js.map +1 -1
- package/dist/cjs/src/core/SessionManager.d.ts +9 -1
- package/dist/cjs/src/core/SessionManager.d.ts.map +1 -1
- package/dist/cjs/src/core/SessionManager.js +27 -5
- package/dist/cjs/src/core/SessionManager.js.map +1 -1
- package/dist/cjs/src/core/Step.d.ts +60 -7
- package/dist/cjs/src/core/Step.d.ts.map +1 -1
- package/dist/cjs/src/core/Step.js +151 -4
- package/dist/cjs/src/core/Step.js.map +1 -1
- package/dist/cjs/src/core/ToolManager.d.ts +234 -0
- package/dist/cjs/src/core/ToolManager.d.ts.map +1 -0
- package/dist/cjs/src/core/ToolManager.js +1117 -0
- package/dist/cjs/src/core/ToolManager.js.map +1 -0
- package/dist/cjs/src/index.d.ts +5 -4
- package/dist/cjs/src/index.d.ts.map +1 -1
- package/dist/cjs/src/index.js +11 -3
- package/dist/cjs/src/index.js.map +1 -1
- package/dist/cjs/src/types/agent.d.ts +2 -1
- package/dist/cjs/src/types/agent.d.ts.map +1 -1
- package/dist/cjs/src/types/ai.d.ts +1 -1
- package/dist/cjs/src/types/ai.d.ts.map +1 -1
- package/dist/cjs/src/types/index.d.ts +3 -2
- package/dist/cjs/src/types/index.d.ts.map +1 -1
- package/dist/cjs/src/types/index.js +3 -1
- package/dist/cjs/src/types/index.js.map +1 -1
- package/dist/cjs/src/types/route.d.ts +6 -4
- package/dist/cjs/src/types/route.d.ts.map +1 -1
- package/dist/cjs/src/types/tool.d.ts +84 -14
- package/dist/cjs/src/types/tool.d.ts.map +1 -1
- package/dist/cjs/src/types/tool.js +13 -0
- package/dist/cjs/src/types/tool.js.map +1 -1
- package/dist/cjs/src/utils/clone.d.ts.map +1 -1
- package/dist/cjs/src/utils/clone.js +0 -4
- package/dist/cjs/src/utils/clone.js.map +1 -1
- package/dist/cjs/src/utils/history.d.ts +30 -1
- package/dist/cjs/src/utils/history.d.ts.map +1 -1
- package/dist/cjs/src/utils/history.js +169 -23
- package/dist/cjs/src/utils/history.js.map +1 -1
- package/dist/cjs/src/utils/index.d.ts +1 -1
- package/dist/cjs/src/utils/index.d.ts.map +1 -1
- package/dist/cjs/src/utils/index.js +5 -1
- package/dist/cjs/src/utils/index.js.map +1 -1
- package/dist/src/core/Agent.d.ts +48 -44
- package/dist/src/core/Agent.d.ts.map +1 -1
- package/dist/src/core/Agent.js +152 -1111
- package/dist/src/core/Agent.js.map +1 -1
- package/dist/src/core/ResponseModal.d.ts +211 -0
- package/dist/src/core/ResponseModal.d.ts.map +1 -0
- package/dist/src/core/ResponseModal.js +1389 -0
- package/dist/src/core/ResponseModal.js.map +1 -0
- package/dist/src/core/ResponsePipeline.d.ts +8 -4
- package/dist/src/core/ResponsePipeline.d.ts.map +1 -1
- package/dist/src/core/ResponsePipeline.js +48 -20
- package/dist/src/core/ResponsePipeline.js.map +1 -1
- package/dist/src/core/Route.d.ts +12 -5
- package/dist/src/core/Route.d.ts.map +1 -1
- package/dist/src/core/Route.js +26 -5
- package/dist/src/core/Route.js.map +1 -1
- package/dist/src/core/RoutingEngine.d.ts +5 -0
- package/dist/src/core/RoutingEngine.d.ts.map +1 -1
- package/dist/src/core/RoutingEngine.js +37 -25
- package/dist/src/core/RoutingEngine.js.map +1 -1
- package/dist/src/core/SessionManager.d.ts +9 -1
- package/dist/src/core/SessionManager.d.ts.map +1 -1
- package/dist/src/core/SessionManager.js +27 -5
- package/dist/src/core/SessionManager.js.map +1 -1
- package/dist/src/core/Step.d.ts +60 -7
- package/dist/src/core/Step.d.ts.map +1 -1
- package/dist/src/core/Step.js +151 -4
- package/dist/src/core/Step.js.map +1 -1
- package/dist/src/core/ToolManager.d.ts +234 -0
- package/dist/src/core/ToolManager.d.ts.map +1 -0
- package/dist/src/core/ToolManager.js +1111 -0
- package/dist/src/core/ToolManager.js.map +1 -0
- package/dist/src/index.d.ts +5 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/types/agent.d.ts +2 -1
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/ai.d.ts +1 -1
- package/dist/src/types/ai.d.ts.map +1 -1
- package/dist/src/types/index.d.ts +3 -2
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/types/index.js +1 -0
- package/dist/src/types/index.js.map +1 -1
- package/dist/src/types/route.d.ts +6 -4
- package/dist/src/types/route.d.ts.map +1 -1
- package/dist/src/types/tool.d.ts +84 -14
- package/dist/src/types/tool.d.ts.map +1 -1
- package/dist/src/types/tool.js +12 -1
- package/dist/src/types/tool.js.map +1 -1
- package/dist/src/utils/clone.d.ts.map +1 -1
- package/dist/src/utils/clone.js +0 -4
- package/dist/src/utils/clone.js.map +1 -1
- package/dist/src/utils/history.d.ts +30 -1
- package/dist/src/utils/history.d.ts.map +1 -1
- package/dist/src/utils/history.js +165 -23
- package/dist/src/utils/history.js.map +1 -1
- package/dist/src/utils/index.d.ts +1 -1
- package/dist/src/utils/index.d.ts.map +1 -1
- package/dist/src/utils/index.js +1 -1
- package/dist/src/utils/index.js.map +1 -1
- package/docs/CONTRIBUTING.md +40 -0
- package/docs/README.md +14 -6
- package/docs/api/README.md +235 -45
- package/docs/api/overview.md +140 -33
- package/docs/core/agent/session-management.md +152 -5
- package/docs/core/ai-integration/response-processing.md +115 -4
- package/docs/core/conversation-flows/routes.md +130 -0
- package/docs/core/error-handling.md +638 -0
- package/docs/core/tools/tool-definition.md +684 -60
- package/docs/core/tools/tool-scoping.md +244 -53
- package/docs/guides/error-handling-patterns.md +578 -0
- package/docs/guides/getting-started/README.md +139 -28
- package/docs/guides/migration/README.md +72 -0
- package/docs/guides/migration/response-modal-refactor.md +518 -0
- package/examples/advanced-patterns/knowledge-based-agent.ts +6 -6
- package/examples/advanced-patterns/persistent-onboarding.ts +30 -43
- package/examples/advanced-patterns/streaming-responses.ts +169 -96
- package/examples/ai-providers/anthropic-integration.ts +9 -5
- package/examples/ai-providers/openai-integration.ts +11 -7
- package/examples/core-concepts/basic-agent.ts +106 -67
- package/examples/core-concepts/modern-streaming-api.ts +309 -0
- package/examples/core-concepts/schema-driven-extraction.ts +10 -7
- package/examples/core-concepts/session-management.ts +71 -18
- package/examples/integrations/healthcare-integration.ts +15 -29
- package/examples/integrations/server-session-management.ts +3 -3
- package/examples/persistence/memory-sessions.ts +3 -3
- package/examples/tools/basic-tools.ts +293 -89
- package/examples/tools/data-enrichment-tools.ts +185 -75
- package/package.json +1 -1
- package/src/core/Agent.ts +190 -1529
- package/src/core/ResponseModal.ts +1798 -0
- package/src/core/ResponsePipeline.ts +83 -57
- package/src/core/Route.ts +39 -12
- package/src/core/RoutingEngine.ts +46 -42
- package/src/core/SessionManager.ts +39 -7
- package/src/core/Step.ts +198 -20
- package/src/core/ToolManager.ts +1394 -0
- package/src/index.ts +19 -3
- package/src/types/agent.ts +2 -1
- package/src/types/ai.ts +1 -1
- package/src/types/index.ts +13 -2
- package/src/types/route.ts +6 -4
- package/src/types/tool.ts +116 -25
- package/src/utils/clone.ts +6 -8
- package/src/utils/history.ts +190 -27
- package/src/utils/index.ts +4 -0
- package/dist/cjs/src/core/ToolExecutor.d.ts +0 -45
- package/dist/cjs/src/core/ToolExecutor.d.ts.map +0 -1
- package/dist/cjs/src/core/ToolExecutor.js +0 -84
- package/dist/cjs/src/core/ToolExecutor.js.map +0 -1
- package/dist/src/core/ToolExecutor.d.ts +0 -45
- package/dist/src/core/ToolExecutor.d.ts.map +0 -1
- package/dist/src/core/ToolExecutor.js +0 -80
- package/dist/src/core/ToolExecutor.js.map +0 -1
- package/docs/core/tools/tool-execution.md +0 -815
- package/src/core/ToolExecutor.ts +0 -126
|
@@ -0,0 +1,1798 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResponseModal handles all response generation logic for the Agent
|
|
3
|
+
* Provides both streaming and non-streaming response generation with unified logic
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
AgentResponse,
|
|
8
|
+
AgentResponseStreamChunk,
|
|
9
|
+
History,
|
|
10
|
+
SessionState,
|
|
11
|
+
StepRef,
|
|
12
|
+
HistoryItem,
|
|
13
|
+
Tool,
|
|
14
|
+
Event,
|
|
15
|
+
ToolEventData,
|
|
16
|
+
AgentStructuredResponse,
|
|
17
|
+
Term,
|
|
18
|
+
} from "../types";
|
|
19
|
+
import { EventKind, MessageRole } from "../types";
|
|
20
|
+
import type { Agent } from "./Agent";
|
|
21
|
+
import type { Route } from "./Route";
|
|
22
|
+
import { Step } from "./Step";
|
|
23
|
+
import { ResponseEngine } from "./ResponseEngine";
|
|
24
|
+
import { ResponsePipeline } from "./ResponsePipeline";
|
|
25
|
+
import { cloneDeep, mergeCollected, enterStep, getLastMessageFromHistory, render, logger, historyToEvents } from "../utils";
|
|
26
|
+
import type { ToolManager } from "./ToolManager";
|
|
27
|
+
import { END_ROUTE_ID } from "../constants";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Configuration options for ResponseModal
|
|
31
|
+
*/
|
|
32
|
+
export interface ResponseModalOptions {
|
|
33
|
+
/** Maximum number of tool loops allowed during response generation */
|
|
34
|
+
maxToolLoops?: number;
|
|
35
|
+
/** Enable automatic session saving after response generation */
|
|
36
|
+
enableAutoSave?: boolean;
|
|
37
|
+
/** Enable debug mode for detailed logging */
|
|
38
|
+
debugMode?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parameters for respond and respondStream methods
|
|
43
|
+
*/
|
|
44
|
+
export interface RespondParams<TContext = unknown, TData = unknown> extends Record<string, unknown> {
|
|
45
|
+
history: History;
|
|
46
|
+
step?: StepRef;
|
|
47
|
+
session?: SessionState<TData>;
|
|
48
|
+
contextOverride?: Partial<TContext>;
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Options for the modern stream() method
|
|
54
|
+
*/
|
|
55
|
+
export interface StreamOptions<TContext = unknown> {
|
|
56
|
+
contextOverride?: Partial<TContext>;
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
history?: History; // Optional: override session history
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Options for the modern generate() method
|
|
63
|
+
*/
|
|
64
|
+
export interface GenerateOptions<TContext = unknown> {
|
|
65
|
+
contextOverride?: Partial<TContext>;
|
|
66
|
+
signal?: AbortSignal;
|
|
67
|
+
history?: History; // Optional: override session history
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Error details for response generation failures
|
|
72
|
+
*/
|
|
73
|
+
interface ResponseGenerationErrorDetails {
|
|
74
|
+
originalError?: unknown;
|
|
75
|
+
params?: Record<string, unknown>;
|
|
76
|
+
phase?: string;
|
|
77
|
+
context?: Record<string, unknown>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Error class for response generation failures
|
|
82
|
+
*/
|
|
83
|
+
export class ResponseGenerationError extends Error {
|
|
84
|
+
constructor(
|
|
85
|
+
message: string,
|
|
86
|
+
public readonly details?: ResponseGenerationErrorDetails
|
|
87
|
+
) {
|
|
88
|
+
super(message);
|
|
89
|
+
this.name = 'ResponseGenerationError';
|
|
90
|
+
|
|
91
|
+
// Preserve stack trace from original error if available
|
|
92
|
+
if (details?.originalError instanceof Error && details.originalError.stack) {
|
|
93
|
+
this.stack = `${this.stack}\nCaused by: ${details.originalError.stack}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create a ResponseGenerationError from an unknown error
|
|
99
|
+
*/
|
|
100
|
+
static fromError(
|
|
101
|
+
error: unknown,
|
|
102
|
+
phase: string,
|
|
103
|
+
params?: Record<string, unknown>,
|
|
104
|
+
context?: Record<string, unknown>
|
|
105
|
+
): ResponseGenerationError {
|
|
106
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
107
|
+
return new ResponseGenerationError(
|
|
108
|
+
`Response generation failed in ${phase}: ${message}`,
|
|
109
|
+
{ originalError: error, params, phase, context }
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if an error is a ResponseGenerationError
|
|
115
|
+
*/
|
|
116
|
+
static isResponseGenerationError(error: unknown): error is ResponseGenerationError {
|
|
117
|
+
return error instanceof ResponseGenerationError;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Common response context used across all response methods
|
|
123
|
+
*/
|
|
124
|
+
interface ResponseContext<TContext = unknown, TData = unknown> {
|
|
125
|
+
effectiveContext: TContext;
|
|
126
|
+
session: SessionState<TData>;
|
|
127
|
+
history: HistoryItem[]; // Keep as HistoryItem[] for external API compatibility
|
|
128
|
+
selectedRoute?: Route<TContext, TData>;
|
|
129
|
+
selectedStep?: Step<TContext, TData>;
|
|
130
|
+
responseDirectives?: string[];
|
|
131
|
+
isRouteComplete: boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* ResponseModal class that encapsulates all response generation logic
|
|
136
|
+
* Uses unified approach for both streaming and non-streaming responses
|
|
137
|
+
*/
|
|
138
|
+
export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
139
|
+
private readonly responseEngine: ResponseEngine<TContext, TData>;
|
|
140
|
+
private readonly responsePipeline: ResponsePipeline<TContext, TData>;
|
|
141
|
+
|
|
142
|
+
constructor(
|
|
143
|
+
private readonly agent: Agent<TContext, TData>,
|
|
144
|
+
private readonly options?: ResponseModalOptions
|
|
145
|
+
) {
|
|
146
|
+
// Initialize response engine
|
|
147
|
+
this.responseEngine = new ResponseEngine<TContext, TData>();
|
|
148
|
+
|
|
149
|
+
// Initialize response pipeline with agent dependencies
|
|
150
|
+
this.responsePipeline = new ResponsePipeline<TContext, TData>(
|
|
151
|
+
this.agent.getAgentOptions(),
|
|
152
|
+
() => this.agent.getRoutes(), // Pass a function to get routes dynamically
|
|
153
|
+
this.agent.getTools(),
|
|
154
|
+
this.agent.getRoutingEngine(),
|
|
155
|
+
this.agent.updateContext.bind(this.agent),
|
|
156
|
+
this.agent.getUpdateDataMethod(),
|
|
157
|
+
this.agent.updateCollectedData.bind(this.agent),
|
|
158
|
+
this.getToolManager()
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Generate a non-streaming response using unified logic
|
|
164
|
+
*/
|
|
165
|
+
async respond(params: RespondParams<TContext, TData>): Promise<AgentResponse<TData>> {
|
|
166
|
+
try {
|
|
167
|
+
// Use unified response preparation and routing
|
|
168
|
+
const responseContext = await this.prepareUnifiedResponseContext(params);
|
|
169
|
+
// Generate response using unified logic
|
|
170
|
+
const result = await this.generateUnifiedResponse(responseContext);
|
|
171
|
+
|
|
172
|
+
// Finalize session
|
|
173
|
+
await this.finalizeSession(result.session!, responseContext.effectiveContext);
|
|
174
|
+
|
|
175
|
+
return result;
|
|
176
|
+
|
|
177
|
+
} catch (error) {
|
|
178
|
+
throw new ResponseGenerationError(
|
|
179
|
+
`Failed to generate response: ${error instanceof Error ? error.message : String(error)}`,
|
|
180
|
+
{ originalError: error, params, phase: 'response_generation' }
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Generate a streaming response using unified logic
|
|
187
|
+
*/
|
|
188
|
+
async *respondStream(params: RespondParams<TContext, TData>): AsyncGenerator<AgentResponseStreamChunk<TData>> {
|
|
189
|
+
try {
|
|
190
|
+
// Use unified response preparation and routing
|
|
191
|
+
const responseContext = await this.prepareUnifiedResponseContext(params);
|
|
192
|
+
|
|
193
|
+
// Generate streaming response using unified logic
|
|
194
|
+
yield* this.generateUnifiedStreamingResponse(responseContext);
|
|
195
|
+
|
|
196
|
+
} catch (error) {
|
|
197
|
+
// Stream error to caller
|
|
198
|
+
yield {
|
|
199
|
+
delta: "",
|
|
200
|
+
accumulated: "",
|
|
201
|
+
done: true,
|
|
202
|
+
session: params.session || await this.agent.session.getOrCreate(),
|
|
203
|
+
error: new ResponseGenerationError(
|
|
204
|
+
`Streaming response failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
205
|
+
{ originalError: error, params, phase: 'streaming' }
|
|
206
|
+
),
|
|
207
|
+
} as AgentResponseStreamChunk<TData>;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Modern streaming API - simple interface like chat()
|
|
213
|
+
*/
|
|
214
|
+
async *stream(
|
|
215
|
+
message?: string,
|
|
216
|
+
options?: StreamOptions<TContext>
|
|
217
|
+
): AsyncGenerator<AgentResponseStreamChunk<TData>> {
|
|
218
|
+
// Determine which history to use
|
|
219
|
+
let history: History;
|
|
220
|
+
if (options?.history) {
|
|
221
|
+
// Use provided history for this response only
|
|
222
|
+
history = options.history;
|
|
223
|
+
} else {
|
|
224
|
+
// Add user message to session history if provided
|
|
225
|
+
if (message) {
|
|
226
|
+
await this.agent.session.addMessage("user", message);
|
|
227
|
+
}
|
|
228
|
+
history = this.agent.session.getHistory();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Get or create session
|
|
232
|
+
let session = await this.agent.session.getOrCreate();
|
|
233
|
+
|
|
234
|
+
// Merge agent's collected data into session (agent data takes precedence)
|
|
235
|
+
const collectedData = this.agent.getCollectedData();
|
|
236
|
+
if (Object.keys(collectedData).length > 0) {
|
|
237
|
+
session = mergeCollected(session, collectedData);
|
|
238
|
+
// Update the session manager with the merged data
|
|
239
|
+
await this.agent.session.setData(collectedData);
|
|
240
|
+
logger.debug("[ResponseModal] Merged agent collected data into stream session:", collectedData);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Stream response using existing respondStream method
|
|
244
|
+
let finalMessage = "";
|
|
245
|
+
for await (const chunk of this.respondStream({
|
|
246
|
+
history,
|
|
247
|
+
session,
|
|
248
|
+
contextOverride: options?.contextOverride,
|
|
249
|
+
signal: options?.signal,
|
|
250
|
+
})) {
|
|
251
|
+
// Accumulate the final message for session history
|
|
252
|
+
if (chunk.done) {
|
|
253
|
+
finalMessage = chunk.accumulated;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
yield chunk;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Add agent response to session history (only if not using override history)
|
|
260
|
+
if (!options?.history && finalMessage) {
|
|
261
|
+
await this.agent.session.addMessage("assistant", finalMessage);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Modern non-streaming API - equivalent to chat() but more explicit
|
|
267
|
+
*/
|
|
268
|
+
async generate(
|
|
269
|
+
message?: string,
|
|
270
|
+
options?: GenerateOptions<TContext>
|
|
271
|
+
): Promise<AgentResponse<TData>> {
|
|
272
|
+
// Determine which history to use
|
|
273
|
+
let history: History;
|
|
274
|
+
if (options?.history) {
|
|
275
|
+
// Use provided history for this response only
|
|
276
|
+
history = options.history;
|
|
277
|
+
} else {
|
|
278
|
+
// Add user message to session history if provided
|
|
279
|
+
if (message) {
|
|
280
|
+
await this.agent.session.addMessage("user", message);
|
|
281
|
+
}
|
|
282
|
+
history = this.agent.session.getHistory();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Get or create session
|
|
286
|
+
let session = await this.agent.session.getOrCreate();
|
|
287
|
+
|
|
288
|
+
// Merge agent's collected data into session (agent data takes precedence)
|
|
289
|
+
const collectedData = this.agent.getCollectedData();
|
|
290
|
+
if (Object.keys(collectedData).length > 0) {
|
|
291
|
+
session = mergeCollected(session, collectedData);
|
|
292
|
+
// Update the session manager with the merged data
|
|
293
|
+
await this.agent.session.setData(collectedData);
|
|
294
|
+
logger.debug("[ResponseModal] Merged agent collected data into generate session:", collectedData);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Generate response using existing respond method
|
|
298
|
+
const result = await this.respond({
|
|
299
|
+
history,
|
|
300
|
+
session,
|
|
301
|
+
contextOverride: options?.contextOverride,
|
|
302
|
+
signal: options?.signal,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Add agent response to session history (only if not using override history)
|
|
306
|
+
if (!options?.history) {
|
|
307
|
+
await this.agent.session.addMessage("assistant", result.message);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Ensure the result includes the current session
|
|
311
|
+
return {
|
|
312
|
+
...result,
|
|
313
|
+
session: result.session || this.agent.session.current,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get the response engine instance
|
|
319
|
+
* @internal
|
|
320
|
+
*/
|
|
321
|
+
getResponseEngine(): ResponseEngine<TContext, TData> {
|
|
322
|
+
return this.responseEngine;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Get the response pipeline instance
|
|
327
|
+
* @internal
|
|
328
|
+
*/
|
|
329
|
+
getResponsePipeline(): ResponsePipeline<TContext, TData> {
|
|
330
|
+
return this.responsePipeline;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get the ToolManager instance from the agent
|
|
335
|
+
* @private
|
|
336
|
+
*/
|
|
337
|
+
private getToolManager(): ToolManager<TContext, TData> | undefined {
|
|
338
|
+
// Check if agent has a tool property (ToolManager)
|
|
339
|
+
if (this.agent && 'tool' in this.agent && this.agent.tool) {
|
|
340
|
+
return this.agent.tool;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Log warning if ToolManager is not available
|
|
344
|
+
logger.warn(`[ResponseModal] ToolManager not available on agent - tool execution will use fallback methods`);
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// UNIFIED RESPONSE LOGIC - Consolidates common logic between streaming and non-streaming
|
|
349
|
+
// ============================================================================
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Unified response preparation - handles context setup, session management, and routing
|
|
353
|
+
* This consolidates common logic between streaming and non-streaming responses
|
|
354
|
+
* @private
|
|
355
|
+
*/
|
|
356
|
+
private async prepareUnifiedResponseContext(params: RespondParams<TContext, TData>): Promise<ResponseContext<TContext, TData>> {
|
|
357
|
+
try {
|
|
358
|
+
const { history: simpleHistory, contextOverride, signal } = params;
|
|
359
|
+
|
|
360
|
+
// Validate input parameters
|
|
361
|
+
if (!simpleHistory) {
|
|
362
|
+
throw new ResponseGenerationError('History is required for response generation', { params, phase: 'validation' });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Convert HistoryItem[] to Event[] for internal processing
|
|
366
|
+
const historyEvents = historyToEvents(simpleHistory);
|
|
367
|
+
// Keep original HistoryItem[] format for external APIs
|
|
368
|
+
const history = simpleHistory;
|
|
369
|
+
|
|
370
|
+
// Use ResponsePipeline for optimized context and session preparation
|
|
371
|
+
// This leverages existing optimizations and avoids code duplication
|
|
372
|
+
let responseContext: {
|
|
373
|
+
effectiveContext: TContext;
|
|
374
|
+
session: SessionState<TData>;
|
|
375
|
+
};
|
|
376
|
+
try {
|
|
377
|
+
// Set current context and session in pipeline for consistency
|
|
378
|
+
this.responsePipeline.setContext(await this.agent.getContext());
|
|
379
|
+
this.responsePipeline.setCurrentSession(this.agent.getCurrentSession());
|
|
380
|
+
|
|
381
|
+
responseContext = await this.responsePipeline.prepareResponseContext({
|
|
382
|
+
contextOverride,
|
|
383
|
+
session: params.session ? cloneDeep(params.session) : undefined,
|
|
384
|
+
});
|
|
385
|
+
} catch (error) {
|
|
386
|
+
throw ResponseGenerationError.fromError(error, 'pipeline_context_preparation', params);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const { effectiveContext } = responseContext;
|
|
390
|
+
let session = responseContext.session;
|
|
391
|
+
|
|
392
|
+
// Update our stored context if it was modified by beforeRespond hook
|
|
393
|
+
const storedContext = this.responsePipeline.getStoredContext();
|
|
394
|
+
if (storedContext !== undefined) {
|
|
395
|
+
try {
|
|
396
|
+
await this.agent.updateContext(storedContext as Partial<TContext>);
|
|
397
|
+
} catch (error) {
|
|
398
|
+
throw ResponseGenerationError.fromError(error, 'context_update_from_pipeline', params, { storedContext });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Merge agent's collected data into session (agent data takes precedence)
|
|
403
|
+
const collectedData = this.agent.getCollectedData();
|
|
404
|
+
if (Object.keys(collectedData).length > 0) {
|
|
405
|
+
try {
|
|
406
|
+
session = mergeCollected(session, collectedData);
|
|
407
|
+
logger.debug("[ResponseModal] Merged agent collected data into session:", collectedData);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
throw ResponseGenerationError.fromError(error, 'data_merging', params, { collectedData });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// PHASE 1: PREPARE - Execute prepare function if current step has one
|
|
414
|
+
try {
|
|
415
|
+
await this.executeStepPrepare(session, effectiveContext);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
throw ResponseGenerationError.fromError(error, 'step_preparation', params, { session, effectiveContext });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// PHASE 2: ROUTING + STEP SELECTION - Determine which route and step to use
|
|
421
|
+
let routingResult: {
|
|
422
|
+
selectedRoute?: Route<TContext, TData>;
|
|
423
|
+
selectedStep?: Step<TContext, TData>;
|
|
424
|
+
responseDirectives?: string[];
|
|
425
|
+
session: SessionState<TData>;
|
|
426
|
+
isRouteComplete: boolean;
|
|
427
|
+
};
|
|
428
|
+
try {
|
|
429
|
+
routingResult = await this.handleUnifiedRoutingAndStepSelection({
|
|
430
|
+
session,
|
|
431
|
+
history: historyEvents,
|
|
432
|
+
context: effectiveContext,
|
|
433
|
+
signal,
|
|
434
|
+
});
|
|
435
|
+
} catch (error) {
|
|
436
|
+
throw ResponseGenerationError.fromError(error, 'routing_and_step_selection', params, { session, effectiveContext });
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
effectiveContext,
|
|
441
|
+
session: routingResult.session,
|
|
442
|
+
history,
|
|
443
|
+
selectedRoute: routingResult.selectedRoute,
|
|
444
|
+
selectedStep: routingResult.selectedStep,
|
|
445
|
+
responseDirectives: routingResult.responseDirectives,
|
|
446
|
+
isRouteComplete: routingResult.isRouteComplete,
|
|
447
|
+
};
|
|
448
|
+
} catch (error) {
|
|
449
|
+
// Re-throw ResponseGenerationError as-is, wrap others
|
|
450
|
+
if (ResponseGenerationError.isResponseGenerationError(error)) {
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
throw ResponseGenerationError.fromError(error, 'preparation', params);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Unified routing and step selection logic using ResponsePipeline for optimization
|
|
459
|
+
* @private
|
|
460
|
+
*/
|
|
461
|
+
private async handleUnifiedRoutingAndStepSelection(params: {
|
|
462
|
+
session: SessionState<TData>;
|
|
463
|
+
history: Event[]; // Use Event[] for internal processing
|
|
464
|
+
context: TContext;
|
|
465
|
+
signal?: AbortSignal;
|
|
466
|
+
}): Promise<{
|
|
467
|
+
selectedRoute?: Route<TContext, TData>;
|
|
468
|
+
selectedStep?: Step<TContext, TData>;
|
|
469
|
+
responseDirectives?: string[];
|
|
470
|
+
session: SessionState<TData>;
|
|
471
|
+
isRouteComplete: boolean;
|
|
472
|
+
}> {
|
|
473
|
+
try {
|
|
474
|
+
// Use the ResponsePipeline for optimized routing and step selection
|
|
475
|
+
// This avoids duplicate logic and leverages existing optimizations
|
|
476
|
+
// ResponsePipeline expects Event[] for history
|
|
477
|
+
const routingResult = await this.responsePipeline.handleRoutingAndStepSelection({
|
|
478
|
+
session: params.session,
|
|
479
|
+
history: params.history, // Already Event[]
|
|
480
|
+
context: params.context,
|
|
481
|
+
signal: params.signal,
|
|
482
|
+
});
|
|
483
|
+
// Determine next step using pipeline method for consistency
|
|
484
|
+
const stepResult = this.responsePipeline.determineNextStep({
|
|
485
|
+
selectedRoute: routingResult.selectedRoute,
|
|
486
|
+
selectedStep: routingResult.selectedStep,
|
|
487
|
+
session: routingResult.session,
|
|
488
|
+
isRouteComplete: routingResult.isRouteComplete,
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
selectedRoute: routingResult.selectedRoute,
|
|
493
|
+
selectedStep: stepResult.nextStep, // Use the determined next step
|
|
494
|
+
responseDirectives: routingResult.responseDirectives,
|
|
495
|
+
session: stepResult.session,
|
|
496
|
+
isRouteComplete: routingResult.isRouteComplete,
|
|
497
|
+
};
|
|
498
|
+
} catch (error) {
|
|
499
|
+
throw ResponseGenerationError.fromError(error, 'routing_optimization', params);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Unified response generation for non-streaming responses
|
|
505
|
+
* @private
|
|
506
|
+
*/
|
|
507
|
+
private async generateUnifiedResponse(
|
|
508
|
+
responseContext: ResponseContext<TContext, TData>
|
|
509
|
+
): Promise<AgentResponse<TData>> {
|
|
510
|
+
const { effectiveContext, session: initialSession, history, selectedRoute, selectedStep, responseDirectives, isRouteComplete } = responseContext;
|
|
511
|
+
let session = initialSession;
|
|
512
|
+
|
|
513
|
+
// Get last user message (needed for both route and completion handling)
|
|
514
|
+
// Convert HistoryItem[] to Event[] for internal processing
|
|
515
|
+
const historyEvents = historyToEvents(history);
|
|
516
|
+
const lastMessageText = getLastMessageFromHistory(historyEvents);
|
|
517
|
+
|
|
518
|
+
let message: string;
|
|
519
|
+
let toolCalls: Array<{ toolName: string; arguments: Record<string, unknown> }> | undefined = undefined;
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
if (selectedRoute && !isRouteComplete) {
|
|
524
|
+
// Handle normal route processing
|
|
525
|
+
|
|
526
|
+
const result = await this.processRouteResponse({
|
|
527
|
+
selectedRoute,
|
|
528
|
+
selectedStep,
|
|
529
|
+
responseDirectives,
|
|
530
|
+
session,
|
|
531
|
+
history,
|
|
532
|
+
context: effectiveContext,
|
|
533
|
+
lastMessageText,
|
|
534
|
+
historyEvents,
|
|
535
|
+
signal: responseContext.history ? undefined : undefined, // TODO: Fix signal passing
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
message = result.message;
|
|
539
|
+
toolCalls = result.toolCalls;
|
|
540
|
+
session = result.session;
|
|
541
|
+
|
|
542
|
+
} else if (isRouteComplete && selectedRoute) {
|
|
543
|
+
// Handle route completion
|
|
544
|
+
|
|
545
|
+
message = await this.handleRouteCompletion({
|
|
546
|
+
selectedRoute,
|
|
547
|
+
session,
|
|
548
|
+
context: effectiveContext,
|
|
549
|
+
lastMessageText,
|
|
550
|
+
historyEvents,
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Set step to END_ROUTE marker
|
|
554
|
+
session = enterStep(session, END_ROUTE_ID, "Route completed");
|
|
555
|
+
logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed. Entered END_ROUTE step.`);
|
|
556
|
+
|
|
557
|
+
} else {
|
|
558
|
+
// Fallback: No routes defined, generate a simple response
|
|
559
|
+
|
|
560
|
+
message = await this.generateFallbackResponse({
|
|
561
|
+
history: historyEvents, // Use Event[] for fallback response
|
|
562
|
+
context: effectiveContext,
|
|
563
|
+
session,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
message,
|
|
569
|
+
session,
|
|
570
|
+
toolCalls,
|
|
571
|
+
isRouteComplete,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Unified streaming response generation
|
|
577
|
+
* @private
|
|
578
|
+
*/
|
|
579
|
+
private async *generateUnifiedStreamingResponse(
|
|
580
|
+
responseContext: ResponseContext<TContext, TData>
|
|
581
|
+
): AsyncGenerator<AgentResponseStreamChunk<TData>> {
|
|
582
|
+
const { effectiveContext, session: initialSession, history, selectedRoute, selectedStep, responseDirectives, isRouteComplete } = responseContext;
|
|
583
|
+
const session = initialSession;
|
|
584
|
+
|
|
585
|
+
// Get last user message (needed for both route and completion handling)
|
|
586
|
+
// Convert HistoryItem[] to Event[] for internal processing
|
|
587
|
+
const historyEvents = historyToEvents(history);
|
|
588
|
+
const lastMessageText = getLastMessageFromHistory(historyEvents);
|
|
589
|
+
|
|
590
|
+
if (selectedRoute && !isRouteComplete) {
|
|
591
|
+
// Handle normal route processing with streaming
|
|
592
|
+
yield* this.processRouteStreamingResponse({
|
|
593
|
+
selectedRoute,
|
|
594
|
+
selectedStep,
|
|
595
|
+
responseDirectives,
|
|
596
|
+
session,
|
|
597
|
+
history,
|
|
598
|
+
context: effectiveContext,
|
|
599
|
+
lastMessageText,
|
|
600
|
+
historyEvents,
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
} else if (isRouteComplete && selectedRoute) {
|
|
604
|
+
// Handle route completion streaming
|
|
605
|
+
yield* this.streamRouteCompletion({
|
|
606
|
+
selectedRoute,
|
|
607
|
+
session,
|
|
608
|
+
context: effectiveContext,
|
|
609
|
+
lastMessageText,
|
|
610
|
+
historyEvents,
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
} else {
|
|
614
|
+
// Fallback: No routes defined, stream a simple response
|
|
615
|
+
yield* this.streamFallbackResponse({
|
|
616
|
+
history: historyEvents, // Use Event[] for fallback response
|
|
617
|
+
context: effectiveContext,
|
|
618
|
+
session,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Execute prepare function for current step if available
|
|
624
|
+
* @private
|
|
625
|
+
*/
|
|
626
|
+
private async executeStepPrepare(session: SessionState<TData>, context: TContext): Promise<void> {
|
|
627
|
+
if (session.currentRoute && session.currentStep) {
|
|
628
|
+
const currentRoute = this.agent.getRoutes().find(
|
|
629
|
+
(r) => r.id === session.currentRoute?.id
|
|
630
|
+
);
|
|
631
|
+
if (currentRoute) {
|
|
632
|
+
const currentStep = currentRoute.getStep(session.currentStep.id);
|
|
633
|
+
if (currentStep?.prepare) {
|
|
634
|
+
logger.debug(`[ResponseModal] Executing prepare for step: ${currentStep.id}`);
|
|
635
|
+
await this.executePrepareFinalize(
|
|
636
|
+
currentStep.prepare,
|
|
637
|
+
context,
|
|
638
|
+
session.data,
|
|
639
|
+
currentRoute,
|
|
640
|
+
currentStep
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Execute finalize function for current step if available
|
|
649
|
+
* @private
|
|
650
|
+
*/
|
|
651
|
+
private async executeStepFinalize(session: SessionState<TData>, context: TContext): Promise<void> {
|
|
652
|
+
if (session.currentRoute && session.currentStep) {
|
|
653
|
+
const currentRoute = this.agent.getRoutes().find(
|
|
654
|
+
(r) => r.id === session.currentRoute?.id
|
|
655
|
+
);
|
|
656
|
+
if (currentRoute) {
|
|
657
|
+
const currentStep = currentRoute.getStep(session.currentStep.id);
|
|
658
|
+
if (currentStep?.finalize) {
|
|
659
|
+
logger.debug(
|
|
660
|
+
`[ResponseModal] Executing finalize for step: ${currentStep.id}`
|
|
661
|
+
);
|
|
662
|
+
await this.executePrepareFinalize(
|
|
663
|
+
currentStep.finalize,
|
|
664
|
+
context,
|
|
665
|
+
session.data,
|
|
666
|
+
currentRoute,
|
|
667
|
+
currentStep
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Process route response with unified tool execution and data collection
|
|
676
|
+
* @private
|
|
677
|
+
*/
|
|
678
|
+
private async processRouteResponse(params: {
|
|
679
|
+
selectedRoute: Route<TContext, TData>;
|
|
680
|
+
selectedStep?: Step<TContext, TData>;
|
|
681
|
+
responseDirectives?: string[];
|
|
682
|
+
session: SessionState<TData>;
|
|
683
|
+
history: HistoryItem[]; // Keep as HistoryItem[] for AI provider compatibility
|
|
684
|
+
context: TContext;
|
|
685
|
+
lastMessageText: string; // String version for buildResponsePrompt
|
|
686
|
+
historyEvents: Event[]; // Event[] version for buildResponsePrompt
|
|
687
|
+
signal?: AbortSignal;
|
|
688
|
+
}): Promise<{
|
|
689
|
+
message: string;
|
|
690
|
+
toolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
|
|
691
|
+
session: SessionState<TData>;
|
|
692
|
+
}> {
|
|
693
|
+
const { selectedRoute, selectedStep, responseDirectives, history, context, lastMessageText, historyEvents, signal } = params;
|
|
694
|
+
let session = params.session;
|
|
695
|
+
|
|
696
|
+
// Determine next step
|
|
697
|
+
let nextStep: Step<TContext, TData>;
|
|
698
|
+
if (selectedStep) {
|
|
699
|
+
nextStep = selectedStep;
|
|
700
|
+
} else {
|
|
701
|
+
// New route or no step selected - get initial step or first valid step
|
|
702
|
+
const routingEngine = this.agent.getRoutingEngine();
|
|
703
|
+
const candidates = routingEngine.getCandidateSteps(selectedRoute, undefined, session.data || {});
|
|
704
|
+
if (candidates.length > 0) {
|
|
705
|
+
nextStep = candidates[0].step;
|
|
706
|
+
logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id} for new route`);
|
|
707
|
+
} else {
|
|
708
|
+
// Fallback to initial step even if it should be skipped
|
|
709
|
+
nextStep = selectedRoute.initialStep;
|
|
710
|
+
logger.warn(`[ResponseModal] No valid steps found, using initial step: ${nextStep.id}`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Update session with next step
|
|
715
|
+
session = enterStep(session, nextStep.id, nextStep.description);
|
|
716
|
+
logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
|
|
717
|
+
|
|
718
|
+
// Build response schema for this route (with collect fields from step)
|
|
719
|
+
const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, nextStep, this.agent.getSchema());
|
|
720
|
+
|
|
721
|
+
// Build response prompt
|
|
722
|
+
const responsePrompt = await this.responseEngine.buildResponsePrompt({
|
|
723
|
+
route: selectedRoute,
|
|
724
|
+
currentStep: nextStep,
|
|
725
|
+
rules: selectedRoute.getRules(),
|
|
726
|
+
prohibitions: selectedRoute.getProhibitions(),
|
|
727
|
+
directives: responseDirectives,
|
|
728
|
+
history: historyEvents, // Use Event[] for buildResponsePrompt
|
|
729
|
+
lastMessage: lastMessageText, // Use string for buildResponsePrompt
|
|
730
|
+
agentOptions: this.agent.getAgentOptions(),
|
|
731
|
+
combinedGuidelines: [...this.agent.getGuidelines(), ...selectedRoute.getGuidelines()],
|
|
732
|
+
combinedTerms: this.mergeTerms(this.agent.getTerms(), selectedRoute.getTerms()),
|
|
733
|
+
context,
|
|
734
|
+
session,
|
|
735
|
+
agentSchema: this.agent.getSchema(),
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
// Collect available tools for AI
|
|
739
|
+
const availableTools = this.collectAvailableTools(selectedRoute, nextStep);
|
|
740
|
+
|
|
741
|
+
// Generate message using AI provider
|
|
742
|
+
const agentOptions = this.agent.getAgentOptions();
|
|
743
|
+
const result = await agentOptions.provider.generateMessage({
|
|
744
|
+
prompt: responsePrompt,
|
|
745
|
+
history: historyEvents, // Use Event[] for AI provider
|
|
746
|
+
context,
|
|
747
|
+
tools: availableTools,
|
|
748
|
+
signal,
|
|
749
|
+
parameters: responseSchema ? { jsonSchema: responseSchema, schemaName: "response_output" } : undefined,
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
let message = result.structured?.message || result.message;
|
|
753
|
+
let toolCalls = result.structured?.toolCalls;
|
|
754
|
+
|
|
755
|
+
// Execute tools with unified loop handling
|
|
756
|
+
const toolResult = await this.executeUnifiedToolLoop({
|
|
757
|
+
toolCalls,
|
|
758
|
+
context,
|
|
759
|
+
session,
|
|
760
|
+
history,
|
|
761
|
+
selectedRoute,
|
|
762
|
+
responsePrompt,
|
|
763
|
+
availableTools,
|
|
764
|
+
responseSchema,
|
|
765
|
+
signal,
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
session = toolResult.session;
|
|
769
|
+
toolCalls = toolResult.finalToolCalls;
|
|
770
|
+
if (toolResult.finalMessage) {
|
|
771
|
+
message = toolResult.finalMessage;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Collect data from response
|
|
775
|
+
session = await this.collectDataFromResponse({ result, selectedRoute, nextStep, session });
|
|
776
|
+
|
|
777
|
+
return { message, toolCalls, session };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Process route streaming response with unified tool execution and data collection
|
|
782
|
+
* @private
|
|
783
|
+
*/
|
|
784
|
+
private async *processRouteStreamingResponse(params: {
|
|
785
|
+
selectedRoute: Route<TContext, TData>;
|
|
786
|
+
selectedStep?: Step<TContext, TData>;
|
|
787
|
+
responseDirectives?: string[];
|
|
788
|
+
session: SessionState<TData>;
|
|
789
|
+
history: HistoryItem[];
|
|
790
|
+
context: TContext;
|
|
791
|
+
lastMessageText: string; // String version for buildResponsePrompt
|
|
792
|
+
historyEvents: Event[]; // Event[] version for buildResponsePrompt
|
|
793
|
+
signal?: AbortSignal;
|
|
794
|
+
}): AsyncGenerator<AgentResponseStreamChunk<TData>> {
|
|
795
|
+
const { selectedRoute, selectedStep, responseDirectives, history, context, lastMessageText, historyEvents, signal } = params;
|
|
796
|
+
let session = params.session;
|
|
797
|
+
|
|
798
|
+
// Determine next step (same logic as non-streaming)
|
|
799
|
+
let nextStep: Step<TContext, TData>;
|
|
800
|
+
if (selectedStep) {
|
|
801
|
+
nextStep = selectedStep;
|
|
802
|
+
} else {
|
|
803
|
+
const routingEngine = this.agent.getRoutingEngine();
|
|
804
|
+
const candidates = routingEngine.getCandidateSteps(selectedRoute, undefined, session.data || {});
|
|
805
|
+
if (candidates.length > 0) {
|
|
806
|
+
nextStep = candidates[0].step;
|
|
807
|
+
logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id} for new route`);
|
|
808
|
+
} else {
|
|
809
|
+
nextStep = selectedRoute.initialStep;
|
|
810
|
+
logger.warn(`[ResponseModal] No valid steps found, using initial step: ${nextStep.id}`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Update session with next step
|
|
815
|
+
session = enterStep(session, nextStep.id, nextStep.description);
|
|
816
|
+
logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
|
|
817
|
+
|
|
818
|
+
// Build response schema and prompt (same as non-streaming)
|
|
819
|
+
const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, nextStep, this.agent.getSchema());
|
|
820
|
+
const responsePrompt = await this.responseEngine.buildResponsePrompt({
|
|
821
|
+
route: selectedRoute,
|
|
822
|
+
currentStep: nextStep,
|
|
823
|
+
rules: selectedRoute.getRules(),
|
|
824
|
+
prohibitions: selectedRoute.getProhibitions(),
|
|
825
|
+
directives: responseDirectives,
|
|
826
|
+
history: historyEvents, // Use Event[] for buildResponsePrompt
|
|
827
|
+
lastMessage: lastMessageText, // Use string for buildResponsePrompt
|
|
828
|
+
agentOptions: this.agent.getAgentOptions(),
|
|
829
|
+
combinedGuidelines: [...this.agent.getGuidelines(), ...selectedRoute.getGuidelines()],
|
|
830
|
+
combinedTerms: this.mergeTerms(this.agent.getTerms(), selectedRoute.getTerms()),
|
|
831
|
+
context,
|
|
832
|
+
session,
|
|
833
|
+
agentSchema: this.agent.getSchema(),
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// Collect available tools for AI
|
|
837
|
+
const availableTools = this.collectAvailableTools(selectedRoute, nextStep);
|
|
838
|
+
|
|
839
|
+
// Generate message stream using AI provider
|
|
840
|
+
const agentOptions = this.agent.getAgentOptions();
|
|
841
|
+
const stream = agentOptions.provider.generateMessageStream({
|
|
842
|
+
prompt: responsePrompt,
|
|
843
|
+
history: historyEvents, // Use Event[] for AI provider
|
|
844
|
+
context,
|
|
845
|
+
tools: availableTools,
|
|
846
|
+
signal,
|
|
847
|
+
parameters: { jsonSchema: responseSchema, schemaName: "response_stream_output" },
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
// Stream chunks with unified tool handling
|
|
851
|
+
for await (const chunk of stream) {
|
|
852
|
+
let toolCalls: Array<{ toolName: string; arguments: Record<string, unknown> }> | undefined = undefined;
|
|
853
|
+
|
|
854
|
+
// Extract tool calls from AI response on final chunk
|
|
855
|
+
if (chunk.done && chunk.structured?.toolCalls) {
|
|
856
|
+
toolCalls = chunk.structured.toolCalls;
|
|
857
|
+
|
|
858
|
+
// Execute tools with unified loop handling
|
|
859
|
+
const toolResult = await this.executeUnifiedToolLoop({
|
|
860
|
+
toolCalls,
|
|
861
|
+
context,
|
|
862
|
+
session,
|
|
863
|
+
history,
|
|
864
|
+
selectedRoute,
|
|
865
|
+
responsePrompt,
|
|
866
|
+
availableTools,
|
|
867
|
+
responseSchema,
|
|
868
|
+
signal,
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
session = toolResult.session;
|
|
872
|
+
toolCalls = toolResult.finalToolCalls;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Extract collected data on final chunk
|
|
876
|
+
if (chunk.done && chunk.structured && nextStep.collect) {
|
|
877
|
+
session = await this.collectDataFromResponse({
|
|
878
|
+
result: { structured: chunk.structured },
|
|
879
|
+
selectedRoute,
|
|
880
|
+
nextStep,
|
|
881
|
+
session,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Handle session finalization on final chunk
|
|
886
|
+
if (chunk.done) {
|
|
887
|
+
await this.finalizeSession(session, context);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
yield {
|
|
891
|
+
delta: chunk.delta,
|
|
892
|
+
accumulated: chunk.accumulated,
|
|
893
|
+
done: chunk.done,
|
|
894
|
+
session,
|
|
895
|
+
toolCalls,
|
|
896
|
+
isRouteComplete: false,
|
|
897
|
+
metadata: chunk.metadata,
|
|
898
|
+
structured: chunk.structured,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Unified tool execution logic with loop handling
|
|
905
|
+
* Consolidates the complex tool execution logic from both streaming and non-streaming responses
|
|
906
|
+
* @private
|
|
907
|
+
*/
|
|
908
|
+
private async executeUnifiedToolLoop(params: {
|
|
909
|
+
toolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
|
|
910
|
+
context: TContext;
|
|
911
|
+
session: SessionState<TData>;
|
|
912
|
+
history: HistoryItem[];
|
|
913
|
+
selectedRoute?: Route<TContext, TData>;
|
|
914
|
+
responsePrompt: string;
|
|
915
|
+
availableTools: Array<{
|
|
916
|
+
id: string;
|
|
917
|
+
name: string;
|
|
918
|
+
description?: string;
|
|
919
|
+
parameters?: unknown;
|
|
920
|
+
}>;
|
|
921
|
+
responseSchema?: Record<string, unknown>;
|
|
922
|
+
signal?: AbortSignal;
|
|
923
|
+
}): Promise<{
|
|
924
|
+
session: SessionState<TData>;
|
|
925
|
+
finalToolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
|
|
926
|
+
finalMessage?: string;
|
|
927
|
+
}> {
|
|
928
|
+
try {
|
|
929
|
+
const { context, history, selectedRoute, responsePrompt, availableTools, responseSchema, signal } = params;
|
|
930
|
+
let { toolCalls, session } = params;
|
|
931
|
+
|
|
932
|
+
// Convert HistoryItem[] to Event[] for internal processing
|
|
933
|
+
const historyEvents = historyToEvents(history);
|
|
934
|
+
|
|
935
|
+
// Execute initial dynamic tool calls
|
|
936
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
937
|
+
logger.debug(`[ResponseModal] Executing ${toolCalls.length} dynamic tool calls`);
|
|
938
|
+
|
|
939
|
+
for (const toolCall of toolCalls) {
|
|
940
|
+
const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
|
|
941
|
+
if (!tool) {
|
|
942
|
+
logger.warn(`[ResponseModal] Tool not found: ${toolCall.toolName}`);
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
try {
|
|
947
|
+
// Use ToolManager for unified tool execution
|
|
948
|
+
const toolManager = this.getToolManager();
|
|
949
|
+
let toolResult;
|
|
950
|
+
|
|
951
|
+
if (toolManager) {
|
|
952
|
+
toolResult = await toolManager.executeTool({
|
|
953
|
+
tool: tool,
|
|
954
|
+
context,
|
|
955
|
+
updateContext: this.agent.updateContext.bind(this.agent),
|
|
956
|
+
updateData: this.agent.updateCollectedData.bind(this.agent),
|
|
957
|
+
history: historyEvents, // Use Event[] for tool execution
|
|
958
|
+
data: session.data,
|
|
959
|
+
toolArguments: toolCall.arguments,
|
|
960
|
+
});
|
|
961
|
+
} else {
|
|
962
|
+
// Fallback: execute tool directly if ToolManager not available
|
|
963
|
+
throw new Error(`ToolManager not available for tool execution: ${toolCall.toolName}`);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Check if tool execution was successful
|
|
967
|
+
if (!toolResult.success) {
|
|
968
|
+
logger.error(`[ResponseModal] Tool execution failed: ${toolCall.toolName} - ${toolResult.error}`);
|
|
969
|
+
// Continue with other tools rather than failing completely
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Update context with tool results
|
|
974
|
+
if (toolResult.contextUpdate) {
|
|
975
|
+
try {
|
|
976
|
+
await this.agent.updateContext(toolResult.contextUpdate as Partial<TContext>);
|
|
977
|
+
} catch (error) {
|
|
978
|
+
logger.error(`[ResponseModal] Failed to update context from tool ${toolCall.toolName}:`, error);
|
|
979
|
+
// Continue execution but log the error
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Update collected data with tool results
|
|
984
|
+
if (toolResult.dataUpdate) {
|
|
985
|
+
try {
|
|
986
|
+
const updateDataMethod = this.agent.getUpdateDataMethod();
|
|
987
|
+
session = await updateDataMethod(session, toolResult.dataUpdate as Partial<TData>);
|
|
988
|
+
logger.debug(`[ResponseModal] Tool updated collected data:`, toolResult.dataUpdate);
|
|
989
|
+
} catch (error) {
|
|
990
|
+
logger.error(`[ResponseModal] Failed to update data from tool ${toolCall.toolName}:`, error);
|
|
991
|
+
// Continue execution but log the error
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
logger.debug(`[ResponseModal] Executed dynamic tool: ${toolCall.toolName} (success: ${toolResult.success})`);
|
|
996
|
+
} catch (error) {
|
|
997
|
+
logger.error(`[ResponseModal] Tool execution error for ${toolCall.toolName}:`, error);
|
|
998
|
+
// Continue with other tools rather than failing the entire response
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// TOOL LOOP: Allow AI to make follow-up tool calls after initial tool execution
|
|
1005
|
+
const MAX_TOOL_LOOPS = this.options?.maxToolLoops || 5;
|
|
1006
|
+
let toolLoopCount = 0;
|
|
1007
|
+
let hasToolCalls = toolCalls && toolCalls.length > 0;
|
|
1008
|
+
let finalMessage: string | undefined;
|
|
1009
|
+
|
|
1010
|
+
while (hasToolCalls && toolLoopCount < MAX_TOOL_LOOPS) {
|
|
1011
|
+
toolLoopCount++;
|
|
1012
|
+
logger.debug(`[ResponseModal] Starting tool loop ${toolLoopCount}/${MAX_TOOL_LOOPS}`);
|
|
1013
|
+
|
|
1014
|
+
// Create tool result events with proper Event format structure
|
|
1015
|
+
const toolResultEvents: Event<ToolEventData>[] = [];
|
|
1016
|
+
for (const toolCall of toolCalls || []) {
|
|
1017
|
+
const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
|
|
1018
|
+
if (tool) {
|
|
1019
|
+
// Create proper Event format for tool results
|
|
1020
|
+
const toolResultEvent: Event<ToolEventData> = {
|
|
1021
|
+
kind: EventKind.TOOL,
|
|
1022
|
+
source: MessageRole.AGENT,
|
|
1023
|
+
timestamp: new Date().toISOString(),
|
|
1024
|
+
data: {
|
|
1025
|
+
tool_calls: [
|
|
1026
|
+
{
|
|
1027
|
+
tool_id: toolCall.toolName,
|
|
1028
|
+
arguments: toolCall.arguments,
|
|
1029
|
+
result: {
|
|
1030
|
+
data: "Tool executed successfully",
|
|
1031
|
+
},
|
|
1032
|
+
},
|
|
1033
|
+
],
|
|
1034
|
+
},
|
|
1035
|
+
};
|
|
1036
|
+
toolResultEvents.push(toolResultEvent);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Create updated history with tool results (combine Event arrays)
|
|
1041
|
+
const updatedHistoryEvents = [...historyEvents, ...toolResultEvents];
|
|
1042
|
+
|
|
1043
|
+
// Make follow-up AI call to see if more tools are needed
|
|
1044
|
+
const agentOptions = this.agent.getAgentOptions();
|
|
1045
|
+
const followUpResult = await agentOptions.provider.generateMessage({
|
|
1046
|
+
prompt: responsePrompt,
|
|
1047
|
+
history: updatedHistoryEvents, // Use Event[] for AI provider
|
|
1048
|
+
context,
|
|
1049
|
+
tools: availableTools,
|
|
1050
|
+
parameters: responseSchema ? {
|
|
1051
|
+
jsonSchema: responseSchema,
|
|
1052
|
+
schemaName: "tool_followup",
|
|
1053
|
+
} : undefined,
|
|
1054
|
+
signal,
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
// Check if follow-up call has more tool calls
|
|
1058
|
+
const followUpToolCalls = followUpResult.structured?.toolCalls;
|
|
1059
|
+
hasToolCalls = followUpToolCalls && followUpToolCalls.length > 0;
|
|
1060
|
+
|
|
1061
|
+
if (hasToolCalls) {
|
|
1062
|
+
logger.debug(`[ResponseModal] Follow-up call produced ${followUpToolCalls!.length} additional tool calls`);
|
|
1063
|
+
|
|
1064
|
+
// Execute the follow-up tool calls
|
|
1065
|
+
for (const toolCall of followUpToolCalls!) {
|
|
1066
|
+
const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
|
|
1067
|
+
if (!tool) {
|
|
1068
|
+
logger.warn(`[ResponseModal] Tool not found in follow-up: ${toolCall.toolName}`);
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
try {
|
|
1073
|
+
// Use ToolManager for unified tool execution
|
|
1074
|
+
const toolManager = this.getToolManager();
|
|
1075
|
+
let toolResult;
|
|
1076
|
+
|
|
1077
|
+
if (toolManager) {
|
|
1078
|
+
toolResult = await toolManager.executeTool({
|
|
1079
|
+
tool: tool,
|
|
1080
|
+
context,
|
|
1081
|
+
updateContext: this.agent.updateContext.bind(this.agent),
|
|
1082
|
+
updateData: this.agent.updateCollectedData.bind(this.agent),
|
|
1083
|
+
history: updatedHistoryEvents, // Use Event[] for tool execution
|
|
1084
|
+
data: session.data,
|
|
1085
|
+
toolArguments: toolCall.arguments,
|
|
1086
|
+
});
|
|
1087
|
+
} else {
|
|
1088
|
+
// Fallback: execute tool directly if ToolManager not available
|
|
1089
|
+
throw new Error(`ToolManager not available for follow-up tool execution: ${toolCall.toolName}`);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Check if tool execution was successful
|
|
1093
|
+
if (!toolResult.success) {
|
|
1094
|
+
logger.error(`[ResponseModal] Follow-up tool execution failed: ${toolCall.toolName} - ${toolResult.error}`);
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Update context with follow-up tool results
|
|
1099
|
+
if (toolResult.contextUpdate) {
|
|
1100
|
+
try {
|
|
1101
|
+
await this.agent.updateContext(toolResult.contextUpdate as Partial<TContext>);
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
logger.error(`[ResponseModal] Failed to update context from follow-up tool ${toolCall.toolName}:`, error);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (toolResult.dataUpdate) {
|
|
1108
|
+
try {
|
|
1109
|
+
const updateDataMethod = this.agent.getUpdateDataMethod();
|
|
1110
|
+
session = await updateDataMethod(session, toolResult.dataUpdate as Partial<TData>);
|
|
1111
|
+
logger.debug(`[ResponseModal] Follow-up tool updated collected data:`, toolResult.dataUpdate);
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
logger.error(`[ResponseModal] Failed to update data from follow-up tool ${toolCall.toolName}:`, error);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
logger.debug(`[ResponseModal] Executed follow-up tool: ${toolCall.toolName} (success: ${toolResult.success})`);
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
logger.error(`[ResponseModal] Follow-up tool execution error for ${toolCall.toolName}:`, error);
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Update toolCalls for next iteration or final response
|
|
1125
|
+
toolCalls = followUpToolCalls;
|
|
1126
|
+
} else {
|
|
1127
|
+
logger.debug(`[ResponseModal] Tool loop completed after ${toolLoopCount} iterations`);
|
|
1128
|
+
// Update final message and toolCalls from follow-up result if no more tools
|
|
1129
|
+
finalMessage = followUpResult.structured?.message || followUpResult.message;
|
|
1130
|
+
toolCalls = followUpToolCalls || [];
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (toolLoopCount >= MAX_TOOL_LOOPS) {
|
|
1136
|
+
logger.warn(`[ResponseModal] Tool loop limit reached (${MAX_TOOL_LOOPS}), stopping`);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
return {
|
|
1140
|
+
session,
|
|
1141
|
+
finalToolCalls: toolCalls,
|
|
1142
|
+
finalMessage,
|
|
1143
|
+
};
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
throw ResponseGenerationError.fromError(error, 'tool_execution', params, {
|
|
1146
|
+
toolCallsCount: params.toolCalls?.length || 0,
|
|
1147
|
+
availableToolsCount: params.availableTools.length
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
} /**
|
|
1151
|
+
* Unified data collection from AI response
|
|
1152
|
+
* @private
|
|
1153
|
+
*/
|
|
1154
|
+
private async collectDataFromResponse(params: {
|
|
1155
|
+
result: { structured?: AgentStructuredResponse };
|
|
1156
|
+
selectedRoute?: Route<TContext, TData>;
|
|
1157
|
+
nextStep?: Step<TContext, TData>;
|
|
1158
|
+
session: SessionState<TData>;
|
|
1159
|
+
}): Promise<SessionState<TData>> {
|
|
1160
|
+
try {
|
|
1161
|
+
const { result, selectedRoute, nextStep, session } = params;
|
|
1162
|
+
let updatedSession = session;
|
|
1163
|
+
|
|
1164
|
+
// Extract collected data from final response (only for route-based interactions)
|
|
1165
|
+
if (selectedRoute && result.structured && nextStep?.collect) {
|
|
1166
|
+
try {
|
|
1167
|
+
const collectedData: Record<string, unknown> = {};
|
|
1168
|
+
// AgentStructuredResponse extends Record<string, unknown>, so we can safely access properties
|
|
1169
|
+
const structuredData = result.structured;
|
|
1170
|
+
|
|
1171
|
+
for (const field of nextStep.collect) {
|
|
1172
|
+
const fieldKey = String(field);
|
|
1173
|
+
if (fieldKey in structuredData) {
|
|
1174
|
+
collectedData[fieldKey] = structuredData[fieldKey];
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Merge collected data into session using agent-level data validation
|
|
1179
|
+
if (Object.keys(collectedData).length > 0) {
|
|
1180
|
+
try {
|
|
1181
|
+
// Update agent-level collected data with validation
|
|
1182
|
+
await this.agent.updateCollectedData(collectedData as Partial<TData>);
|
|
1183
|
+
|
|
1184
|
+
// Update session with validated data
|
|
1185
|
+
const updateDataMethod = this.agent.getUpdateDataMethod();
|
|
1186
|
+
updatedSession = await updateDataMethod(updatedSession, collectedData as Partial<TData>);
|
|
1187
|
+
logger.debug(`[ResponseModal] Collected data:`, collectedData);
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
logger.error(`[ResponseModal] Failed to update collected data:`, error);
|
|
1190
|
+
// Continue without updating data rather than failing completely
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
logger.error(`[ResponseModal] Error during data collection:`, error);
|
|
1195
|
+
// Continue without collecting data rather than failing completely
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Extract any additional data from structured response
|
|
1200
|
+
// Since AgentStructuredResponse extends Record<string, unknown>, we can safely check for additional properties
|
|
1201
|
+
if (result.structured && "contextUpdate" in result.structured) {
|
|
1202
|
+
try {
|
|
1203
|
+
const contextUpdate = (result.structured as AgentStructuredResponse & { contextUpdate?: Partial<TContext> }).contextUpdate;
|
|
1204
|
+
if (contextUpdate) {
|
|
1205
|
+
await this.agent.updateContext(contextUpdate);
|
|
1206
|
+
}
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
logger.error(`[ResponseModal] Failed to update context from structured response:`, error);
|
|
1209
|
+
// Continue without updating context rather than failing completely
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
return updatedSession;
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
logger.error(`[ResponseModal] Error in collectDataFromResponse:`, error);
|
|
1216
|
+
// Return original session if data collection fails completely
|
|
1217
|
+
return params.session;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Handle route completion logic
|
|
1223
|
+
* @private
|
|
1224
|
+
*/
|
|
1225
|
+
private async handleRouteCompletion(params: {
|
|
1226
|
+
selectedRoute: Route<TContext, TData>;
|
|
1227
|
+
session: SessionState<TData>;
|
|
1228
|
+
context: TContext;
|
|
1229
|
+
lastMessageText: string; // String version for buildResponsePrompt
|
|
1230
|
+
historyEvents: Event[]; // Event[] version for buildResponsePrompt
|
|
1231
|
+
signal?: AbortSignal;
|
|
1232
|
+
}): Promise<string> {
|
|
1233
|
+
const { selectedRoute, session, context, lastMessageText, historyEvents, signal } = params;
|
|
1234
|
+
|
|
1235
|
+
// Get endStep spec from route
|
|
1236
|
+
const endStepSpec = selectedRoute.endStepSpec;
|
|
1237
|
+
|
|
1238
|
+
// Create a temporary step for completion message generation using endStep configuration
|
|
1239
|
+
const completionStep = new Step<TContext, TData>(selectedRoute.id, {
|
|
1240
|
+
description: endStepSpec.description,
|
|
1241
|
+
id: endStepSpec.id || END_ROUTE_ID,
|
|
1242
|
+
collect: endStepSpec.collect,
|
|
1243
|
+
requires: endStepSpec.requires,
|
|
1244
|
+
prompt: endStepSpec.prompt || "Summarize what was accomplished and confirm completion based on the conversation history and collected data",
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
// Build response schema for completion
|
|
1248
|
+
const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, completionStep, this.agent.getSchema());
|
|
1249
|
+
const templateContext = { context, session, history: historyEvents }; // Use Event[] for template context
|
|
1250
|
+
|
|
1251
|
+
// Build completion response prompt
|
|
1252
|
+
const completionPrompt = await this.responseEngine.buildResponsePrompt({
|
|
1253
|
+
route: selectedRoute,
|
|
1254
|
+
currentStep: completionStep,
|
|
1255
|
+
rules: selectedRoute.getRules(),
|
|
1256
|
+
prohibitions: selectedRoute.getProhibitions(),
|
|
1257
|
+
directives: undefined, // No directives for completion
|
|
1258
|
+
history: historyEvents, // Use Event[] for buildResponsePrompt
|
|
1259
|
+
lastMessage: lastMessageText, // Use string for buildResponsePrompt
|
|
1260
|
+
agentOptions: this.agent.getAgentOptions(),
|
|
1261
|
+
combinedGuidelines: [...this.agent.getGuidelines(), ...selectedRoute.getGuidelines()],
|
|
1262
|
+
combinedTerms: this.mergeTerms(this.agent.getTerms(), selectedRoute.getTerms()),
|
|
1263
|
+
context,
|
|
1264
|
+
session,
|
|
1265
|
+
agentSchema: this.agent.getSchema(),
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
// Generate completion message using AI provider
|
|
1269
|
+
const agentOptions = this.agent.getAgentOptions();
|
|
1270
|
+
const completionResult = await agentOptions.provider.generateMessage({
|
|
1271
|
+
prompt: completionPrompt,
|
|
1272
|
+
history: historyEvents, // Use Event[] for AI provider
|
|
1273
|
+
context,
|
|
1274
|
+
signal,
|
|
1275
|
+
parameters: { jsonSchema: responseSchema, schemaName: "completion_message" },
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
const message = completionResult.structured?.message || completionResult.message;
|
|
1279
|
+
logger.debug(`[ResponseModal] Generated completion message for route: ${selectedRoute.title}`);
|
|
1280
|
+
|
|
1281
|
+
// Check for onComplete transition
|
|
1282
|
+
const transitionConfig = await selectedRoute.evaluateOnComplete({ data: session.data }, context);
|
|
1283
|
+
|
|
1284
|
+
if (transitionConfig) {
|
|
1285
|
+
// Find target route by ID or title
|
|
1286
|
+
const targetRoute = this.agent.getRoutes().find(
|
|
1287
|
+
(r) => r.id === transitionConfig.nextStep || r.title === transitionConfig.nextStep
|
|
1288
|
+
);
|
|
1289
|
+
|
|
1290
|
+
if (targetRoute) {
|
|
1291
|
+
const renderedCondition = await render(transitionConfig.condition, templateContext);
|
|
1292
|
+
// Set pending transition in session
|
|
1293
|
+
session.pendingTransition = {
|
|
1294
|
+
targetRouteId: targetRoute.id,
|
|
1295
|
+
condition: renderedCondition,
|
|
1296
|
+
reason: "route_complete",
|
|
1297
|
+
};
|
|
1298
|
+
logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed with pending transition to: ${targetRoute.title}`);
|
|
1299
|
+
} else {
|
|
1300
|
+
logger.warn(`[ResponseModal] Route ${selectedRoute.title} completed but target route not found: ${transitionConfig.nextStep}`);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
return message;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Stream route completion response
|
|
1309
|
+
* @private
|
|
1310
|
+
*/
|
|
1311
|
+
private async *streamRouteCompletion(params: {
|
|
1312
|
+
selectedRoute: Route<TContext, TData>;
|
|
1313
|
+
session: SessionState<TData>;
|
|
1314
|
+
context: TContext;
|
|
1315
|
+
lastMessageText: string; // String version for buildResponsePrompt
|
|
1316
|
+
historyEvents: Event[]; // Event[] version for buildResponsePrompt
|
|
1317
|
+
signal?: AbortSignal;
|
|
1318
|
+
}): AsyncGenerator<AgentResponseStreamChunk<TData>> {
|
|
1319
|
+
const { selectedRoute, context, lastMessageText, historyEvents, signal } = params;
|
|
1320
|
+
let session = params.session;
|
|
1321
|
+
|
|
1322
|
+
// Get endStep spec from route
|
|
1323
|
+
const endStepSpec = selectedRoute.endStepSpec;
|
|
1324
|
+
|
|
1325
|
+
// Create a temporary step for completion message generation using endStep configuration
|
|
1326
|
+
const completionStep = new Step<TContext, TData>(selectedRoute.id, {
|
|
1327
|
+
description: endStepSpec.description,
|
|
1328
|
+
id: endStepSpec.id || END_ROUTE_ID,
|
|
1329
|
+
collect: endStepSpec.collect,
|
|
1330
|
+
requires: endStepSpec.requires,
|
|
1331
|
+
prompt: endStepSpec.prompt || "Summarize what was accomplished and confirm completion based on the conversation history and collected data",
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
// Build response schema for completion
|
|
1335
|
+
const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, completionStep, this.agent.getSchema());
|
|
1336
|
+
const templateContext = { context, session, history: historyEvents }; // Use Event[] for template context
|
|
1337
|
+
|
|
1338
|
+
// Build completion response prompt
|
|
1339
|
+
const completionPrompt = await this.responseEngine.buildResponsePrompt({
|
|
1340
|
+
route: selectedRoute,
|
|
1341
|
+
currentStep: completionStep,
|
|
1342
|
+
rules: selectedRoute.getRules(),
|
|
1343
|
+
prohibitions: selectedRoute.getProhibitions(),
|
|
1344
|
+
directives: undefined, // No directives for completion
|
|
1345
|
+
history: historyEvents, // Use Event[] for buildResponsePrompt
|
|
1346
|
+
lastMessage: lastMessageText, // Use string for buildResponsePrompt
|
|
1347
|
+
agentOptions: this.agent.getAgentOptions(),
|
|
1348
|
+
combinedGuidelines: [...this.agent.getGuidelines(), ...selectedRoute.getGuidelines()],
|
|
1349
|
+
combinedTerms: this.mergeTerms(this.agent.getTerms(), selectedRoute.getTerms()),
|
|
1350
|
+
context,
|
|
1351
|
+
session,
|
|
1352
|
+
agentSchema: this.agent.getSchema(),
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
// Stream completion message using AI provider
|
|
1356
|
+
const agentOptions = this.agent.getAgentOptions();
|
|
1357
|
+
const stream = agentOptions.provider.generateMessageStream({
|
|
1358
|
+
prompt: completionPrompt,
|
|
1359
|
+
history: historyEvents, // Use Event[] for AI provider
|
|
1360
|
+
context,
|
|
1361
|
+
signal,
|
|
1362
|
+
parameters: { jsonSchema: responseSchema, schemaName: "completion_message_stream" },
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
logger.debug(`[ResponseModal] Streaming completion message for route: ${selectedRoute.title}`);
|
|
1366
|
+
|
|
1367
|
+
// Check for onComplete transition
|
|
1368
|
+
const transitionConfig = await selectedRoute.evaluateOnComplete({ data: session.data }, context);
|
|
1369
|
+
|
|
1370
|
+
if (transitionConfig) {
|
|
1371
|
+
// Find target route by ID or title
|
|
1372
|
+
const targetRoute = this.agent.getRoutes().find(
|
|
1373
|
+
(r) => r.id === transitionConfig.nextStep || r.title === transitionConfig.nextStep
|
|
1374
|
+
);
|
|
1375
|
+
|
|
1376
|
+
if (targetRoute) {
|
|
1377
|
+
const renderedCondition = await render(transitionConfig.condition, templateContext);
|
|
1378
|
+
// Set pending transition in session
|
|
1379
|
+
session = {
|
|
1380
|
+
...session,
|
|
1381
|
+
pendingTransition: {
|
|
1382
|
+
targetRouteId: targetRoute.id,
|
|
1383
|
+
condition: renderedCondition,
|
|
1384
|
+
reason: "route_complete",
|
|
1385
|
+
},
|
|
1386
|
+
};
|
|
1387
|
+
logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed with pending transition to: ${targetRoute.title}`);
|
|
1388
|
+
} else {
|
|
1389
|
+
logger.warn(`[ResponseModal] Route ${selectedRoute.title} completed but target route not found: ${transitionConfig.nextStep}`);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Set step to END_ROUTE marker
|
|
1394
|
+
session = enterStep(session, END_ROUTE_ID, "Route completed");
|
|
1395
|
+
logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed. Entered END_ROUTE step.`);
|
|
1396
|
+
|
|
1397
|
+
// Stream completion chunks
|
|
1398
|
+
for await (const chunk of stream) {
|
|
1399
|
+
// Update current session if we have one
|
|
1400
|
+
if (chunk.done) {
|
|
1401
|
+
await this.finalizeSession(session, context);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
yield {
|
|
1405
|
+
delta: chunk.delta,
|
|
1406
|
+
accumulated: chunk.accumulated,
|
|
1407
|
+
done: chunk.done,
|
|
1408
|
+
session,
|
|
1409
|
+
toolCalls: undefined,
|
|
1410
|
+
isRouteComplete: true,
|
|
1411
|
+
metadata: chunk.metadata,
|
|
1412
|
+
structured: chunk.structured,
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Generate fallback response when no routes are available
|
|
1419
|
+
* @private
|
|
1420
|
+
*/
|
|
1421
|
+
private async generateFallbackResponse(params: {
|
|
1422
|
+
history: Event[]; // Use Event[] for buildFallbackPrompt
|
|
1423
|
+
context: TContext;
|
|
1424
|
+
session: SessionState<TData>;
|
|
1425
|
+
signal?: AbortSignal;
|
|
1426
|
+
}): Promise<string> {
|
|
1427
|
+
const { history, context, session, signal } = params;
|
|
1428
|
+
|
|
1429
|
+
logger.debug(`[ResponseModal] No route selected, generating basic response`);
|
|
1430
|
+
|
|
1431
|
+
// Build basic response prompt without route context
|
|
1432
|
+
const fallbackPrompt = await this.responseEngine.buildFallbackPrompt({
|
|
1433
|
+
history,
|
|
1434
|
+
agentOptions: this.agent.getAgentOptions(),
|
|
1435
|
+
terms: this.agent.getTerms(),
|
|
1436
|
+
guidelines: this.agent.getGuidelines(),
|
|
1437
|
+
context,
|
|
1438
|
+
session,
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
const agentOptions = this.agent.getAgentOptions();
|
|
1442
|
+
const result = await agentOptions.provider.generateMessage({
|
|
1443
|
+
prompt: fallbackPrompt,
|
|
1444
|
+
history,
|
|
1445
|
+
context,
|
|
1446
|
+
signal,
|
|
1447
|
+
parameters: {
|
|
1448
|
+
jsonSchema: {
|
|
1449
|
+
type: "object",
|
|
1450
|
+
properties: { message: { type: "string" } },
|
|
1451
|
+
required: ["message"],
|
|
1452
|
+
additionalProperties: false,
|
|
1453
|
+
},
|
|
1454
|
+
schemaName: "fallback_response",
|
|
1455
|
+
},
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
return result.structured?.message || result.message;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* Stream fallback response when no routes are available
|
|
1463
|
+
* @private
|
|
1464
|
+
*/
|
|
1465
|
+
private async *streamFallbackResponse(params: {
|
|
1466
|
+
history: Event[]; // Use Event[] for buildFallbackPrompt
|
|
1467
|
+
context: TContext;
|
|
1468
|
+
session: SessionState<TData>;
|
|
1469
|
+
signal?: AbortSignal;
|
|
1470
|
+
}): AsyncGenerator<AgentResponseStreamChunk<TData>> {
|
|
1471
|
+
const { history, context, session, signal } = params;
|
|
1472
|
+
|
|
1473
|
+
const fallbackPrompt = await this.responseEngine.buildFallbackPrompt({
|
|
1474
|
+
history,
|
|
1475
|
+
agentOptions: this.agent.getAgentOptions(),
|
|
1476
|
+
terms: this.agent.getTerms(),
|
|
1477
|
+
guidelines: this.agent.getGuidelines(),
|
|
1478
|
+
context,
|
|
1479
|
+
session,
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
const agentOptions = this.agent.getAgentOptions();
|
|
1483
|
+
const stream = agentOptions.provider.generateMessageStream({
|
|
1484
|
+
prompt: fallbackPrompt,
|
|
1485
|
+
history,
|
|
1486
|
+
context,
|
|
1487
|
+
signal,
|
|
1488
|
+
parameters: {
|
|
1489
|
+
jsonSchema: {
|
|
1490
|
+
type: "object",
|
|
1491
|
+
properties: { message: { type: "string" } },
|
|
1492
|
+
required: ["message"],
|
|
1493
|
+
additionalProperties: false,
|
|
1494
|
+
},
|
|
1495
|
+
schemaName: "fallback_stream_response",
|
|
1496
|
+
},
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
for await (const chunk of stream) {
|
|
1500
|
+
// Update current session if we have one
|
|
1501
|
+
if (chunk.done) {
|
|
1502
|
+
await this.finalizeSession(session, context);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
yield {
|
|
1506
|
+
delta: chunk.delta,
|
|
1507
|
+
accumulated: chunk.accumulated,
|
|
1508
|
+
done: chunk.done,
|
|
1509
|
+
session,
|
|
1510
|
+
toolCalls: undefined,
|
|
1511
|
+
isRouteComplete: false,
|
|
1512
|
+
metadata: chunk.metadata,
|
|
1513
|
+
structured: chunk.structured,
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
/**
|
|
1519
|
+
* Handle session persistence and finalization
|
|
1520
|
+
* @private
|
|
1521
|
+
*/
|
|
1522
|
+
private async finalizeSession(session: SessionState<TData>, context: TContext): Promise<void> {
|
|
1523
|
+
// Auto-save session step to persistence if configured
|
|
1524
|
+
const persistenceManager = this.agent.getPersistenceManager();
|
|
1525
|
+
const agentOptions = this.agent.getAgentOptions();
|
|
1526
|
+
if (
|
|
1527
|
+
persistenceManager &&
|
|
1528
|
+
session.id &&
|
|
1529
|
+
(this.options?.enableAutoSave !== false && agentOptions.persistence?.autoSave !== false)
|
|
1530
|
+
) {
|
|
1531
|
+
await persistenceManager.saveSessionState(session.id, session);
|
|
1532
|
+
logger.debug(`[ResponseModal] Auto-saved session step to persistence: ${session.id}`);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// Execute finalize function
|
|
1536
|
+
await this.executeStepFinalize(session, context);
|
|
1537
|
+
|
|
1538
|
+
// Update current session if we have one
|
|
1539
|
+
const currentSession = this.agent.getCurrentSession();
|
|
1540
|
+
if (currentSession) {
|
|
1541
|
+
this.agent.setCurrentSession(session);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
// ============================================================================
|
|
1545
|
+
// UTILITY METHODS - Helper methods for tool management and other utilities
|
|
1546
|
+
// ============================================================================
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* Find an available tool by name for the given route using ToolManager
|
|
1550
|
+
* Delegates to ToolManager for unified tool resolution
|
|
1551
|
+
* @private
|
|
1552
|
+
*/
|
|
1553
|
+
private findAvailableTool(
|
|
1554
|
+
toolName: string,
|
|
1555
|
+
route?: Route<TContext, TData>
|
|
1556
|
+
): Tool<TContext, TData> | undefined {
|
|
1557
|
+
// Use ToolManager for unified tool resolution
|
|
1558
|
+
const toolManager = this.getToolManager();
|
|
1559
|
+
if (toolManager) {
|
|
1560
|
+
return toolManager.find(toolName, undefined, undefined, route);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// Fallback to legacy resolution if ToolManager not available
|
|
1564
|
+
logger.warn(`[ResponseModal] ToolManager not available, using legacy tool resolution for: ${toolName}`);
|
|
1565
|
+
|
|
1566
|
+
// Check route-level tools first (if route provided)
|
|
1567
|
+
if (route) {
|
|
1568
|
+
const routeTool = route
|
|
1569
|
+
.getTools()
|
|
1570
|
+
.find((tool: Tool<TContext, TData>) => tool.id === toolName || tool.name === toolName);
|
|
1571
|
+
if (routeTool) return routeTool;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// Fall back to agent-level tools
|
|
1575
|
+
const agentTools = this.agent.getTools();
|
|
1576
|
+
return agentTools.find(
|
|
1577
|
+
(tool) => tool.id === toolName || tool.name === toolName
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
/**
|
|
1582
|
+
* Collect all available tools for the given route and step context using ToolManager
|
|
1583
|
+
* Delegates to ToolManager for unified tool resolution and deduplication
|
|
1584
|
+
* @private
|
|
1585
|
+
*/
|
|
1586
|
+
private collectAvailableTools(
|
|
1587
|
+
route?: Route<TContext, TData>,
|
|
1588
|
+
step?: Step<TContext, TData>
|
|
1589
|
+
): Array<{
|
|
1590
|
+
id: string;
|
|
1591
|
+
name: string;
|
|
1592
|
+
description?: string;
|
|
1593
|
+
parameters?: unknown;
|
|
1594
|
+
}> {
|
|
1595
|
+
// Use ToolManager for unified tool collection if available
|
|
1596
|
+
const toolManager = this.getToolManager();
|
|
1597
|
+
if (toolManager) {
|
|
1598
|
+
const availableTools = toolManager.getAvailable(undefined, step, route);
|
|
1599
|
+
return availableTools.map((tool) => ({
|
|
1600
|
+
id: tool.id,
|
|
1601
|
+
name: tool.name || tool.id,
|
|
1602
|
+
description: tool.description,
|
|
1603
|
+
parameters: tool.parameters,
|
|
1604
|
+
}));
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// Fallback to legacy collection logic if ToolManager not available
|
|
1608
|
+
logger.warn(`[ResponseModal] ToolManager not available, using legacy tool collection`);
|
|
1609
|
+
|
|
1610
|
+
const availableTools = new Map<string, Tool<TContext, TData>>();
|
|
1611
|
+
|
|
1612
|
+
// Add agent-level tools
|
|
1613
|
+
this.agent.getTools().forEach((tool) => {
|
|
1614
|
+
availableTools.set(tool.id, tool);
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
// Add route-level tools (these take precedence)
|
|
1618
|
+
if (route) {
|
|
1619
|
+
route.getTools().forEach((tool: Tool<TContext, TData>) => {
|
|
1620
|
+
availableTools.set(tool.id, tool);
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// Filter by step-level allowed tools if specified
|
|
1625
|
+
if (step?.tools) {
|
|
1626
|
+
const allowedToolIds = new Set<string>();
|
|
1627
|
+
const stepTools: Tool<TContext, TData>[] = [];
|
|
1628
|
+
|
|
1629
|
+
for (const toolRef of step.tools) {
|
|
1630
|
+
if (typeof toolRef === "string") {
|
|
1631
|
+
// Reference to registered tool
|
|
1632
|
+
allowedToolIds.add(toolRef);
|
|
1633
|
+
} else {
|
|
1634
|
+
// Inline tool definition
|
|
1635
|
+
if (toolRef.id) {
|
|
1636
|
+
allowedToolIds.add(toolRef.id);
|
|
1637
|
+
stepTools.push(toolRef);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// If step specifies tools, only include those
|
|
1643
|
+
if (allowedToolIds.size > 0) {
|
|
1644
|
+
const filteredTools = new Map<string, Tool<TContext, TData>>();
|
|
1645
|
+
for (const toolId of Array.from(allowedToolIds)) {
|
|
1646
|
+
const tool = availableTools.get(toolId);
|
|
1647
|
+
if (tool) {
|
|
1648
|
+
filteredTools.set(toolId, tool);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
// Add inline tools
|
|
1652
|
+
stepTools.forEach((tool) => {
|
|
1653
|
+
if (tool.id) {
|
|
1654
|
+
filteredTools.set(tool.id, tool);
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
availableTools.clear();
|
|
1658
|
+
filteredTools.forEach((tool, id) => availableTools.set(id, tool));
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// Convert to the format expected by AI providers
|
|
1663
|
+
return Array.from(availableTools.values()).map((tool) => ({
|
|
1664
|
+
id: tool.id,
|
|
1665
|
+
name: tool.name || tool.id,
|
|
1666
|
+
description: tool.description,
|
|
1667
|
+
parameters: tool.parameters,
|
|
1668
|
+
}));
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
/**
|
|
1672
|
+
* Execute a prepare or finalize function/tool
|
|
1673
|
+
* @private
|
|
1674
|
+
*/
|
|
1675
|
+
private async executePrepareFinalize(
|
|
1676
|
+
prepareOrFinalize:
|
|
1677
|
+
| string
|
|
1678
|
+
| Tool<TContext, TData>
|
|
1679
|
+
| ((context: TContext, data?: Partial<TData>) => void | Promise<void>)
|
|
1680
|
+
| undefined,
|
|
1681
|
+
context: TContext,
|
|
1682
|
+
data?: Partial<TData>,
|
|
1683
|
+
route?: Route<TContext, TData>,
|
|
1684
|
+
step?: Step<TContext, TData>
|
|
1685
|
+
): Promise<void> {
|
|
1686
|
+
if (!prepareOrFinalize) return;
|
|
1687
|
+
|
|
1688
|
+
if (typeof prepareOrFinalize === "function") {
|
|
1689
|
+
// It's a function - call it directly
|
|
1690
|
+
await prepareOrFinalize(context, data);
|
|
1691
|
+
} else {
|
|
1692
|
+
// It's a tool reference - find and execute the tool
|
|
1693
|
+
let tool: Tool<TContext, TData> | undefined;
|
|
1694
|
+
|
|
1695
|
+
if (typeof prepareOrFinalize === "string") {
|
|
1696
|
+
// Tool ID - use ToolManager for unified resolution
|
|
1697
|
+
const toolManager = this.getToolManager();
|
|
1698
|
+
if (toolManager) {
|
|
1699
|
+
tool = toolManager.find(prepareOrFinalize, undefined, step, route);
|
|
1700
|
+
} else {
|
|
1701
|
+
// Fallback to legacy resolution if ToolManager not available
|
|
1702
|
+
logger.warn(`[ResponseModal] ToolManager not available, using legacy tool resolution for prepare/finalize: ${prepareOrFinalize}`);
|
|
1703
|
+
|
|
1704
|
+
const availableTools = new Map<string, Tool<TContext, TData>>();
|
|
1705
|
+
|
|
1706
|
+
// Add agent-level tools
|
|
1707
|
+
this.agent.getTools().forEach((t) => {
|
|
1708
|
+
availableTools.set(t.id, t);
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1711
|
+
// Add route-level tools
|
|
1712
|
+
if (route) {
|
|
1713
|
+
route.getTools().forEach((t: Tool<TContext, TData>) => {
|
|
1714
|
+
availableTools.set(t.id, t);
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// Add step-level tools
|
|
1719
|
+
if (step?.tools) {
|
|
1720
|
+
for (const toolRef of step.tools) {
|
|
1721
|
+
if (typeof toolRef === "string") {
|
|
1722
|
+
// Keep as is
|
|
1723
|
+
} else if (typeof toolRef === 'object' && 'id' in toolRef && toolRef.id) {
|
|
1724
|
+
availableTools.set(toolRef.id, toolRef);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
tool = availableTools.get(prepareOrFinalize);
|
|
1730
|
+
}
|
|
1731
|
+
} else {
|
|
1732
|
+
// Tool object - use directly
|
|
1733
|
+
tool = prepareOrFinalize;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
if (tool) {
|
|
1737
|
+
// Use ToolManager for unified tool execution
|
|
1738
|
+
const toolManager = this.getToolManager();
|
|
1739
|
+
let result;
|
|
1740
|
+
|
|
1741
|
+
if (toolManager) {
|
|
1742
|
+
result = await toolManager.executeTool({
|
|
1743
|
+
tool,
|
|
1744
|
+
context,
|
|
1745
|
+
updateContext: this.agent.updateContext.bind(this.agent),
|
|
1746
|
+
updateData: this.agent.updateCollectedData.bind(this.agent),
|
|
1747
|
+
history: [], // Empty history for prepare/finalize
|
|
1748
|
+
data,
|
|
1749
|
+
});
|
|
1750
|
+
} else {
|
|
1751
|
+
// Fallback: execute tool directly if ToolManager not available
|
|
1752
|
+
throw new Error(`ToolManager not available for prepare/finalize tool execution: ${typeof prepareOrFinalize === "string" ? prepareOrFinalize : "inline tool"}`);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
if (!result.success) {
|
|
1756
|
+
logger.error(
|
|
1757
|
+
`[ResponseModal] Tool execution failed in prepare/finalize: ${result.error}`
|
|
1758
|
+
);
|
|
1759
|
+
throw new Error(`Tool execution failed: ${result.error}`);
|
|
1760
|
+
}
|
|
1761
|
+
} else {
|
|
1762
|
+
logger.warn(
|
|
1763
|
+
`[ResponseModal] Tool not found for prepare/finalize: ${typeof prepareOrFinalize === "string"
|
|
1764
|
+
? prepareOrFinalize
|
|
1765
|
+
: "inline tool"
|
|
1766
|
+
}`
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
/**
|
|
1773
|
+
* Merge terms with route-specific taking precedence on conflicts
|
|
1774
|
+
* @private
|
|
1775
|
+
*/
|
|
1776
|
+
private mergeTerms(
|
|
1777
|
+
agentTerms: Term<TContext, TData>[],
|
|
1778
|
+
routeTerms: Term<TContext, TData>[]
|
|
1779
|
+
): Term<TContext, TData>[] {
|
|
1780
|
+
const merged = new Map<string, Term<TContext, TData>>();
|
|
1781
|
+
|
|
1782
|
+
// Add agent terms first
|
|
1783
|
+
agentTerms.forEach((term) => {
|
|
1784
|
+
const name =
|
|
1785
|
+
typeof term.name === "string" ? term.name : term.name.toString();
|
|
1786
|
+
merged.set(name, term);
|
|
1787
|
+
});
|
|
1788
|
+
|
|
1789
|
+
// Add route terms (these take precedence)
|
|
1790
|
+
routeTerms.forEach((term) => {
|
|
1791
|
+
const name =
|
|
1792
|
+
typeof term.name === "string" ? term.name : term.name.toString();
|
|
1793
|
+
merged.set(name, term);
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
return Array.from(merged.values());
|
|
1797
|
+
}
|
|
1798
|
+
}
|