@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
|
@@ -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
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
if (
|
|
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
|
-
//
|
|
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
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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,
|
|
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
|
-
//
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
};
|