@falai/agent 1.0.1 → 1.1.0
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/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +1 -3
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/BatchExecutor.d.ts +6 -0
- package/dist/cjs/core/BatchExecutor.d.ts.map +1 -1
- package/dist/cjs/core/BatchExecutor.js +13 -1
- package/dist/cjs/core/BatchExecutor.js.map +1 -1
- package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
- package/dist/cjs/core/ResponseModal.js +1 -0
- package/dist/cjs/core/ResponseModal.js.map +1 -1
- package/dist/cjs/core/RoutingEngine.d.ts +8 -9
- package/dist/cjs/core/RoutingEngine.d.ts.map +1 -1
- package/dist/cjs/core/RoutingEngine.js +29 -23
- package/dist/cjs/core/RoutingEngine.js.map +1 -1
- package/dist/cjs/types/agent.d.ts +18 -0
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/route.d.ts +1 -1
- package/dist/cjs/types/route.d.ts.map +1 -1
- package/dist/cjs/types/routing.d.ts +0 -4
- package/dist/cjs/types/routing.d.ts.map +1 -1
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +1 -3
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/BatchExecutor.d.ts +6 -0
- package/dist/core/BatchExecutor.d.ts.map +1 -1
- package/dist/core/BatchExecutor.js +13 -1
- package/dist/core/BatchExecutor.js.map +1 -1
- package/dist/core/ResponseModal.d.ts.map +1 -1
- package/dist/core/ResponseModal.js +1 -0
- package/dist/core/ResponseModal.js.map +1 -1
- package/dist/core/RoutingEngine.d.ts +8 -9
- package/dist/core/RoutingEngine.d.ts.map +1 -1
- package/dist/core/RoutingEngine.js +29 -23
- package/dist/core/RoutingEngine.js.map +1 -1
- package/dist/types/agent.d.ts +18 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/route.d.ts +1 -1
- package/dist/types/route.d.ts.map +1 -1
- package/dist/types/routing.d.ts +0 -4
- package/dist/types/routing.d.ts.map +1 -1
- package/docs/README.md +6 -17
- package/docs/api/README.md +12 -16
- package/docs/api/overview.md +11 -7
- package/docs/architecture/data-extraction-flow.md +0 -1
- package/docs/architecture/multi-step-execution.md +37 -3
- package/docs/core/agent/context-management.md +6 -6
- package/docs/core/agent/session-management.md +3 -4
- package/docs/core/conversation-flows/data-collection.md +1 -1
- package/docs/core/conversation-flows/step-transitions.md +4 -0
- package/docs/core/conversation-flows/steps.md +3 -1
- package/docs/core/routing/intelligent-routing.md +12 -8
- package/docs/guides/getting-started/README.md +10 -13
- package/docs/guides/migration/README.md +2 -0
- package/docs/guides/migration/multi-step-execution.md +29 -9
- package/docs/guides/migration/response-modal-refactor.md +2 -2
- package/package.json +1 -1
- package/src/core/Agent.ts +1 -3
- package/src/core/BatchExecutor.ts +22 -1
- package/src/core/ResponseModal.ts +1 -0
- package/src/core/RoutingEngine.ts +63 -50
- package/src/types/agent.ts +18 -0
- package/src/types/route.ts +8 -7
- package/src/types/routing.ts +0 -5
|
@@ -103,9 +103,14 @@ const response3 = await agent.respond("2 people");
|
|
|
103
103
|
|
|
104
104
|
### After: Multi-Step Execution
|
|
105
105
|
|
|
106
|
-
Now, multiple steps can execute in a single call when data requirements are satisfied
|
|
106
|
+
Now, multiple steps can execute in a single call when data requirements are satisfied and `maxStepsPerBatch` is set higher than `1`:
|
|
107
107
|
|
|
108
108
|
```typescript
|
|
109
|
+
const agent = new Agent({
|
|
110
|
+
// ...
|
|
111
|
+
maxStepsPerBatch: Infinity, // Enable batching
|
|
112
|
+
});
|
|
113
|
+
|
|
109
114
|
// Turn 1
|
|
110
115
|
const response = await agent.respond("Book Grand Hotel for 2 on Friday");
|
|
111
116
|
// Pre-extraction captures: { hotel: "Grand Hotel", date: "Friday", guests: 2 }
|
|
@@ -115,6 +120,8 @@ const response = await agent.respond("Book Grand Hotel for 2 on Friday");
|
|
|
115
120
|
// Total: 1 LLM call
|
|
116
121
|
```
|
|
117
122
|
|
|
123
|
+
> **Note:** By default, `maxStepsPerBatch` is `1`, which preserves the classic single-step behavior. You must explicitly opt in to batching.
|
|
124
|
+
|
|
118
125
|
## What Changed
|
|
119
126
|
|
|
120
127
|
| Aspect | Before | After |
|
|
@@ -326,9 +333,20 @@ const step2 = { skipIf: (d) => !!d.email }; // Skipped if email exists
|
|
|
326
333
|
// - Both steps skipped, route may complete immediately
|
|
327
334
|
```
|
|
328
335
|
|
|
329
|
-
##
|
|
336
|
+
## Controlling Batching with `maxStepsPerBatch`
|
|
337
|
+
|
|
338
|
+
As of v1.1.0, batching is **off by default** (`maxStepsPerBatch: 1`). To enable multi-step batching, set the option on your agent:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
const agent = new Agent({
|
|
342
|
+
name: "Assistant",
|
|
343
|
+
provider: provider,
|
|
344
|
+
maxStepsPerBatch: Infinity, // Batch all eligible steps (v1.0.x behavior)
|
|
345
|
+
// maxStepsPerBatch: 3, // Or cap at 3 steps per batch
|
|
346
|
+
});
|
|
347
|
+
```
|
|
330
348
|
|
|
331
|
-
If you need single-step behavior for specific steps, use `requires` to create dependencies:
|
|
349
|
+
If you need single-step behavior for specific steps while batching is enabled, use `requires` to create dependencies:
|
|
332
350
|
|
|
333
351
|
```typescript
|
|
334
352
|
// Force step2 to wait for step1's data
|
|
@@ -363,11 +381,13 @@ const agent = new Agent({
|
|
|
363
381
|
|
|
364
382
|
## Summary
|
|
365
383
|
|
|
366
|
-
1.
|
|
367
|
-
2. **
|
|
368
|
-
3. **
|
|
369
|
-
4. **
|
|
370
|
-
5. **
|
|
371
|
-
6. **
|
|
384
|
+
1. **`maxStepsPerBatch` defaults to `1`** - single-step execution by default (v1.1.0 breaking change)
|
|
385
|
+
2. **Set `maxStepsPerBatch: Infinity`** to restore v1.0.x batching behavior
|
|
386
|
+
3. **Multiple steps can execute together** when batching is enabled, reducing LLM calls
|
|
387
|
+
4. **Pre-extraction happens before batch determination** - maximizing batching
|
|
388
|
+
5. **New response fields** - `executedSteps`, `stoppedReason`, `error`
|
|
389
|
+
6. **Hook execution order changed** - all prepare, then LLM, then all finalize
|
|
390
|
+
7. **SkipIf affects batching** - evaluated during batch determination
|
|
391
|
+
8. **Partial progress preserved** - on errors, completed steps are retained
|
|
372
392
|
|
|
373
393
|
The changes improve efficiency and UX while maintaining API compatibility. Most existing code will work without changes, but reviewing hook dependencies and test expectations is recommended.
|
|
@@ -513,6 +513,6 @@ The modern `stream()` API is the recommended approach for new streaming implemen
|
|
|
513
513
|
---
|
|
514
514
|
|
|
515
515
|
**Need Help?**
|
|
516
|
-
- Check the [examples](
|
|
516
|
+
- Check the [examples](../../../examples/) directory for complete working examples
|
|
517
517
|
- Review the [API documentation](../../api/) for detailed method signatures
|
|
518
|
-
- Look at the [streaming examples](
|
|
518
|
+
- Look at the [streaming examples](../../../examples/advanced-patterns/streaming-responses.ts) for side-by-side comparisons
|
package/package.json
CHANGED
package/src/core/Agent.ts
CHANGED
|
@@ -126,9 +126,7 @@ export class Agent<TContext = any, TData = any> {
|
|
|
126
126
|
|
|
127
127
|
// Initialize routing engine
|
|
128
128
|
this.routingEngine = new RoutingEngine<TContext, TData>({
|
|
129
|
-
|
|
130
|
-
allowRouteSwitch: true,
|
|
131
|
-
switchThreshold: 70,
|
|
129
|
+
routeSwitchMargin: options.routeSwitchMargin,
|
|
132
130
|
});
|
|
133
131
|
|
|
134
132
|
// Initialize ResponseModal for handling all response generation
|
|
@@ -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
|
/**
|
|
@@ -222,7 +228,7 @@ 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>[] = [];
|
|
@@ -361,6 +367,21 @@ export class BatchExecutor<TContext = unknown, TData = unknown> {
|
|
|
361
367
|
batchSize: batchSteps.length,
|
|
362
368
|
});
|
|
363
369
|
|
|
370
|
+
// Check if we've reached the max steps limit
|
|
371
|
+
if (batchSteps.length >= maxSteps) {
|
|
372
|
+
stoppedReason = 'max_steps_reached';
|
|
373
|
+
|
|
374
|
+
logger.debug(`[BatchExecutor] Reached maxStepsPerBatch limit (${maxSteps}), stopping batch`);
|
|
375
|
+
|
|
376
|
+
this.emitBatchEvent('batch_stop', {
|
|
377
|
+
stepId: step.id,
|
|
378
|
+
reason: `Reached maxStepsPerBatch limit (${maxSteps})`,
|
|
379
|
+
stoppedReason: 'max_steps_reached',
|
|
380
|
+
batchSize: batchSteps.length,
|
|
381
|
+
});
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
|
|
364
385
|
// Move to next step in the sequence
|
|
365
386
|
const transitions = step.getTransitions();
|
|
366
387
|
if (transitions.length === 0) {
|
|
@@ -575,6 +575,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
575
575
|
currentStep,
|
|
576
576
|
sessionData: updatedSession.data || {},
|
|
577
577
|
context: params.context,
|
|
578
|
+
maxSteps: this.agent.getAgentOptions().maxStepsPerBatch,
|
|
578
579
|
});
|
|
579
580
|
|
|
580
581
|
batchSteps = batchResult.steps;
|
|
@@ -2,7 +2,6 @@ import type {
|
|
|
2
2
|
Event,
|
|
3
3
|
AgentOptions,
|
|
4
4
|
StructuredSchema,
|
|
5
|
-
RoutingDecision,
|
|
6
5
|
SessionState,
|
|
7
6
|
AiProvider,
|
|
8
7
|
TemplateContext,
|
|
@@ -36,9 +35,12 @@ export interface RoutingDecisionOutput {
|
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
export interface RoutingEngineOptions {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Score margin the best alternative route must exceed the current route's score
|
|
40
|
+
* by before the agent switches routes. Prevents flip-flopping on marginal differences.
|
|
41
|
+
* @default 15
|
|
42
|
+
*/
|
|
43
|
+
routeSwitchMargin?: number;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export interface BuildStepSelectionPromptParams<
|
|
@@ -746,7 +748,8 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
|
|
|
746
748
|
const optimalRoute = this.selectOptimalRoute(
|
|
747
749
|
eligibleRoutes,
|
|
748
750
|
updatedSession.data || {},
|
|
749
|
-
routingResult.structured.routes
|
|
751
|
+
routingResult.structured.routes,
|
|
752
|
+
updatedSession.currentRoute?.id
|
|
750
753
|
);
|
|
751
754
|
|
|
752
755
|
// If no optimal route found, check why
|
|
@@ -908,42 +911,68 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
|
|
|
908
911
|
* @returns Route that should be prioritized for continuation
|
|
909
912
|
*/
|
|
910
913
|
selectOptimalRoute(
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
914
|
+
routes: Route<TContext, TData>[],
|
|
915
|
+
data: Partial<TData>,
|
|
916
|
+
routeScores: Record<string, number>,
|
|
917
|
+
currentRouteId?: string
|
|
918
|
+
): Route<TContext, TData> | undefined {
|
|
919
|
+
const completionStatus = this.getRouteCompletionStatus(routes, data);
|
|
920
|
+
const switchMargin = this.options?.routeSwitchMargin ?? 15;
|
|
921
|
+
|
|
922
|
+
// Create weighted scores combining AI intent scores with completion progress
|
|
923
|
+
const weightedScores: Array<{ route: Route<TContext, TData>; score: number }> = [];
|
|
924
|
+
|
|
925
|
+
for (const route of routes) {
|
|
926
|
+
const aiScore = routeScores[route.id] || 0;
|
|
927
|
+
const completionProgress = completionStatus.get(route.id) || 0;
|
|
928
|
+
|
|
929
|
+
// ALWAYS skip fully completed routes to prevent re-entering finished tasks
|
|
930
|
+
if (completionProgress >= 1.0) {
|
|
931
|
+
logger.debug(
|
|
932
|
+
`[RoutingEngine] Excluding completed route: ${route.title} (100% complete)`
|
|
933
|
+
);
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
919
936
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
937
|
+
// Boost partially complete routes that match user intent
|
|
938
|
+
let weightedScore = aiScore;
|
|
939
|
+
if (completionProgress > 0 && completionProgress < 1.0) {
|
|
940
|
+
weightedScore += (completionProgress * 20); // Up to 20 point boost
|
|
941
|
+
}
|
|
923
942
|
|
|
924
|
-
|
|
925
|
-
// Users should not be forced back into completed routes
|
|
926
|
-
if (completionProgress >= 1.0) {
|
|
927
|
-
logger.debug(
|
|
928
|
-
`[RoutingEngine] Excluding completed route: ${route.title} (100% complete)`
|
|
929
|
-
);
|
|
930
|
-
continue;
|
|
943
|
+
weightedScores.push({ route, score: weightedScore });
|
|
931
944
|
}
|
|
932
945
|
|
|
933
|
-
//
|
|
934
|
-
|
|
935
|
-
if (completionProgress > 0 && completionProgress < 1.0) {
|
|
936
|
-
// Boost score for partially complete routes
|
|
937
|
-
weightedScore += (completionProgress * 20); // Up to 20 point boost
|
|
938
|
-
}
|
|
946
|
+
// Sort by weighted score descending
|
|
947
|
+
weightedScores.sort((a, b) => b.score - a.score);
|
|
939
948
|
|
|
940
|
-
weightedScores.
|
|
941
|
-
|
|
949
|
+
if (weightedScores.length === 0) {
|
|
950
|
+
return undefined;
|
|
951
|
+
}
|
|
942
952
|
|
|
943
|
-
|
|
944
|
-
|
|
953
|
+
// Apply sticky routing: if there's a current route, only switch if the
|
|
954
|
+
// best alternative exceeds the current route's score by the configured margin
|
|
955
|
+
if (currentRouteId) {
|
|
956
|
+
const currentEntry = weightedScores.find(e => e.route.id === currentRouteId);
|
|
957
|
+
const bestEntry = weightedScores[0];
|
|
958
|
+
|
|
959
|
+
if (currentEntry && bestEntry.route.id !== currentRouteId) {
|
|
960
|
+
if (bestEntry.score < currentEntry.score + switchMargin) {
|
|
961
|
+
logger.debug(
|
|
962
|
+
`[RoutingEngine] Staying on current route: ${currentEntry.route.title} ` +
|
|
963
|
+
`(current: ${currentEntry.score}, best alternative: ${bestEntry.score}, ` +
|
|
964
|
+
`margin required: ${switchMargin})`
|
|
965
|
+
);
|
|
966
|
+
return currentEntry.route;
|
|
967
|
+
}
|
|
968
|
+
logger.debug(
|
|
969
|
+
`[RoutingEngine] Switching route: ${currentEntry.route.title} → ${bestEntry.route.title} ` +
|
|
970
|
+
`(current: ${currentEntry.score}, alternative: ${bestEntry.score}, ` +
|
|
971
|
+
`margin: ${switchMargin})`
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
945
975
|
|
|
946
|
-
if (weightedScores.length > 0) {
|
|
947
976
|
logger.debug(
|
|
948
977
|
`[RoutingEngine] Selected optimal route: ${weightedScores[0].route.title} ` +
|
|
949
978
|
`(AI: ${routeScores[weightedScores[0].route.id]}, ` +
|
|
@@ -953,9 +982,6 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
|
|
|
953
982
|
return weightedScores[0].route;
|
|
954
983
|
}
|
|
955
984
|
|
|
956
|
-
return undefined;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
985
|
/**
|
|
960
986
|
* Build prompt for step selection within a single route
|
|
961
987
|
* @private
|
|
@@ -1380,17 +1406,4 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
|
|
|
1380
1406
|
return pc.build();
|
|
1381
1407
|
}
|
|
1382
1408
|
|
|
1383
|
-
decideRouteFromScores(output: RoutingDecision): {
|
|
1384
|
-
routeId: string;
|
|
1385
|
-
maxScore: number;
|
|
1386
|
-
} {
|
|
1387
|
-
// Optionally limit candidates and apply switching threshold
|
|
1388
|
-
const entries = Object.entries(output.routes).sort((a, b) => b[1] - a[1]);
|
|
1389
|
-
const limited = this.options?.maxCandidates
|
|
1390
|
-
? entries.slice(0, this.options.maxCandidates)
|
|
1391
|
-
: entries;
|
|
1392
|
-
const [topId, topScore] = limited[0] || ["", 0];
|
|
1393
|
-
// switchThreshold is enforced by caller when a current route exists
|
|
1394
|
-
return { routeId: topId, maxScore: topScore };
|
|
1395
|
-
}
|
|
1396
1409
|
}
|
package/src/types/agent.ts
CHANGED
|
@@ -114,6 +114,24 @@ export interface AgentOptions<TContext = unknown, TData = unknown> {
|
|
|
114
114
|
schema?: StructuredSchema;
|
|
115
115
|
/** Initial data to pre-populate when creating the agent */
|
|
116
116
|
initialData?: Partial<TData>;
|
|
117
|
+
/**
|
|
118
|
+
* Margin (0-100) the best alternative route must exceed the current route's score
|
|
119
|
+
* by before the agent switches. Higher values make the agent "stickier" to the
|
|
120
|
+
* current route. Set to 0 to switch whenever any route scores higher.
|
|
121
|
+
* @default 15
|
|
122
|
+
*/
|
|
123
|
+
routeSwitchMargin?: number;
|
|
124
|
+
/**
|
|
125
|
+
* Maximum number of steps to execute in a single batch.
|
|
126
|
+
* Controls how many consecutive steps can run together in one LLM call.
|
|
127
|
+
*
|
|
128
|
+
* - `1` (default): Steps execute one at a time (classic behavior)
|
|
129
|
+
* - `Infinity`: No limit — all eligible steps batch together
|
|
130
|
+
* - Any positive integer: Cap the batch to that many steps
|
|
131
|
+
*
|
|
132
|
+
* @default 1
|
|
133
|
+
*/
|
|
134
|
+
maxStepsPerBatch?: number;
|
|
117
135
|
}
|
|
118
136
|
|
|
119
137
|
/**
|
package/src/types/route.ts
CHANGED
|
@@ -12,13 +12,14 @@ import { Template, ConditionTemplate } from "./template";
|
|
|
12
12
|
* Used to indicate the stopping condition for multi-step execution
|
|
13
13
|
*/
|
|
14
14
|
export type StoppedReason =
|
|
15
|
-
| 'needs_input'
|
|
16
|
-
| 'end_route'
|
|
17
|
-
| 'route_complete'
|
|
18
|
-
| '
|
|
19
|
-
| '
|
|
20
|
-
| '
|
|
21
|
-
| '
|
|
15
|
+
| 'needs_input' // Step requires uncollected data
|
|
16
|
+
| 'end_route' // Reached END_ROUTE
|
|
17
|
+
| 'route_complete' // All Steps processed
|
|
18
|
+
| 'max_steps_reached' // Batch hit the maxStepsPerBatch limit
|
|
19
|
+
| 'prepare_error' // Error in prepare hook
|
|
20
|
+
| 'llm_error' // Error during LLM call
|
|
21
|
+
| 'validation_error' // Error validating collected data
|
|
22
|
+
| 'finalize_error'; // Error in finalize hook (non-fatal, logged)
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Event types for batch execution observability
|
package/src/types/routing.ts
CHANGED
|
@@ -8,11 +8,6 @@ export interface RoutingDecision {
|
|
|
8
8
|
contextUpdate?: Record<string, unknown>;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export interface RoutingDecisionWithRoute extends RoutingDecision {
|
|
12
|
-
selectedRouteId: string;
|
|
13
|
-
maxScore: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
11
|
export interface RoutingSchemaOptions {
|
|
17
12
|
extrasSchema?: StructuredSchema;
|
|
18
13
|
}
|