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