@falai/agent 0.1.0-alpha2 → 0.1.0-alpha3
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 +106 -0
- package/dist/cjs/src/core/BatchExecutor.d.ts +353 -0
- package/dist/cjs/src/core/BatchExecutor.d.ts.map +1 -0
- package/dist/cjs/src/core/BatchExecutor.js +842 -0
- package/dist/cjs/src/core/BatchExecutor.js.map +1 -0
- package/dist/cjs/src/core/BatchPromptBuilder.d.ts +86 -0
- package/dist/cjs/src/core/BatchPromptBuilder.d.ts.map +1 -0
- package/dist/cjs/src/core/BatchPromptBuilder.js +201 -0
- package/dist/cjs/src/core/BatchPromptBuilder.js.map +1 -0
- package/dist/cjs/src/core/ResponseModal.d.ts +34 -0
- package/dist/cjs/src/core/ResponseModal.d.ts.map +1 -1
- package/dist/cjs/src/core/ResponseModal.js +460 -34
- package/dist/cjs/src/core/ResponseModal.js.map +1 -1
- package/dist/cjs/src/index.d.ts +2 -0
- package/dist/cjs/src/index.d.ts.map +1 -1
- package/dist/cjs/src/index.js +7 -1
- package/dist/cjs/src/index.js.map +1 -1
- package/dist/cjs/src/types/agent.d.ts +9 -1
- package/dist/cjs/src/types/agent.d.ts.map +1 -1
- package/dist/cjs/src/types/route.d.ts +98 -0
- package/dist/cjs/src/types/route.d.ts.map +1 -1
- package/dist/src/core/BatchExecutor.d.ts +353 -0
- package/dist/src/core/BatchExecutor.d.ts.map +1 -0
- package/dist/src/core/BatchExecutor.js +837 -0
- package/dist/src/core/BatchExecutor.js.map +1 -0
- package/dist/src/core/BatchPromptBuilder.d.ts +86 -0
- package/dist/src/core/BatchPromptBuilder.d.ts.map +1 -0
- package/dist/src/core/BatchPromptBuilder.js +197 -0
- package/dist/src/core/BatchPromptBuilder.js.map +1 -0
- package/dist/src/core/ResponseModal.d.ts +34 -0
- package/dist/src/core/ResponseModal.d.ts.map +1 -1
- package/dist/src/core/ResponseModal.js +460 -34
- package/dist/src/core/ResponseModal.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/types/agent.d.ts +9 -1
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/route.d.ts +98 -0
- package/dist/src/types/route.d.ts.map +1 -1
- package/docs/api/overview.md +124 -0
- package/docs/architecture/multi-step-execution.md +243 -0
- package/docs/core/ai-integration/prompt-composition.md +135 -0
- package/docs/core/ai-integration/response-processing.md +146 -0
- package/docs/core/conversation-flows/data-collection.md +143 -0
- package/docs/core/conversation-flows/step-transitions.md +132 -0
- package/docs/core/conversation-flows/steps.md +112 -0
- package/docs/core/error-handling.md +193 -0
- package/docs/guides/getting-started/README.md +102 -0
- package/docs/guides/migration/README.md +23 -0
- package/docs/guides/migration/multi-step-execution.md +303 -0
- package/package.json +4 -2
- package/src/core/BatchExecutor.ts +1156 -0
- package/src/core/BatchPromptBuilder.ts +275 -0
- package/src/core/ResponseModal.ts +605 -35
- package/src/index.ts +2 -0
- package/src/types/agent.ts +9 -1
- package/src/types/route.ts +119 -0
|
@@ -6,6 +6,8 @@ import { EventKind, MessageRole } from "../types";
|
|
|
6
6
|
import { Step } from "./Step";
|
|
7
7
|
import { ResponseEngine } from "./ResponseEngine";
|
|
8
8
|
import { ResponsePipeline } from "./ResponsePipeline";
|
|
9
|
+
import { BatchExecutor } from "./BatchExecutor";
|
|
10
|
+
import { BatchPromptBuilder } from "./BatchPromptBuilder";
|
|
9
11
|
import { cloneDeep, mergeCollected, enterStep, getLastMessageFromHistory, render, logger, historyToEvents } from "../utils";
|
|
10
12
|
import { createTemplateContext } from "../utils/template";
|
|
11
13
|
import { END_ROUTE_ID } from "../constants";
|
|
@@ -49,6 +51,10 @@ export class ResponseModal {
|
|
|
49
51
|
// Initialize response pipeline with agent dependencies
|
|
50
52
|
this.responsePipeline = new ResponsePipeline(this.agent.getAgentOptions(), () => this.agent.getRoutes(), // Pass a function to get routes dynamically
|
|
51
53
|
this.agent.getTools(), this.agent.getRoutingEngine(), this.agent.updateContext.bind(this.agent), this.agent.getUpdateDataMethod(), this.agent.updateCollectedData.bind(this.agent), this.getToolManager());
|
|
54
|
+
// Initialize batch executor for multi-step execution
|
|
55
|
+
this.batchExecutor = new BatchExecutor();
|
|
56
|
+
// Initialize batch prompt builder for combined prompts
|
|
57
|
+
this.batchPromptBuilder = new BatchPromptBuilder();
|
|
52
58
|
}
|
|
53
59
|
/**
|
|
54
60
|
* Generate a non-streaming response using unified logic
|
|
@@ -269,6 +275,7 @@ export class ResponseModal {
|
|
|
269
275
|
throw ResponseGenerationError.fromError(error, 'step_preparation', params, { session, effectiveContext });
|
|
270
276
|
}
|
|
271
277
|
// PHASE 2: ROUTING + STEP SELECTION - Determine which route and step to use
|
|
278
|
+
// Also performs pre-extraction and batch determination
|
|
272
279
|
let routingResult;
|
|
273
280
|
try {
|
|
274
281
|
routingResult = await this.handleUnifiedRoutingAndStepSelection({
|
|
@@ -289,6 +296,9 @@ export class ResponseModal {
|
|
|
289
296
|
selectedStep: routingResult.selectedStep,
|
|
290
297
|
responseDirectives: routingResult.responseDirectives,
|
|
291
298
|
isRouteComplete: routingResult.isRouteComplete,
|
|
299
|
+
batchSteps: routingResult.batchSteps,
|
|
300
|
+
batchStoppedReason: routingResult.batchStoppedReason,
|
|
301
|
+
batchStoppedAtStep: routingResult.batchStoppedAtStep,
|
|
292
302
|
};
|
|
293
303
|
}
|
|
294
304
|
catch (error) {
|
|
@@ -316,12 +326,13 @@ export class ResponseModal {
|
|
|
316
326
|
});
|
|
317
327
|
let updatedSession = routingResult.session;
|
|
318
328
|
let isRouteComplete = routingResult.isRouteComplete;
|
|
319
|
-
// PRE-EXTRACTION: If entering a
|
|
329
|
+
// PRE-EXTRACTION: If entering a route that collects data, extract data from user message first
|
|
320
330
|
// This allows us to skip steps whose data is already provided
|
|
331
|
+
// Requirement 3.1: Perform Pre_Extraction before determining the Batch
|
|
321
332
|
if (routingResult.selectedRoute && !isRouteComplete) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (
|
|
333
|
+
// Always pre-extract when route collects data (not just on new route entry)
|
|
334
|
+
// This ensures batch determination has the most up-to-date data
|
|
335
|
+
if (this.shouldPreExtractData(routingResult.selectedRoute)) {
|
|
325
336
|
logger.debug(`[ResponseModal] Pre-extracting data for route: ${routingResult.selectedRoute.title}`);
|
|
326
337
|
const extractedData = await this.preExtractRouteData({
|
|
327
338
|
route: routingResult.selectedRoute,
|
|
@@ -332,7 +343,7 @@ export class ResponseModal {
|
|
|
332
343
|
});
|
|
333
344
|
if (extractedData && Object.keys(extractedData).length > 0) {
|
|
334
345
|
logger.debug(`[ResponseModal] Pre-extracted data:`, extractedData);
|
|
335
|
-
//
|
|
346
|
+
// Requirement 3.3: Merge pre-extracted data into session before batch determination
|
|
336
347
|
updatedSession = mergeCollected(updatedSession, extractedData);
|
|
337
348
|
// Also update agent's collected data
|
|
338
349
|
await this.agent.updateCollectedData(extractedData);
|
|
@@ -345,6 +356,27 @@ export class ResponseModal {
|
|
|
345
356
|
}
|
|
346
357
|
}
|
|
347
358
|
}
|
|
359
|
+
// BATCH DETERMINATION: Use BatchExecutor to determine which steps can execute together
|
|
360
|
+
// Requirement 3.4: Pre-extraction results affect batch determination
|
|
361
|
+
let batchSteps;
|
|
362
|
+
let batchStoppedReason;
|
|
363
|
+
let batchStoppedAtStep;
|
|
364
|
+
if (routingResult.selectedRoute && !isRouteComplete) {
|
|
365
|
+
// Determine current step position for batch determination
|
|
366
|
+
const currentStep = routingResult.selectedStep ||
|
|
367
|
+
(updatedSession.currentStep ? routingResult.selectedRoute.getStep(updatedSession.currentStep.id) : undefined);
|
|
368
|
+
logger.debug(`[ResponseModal] Determining batch starting from step: ${currentStep?.id || 'initial'}`);
|
|
369
|
+
const batchResult = await this.batchExecutor.determineBatch({
|
|
370
|
+
route: routingResult.selectedRoute,
|
|
371
|
+
currentStep,
|
|
372
|
+
sessionData: updatedSession.data || {},
|
|
373
|
+
context: params.context,
|
|
374
|
+
});
|
|
375
|
+
batchSteps = batchResult.steps;
|
|
376
|
+
batchStoppedReason = batchResult.stoppedReason;
|
|
377
|
+
batchStoppedAtStep = batchResult.stoppedAtStep;
|
|
378
|
+
logger.debug(`[ResponseModal] Batch determined: ${batchSteps.length} steps, stopped reason: ${batchStoppedReason}`);
|
|
379
|
+
}
|
|
348
380
|
// Determine next step using pipeline method for consistency
|
|
349
381
|
const stepResult = await this.responsePipeline.determineNextStep({
|
|
350
382
|
selectedRoute: routingResult.selectedRoute,
|
|
@@ -358,6 +390,9 @@ export class ResponseModal {
|
|
|
358
390
|
responseDirectives: routingResult.responseDirectives,
|
|
359
391
|
session: stepResult.session,
|
|
360
392
|
isRouteComplete, // Use updated completion status
|
|
393
|
+
batchSteps,
|
|
394
|
+
batchStoppedReason,
|
|
395
|
+
batchStoppedAtStep,
|
|
361
396
|
};
|
|
362
397
|
}
|
|
363
398
|
catch (error) {
|
|
@@ -438,7 +473,7 @@ export class ResponseModal {
|
|
|
438
473
|
* @private
|
|
439
474
|
*/
|
|
440
475
|
async generateUnifiedResponse(responseContext) {
|
|
441
|
-
const { effectiveContext, session: initialSession, history, selectedRoute, selectedStep, responseDirectives, isRouteComplete } = responseContext;
|
|
476
|
+
const { effectiveContext, session: initialSession, history, selectedRoute, selectedStep, responseDirectives, isRouteComplete, batchSteps, batchStoppedReason, } = responseContext;
|
|
442
477
|
let session = initialSession;
|
|
443
478
|
// Get last user message (needed for both route and completion handling)
|
|
444
479
|
// Convert HistoryItem[] to Event[] for internal processing
|
|
@@ -446,22 +481,54 @@ export class ResponseModal {
|
|
|
446
481
|
const lastMessageText = getLastMessageFromHistory(historyEvents);
|
|
447
482
|
let message;
|
|
448
483
|
let toolCalls = undefined;
|
|
484
|
+
let executedSteps;
|
|
485
|
+
let stoppedReason;
|
|
449
486
|
if (selectedRoute && !isRouteComplete) {
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
487
|
+
// Check if we have batch steps to execute
|
|
488
|
+
if (batchSteps && batchSteps.length > 0) {
|
|
489
|
+
// BATCH EXECUTION: Execute multiple steps in a single LLM call
|
|
490
|
+
logger.debug(`[ResponseModal] Executing batch of ${batchSteps.length} steps`);
|
|
491
|
+
const batchResult = await this.executeBatchResponse({
|
|
492
|
+
selectedRoute,
|
|
493
|
+
batchSteps,
|
|
494
|
+
responseDirectives,
|
|
495
|
+
session,
|
|
496
|
+
history,
|
|
497
|
+
context: effectiveContext,
|
|
498
|
+
historyEvents,
|
|
499
|
+
});
|
|
500
|
+
message = batchResult.message;
|
|
501
|
+
toolCalls = batchResult.toolCalls;
|
|
502
|
+
session = batchResult.session;
|
|
503
|
+
executedSteps = batchResult.executedSteps;
|
|
504
|
+
stoppedReason = batchStoppedReason;
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
// SINGLE STEP EXECUTION: Fall back to single-step processing
|
|
508
|
+
// This happens when batch determination returns empty (first step needs input)
|
|
509
|
+
const result = await this.processRouteResponse({
|
|
510
|
+
selectedRoute,
|
|
511
|
+
selectedStep,
|
|
512
|
+
responseDirectives,
|
|
513
|
+
session,
|
|
514
|
+
history,
|
|
515
|
+
context: effectiveContext,
|
|
516
|
+
lastMessageText,
|
|
517
|
+
historyEvents,
|
|
518
|
+
signal: undefined,
|
|
519
|
+
});
|
|
520
|
+
message = result.message;
|
|
521
|
+
toolCalls = result.toolCalls;
|
|
522
|
+
session = result.session;
|
|
523
|
+
// Track executed step for single-step execution
|
|
524
|
+
if (selectedStep) {
|
|
525
|
+
executedSteps = [{
|
|
526
|
+
id: selectedStep.id,
|
|
527
|
+
routeId: selectedRoute.id,
|
|
528
|
+
}];
|
|
529
|
+
}
|
|
530
|
+
stoppedReason = batchStoppedReason || 'needs_input';
|
|
531
|
+
}
|
|
465
532
|
}
|
|
466
533
|
else if (isRouteComplete && selectedRoute) {
|
|
467
534
|
// Handle route completion
|
|
@@ -473,10 +540,11 @@ export class ResponseModal {
|
|
|
473
540
|
context: effectiveContext,
|
|
474
541
|
lastMessageText,
|
|
475
542
|
historyEvents,
|
|
476
|
-
signal: undefined,
|
|
543
|
+
signal: undefined,
|
|
477
544
|
});
|
|
478
545
|
// Set step to END_ROUTE marker
|
|
479
546
|
session = enterStep(session, END_ROUTE_ID, "Route completed");
|
|
547
|
+
stoppedReason = 'route_complete';
|
|
480
548
|
logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed. Entered END_ROUTE step.`);
|
|
481
549
|
}
|
|
482
550
|
catch (error) {
|
|
@@ -484,6 +552,7 @@ export class ResponseModal {
|
|
|
484
552
|
// Fallback to simple completion message
|
|
485
553
|
message = `Thank you! I've recorded all the information for your ${selectedRoute.title.toLowerCase()}.`;
|
|
486
554
|
session = enterStep(session, END_ROUTE_ID, "Route completed");
|
|
555
|
+
stoppedReason = 'route_complete';
|
|
487
556
|
}
|
|
488
557
|
}
|
|
489
558
|
else {
|
|
@@ -493,37 +562,268 @@ export class ResponseModal {
|
|
|
493
562
|
context: effectiveContext,
|
|
494
563
|
session,
|
|
495
564
|
});
|
|
565
|
+
// For fallback responses, set empty executedSteps and no stoppedReason
|
|
566
|
+
// since there's no route/step execution happening
|
|
567
|
+
executedSteps = [];
|
|
568
|
+
stoppedReason = undefined;
|
|
496
569
|
}
|
|
570
|
+
// Ensure response structure completeness (Requirement 8.1, 8.2, 8.3)
|
|
571
|
+
// - executedSteps: array of steps executed (empty array if none)
|
|
572
|
+
// - stoppedReason: why execution stopped (undefined for fallback)
|
|
573
|
+
// - session.currentStep: reflects final step position
|
|
497
574
|
return {
|
|
498
575
|
message,
|
|
499
576
|
session,
|
|
500
577
|
toolCalls,
|
|
501
578
|
isRouteComplete,
|
|
579
|
+
executedSteps: executedSteps || [],
|
|
580
|
+
stoppedReason,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Execute a batch of steps with a single LLM call
|
|
585
|
+
*
|
|
586
|
+
* This method:
|
|
587
|
+
* 1. Executes all prepare hooks for steps in the batch (in order)
|
|
588
|
+
* 2. Builds a combined prompt using BatchPromptBuilder
|
|
589
|
+
* 3. Makes a single LLM call
|
|
590
|
+
* 4. Collects data from the response for all steps
|
|
591
|
+
* 5. Executes all finalize hooks for steps in the batch (in order)
|
|
592
|
+
*
|
|
593
|
+
* @private
|
|
594
|
+
* **Validates: Requirements 1.1, 4.4, 5.1, 5.2**
|
|
595
|
+
*/
|
|
596
|
+
async executeBatchResponse(params) {
|
|
597
|
+
const { selectedRoute, batchSteps, history, context, historyEvents, signal } = params;
|
|
598
|
+
let session = params.session;
|
|
599
|
+
logger.debug(`[ResponseModal] Starting batch execution for ${batchSteps.length} steps`);
|
|
600
|
+
// Create hook executor function
|
|
601
|
+
const executeHook = async (hook, hookContext, data, step) => {
|
|
602
|
+
// Find the route for this step
|
|
603
|
+
const route = selectedRoute;
|
|
604
|
+
// Convert StepOptions to Step if needed for executePrepareFinalize
|
|
605
|
+
const stepInstance = step?.id ? route.getStep(step.id) : undefined;
|
|
606
|
+
await this.executePrepareFinalize(hook, hookContext, data, route, stepInstance);
|
|
607
|
+
};
|
|
608
|
+
// PHASE 1: Execute all prepare hooks (Requirement 5.1)
|
|
609
|
+
logger.debug(`[ResponseModal] Executing prepare hooks for batch`);
|
|
610
|
+
const prepareResult = await this.batchExecutor.executePrepareHooks({
|
|
611
|
+
steps: batchSteps,
|
|
612
|
+
context,
|
|
613
|
+
data: session.data,
|
|
614
|
+
executeHook,
|
|
615
|
+
});
|
|
616
|
+
if (!prepareResult.success) {
|
|
617
|
+
// Prepare hook failed - return error response
|
|
618
|
+
logger.error(`[ResponseModal] Prepare hook failed:`, prepareResult.error);
|
|
619
|
+
throw new ResponseGenerationError(`Prepare hook failed: ${prepareResult.error?.message}`, {
|
|
620
|
+
phase: 'prepare_hooks',
|
|
621
|
+
context: {
|
|
622
|
+
stepId: prepareResult.error?.stepId,
|
|
623
|
+
executedSteps: prepareResult.executedSteps,
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
// PHASE 2: Build combined prompt using BatchPromptBuilder (Requirement 4.4)
|
|
628
|
+
logger.debug(`[ResponseModal] Building batch prompt`);
|
|
629
|
+
const batchPromptResult = await this.batchPromptBuilder.buildBatchPrompt({
|
|
630
|
+
steps: batchSteps,
|
|
631
|
+
route: selectedRoute,
|
|
632
|
+
history: historyEvents,
|
|
633
|
+
context,
|
|
634
|
+
session,
|
|
635
|
+
agentOptions: this.agent.getAgentOptions(),
|
|
636
|
+
});
|
|
637
|
+
logger.debug(`[ResponseModal] Batch prompt built with ${batchPromptResult.stepCount} steps, collecting: ${batchPromptResult.collectFields.join(', ')}`);
|
|
638
|
+
// Build response schema for batch (includes all collect fields)
|
|
639
|
+
const responseSchema = this.buildBatchResponseSchema(batchPromptResult.collectFields);
|
|
640
|
+
// Collect available tools for AI (from all steps in batch)
|
|
641
|
+
const availableTools = this.collectBatchAvailableTools(selectedRoute, batchSteps);
|
|
642
|
+
// PHASE 3: Make single LLM call (Requirement 4.4)
|
|
643
|
+
logger.debug(`[ResponseModal] Making LLM call for batch`);
|
|
644
|
+
const agentOptions = this.agent.getAgentOptions();
|
|
645
|
+
const result = await agentOptions.provider.generateMessage({
|
|
646
|
+
prompt: batchPromptResult.prompt,
|
|
647
|
+
history: historyEvents,
|
|
648
|
+
context,
|
|
649
|
+
tools: availableTools,
|
|
650
|
+
signal,
|
|
651
|
+
parameters: responseSchema ? { jsonSchema: responseSchema, schemaName: "batch_response" } : undefined,
|
|
652
|
+
});
|
|
653
|
+
let message = result.structured?.message || result.message;
|
|
654
|
+
let toolCalls = result.structured?.toolCalls;
|
|
655
|
+
logger.debug(`[ResponseModal] LLM response received for batch`);
|
|
656
|
+
// Execute tools if any
|
|
657
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
658
|
+
const toolResult = await this.executeUnifiedToolLoop({
|
|
659
|
+
toolCalls,
|
|
660
|
+
context,
|
|
661
|
+
session,
|
|
662
|
+
history,
|
|
663
|
+
selectedRoute,
|
|
664
|
+
responsePrompt: batchPromptResult.prompt,
|
|
665
|
+
availableTools,
|
|
666
|
+
responseSchema,
|
|
667
|
+
signal,
|
|
668
|
+
});
|
|
669
|
+
session = toolResult.session;
|
|
670
|
+
toolCalls = toolResult.finalToolCalls;
|
|
671
|
+
if (toolResult.finalMessage) {
|
|
672
|
+
message = toolResult.finalMessage;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// PHASE 4: Collect data from response for all steps (Requirement 6.1, 6.2, 6.3)
|
|
676
|
+
logger.debug(`[ResponseModal] Collecting batch data`);
|
|
677
|
+
const collectResult = this.batchExecutor.collectBatchData({
|
|
678
|
+
steps: batchSteps,
|
|
679
|
+
llmResponse: result.structured || {},
|
|
680
|
+
session,
|
|
681
|
+
schema: this.agent.getSchema(),
|
|
682
|
+
});
|
|
683
|
+
session = collectResult.session;
|
|
684
|
+
if (collectResult.collectedData && Object.keys(collectResult.collectedData).length > 0) {
|
|
685
|
+
// Update agent's collected data
|
|
686
|
+
await this.agent.updateCollectedData(collectResult.collectedData);
|
|
687
|
+
logger.debug(`[ResponseModal] Batch collected data:`, collectResult.collectedData);
|
|
688
|
+
}
|
|
689
|
+
if (collectResult.validationErrors && collectResult.validationErrors.length > 0) {
|
|
690
|
+
logger.warn(`[ResponseModal] Batch data validation errors:`, collectResult.validationErrors);
|
|
691
|
+
}
|
|
692
|
+
// Update session to final step position
|
|
693
|
+
const lastStep = batchSteps[batchSteps.length - 1];
|
|
694
|
+
if (lastStep?.id) {
|
|
695
|
+
session = enterStep(session, lastStep.id, lastStep.description);
|
|
696
|
+
logger.debug(`[ResponseModal] Updated session to final batch step: ${lastStep.id}`);
|
|
697
|
+
}
|
|
698
|
+
// PHASE 5: Execute all finalize hooks (Requirement 5.2)
|
|
699
|
+
logger.debug(`[ResponseModal] Executing finalize hooks for batch`);
|
|
700
|
+
const finalizeResult = await this.batchExecutor.executeFinalizeHooks({
|
|
701
|
+
steps: batchSteps,
|
|
702
|
+
context,
|
|
703
|
+
data: session.data,
|
|
704
|
+
executeHook,
|
|
705
|
+
});
|
|
706
|
+
if (finalizeResult.errors && finalizeResult.errors.length > 0) {
|
|
707
|
+
// Log finalize errors but don't fail (Requirement 5.5)
|
|
708
|
+
logger.warn(`[ResponseModal] Some finalize hooks failed:`, finalizeResult.errors);
|
|
709
|
+
}
|
|
710
|
+
// Build executed steps list
|
|
711
|
+
const executedSteps = batchSteps
|
|
712
|
+
.filter(step => step.id)
|
|
713
|
+
.map(step => ({
|
|
714
|
+
id: step.id,
|
|
715
|
+
routeId: selectedRoute.id,
|
|
716
|
+
}));
|
|
717
|
+
logger.debug(`[ResponseModal] Batch execution complete. Executed ${executedSteps.length} steps`);
|
|
718
|
+
return {
|
|
719
|
+
message,
|
|
720
|
+
toolCalls,
|
|
721
|
+
session,
|
|
722
|
+
executedSteps,
|
|
502
723
|
};
|
|
503
724
|
}
|
|
725
|
+
/**
|
|
726
|
+
* Build response schema for batch execution
|
|
727
|
+
* @private
|
|
728
|
+
*/
|
|
729
|
+
buildBatchResponseSchema(collectFields) {
|
|
730
|
+
const properties = {
|
|
731
|
+
message: {
|
|
732
|
+
type: "string",
|
|
733
|
+
description: "Your response to the user",
|
|
734
|
+
},
|
|
735
|
+
};
|
|
736
|
+
// Add collect fields to schema
|
|
737
|
+
for (const field of collectFields) {
|
|
738
|
+
properties[field] = {
|
|
739
|
+
type: "string",
|
|
740
|
+
description: `Collected value for ${field}`,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
type: "object",
|
|
745
|
+
properties,
|
|
746
|
+
required: ["message"],
|
|
747
|
+
additionalProperties: true,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Collect available tools from all steps in the batch
|
|
752
|
+
* @private
|
|
753
|
+
*/
|
|
754
|
+
collectBatchAvailableTools(route, batchSteps) {
|
|
755
|
+
const availableTools = new Map();
|
|
756
|
+
// Add agent-level tools
|
|
757
|
+
this.agent.getTools().forEach((tool) => {
|
|
758
|
+
availableTools.set(tool.id, tool);
|
|
759
|
+
});
|
|
760
|
+
// Add route-level tools
|
|
761
|
+
route.getTools().forEach((tool) => {
|
|
762
|
+
availableTools.set(tool.id, tool);
|
|
763
|
+
});
|
|
764
|
+
// Add step-level tools from all batch steps
|
|
765
|
+
for (const step of batchSteps) {
|
|
766
|
+
if (step.tools) {
|
|
767
|
+
for (const toolRef of step.tools) {
|
|
768
|
+
if (typeof toolRef === "string") {
|
|
769
|
+
// Reference to registered tool - already in availableTools
|
|
770
|
+
}
|
|
771
|
+
else if (typeof toolRef === 'object' && 'id' in toolRef && toolRef.id) {
|
|
772
|
+
// Inline tool definition
|
|
773
|
+
availableTools.set(toolRef.id, toolRef);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// Convert to the format expected by AI providers
|
|
779
|
+
return Array.from(availableTools.values()).map((tool) => ({
|
|
780
|
+
id: tool.id,
|
|
781
|
+
name: tool.name || tool.id,
|
|
782
|
+
description: tool.description,
|
|
783
|
+
parameters: tool.parameters,
|
|
784
|
+
}));
|
|
785
|
+
}
|
|
504
786
|
/**
|
|
505
787
|
* Unified streaming response generation
|
|
506
788
|
* @private
|
|
507
789
|
*/
|
|
508
790
|
async *generateUnifiedStreamingResponse(responseContext) {
|
|
509
|
-
const { effectiveContext, session: initialSession, history, selectedRoute, selectedStep, responseDirectives, isRouteComplete } = responseContext;
|
|
791
|
+
const { effectiveContext, session: initialSession, history, selectedRoute, selectedStep, responseDirectives, isRouteComplete, batchSteps, batchStoppedReason, } = responseContext;
|
|
510
792
|
const session = initialSession;
|
|
511
793
|
// Get last user message (needed for both route and completion handling)
|
|
512
794
|
// Convert HistoryItem[] to Event[] for internal processing
|
|
513
795
|
const historyEvents = historyToEvents(history);
|
|
514
796
|
const lastMessageText = getLastMessageFromHistory(historyEvents);
|
|
515
797
|
if (selectedRoute && !isRouteComplete) {
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
798
|
+
// Check if we have batch steps to execute
|
|
799
|
+
if (batchSteps && batchSteps.length > 0) {
|
|
800
|
+
// BATCH EXECUTION: Execute multiple steps with streaming
|
|
801
|
+
// Note: For streaming, we still use batch execution but stream the response
|
|
802
|
+
logger.debug(`[ResponseModal] Streaming batch execution for ${batchSteps.length} steps`);
|
|
803
|
+
yield* this.streamBatchResponse({
|
|
804
|
+
selectedRoute,
|
|
805
|
+
batchSteps,
|
|
806
|
+
responseDirectives,
|
|
807
|
+
session,
|
|
808
|
+
history,
|
|
809
|
+
context: effectiveContext,
|
|
810
|
+
historyEvents,
|
|
811
|
+
batchStoppedReason,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
// SINGLE STEP EXECUTION: Fall back to single-step streaming
|
|
816
|
+
yield* this.processRouteStreamingResponse({
|
|
817
|
+
selectedRoute,
|
|
818
|
+
selectedStep,
|
|
819
|
+
responseDirectives,
|
|
820
|
+
session,
|
|
821
|
+
history,
|
|
822
|
+
context: effectiveContext,
|
|
823
|
+
lastMessageText,
|
|
824
|
+
historyEvents,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
527
827
|
}
|
|
528
828
|
else if (isRouteComplete && selectedRoute) {
|
|
529
829
|
// Handle route completion streaming
|
|
@@ -544,6 +844,114 @@ export class ResponseModal {
|
|
|
544
844
|
});
|
|
545
845
|
}
|
|
546
846
|
}
|
|
847
|
+
/**
|
|
848
|
+
* Stream a batch response with multiple steps
|
|
849
|
+
*
|
|
850
|
+
* Similar to executeBatchResponse but streams the LLM response.
|
|
851
|
+
*
|
|
852
|
+
* @private
|
|
853
|
+
*/
|
|
854
|
+
async *streamBatchResponse(params) {
|
|
855
|
+
const { selectedRoute, batchSteps, context, historyEvents, batchStoppedReason, signal } = params;
|
|
856
|
+
let session = params.session;
|
|
857
|
+
// Create hook executor function
|
|
858
|
+
const executeHook = async (hook, hookContext, data, step) => {
|
|
859
|
+
const route = selectedRoute;
|
|
860
|
+
const stepInstance = step?.id ? route.getStep(step.id) : undefined;
|
|
861
|
+
await this.executePrepareFinalize(hook, hookContext, data, route, stepInstance);
|
|
862
|
+
};
|
|
863
|
+
// PHASE 1: Execute all prepare hooks
|
|
864
|
+
const prepareResult = await this.batchExecutor.executePrepareHooks({
|
|
865
|
+
steps: batchSteps,
|
|
866
|
+
context,
|
|
867
|
+
data: session.data,
|
|
868
|
+
executeHook,
|
|
869
|
+
});
|
|
870
|
+
if (!prepareResult.success) {
|
|
871
|
+
// Yield error chunk
|
|
872
|
+
yield {
|
|
873
|
+
delta: "",
|
|
874
|
+
accumulated: "",
|
|
875
|
+
done: true,
|
|
876
|
+
session,
|
|
877
|
+
error: new ResponseGenerationError(`Prepare hook failed: ${prepareResult.error?.message}`, { phase: 'prepare_hooks' }),
|
|
878
|
+
};
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
// PHASE 2: Build combined prompt
|
|
882
|
+
const batchPromptResult = await this.batchPromptBuilder.buildBatchPrompt({
|
|
883
|
+
steps: batchSteps,
|
|
884
|
+
route: selectedRoute,
|
|
885
|
+
history: historyEvents,
|
|
886
|
+
context,
|
|
887
|
+
session,
|
|
888
|
+
agentOptions: this.agent.getAgentOptions(),
|
|
889
|
+
});
|
|
890
|
+
const responseSchema = this.buildBatchResponseSchema(batchPromptResult.collectFields);
|
|
891
|
+
const availableTools = this.collectBatchAvailableTools(selectedRoute, batchSteps);
|
|
892
|
+
// PHASE 3: Stream LLM response
|
|
893
|
+
const agentOptions = this.agent.getAgentOptions();
|
|
894
|
+
const stream = agentOptions.provider.generateMessageStream({
|
|
895
|
+
prompt: batchPromptResult.prompt,
|
|
896
|
+
history: historyEvents,
|
|
897
|
+
context,
|
|
898
|
+
tools: availableTools,
|
|
899
|
+
signal,
|
|
900
|
+
parameters: responseSchema ? { jsonSchema: responseSchema, schemaName: "batch_stream_response" } : undefined,
|
|
901
|
+
});
|
|
902
|
+
// Build executed steps list
|
|
903
|
+
const executedSteps = batchSteps
|
|
904
|
+
.filter(step => step.id)
|
|
905
|
+
.map(step => ({
|
|
906
|
+
id: step.id,
|
|
907
|
+
routeId: selectedRoute.id,
|
|
908
|
+
}));
|
|
909
|
+
// Stream chunks
|
|
910
|
+
for await (const chunk of stream) {
|
|
911
|
+
// On final chunk, collect data and execute finalize hooks
|
|
912
|
+
if (chunk.done) {
|
|
913
|
+
// Collect data from response
|
|
914
|
+
if (chunk.structured) {
|
|
915
|
+
const collectResult = this.batchExecutor.collectBatchData({
|
|
916
|
+
steps: batchSteps,
|
|
917
|
+
llmResponse: chunk.structured,
|
|
918
|
+
session,
|
|
919
|
+
schema: this.agent.getSchema(),
|
|
920
|
+
});
|
|
921
|
+
session = collectResult.session;
|
|
922
|
+
if (collectResult.collectedData && Object.keys(collectResult.collectedData).length > 0) {
|
|
923
|
+
await this.agent.updateCollectedData(collectResult.collectedData);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
// Update session to final step position
|
|
927
|
+
const lastStep = batchSteps[batchSteps.length - 1];
|
|
928
|
+
if (lastStep?.id) {
|
|
929
|
+
session = enterStep(session, lastStep.id, lastStep.description);
|
|
930
|
+
}
|
|
931
|
+
// Execute finalize hooks
|
|
932
|
+
await this.batchExecutor.executeFinalizeHooks({
|
|
933
|
+
steps: batchSteps,
|
|
934
|
+
context,
|
|
935
|
+
data: session.data,
|
|
936
|
+
executeHook,
|
|
937
|
+
});
|
|
938
|
+
// Finalize session
|
|
939
|
+
await this.finalizeSession(session, context);
|
|
940
|
+
}
|
|
941
|
+
yield {
|
|
942
|
+
delta: chunk.delta,
|
|
943
|
+
accumulated: chunk.accumulated,
|
|
944
|
+
done: chunk.done,
|
|
945
|
+
session,
|
|
946
|
+
toolCalls: chunk.structured?.toolCalls,
|
|
947
|
+
isRouteComplete: false,
|
|
948
|
+
executedSteps: chunk.done ? executedSteps : undefined,
|
|
949
|
+
stoppedReason: chunk.done ? batchStoppedReason : undefined,
|
|
950
|
+
metadata: chunk.metadata,
|
|
951
|
+
structured: chunk.structured,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
}
|
|
547
955
|
/**
|
|
548
956
|
* Execute prepare function for current step if available
|
|
549
957
|
* @private
|
|
@@ -770,6 +1178,10 @@ export class ResponseModal {
|
|
|
770
1178
|
if (chunk.done) {
|
|
771
1179
|
await this.finalizeSession(session, context);
|
|
772
1180
|
}
|
|
1181
|
+
// Response structure completeness (Requirement 8.1, 8.2, 8.3)
|
|
1182
|
+
// - executedSteps: single step executed in this response
|
|
1183
|
+
// - stoppedReason: 'needs_input' for single-step execution (waiting for user input)
|
|
1184
|
+
// - session.currentStep: reflects the executed step
|
|
773
1185
|
yield {
|
|
774
1186
|
delta: chunk.delta,
|
|
775
1187
|
accumulated: chunk.accumulated,
|
|
@@ -777,6 +1189,8 @@ export class ResponseModal {
|
|
|
777
1189
|
session,
|
|
778
1190
|
toolCalls,
|
|
779
1191
|
isRouteComplete: false,
|
|
1192
|
+
executedSteps: chunk.done ? [{ id: nextStep.id, routeId: selectedRoute.id }] : undefined,
|
|
1193
|
+
stoppedReason: chunk.done ? 'needs_input' : undefined,
|
|
780
1194
|
metadata: chunk.metadata,
|
|
781
1195
|
structured: chunk.structured,
|
|
782
1196
|
};
|
|
@@ -1262,6 +1676,10 @@ export class ResponseModal {
|
|
|
1262
1676
|
if (chunk.done) {
|
|
1263
1677
|
await this.finalizeSession(session, context);
|
|
1264
1678
|
}
|
|
1679
|
+
// Response structure completeness (Requirement 8.1, 8.2, 8.3)
|
|
1680
|
+
// - executedSteps: empty for route completion (no new steps executed)
|
|
1681
|
+
// - stoppedReason: 'route_complete' for completed routes
|
|
1682
|
+
// - session.currentStep: set to END_ROUTE
|
|
1265
1683
|
yield {
|
|
1266
1684
|
delta: chunk.delta,
|
|
1267
1685
|
accumulated: chunk.accumulated,
|
|
@@ -1269,6 +1687,8 @@ export class ResponseModal {
|
|
|
1269
1687
|
session,
|
|
1270
1688
|
toolCalls: undefined,
|
|
1271
1689
|
isRouteComplete: true,
|
|
1690
|
+
executedSteps: chunk.done ? [] : undefined,
|
|
1691
|
+
stoppedReason: chunk.done ? 'route_complete' : undefined,
|
|
1272
1692
|
metadata: chunk.metadata,
|
|
1273
1693
|
structured: chunk.structured,
|
|
1274
1694
|
};
|
|
@@ -1343,6 +1763,10 @@ export class ResponseModal {
|
|
|
1343
1763
|
if (chunk.done) {
|
|
1344
1764
|
await this.finalizeSession(session, context);
|
|
1345
1765
|
}
|
|
1766
|
+
// Response structure completeness (Requirement 8.1, 8.2, 8.3)
|
|
1767
|
+
// - executedSteps: empty for fallback (no route/step execution)
|
|
1768
|
+
// - stoppedReason: undefined for fallback (no route context)
|
|
1769
|
+
// - session.currentStep: unchanged (no step progression)
|
|
1346
1770
|
yield {
|
|
1347
1771
|
delta: chunk.delta,
|
|
1348
1772
|
accumulated: chunk.accumulated,
|
|
@@ -1350,6 +1774,8 @@ export class ResponseModal {
|
|
|
1350
1774
|
session,
|
|
1351
1775
|
toolCalls: undefined,
|
|
1352
1776
|
isRouteComplete: false,
|
|
1777
|
+
executedSteps: chunk.done ? [] : undefined,
|
|
1778
|
+
stoppedReason: undefined,
|
|
1353
1779
|
metadata: chunk.metadata,
|
|
1354
1780
|
structured: chunk.structured,
|
|
1355
1781
|
};
|