@falai/agent 1.2.4 → 1.2.6

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.
@@ -139,6 +139,8 @@ interface ResponseContext<TContext = unknown, TData = unknown> {
139
139
  batchStoppedReason?: StoppedReason;
140
140
  /** Step that caused batch to stop (if applicable) */
141
141
  batchStoppedAtStep?: StepOptions<TContext, TData>;
142
+ /** AbortSignal for cancellation propagation */
143
+ signal?: AbortSignal;
142
144
  }
143
145
 
144
146
  /**
@@ -260,20 +262,28 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
260
262
 
261
263
  // Stream response using existing respondStream method
262
264
  let finalMessage = "";
265
+ let finalizedSession: SessionState<TData> | undefined;
263
266
  for await (const chunk of this.respondStream({
264
267
  history,
265
268
  session,
266
269
  contextOverride: options?.contextOverride,
267
270
  signal: options?.signal,
268
271
  })) {
269
- // Accumulate the final message for session history
272
+ // Accumulate the final message and capture finalized session
270
273
  if (chunk.done) {
271
274
  finalMessage = chunk.accumulated;
275
+ finalizedSession = chunk.session;
272
276
  }
273
277
 
274
278
  yield chunk;
275
279
  }
276
280
 
281
+ // Sync finalized session to agent.session.current (skip in override-history mode)
282
+ // Must happen BEFORE addMessage so the assistant message is added on top of the synced session state
283
+ if (!options?.history && finalizedSession) {
284
+ this.agent.session.syncSession(finalizedSession);
285
+ }
286
+
277
287
  // Add agent response to session history (only if not using override history)
278
288
  if (!options?.history && finalMessage) {
279
289
  await this.agent.session.addMessage("assistant", finalMessage);
@@ -320,6 +330,12 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
320
330
  signal: options?.signal,
321
331
  });
322
332
 
333
+ // Sync finalized session to agent.session.current (skip in override-history mode)
334
+ // Must happen BEFORE addMessage so the assistant message is added on top of the synced session state
335
+ if (!options?.history && result.session) {
336
+ this.agent.session.syncSession(result.session);
337
+ }
338
+
323
339
  // Add agent response to session history (only if not using override history)
324
340
  if (!options?.history) {
325
341
  await this.agent.session.addMessage("assistant", result.message);
@@ -469,6 +485,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
469
485
  batchSteps: routingResult.batchSteps,
470
486
  batchStoppedReason: routingResult.batchStoppedReason,
471
487
  batchStoppedAtStep: routingResult.batchStoppedAtStep,
488
+ signal,
472
489
  };
473
490
  } catch (error) {
474
491
  // Re-throw ResponseGenerationError as-is, wrap others
@@ -711,6 +728,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
711
728
  isRouteComplete,
712
729
  batchSteps,
713
730
  batchStoppedReason,
731
+ signal,
714
732
  } = responseContext;
715
733
  let session = initialSession;
716
734
 
@@ -758,7 +776,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
758
776
  history,
759
777
  context: effectiveContext,
760
778
  historyEvents,
761
- signal: undefined,
779
+ signal,
762
780
  });
763
781
 
764
782
  message = result.message;
@@ -786,7 +804,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
786
804
  context: effectiveContext,
787
805
  history,
788
806
  historyEvents,
789
- signal: undefined,
807
+ signal,
790
808
  });
791
809
 
792
810
  // Set step to END_ROUTE marker
@@ -1517,7 +1535,11 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1517
1535
  }
1518
1536
 
1519
1537
  // Collect data from response
1520
- session = await this.collectDataFromResponse({ result, selectedRoute, nextStep, session });
1538
+ // Use follow-up structured data from tool loop when available, fall back to original result
1539
+ const dataSource = toolResult.structured
1540
+ ? { structured: toolResult.structured }
1541
+ : result;
1542
+ session = await this.collectDataFromResponse({ result: dataSource, selectedRoute, nextStep, session });
1521
1543
 
1522
1544
  return { message, toolCalls, session };
1523
1545
  }
@@ -1769,6 +1791,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1769
1791
  session: SessionState<TData>;
1770
1792
  finalToolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
1771
1793
  finalMessage?: string;
1794
+ structured?: AgentStructuredResponse;
1772
1795
  }> {
1773
1796
  try {
1774
1797
  const { context, history, selectedRoute, responsePrompt, availableTools, responseSchema, signal } = params;
@@ -1857,6 +1880,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1857
1880
  let toolLoopCount = 0;
1858
1881
  let hasToolCalls = toolCalls && toolCalls.length > 0;
1859
1882
  let finalMessage: string | undefined;
1883
+ let followUpStructured: AgentStructuredResponse | undefined;
1860
1884
 
1861
1885
  while (hasToolCalls && toolLoopCount < MAX_TOOL_LOOPS) {
1862
1886
  toolLoopCount++;
@@ -1998,6 +2022,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1998
2022
  logger.debug(`[ResponseModal] Tool loop completed after ${toolLoopCount} iterations`);
1999
2023
  // Update final message and toolCalls from follow-up result if no more tools
2000
2024
  finalMessage = followUpResult.structured?.message || followUpResult.message;
2025
+ followUpStructured = followUpResult.structured;
2001
2026
  toolCalls = followUpToolCalls || [];
2002
2027
  break;
2003
2028
  }
@@ -2007,6 +2032,64 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2007
2032
  logger.warn(`[ResponseModal] Tool loop limit reached (${MAX_TOOL_LOOPS}), stopping`);
2008
2033
  }
2009
2034
 
2035
+ // If tools were executed but no final text message was produced,
2036
+ // make one more LLM call to generate a proper text response from tool results.
2037
+ // This prevents the original tool-invocation message (e.g. "Let me check...")
2038
+ // from being returned as the final user-facing response.
2039
+ if (!finalMessage && toolLoopCount > 0) {
2040
+ logger.debug(`[ResponseModal] No final message after tool loop, making additional LLM call for text response`);
2041
+
2042
+ // Build tool result history for the final call
2043
+ const finalToolResultHistoryItems: HistoryItem[] = [];
2044
+ for (const toolCall of toolCalls || []) {
2045
+ finalToolResultHistoryItems.push({
2046
+ role: "assistant" as const,
2047
+ content: null,
2048
+ tool_calls: [{
2049
+ id: toolCall.toolName,
2050
+ name: toolCall.toolName,
2051
+ arguments: toolCall.arguments,
2052
+ }],
2053
+ });
2054
+ finalToolResultHistoryItems.push({
2055
+ role: "tool" as const,
2056
+ tool_call_id: toolCall.toolName,
2057
+ name: toolCall.toolName,
2058
+ content: toolResultsMap.get(toolCall.toolName) || "Tool executed successfully",
2059
+ });
2060
+ }
2061
+
2062
+ const finalHistory = [...history, ...finalToolResultHistoryItems];
2063
+ const agentOptions = this.agent.getAgentOptions();
2064
+
2065
+ try {
2066
+ const textResult = await agentOptions.provider.generateMessage({
2067
+ prompt: responsePrompt + "\n\nProvide a text response to the user based on the tool results. Do not call any tools.",
2068
+ history: finalHistory,
2069
+ context,
2070
+ tools: [], // No tools - force text response
2071
+ parameters: responseSchema ? {
2072
+ jsonSchema: responseSchema,
2073
+ schemaName: "tool_final_text",
2074
+ } : undefined,
2075
+ signal,
2076
+ });
2077
+
2078
+ finalMessage = textResult.structured?.message || textResult.message;
2079
+ if (textResult.structured) {
2080
+ followUpStructured = textResult.structured;
2081
+ }
2082
+
2083
+ logger.debug(`[ResponseModal] Generated final text response after tool loop:`, {
2084
+ hasMessage: !!finalMessage,
2085
+ messageLength: finalMessage?.length || 0,
2086
+ });
2087
+ } catch (error) {
2088
+ logger.error(`[ResponseModal] Failed to generate final text response after tool loop:`, error);
2089
+ // finalMessage remains undefined; caller will use original message as fallback
2090
+ }
2091
+ }
2092
+
2010
2093
  logger.debug(`[ResponseModal] Tool loop completed:`, {
2011
2094
  totalIterations: toolLoopCount,
2012
2095
  hasFinalMessage: !!finalMessage,
@@ -2018,6 +2101,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2018
2101
  session,
2019
2102
  finalToolCalls: toolCalls,
2020
2103
  finalMessage,
2104
+ structured: followUpStructured,
2021
2105
  };
2022
2106
  } catch (error) {
2023
2107
  throw ResponseGenerationError.fromError(error, 'tool_execution', params, {
package/src/core/Route.ts CHANGED
@@ -567,9 +567,8 @@ export class Route<TContext = unknown, TData = unknown> {
567
567
  * @param data - Currently collected agent-level data
568
568
  * @returns true if all required fields are present, false otherwise
569
569
  *
570
- * Note: Routes with no requiredFields AND no optionalFields are never complete
571
- * based on data (they complete via END_ROUTE). Routes with only optionalFields
572
- * are always complete (optional data doesn't block completion).
570
+ * Note: Routes with no requiredFields (whether they have optionalFields or not)
571
+ * are never complete based on data they can only complete via END_ROUTE.
573
572
  */
574
573
  isComplete(data: Partial<TData>): boolean {
575
574
  // If route has required fields, check if they're all collected
@@ -580,9 +579,10 @@ export class Route<TContext = unknown, TData = unknown> {
580
579
  });
581
580
  }
582
581
 
583
- // If route has optional fields but no required fields, it's always complete
582
+ // If route has optional fields but no required fields, it's NOT complete
583
+ // Optional-only routes can only complete via END_ROUTE
584
584
  if (this.optionalFields && this.optionalFields.length > 0) {
585
- return true;
585
+ return false;
586
586
  }
587
587
 
588
588
  // No required or optional fields - route doesn't complete based on data
@@ -623,9 +623,10 @@ export class Route<TContext = unknown, TData = unknown> {
623
623
  return completedFields.length / this.requiredFields.length;
624
624
  }
625
625
 
626
- // If route has optional fields but no required fields, it's always complete
626
+ // If route has optional fields but no required fields, it's NOT complete
627
+ // Optional-only routes can only complete via END_ROUTE, progress is 0
627
628
  if (this.optionalFields && this.optionalFields.length > 0) {
628
- return 1;
629
+ return 0;
629
630
  }
630
631
 
631
632
  // No required or optional fields - route doesn't complete based on data
@@ -298,6 +298,18 @@ export class SessionManager<TData = unknown> {
298
298
  return newSession;
299
299
  }
300
300
 
301
+ /**
302
+ * Sync the session state without triggering persistence save.
303
+ * Used by modern APIs (stream, generate, chat) to push the finalized session
304
+ * back to `currentSession` after completion. Persistence is handled separately
305
+ * by `finalizeSession()`.
306
+ *
307
+ * @internal Called from ResponseModal after stream()/generate() completion
308
+ */
309
+ syncSession(session: SessionState<TData>): void {
310
+ this.currentSession = session;
311
+ }
312
+
301
313
  /**
302
314
  * Get the persistence manager (for testing purposes)
303
315
  */