@falai/agent 3.2.0 → 3.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 (35) hide show
  1. package/dist/cjs/core/ResponseModal.d.ts +14 -7
  2. package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
  3. package/dist/cjs/core/ResponseModal.js +52 -20
  4. package/dist/cjs/core/ResponseModal.js.map +1 -1
  5. package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -1
  6. package/dist/cjs/providers/ProviderAdapter.js +20 -8
  7. package/dist/cjs/providers/ProviderAdapter.js.map +1 -1
  8. package/dist/cjs/utils/index.d.ts +1 -1
  9. package/dist/cjs/utils/index.d.ts.map +1 -1
  10. package/dist/cjs/utils/index.js +2 -1
  11. package/dist/cjs/utils/index.js.map +1 -1
  12. package/dist/cjs/utils/json.d.ts +22 -0
  13. package/dist/cjs/utils/json.d.ts.map +1 -1
  14. package/dist/cjs/utils/json.js +156 -1
  15. package/dist/cjs/utils/json.js.map +1 -1
  16. package/dist/core/ResponseModal.d.ts +14 -7
  17. package/dist/core/ResponseModal.d.ts.map +1 -1
  18. package/dist/core/ResponseModal.js +53 -21
  19. package/dist/core/ResponseModal.js.map +1 -1
  20. package/dist/providers/ProviderAdapter.d.ts.map +1 -1
  21. package/dist/providers/ProviderAdapter.js +20 -8
  22. package/dist/providers/ProviderAdapter.js.map +1 -1
  23. package/dist/utils/index.d.ts +1 -1
  24. package/dist/utils/index.d.ts.map +1 -1
  25. package/dist/utils/index.js +1 -1
  26. package/dist/utils/index.js.map +1 -1
  27. package/dist/utils/json.d.ts +22 -0
  28. package/dist/utils/json.d.ts.map +1 -1
  29. package/dist/utils/json.js +154 -1
  30. package/dist/utils/json.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/core/ResponseModal.ts +59 -28
  33. package/src/providers/ProviderAdapter.ts +21 -7
  34. package/src/utils/index.ts +1 -1
  35. package/src/utils/json.ts +153 -1
package/src/utils/json.ts CHANGED
@@ -2,12 +2,103 @@
2
2
  * JSON parsing utilities
3
3
  */
4
4
 
5
+ /** JSON's own short escapes for the control characters a model actually types. */
6
+ const CONTROL_ESCAPES: Record<string, string> = {
7
+ "\n": "\\n",
8
+ "\r": "\\r",
9
+ "\t": "\\t",
10
+ "\b": "\\b",
11
+ "\f": "\\f",
12
+ };
13
+
14
+ /**
15
+ * Escape the raw control characters a model left unescaped inside a string
16
+ * literal.
17
+ *
18
+ * JSON forbids a literal newline between quotes. A model whose decoder is
19
+ * pinned to the schema cannot break that rule, but one merely ASKED for the
20
+ * envelope in its prompt — which is how a schema rides on any call that also
21
+ * carries tools — breaks it constantly, because it pretty-prints the reply it
22
+ * would have sent:
23
+ *
24
+ * {
25
+ * "message": "Boa escolha!
26
+ * À vista: R$ 3.149"
27
+ * }
28
+ *
29
+ * `JSON.parse` calls that an unterminated string and gives up, and the whole
30
+ * envelope travels on as the user-visible reply. Re-escaping the control
31
+ * characters makes it parse into exactly what the model meant.
32
+ *
33
+ * A string ends at the next unescaped quote, so a stray quote inside the
34
+ * message shifts the boundary and the result fails to parse. That is the
35
+ * intended outcome: a wrong guess must never become a reply.
36
+ */
37
+ function escapeControlCharsInStrings(text: string): string {
38
+ let out = "";
39
+ let inString = false;
40
+ let escaped = false;
41
+
42
+ for (const ch of text) {
43
+ if (!inString) {
44
+ if (ch === '"') inString = true;
45
+ out += ch;
46
+ continue;
47
+ }
48
+ if (escaped) {
49
+ escaped = false;
50
+ out += ch;
51
+ } else if (ch === "\\") {
52
+ escaped = true;
53
+ out += ch;
54
+ } else if (ch === '"') {
55
+ inString = false;
56
+ out += ch;
57
+ } else if (ch < " ") {
58
+ out += CONTROL_ESCAPES[ch] ?? `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`;
59
+ } else {
60
+ out += ch;
61
+ }
62
+ }
63
+
64
+ return out;
65
+ }
66
+
67
+ /**
68
+ * Parse strictly, then once more with {@link escapeControlCharsInStrings}.
69
+ * Throws when neither reading is valid JSON.
70
+ */
71
+ function parseLenient(text: string): unknown {
72
+ try {
73
+ return JSON.parse(text);
74
+ } catch (strictError) {
75
+ try {
76
+ return JSON.parse(escapeControlCharsInStrings(text));
77
+ } catch {
78
+ // The repair is a second reading of the same text, not a different
79
+ // dialect — so the first violation is the one worth reporting.
80
+ throw strictError;
81
+ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Whether text is shaped like a protocol envelope rather than a reply to a
87
+ * person — an opening brace or a code fence. Text that fails to parse AND
88
+ * looks like this is never user-worthy: it is a broken envelope, and showing
89
+ * it is the leak this module exists to prevent.
90
+ */
91
+ export function isJSONShaped(text: string): boolean {
92
+ return /^\s*(```|\{)/.test(text);
93
+ }
94
+
5
95
  /**
6
96
  * Clean and parse JSON response that might be wrapped in markdown code blocks
7
97
  * Handles cases like:
8
98
  * - ```json\n{...}\n```
9
99
  * - ```\n{...}\n```
10
100
  * - Plain JSON: {...}
101
+ * - An object whose string values carry unescaped newlines
11
102
  */
12
103
  export function parseJSONResponse(text: string): unknown {
13
104
  if (!text || typeof text !== 'string') {
@@ -28,7 +119,7 @@ export function parseJSONResponse(text: string): unknown {
28
119
 
29
120
  // Try to parse the cleaned JSON
30
121
  try {
31
- return JSON.parse(cleaned);
122
+ return parseLenient(cleaned);
32
123
  } catch (error) {
33
124
  throw new Error(`Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}\nContent: ${cleaned.substring(0, 200)}...`);
34
125
  }
@@ -44,3 +135,64 @@ export function tryParseJSONResponse(text: string): unknown {
44
135
  return undefined;
45
136
  }
46
137
  }
138
+
139
+ /**
140
+ * Find a complete JSON object embedded in surrounding text.
141
+ *
142
+ * {@link parseJSONResponse} requires the WHOLE string to be the object. A model
143
+ * told to answer in JSON sometimes answers twice instead — the conversational
144
+ * text for the user, and then the protocol envelope repeating it — and such a
145
+ * turn parses as nothing, so the envelope travels on as user-visible content.
146
+ * This scans for the first balanced `{...}` that parses as an object, ignoring
147
+ * braces that sit inside string literals.
148
+ *
149
+ * Returns `undefined` when no complete object is present (prose that merely
150
+ * contains a brace, or an envelope truncated before its closing brace).
151
+ */
152
+ export function extractEmbeddedJSONObject(text: string): Record<string, unknown> | undefined {
153
+ if (!text) return undefined;
154
+
155
+ for (let start = text.indexOf("{"); start !== -1; start = text.indexOf("{", start + 1)) {
156
+ const end = findObjectEnd(text, start);
157
+ if (end === -1) continue;
158
+
159
+ try {
160
+ const parsed = parseLenient(text.slice(start, end + 1));
161
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
162
+ return parsed as Record<string, unknown>;
163
+ }
164
+ } catch {
165
+ // Not an object after all — keep scanning for a later candidate.
166
+ }
167
+ }
168
+
169
+ return undefined;
170
+ }
171
+
172
+ /**
173
+ * Index of the `}` closing the object that opens at `start`, or -1 when the
174
+ * text ends first. Quotes and their escapes are tracked so a brace inside a
175
+ * string value never opens or closes a level.
176
+ */
177
+ function findObjectEnd(text: string, start: number): number {
178
+ let depth = 0;
179
+ let inString = false;
180
+ let escaped = false;
181
+
182
+ for (let i = start; i < text.length; i++) {
183
+ const ch = text[i];
184
+
185
+ if (inString) {
186
+ if (escaped) escaped = false;
187
+ else if (ch === "\\") escaped = true;
188
+ else if (ch === '"') inString = false;
189
+ continue;
190
+ }
191
+
192
+ if (ch === '"') inString = true;
193
+ else if (ch === "{") depth++;
194
+ else if (ch === "}" && --depth === 0) return i;
195
+ }
196
+
197
+ return -1;
198
+ }