@falai/agent 2.5.0 → 2.6.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 (51) hide show
  1. package/dist/cjs/core/Agent.d.ts.map +1 -1
  2. package/dist/cjs/core/Agent.js +3 -1
  3. package/dist/cjs/core/Agent.js.map +1 -1
  4. package/dist/cjs/core/FlowRouter.d.ts.map +1 -1
  5. package/dist/cjs/core/FlowRouter.js +0 -11
  6. package/dist/cjs/core/FlowRouter.js.map +1 -1
  7. package/dist/cjs/core/ResponseModal.d.ts +51 -2
  8. package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
  9. package/dist/cjs/core/ResponseModal.js +272 -269
  10. package/dist/cjs/core/ResponseModal.js.map +1 -1
  11. package/dist/cjs/core/ToolLoopExecutor.js +1 -1
  12. package/dist/cjs/core/ToolLoopExecutor.js.map +1 -1
  13. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  14. package/dist/cjs/providers/GeminiProvider.js +3 -1
  15. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  16. package/dist/cjs/types/agent.d.ts +9 -0
  17. package/dist/cjs/types/agent.d.ts.map +1 -1
  18. package/dist/cjs/utils/streamingMessage.d.ts +48 -0
  19. package/dist/cjs/utils/streamingMessage.d.ts.map +1 -0
  20. package/dist/cjs/utils/streamingMessage.js +210 -0
  21. package/dist/cjs/utils/streamingMessage.js.map +1 -0
  22. package/dist/core/Agent.d.ts.map +1 -1
  23. package/dist/core/Agent.js +3 -1
  24. package/dist/core/Agent.js.map +1 -1
  25. package/dist/core/FlowRouter.d.ts.map +1 -1
  26. package/dist/core/FlowRouter.js +0 -11
  27. package/dist/core/FlowRouter.js.map +1 -1
  28. package/dist/core/ResponseModal.d.ts +51 -2
  29. package/dist/core/ResponseModal.d.ts.map +1 -1
  30. package/dist/core/ResponseModal.js +272 -269
  31. package/dist/core/ResponseModal.js.map +1 -1
  32. package/dist/core/ToolLoopExecutor.js +1 -1
  33. package/dist/core/ToolLoopExecutor.js.map +1 -1
  34. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  35. package/dist/providers/GeminiProvider.js +3 -1
  36. package/dist/providers/GeminiProvider.js.map +1 -1
  37. package/dist/types/agent.d.ts +9 -0
  38. package/dist/types/agent.d.ts.map +1 -1
  39. package/dist/utils/streamingMessage.d.ts +48 -0
  40. package/dist/utils/streamingMessage.d.ts.map +1 -0
  41. package/dist/utils/streamingMessage.js +205 -0
  42. package/dist/utils/streamingMessage.js.map +1 -0
  43. package/docs/reference/create-agent.md +2 -0
  44. package/package.json +1 -1
  45. package/src/core/Agent.ts +3 -1
  46. package/src/core/FlowRouter.ts +0 -14
  47. package/src/core/ResponseModal.ts +332 -299
  48. package/src/core/ToolLoopExecutor.ts +1 -1
  49. package/src/providers/GeminiProvider.ts +4 -2
  50. package/src/types/agent.ts +9 -0
  51. package/src/utils/streamingMessage.ts +220 -0
@@ -351,7 +351,7 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
351
351
  }
352
352
 
353
353
  if (toolLoopCount >= MAX_TOOL_LOOPS) {
354
- logger.warn(`[ResponseGenerationError] Tool loop limit reached: ${toolLoopCount} iterations hit the cap (${MAX_TOOL_LOOPS}). Stopping tool execution. Increase MAX_TOOL_LOOPS or reduce recursive tool calls.`);
354
+ logger.warn(`[ResponseGenerationError] Tool loop limit reached: ${toolLoopCount} iterations hit the cap (${MAX_TOOL_LOOPS}). Stopping tool execution. Increase the agent's maxToolLoops option or reduce recursive tool calls.`);
355
355
  }
356
356
 
357
357
  // If tools were executed but no final text message was produced,
@@ -189,13 +189,15 @@ export class GeminiProvider implements AiProvider {
189
189
  *
190
190
  * @private
191
191
  */
192
- private safeExtractText(responseOrChunk: { text?: string; candidates?: Array<{ content?: { parts?: Array<{ text?: string; functionCall?: unknown }> } }> }): string {
192
+ private safeExtractText(responseOrChunk: { text?: string; candidates?: Array<{ content?: { parts?: Array<{ text?: string; thought?: boolean; functionCall?: unknown }> } }> }): string {
193
193
  // Always extract text parts manually to avoid SDK warnings about
194
194
  // non-text parts like "thoughtSignature" in the response.
195
195
  const parts = responseOrChunk.candidates?.[0]?.content?.parts;
196
196
  if (parts) {
197
+ // Exclude reasoning parts (thought: true) — with includeThoughts enabled
198
+ // they carry text but must never leak into the user-facing message.
197
199
  return parts
198
- .filter((p) => p.text != null)
200
+ .filter((p) => p.text != null && !p.thought)
199
201
  .map((p) => p.text)
200
202
  .join("");
201
203
  }
@@ -187,6 +187,15 @@ export interface AgentOptions<TContext = unknown, TData = unknown> {
187
187
  * @default 10
188
188
  */
189
189
  maxDirectiveChain?: number;
190
+ /**
191
+ * Maximum number of tool loop iterations allowed within a single response
192
+ * generation before the pipeline stops executing further tool calls.
193
+ * Guards against runaway recursive tool calling. An explicit `0` is honored
194
+ * (no tool loops). Applies to both `respond()` and streaming paths.
195
+ *
196
+ * @default 5
197
+ */
198
+ maxToolLoops?: number;
190
199
  /**
191
200
  * Optional compaction configuration for managing conversation history size.
192
201
  * When provided, the agent will validate the options and make them available
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Incremental extraction of the `message` field from a streamed structured
3
+ * JSON response.
4
+ *
5
+ * Providers that enforce a JSON schema stream the raw wrapper object
6
+ * (`{"message":"Hel` → `{"message":"Hello"}`), not the message text. Without
7
+ * this, every streaming consumer has to re-implement partial-JSON unwrapping to
8
+ * recover the user-facing tokens. These helpers do it once, at the framework
9
+ * boundary, so `delta`/`accumulated` carry clean message text and the parsed
10
+ * object is surfaced only when complete.
11
+ *
12
+ * The extractor targets the top-level `message` string field specifically
13
+ * (tracking object depth and string context, so a nested decoy `"message"` key
14
+ * or a `"message"` substring inside another value is never mistaken for it),
15
+ * and is tolerant of truncation at any byte — a dangling escape sequence is
16
+ * held back rather than emitted half-decoded. Input that is not a JSON object
17
+ * (e.g. a provider streaming plain text) is passed through verbatim.
18
+ */
19
+
20
+ const WHITESPACE = " \t\n\r";
21
+
22
+ const ESCAPE_CHARS: Record<string, string> = {
23
+ '"': '"',
24
+ "\\": "\\",
25
+ "/": "/",
26
+ b: "\b",
27
+ f: "\f",
28
+ n: "\n",
29
+ r: "\r",
30
+ t: "\t",
31
+ };
32
+
33
+ interface StringRead {
34
+ /** Decoded value, or decoded prefix when the closing quote has not arrived. */
35
+ value: string;
36
+ /** Index just past the closing quote when complete; the input length otherwise. */
37
+ end: number;
38
+ /** Whether the closing quote was seen. */
39
+ complete: boolean;
40
+ }
41
+
42
+ /**
43
+ * Read a JSON string token whose opening quote is at `s[start]`. Decodes
44
+ * escapes. When the closing quote has not arrived, returns the decoded prefix
45
+ * with any trailing incomplete escape (`\` or a partial `\uXXXX`) held back, so
46
+ * a half-decoded character is never produced.
47
+ */
48
+ function readJsonString(s: string, start: number): StringRead {
49
+ let out = "";
50
+ const n = s.length;
51
+ let i = start + 1; // skip opening quote
52
+
53
+ while (i < n) {
54
+ const ch = s[i];
55
+
56
+ if (ch === '"') {
57
+ return { value: out, end: i + 1, complete: true };
58
+ }
59
+
60
+ if (ch === "\\") {
61
+ const esc = s[i + 1];
62
+ if (esc === undefined) {
63
+ // Dangling backslash — wait for the rest of the escape.
64
+ return { value: out, end: n, complete: false };
65
+ }
66
+ if (esc === "u") {
67
+ if (i + 6 > n) {
68
+ // Incomplete \uXXXX — hold it back.
69
+ return { value: out, end: n, complete: false };
70
+ }
71
+ const code = parseInt(s.slice(i + 2, i + 6), 16);
72
+ if (Number.isNaN(code)) {
73
+ return { value: out, end: n, complete: false };
74
+ }
75
+ out += String.fromCharCode(code);
76
+ i += 6;
77
+ } else if (esc in ESCAPE_CHARS) {
78
+ out += ESCAPE_CHARS[esc];
79
+ i += 2;
80
+ } else {
81
+ // Not a valid JSON escape; pass the character through leniently.
82
+ out += esc;
83
+ i += 2;
84
+ }
85
+ continue;
86
+ }
87
+
88
+ out += ch;
89
+ i++;
90
+ }
91
+
92
+ return { value: out, end: n, complete: false };
93
+ }
94
+
95
+ interface ValueSkip {
96
+ end: number;
97
+ complete: boolean;
98
+ }
99
+
100
+ /**
101
+ * Skip one JSON value (string, object, array, or primitive) starting at `s[i]`.
102
+ * Tolerant of truncation: an unfinished value reports `complete: false`.
103
+ */
104
+ function skipJsonValue(s: string, i: number): ValueSkip {
105
+ const n = s.length;
106
+ if (i >= n) return { end: n, complete: false };
107
+
108
+ const ch = s[i];
109
+
110
+ if (ch === '"') {
111
+ const r = readJsonString(s, i);
112
+ return { end: r.end, complete: r.complete };
113
+ }
114
+
115
+ if (ch === "{" || ch === "[") {
116
+ let depth = 0;
117
+ let j = i;
118
+ while (j < n) {
119
+ const c = s[j];
120
+ if (c === '"') {
121
+ const r = readJsonString(s, j);
122
+ if (!r.complete) return { end: n, complete: false };
123
+ j = r.end;
124
+ continue;
125
+ }
126
+ if (c === "{" || c === "[") depth++;
127
+ else if (c === "}" || c === "]") {
128
+ depth--;
129
+ if (depth === 0) return { end: j + 1, complete: true };
130
+ }
131
+ j++;
132
+ }
133
+ return { end: n, complete: false };
134
+ }
135
+
136
+ // Primitive (number, true, false, null): runs until a structural delimiter.
137
+ // Hitting end-of-input first means it may still be streaming.
138
+ let j = i;
139
+ while (j < n && !`,}]${WHITESPACE}`.includes(s[j])) j++;
140
+ return j < n ? { end: j, complete: true } : { end: n, complete: false };
141
+ }
142
+
143
+ /**
144
+ * Extract the decoded value of the top-level `message` string field from a
145
+ * (possibly partial) JSON object string, returning the text available so far.
146
+ *
147
+ * Returns `""` while the `message` value has not begun streaming, and passes
148
+ * `accumulated` through unchanged when it is not a JSON object.
149
+ */
150
+ export function extractMessageSoFar(accumulated: string): string {
151
+ const s = accumulated;
152
+ const n = s.length;
153
+
154
+ let i = 0;
155
+ while (i < n && WHITESPACE.includes(s[i])) i++;
156
+
157
+ // Not a JSON object — a plain-text stream; emit verbatim.
158
+ if (i >= n || s[i] !== "{") return accumulated;
159
+ i++; // skip '{'
160
+
161
+ while (i < n) {
162
+ while (i < n && (WHITESPACE.includes(s[i]) || s[i] === ",")) i++;
163
+ if (i >= n) return "";
164
+ if (s[i] === "}") return ""; // object closed without a message
165
+ if (s[i] !== '"') return ""; // key not (fully) arrived
166
+
167
+ const key = readJsonString(s, i);
168
+ if (!key.complete) return ""; // key still streaming
169
+ i = key.end;
170
+
171
+ while (i < n && WHITESPACE.includes(s[i])) i++;
172
+ if (i >= n || s[i] !== ":") return ""; // colon not arrived
173
+ i++;
174
+ while (i < n && WHITESPACE.includes(s[i])) i++;
175
+ if (i >= n) return "";
176
+
177
+ if (key.value === "message") {
178
+ // Found it. Only a string value yields text; null/other → no message yet.
179
+ if (s[i] !== '"') return "";
180
+ return readJsonString(s, i).value;
181
+ }
182
+
183
+ // A field before `message`: skip its value. If it is still streaming we
184
+ // cannot have reached `message` yet.
185
+ const skipped = skipJsonValue(s, i);
186
+ if (!skipped.complete) return "";
187
+ i = skipped.end;
188
+ }
189
+
190
+ return "";
191
+ }
192
+
193
+ /**
194
+ * Stateful wrapper over {@link extractMessageSoFar} for a single stream: feed
195
+ * each chunk's accumulated JSON and get back the clean message-so-far plus the
196
+ * newly revealed delta.
197
+ *
198
+ * Each push re-scans the full accumulated buffer (O(n) per chunk, O(n²) over a
199
+ * stream) — deliberately kept simple and stateless: at LLM response sizes (KBs)
200
+ * the cost is negligible, and it avoids carrying cross-chunk parser/escape state.
201
+ */
202
+ export class StreamingMessageDecoder {
203
+ private previous = "";
204
+
205
+ /**
206
+ * @param accumulated The provider chunk's full accumulated output so far.
207
+ * @returns `message` (clean text so far) and `delta` (the new text since the
208
+ * previous push).
209
+ */
210
+ push(accumulated: string): { message: string; delta: string } {
211
+ const message = extractMessageSoFar(accumulated);
212
+ // Decoding is monotonic (each push extends the prefix); the guard is a
213
+ // belt-and-braces reset for any non-prefix anomaly.
214
+ const delta = message.startsWith(this.previous)
215
+ ? message.slice(this.previous.length)
216
+ : message;
217
+ this.previous = message;
218
+ return { message, delta };
219
+ }
220
+ }