@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.
- package/dist/cjs/core/ResponseModal.d.ts +14 -7
- package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
- package/dist/cjs/core/ResponseModal.js +52 -20
- package/dist/cjs/core/ResponseModal.js.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.js +20 -8
- package/dist/cjs/providers/ProviderAdapter.js.map +1 -1
- package/dist/cjs/utils/index.d.ts +1 -1
- package/dist/cjs/utils/index.d.ts.map +1 -1
- package/dist/cjs/utils/index.js +2 -1
- package/dist/cjs/utils/index.js.map +1 -1
- package/dist/cjs/utils/json.d.ts +22 -0
- package/dist/cjs/utils/json.d.ts.map +1 -1
- package/dist/cjs/utils/json.js +156 -1
- package/dist/cjs/utils/json.js.map +1 -1
- package/dist/core/ResponseModal.d.ts +14 -7
- package/dist/core/ResponseModal.d.ts.map +1 -1
- package/dist/core/ResponseModal.js +53 -21
- package/dist/core/ResponseModal.js.map +1 -1
- package/dist/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/providers/ProviderAdapter.js +20 -8
- package/dist/providers/ProviderAdapter.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/json.d.ts +22 -0
- package/dist/utils/json.d.ts.map +1 -1
- package/dist/utils/json.js +154 -1
- package/dist/utils/json.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ResponseModal.ts +59 -28
- package/src/providers/ProviderAdapter.ts +21 -7
- package/src/utils/index.ts +1 -1
- 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
|
|
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
|
+
}
|