@falai/agent 1.0.2 → 1.1.1

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 (109) hide show
  1. package/dist/adapters/MemoryAdapter.d.ts.map +1 -1
  2. package/dist/adapters/MemoryAdapter.js +2 -1
  3. package/dist/adapters/MemoryAdapter.js.map +1 -1
  4. package/dist/adapters/MongoAdapter.d.ts.map +1 -1
  5. package/dist/adapters/MongoAdapter.js +2 -1
  6. package/dist/adapters/MongoAdapter.js.map +1 -1
  7. package/dist/adapters/OpenSearchAdapter.d.ts.map +1 -1
  8. package/dist/adapters/OpenSearchAdapter.js +2 -1
  9. package/dist/adapters/OpenSearchAdapter.js.map +1 -1
  10. package/dist/adapters/PostgreSQLAdapter.d.ts.map +1 -1
  11. package/dist/adapters/PostgreSQLAdapter.js +2 -1
  12. package/dist/adapters/PostgreSQLAdapter.js.map +1 -1
  13. package/dist/adapters/PrismaAdapter.d.ts.map +1 -1
  14. package/dist/adapters/PrismaAdapter.js +2 -1
  15. package/dist/adapters/PrismaAdapter.js.map +1 -1
  16. package/dist/adapters/RedisAdapter.d.ts.map +1 -1
  17. package/dist/adapters/RedisAdapter.js.map +1 -1
  18. package/dist/adapters/SQLiteAdapter.d.ts.map +1 -1
  19. package/dist/adapters/SQLiteAdapter.js.map +1 -1
  20. package/dist/cjs/adapters/MemoryAdapter.d.ts.map +1 -1
  21. package/dist/cjs/adapters/MemoryAdapter.js +2 -1
  22. package/dist/cjs/adapters/MemoryAdapter.js.map +1 -1
  23. package/dist/cjs/adapters/MongoAdapter.d.ts.map +1 -1
  24. package/dist/cjs/adapters/MongoAdapter.js +2 -1
  25. package/dist/cjs/adapters/MongoAdapter.js.map +1 -1
  26. package/dist/cjs/adapters/OpenSearchAdapter.d.ts.map +1 -1
  27. package/dist/cjs/adapters/OpenSearchAdapter.js +2 -1
  28. package/dist/cjs/adapters/OpenSearchAdapter.js.map +1 -1
  29. package/dist/cjs/adapters/PostgreSQLAdapter.d.ts.map +1 -1
  30. package/dist/cjs/adapters/PostgreSQLAdapter.js +2 -1
  31. package/dist/cjs/adapters/PostgreSQLAdapter.js.map +1 -1
  32. package/dist/cjs/adapters/PrismaAdapter.d.ts.map +1 -1
  33. package/dist/cjs/adapters/PrismaAdapter.js +2 -1
  34. package/dist/cjs/adapters/PrismaAdapter.js.map +1 -1
  35. package/dist/cjs/adapters/RedisAdapter.d.ts.map +1 -1
  36. package/dist/cjs/adapters/RedisAdapter.js.map +1 -1
  37. package/dist/cjs/adapters/SQLiteAdapter.d.ts.map +1 -1
  38. package/dist/cjs/adapters/SQLiteAdapter.js.map +1 -1
  39. package/dist/cjs/core/BatchExecutor.d.ts +6 -0
  40. package/dist/cjs/core/BatchExecutor.d.ts.map +1 -1
  41. package/dist/cjs/core/BatchExecutor.js +14 -5
  42. package/dist/cjs/core/BatchExecutor.js.map +1 -1
  43. package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
  44. package/dist/cjs/core/ResponseModal.js +1 -0
  45. package/dist/cjs/core/ResponseModal.js.map +1 -1
  46. package/dist/cjs/core/SessionManager.d.ts.map +1 -1
  47. package/dist/cjs/core/SessionManager.js +4 -11
  48. package/dist/cjs/core/SessionManager.js.map +1 -1
  49. package/dist/cjs/index.d.ts +1 -1
  50. package/dist/cjs/index.d.ts.map +1 -1
  51. package/dist/cjs/index.js +3 -2
  52. package/dist/cjs/index.js.map +1 -1
  53. package/dist/cjs/types/agent.d.ts +11 -0
  54. package/dist/cjs/types/agent.d.ts.map +1 -1
  55. package/dist/cjs/types/route.d.ts +1 -1
  56. package/dist/cjs/types/route.d.ts.map +1 -1
  57. package/dist/cjs/utils/session.d.ts +6 -0
  58. package/dist/cjs/utils/session.d.ts.map +1 -1
  59. package/dist/cjs/utils/session.js +26 -8
  60. package/dist/cjs/utils/session.js.map +1 -1
  61. package/dist/core/BatchExecutor.d.ts +6 -0
  62. package/dist/core/BatchExecutor.d.ts.map +1 -1
  63. package/dist/core/BatchExecutor.js +15 -6
  64. package/dist/core/BatchExecutor.js.map +1 -1
  65. package/dist/core/ResponseModal.d.ts.map +1 -1
  66. package/dist/core/ResponseModal.js +1 -0
  67. package/dist/core/ResponseModal.js.map +1 -1
  68. package/dist/core/SessionManager.d.ts.map +1 -1
  69. package/dist/core/SessionManager.js +4 -11
  70. package/dist/core/SessionManager.js.map +1 -1
  71. package/dist/index.d.ts +1 -1
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +1 -1
  74. package/dist/index.js.map +1 -1
  75. package/dist/types/agent.d.ts +11 -0
  76. package/dist/types/agent.d.ts.map +1 -1
  77. package/dist/types/route.d.ts +1 -1
  78. package/dist/types/route.d.ts.map +1 -1
  79. package/dist/utils/session.d.ts +6 -0
  80. package/dist/utils/session.d.ts.map +1 -1
  81. package/dist/utils/session.js +26 -8
  82. package/dist/utils/session.js.map +1 -1
  83. package/docs/api/README.md +31 -2
  84. package/docs/api/overview.md +19 -11
  85. package/docs/architecture/multi-step-execution.md +37 -3
  86. package/docs/core/conversation-flows/data-collection.md +8 -2
  87. package/docs/core/conversation-flows/route-dsl.md +8 -1
  88. package/docs/core/conversation-flows/step-transitions.md +4 -0
  89. package/docs/core/conversation-flows/steps.md +3 -1
  90. package/docs/core/persistence/session-storage.md +12 -0
  91. package/docs/guides/migration/README.md +2 -0
  92. package/docs/guides/migration/multi-step-execution.md +29 -9
  93. package/examples/integrations/database-integration.ts +10 -9
  94. package/examples/persistence/custom-adapter.ts +12 -15
  95. package/package.json +1 -1
  96. package/src/adapters/MemoryAdapter.ts +6 -8
  97. package/src/adapters/MongoAdapter.ts +6 -8
  98. package/src/adapters/OpenSearchAdapter.ts +6 -8
  99. package/src/adapters/PostgreSQLAdapter.ts +6 -8
  100. package/src/adapters/PrismaAdapter.ts +4 -6
  101. package/src/adapters/RedisAdapter.ts +4 -7
  102. package/src/adapters/SQLiteAdapter.ts +6 -9
  103. package/src/core/BatchExecutor.ts +143 -125
  104. package/src/core/ResponseModal.ts +1 -0
  105. package/src/core/SessionManager.ts +4 -13
  106. package/src/index.ts +11 -11
  107. package/src/types/agent.ts +11 -0
  108. package/src/types/route.ts +8 -7
  109. package/src/utils/session.ts +48 -10
@@ -5,12 +5,12 @@
5
5
  * and orchestrating their execution with a single LLM call.
6
6
  */
7
7
 
8
- import type {
9
- BatchResult,
10
- StoppedReason,
11
- StepOptions,
12
- BatchExecutionError,
13
- BatchExecutionResult,
8
+ import type {
9
+ BatchResult,
10
+ StoppedReason,
11
+ StepOptions,
12
+ BatchExecutionError,
13
+ BatchExecutionResult,
14
14
  StepRef,
15
15
  BatchExecutionEvent,
16
16
  BatchExecutionEventListener,
@@ -22,7 +22,7 @@ import type { StructuredSchema } from '../types/schema';
22
22
  import { Step } from './Step';
23
23
  import { Route } from './Route';
24
24
  import { END_ROUTE_ID } from '../constants';
25
- import { logger, createTemplateContext, mergeCollected } from '../utils';
25
+ import { logger, createTemplateContext, mergeCollected, createSession } from '../utils';
26
26
 
27
27
  /**
28
28
  * Step configuration relevant for needs-input detection
@@ -108,6 +108,12 @@ export interface DetermineBatchParams<TContext, TData> {
108
108
  sessionData: Partial<TData>;
109
109
  /** Agent context for condition evaluation */
110
110
  context: TContext;
111
+ /**
112
+ * Maximum number of steps to include in this batch.
113
+ * When reached, the batch stops with 'max_steps_reached'.
114
+ * Defaults to 1 (single-step execution).
115
+ */
116
+ maxSteps?: number;
111
117
  }
112
118
 
113
119
  /**
@@ -195,7 +201,7 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
195
201
  details,
196
202
  };
197
203
  this.emitEvent(event);
198
-
204
+
199
205
  // Also log the event when debug mode is enabled
200
206
  logger.debug(`[BatchExecutor] Event: ${type}`, details);
201
207
  }
@@ -222,16 +228,16 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
222
228
  * **Validates: Requirements 1.1, 1.4, 1.5, 7.1, 7.2, 7.3**
223
229
  */
224
230
  async determineBatch(params: DetermineBatchParams<TContext, TData>): Promise<BatchResult<TContext, TData>> {
225
- const { route, currentStep, sessionData, context } = params;
231
+ const { route, currentStep, sessionData, context, maxSteps = 1 } = params;
226
232
  const startTime = Date.now();
227
-
233
+
228
234
  const batchSteps: StepOptions<TContext, TData>[] = [];
229
235
  let stoppedReason: StoppedReason = 'route_complete';
230
236
  let stoppedAtStep: StepOptions<TContext, TData> | undefined;
231
-
237
+
232
238
  // Get all steps in the route for traversal
233
239
  const allSteps = route.getAllSteps();
234
-
240
+
235
241
  // Find starting position
236
242
  let startIndex = 0;
237
243
  if (currentStep) {
@@ -240,40 +246,37 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
240
246
  startIndex = currentIndex;
241
247
  }
242
248
  }
243
-
249
+
244
250
  // Log batch determination start (Requirement 11.1)
245
251
  logger.debug(`[BatchExecutor] Starting batch determination from step index ${startIndex}, total steps: ${allSteps.length}`);
246
-
252
+
247
253
  // Emit batch_start event (Requirement 11.3)
248
254
  this.emitBatchEvent('batch_start', {
249
255
  stepId: currentStep?.id,
250
256
  reason: `Starting batch determination from ${currentStep?.id || 'initial step'}`,
251
257
  batchSize: 0,
252
258
  });
253
-
259
+
254
260
  // Create template context for condition evaluation
255
261
  const templateContext = createTemplateContext<TContext, TData>({
256
262
  context,
257
263
  data: sessionData,
258
- session: {
259
- id: `batch-${Date.now()}`,
260
- data: sessionData
261
- } as SessionState<TData>,
264
+ session: createSession<TData>({ data: sessionData }),
262
265
  });
263
-
266
+
264
267
  // Walk through steps starting from current position
265
268
  for (let i = startIndex; i < allSteps.length; i++) {
266
269
  const step = allSteps[i];
267
270
  const stepOptions = step.toOptions();
268
-
271
+
269
272
  // Check for END_ROUTE (Requirement 2.2)
270
273
  if (step.id === END_ROUTE_ID) {
271
274
  stoppedReason = 'end_route';
272
275
  stoppedAtStep = stepOptions;
273
-
276
+
274
277
  // Log stopping reason (Requirement 11.2)
275
278
  logger.debug(`[BatchExecutor] Reached END_ROUTE, stopping batch`);
276
-
279
+
277
280
  // Emit batch_stop event (Requirement 11.3)
278
281
  this.emitBatchEvent('batch_stop', {
279
282
  stepId: step.id,
@@ -283,14 +286,14 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
283
286
  });
284
287
  break;
285
288
  }
286
-
289
+
287
290
  // Evaluate skipIf condition (Requirements 7.1, 7.2, 7.3)
288
291
  let shouldSkip = false;
289
292
  if (step.skipIf) {
290
293
  try {
291
294
  const skipResult = await step.evaluateSkipIf(templateContext);
292
295
  shouldSkip = skipResult.shouldSkip;
293
-
296
+
294
297
  // Log skipIf evaluation (Requirement 11.1)
295
298
  logger.debug(`[BatchExecutor] Step ${step.id} skipIf evaluated to: ${shouldSkip}`);
296
299
  } catch (error) {
@@ -299,12 +302,12 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
299
302
  shouldSkip = false;
300
303
  }
301
304
  }
302
-
305
+
303
306
  // If skipIf is true, skip this step and continue (Requirement 7.2)
304
307
  if (shouldSkip) {
305
308
  // Log step skip (Requirement 11.1)
306
309
  logger.debug(`[BatchExecutor] Skipping step ${step.id} due to skipIf condition`);
307
-
310
+
308
311
  // Emit step_skipped event (Requirement 11.3)
309
312
  this.emitBatchEvent('step_skipped', {
310
313
  stepId: step.id,
@@ -312,15 +315,15 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
312
315
  });
313
316
  continue;
314
317
  }
315
-
318
+
316
319
  // Evaluate needsInput (Requirements 1.2, 1.3)
317
320
  const stepNeedsInput = needsInput(step, sessionData);
318
-
321
+
319
322
  if (stepNeedsInput) {
320
323
  // Requirement 1.5: Stop when a step needs input
321
324
  stoppedReason = 'needs_input';
322
325
  stoppedAtStep = stepOptions;
323
-
326
+
324
327
  // Log stopping reason with details (Requirement 11.1, 11.2)
325
328
  const missingRequires = step.requires?.filter(
326
329
  field => (sessionData as Record<string, unknown>)[String(field)] === undefined
@@ -337,7 +340,7 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
337
340
  }
338
341
 
339
342
  logger.debug(`[BatchExecutor] Step ${step.id} needs input, stopping batch. Missing requires: [${missingRequires.join(', ')}], Collect fields: [${collectFields.join(', ')}]`);
340
-
343
+
341
344
  // Emit batch_stop event (Requirement 11.3)
342
345
  this.emitBatchEvent('batch_stop', {
343
346
  stepId: step.id,
@@ -347,29 +350,44 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
347
350
  });
348
351
  break;
349
352
  }
350
-
353
+
351
354
  // Requirement 1.4: Step doesn't need input, include in batch
352
355
  batchSteps.push(stepOptions);
353
-
356
+
354
357
  // Log step inclusion with reason (Requirement 11.1)
355
358
  logger.debug(`[BatchExecutor] Including step ${step.id} in batch (all requirements satisfied)`);
356
-
359
+
357
360
  // Emit step_included event (Requirement 11.3)
358
361
  this.emitBatchEvent('step_included', {
359
362
  stepId: step.id,
360
363
  reason: 'All requirements satisfied, no input needed',
361
364
  batchSize: batchSteps.length,
362
365
  });
363
-
366
+
367
+ // Check if we've reached the max steps limit
368
+ if (batchSteps.length >= maxSteps) {
369
+ stoppedReason = 'max_steps_reached';
370
+
371
+ logger.debug(`[BatchExecutor] Reached maxStepsPerBatch limit (${maxSteps}), stopping batch`);
372
+
373
+ this.emitBatchEvent('batch_stop', {
374
+ stepId: step.id,
375
+ reason: `Reached maxStepsPerBatch limit (${maxSteps})`,
376
+ stoppedReason: 'max_steps_reached',
377
+ batchSize: batchSteps.length,
378
+ });
379
+ break;
380
+ }
381
+
364
382
  // Move to next step in the sequence
365
383
  const transitions = step.getTransitions();
366
384
  if (transitions.length === 0) {
367
385
  // No more transitions, route is complete
368
386
  stoppedReason = 'route_complete';
369
-
387
+
370
388
  // Log stopping reason (Requirement 11.2)
371
389
  logger.debug(`[BatchExecutor] No more transitions from step ${step.id}, route complete`);
372
-
390
+
373
391
  // Emit batch_stop event (Requirement 11.3)
374
392
  this.emitBatchEvent('batch_stop', {
375
393
  stepId: step.id,
@@ -379,7 +397,7 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
379
397
  });
380
398
  break;
381
399
  }
382
-
400
+
383
401
  // For linear routes, follow the first transition
384
402
  // For branching routes, we'd need more complex logic
385
403
  const nextStep = transitions[0];
@@ -393,10 +411,10 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
393
411
  // Next step is END_ROUTE
394
412
  stoppedReason = 'end_route';
395
413
  stoppedAtStep = nextStep.toOptions();
396
-
414
+
397
415
  // Log stopping reason (Requirement 11.2)
398
416
  logger.debug(`[BatchExecutor] Next step is END_ROUTE, stopping batch`);
399
-
417
+
400
418
  // Emit batch_stop event (Requirement 11.3)
401
419
  this.emitBatchEvent('batch_stop', {
402
420
  stepId: nextStep.id,
@@ -408,11 +426,11 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
408
426
  }
409
427
  }
410
428
  }
411
-
429
+
412
430
  // Log batch determination complete with timing (Requirement 11.1, 11.2)
413
431
  const determinationTime = Date.now() - startTime;
414
432
  logger.debug(`[BatchExecutor] Batch determination complete. Steps: ${batchSteps.length}, Stopped reason: ${stoppedReason}, Time: ${determinationTime}ms`);
415
-
433
+
416
434
  return {
417
435
  steps: batchSteps,
418
436
  stoppedReason,
@@ -434,13 +452,13 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
434
452
  async executePrepareHooks(params: ExecuteHooksParams<TContext, TData>): Promise<HookExecutionResult> {
435
453
  const { steps, context, data, executeHook } = params;
436
454
  const executedSteps: string[] = [];
437
-
455
+
438
456
  logger.debug(`[BatchExecutor] Executing prepare hooks for ${steps.length} steps`);
439
-
457
+
440
458
  for (const step of steps) {
441
459
  if (step.prepare) {
442
460
  logger.debug(`[BatchExecutor] Executing prepare hook for step: ${step.id}`);
443
-
461
+
444
462
  try {
445
463
  await executeHook(step.prepare, context, data, step);
446
464
  executedSteps.push(step.id || 'unknown');
@@ -449,7 +467,7 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
449
467
  // Requirement 5.4: Stop on prepare hook failure
450
468
  const errorMessage = error instanceof Error ? error.message : String(error);
451
469
  logger.error(`[BatchExecutor] Prepare hook failed for step ${step.id}: ${errorMessage}`);
452
-
470
+
453
471
  return {
454
472
  success: false,
455
473
  executedSteps,
@@ -463,7 +481,7 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
463
481
  }
464
482
  }
465
483
  }
466
-
484
+
467
485
  logger.debug(`[BatchExecutor] All prepare hooks completed successfully`);
468
486
  return {
469
487
  success: true,
@@ -487,13 +505,13 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
487
505
  const { steps, context, data, executeHook } = params;
488
506
  const executedSteps: string[] = [];
489
507
  const errors: Array<{ stepId: string; error: BatchExecutionError }> = [];
490
-
508
+
491
509
  logger.debug(`[BatchExecutor] Executing finalize hooks for ${steps.length} steps`);
492
-
510
+
493
511
  for (const step of steps) {
494
512
  if (step.finalize) {
495
513
  logger.debug(`[BatchExecutor] Executing finalize hook for step: ${step.id}`);
496
-
514
+
497
515
  try {
498
516
  await executeHook(step.finalize, context, data, step);
499
517
  executedSteps.push(step.id || 'unknown');
@@ -502,7 +520,7 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
502
520
  // Requirement 5.5: Log error and continue with remaining hooks
503
521
  const errorMessage = error instanceof Error ? error.message : String(error);
504
522
  logger.error(`[BatchExecutor] Finalize hook failed for step ${step.id}: ${errorMessage}`);
505
-
523
+
506
524
  errors.push({
507
525
  stepId: step.id || 'unknown',
508
526
  error: {
@@ -512,18 +530,18 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
512
530
  details: error,
513
531
  },
514
532
  });
515
-
533
+
516
534
  // Continue to next step despite error
517
535
  }
518
536
  }
519
537
  }
520
-
538
+
521
539
  if (errors.length > 0) {
522
540
  logger.warn(`[BatchExecutor] ${errors.length} finalize hook(s) failed, but execution continued`);
523
541
  } else {
524
542
  logger.debug(`[BatchExecutor] All finalize hooks completed successfully`);
525
543
  }
526
-
544
+
527
545
  // Always return success for finalize hooks (errors are logged but don't stop execution)
528
546
  return {
529
547
  success: true,
@@ -571,36 +589,36 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
571
589
  * **Validates: Requirements 9.1, 9.2, 9.3, 2.4**
572
590
  */
573
591
  async executeBatch(params: ExecuteBatchParams<TContext, TData>): Promise<BatchExecutionResult<TData>> {
574
- const {
575
- batch,
576
- session: initialSession,
577
- context,
578
- executeHook,
579
- generateMessage,
592
+ const {
593
+ batch,
594
+ session: initialSession,
595
+ context,
596
+ executeHook,
597
+ generateMessage,
580
598
  schema,
581
599
  routeId,
582
600
  } = params;
583
-
601
+
584
602
  // Track timing for each phase (Requirement 11.1)
585
603
  const timing: BatchExecutionTiming = {
586
604
  totalMs: 0,
587
605
  };
588
606
  const batchStartTime = Date.now();
589
-
607
+
590
608
  // Track the last successful session state for error recovery
591
609
  let lastSuccessfulSession = initialSession;
592
610
  let currentSession = initialSession;
593
-
611
+
594
612
  // Track executed steps for the response
595
613
  const executedSteps: StepRef[] = [];
596
-
614
+
597
615
  // Log batch execution start with details (Requirement 11.1)
598
616
  logger.debug(`[BatchExecutor] Starting batch execution with ${batch.steps.length} steps, route: ${routeId || 'unknown'}`);
599
-
617
+
600
618
  // If batch is empty, return early with appropriate reason
601
619
  if (batch.steps.length === 0) {
602
620
  logger.debug(`[BatchExecutor] Empty batch, returning with stopped reason: ${batch.stoppedReason}`);
603
-
621
+
604
622
  // Emit batch_complete event for empty batch (Requirement 11.3)
605
623
  timing.totalMs = Date.now() - batchStartTime;
606
624
  this.emitBatchEvent('batch_complete', {
@@ -609,7 +627,7 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
609
627
  reason: 'Empty batch',
610
628
  timing,
611
629
  });
612
-
630
+
613
631
  return {
614
632
  message: '',
615
633
  session: currentSession,
@@ -618,26 +636,26 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
618
636
  collectedData: {},
619
637
  };
620
638
  }
621
-
639
+
622
640
  // PHASE 1: Execute prepare hooks (Requirement 5.4 - stop on failure)
623
641
  const prepareStartTime = Date.now();
624
642
  logger.debug(`[BatchExecutor] Phase 1: Executing prepare hooks`);
625
-
643
+
626
644
  const prepareResult = await this.executePrepareHooks({
627
645
  steps: batch.steps,
628
646
  context,
629
647
  data: currentSession.data,
630
648
  executeHook,
631
649
  });
632
-
650
+
633
651
  timing.prepareHooksMs = Date.now() - prepareStartTime;
634
652
  logger.debug(`[BatchExecutor] Prepare hooks completed in ${timing.prepareHooksMs}ms`);
635
-
653
+
636
654
  if (!prepareResult.success) {
637
655
  // Requirement 9.3: Preserve partial progress on errors
638
656
  // Requirement 2.4: Stop and include error information
639
657
  logger.error(`[BatchExecutor] Prepare hook failed:`, prepareResult.error);
640
-
658
+
641
659
  // Emit batch_complete event with error (Requirement 11.3)
642
660
  timing.totalMs = Date.now() - batchStartTime;
643
661
  this.emitBatchEvent('batch_complete', {
@@ -646,7 +664,7 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
646
664
  reason: `Prepare hook failed: ${prepareResult.error?.message}`,
647
665
  timing,
648
666
  });
649
-
667
+
650
668
  return {
651
669
  message: '',
652
670
  session: lastSuccessfulSession, // Return last successful state
@@ -658,22 +676,22 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
658
676
  error: prepareResult.error,
659
677
  };
660
678
  }
661
-
679
+
662
680
  // Update last successful session after prepare hooks complete
663
681
  lastSuccessfulSession = currentSession;
664
-
682
+
665
683
  // PHASE 2: Make LLM call (Requirement 9.1 - preserve session state on failure)
666
684
  const llmStartTime = Date.now();
667
685
  logger.debug(`[BatchExecutor] Phase 2: Making LLM call`);
668
-
686
+
669
687
  let llmResponse: Record<string, unknown>;
670
688
  let message: string;
671
-
689
+
672
690
  try {
673
691
  const result = await generateMessage();
674
692
  llmResponse = result.structured || {};
675
693
  message = result.message || '';
676
-
694
+
677
695
  timing.llmCallMs = Date.now() - llmStartTime;
678
696
  logger.debug(`[BatchExecutor] LLM call successful in ${timing.llmCallMs}ms`);
679
697
  } catch (error) {
@@ -681,7 +699,7 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
681
699
  const errorMessage = error instanceof Error ? error.message : String(error);
682
700
  timing.llmCallMs = Date.now() - llmStartTime;
683
701
  logger.error(`[BatchExecutor] LLM call failed after ${timing.llmCallMs}ms:`, errorMessage);
684
-
702
+
685
703
  // Emit batch_complete event with error (Requirement 11.3)
686
704
  timing.totalMs = Date.now() - batchStartTime;
687
705
  this.emitBatchEvent('batch_complete', {
@@ -690,7 +708,7 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
690
708
  reason: `LLM call failed: ${errorMessage}`,
691
709
  timing,
692
710
  });
693
-
711
+
694
712
  return {
695
713
  message: '',
696
714
  session: lastSuccessfulSession, // Preserve session state
@@ -703,50 +721,50 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
703
721
  },
704
722
  };
705
723
  }
706
-
724
+
707
725
  // Update last successful session after LLM call
708
726
  lastSuccessfulSession = currentSession;
709
-
727
+
710
728
  // PHASE 3: Collect and validate data (Requirement 9.2 - include validation errors)
711
729
  const collectStartTime = Date.now();
712
730
  logger.debug(`[BatchExecutor] Phase 3: Collecting and validating data`);
713
-
731
+
714
732
  const collectResult = this.collectBatchData({
715
733
  steps: batch.steps,
716
734
  llmResponse,
717
735
  session: currentSession,
718
736
  schema,
719
737
  });
720
-
738
+
721
739
  timing.dataCollectionMs = Date.now() - collectStartTime;
722
740
  logger.debug(`[BatchExecutor] Data collection completed in ${timing.dataCollectionMs}ms`);
723
-
741
+
724
742
  // Update session with collected data (even if validation failed)
725
743
  currentSession = collectResult.session;
726
-
744
+
727
745
  // Track collected data for response
728
746
  const collectedData = collectResult.collectedData;
729
-
747
+
730
748
  // Check for validation errors
731
749
  let validationError: BatchExecutionError | undefined;
732
750
  if (!collectResult.success && collectResult.validationErrors && collectResult.validationErrors.length > 0) {
733
751
  // Requirement 9.2: Include validation errors in response
734
752
  logger.warn(`[BatchExecutor] Data validation failed:`, collectResult.validationErrors);
735
-
753
+
736
754
  validationError = {
737
755
  type: 'data_validation',
738
756
  message: `Validation failed for ${collectResult.validationErrors.length} field(s): ${collectResult.validationErrors.map(e => e.field).join(', ')}`,
739
757
  details: collectResult.validationErrors,
740
758
  };
741
-
759
+
742
760
  // Note: We continue execution despite validation errors
743
761
  // The error is included in the response for the caller to handle
744
762
  }
745
-
763
+
746
764
  // Update last successful session after data collection
747
765
  // (even with validation errors, we preserve the collected data)
748
766
  lastSuccessfulSession = currentSession;
749
-
767
+
750
768
  // Build executed steps list
751
769
  for (const step of batch.steps) {
752
770
  if (step.id) {
@@ -756,26 +774,26 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
756
774
  });
757
775
  }
758
776
  }
759
-
777
+
760
778
  // PHASE 4: Execute finalize hooks (Requirement 5.5 - continue on failure)
761
779
  const finalizeStartTime = Date.now();
762
780
  logger.debug(`[BatchExecutor] Phase 4: Executing finalize hooks`);
763
-
781
+
764
782
  const finalizeResult = await this.executeFinalizeHooks({
765
783
  steps: batch.steps,
766
784
  context,
767
785
  data: currentSession.data,
768
786
  executeHook,
769
787
  });
770
-
788
+
771
789
  timing.finalizeHooksMs = Date.now() - finalizeStartTime;
772
790
  logger.debug(`[BatchExecutor] Finalize hooks completed in ${timing.finalizeHooksMs}ms`);
773
-
791
+
774
792
  // Log finalize errors but don't fail the batch
775
793
  let finalizeError: BatchExecutionError | undefined;
776
794
  if (finalizeResult.errors && finalizeResult.errors.length > 0) {
777
795
  logger.warn(`[BatchExecutor] Some finalize hooks failed:`, finalizeResult.errors);
778
-
796
+
779
797
  // Create a summary error for finalize failures
780
798
  finalizeError = {
781
799
  type: 'finalize_hook',
@@ -783,12 +801,12 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
783
801
  details: finalizeResult.errors,
784
802
  };
785
803
  }
786
-
804
+
787
805
  // Determine the final stopped reason
788
806
  // Priority: validation_error > finalize_error > batch.stoppedReason
789
807
  let finalStoppedReason: StoppedReason = batch.stoppedReason;
790
808
  let finalError: BatchExecutionError | undefined;
791
-
809
+
792
810
  if (validationError) {
793
811
  finalStoppedReason = 'validation_error';
794
812
  finalError = validationError;
@@ -797,11 +815,11 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
797
815
  // but include the error in the response
798
816
  finalError = finalizeError;
799
817
  }
800
-
818
+
801
819
  // Calculate total time and log completion (Requirement 11.1, 11.2)
802
820
  timing.totalMs = Date.now() - batchStartTime;
803
821
  logger.debug(`[BatchExecutor] Batch execution complete. Stopped reason: ${finalStoppedReason}, Executed steps: ${executedSteps.length}, Total time: ${timing.totalMs}ms`);
804
-
822
+
805
823
  // Emit batch_complete event (Requirement 11.3)
806
824
  this.emitBatchEvent('batch_complete', {
807
825
  batchSize: executedSteps.length,
@@ -809,7 +827,7 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
809
827
  reason: `Batch completed with ${executedSteps.length} steps`,
810
828
  timing,
811
829
  });
812
-
830
+
813
831
  return {
814
832
  message,
815
833
  session: currentSession,
@@ -836,9 +854,9 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
836
854
  */
837
855
  collectBatchData(params: CollectBatchDataParams<TData>): CollectBatchDataResult<TData> {
838
856
  const { steps, llmResponse, session, schema } = params;
839
-
857
+
840
858
  logger.debug(`[BatchExecutor] Collecting batch data from ${steps.length} steps`);
841
-
859
+
842
860
  // Requirement 6.1: Gather all collect fields from all steps in the batch
843
861
  const allCollectFields = new Set<string>();
844
862
  for (const step of steps) {
@@ -848,9 +866,9 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
848
866
  }
849
867
  }
850
868
  }
851
-
869
+
852
870
  logger.debug(`[BatchExecutor] Collect fields to extract: ${Array.from(allCollectFields).join(', ')}`);
853
-
871
+
854
872
  // If no fields to collect, return early with unchanged session
855
873
  if (allCollectFields.size === 0) {
856
874
  logger.debug(`[BatchExecutor] No collect fields defined, skipping data collection`);
@@ -861,12 +879,12 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
861
879
  fieldsCollected: [],
862
880
  };
863
881
  }
864
-
882
+
865
883
  // Extract data from LLM response for all collect fields
866
884
  const collectedData: Partial<TData> = {};
867
885
  const fieldsCollected: string[] = [];
868
886
  const fieldsMissing: string[] = [];
869
-
887
+
870
888
  for (const field of allCollectFields) {
871
889
  // Check if the field exists in the LLM response
872
890
  if (field in llmResponse && llmResponse[field] !== undefined) {
@@ -878,20 +896,20 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
878
896
  logger.debug(`[BatchExecutor] Field '${field}' not found in LLM response`);
879
897
  }
880
898
  }
881
-
899
+
882
900
  // Requirement 6.2: Validate collected data against the agent schema
883
901
  const validationErrors: ValidationError[] = [];
884
-
902
+
885
903
  if (schema && Object.keys(collectedData).length > 0) {
886
904
  logger.debug(`[BatchExecutor] Validating collected data against schema`);
887
-
905
+
888
906
  const validationResult = this.validateAgainstSchema(collectedData, schema);
889
907
  if (!validationResult.valid) {
890
908
  validationErrors.push(...validationResult.errors);
891
909
  logger.warn(`[BatchExecutor] Schema validation found ${validationErrors.length} error(s)`);
892
910
  }
893
911
  }
894
-
912
+
895
913
  // Requirement 6.3: Update session data with all collected values
896
914
  // Only merge valid data (data that passed validation or had no schema to validate against)
897
915
  let updatedSession = session;
@@ -901,11 +919,11 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
901
919
  updatedSession = mergeCollected(session, collectedData);
902
920
  logger.debug(`[BatchExecutor] Updated session with collected data`);
903
921
  }
904
-
922
+
905
923
  const success = validationErrors.length === 0;
906
-
924
+
907
925
  logger.debug(`[BatchExecutor] Data collection complete. Success: ${success}, Fields collected: ${fieldsCollected.length}, Fields missing: ${fieldsMissing.length}`);
908
-
926
+
909
927
  return {
910
928
  success,
911
929
  collectedData,
@@ -935,7 +953,7 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
935
953
  schema: StructuredSchema
936
954
  ): { valid: boolean; errors: ValidationError[] } {
937
955
  const errors: ValidationError[] = [];
938
-
956
+
939
957
  // Check if provided fields exist in schema
940
958
  if (schema.properties) {
941
959
  for (const [key, value] of Object.entries(data)) {
@@ -956,7 +974,7 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
956
974
  }
957
975
  }
958
976
  }
959
-
977
+
960
978
  return {
961
979
  valid: errors.length === 0,
962
980
  errors,
@@ -976,18 +994,18 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
976
994
  // Null/undefined values are handled separately (required field check)
977
995
  return null;
978
996
  }
979
-
997
+
980
998
  const expectedType = fieldSchema.type;
981
999
  if (!expectedType) {
982
1000
  // No type specified, consider valid
983
1001
  return null;
984
1002
  }
985
-
1003
+
986
1004
  const actualType = Array.isArray(value) ? 'array' : typeof value;
987
-
1005
+
988
1006
  // Handle array of types
989
1007
  const allowedTypes = Array.isArray(expectedType) ? expectedType : [expectedType];
990
-
1008
+
991
1009
  // Map JavaScript types to JSON Schema types
992
1010
  const typeMapping: Record<string, string> = {
993
1011
  'string': 'string',
@@ -996,9 +1014,9 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
996
1014
  'object': 'object',
997
1015
  'array': 'array',
998
1016
  };
999
-
1017
+
1000
1018
  const mappedActualType = typeMapping[actualType] || actualType;
1001
-
1019
+
1002
1020
  // Check if actual type matches any allowed type
1003
1021
  // Also handle 'integer' as a valid number type
1004
1022
  const isValidType = allowedTypes.some(t => {
@@ -1007,7 +1025,7 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
1007
1025
  }
1008
1026
  return t === mappedActualType;
1009
1027
  });
1010
-
1028
+
1011
1029
  if (!isValidType) {
1012
1030
  return {
1013
1031
  field,
@@ -1016,7 +1034,7 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
1016
1034
  schemaPath: `properties.${field}.type`,
1017
1035
  };
1018
1036
  }
1019
-
1037
+
1020
1038
  return null;
1021
1039
  }
1022
1040
  }
@@ -1047,7 +1065,7 @@ export interface ExecuteHooksParams<TContext, TData> {
1047
1065
  /**
1048
1066
  * Type for the hook execution function
1049
1067
  */
1050
- export type HookFunction<TContext, TData> =
1068
+ export type HookFunction<TContext, TData> =
1051
1069
  | string
1052
1070
  | Tool<TContext, TData>
1053
1071
  | ((context: TContext, data?: Partial<TData>) => void | Promise<void>);