@falai/agent 3.4.3 → 3.4.4

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.
Files changed (110) hide show
  1. package/dist/cjs/adapters/MemoryAdapter.d.ts +47 -0
  2. package/dist/cjs/adapters/MemoryAdapter.d.ts.map +1 -0
  3. package/dist/cjs/adapters/MemoryAdapter.js +208 -0
  4. package/dist/cjs/adapters/MemoryAdapter.js.map +1 -0
  5. package/dist/cjs/adapters/MongoAdapter.d.ts +97 -0
  6. package/dist/cjs/adapters/MongoAdapter.d.ts.map +1 -0
  7. package/dist/cjs/adapters/MongoAdapter.js +200 -0
  8. package/dist/cjs/adapters/MongoAdapter.js.map +1 -0
  9. package/dist/cjs/adapters/OpenSearchAdapter.d.ts +169 -0
  10. package/dist/cjs/adapters/OpenSearchAdapter.d.ts.map +1 -0
  11. package/dist/cjs/adapters/OpenSearchAdapter.js +475 -0
  12. package/dist/cjs/adapters/OpenSearchAdapter.js.map +1 -0
  13. package/dist/cjs/adapters/PostgreSQLAdapter.d.ts +85 -0
  14. package/dist/cjs/adapters/PostgreSQLAdapter.d.ts.map +1 -0
  15. package/dist/cjs/adapters/PostgreSQLAdapter.js +312 -0
  16. package/dist/cjs/adapters/PostgreSQLAdapter.js.map +1 -0
  17. package/dist/cjs/adapters/PrismaAdapter.d.ts +115 -0
  18. package/dist/cjs/adapters/PrismaAdapter.d.ts.map +1 -0
  19. package/dist/cjs/adapters/PrismaAdapter.js +410 -0
  20. package/dist/cjs/adapters/PrismaAdapter.js.map +1 -0
  21. package/dist/cjs/adapters/RedisAdapter.d.ts +72 -0
  22. package/dist/cjs/adapters/RedisAdapter.d.ts.map +1 -0
  23. package/dist/cjs/adapters/RedisAdapter.js +290 -0
  24. package/dist/cjs/adapters/RedisAdapter.js.map +1 -0
  25. package/dist/cjs/adapters/SQLiteAdapter.d.ts +86 -0
  26. package/dist/cjs/adapters/SQLiteAdapter.d.ts.map +1 -0
  27. package/dist/cjs/adapters/SQLiteAdapter.js +341 -0
  28. package/dist/cjs/adapters/SQLiteAdapter.js.map +1 -0
  29. package/dist/cjs/adapters/index.d.ts +17 -0
  30. package/dist/cjs/adapters/index.d.ts.map +1 -0
  31. package/dist/cjs/adapters/index.js +21 -0
  32. package/dist/cjs/adapters/index.js.map +1 -0
  33. package/dist/cjs/adapters/sessionRow.d.ts +22 -0
  34. package/dist/cjs/adapters/sessionRow.d.ts.map +1 -0
  35. package/dist/cjs/adapters/sessionRow.js +52 -0
  36. package/dist/cjs/adapters/sessionRow.js.map +1 -0
  37. package/dist/cjs/constants/index.d.ts +1 -0
  38. package/dist/cjs/constants/index.d.ts.map +1 -0
  39. package/dist/cjs/constants/index.js +4 -0
  40. package/dist/cjs/constants/index.js.map +1 -0
  41. package/dist/cjs/core/Agent.d.ts +382 -0
  42. package/dist/cjs/core/Agent.d.ts.map +1 -0
  43. package/dist/cjs/core/Agent.js +1198 -0
  44. package/dist/cjs/core/Agent.js.map +1 -0
  45. package/dist/cjs/core/ResponseModal.d.ts +305 -0
  46. package/dist/cjs/core/ResponseModal.d.ts.map +1 -0
  47. package/dist/cjs/core/ResponseModal.js +1414 -0
  48. package/dist/cjs/core/ResponseModal.js.map +1 -0
  49. package/dist/cjs/core/ToolLoopExecutor.d.ts +133 -0
  50. package/dist/cjs/core/ToolLoopExecutor.d.ts.map +1 -0
  51. package/dist/cjs/core/ToolLoopExecutor.js +568 -0
  52. package/dist/cjs/core/ToolLoopExecutor.js.map +1 -0
  53. package/dist/cjs/core/createAgent.d.ts +35 -0
  54. package/dist/cjs/core/createAgent.d.ts.map +1 -0
  55. package/dist/cjs/core/createAgent.js +39 -0
  56. package/dist/cjs/core/createAgent.js.map +1 -0
  57. package/dist/cjs/index.d.ts +61 -0
  58. package/dist/cjs/index.d.ts.map +1 -0
  59. package/dist/cjs/index.js +113 -0
  60. package/dist/cjs/index.js.map +1 -0
  61. package/dist/cjs/package.json +1 -0
  62. package/dist/cjs/providers/AnthropicProvider.d.ts +46 -0
  63. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -0
  64. package/dist/cjs/providers/AnthropicProvider.js +50 -0
  65. package/dist/cjs/providers/AnthropicProvider.js.map +1 -0
  66. package/dist/cjs/providers/DeepSeekProvider.d.ts +47 -0
  67. package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -0
  68. package/dist/cjs/providers/DeepSeekProvider.js +42 -0
  69. package/dist/cjs/providers/DeepSeekProvider.js.map +1 -0
  70. package/dist/cjs/providers/FallbackAiProvider.d.ts +26 -0
  71. package/dist/cjs/providers/FallbackAiProvider.d.ts.map +1 -0
  72. package/dist/cjs/providers/FallbackAiProvider.js +54 -0
  73. package/dist/cjs/providers/FallbackAiProvider.js.map +1 -0
  74. package/dist/cjs/providers/GeminiProvider.d.ts +51 -0
  75. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -0
  76. package/dist/cjs/providers/GeminiProvider.js +53 -0
  77. package/dist/cjs/providers/GeminiProvider.js.map +1 -0
  78. package/dist/cjs/providers/GenericOpenAICompatibleProvider.d.ts +80 -0
  79. package/dist/cjs/providers/GenericOpenAICompatibleProvider.d.ts.map +1 -0
  80. package/dist/cjs/providers/GenericOpenAICompatibleProvider.js +89 -0
  81. package/dist/cjs/providers/GenericOpenAICompatibleProvider.js.map +1 -0
  82. package/dist/cjs/providers/OpenAICompatibleProvider.d.ts +61 -0
  83. package/dist/cjs/providers/OpenAICompatibleProvider.d.ts.map +1 -0
  84. package/dist/cjs/providers/OpenAICompatibleProvider.js +58 -0
  85. package/dist/cjs/providers/OpenAICompatibleProvider.js.map +1 -0
  86. package/dist/cjs/providers/OpenAIProvider.d.ts +40 -0
  87. package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -0
  88. package/dist/cjs/providers/OpenAIProvider.js +42 -0
  89. package/dist/cjs/providers/OpenAIProvider.js.map +1 -0
  90. package/dist/cjs/providers/OpenRouterProvider.d.ts +56 -0
  91. package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -0
  92. package/dist/cjs/providers/OpenRouterProvider.js +45 -0
  93. package/dist/cjs/providers/OpenRouterProvider.js.map +1 -0
  94. package/dist/cjs/providers/ProviderAdapter.d.ts +126 -0
  95. package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -0
  96. package/dist/cjs/providers/ProviderAdapter.js +313 -0
  97. package/dist/cjs/providers/ProviderAdapter.js.map +1 -0
  98. package/dist/cjs/providers/ZaiProvider.d.ts +45 -0
  99. package/dist/cjs/providers/ZaiProvider.d.ts.map +1 -0
  100. package/dist/cjs/providers/ZaiProvider.js +51 -0
  101. package/dist/cjs/providers/ZaiProvider.js.map +1 -0
  102. package/dist/cjs/providers/index.d.ts +26 -0
  103. package/dist/cjs/providers/index.d.ts.map +1 -0
  104. package/dist/cjs/providers/index.js +30 -0
  105. package/dist/cjs/providers/index.js.map +1 -0
  106. package/dist/cjs/utils/streamingMessage.d.ts +48 -0
  107. package/dist/cjs/utils/streamingMessage.d.ts.map +1 -0
  108. package/dist/cjs/utils/streamingMessage.js +210 -0
  109. package/dist/cjs/utils/streamingMessage.js.map +1 -0
  110. package/package.json +1 -1
@@ -0,0 +1,1414 @@
1
+ "use strict";
2
+ /**
3
+ * ResponseModal handles all response generation logic for the Agent
4
+ * Provides both streaming and non-streaming response generation with unified logic
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.ResponseModal = void 0;
8
+ const ResponseEngine_js_1 = require("./ResponseEngine.js");
9
+ const ResponsePipeline_js_1 = require("./ResponsePipeline.js");
10
+ const AutoChainExecutor_js_1 = require("./AutoChainExecutor.js");
11
+ const StepLifecycle_js_1 = require("./StepLifecycle.js");
12
+ const SessionFinalizer_js_1 = require("./SessionFinalizer.js");
13
+ const ToolLoopExecutor_js_1 = require("./ToolLoopExecutor.js");
14
+ const flow_namespace_js_1 = require("./flow-namespace.js");
15
+ const SignalCoordinator_js_1 = require("./SignalCoordinator.js");
16
+ const ResponseGenerationError_js_1 = require("./ResponseGenerationError.js");
17
+ const errors_js_1 = require("../types/errors.js");
18
+ const index_js_1 = require("../utils/index.js");
19
+ const template_js_1 = require("../utils/template.js");
20
+ const streamingMessage_js_1 = require("../utils/streamingMessage.js");
21
+ const json_js_1 = require("../utils/json.js");
22
+ /**
23
+ * ResponseModal class that encapsulates all response generation logic
24
+ * Uses unified approach for both streaming and non-streaming responses
25
+ */
26
+ class ResponseModal {
27
+ constructor(agent, options) {
28
+ this.agent = agent;
29
+ this.options = options;
30
+ // Initialize response engine
31
+ this.responseEngine = new ResponseEngine_js_1.ResponseEngine(this.agent.promptSectionCache);
32
+ // Signal pre/post phase orchestration
33
+ this.signalCoordinator = new SignalCoordinator_js_1.SignalCoordinator({
34
+ getFlows: () => this.agent.getFlows(),
35
+ signalProcessor: this.agent.signalProcessor,
36
+ });
37
+ // Initialize response pipeline with agent dependencies
38
+ this.responsePipeline = new ResponsePipeline_js_1.ResponsePipeline(this.agent.getAgentOptions(), () => this.agent.getFlows(), // Pass a function to get flows dynamically
39
+ this.agent.getFlowRouter(), this.signalCoordinator, this.agent.updateCollectedData.bind(this.agent), () => this.agent.schema);
40
+ // Step prepare/finalize execution, shared by the prepare phase and finalizer
41
+ this.stepLifecycle = new StepLifecycle_js_1.StepLifecycle({
42
+ getFlows: () => this.agent.getFlows(),
43
+ toolManager: this.getToolManager(),
44
+ updateContext: this.agent.updateContext.bind(this.agent),
45
+ updateData: this.agent.updateCollectedData.bind(this.agent),
46
+ });
47
+ // Single owner of end-of-turn finalization (compaction + persistence + sync)
48
+ this.sessionFinalizer = new SessionFinalizer_js_1.SessionFinalizer({
49
+ getCompactionOptions: () => this.agent.getCompactionOptions(),
50
+ getPersistenceManager: () => this.agent.getPersistenceManager(),
51
+ getAgentOptions: () => this.agent.getAgentOptions(),
52
+ getCurrentSession: () => this.agent.currentSession,
53
+ setCurrentSession: (session) => { this.agent.currentSession = session; },
54
+ stepLifecycle: this.stepLifecycle,
55
+ enableAutoSave: this.options?.enableAutoSave,
56
+ });
57
+ // Tool follow-up loop (run tools, ask the LLM again) + streaming batch execution
58
+ this.toolLoopExecutor = new ToolLoopExecutor_js_1.ToolLoopExecutor({
59
+ toolManager: this.getToolManager(),
60
+ getAgentOptions: () => this.agent.getAgentOptions(),
61
+ updateContext: this.agent.updateContext.bind(this.agent),
62
+ updateCollectedData: this.agent.updateCollectedData.bind(this.agent),
63
+ updateSessionData: this.agent.getUpdateDataMethod(),
64
+ maxToolLoops: this.options?.maxToolLoops,
65
+ });
66
+ }
67
+ /**
68
+ * Generate a non-streaming response using unified logic
69
+ */
70
+ async respond(params) {
71
+ // Snapshot the managed session so a failed turn has no in-memory effect:
72
+ // without this, mutations made before the failure leave the live session
73
+ // diverged from persisted state
74
+ const preTurnSession = this.agent.session.current
75
+ ? (0, index_js_1.cloneDeep)(this.agent.session.current)
76
+ : undefined;
77
+ try {
78
+ // Use unified response preparation and routing
79
+ const responseContext = await this.prepareUnifiedResponseContext(params);
80
+ // Generate response using unified logic
81
+ const result = await this.generateUnifiedResponse(responseContext);
82
+ // Finalize session — the non-streaming turn's single finalize
83
+ await this.sessionFinalizer.finalize(result.session, responseContext.effectiveContext);
84
+ return result;
85
+ }
86
+ catch (error) {
87
+ if (preTurnSession) {
88
+ this.agent.session.syncSession(preTurnSession);
89
+ }
90
+ // Typed library errors carry their own semantics (ProviderError.code,
91
+ // SessionConflictError) — rethrow bare so consumers can branch on
92
+ // them instead of string-matching. Everything else wraps with the
93
+ // original attached as `cause`.
94
+ if (error instanceof errors_js_1.ProviderError || error instanceof errors_js_1.SessionConflictError) {
95
+ throw error;
96
+ }
97
+ throw new ResponseGenerationError_js_1.ResponseGenerationError(`[ResponseGenerationError] Response generation failed: ${error instanceof Error ? error.message : String(error)}. ` +
98
+ `Check provider configuration and network connectivity.`, { originalError: error, params, phase: 'response_generation' });
99
+ }
100
+ }
101
+ /**
102
+ * Generate a streaming response using unified logic
103
+ */
104
+ async *respondStream(params) {
105
+ // Same failed-turn rollback semantics as respond()
106
+ const preTurnSession = this.agent.session.current
107
+ ? (0, index_js_1.cloneDeep)(this.agent.session.current)
108
+ : undefined;
109
+ try {
110
+ // Use unified response preparation and routing
111
+ const responseContext = await this.prepareUnifiedResponseContext(params);
112
+ // Generate streaming response using unified logic
113
+ yield* this.generateUnifiedStreamingResponse(responseContext);
114
+ }
115
+ catch (error) {
116
+ if (preTurnSession) {
117
+ this.agent.session.syncSession(preTurnSession);
118
+ }
119
+ // Stream error to caller
120
+ yield {
121
+ delta: "",
122
+ accumulated: "",
123
+ done: true,
124
+ session: params.session || await this.agent.session.getOrCreate(),
125
+ error: new ResponseGenerationError_js_1.ResponseGenerationError(`Streaming response failed: ${error instanceof Error ? error.message : String(error)}`, { originalError: error, params, phase: 'streaming' }),
126
+ };
127
+ }
128
+ }
129
+ /**
130
+ * Modern streaming API - simple interface like chat()
131
+ */
132
+ async *stream(message, options) {
133
+ // Determine which history to use
134
+ let history;
135
+ if (options?.history) {
136
+ // Use provided history for this response only
137
+ history = options.history;
138
+ }
139
+ else {
140
+ // Add user message to session history if provided
141
+ if (message) {
142
+ await this.agent.session.addMessage("user", message);
143
+ }
144
+ history = this.agent.session.getHistory();
145
+ }
146
+ // Get or create session — session.data is the single source of truth,
147
+ // so no agent-side data merge is needed
148
+ const session = await this.agent.session.getOrCreate();
149
+ // Stream response using existing respondStream method
150
+ let finalMessage = "";
151
+ let finalizedSession;
152
+ for await (const chunk of this.respondStream({
153
+ history,
154
+ session,
155
+ contextOverride: options?.contextOverride,
156
+ signal: options?.signal,
157
+ })) {
158
+ // Accumulate the final message and capture finalized session
159
+ if (chunk.done) {
160
+ finalMessage = chunk.accumulated;
161
+ finalizedSession = chunk.session;
162
+ }
163
+ yield chunk;
164
+ }
165
+ // Sync finalized session to agent.session.current (skip in override-history mode)
166
+ // Must happen BEFORE addMessage so the assistant message is added on top of the synced session state
167
+ if (!options?.history && finalizedSession) {
168
+ this.agent.session.syncSession(finalizedSession);
169
+ }
170
+ // Add agent response to session history (only if not using override history)
171
+ if (!options?.history && finalMessage) {
172
+ await this.agent.session.addMessage("assistant", finalMessage);
173
+ }
174
+ }
175
+ /**
176
+ * Modern non-streaming API - equivalent to chat() but more explicit
177
+ */
178
+ async generate(message, options) {
179
+ // Determine which history to use
180
+ let history;
181
+ if (options?.history) {
182
+ // Use provided history for this response only
183
+ history = options.history;
184
+ }
185
+ else {
186
+ // Add user message to session history if provided
187
+ if (message) {
188
+ await this.agent.session.addMessage("user", message);
189
+ }
190
+ history = this.agent.session.getHistory();
191
+ }
192
+ // Get or create session — session.data is the single source of truth,
193
+ // so no agent-side data merge is needed
194
+ const session = await this.agent.session.getOrCreate();
195
+ // Generate response using existing respond method
196
+ const result = await this.respond({
197
+ history,
198
+ session,
199
+ contextOverride: options?.contextOverride,
200
+ signal: options?.signal,
201
+ });
202
+ // Sync finalized session to agent.session.current (skip in override-history mode)
203
+ // Must happen BEFORE addMessage so the assistant message is added on top of the synced session state
204
+ if (!options?.history && result.session) {
205
+ this.agent.session.syncSession(result.session);
206
+ }
207
+ // Add agent response to session history (only if not using override history)
208
+ if (!options?.history) {
209
+ await this.agent.session.addMessage("assistant", result.message);
210
+ }
211
+ // Ensure the result includes the current session
212
+ return {
213
+ ...result,
214
+ session: result.session || this.agent.session.current,
215
+ };
216
+ }
217
+ /**
218
+ * Get the response engine instance
219
+ * @internal
220
+ */
221
+ getResponseEngine() {
222
+ return this.responseEngine;
223
+ }
224
+ /**
225
+ * Get the response pipeline instance
226
+ * @internal
227
+ */
228
+ getResponsePipeline() {
229
+ return this.responsePipeline;
230
+ }
231
+ /**
232
+ * Get the ToolManager instance from the agent.
233
+ * @private
234
+ */
235
+ getToolManager() {
236
+ return this.agent.tool;
237
+ }
238
+ /**
239
+ * Recover a structured payload from a schema-mandated response the provider
240
+ * could not parse. Raw protocol fragments must never surface as the
241
+ * user-visible reply. Three shapes arrive here:
242
+ * - a truncated or fence-wrapped envelope → one repair-parse;
243
+ * - conversational prose FOLLOWED BY the envelope (the model answered
244
+ * twice — the observed WhatsApp leak) → recover the embedded envelope,
245
+ * whose "message" field is the complete intended reply;
246
+ * - plain prose with nothing recoverable → `undefined`; the caller passes
247
+ * it through untouched. (An envelope truncated mid-stream after prose
248
+ * also lands here: the prose is user-worthy and the fragment carries
249
+ * nothing recoverable.)
250
+ * An unrecoverable JSON-SHAPED fragment throws so the turn fails LOUDLY
251
+ * and the caller's rollback/retry path engages instead of leaking
252
+ * `{"message": "…` to an end user.
253
+ */
254
+ salvageStructuredOutput(raw, surface) {
255
+ const salvaged = (0, json_js_1.tryParseJSONResponse)(raw);
256
+ if (salvaged && typeof salvaged.message === "string") {
257
+ index_js_1.logger.warn(`[ResponseModal] Salvaged malformed structured output from ${surface} via JSON repair parse.`);
258
+ return { ...salvaged, message: salvaged.message };
259
+ }
260
+ const embedded = (0, json_js_1.extractEmbeddedJSONObject)(raw);
261
+ if (embedded && typeof embedded.message === "string") {
262
+ index_js_1.logger.warn(`[ResponseModal] Salvaged structured output embedded after prose from ${surface}.`);
263
+ return { ...embedded, message: embedded.message };
264
+ }
265
+ if ((0, json_js_1.isJSONShaped)(raw)) {
266
+ throw ResponseGenerationError_js_1.ResponseGenerationError.fromError(new Error("Model returned a schema-mandated response that could not be parsed as JSON. " +
267
+ `The ${surface} was failed instead of delivering raw protocol output to the user.`), 'structured_output_malformed', { responseSchemaName: 'response_output' });
268
+ }
269
+ return undefined;
270
+ }
271
+ /**
272
+ * Tool-emitted directives (ctx.dispatch / `{directive}` returns): state
273
+ * writes apply now; control flow queues for the next turn's
274
+ * pendingDirective applier (same deferred semantics as dispatch()).
275
+ */
276
+ async applyToolEmittedDirectives(session, d) {
277
+ if (d.dataUpdate) {
278
+ session = (0, index_js_1.mergeCollected)(session, d.dataUpdate);
279
+ }
280
+ if (d.contextUpdate) {
281
+ await this.agent.updateContext(d.contextUpdate);
282
+ }
283
+ const control = { ...d };
284
+ delete control.dataUpdate;
285
+ delete control.contextUpdate;
286
+ if (Object.keys(control).length > 0) {
287
+ flow_namespace_js_1.flow.queuePending(session, control);
288
+ }
289
+ return session;
290
+ }
291
+ /**
292
+ * Collect scoped instructions from agent, flow, and step into a ScopedInstructions value.
293
+ * @private
294
+ */
295
+ collectScopedInstructions(flow, step) {
296
+ return {
297
+ global: this.agent.instructions,
298
+ flow: flow ? { flowTitle: flow.title, items: flow.instructions } : undefined,
299
+ step: step ? { stepId: step.id, items: step.getInstructions() } : undefined,
300
+ };
301
+ }
302
+ // UNIFIED RESPONSE LOGIC - Consolidates common logic between streaming and non-streaming
303
+ // ============================================================================
304
+ /**
305
+ * Unified response preparation - handles context setup, session management, and routing
306
+ * This consolidates common logic between streaming and non-streaming responses
307
+ * @private
308
+ */
309
+ async prepareUnifiedResponseContext(params) {
310
+ try {
311
+ const { history: simpleHistory, contextOverride, signal, message: turnMessage, allowedFlows } = params;
312
+ // Validate input parameters
313
+ if (!simpleHistory) {
314
+ throw new ResponseGenerationError_js_1.ResponseGenerationError('[ResponseGenerationError] Missing history: history is required for response generation. ' +
315
+ 'Pass a valid history array (or pass `message` alongside an existing history base).', { params, phase: 'validation' });
316
+ }
317
+ // `message` is the user turn: appended to what the model sees this
318
+ // turn AND recorded on the returned session's history.
319
+ const history = turnMessage
320
+ ? [...simpleHistory, (0, index_js_1.userMessage)(turnMessage)]
321
+ : simpleHistory;
322
+ // Convert HistoryItem[] to Event[] for internal processing
323
+ const historyEvents = (0, index_js_1.historyToEvents)(history);
324
+ // Use ResponsePipeline for context and session preparation; context
325
+ // and session are passed explicitly — the pipeline holds no state
326
+ let responseContext;
327
+ try {
328
+ responseContext = await this.responsePipeline.prepareResponseContext({
329
+ contextOverride,
330
+ session: params.session ? (0, index_js_1.cloneDeep)(params.session) : undefined,
331
+ currentContext: await this.agent.getContext(),
332
+ currentSession: this.agent.currentSession,
333
+ });
334
+ }
335
+ catch (error) {
336
+ throw ResponseGenerationError_js_1.ResponseGenerationError.fromError(error, 'pipeline_context_preparation', params);
337
+ }
338
+ const { effectiveContext, contextAfterHook } = responseContext;
339
+ let session = responseContext.session;
340
+ // Sync the beforeRespond hook's context result back to the agent
341
+ if (contextAfterHook !== undefined) {
342
+ try {
343
+ await this.agent.updateContext(contextAfterHook);
344
+ }
345
+ catch (error) {
346
+ throw ResponseGenerationError_js_1.ResponseGenerationError.fromError(error, 'context_update_from_pipeline', params, { contextAfterHook });
347
+ }
348
+ }
349
+ // Apply data staged before any session existed (initialData,
350
+ // pre-session updateCollectedData calls). Reading the live session's
351
+ // data here would leak state across sessions when an explicit
352
+ // session is passed, so only the staging buffer is merged.
353
+ const stagedData = this.agent.consumePendingData();
354
+ if (Object.keys(stagedData).length > 0) {
355
+ try {
356
+ session = (0, index_js_1.mergeCollected)(session, stagedData);
357
+ index_js_1.logger.debug("[ResponseModal] Merged staged agent data into session:", stagedData);
358
+ }
359
+ catch (error) {
360
+ throw ResponseGenerationError_js_1.ResponseGenerationError.fromError(error, 'data_merging', params, { stagedData });
361
+ }
362
+ }
363
+ // Record the user turn on the session's own history so the returned
364
+ // session carries the full exchange (respond owns session.history
365
+ // when `message` is used).
366
+ if (turnMessage) {
367
+ session.history = [...(session.history ?? []), (0, index_js_1.userMessage)(turnMessage)];
368
+ }
369
+ // PHASE 1: PREPARE - Execute prepare function if current step has one
370
+ try {
371
+ const prepareDirective = await this.stepLifecycle.runPrepare(session, effectiveContext);
372
+ // Queue the control-flow directive for THIS turn: routing
373
+ // (handleRoutingAndStepSelection) consumes session.pendingDirective
374
+ // before deciding flow/step, so a prepare-phase goTo/goToStep/
375
+ // reset steers the current turn.
376
+ if (prepareDirective) {
377
+ flow_namespace_js_1.flow.queuePending(session, prepareDirective);
378
+ }
379
+ }
380
+ catch (error) {
381
+ throw ResponseGenerationError_js_1.ResponseGenerationError.fromError(error, 'step_preparation', params, { session, effectiveContext });
382
+ }
383
+ // PHASE 2: ROUTING + STEP SELECTION - Determine which flow and step to use
384
+ // Performs pre-extraction and step selection
385
+ let routingResult;
386
+ try {
387
+ routingResult = await this.responsePipeline.routeAndSelectStep({
388
+ session,
389
+ history: historyEvents,
390
+ context: effectiveContext,
391
+ signal,
392
+ allowedFlows,
393
+ });
394
+ }
395
+ catch (error) {
396
+ throw ResponseGenerationError_js_1.ResponseGenerationError.fromError(error, 'routing_and_step_selection', params, { session, effectiveContext });
397
+ }
398
+ return {
399
+ effectiveContext,
400
+ session: routingResult.session,
401
+ history,
402
+ turnMessage,
403
+ selectedFlow: routingResult.selectedFlow,
404
+ selectedStep: routingResult.selectedStep,
405
+ responseDirectives: routingResult.responseDirectives,
406
+ isFlowComplete: routingResult.isFlowComplete,
407
+ signal,
408
+ signalFirings: routingResult.signalFirings,
409
+ signalPreDirective: routingResult.signalPreDirective,
410
+ signalHalted: routingResult.signalHalted,
411
+ signalHaltReply: routingResult.signalHaltReply,
412
+ endedFlows: routingResult.endedFlows,
413
+ };
414
+ }
415
+ catch (error) {
416
+ // Re-throw ResponseGenerationError as-is, wrap others
417
+ if (ResponseGenerationError_js_1.ResponseGenerationError.isResponseGenerationError(error)) {
418
+ throw error;
419
+ }
420
+ throw ResponseGenerationError_js_1.ResponseGenerationError.fromError(error, 'preparation', params);
421
+ }
422
+ }
423
+ /**
424
+ * Plan a turn: run signal-halt detection, the auto-chain walk, and flow/step
425
+ * selection, collapsing them into a single {@link TurnOutcome}. This is the
426
+ * shared decision spine for both the streaming and non-streaming paths — the
427
+ * only logic that genuinely differs between them is how each *renders* the
428
+ * outcome (await a value vs. yield chunks) and the leaf provider primitive it
429
+ * uses. Centralizing the decision here is what keeps the two paths from
430
+ * drifting (the class of bug behind the 2.4.x retry/empty fixes).
431
+ *
432
+ * The returned `session` reflects any auto-chain mutation; `signalFirings`
433
+ * is seeded with the pre-signal phase firings and is the live accumulator the
434
+ * post-phase tail appends to.
435
+ * @private
436
+ */
437
+ async planTurn(responseContext) {
438
+ const { effectiveContext, history, selectedFlow, selectedStep, responseDirectives, isFlowComplete, signal, signalFirings: preSignalFirings, signalPreDirective, signalHalted, signalHaltReply, } = responseContext;
439
+ let session = responseContext.session;
440
+ // Accumulator for signal firings across both phases (fire order)
441
+ const signalFirings = [...(preSignalFirings || [])];
442
+ // Convert HistoryItem[] to Event[] for internal processing
443
+ const historyEvents = (0, index_js_1.historyToEvents)(history);
444
+ const base = { effectiveContext, history, historyEvents, signal, signalFirings };
445
+ // ── SIGNAL HALT (Requirement 8.2) ─────────────────────────────────────
446
+ // Pre-signal phase emitted halt → skip LLM call entirely. The post-signal
447
+ // phase still runs (it sees the complete turn context).
448
+ if (signalHalted) {
449
+ const haltMessage = signalHaltReply || '';
450
+ return {
451
+ ...base, session,
452
+ outcome: { kind: 'halt', message: haltMessage, stoppedReason: haltMessage ? 'reply' : 'halt', runPostPhase: true },
453
+ };
454
+ }
455
+ if (selectedFlow && !isFlowComplete) {
456
+ // AUTO-CHAIN: Walk consecutive auto-steps before any LLM work. If the
457
+ // current step is auto, the executor advances through it (and any
458
+ // subsequent auto-steps) until an interactive step or terminal condition.
459
+ let resolvedStep = selectedStep;
460
+ const currentStepInstance = session.currentStep
461
+ ? selectedFlow.getStep(session.currentStep.id)
462
+ : selectedStep;
463
+ if (currentStepInstance?.auto) {
464
+ const autoChainExecutor = new AutoChainExecutor_js_1.AutoChainExecutor({
465
+ maxAutoStepsPerTurn: this.agent.maxAutoStepsPerTurn,
466
+ });
467
+ const autoResult = await autoChainExecutor.run({
468
+ session,
469
+ context: effectiveContext,
470
+ flow: selectedFlow,
471
+ });
472
+ session = autoResult.session;
473
+ // Halt: emit the verbatim reply, no LLM call. Unlike signal halt,
474
+ // the auto-chain halt is a hard short-circuit that does NOT run the
475
+ // post-signal phase (preserved across both paths).
476
+ if (autoResult.stoppedReason === 'halt') {
477
+ return {
478
+ ...base, session,
479
+ outcome: { kind: 'halt', message: autoResult.mergedDirective?.reply || '', stoppedReason: 'halt', runPostPhase: false },
480
+ };
481
+ }
482
+ // Flow completion or cross-flow redirect from auto-chain: the chain
483
+ // ended without resolving to an interactive step (last_step: no
484
+ // successor; completed: explicit complete; goto: cross-flow redirect).
485
+ if (autoResult.stoppedReason === 'last_step' || autoResult.stoppedReason === 'completed' || autoResult.stoppedReason === 'goto') {
486
+ index_js_1.logger.debug(`[ResponseModal] Auto-chain ended with ${autoResult.stoppedReason}`);
487
+ return {
488
+ ...base, session,
489
+ outcome: { kind: 'flowComplete', selectedFlow, stoppedReason: autoResult.stoppedReason },
490
+ };
491
+ }
492
+ // Normal case: auto-chain resolved to an interactive step.
493
+ resolvedStep = autoResult.resolvedStep;
494
+ }
495
+ return {
496
+ ...base, session,
497
+ outcome: { kind: 'flowStep', selectedFlow, step: resolvedStep, responseDirectives, signalPreDirective },
498
+ };
499
+ }
500
+ if (isFlowComplete && selectedFlow) {
501
+ // Flow completion path: pure state transition, no LLM call. The reason
502
+ // is 'last_step' (implicit terminus — no successor or all skipped).
503
+ index_js_1.logger.debug(`[ResponseModal] Releasing session to idle for completed flow: ${selectedFlow.title}`);
504
+ return {
505
+ ...base, session,
506
+ outcome: { kind: 'flowComplete', selectedFlow, stoppedReason: 'last_step' },
507
+ };
508
+ }
509
+ // Fallback: no flows defined, generate a simple response.
510
+ return { ...base, session, outcome: { kind: 'fallback' } };
511
+ }
512
+ /**
513
+ * The shared post-signal phase tail (Requirement 9.1–9.4). Runs after the
514
+ * turn's message is known and before persistence, so post-phase signals see
515
+ * the complete turn result (assistant message, collected data, tool results)
516
+ * and can override the reply or wire a pendingDirective.
517
+ *
518
+ * `runPostPhase` is false only for the auto-chain halt short-circuit, which
519
+ * deliberately bypasses the post-phase in both paths; that branch still
520
+ * surfaces any pre-phase firings via `triggeredSignals`.
521
+ * @private
522
+ */
523
+ async applyTurnPostPhase(params) {
524
+ const { session, context, historyEvents, message, signalFirings, runPostPhase } = params;
525
+ if (!runPostPhase) {
526
+ return {
527
+ session, message, replyOverridden: false,
528
+ triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
529
+ };
530
+ }
531
+ const post = await this.signalCoordinator.applyPostPhase({ session, context, historyEvents, message });
532
+ signalFirings.push(...post.firings);
533
+ return {
534
+ session: post.session,
535
+ message: post.message,
536
+ replyOverridden: post.replyOverridden ?? false,
537
+ triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
538
+ };
539
+ }
540
+ /**
541
+ * Unified response generation for non-streaming responses.
542
+ * Renders the shared {@link planTurn} outcome by awaiting the leaf primitive
543
+ * and running the shared post-phase tail; respond() owns the single finalize.
544
+ * @private
545
+ */
546
+ async generateUnifiedResponse(responseContext) {
547
+ const plan = await this.planTurn(responseContext);
548
+ const { effectiveContext, history, historyEvents, signal, signalFirings } = plan;
549
+ let session = plan.session;
550
+ let message = '';
551
+ let toolCalls = undefined;
552
+ let executedSteps = [];
553
+ let stoppedReason;
554
+ let isFlowComplete = false;
555
+ let appliedInstructions;
556
+ let runPostPhase = true;
557
+ let tokensUsed;
558
+ const endedFlows = [...(responseContext.endedFlows ?? [])];
559
+ switch (plan.outcome.kind) {
560
+ case 'halt': {
561
+ message = plan.outcome.message;
562
+ stoppedReason = plan.outcome.stoppedReason;
563
+ runPostPhase = plan.outcome.runPostPhase;
564
+ break;
565
+ }
566
+ case 'flowComplete': {
567
+ session = await this.applyFlowCompletion({
568
+ selectedFlow: plan.outcome.selectedFlow,
569
+ session,
570
+ context: effectiveContext,
571
+ history,
572
+ });
573
+ isFlowComplete = true;
574
+ stoppedReason = plan.outcome.stoppedReason;
575
+ endedFlows.push({
576
+ flowId: plan.outcome.selectedFlow.id,
577
+ title: plan.outcome.selectedFlow.title,
578
+ reason: plan.outcome.stoppedReason,
579
+ });
580
+ break;
581
+ }
582
+ case 'flowStep': {
583
+ const result = await this.processFlowResponse({
584
+ selectedFlow: plan.outcome.selectedFlow,
585
+ selectedStep: plan.outcome.step,
586
+ responseDirectives: plan.outcome.responseDirectives,
587
+ session,
588
+ history,
589
+ context: effectiveContext,
590
+ historyEvents,
591
+ signal,
592
+ // Propagate signal pre-directive's appendPrompt for this turn's LLM call (Requirement 8.4)
593
+ transientAppendage: plan.outcome.signalPreDirective?.appendPrompt,
594
+ // Merge signal pre-directive (halt/reply/injectTools) into the pre-LLM bus
595
+ mergedPreDirective: plan.outcome.signalPreDirective,
596
+ });
597
+ message = result.message;
598
+ toolCalls = result.toolCalls;
599
+ session = result.session;
600
+ appliedInstructions = result.appliedInstructions;
601
+ tokensUsed = result.tokensUsed;
602
+ if (plan.outcome.step) {
603
+ executedSteps = [{ id: plan.outcome.step.id, flowId: plan.outcome.selectedFlow.id }];
604
+ }
605
+ // Use stoppedReason from processFlowResponse if set (halt/reply),
606
+ // otherwise default to 'needs_input' for normal LLM responses.
607
+ stoppedReason = result.stoppedReason || 'needs_input';
608
+ break;
609
+ }
610
+ case 'fallback': {
611
+ const fallbackResult = await this.generateFallbackResponse({
612
+ history,
613
+ context: effectiveContext,
614
+ session,
615
+ signal,
616
+ });
617
+ message = fallbackResult.message;
618
+ appliedInstructions = fallbackResult.appliedInstructions;
619
+ break;
620
+ }
621
+ }
622
+ const tail = await this.applyTurnPostPhase({
623
+ session, context: effectiveContext, historyEvents, message, signalFirings, runPostPhase,
624
+ });
625
+ // History ownership: with params.message the returned session carries
626
+ // the full exchange — the user turn was recorded pre-routing, the
627
+ // assistant tail lands here (post-phase, so overridden replies are the
628
+ // ones recorded). Empty tails are skipped.
629
+ if (responseContext.turnMessage && tail.message) {
630
+ tail.session.history = [...(tail.session.history ?? []), (0, index_js_1.assistantMessage)(tail.message)];
631
+ }
632
+ return {
633
+ message: tail.message,
634
+ session: tail.session,
635
+ toolCalls,
636
+ isFlowComplete,
637
+ executedSteps,
638
+ stoppedReason,
639
+ appliedInstructions,
640
+ triggeredSignals: tail.triggeredSignals,
641
+ ...(tokensUsed !== undefined ? { metadata: { tokensUsed } } : {}),
642
+ ...(endedFlows.length > 0 ? { endedFlows } : {}),
643
+ };
644
+ }
645
+ /**
646
+ * Process flow response with unified tool execution and data collection
647
+ * @private
648
+ */
649
+ async processFlowResponse(params) {
650
+ const { selectedFlow, selectedStep, responseDirectives, history, context, historyEvents, signal, transientAppendage, mergedPreDirective } = params;
651
+ let session = params.session;
652
+ // Resolve the step to render (branches win over linear chain; requires enforced)
653
+ const stepResolution = await this.responsePipeline.resolveRenderStep({
654
+ selectedFlow,
655
+ selectedStep,
656
+ session,
657
+ context,
658
+ });
659
+ if (stepResolution.flowTransition) {
660
+ // Flow transition or completion — no local step to render
661
+ // Return empty message with updated session; caller handles flow transition
662
+ return { message: '', session: stepResolution.session };
663
+ }
664
+ const nextStep = stepResolution.nextStep;
665
+ session = stepResolution.session;
666
+ // Build response schema for this flow (with collect fields from step)
667
+ const responseSchema = this.responseEngine.responseSchemaForFlow(selectedFlow, nextStep, this.agent.schema);
668
+ // ── HALT SHORT-CIRCUIT (Requirement 2.5, 2.6, 2.7) ──────────────────────
669
+ // After pre-LLM emissions are merged, if `halt: true` then skip the LLM
670
+ // call entirely. The behavior depends on whether `reply` is also set.
671
+ if (mergedPreDirective?.halt) {
672
+ if (mergedPreDirective.reply) {
673
+ // halt + reply: emit the reply as the assistant message
674
+ index_js_1.logger.debug(`[ResponseModal] Halt with reply — skipping LLM call for step ${nextStep.id}`);
675
+ return { message: mergedPreDirective.reply, session, stoppedReason: 'reply' };
676
+ }
677
+ else {
678
+ // halt without reply: emit empty assistant content
679
+ index_js_1.logger.debug(`[ResponseModal] Halt without reply — skipping LLM call for step ${nextStep.id}`);
680
+ return { message: '', session, stoppedReason: 'halt' };
681
+ }
682
+ }
683
+ // ── STEP.REPLY SHORT-CIRCUIT (Requirement 25.1–25.7, 17.9) ──────────────
684
+ // A step with `reply` set emits a verbatim template response without LLM.
685
+ // onEnter and prepare have already fired normally at this point.
686
+ // If prepare returned a Directive with `reply`, that overrides
687
+ // the step-declared reply (last-emission-wins per Algorithm 4).
688
+ if (nextStep.reply != null) {
689
+ // Determine the effective reply: prepare-emitted reply wins over step-declared
690
+ const effectiveReply = mergedPreDirective?.reply ?? await (0, index_js_1.render)(nextStep.reply, (0, template_js_1.createTemplateContext)({ data: session.data || {}, context, session }));
691
+ index_js_1.logger.debug(`[ResponseModal] Step.reply — skipping LLM call for step ${nextStep.id}`);
692
+ return { message: effectiveReply, session, stoppedReason: 'reply' };
693
+ }
694
+ // Transient appendage: per-turn slot from Directive.appendPrompt.
695
+ // Fresh each turn, never cached, never persisted.
696
+ // Wrapped in try/finally to ensure cleanup even on abnormal termination.
697
+ let turnTransientAppendage = transientAppendage;
698
+ try {
699
+ // Build response prompt
700
+ const { prompt: responsePrompt, appliedInstructions } = await this.responseEngine.buildResponsePrompt({
701
+ flow: selectedFlow,
702
+ currentStep: nextStep,
703
+ rules: [],
704
+ prohibitions: [],
705
+ directives: responseDirectives,
706
+ history: historyEvents,
707
+ agentOptions: this.agent.getAgentOptions(),
708
+ instructions: this.collectScopedInstructions(selectedFlow, nextStep),
709
+ combinedTerms: this.agent.getTerms(),
710
+ context,
711
+ session,
712
+ agentSchema: this.agent.schema,
713
+ transientAppendage: turnTransientAppendage,
714
+ });
715
+ // Collect available tools for AI
716
+ const availableTools = this.collectAvailableTools(selectedFlow, nextStep);
717
+ // Generate message using AI provider
718
+ const agentOptions = this.agent.getAgentOptions();
719
+ const result = await agentOptions.provider.generateMessage({
720
+ prompt: responsePrompt,
721
+ history, // Use HistoryItem[] for AI provider
722
+ context,
723
+ tools: availableTools,
724
+ signal,
725
+ parameters: responseSchema ? { jsonSchema: responseSchema, schemaName: "response_output" } : undefined,
726
+ });
727
+ let structuredData = result.structured;
728
+ let message = structuredData?.message || result.message;
729
+ const tokensUsed = result.metadata?.tokensUsed;
730
+ // A schema was requested but the provider failed to parse the model's
731
+ // JSON (truncated output, fence-wrapped fragments). Raw protocol
732
+ // fragments must never surface as the user-visible reply: attempt one
733
+ // repair-parse, and if that fails fail the turn LOUDLY so the caller's
734
+ // rollback/retry path engages instead of leaking `{"message": "…` to
735
+ // an end user. Plain prose (not JSON-shaped at all) still passes
736
+ // through — it is not a protocol fragment.
737
+ if (!structuredData && responseSchema && message) {
738
+ const salvaged = this.salvageStructuredOutput(message, "turn");
739
+ if (salvaged) {
740
+ structuredData = salvaged;
741
+ message = salvaged.message;
742
+ }
743
+ }
744
+ const effectiveResult = structuredData ? { ...result, structured: structuredData } : result;
745
+ let toolCalls = structuredData?.toolCalls;
746
+ // Execute tools with unified loop handling
747
+ const toolResult = await this.toolLoopExecutor.runLoop({
748
+ toolCalls,
749
+ context,
750
+ session,
751
+ history,
752
+ selectedFlow,
753
+ responsePrompt,
754
+ availableTools,
755
+ responseSchema,
756
+ signal,
757
+ });
758
+ session = toolResult.session;
759
+ toolCalls = toolResult.finalToolCalls;
760
+ let toolStructured = toolResult.structured;
761
+ if (toolResult.finalMessage) {
762
+ // The tool loop's follow-up calls carry the SAME response schema,
763
+ // and their message replaces the one the guard above already
764
+ // cleared — so an envelope produced after the tools ran reaches
765
+ // the user through a path that guard never sees. (Observed
766
+ // 2026-09-11: a catalog lookup answered, then the closing call
767
+ // returned `{"message": "…"}` raw to a WhatsApp customer.)
768
+ const salvaged = responseSchema
769
+ ? this.salvageStructuredOutput(toolResult.finalMessage, "turn")
770
+ : undefined;
771
+ message = salvaged?.message ?? toolResult.finalMessage;
772
+ if (salvaged)
773
+ toolStructured = salvaged;
774
+ }
775
+ // Tool-emitted directives (ctx.dispatch / `{directive}` returns):
776
+ // state writes apply now; control flow queues for the next turn's
777
+ // pendingDirective applier (same deferred semantics as dispatch()).
778
+ if (toolResult.directives) {
779
+ session = await this.applyToolEmittedDirectives(session, toolResult.directives);
780
+ }
781
+ // Collect data from response
782
+ // Use follow-up structured data from tool loop when available, fall back to original result
783
+ const dataSource = toolStructured
784
+ ? { structured: toolStructured }
785
+ : effectiveResult;
786
+ session = await this.collectDataFromResponse({ result: dataSource, selectedFlow, nextStep, session });
787
+ return { message, toolCalls, session, appliedInstructions, tokensUsed };
788
+ }
789
+ finally {
790
+ // Drain the transient appendage at end of turn.
791
+ // This ensures Directive.appendPrompt does not leak to subsequent
792
+ // turns even when the turn terminates abnormally (error, abort, reject).
793
+ turnTransientAppendage = undefined;
794
+ }
795
+ }
796
+ /**
797
+ * Unified streaming response generation.
798
+ * Renders the shared {@link planTurn} outcome as a chunk stream and runs the
799
+ * shared post-phase tail on the final chunk (finalizing exactly once).
800
+ * @private
801
+ */
802
+ async *generateUnifiedStreamingResponse(responseContext) {
803
+ const plan = await this.planTurn(responseContext);
804
+ const { effectiveContext, history, historyEvents, signal, signalFirings } = plan;
805
+ const session = plan.session;
806
+ // Build the inner chunk stream for the planned outcome. `runPostPhase` is
807
+ // the single post-phase gate (false only for auto-chain halt).
808
+ let innerStream;
809
+ let runPostPhase = true;
810
+ switch (plan.outcome.kind) {
811
+ case 'halt': {
812
+ runPostPhase = plan.outcome.runPostPhase;
813
+ innerStream = this.streamTerminalMessage({
814
+ message: plan.outcome.message,
815
+ stoppedReason: plan.outcome.stoppedReason,
816
+ session,
817
+ });
818
+ break;
819
+ }
820
+ case 'flowComplete': {
821
+ innerStream = this.streamFlowCompletion({
822
+ selectedFlow: plan.outcome.selectedFlow,
823
+ session,
824
+ context: effectiveContext,
825
+ history,
826
+ historyEvents,
827
+ stoppedReason: plan.outcome.stoppedReason,
828
+ });
829
+ break;
830
+ }
831
+ case 'flowStep': {
832
+ innerStream = this.processFlowStreamingResponse({
833
+ selectedFlow: plan.outcome.selectedFlow,
834
+ selectedStep: plan.outcome.step,
835
+ responseDirectives: plan.outcome.responseDirectives,
836
+ session,
837
+ history,
838
+ context: effectiveContext,
839
+ historyEvents,
840
+ signal,
841
+ transientAppendage: plan.outcome.signalPreDirective?.appendPrompt,
842
+ mergedPreDirective: plan.outcome.signalPreDirective,
843
+ });
844
+ break;
845
+ }
846
+ case 'fallback': {
847
+ innerStream = this.streamFallbackResponse({
848
+ history,
849
+ context: effectiveContext,
850
+ session,
851
+ signal,
852
+ });
853
+ break;
854
+ }
855
+ }
856
+ // ── Intercept the inner stream on the final chunk ──────────────────────
857
+ // Mirrors the non-streaming tail: post-signal phase runs first (when
858
+ // applicable), then the session is finalized exactly once, attaching
859
+ // triggeredSignals to the final chunk (Requirement 11.2).
860
+ for await (const chunk of innerStream) {
861
+ if (chunk.done) {
862
+ const tail = await this.applyTurnPostPhase({
863
+ session: chunk.session || session,
864
+ context: effectiveContext,
865
+ historyEvents,
866
+ message: chunk.accumulated,
867
+ signalFirings,
868
+ runPostPhase,
869
+ });
870
+ const accumulated = tail.message;
871
+ const delta = tail.replyOverridden ? accumulated : chunk.delta;
872
+ // History ownership (parity with the sync tail): with
873
+ // params.message the returned session carries the full
874
+ // exchange — the user turn was recorded pre-routing, the
875
+ // assistant tail lands here, BEFORE finalize persists it.
876
+ if (responseContext.turnMessage && tail.message) {
877
+ tail.session.history = [...(tail.session.history ?? []), (0, index_js_1.assistantMessage)(tail.message)];
878
+ }
879
+ // Single streaming exit: finalize the post-phase session so
880
+ // post-signal mutations (e.g. pendingDirective) are persisted.
881
+ await this.sessionFinalizer.finalize(tail.session, effectiveContext);
882
+ yield {
883
+ ...chunk,
884
+ delta,
885
+ accumulated,
886
+ session: tail.session,
887
+ triggeredSignals: tail.triggeredSignals,
888
+ };
889
+ }
890
+ else {
891
+ yield chunk;
892
+ }
893
+ }
894
+ }
895
+ /**
896
+ * Emit a framework-authored message (a halt reply) as a single terminal
897
+ * chunk, to flow through the shared post-phase tail like any other inner
898
+ * stream. No LLM call, no provider text — so nothing to extract or finalize
899
+ * here; the caller's tail owns post-phase + finalize.
900
+ * @private
901
+ */
902
+ // eslint-disable-next-line @typescript-eslint/require-await -- yield-only async generator; must be `async *` to satisfy the AsyncGenerator return type the caller switches on
903
+ async *streamTerminalMessage(params) {
904
+ yield {
905
+ delta: params.message,
906
+ accumulated: params.message,
907
+ done: true,
908
+ session: params.session,
909
+ toolCalls: undefined,
910
+ isFlowComplete: false,
911
+ stoppedReason: params.stoppedReason,
912
+ executedSteps: [],
913
+ };
914
+ }
915
+ /**
916
+ * Wrap a provider message stream so each chunk's `delta`/`accumulated` carry
917
+ * clean message text instead of the raw structured-JSON wrapper. The single
918
+ * point where streamed JSON is unwrapped — every streaming response variant
919
+ * (flow step, fallback) consumes provider chunks through here, so consumers
920
+ * and stored history never see `{"message":...}` fragments. `structured`,
921
+ * `done`, and `metadata` pass through untouched.
922
+ * @private
923
+ */
924
+ async *decodeMessageStream(stream) {
925
+ const decoder = new streamingMessage_js_1.StreamingMessageDecoder();
926
+ for await (const chunk of stream) {
927
+ const clean = decoder.push(chunk.accumulated);
928
+ yield { ...chunk, delta: clean.delta, accumulated: clean.message };
929
+ }
930
+ }
931
+ /**
932
+ * Process flow streaming response with unified tool execution and data collection
933
+ * @private
934
+ */
935
+ async *processFlowStreamingResponse(params) {
936
+ const { selectedFlow, selectedStep, responseDirectives, history, context, historyEvents, signal, transientAppendage, mergedPreDirective } = params;
937
+ let session = params.session;
938
+ // Resolve the step to render (same logic as non-streaming)
939
+ const stepResolution = await this.responsePipeline.resolveRenderStep({
940
+ selectedFlow,
941
+ selectedStep,
942
+ session,
943
+ context,
944
+ });
945
+ if (stepResolution.flowTransition) {
946
+ // Flow transition or completion — no step to render
947
+ yield {
948
+ delta: '',
949
+ accumulated: '',
950
+ done: true,
951
+ session: stepResolution.session,
952
+ };
953
+ return;
954
+ }
955
+ const nextStep = stepResolution.nextStep;
956
+ session = stepResolution.session;
957
+ // Build response schema and prompt (same as non-streaming)
958
+ const responseSchema = this.responseEngine.responseSchemaForFlow(selectedFlow, nextStep, this.agent.schema);
959
+ // ── HALT SHORT-CIRCUIT (Requirement 2.5, 2.6, 2.7) ──────────────────────
960
+ // After pre-LLM emissions are merged, if `halt: true` then skip the LLM
961
+ // call entirely. Emit a single done chunk with the appropriate content.
962
+ if (mergedPreDirective?.halt) {
963
+ const reply = mergedPreDirective.reply || '';
964
+ const reason = mergedPreDirective.reply ? 'reply' : 'halt';
965
+ index_js_1.logger.debug(`[ResponseModal] Halt (streaming) — skipping LLM call for step ${nextStep.id}, stoppedReason: ${reason}`);
966
+ yield {
967
+ delta: reply,
968
+ accumulated: reply,
969
+ done: true,
970
+ session,
971
+ stoppedReason: reason,
972
+ executedSteps: [{ id: nextStep.id, flowId: selectedFlow.id }],
973
+ };
974
+ return;
975
+ }
976
+ // ── STEP.REPLY SHORT-CIRCUIT (Requirement 25.1–25.7, 17.9) ──────────────
977
+ // A step with `reply` set emits a verbatim template response without LLM.
978
+ // onEnter and prepare have already fired normally. If prepare returned
979
+ // a Directive with `reply`, that overrides the step-declared reply.
980
+ if (nextStep.reply != null) {
981
+ const effectiveReply = mergedPreDirective?.reply ?? await (0, index_js_1.render)(nextStep.reply, (0, template_js_1.createTemplateContext)({ data: session.data || {}, context, session }));
982
+ index_js_1.logger.debug(`[ResponseModal] Step.reply (streaming) — skipping LLM call for step ${nextStep.id}`);
983
+ yield {
984
+ delta: effectiveReply,
985
+ accumulated: effectiveReply,
986
+ done: true,
987
+ session,
988
+ stoppedReason: 'reply',
989
+ executedSteps: [{ id: nextStep.id, flowId: selectedFlow.id }],
990
+ };
991
+ return;
992
+ }
993
+ // Transient appendage: per-turn slot from Directive.appendPrompt.
994
+ // Fresh each turn, never cached, never persisted.
995
+ // Wrapped in try/finally to ensure cleanup even on abnormal termination.
996
+ let turnTransientAppendage = transientAppendage;
997
+ try {
998
+ const { prompt: responsePrompt, appliedInstructions } = await this.responseEngine.buildResponsePrompt({
999
+ flow: selectedFlow,
1000
+ currentStep: nextStep,
1001
+ rules: [],
1002
+ prohibitions: [],
1003
+ directives: responseDirectives,
1004
+ history: historyEvents,
1005
+ agentOptions: this.agent.getAgentOptions(),
1006
+ instructions: this.collectScopedInstructions(selectedFlow, nextStep),
1007
+ combinedTerms: this.agent.getTerms(),
1008
+ context,
1009
+ session,
1010
+ agentSchema: this.agent.schema,
1011
+ transientAppendage: turnTransientAppendage,
1012
+ });
1013
+ // Collect available tools for AI
1014
+ const availableTools = this.collectAvailableTools(selectedFlow, nextStep);
1015
+ // Generate message stream using AI provider
1016
+ const agentOptions = this.agent.getAgentOptions();
1017
+ const stream = agentOptions.provider.generateMessageStream({
1018
+ prompt: responsePrompt,
1019
+ history, // Use HistoryItem[] for AI provider
1020
+ context,
1021
+ tools: availableTools,
1022
+ signal,
1023
+ parameters: { jsonSchema: responseSchema, schemaName: "response_stream_output" },
1024
+ });
1025
+ // Stream chunks with unified tool handling. decodeMessageStream gives
1026
+ // each chunk clean message text in delta/accumulated, so the non-done
1027
+ // deltas, the final accumulated, the post-phase message input, and the
1028
+ // assistant message stored by stream() are all clean — never the raw
1029
+ // JSON wrapper (matching the non-streaming structured.message extraction).
1030
+ for await (const chunk of this.decodeMessageStream(stream)) {
1031
+ let toolCalls = undefined;
1032
+ // Final message/structured may be replaced by a forced post-tool
1033
+ // response (see runStreamingBatch / gap: tools-ran-but-no-text).
1034
+ let finalDelta = chunk.delta;
1035
+ let finalAccumulated = chunk.accumulated;
1036
+ let finalStructured = chunk.structured;
1037
+ // Extract tool calls from AI response on final chunk
1038
+ if (chunk.done && chunk.structured?.toolCalls) {
1039
+ toolCalls = chunk.structured.toolCalls;
1040
+ // Concurrent execution for the initial batch of tool calls,
1041
+ // yielding tool-progress chunks as they arrive. The accumulated
1042
+ // preamble is already clean text.
1043
+ const batchResult = yield* this.toolLoopExecutor.runStreamingBatch({
1044
+ toolCalls,
1045
+ context,
1046
+ session,
1047
+ history,
1048
+ selectedFlow,
1049
+ step: nextStep,
1050
+ accumulated: chunk.accumulated,
1051
+ responsePrompt,
1052
+ availableTools,
1053
+ responseSchema,
1054
+ signal,
1055
+ });
1056
+ session = batchResult.session;
1057
+ toolCalls = batchResult.toolCalls;
1058
+ // Tool-emitted directives (ctx.dispatch / `{directive}`):
1059
+ // state writes apply now; control flow queues for the next
1060
+ // turn's pendingDirective applier (same deferred semantics
1061
+ // as dispatch()). A verbatim tool reply already replaced the
1062
+ // closing message inside runStreamingBatch.
1063
+ if (batchResult.directives) {
1064
+ session = await this.applyToolEmittedDirectives(session, batchResult.directives);
1065
+ }
1066
+ // Prefer the post-tool follow-up structured for collection and
1067
+ // emission whenever present — independent of whether a closing
1068
+ // message was forced — matching the non-streaming path's
1069
+ // `toolResult.structured ?? result` selection.
1070
+ finalStructured = batchResult.structured ?? finalStructured;
1071
+ // Tools ran but the model produced no result-aware text — use
1072
+ // the forced closing message (already clean) so we never emit the
1073
+ // bare preamble (or an empty message) as the final response. Its
1074
+ // delta is the portion not already streamed as the preamble.
1075
+ if (batchResult.finalMessage) {
1076
+ finalAccumulated = batchResult.finalMessage;
1077
+ finalDelta = batchResult.finalMessage.startsWith(chunk.accumulated)
1078
+ ? batchResult.finalMessage.slice(chunk.accumulated.length)
1079
+ : batchResult.finalMessage;
1080
+ }
1081
+ }
1082
+ // Streaming twin of the non-streaming salvage guard: a schema was
1083
+ // requested but no structured payload arrived, and the accumulated
1084
+ // text is JSON-shaped (a protocol fragment) — repair-parse it or
1085
+ // fail the turn rather than leaking raw output to the user.
1086
+ if (chunk.done && !finalStructured && responseSchema && finalAccumulated) {
1087
+ const salvaged = this.salvageStructuredOutput(finalAccumulated, "stream");
1088
+ if (salvaged) {
1089
+ finalDelta = salvaged.message.startsWith(finalAccumulated)
1090
+ ? salvaged.message.slice(finalAccumulated.length)
1091
+ : salvaged.message;
1092
+ finalStructured = salvaged;
1093
+ finalAccumulated = salvaged.message;
1094
+ }
1095
+ }
1096
+ // Collect data on the final chunk for any flow step — flow
1097
+ // required/optional fields are valid targets even without a step
1098
+ // `collect` — preferring the post-tool follow-up structured so a
1099
+ // tool-driven turn harvests fields the model produced after tools.
1100
+ if (chunk.done && finalStructured) {
1101
+ session = await this.collectDataFromResponse({
1102
+ result: { structured: finalStructured },
1103
+ selectedFlow,
1104
+ nextStep,
1105
+ session,
1106
+ });
1107
+ }
1108
+ // Response structure completeness (Requirement 8.1, 8.2, 8.3)
1109
+ // - executedSteps: single step executed in this response
1110
+ // - stoppedReason: 'needs_input' for single-step execution (waiting for user input)
1111
+ // - session.currentStep: reflects the executed step
1112
+ yield {
1113
+ delta: finalDelta,
1114
+ accumulated: finalAccumulated,
1115
+ done: chunk.done,
1116
+ session,
1117
+ toolCalls,
1118
+ isFlowComplete: false,
1119
+ executedSteps: chunk.done ? [{ id: nextStep.id, flowId: selectedFlow.id }] : undefined,
1120
+ stoppedReason: chunk.done ? 'needs_input' : undefined,
1121
+ metadata: chunk.metadata,
1122
+ structured: finalStructured,
1123
+ appliedInstructions: chunk.done ? appliedInstructions : undefined,
1124
+ };
1125
+ }
1126
+ }
1127
+ finally {
1128
+ // Drain the transient appendage at end of turn.
1129
+ // This ensures Directive.appendPrompt does not leak to subsequent
1130
+ // turns even when the turn terminates abnormally (error, abort, reject).
1131
+ turnTransientAppendage = undefined;
1132
+ }
1133
+ }
1134
+ /**
1135
+ * Unified data collection from AI response
1136
+ * @private
1137
+ */
1138
+ async collectDataFromResponse(params) {
1139
+ try {
1140
+ const { result, selectedFlow, nextStep, session } = params;
1141
+ let updatedSession = session;
1142
+ // Extract collected data from final response (only for flow-based interactions)
1143
+ if (selectedFlow && result.structured) {
1144
+ try {
1145
+ const collectedData = {};
1146
+ // AgentStructuredResponse extends Record<string, unknown>, so we can safely access properties
1147
+ const structuredData = result.structured;
1148
+ // Collect ALL flow fields (required + optional) from structured response
1149
+ const allFlowFields = new Set();
1150
+ // Add flow required fields
1151
+ if (selectedFlow.requiredFields) {
1152
+ selectedFlow.requiredFields.forEach(field => allFlowFields.add(String(field)));
1153
+ }
1154
+ // Add flow optional fields
1155
+ if (selectedFlow.optionalFields) {
1156
+ selectedFlow.optionalFields.forEach(field => allFlowFields.add(String(field)));
1157
+ }
1158
+ // Also include current step's collect fields (in case they're not in flow fields)
1159
+ if (nextStep?.collect) {
1160
+ nextStep.collect.forEach(field => allFlowFields.add(String(field)));
1161
+ }
1162
+ // Extract all available fields from structured response
1163
+ for (const field of allFlowFields) {
1164
+ const fieldKey = String(field);
1165
+ if (fieldKey in structuredData && structuredData[fieldKey] !== undefined && structuredData[fieldKey] !== null) {
1166
+ collectedData[fieldKey] = structuredData[fieldKey];
1167
+ }
1168
+ }
1169
+ // Merge collected data into session using agent-level data validation
1170
+ if (Object.keys(collectedData).length > 0) {
1171
+ try {
1172
+ // Update agent-level collected data with validation
1173
+ await this.agent.updateCollectedData(collectedData);
1174
+ // Update session with validated data
1175
+ const updateDataMethod = this.agent.getUpdateDataMethod();
1176
+ updatedSession = await updateDataMethod(updatedSession, collectedData);
1177
+ index_js_1.logger.debug(`[ResponseModal] Collected data:`, collectedData);
1178
+ }
1179
+ catch (error) {
1180
+ index_js_1.logger.error(`[ResponseModal] Failed to update collected data:`, error);
1181
+ // Continue without updating data rather than failing completely
1182
+ }
1183
+ }
1184
+ }
1185
+ catch (error) {
1186
+ index_js_1.logger.error(`[ResponseModal] Error during data collection:`, error);
1187
+ // Continue without collecting data rather than failing completely
1188
+ }
1189
+ }
1190
+ // Extract any additional data from structured response
1191
+ // Since AgentStructuredResponse extends Record<string, unknown>, we can safely check for additional properties
1192
+ if (result.structured && "contextUpdate" in result.structured) {
1193
+ try {
1194
+ const contextUpdate = result.structured.contextUpdate;
1195
+ if (contextUpdate) {
1196
+ await this.agent.updateContext(contextUpdate);
1197
+ }
1198
+ }
1199
+ catch (error) {
1200
+ index_js_1.logger.error(`[ResponseModal] Failed to update context from structured response:`, error);
1201
+ // Continue without updating context rather than failing completely
1202
+ }
1203
+ }
1204
+ return updatedSession;
1205
+ }
1206
+ catch (error) {
1207
+ index_js_1.logger.error(`[ResponseModal] Error in collectDataFromResponse:`, error);
1208
+ // Return original session if data collection fails completely
1209
+ return params.session;
1210
+ }
1211
+ }
1212
+ /**
1213
+ * Apply flow completion: release the session to idle state.
1214
+ *
1215
+ * This is a pure state transition. The framework emits **no message of
1216
+ * its own** at the completion boundary — every word delivered to the
1217
+ * user comes from a developer-defined step prompt. If the dev wants a
1218
+ * closing turn, they add a final interactive step with their own
1219
+ * `prompt`; the framework respects that step's natural LLM output.
1220
+ *
1221
+ * Behavior:
1222
+ * - Marks the active `flowHistory` entry as `completed: true` and
1223
+ * stamps `exitedAt`.
1224
+ * - Evaluates `flow.onComplete` for an explicit follow-up transition.
1225
+ * When set, populates `session.pendingDirective` (the next turn's
1226
+ * pipeline applies it). When absent, the session is fully idle.
1227
+ * - Clears `currentFlow` and `currentStep` to `undefined`.
1228
+ * - Clears owned fields when the flow is `reentrant` so subsequent
1229
+ * re-selections start from a clean state.
1230
+ *
1231
+ * Returns the updated session. Callers compose any reply text from
1232
+ * their own sources (an upstream LLM turn, a directive's `reply`, or
1233
+ * an empty string for silent completion).
1234
+ *
1235
+ * @private
1236
+ */
1237
+ async applyFlowCompletion(params) {
1238
+ const { selectedFlow, session, context } = params;
1239
+ // 1) Evaluate onComplete first — needs the still-active session shape.
1240
+ const transitionConfig = await selectedFlow.evaluateOnComplete({ data: session.data }, context);
1241
+ // 2) Release to idle. If the flow is reentrant, scrub its owned
1242
+ // fields so re-selection on a future turn starts clean. When
1243
+ // onComplete fires we still go idle here — the next turn's
1244
+ // pipeline applies the pendingDirective before any routing.
1245
+ const ownedFields = selectedFlow.reentrant
1246
+ ? [
1247
+ ...(selectedFlow.requiredFields ?? []),
1248
+ ...(selectedFlow.optionalFields ?? []),
1249
+ ]
1250
+ : undefined;
1251
+ let nextSession = (0, index_js_1.completeCurrentFlow)(session, {
1252
+ clearOwnedFields: ownedFields,
1253
+ });
1254
+ // 3) Wire pendingDirective when onComplete returned a target.
1255
+ if (transitionConfig) {
1256
+ const goToTarget = typeof transitionConfig.goTo === 'string'
1257
+ ? transitionConfig.goTo
1258
+ : transitionConfig.goTo?.flow;
1259
+ const targetFlow = goToTarget ? this.agent.getFlows().find((r) => r.id === goToTarget ||
1260
+ r.title === goToTarget) : undefined;
1261
+ if (targetFlow) {
1262
+ nextSession = {
1263
+ ...nextSession,
1264
+ pendingDirective: {
1265
+ goTo: targetFlow.id,
1266
+ },
1267
+ };
1268
+ index_js_1.logger.debug(`[ResponseModal] Flow ${selectedFlow.title} completed with pending directive to: ${targetFlow.title}`);
1269
+ }
1270
+ else if (goToTarget) {
1271
+ index_js_1.logger.warn(`[FlowConfigurationError] onComplete target not found: flow "${selectedFlow.title}" completed but onComplete target "${goToTarget}" does not match any flow. ` +
1272
+ `Fix the onComplete value to reference an existing flow id/title, or remove onComplete to release the session to idle.`);
1273
+ }
1274
+ }
1275
+ else {
1276
+ index_js_1.logger.debug(`[ResponseModal] Flow ${selectedFlow.title} completed; session released to idle.`);
1277
+ }
1278
+ return nextSession;
1279
+ }
1280
+ /**
1281
+ * Stream a flow completion as a single terminal chunk.
1282
+ *
1283
+ * No LLM call is made. The framework no longer authors a farewell — the
1284
+ * completion path is a pure state transition. The chunk emits an empty
1285
+ * `delta` and a `done: true` flag with the idle session attached so
1286
+ * downstream consumers can finalize cleanly.
1287
+ *
1288
+ * If the developer wants closing copy in a streaming response, they
1289
+ * should add a final interactive step whose own LLM turn delivers it.
1290
+ *
1291
+ * @private
1292
+ */
1293
+ async *streamFlowCompletion(params) {
1294
+ const { selectedFlow, context, history } = params;
1295
+ const session = await this.applyFlowCompletion({
1296
+ selectedFlow,
1297
+ session: params.session,
1298
+ context,
1299
+ history,
1300
+ });
1301
+ yield {
1302
+ delta: '',
1303
+ accumulated: '',
1304
+ done: true,
1305
+ session,
1306
+ toolCalls: undefined,
1307
+ isFlowComplete: true,
1308
+ executedSteps: [],
1309
+ stoppedReason: params.stoppedReason ?? 'completed',
1310
+ };
1311
+ }
1312
+ /**
1313
+ * Generate fallback response when no flows are available
1314
+ * @private
1315
+ */
1316
+ async generateFallbackResponse(params) {
1317
+ const { history, context, session, signal } = params;
1318
+ index_js_1.logger.debug(`[ResponseModal] No flow selected, generating basic response`);
1319
+ // Build basic response prompt without flow context
1320
+ const { prompt: fallbackPrompt, appliedInstructions } = await this.responseEngine.buildFallbackPrompt({
1321
+ agentOptions: this.agent.getAgentOptions(),
1322
+ terms: this.agent.getTerms(),
1323
+ instructions: this.collectScopedInstructions(),
1324
+ context,
1325
+ session,
1326
+ });
1327
+ const agentOptions = this.agent.getAgentOptions();
1328
+ const result = await agentOptions.provider.generateMessage({
1329
+ prompt: fallbackPrompt,
1330
+ history,
1331
+ context,
1332
+ signal,
1333
+ parameters: {
1334
+ jsonSchema: {
1335
+ type: "object",
1336
+ properties: { message: { type: "string" } },
1337
+ required: ["message"],
1338
+ additionalProperties: false,
1339
+ },
1340
+ schemaName: "fallback_response",
1341
+ },
1342
+ });
1343
+ return { message: result.structured?.message || result.message, appliedInstructions };
1344
+ }
1345
+ /**
1346
+ * Stream fallback response when no flows are available
1347
+ * @private
1348
+ */
1349
+ async *streamFallbackResponse(params) {
1350
+ const { history, context, session, signal } = params;
1351
+ const { prompt: fallbackPrompt, appliedInstructions } = await this.responseEngine.buildFallbackPrompt({
1352
+ agentOptions: this.agent.getAgentOptions(),
1353
+ terms: this.agent.getTerms(),
1354
+ instructions: this.collectScopedInstructions(),
1355
+ context,
1356
+ session,
1357
+ });
1358
+ const agentOptions = this.agent.getAgentOptions();
1359
+ const stream = agentOptions.provider.generateMessageStream({
1360
+ prompt: fallbackPrompt,
1361
+ history,
1362
+ context,
1363
+ signal,
1364
+ parameters: {
1365
+ jsonSchema: {
1366
+ type: "object",
1367
+ properties: { message: { type: "string" } },
1368
+ required: ["message"],
1369
+ additionalProperties: false,
1370
+ },
1371
+ schemaName: "fallback_stream_response",
1372
+ },
1373
+ });
1374
+ // Decode the JSON wrapper to clean message text (same as the flow path).
1375
+ for await (const chunk of this.decodeMessageStream(stream)) {
1376
+ // Response structure completeness (Requirement 8.1, 8.2, 8.3)
1377
+ // - executedSteps: empty for fallback (no flow/step execution)
1378
+ // - stoppedReason: undefined for fallback (no flow context)
1379
+ // - session.currentStep: unchanged (no step progression)
1380
+ yield {
1381
+ delta: chunk.delta,
1382
+ accumulated: chunk.accumulated,
1383
+ done: chunk.done,
1384
+ session,
1385
+ toolCalls: undefined,
1386
+ isFlowComplete: false,
1387
+ executedSteps: chunk.done ? [] : undefined,
1388
+ stoppedReason: undefined,
1389
+ metadata: chunk.metadata,
1390
+ structured: chunk.structured,
1391
+ appliedInstructions: chunk.done ? appliedInstructions : undefined,
1392
+ };
1393
+ }
1394
+ }
1395
+ // ============================================================================
1396
+ // UTILITY METHODS - Helper methods for tool management and other utilities
1397
+ // ============================================================================
1398
+ /**
1399
+ * Collect all available tools for the given flow and step context.
1400
+ * Delegates to ToolManager for unified tool resolution and deduplication.
1401
+ * @private
1402
+ */
1403
+ collectAvailableTools(flow, step) {
1404
+ const availableTools = this.getToolManager().getAvailable(undefined, step, flow);
1405
+ return availableTools.map((tool) => ({
1406
+ id: tool.id,
1407
+ name: tool.id,
1408
+ description: tool.description,
1409
+ parameters: tool.parameters,
1410
+ }));
1411
+ }
1412
+ }
1413
+ exports.ResponseModal = ResponseModal;
1414
+ //# sourceMappingURL=ResponseModal.js.map