@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
@@ -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 new route that collects data, extract data from user message first
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
- const isEnteringNewRoute = !params.session.currentRoute ||
323
- params.session.currentRoute.id !== routingResult.selectedRoute.id;
324
- if (isEnteringNewRoute && this.shouldPreExtractData(routingResult.selectedRoute)) {
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
- // Update session with pre-extracted data
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
- // Handle normal route processing
451
- const result = await this.processRouteResponse({
452
- selectedRoute,
453
- selectedStep,
454
- responseDirectives,
455
- session,
456
- history,
457
- context: effectiveContext,
458
- lastMessageText,
459
- historyEvents,
460
- signal: responseContext.history ? undefined : undefined, // TODO: Fix signal passing
461
- });
462
- message = result.message;
463
- toolCalls = result.toolCalls;
464
- session = result.session;
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, // TODO: Pass signal from responseContext
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
- // Handle normal route processing with streaming
517
- yield* this.processRouteStreamingResponse({
518
- selectedRoute,
519
- selectedStep,
520
- responseDirectives,
521
- session,
522
- history,
523
- context: effectiveContext,
524
- lastMessageText,
525
- historyEvents,
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
  };