@falai/agent 0.5.4 → 0.5.5

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/src/core/Agent.ts CHANGED
@@ -3,18 +3,12 @@
3
3
  */
4
4
 
5
5
  import type { AgentOptions, Term, Guideline, Capability } from "../types/agent";
6
- import type { Event, StateRef, MessageEventData } from "../types/index";
6
+ import type { Event, StateRef } from "../types/index";
7
7
  import type { RouteOptions } from "../types/route";
8
- import type { RoutingDecisionOutput } from "./RoutingEngine";
8
+
9
9
  import type { SessionState } from "../types/session";
10
10
  import type { AgentStructuredResponse } from "../types/ai";
11
- import {
12
- createSession,
13
- enterRoute,
14
- enterState,
15
- mergeExtracted,
16
- } from "../types/session";
17
- import { EventKind } from "../types/history";
11
+ import { createSession, enterState, mergeExtracted } from "../types/session";
18
12
  import { PromptComposer } from "./PromptComposer";
19
13
 
20
14
  import { Route } from "./Route";
@@ -24,19 +18,7 @@ import { PersistenceManager } from "./PersistenceManager";
24
18
  import { RoutingEngine } from "./RoutingEngine";
25
19
  import { ResponseEngine } from "./ResponseEngine";
26
20
  import { ToolExecutor } from "./ToolExecutor";
27
-
28
- /**
29
- * Helper to extract last message from history
30
- */
31
- function getLastMessageFromHistory(history: Event[]): string {
32
- for (let i = history.length - 1; i >= 0; i--) {
33
- const event = history[i];
34
- if (event.kind === EventKind.MESSAGE) {
35
- return (event.data as MessageEventData).message;
36
- }
37
- }
38
- return "";
39
- }
21
+ import { getLastMessageFromHistory } from "../utils/event";
40
22
 
41
23
  /**
42
24
  * Main Agent class with generic context support
@@ -275,57 +257,6 @@ export class Agent<TContext = unknown> {
275
257
  return this.context;
276
258
  }
277
259
 
278
- /**
279
- * Determine the next state in a route based on extracted data
280
- * @internal
281
- */
282
- private getNextState<TExtracted = unknown>(
283
- route: Route<TContext, TExtracted>,
284
- currentState: State<TContext, TExtracted> | undefined,
285
- extracted: Partial<TExtracted>
286
- ): State<TContext, TExtracted> {
287
- // If no current state, start from initial state
288
- if (!currentState) {
289
- // Check if initial state should be skipped
290
- if (route.initialState.shouldSkip(extracted)) {
291
- return this.getNextState(route, route.initialState, extracted);
292
- }
293
- return route.initialState;
294
- }
295
-
296
- // Get transitions from current state
297
- const transitions = currentState.getTransitions();
298
-
299
- // If no transitions, stay in current state
300
- if (transitions.length === 0) {
301
- return currentState;
302
- }
303
-
304
- // Try to find the next state to transition to
305
- for (const transition of transitions) {
306
- const target = transition.getTarget();
307
- if (!target) continue;
308
-
309
- // Check if target state should be skipped
310
- if (target.shouldSkip(extracted)) {
311
- // Recursively find next non-skipped state
312
- return this.getNextState(route, target, extracted);
313
- }
314
-
315
- // Check if target state has required data
316
- if (!target.hasRequiredData(extracted)) {
317
- // Cannot enter this state yet, stay in current state
318
- continue;
319
- }
320
-
321
- // Found valid next state
322
- return target;
323
- }
324
-
325
- // No valid transition found, stay in current state
326
- return currentState;
327
- }
328
-
329
260
  /**
330
261
  * Generate a response based on history and context as a stream
331
262
  */
@@ -414,106 +345,66 @@ export class Agent<TContext = unknown> {
414
345
  }
415
346
  }
416
347
 
417
- // PHASE 2: ROUTING - Determine which route to use
348
+ // PHASE 2: ROUTING + STATE SELECTION - Determine which route and state to use (combined)
418
349
  let selectedRoute: Route<TContext> | undefined;
419
350
  let responseDirectives: string[] | undefined;
351
+ let selectedState: State<TContext> | undefined;
420
352
 
421
353
  if (this.routes.length > 0) {
422
- // Get last user message
423
- const lastUserMessage = getLastMessageFromHistory(history);
424
-
425
- // Build routing schema
426
- const routingSchema = this.routingEngine.buildDynamicRoutingSchema(
427
- this.routes
428
- );
429
-
430
- // Build routing prompt with session context
431
- const routingPrompt = this.routingEngine.buildRoutingPrompt(
354
+ const orchestration = await this.routingEngine.decideRouteAndState({
355
+ routes: this.routes,
356
+ session,
432
357
  history,
433
- this.routes,
434
- lastUserMessage,
435
- {
358
+ agentMeta: {
436
359
  name: this.options.name,
437
360
  goal: this.options.goal,
438
361
  description: this.options.description,
439
362
  personality: this.options.personality,
440
363
  },
441
- session // Pass session for context-aware routing
442
- );
443
-
444
- // Call AI to score routes (non-streaming for routing decision)
445
- const routingResult = await this.options.ai.generateMessage<
446
- TContext,
447
- RoutingDecisionOutput
448
- >({
449
- prompt: routingPrompt,
450
- history,
364
+ ai: this.options.ai,
451
365
  context: effectiveContext,
452
366
  signal,
453
- parameters: {
454
- jsonSchema: routingSchema,
455
- schemaName: "routing_output",
456
- },
457
367
  });
458
368
 
459
- // Select best route from scores
460
- if (routingResult.structured?.routes) {
461
- const decision = this.routingEngine.decideRouteFromScores({
462
- context: routingResult.structured.context,
463
- routes: routingResult.structured.routes,
464
- responseDirectives: routingResult.structured.responseDirectives,
465
- });
466
- selectedRoute = this.routes.find((r) => r.id === decision.routeId);
467
- responseDirectives = routingResult.structured.responseDirectives;
369
+ selectedRoute = orchestration.selectedRoute;
370
+ selectedState = orchestration.selectedState;
371
+ responseDirectives = orchestration.responseDirectives;
372
+ session = orchestration.session;
373
+ }
468
374
 
469
- if (selectedRoute) {
375
+ // PHASE 3: DETERMINE NEXT STATE - Use state from combined decision or get initial state
376
+ if (selectedRoute) {
377
+ let nextState: State<TContext>;
378
+
379
+ // If we have a selected state from the combined routing decision, use it
380
+ if (selectedState) {
381
+ nextState = selectedState;
382
+ } else {
383
+ // New route or no state selected - get initial state or first valid state
384
+ const candidates = this.routingEngine.getCandidateStates(
385
+ selectedRoute,
386
+ undefined,
387
+ session.extracted
388
+ );
389
+ if (candidates.length > 0) {
390
+ nextState = candidates[0].state;
470
391
  console.log(
471
- `[Agent] Selected route: ${selectedRoute.title} (score: ${decision.maxScore})`
392
+ `[Agent] Using first valid state: ${nextState.id} for new route`
393
+ );
394
+ } else {
395
+ // Fallback to initial state even if it should be skipped
396
+ nextState = selectedRoute.initialState;
397
+ console.warn(
398
+ `[Agent] No valid states found, using initial state: ${nextState.id}`
472
399
  );
473
-
474
- // Update session with selected route (if changed)
475
- if (
476
- !session.currentRoute ||
477
- session.currentRoute.id !== selectedRoute.id
478
- ) {
479
- session = enterRoute(
480
- session,
481
- selectedRoute.id,
482
- selectedRoute.title
483
- );
484
-
485
- // Merge initial data if provided by the route
486
- if (selectedRoute.initialData) {
487
- session = mergeExtracted(session, selectedRoute.initialData);
488
- console.log(
489
- `[Agent] Merged initial data:`,
490
- selectedRoute.initialData
491
- );
492
- }
493
-
494
- console.log(`[Agent] Entered route: ${selectedRoute.title}`);
495
- }
496
400
  }
497
401
  }
498
- }
499
-
500
- // PHASE 3: RESPONSE - Stream message using selected route
501
- if (selectedRoute) {
502
- // Determine next state based on current extracted data
503
- const currentStateRef = session.currentState;
504
- const currentState = currentStateRef
505
- ? selectedRoute.getState(currentStateRef.id)
506
- : undefined;
507
- const nextState = this.getNextState(
508
- selectedRoute,
509
- currentState,
510
- session.extracted
511
- );
512
402
 
513
403
  // Update session with next state
514
404
  session = enterState(session, nextState.id, nextState.description);
515
405
  console.log(`[Agent] Entered state: ${nextState.id}`);
516
406
 
407
+ // PHASE 4: RESPONSE GENERATION - Stream message using selected route and state
517
408
  // Get last user message
518
409
  const lastUserMessage = getLastMessageFromHistory(history);
519
410
 
@@ -742,111 +633,71 @@ export class Agent<TContext = unknown> {
742
633
  }
743
634
  }
744
635
 
745
- // PHASE 2: ROUTING - Determine which route to use
636
+ // PHASE 2: ROUTING + STATE SELECTION - Determine which route and state to use (combined)
746
637
  let selectedRoute: Route<TContext> | undefined;
747
638
  let responseDirectives: string[] | undefined;
639
+ let selectedState: State<TContext> | undefined;
748
640
 
749
641
  if (this.routes.length > 0) {
750
- // Get last user message
751
- const lastUserMessage = getLastMessageFromHistory(history);
752
-
753
- // Build routing schema
754
- const routingSchema = this.routingEngine.buildDynamicRoutingSchema(
755
- this.routes
756
- );
757
-
758
- // Build routing prompt with session context
759
- const routingPrompt = this.routingEngine.buildRoutingPrompt(
642
+ const orchestration = await this.routingEngine.decideRouteAndState({
643
+ routes: this.routes,
644
+ session,
760
645
  history,
761
- this.routes,
762
- lastUserMessage,
763
- {
646
+ agentMeta: {
764
647
  name: this.options.name,
765
648
  goal: this.options.goal,
766
649
  description: this.options.description,
767
650
  personality: this.options.personality,
768
651
  },
769
- session // Pass session for context-aware routing
770
- );
771
-
772
- // Call AI to score routes
773
- const routingResult = await this.options.ai.generateMessage<
774
- TContext,
775
- RoutingDecisionOutput
776
- >({
777
- prompt: routingPrompt,
778
- history,
652
+ ai: this.options.ai,
779
653
  context: effectiveContext,
780
654
  signal,
781
- parameters: {
782
- jsonSchema: routingSchema,
783
- schemaName: "routing_output",
784
- },
785
655
  });
786
656
 
787
- // Select best route from scores
788
- if (routingResult.structured?.routes) {
789
- const decision = this.routingEngine.decideRouteFromScores({
790
- context: routingResult.structured.context,
791
- routes: routingResult.structured.routes,
792
- responseDirectives: routingResult.structured.responseDirectives,
793
- });
794
- selectedRoute = this.routes.find((r) => r.id === decision.routeId);
795
- responseDirectives = routingResult.structured.responseDirectives;
796
-
797
- if (selectedRoute) {
798
- console.log(
799
- `[Agent] Selected route: ${selectedRoute.title} (score: ${decision.maxScore})`
800
- );
801
-
802
- // Update session with selected route (if changed)
803
- if (
804
- !session.currentRoute ||
805
- session.currentRoute.id !== selectedRoute.id
806
- ) {
807
- session = enterRoute(
808
- session,
809
- selectedRoute.id,
810
- selectedRoute.title
811
- );
812
-
813
- // Merge initial data if provided by the route
814
- if (selectedRoute.initialData) {
815
- session = mergeExtracted(session, selectedRoute.initialData);
816
- console.log(
817
- `[Agent] Merged initial data:`,
818
- selectedRoute.initialData
819
- );
820
- }
821
-
822
- console.log(`[Agent] Entered route: ${selectedRoute.title}`);
823
- }
824
- }
825
- }
657
+ selectedRoute = orchestration.selectedRoute;
658
+ selectedState = orchestration.selectedState;
659
+ responseDirectives = orchestration.responseDirectives;
660
+ session = orchestration.session;
826
661
  }
827
662
 
828
- // PHASE 3: RESPONSE - Generate message using selected route
663
+ // PHASE 3: DETERMINE NEXT STATE - Use state from combined decision or get initial state
829
664
  let message: string;
830
665
  const toolCalls:
831
666
  | Array<{ toolName: string; arguments: Record<string, unknown> }>
832
667
  | undefined = undefined;
833
668
 
834
669
  if (selectedRoute) {
835
- // Determine next state based on current extracted data
836
- const currentStateRef = session.currentState;
837
- const currentState = currentStateRef
838
- ? selectedRoute.getState(currentStateRef.id)
839
- : undefined;
840
- const nextState = this.getNextState(
841
- selectedRoute,
842
- currentState,
843
- session.extracted
844
- );
670
+ let nextState: State<TContext>;
671
+
672
+ // If we have a selected state from the combined routing decision, use it
673
+ if (selectedState) {
674
+ nextState = selectedState;
675
+ } else {
676
+ // New route or no state selected - get initial state or first valid state
677
+ const candidates = this.routingEngine.getCandidateStates(
678
+ selectedRoute,
679
+ undefined,
680
+ session.extracted
681
+ );
682
+ if (candidates.length > 0) {
683
+ nextState = candidates[0].state;
684
+ console.log(
685
+ `[Agent] Using first valid state: ${nextState.id} for new route`
686
+ );
687
+ } else {
688
+ // Fallback to initial state even if it should be skipped
689
+ nextState = selectedRoute.initialState;
690
+ console.warn(
691
+ `[Agent] No valid states found, using initial state: ${nextState.id}`
692
+ );
693
+ }
694
+ }
845
695
 
846
696
  // Update session with next state
847
697
  session = enterState(session, nextState.id, nextState.description);
848
698
  console.log(`[Agent] Entered state: ${nextState.id}`);
849
699
 
700
+ // PHASE 4: RESPONSE GENERATION - Generate message using selected route and state
850
701
  // Get last user message
851
702
  const lastUserMessage = getLastMessageFromHistory(history);
852
703