@falai/agent 1.2.0 → 1.2.2

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 (78) hide show
  1. package/dist/cjs/core/ResponseEngine.d.ts +0 -2
  2. package/dist/cjs/core/ResponseEngine.d.ts.map +1 -1
  3. package/dist/cjs/core/ResponseEngine.js +2 -6
  4. package/dist/cjs/core/ResponseEngine.js.map +1 -1
  5. package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
  6. package/dist/cjs/core/ResponseModal.js +45 -54
  7. package/dist/cjs/core/ResponseModal.js.map +1 -1
  8. package/dist/cjs/core/ResponsePipeline.d.ts +2 -1
  9. package/dist/cjs/core/ResponsePipeline.d.ts.map +1 -1
  10. package/dist/cjs/core/ResponsePipeline.js +17 -19
  11. package/dist/cjs/core/ResponsePipeline.js.map +1 -1
  12. package/dist/cjs/core/RoutingEngine.d.ts +18 -0
  13. package/dist/cjs/core/RoutingEngine.d.ts.map +1 -1
  14. package/dist/cjs/core/RoutingEngine.js +108 -2
  15. package/dist/cjs/core/RoutingEngine.js.map +1 -1
  16. package/dist/cjs/providers/AnthropicProvider.d.ts +7 -0
  17. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
  18. package/dist/cjs/providers/AnthropicProvider.js +101 -12
  19. package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
  20. package/dist/cjs/providers/GeminiProvider.d.ts +7 -0
  21. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  22. package/dist/cjs/providers/GeminiProvider.js +81 -2
  23. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  24. package/dist/cjs/providers/OpenAIProvider.d.ts +5 -0
  25. package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
  26. package/dist/cjs/providers/OpenAIProvider.js +51 -12
  27. package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
  28. package/dist/cjs/providers/OpenRouterProvider.d.ts +5 -0
  29. package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
  30. package/dist/cjs/providers/OpenRouterProvider.js +50 -12
  31. package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
  32. package/dist/cjs/types/ai.d.ts +2 -2
  33. package/dist/cjs/types/ai.d.ts.map +1 -1
  34. package/dist/core/ResponseEngine.d.ts +0 -2
  35. package/dist/core/ResponseEngine.d.ts.map +1 -1
  36. package/dist/core/ResponseEngine.js +2 -6
  37. package/dist/core/ResponseEngine.js.map +1 -1
  38. package/dist/core/ResponseModal.d.ts.map +1 -1
  39. package/dist/core/ResponseModal.js +46 -55
  40. package/dist/core/ResponseModal.js.map +1 -1
  41. package/dist/core/ResponsePipeline.d.ts +2 -1
  42. package/dist/core/ResponsePipeline.d.ts.map +1 -1
  43. package/dist/core/ResponsePipeline.js +18 -20
  44. package/dist/core/ResponsePipeline.js.map +1 -1
  45. package/dist/core/RoutingEngine.d.ts +18 -0
  46. package/dist/core/RoutingEngine.d.ts.map +1 -1
  47. package/dist/core/RoutingEngine.js +109 -3
  48. package/dist/core/RoutingEngine.js.map +1 -1
  49. package/dist/providers/AnthropicProvider.d.ts +7 -0
  50. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  51. package/dist/providers/AnthropicProvider.js +101 -12
  52. package/dist/providers/AnthropicProvider.js.map +1 -1
  53. package/dist/providers/GeminiProvider.d.ts +7 -0
  54. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  55. package/dist/providers/GeminiProvider.js +81 -2
  56. package/dist/providers/GeminiProvider.js.map +1 -1
  57. package/dist/providers/OpenAIProvider.d.ts +5 -0
  58. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  59. package/dist/providers/OpenAIProvider.js +51 -12
  60. package/dist/providers/OpenAIProvider.js.map +1 -1
  61. package/dist/providers/OpenRouterProvider.d.ts +5 -0
  62. package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
  63. package/dist/providers/OpenRouterProvider.js +50 -12
  64. package/dist/providers/OpenRouterProvider.js.map +1 -1
  65. package/dist/types/ai.d.ts +2 -2
  66. package/dist/types/ai.d.ts.map +1 -1
  67. package/docs/core/agent/session-management.md +53 -0
  68. package/docs/core/conversation-flows/routes.md +2 -0
  69. package/package.json +1 -1
  70. package/src/core/ResponseEngine.ts +2 -9
  71. package/src/core/ResponseModal.ts +56 -67
  72. package/src/core/ResponsePipeline.ts +22 -22
  73. package/src/core/RoutingEngine.ts +159 -3
  74. package/src/providers/AnthropicProvider.ts +110 -12
  75. package/src/providers/GeminiProvider.ts +91 -2
  76. package/src/providers/OpenAIProvider.ts +56 -12
  77. package/src/providers/OpenRouterProvider.ts +55 -12
  78. package/src/types/ai.ts +2 -2
@@ -15,6 +15,7 @@ import type {
15
15
  GenerateMessageStreamChunk,
16
16
  AgentStructuredResponse,
17
17
  } from "../types";
18
+ import type { HistoryItem } from "../types/history";
18
19
  import { withTimeoutAndRetry, logger } from "../utils";
19
20
 
20
21
  const DEFAULT_RETRY_CONFIG = {
@@ -152,6 +153,67 @@ export class AnthropicProvider implements AiProvider {
152
153
  };
153
154
  }
154
155
 
156
+ /**
157
+ * Build Anthropic-formatted messages from HistoryItem[] array.
158
+ * System messages are extracted separately (Anthropic uses a `system` param).
159
+ * Tool results are mapped to Anthropic's tool_result content blocks.
160
+ * Assistant tool_calls are mapped to tool_use content blocks.
161
+ */
162
+ private buildAnthropicMessages(history: HistoryItem[]): {
163
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
164
+ messages: any[];
165
+ systemMessages: string[];
166
+ } {
167
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
168
+ const messages: any[] = [];
169
+ const systemMessages: string[] = [];
170
+
171
+ for (const item of history) {
172
+ switch (item.role) {
173
+ case "system":
174
+ systemMessages.push(item.content);
175
+ break;
176
+ case "user":
177
+ messages.push({ role: "user", content: item.content });
178
+ break;
179
+ case "assistant":
180
+ if (item.tool_calls && item.tool_calls.length > 0) {
181
+ const content: Array<Record<string, unknown>> = [];
182
+ if (item.content) {
183
+ content.push({ type: "text", text: item.content });
184
+ }
185
+ for (const tc of item.tool_calls) {
186
+ content.push({
187
+ type: "tool_use",
188
+ id: tc.id,
189
+ name: tc.name,
190
+ input: tc.arguments,
191
+ });
192
+ }
193
+ messages.push({ role: "assistant", content });
194
+ } else {
195
+ messages.push({ role: "assistant", content: item.content || "" });
196
+ }
197
+ break;
198
+ case "tool":
199
+ // Anthropic tool results are sent as user messages with tool_result content blocks
200
+ messages.push({
201
+ role: "user",
202
+ content: [
203
+ {
204
+ type: "tool_result",
205
+ tool_use_id: item.tool_call_id,
206
+ content: typeof item.content === "string" ? item.content : JSON.stringify(item.content),
207
+ },
208
+ ],
209
+ });
210
+ break;
211
+ }
212
+ }
213
+
214
+ return { messages, systemMessages };
215
+ }
216
+
155
217
  async generateMessage<
156
218
  TContext = unknown,
157
219
  TStructured = AgentStructuredResponse
@@ -248,18 +310,36 @@ export class AnthropicProvider implements AiProvider {
248
310
  // Anthropic requires max_tokens to be specified
249
311
  const maxTokens = input.parameters?.maxOutputTokens || 4096;
250
312
 
313
+ // Build messages from history
314
+ const { messages: historyMessages, systemMessages } = this.buildAnthropicMessages(input.history);
315
+
316
+ // Append the current prompt as the final user message
317
+ historyMessages.push({
318
+ role: "user",
319
+ content: input.prompt,
320
+ });
321
+
251
322
  const params: MessageCreateParamsNonStreaming = {
252
323
  model,
253
324
  max_tokens: maxTokens,
254
- messages: [
255
- {
256
- role: "user",
257
- content: input.prompt,
258
- },
259
- ],
325
+ messages: historyMessages,
260
326
  ...this.config,
261
327
  };
262
328
 
329
+ // Set system messages from history if present
330
+ if (systemMessages.length > 0) {
331
+ if (typeof this.config?.system === "string") {
332
+ params.system = `${this.config.system}\n\n${systemMessages.join("\n\n")}`;
333
+ } else if (Array.isArray(this.config?.system)) {
334
+ params.system = [
335
+ ...this.config.system,
336
+ ...systemMessages.map(s => ({ type: "text" as const, text: s })),
337
+ ];
338
+ } else {
339
+ params.system = systemMessages.join("\n\n");
340
+ }
341
+ }
342
+
263
343
  // Add tools if provided
264
344
  if (input.tools && input.tools.length > 0) {
265
345
  params.tools = input.tools.map((tool) => ({
@@ -438,19 +518,37 @@ export class AnthropicProvider implements AiProvider {
438
518
  // Anthropic requires max_tokens to be specified
439
519
  const maxTokens = input.parameters?.maxOutputTokens || 4096;
440
520
 
521
+ // Build messages from history
522
+ const { messages: historyMessages, systemMessages } = this.buildAnthropicMessages(input.history);
523
+
524
+ // Append the current prompt as the final user message
525
+ historyMessages.push({
526
+ role: "user" as const,
527
+ content: input.prompt,
528
+ });
529
+
441
530
  const params = {
442
531
  model,
443
532
  max_tokens: maxTokens,
444
- messages: [
445
- {
446
- role: "user" as const,
447
- content: input.prompt,
448
- },
449
- ],
533
+ messages: historyMessages,
450
534
  stream: true,
451
535
  ...this.config,
452
536
  };
453
537
 
538
+ // Set system messages from history if present
539
+ if (systemMessages.length > 0) {
540
+ if (typeof this.config?.system === "string") {
541
+ params.system = `${this.config.system}\n\n${systemMessages.join("\n\n")}`;
542
+ } else if (Array.isArray(this.config?.system)) {
543
+ params.system = [
544
+ ...this.config.system,
545
+ ...systemMessages.map(s => ({ type: "text" as const, text: s })),
546
+ ];
547
+ } else {
548
+ params.system = systemMessages.join("\n\n");
549
+ }
550
+ }
551
+
454
552
  // Add tools if provided
455
553
  if (input.tools && input.tools.length > 0) {
456
554
  params.tools = input.tools.map((tool) => ({
@@ -19,6 +19,7 @@ import type {
19
19
  AgentStructuredResponse,
20
20
  StructuredSchema,
21
21
  } from "../types";
22
+ import type { HistoryItem } from "../types/history";
22
23
  import { withTimeoutAndRetry } from "../utils/retry";
23
24
  import { tryParseJSONResponse } from "../utils/json";
24
25
  import { logger } from "../utils/logger";
@@ -152,6 +153,62 @@ export class GeminiProvider implements AiProvider {
152
153
  };
153
154
  }
154
155
 
156
+ /**
157
+ * Build Gemini-formatted contents from HistoryItem[] array.
158
+ * Gemini uses "user"/"model" roles (not "assistant").
159
+ * System messages are extracted separately for systemInstruction.
160
+ * Tool results map to functionResponse parts, tool calls to functionCall parts.
161
+ */
162
+ private buildGeminiContents(history: HistoryItem[]): {
163
+ contents: Array<{ role: string; parts: Array<Record<string, unknown>> }>;
164
+ systemInstructions: string[];
165
+ } {
166
+ const contents: Array<{ role: string; parts: Array<Record<string, unknown>> }> = [];
167
+ const systemInstructions: string[] = [];
168
+
169
+ for (const item of history) {
170
+ switch (item.role) {
171
+ case "system":
172
+ systemInstructions.push(item.content);
173
+ break;
174
+ case "user":
175
+ contents.push({ role: "user", parts: [{ text: item.content }] });
176
+ break;
177
+ case "assistant":
178
+ if (item.tool_calls && item.tool_calls.length > 0) {
179
+ const parts: Array<Record<string, unknown>> = [];
180
+ if (item.content) {
181
+ parts.push({ text: item.content });
182
+ }
183
+ for (const tc of item.tool_calls) {
184
+ parts.push({
185
+ functionCall: { name: tc.name, args: tc.arguments },
186
+ });
187
+ }
188
+ contents.push({ role: "model", parts });
189
+ } else {
190
+ contents.push({ role: "model", parts: [{ text: item.content || "" }] });
191
+ }
192
+ break;
193
+ case "tool":
194
+ contents.push({
195
+ role: "user",
196
+ parts: [
197
+ {
198
+ functionResponse: {
199
+ name: item.name,
200
+ response: typeof item.content === "object" ? item.content : { result: item.content },
201
+ },
202
+ },
203
+ ],
204
+ });
205
+ break;
206
+ }
207
+ }
208
+
209
+ return { contents, systemInstructions };
210
+ }
211
+
155
212
  /**
156
213
  * Convert tool parameter schemas (JSON Schema) to Gemini's Schema format.
157
214
  * Gemini's FunctionDeclaration.parameters expects its own Schema type,
@@ -430,9 +487,25 @@ export class GeminiProvider implements AiProvider {
430
487
 
431
488
  let response: GenerateContentResponse;
432
489
  try {
490
+ // Build contents from history
491
+ const { contents: historyContents, systemInstructions } = this.buildGeminiContents(input.history);
492
+
493
+ // Append the current prompt as the final user content
494
+ historyContents.push({ role: "user", parts: [{ text: input.prompt }] });
495
+
496
+ // Set system instruction from history if present
497
+ if (systemInstructions.length > 0) {
498
+ const existingSystem = configOverride.systemInstruction;
499
+ if (typeof existingSystem === "string") {
500
+ configOverride.systemInstruction = `${existingSystem}\n\n${systemInstructions.join("\n\n")}`;
501
+ } else {
502
+ configOverride.systemInstruction = systemInstructions.join("\n\n");
503
+ }
504
+ }
505
+
433
506
  response = await this.genAI.models.generateContent({
434
507
  model,
435
- contents: input.prompt,
508
+ contents: historyContents,
436
509
  config: {
437
510
  ...configOverride,
438
511
  ...(input.signal ? { abortSignal: input.signal } : {}),
@@ -620,9 +693,25 @@ export class GeminiProvider implements AiProvider {
620
693
 
621
694
  let stream;
622
695
  try {
696
+ // Build contents from history
697
+ const { contents: historyContents, systemInstructions } = this.buildGeminiContents(input.history);
698
+
699
+ // Append the current prompt as the final user content
700
+ historyContents.push({ role: "user", parts: [{ text: input.prompt }] });
701
+
702
+ // Set system instruction from history if present
703
+ if (systemInstructions.length > 0) {
704
+ const existingSystem = configOverride.systemInstruction;
705
+ if (typeof existingSystem === "string") {
706
+ configOverride.systemInstruction = `${existingSystem}\n\n${systemInstructions.join("\n\n")}`;
707
+ } else {
708
+ configOverride.systemInstruction = systemInstructions.join("\n\n");
709
+ }
710
+ }
711
+
623
712
  stream = await this.genAI.models.generateContentStream({
624
713
  model,
625
- contents: input.prompt,
714
+ contents: historyContents,
626
715
  config: {
627
716
  ...configOverride,
628
717
  ...(input.signal ? { abortSignal: input.signal } : {}),
@@ -13,6 +13,7 @@ import type {
13
13
  AgentStructuredResponse,
14
14
  StructuredSchema,
15
15
  } from "../types";
16
+ import type { HistoryItem } from "../types/history";
16
17
  import { withTimeoutAndRetry, logger } from "../utils";
17
18
  import { FunctionParameters } from "openai/resources/shared.mjs";
18
19
 
@@ -161,6 +162,52 @@ export class OpenAIProvider implements AiProvider {
161
162
  };
162
163
  }
163
164
 
165
+ /**
166
+ * Build OpenAI-formatted messages from HistoryItem[] array.
167
+ * Maps directly to ChatCompletionMessageParam format.
168
+ */
169
+ private buildOpenAIMessages(history: HistoryItem[]): Array<unknown> {
170
+ const messages: Array<unknown> = [];
171
+
172
+ for (const item of history) {
173
+ switch (item.role) {
174
+ case "system":
175
+ messages.push({ role: "system", content: item.content });
176
+ break;
177
+ case "user":
178
+ messages.push({ role: "user", content: item.content });
179
+ break;
180
+ case "assistant":
181
+ if (item.tool_calls && item.tool_calls.length > 0) {
182
+ messages.push({
183
+ role: "assistant",
184
+ content: item.content || null,
185
+ tool_calls: item.tool_calls.map(tc => ({
186
+ id: tc.id,
187
+ type: "function",
188
+ function: {
189
+ name: tc.name,
190
+ arguments: JSON.stringify(tc.arguments),
191
+ },
192
+ })),
193
+ });
194
+ } else {
195
+ messages.push({ role: "assistant", content: item.content || "" });
196
+ }
197
+ break;
198
+ case "tool":
199
+ messages.push({
200
+ role: "tool",
201
+ tool_call_id: item.tool_call_id,
202
+ content: typeof item.content === "string" ? item.content : JSON.stringify(item.content),
203
+ });
204
+ break;
205
+ }
206
+ }
207
+
208
+ return messages;
209
+ }
210
+
164
211
  /**
165
212
  * Adapt common schema format to OpenAI's format.
166
213
  * OpenAI uses standard JSON Schema, so this is mostly a passthrough.
@@ -265,14 +312,12 @@ export class OpenAIProvider implements AiProvider {
265
312
  input: GenerateMessageInput<TContext>
266
313
  ): Promise<GenerateMessageOutput<TStructured>> {
267
314
  const operation = async (): Promise<GenerateMessageOutput> => {
315
+ const historyMessages = this.buildOpenAIMessages(input.history);
316
+ historyMessages.push({ role: "user", content: input.prompt });
317
+
268
318
  const params: ChatCompletionCreateParamsNonStreaming = {
269
319
  model,
270
- messages: [
271
- {
272
- role: "user",
273
- content: input.prompt,
274
- },
275
- ],
320
+ messages: historyMessages as ChatCompletionCreateParamsNonStreaming["messages"],
276
321
  ...this.config,
277
322
  };
278
323
 
@@ -468,15 +513,14 @@ export class OpenAIProvider implements AiProvider {
468
513
  model: string,
469
514
  input: GenerateMessageInput<TContext>
470
515
  ): AsyncGenerator<GenerateMessageStreamChunk<TStructured>> {
516
+ // Build messages from history and append prompt as final user message
517
+ const historyMessages = this.buildOpenAIMessages(input.history);
518
+ historyMessages.push({ role: "user" as const, content: input.prompt });
519
+
471
520
  const params = {
472
521
  ...this.config,
473
522
  model,
474
- messages: [
475
- {
476
- role: "user" as const,
477
- content: input.prompt,
478
- },
479
- ],
523
+ messages: historyMessages as ChatCompletionCreateParamsNonStreaming["messages"],
480
524
  stream: true as const,
481
525
  };
482
526
 
@@ -15,6 +15,7 @@ import type {
15
15
  GenerateMessageStreamChunk,
16
16
  StructuredSchema,
17
17
  } from "../types";
18
+ import type { HistoryItem } from "../types/history";
18
19
  import { withTimeoutAndRetry, logger } from "../utils";
19
20
 
20
21
  const DEFAULT_RETRY_CONFIG = {
@@ -171,6 +172,52 @@ export class OpenRouterProvider implements AiProvider {
171
172
  };
172
173
  }
173
174
 
175
+ /**
176
+ * Build OpenRouter-formatted messages from HistoryItem[] array.
177
+ * OpenRouter uses OpenAI-compatible format.
178
+ */
179
+ private buildOpenRouterMessages(history: HistoryItem[]): Array<unknown> {
180
+ const messages: Array<unknown> = [];
181
+
182
+ for (const item of history) {
183
+ switch (item.role) {
184
+ case "system":
185
+ messages.push({ role: "system", content: item.content });
186
+ break;
187
+ case "user":
188
+ messages.push({ role: "user", content: item.content });
189
+ break;
190
+ case "assistant":
191
+ if (item.tool_calls && item.tool_calls.length > 0) {
192
+ messages.push({
193
+ role: "assistant",
194
+ content: item.content || null,
195
+ tool_calls: item.tool_calls.map(tc => ({
196
+ id: tc.id,
197
+ type: "function",
198
+ function: {
199
+ name: tc.name,
200
+ arguments: JSON.stringify(tc.arguments),
201
+ },
202
+ })),
203
+ });
204
+ } else {
205
+ messages.push({ role: "assistant", content: item.content || "" });
206
+ }
207
+ break;
208
+ case "tool":
209
+ messages.push({
210
+ role: "tool",
211
+ tool_call_id: item.tool_call_id,
212
+ content: typeof item.content === "string" ? item.content : JSON.stringify(item.content),
213
+ });
214
+ break;
215
+ }
216
+ }
217
+
218
+ return messages;
219
+ }
220
+
174
221
  /**
175
222
  * Adapt common schema format to OpenRouter's format.
176
223
  * OpenRouter is OpenAI-compatible and uses standard JSON Schema.
@@ -272,14 +319,12 @@ export class OpenRouterProvider implements AiProvider {
272
319
  input: GenerateMessageInput<TContext>
273
320
  ): Promise<GenerateMessageOutput<TStructured>> {
274
321
  const operation = async (): Promise<GenerateMessageOutput> => {
322
+ const historyMessages = this.buildOpenRouterMessages(input.history);
323
+ historyMessages.push({ role: "user", content: input.prompt });
324
+
275
325
  const params: ChatCompletionCreateParamsNonStreaming = {
276
326
  model,
277
- messages: [
278
- {
279
- role: "user",
280
- content: input.prompt,
281
- },
282
- ],
327
+ messages: historyMessages as ChatCompletionCreateParamsNonStreaming["messages"],
283
328
  ...this.config,
284
329
  };
285
330
 
@@ -471,15 +516,13 @@ export class OpenRouterProvider implements AiProvider {
471
516
  model: string,
472
517
  input: GenerateMessageInput<TContext>
473
518
  ): AsyncGenerator<GenerateMessageStreamChunk<TStructured>> {
519
+ const historyMessages = this.buildOpenRouterMessages(input.history);
520
+ historyMessages.push({ role: "user" as const, content: input.prompt });
521
+
474
522
  const params = {
475
523
  ...this.config,
476
524
  model,
477
- messages: [
478
- {
479
- role: "user" as const,
480
- content: input.prompt,
481
- },
482
- ],
525
+ messages: historyMessages as ChatCompletionCreateParamsNonStreaming["messages"],
483
526
  stream: true as const,
484
527
  };
485
528
 
package/src/types/ai.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * AI provider strategy types
3
3
  */
4
4
 
5
- import type { Event } from "./history";
5
+ import type { HistoryItem } from "./history";
6
6
 
7
7
  /**
8
8
  * Reasoning/thinking configuration for AI models
@@ -36,7 +36,7 @@ export interface GenerateMessageInput<TContext = unknown> {
36
36
  /** The constructed prompt */
37
37
  prompt: string;
38
38
  /** Interaction history */
39
- history: Event[];
39
+ history: HistoryItem[];
40
40
  /** Context data */
41
41
  context: TContext;
42
42
  /** Tools available for AI to call during this interaction */