vanspod 0.0.0-stage → 0.1.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/README.md +74 -2
- package/bin/register.cjs +11 -0
- package/bin/vanspod.cjs +2 -0
- package/dist/adapterSettings.js +37 -0
- package/dist/adapters/chat-output.js +69 -0
- package/dist/adapters/chat-stream.js +44 -0
- package/dist/adapters/chat.js +63 -0
- package/dist/adapters/claude-reasoning.js +118 -0
- package/dist/adapters/claude-request.js +190 -0
- package/dist/adapters/claude-response.js +79 -0
- package/dist/adapters/index.js +19 -0
- package/dist/adapters/messages-stream.js +126 -0
- package/dist/adapters/messages.js +21 -0
- package/dist/adapters/responses-stream.js +70 -0
- package/dist/adapters/responses.js +28 -0
- package/dist/adapters/stream.js +99 -0
- package/dist/adapters/transport.js +26 -0
- package/dist/adapters/types.js +35 -0
- package/dist/appearance.js +10 -0
- package/dist/classroomApp.js +1814 -0
- package/dist/classroomUpdate.js +155 -0
- package/dist/claudeWebSearch.js +248 -0
- package/dist/clientDetection.js +35 -0
- package/dist/clientIcons.js +33 -0
- package/dist/clientRouteRuntime.js +149 -0
- package/dist/clientRoutes.js +118 -0
- package/dist/clientSettingsBackup.js +110 -0
- package/dist/clientTraffic.js +26 -0
- package/dist/clipboard.js +32 -0
- package/dist/codexWebSearch.js +80 -0
- package/dist/codexWire/apply-patch-envelope.js +139 -0
- package/dist/codexWire/chat-options.js +51 -0
- package/dist/codexWire/chat-tools.js +80 -0
- package/dist/codexWire/freeform-wrapper-scan.js +326 -0
- package/dist/codexWire/progressive-freeform-input.js +133 -0
- package/dist/codexWire/request.js +73 -0
- package/dist/codexWire/responses-tools.js +88 -0
- package/dist/codexWire/tool-arguments.js +39 -0
- package/dist/codexWire/tool-guidance.js +77 -0
- package/dist/commandRunner.js +100 -0
- package/dist/connectionMascot.js +235 -0
- package/dist/courseCatalog.js +131 -0
- package/dist/desktop.js +25 -0
- package/dist/entry.js +17 -0
- package/dist/environmentIcons.js +11 -0
- package/dist/exitGuard.js +17 -0
- package/dist/folderPicker.js +35 -0
- package/dist/installProgress.js +171 -0
- package/dist/installations.js +72 -0
- package/dist/instance.js +160 -0
- package/dist/launcher.js +155 -0
- package/dist/legacyMigration.js +87 -0
- package/dist/lifecyclePage.js +41 -0
- package/dist/modelProxy.js +105 -0
- package/dist/modelStatus.js +42 -0
- package/dist/page.js +1016 -0
- package/dist/pagePrototype.js +528 -0
- package/dist/platformStorage.js +91 -0
- package/dist/removal.js +41 -0
- package/dist/responsesChat.js +835 -0
- package/dist/routeFiles.js +646 -0
- package/dist/routerSessionModels.js +32 -0
- package/dist/runtimeOptions.js +14 -0
- package/dist/secureStorage.js +63 -0
- package/dist/settingsActions.js +100 -0
- package/dist/start.js +822 -0
- package/dist/storageErrors.js +17 -0
- package/dist/storageLocation.js +58 -0
- package/dist/version.js +4 -0
- package/dist/vsCodeRouteSettings.js +208 -0
- package/dist/webSearch.js +56 -0
- package/dist/windowsSecureStorage.js +163 -0
- package/media/brand-icon.ico +0 -0
- package/media/brand-logo.png +0 -0
- package/media/fadou-superhero-blink.png +0 -0
- package/media/fadou-superhero.png +0 -0
- package/package.json +27 -4
- package/scripts/tray.ps1 +40 -0
- package/scripts/uninstall.ps1 +133 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Bounded classification of a PARTIAL freeform tool-call wrapper.
|
|
3
|
+
//
|
|
4
|
+
// `unwrapFreeformToolInput` decides the completed input with `JSON.parse`, which does not care
|
|
5
|
+
// what order an object's properties arrive in or how their names are spelled. The streaming side
|
|
6
|
+
// has to reach the same answer from a prefix, and it used to do that by comparing the buffer
|
|
7
|
+
// against the literal `{"input":"`. Spellings `JSON.parse` calls identical therefore matched no
|
|
8
|
+
// wrapper at all, streamed as raw JSON, and then completed as the unwrapped body:
|
|
9
|
+
// `{"metadata":1,"input":"cmd"}` and `{"\u0069nput":"cmd"}` both previewed the whole object and
|
|
10
|
+
// completed as `cmd` (#5151). #5047 and #5129 closed the compact and whitespace spellings of the
|
|
11
|
+
// same disagreement; reordering and escaping are the two the literal matcher could never see,
|
|
12
|
+
// because RFC 8259 objects are unordered and their names are strings with escapes.
|
|
13
|
+
//
|
|
14
|
+
// So the prefix is scanned as JSON instead of matched as text. The scan answers one question —
|
|
15
|
+
// which wrapper, if any, the completed text will unwrap to — and it answers it three ways:
|
|
16
|
+
//
|
|
17
|
+
// `input` the canonical key is present with a string value, whatever preceded it and however
|
|
18
|
+
// its name was spelled. Its value can be decoded progressively, because
|
|
19
|
+
// `unwrapFreeformToolInput` gives an own `input` precedence over every other property.
|
|
20
|
+
// `raw` no wrapper can apply, because the text is not an object or because it is one that
|
|
21
|
+
// `JSON.parse` will reject. Completion returns the buffer, so streaming it agrees.
|
|
22
|
+
// `hold` undecided. A key that has not arrived yet can still change the answer, so nothing
|
|
23
|
+
// is published until the object closes — where the scan itself already holds the
|
|
24
|
+
// member table completion's own rule consults, so no parse of the buffer is needed.
|
|
25
|
+
//
|
|
26
|
+
// The bound matters as much as the classification. A scan that walks the whole buffer on every
|
|
27
|
+
// delta is quadratic in the argument size, so classification gives up after
|
|
28
|
+
// `MAX_FREEFORM_WRAPPER_SCAN_CHARS` and holds. Giving up costs preview, never agreement: a held
|
|
29
|
+
// buffer is still resolved by the authoritative parse at completion.
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.MAX_FREEFORM_WRAPPER_SCAN_CHARS = exports.JSON_ESCAPES = exports.JSON_WHITESPACE = void 0;
|
|
32
|
+
exports.scanFreeformWrapper = scanFreeformWrapper;
|
|
33
|
+
/** The insignificant whitespace `JSON.parse` accepts between tokens. */
|
|
34
|
+
exports.JSON_WHITESPACE = new Set([" ", "\t", "\n", "\r"]);
|
|
35
|
+
/** The two-character escapes JSON defines, and nothing else. */
|
|
36
|
+
exports.JSON_ESCAPES = new Map([
|
|
37
|
+
['"', '"'], ["\\", "\\"], ["/", "/"],
|
|
38
|
+
["b", "\b"], ["f", "\f"], ["n", "\n"], ["r", "\r"], ["t", "\t"],
|
|
39
|
+
]);
|
|
40
|
+
/**
|
|
41
|
+
* How far a classification scan walks before it gives up and holds.
|
|
42
|
+
*
|
|
43
|
+
* A wrapper's own structure — its property names and the small scalar siblings a model puts
|
|
44
|
+
* beside the body — lives at the front of the object. A buffer still undecided after this much
|
|
45
|
+
* scanning is one whose preceding values are large, and holding it is what the routed
|
|
46
|
+
* restoration path already does for every unrecognized object.
|
|
47
|
+
*/
|
|
48
|
+
exports.MAX_FREEFORM_WRAPPER_SCAN_CHARS = 4096;
|
|
49
|
+
const CANONICAL_KEY = "input";
|
|
50
|
+
/** The buffer ends inside a token: what follows can still change the answer. */
|
|
51
|
+
const HOLD = -1;
|
|
52
|
+
/** `JSON.parse` will reject this text no matter what is appended to it. */
|
|
53
|
+
const NEVER = -2;
|
|
54
|
+
/** Index of the first non-whitespace character at or after `from`, or HOLD past the end. */
|
|
55
|
+
function skipWhitespace(text, from) {
|
|
56
|
+
let i = from;
|
|
57
|
+
while (i < text.length && exports.JSON_WHITESPACE.has(text[i]))
|
|
58
|
+
i++;
|
|
59
|
+
return i < text.length ? i : HOLD;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Index just past the complete JSON string opening at `from`, or HOLD / NEVER.
|
|
63
|
+
*
|
|
64
|
+
* This reports a BOUNDARY where `decodeJsonStringPrefix` reports a decoded PREFIX: one has to
|
|
65
|
+
* fail on a truncated string and the other has to return what it decoded so far. They answer
|
|
66
|
+
* different questions about the same bytes and share one escape table so they cannot disagree
|
|
67
|
+
* about which escapes exist.
|
|
68
|
+
*/
|
|
69
|
+
function scanString(text, from) {
|
|
70
|
+
let i = from + 1;
|
|
71
|
+
while (i < text.length) {
|
|
72
|
+
const c = text[i];
|
|
73
|
+
if (c === '"')
|
|
74
|
+
return i + 1;
|
|
75
|
+
if (c === "\\") {
|
|
76
|
+
const n = text[i + 1];
|
|
77
|
+
if (n === undefined)
|
|
78
|
+
return HOLD;
|
|
79
|
+
if (n === "u") {
|
|
80
|
+
const hex = text.slice(i + 2, i + 6);
|
|
81
|
+
if (hex.length < 4)
|
|
82
|
+
return HOLD;
|
|
83
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex))
|
|
84
|
+
return NEVER;
|
|
85
|
+
i += 6;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!exports.JSON_ESCAPES.has(n))
|
|
89
|
+
return NEVER;
|
|
90
|
+
i += 2;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// A literal control character is not legal inside a JSON string.
|
|
94
|
+
if (c.charCodeAt(0) <= 0x1f)
|
|
95
|
+
return NEVER;
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
return HOLD;
|
|
99
|
+
}
|
|
100
|
+
const JSON_NUMBER_CHARS = /[-+0-9.eE]/;
|
|
101
|
+
const JSON_NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
|
|
102
|
+
/** Index just past one complete scalar at `from`, or HOLD / NEVER. */
|
|
103
|
+
function scanScalar(text, from) {
|
|
104
|
+
const c = text[from];
|
|
105
|
+
if (c === '"')
|
|
106
|
+
return scanString(text, from);
|
|
107
|
+
if (c === "t" || c === "f" || c === "n") {
|
|
108
|
+
const literal = c === "t" ? "true" : c === "f" ? "false" : "null";
|
|
109
|
+
if (text.startsWith(literal, from))
|
|
110
|
+
return from + literal.length;
|
|
111
|
+
return literal.startsWith(text.slice(from)) ? HOLD : NEVER;
|
|
112
|
+
}
|
|
113
|
+
let end = from;
|
|
114
|
+
while (end < text.length && JSON_NUMBER_CHARS.test(text[end]))
|
|
115
|
+
end++;
|
|
116
|
+
if (end === from)
|
|
117
|
+
return NEVER;
|
|
118
|
+
// A number running to the end of the buffer can still grow another digit or exponent.
|
|
119
|
+
if (end >= text.length)
|
|
120
|
+
return HOLD;
|
|
121
|
+
return JSON_NUMBER.test(text.slice(from, end)) ? end : NEVER;
|
|
122
|
+
}
|
|
123
|
+
/** Index where the value of the member whose name opens at `from` begins, or HOLD / NEVER. */
|
|
124
|
+
function scanMemberKey(text, from) {
|
|
125
|
+
if (text[from] !== '"')
|
|
126
|
+
return NEVER;
|
|
127
|
+
const nameEnd = scanString(text, from);
|
|
128
|
+
if (nameEnd < 0)
|
|
129
|
+
return nameEnd;
|
|
130
|
+
const colon = skipWhitespace(text, nameEnd);
|
|
131
|
+
if (colon === HOLD)
|
|
132
|
+
return HOLD;
|
|
133
|
+
if (text[colon] !== ":")
|
|
134
|
+
return NEVER;
|
|
135
|
+
return skipWhitespace(text, colon + 1);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Index just past one complete JSON value at `from`, or HOLD / NEVER.
|
|
139
|
+
*
|
|
140
|
+
* Containers are walked with an explicit stack rather than recursion: the value being skipped
|
|
141
|
+
* is provider-controlled, and a deeply nested one must not be able to exhaust the call stack.
|
|
142
|
+
*/
|
|
143
|
+
function scanValue(text, from) {
|
|
144
|
+
const closers = [];
|
|
145
|
+
let i = from;
|
|
146
|
+
for (;;) {
|
|
147
|
+
const c = text[i];
|
|
148
|
+
if (c === "{" || c === "[") {
|
|
149
|
+
const closer = c === "{" ? "}" : "]";
|
|
150
|
+
closers.push(closer);
|
|
151
|
+
const first = skipWhitespace(text, i + 1);
|
|
152
|
+
if (first === HOLD)
|
|
153
|
+
return HOLD;
|
|
154
|
+
if (text[first] === closer) {
|
|
155
|
+
closers.pop();
|
|
156
|
+
i = first + 1;
|
|
157
|
+
}
|
|
158
|
+
else if (closer === "}") {
|
|
159
|
+
const value = scanMemberKey(text, first);
|
|
160
|
+
if (value < 0)
|
|
161
|
+
return value;
|
|
162
|
+
i = value;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
i = first;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const end = scanScalar(text, i);
|
|
172
|
+
if (end < 0)
|
|
173
|
+
return end;
|
|
174
|
+
i = end;
|
|
175
|
+
}
|
|
176
|
+
// One value is complete: close whatever it finished and find the next value, if any.
|
|
177
|
+
for (;;) {
|
|
178
|
+
if (closers.length === 0)
|
|
179
|
+
return i;
|
|
180
|
+
const at = skipWhitespace(text, i);
|
|
181
|
+
if (at === HOLD)
|
|
182
|
+
return HOLD;
|
|
183
|
+
const closer = closers[closers.length - 1];
|
|
184
|
+
if (text[at] === closer) {
|
|
185
|
+
closers.pop();
|
|
186
|
+
i = at + 1;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (text[at] !== ",")
|
|
190
|
+
return NEVER;
|
|
191
|
+
const next = skipWhitespace(text, at + 1);
|
|
192
|
+
if (next === HOLD)
|
|
193
|
+
return HOLD;
|
|
194
|
+
if (closer === "}") {
|
|
195
|
+
const value = scanMemberKey(text, next);
|
|
196
|
+
if (value < 0)
|
|
197
|
+
return value;
|
|
198
|
+
i = value;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
i = next;
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Which wrapper the completed text will unwrap to, as far as this prefix can say.
|
|
209
|
+
*
|
|
210
|
+
* `fallbackKeys` is the tool's alternate-field vocabulary — `freeformFallbackKeys`, the same
|
|
211
|
+
* list `unwrapFreeformToolInput` filters against. No open prefix can decide a fallback key,
|
|
212
|
+
* because it unwraps only as the SINGLE string field and a second one can still arrive; so the
|
|
213
|
+
* scan keeps a table of the members it has already walked and consults it once, at the close.
|
|
214
|
+
*/
|
|
215
|
+
function scanFreeformWrapper(text, fallbackKeys) {
|
|
216
|
+
// One clamp rather than a budget threaded through every helper. Every helper already holds
|
|
217
|
+
// when it runs off the end of what it can see, so a buffer whose classification needs more
|
|
218
|
+
// than this holds for exactly the right reason, and no scan can cost more than this many
|
|
219
|
+
// characters however large the arguments grow. Indices into the clamp are indices into the
|
|
220
|
+
// full text, because the clamp is a prefix of it.
|
|
221
|
+
const wholeText = text.length <= exports.MAX_FREEFORM_WRAPPER_SCAN_CHARS;
|
|
222
|
+
const bounded = wholeText ? text : text.slice(0, exports.MAX_FREEFORM_WRAPPER_SCAN_CHARS);
|
|
223
|
+
const hold = () => ({ kind: "hold" });
|
|
224
|
+
const open = skipWhitespace(bounded, 0);
|
|
225
|
+
if (open === HOLD)
|
|
226
|
+
return hold();
|
|
227
|
+
// Not an object, so no wrapper rule reaches it: arrays, scalars and ordinary bodies all
|
|
228
|
+
// complete as themselves.
|
|
229
|
+
if (bounded[open] !== "{")
|
|
230
|
+
return { kind: "raw" };
|
|
231
|
+
// Every top-level member is recorded as the scan passes it, last occurrence winning — the
|
|
232
|
+
// keep-last rule `JSON.parse` applies to duplicate keys. When the object closes, this table
|
|
233
|
+
// answers the only question completion asks of it — which members carried strings — so the
|
|
234
|
+
// closed object classifies directly instead of reparsing a buffer the scan just walked.
|
|
235
|
+
const members = new Map();
|
|
236
|
+
let i = open + 1;
|
|
237
|
+
for (;;) {
|
|
238
|
+
const at = skipWhitespace(bounded, i);
|
|
239
|
+
if (at === HOLD)
|
|
240
|
+
return hold();
|
|
241
|
+
if (bounded[at] === "}")
|
|
242
|
+
return afterTopLevelClose(bounded, at + 1, members, fallbackKeys, wholeText);
|
|
243
|
+
if (bounded[at] !== '"')
|
|
244
|
+
return { kind: "raw" };
|
|
245
|
+
const nameEnd = scanString(bounded, at);
|
|
246
|
+
if (nameEnd === HOLD)
|
|
247
|
+
return hold();
|
|
248
|
+
if (nameEnd === NEVER)
|
|
249
|
+
return { kind: "raw" };
|
|
250
|
+
let name;
|
|
251
|
+
try {
|
|
252
|
+
// The authoritative decoder for the name, so `{"\u0069nput":...}` resolves to the same
|
|
253
|
+
// key completion sees. Decoding it by hand here is the second implementation that would
|
|
254
|
+
// drift from `JSON.parse`, which is how this defect existed in the first place.
|
|
255
|
+
name = JSON.parse(bounded.slice(at, nameEnd));
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return { kind: "raw" };
|
|
259
|
+
}
|
|
260
|
+
const colon = skipWhitespace(bounded, nameEnd);
|
|
261
|
+
if (colon === HOLD)
|
|
262
|
+
return hold();
|
|
263
|
+
if (bounded[colon] !== ":")
|
|
264
|
+
return { kind: "raw" };
|
|
265
|
+
const valueAt = skipWhitespace(bounded, colon + 1);
|
|
266
|
+
if (valueAt === HOLD)
|
|
267
|
+
return hold();
|
|
268
|
+
if (name === CANONICAL_KEY) {
|
|
269
|
+
// An own `input` wins over everything else in the object, so this is decidable now. A
|
|
270
|
+
// non-string value is decidable too, in the other direction: completion hands back the
|
|
271
|
+
// argument text unchanged rather than unwrapping a value it cannot use.
|
|
272
|
+
return bounded[valueAt] === '"'
|
|
273
|
+
? { kind: "input", valueStart: valueAt + 1 }
|
|
274
|
+
: { kind: "raw" };
|
|
275
|
+
}
|
|
276
|
+
members.set(name, { stringValue: bounded[valueAt] === '"', valueStart: valueAt + 1 });
|
|
277
|
+
const valueEnd = scanValue(bounded, valueAt);
|
|
278
|
+
if (valueEnd === HOLD)
|
|
279
|
+
return hold();
|
|
280
|
+
if (valueEnd === NEVER)
|
|
281
|
+
return { kind: "raw" };
|
|
282
|
+
const next = skipWhitespace(bounded, valueEnd);
|
|
283
|
+
if (next === HOLD)
|
|
284
|
+
return hold();
|
|
285
|
+
if (bounded[next] === ",") {
|
|
286
|
+
// A comma promises another member, so the next non-whitespace character must open a
|
|
287
|
+
// member name. `{"code":"cmd",}` is not a completed object — `JSON.parse` rejects the
|
|
288
|
+
// trailing comma — and treating its `}` as the close would unwrap a preview completion
|
|
289
|
+
// hands back unchanged. Nested members already reject this through `scanMemberKey`;
|
|
290
|
+
// the top level has to ask the same question itself.
|
|
291
|
+
const member = skipWhitespace(bounded, next + 1);
|
|
292
|
+
if (member === HOLD)
|
|
293
|
+
return hold();
|
|
294
|
+
if (bounded[member] !== '"')
|
|
295
|
+
return { kind: "raw" };
|
|
296
|
+
i = member;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (bounded[next] === "}")
|
|
300
|
+
return afterTopLevelClose(bounded, next + 1, members, fallbackKeys, wholeText);
|
|
301
|
+
return { kind: "raw" };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* The object closed without a canonical key. Only whitespace may follow one that parses, so
|
|
306
|
+
* anything else makes the text raw. Whitespace to the end IS legal, but the member table
|
|
307
|
+
* already says which way completion goes on it: exactly one string-valued fallback field is
|
|
308
|
+
* the wrapper's value, and any other member shape streams the raw text — byte-exact through
|
|
309
|
+
* whatever trailing whitespace arrived, where a reparsed growing buffer was quadratic work on
|
|
310
|
+
* every whitespace delta and a held one swallowed the whitespace bytes entirely.
|
|
311
|
+
*/
|
|
312
|
+
function afterTopLevelClose(text, from, members, fallbackKeys, wholeText) {
|
|
313
|
+
const trailing = skipWhitespace(text, from);
|
|
314
|
+
if (trailing !== HOLD)
|
|
315
|
+
return { kind: "raw" };
|
|
316
|
+
// A clamped prefix ends inside this whitespace, so what follows is unseen: hold rather than
|
|
317
|
+
// guess at a tail that could still invalidate the close. When the whole text fit, the object
|
|
318
|
+
// plus its trailing whitespace is complete — and its members decide it without a parse.
|
|
319
|
+
if (!wholeText)
|
|
320
|
+
return { kind: "hold" };
|
|
321
|
+
const candidates = fallbackKeys.filter(key => members.get(key)?.stringValue === true);
|
|
322
|
+
if (candidates.length === 1) {
|
|
323
|
+
return { kind: "input", valueStart: members.get(candidates[0]).valueStart };
|
|
324
|
+
}
|
|
325
|
+
return { kind: "raw" };
|
|
326
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.progressiveFreeformInput = progressiveFreeformInput;
|
|
4
|
+
const apply_patch_envelope_1 = require("./apply-patch-envelope");
|
|
5
|
+
const freeform_wrapper_scan_1 = require("./freeform-wrapper-scan");
|
|
6
|
+
/**
|
|
7
|
+
* Whether a body could still grow into one complete outer Markdown fence.
|
|
8
|
+
*
|
|
9
|
+
* `stripMarkdownCodeFence` removes such a fence at completion for exactly the two tools that
|
|
10
|
+
* own the grammar, so a fenced body's streamed bytes and its completed input disagree unless
|
|
11
|
+
* the stream holds. A buffer that does not open with a fence can never acquire one, so
|
|
12
|
+
* ordinary bodies are unaffected; a buffer that does keeps its preview suppressed for the
|
|
13
|
+
* whole call, because a closing fence can still be followed by more text that withdraws it.
|
|
14
|
+
*/
|
|
15
|
+
function mayBecomeFencedBody(text, toolName) {
|
|
16
|
+
if (toolName !== "exec" && toolName !== "apply_patch")
|
|
17
|
+
return false;
|
|
18
|
+
const head = text.trimStart();
|
|
19
|
+
if (head === "")
|
|
20
|
+
return true;
|
|
21
|
+
return head.startsWith("```") || "```".startsWith(head);
|
|
22
|
+
}
|
|
23
|
+
const LOW_SURROGATE_ESCAPE = /^\\u[dD][c-fC-F][0-9a-fA-F]{2}$/;
|
|
24
|
+
/**
|
|
25
|
+
* The decoded prefix of a JSON string body, stopping at the first byte it cannot resolve.
|
|
26
|
+
*
|
|
27
|
+
* Every stop is a hold rather than a guess, because `JSON.parse` decides the completed value
|
|
28
|
+
* and anything invented here would be retracted. Three of them are not obvious:
|
|
29
|
+
*
|
|
30
|
+
* An escape JSON does not define makes the whole wrapper unparseable no matter what arrives
|
|
31
|
+
* next, so completion falls back to the raw text. Returning `null` for that case stops the
|
|
32
|
+
* preview rather than continuing to decode a value the completed item will never carry.
|
|
33
|
+
* `\\b` and `\\f` are defined, and were previously decoded to the letters b and f.
|
|
34
|
+
*
|
|
35
|
+
* A lone high surrogate is not a character. Emitting one alone puts an unpaired code unit in a
|
|
36
|
+
* delta the client has to decode by itself, so the pair is emitted together or not at all.
|
|
37
|
+
*/
|
|
38
|
+
function decodeJsonStringPrefix(body) {
|
|
39
|
+
let out = "";
|
|
40
|
+
for (let i = 0; i < body.length; i++) {
|
|
41
|
+
const c = body[i];
|
|
42
|
+
if (c === '"')
|
|
43
|
+
break; // unescaped closing quote: value complete
|
|
44
|
+
if (c !== "\\") {
|
|
45
|
+
// A literal control character is not legal inside a JSON string, so `JSON.parse` will
|
|
46
|
+
// reject this wrapper and completion will return the raw text. Emitting the decoded value
|
|
47
|
+
// first is the disagreement this decoder exists to prevent, so stop instead.
|
|
48
|
+
if (c.charCodeAt(0) <= 0x1f)
|
|
49
|
+
return null;
|
|
50
|
+
out += c;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const n = body[i + 1];
|
|
54
|
+
if (n === undefined)
|
|
55
|
+
break; // escape split across chunks: wait for more
|
|
56
|
+
if (n === "u") {
|
|
57
|
+
const hex = body.slice(i + 2, i + 6);
|
|
58
|
+
if (hex.length < 4)
|
|
59
|
+
break; // split across chunks: wait for more
|
|
60
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex))
|
|
61
|
+
return null; // never parses
|
|
62
|
+
const code = parseInt(hex, 16);
|
|
63
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
64
|
+
const low = body.slice(i + 6, i + 12);
|
|
65
|
+
if (!LOW_SURROGATE_ESCAPE.test(low))
|
|
66
|
+
break;
|
|
67
|
+
out += String.fromCharCode(code, parseInt(low.slice(2), 16));
|
|
68
|
+
i += 11;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
out += String.fromCharCode(code);
|
|
72
|
+
i += 5;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const escaped = freeform_wrapper_scan_1.JSON_ESCAPES.get(n);
|
|
76
|
+
if (escaped === undefined)
|
|
77
|
+
return null; // never parses
|
|
78
|
+
out += escaped;
|
|
79
|
+
i += 1;
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The value to stream so far, or `null` to HOLD because nothing can be decided yet.
|
|
85
|
+
*
|
|
86
|
+
* `input` is decidable from its prefix: `unwrapFreeformToolInput` returns it whenever the
|
|
87
|
+
* key is present, whatever else the object carries, so its value can be unescaped
|
|
88
|
+
* progressively and never retracted.
|
|
89
|
+
*
|
|
90
|
+
* One case escapes that claim and is accepted rather than fixed: a duplicate `input` key.
|
|
91
|
+
* `JSON.parse` keeps the last one, so `{"input":"a","input":"b"}` streams a and completes with
|
|
92
|
+
* b. Closing it means holding every canonical wrapper until its object parses, which is the
|
|
93
|
+
* progressive streaming this path exists to provide. The completed item stays authoritative,
|
|
94
|
+
* and no model emits a duplicate key in practice.
|
|
95
|
+
*
|
|
96
|
+
* A wrapper that turns invalid AFTER streaming has committed has the same shape and the same
|
|
97
|
+
* answer. `{"input":"a` followed by `\\qb"}` has already published a when the undefined escape
|
|
98
|
+
* arrives, and completion returns the raw text because nothing parses. The preview stops
|
|
99
|
+
* there: no rewind, and no decoded text the completed item does not contain. Bounding the
|
|
100
|
+
* damage is what is available without giving up progressive streaming, and the args are
|
|
101
|
+
* unusable in that case whichever representation wins.
|
|
102
|
+
*
|
|
103
|
+
* A fallback key decides only at the object's close. It unwraps as the SINGLE string field,
|
|
104
|
+
* and a second key can still arrive while the object is open — so a value emitted early would
|
|
105
|
+
* have to be taken back. The scan therefore keeps a member table and consults it at the
|
|
106
|
+
* close, publishing the wrapped value (or the raw text) without ever reparsing the buffer.
|
|
107
|
+
* The routed passthrough in `responses-custom-tool-repair.ts` already holds any object prefix
|
|
108
|
+
* for the same reason (#5047).
|
|
109
|
+
*
|
|
110
|
+
* An object that has not reached a canonical key YET is in exactly that position, and used to
|
|
111
|
+
* be treated as raw because it did not match the literal `{"input":"`. It holds now: `input`
|
|
112
|
+
* can still arrive after other properties, or wearing an escaped spelling, and completion would
|
|
113
|
+
* then unwrap a body whose wrapper syntax had already been published as deltas (#5151). The
|
|
114
|
+
* cost of holding is preview on an object body that turns out not to be a wrapper; the cost of
|
|
115
|
+
* not holding was publishing bytes the completed item removes.
|
|
116
|
+
*/
|
|
117
|
+
function progressiveFreeformInput(args, toolName) {
|
|
118
|
+
const scan = (0, freeform_wrapper_scan_1.scanFreeformWrapper)(args, (0, apply_patch_envelope_1.freeformFallbackKeys)(toolName));
|
|
119
|
+
if (scan.kind === "input") {
|
|
120
|
+
const decoded = decodeJsonStringPrefix(args.slice(scan.valueStart));
|
|
121
|
+
if (decoded === null)
|
|
122
|
+
return null;
|
|
123
|
+
return mayBecomeFencedBody(decoded, toolName) ? null : decoded;
|
|
124
|
+
}
|
|
125
|
+
if (scan.kind === "raw")
|
|
126
|
+
return mayBecomeFencedBody(args, toolName) ? null : args;
|
|
127
|
+
// Undecided: an incomplete object, or a close whose tail is clamped out of view. Both stay
|
|
128
|
+
// held — the first is provably incomplete, and re-walking a budget-exhausted buffer on every
|
|
129
|
+
// delta is work for a preview that arrives in the same instant as the authoritative
|
|
130
|
+
// completion behind it. A closed object the scan could see whole never reaches here: its
|
|
131
|
+
// member table already resolved it to `input` or `raw`.
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeCodexRequest = normalizeCodexRequest;
|
|
4
|
+
exports.codexResponsesUpstreamBody = codexResponsesUpstreamBody;
|
|
5
|
+
const tool_guidance_1 = require("./tool-guidance");
|
|
6
|
+
const types_1 = require("../adapters/types");
|
|
7
|
+
/** Move Codex's inline tool catalog into the standard Responses tools field. */
|
|
8
|
+
function normalizeCodexRequest(body) {
|
|
9
|
+
if (!Array.isArray(body.input))
|
|
10
|
+
return body;
|
|
11
|
+
const tools = body.tools === undefined ? [] : [...(0, types_1.list)(body.tools, "tools")];
|
|
12
|
+
const input = [];
|
|
13
|
+
let changed = false;
|
|
14
|
+
for (const item of body.input) {
|
|
15
|
+
if (!item || typeof item !== "object" || item.type !== "additional_tools") {
|
|
16
|
+
input.push(item);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const group = (0, types_1.object)(item, "additional_tools");
|
|
20
|
+
(0, types_1.allowedFields)(group, ["type", "id", "role", "tools"], "additional_tools");
|
|
21
|
+
tools.push(...(0, types_1.list)(group.tools, "additional_tools.tools"));
|
|
22
|
+
changed = true;
|
|
23
|
+
}
|
|
24
|
+
return changed ? { ...body, input, tools } : body;
|
|
25
|
+
}
|
|
26
|
+
/** Router's Responses input accepts function history, so lower freeform tools
|
|
27
|
+
* consistently across declarations, selectors, calls and paired results. */
|
|
28
|
+
function codexResponsesUpstreamBody(body) {
|
|
29
|
+
// This Codex private account selector is not part of the Router's Responses
|
|
30
|
+
// contract. Other unknown fields remain intact for upstream extensions.
|
|
31
|
+
const { access_programs: _accessPrograms, ...publicBody } = body;
|
|
32
|
+
const guidance = (0, tool_guidance_1.codexToolGuidance)(body);
|
|
33
|
+
const tool = (value, namespace) => {
|
|
34
|
+
const record = (0, types_1.object)(value, "tool");
|
|
35
|
+
if (record.type === "namespace")
|
|
36
|
+
return { ...record, tools: (0, types_1.list)(record.tools, "namespace tools").map(value => tool(value, String(record.name))) };
|
|
37
|
+
if (record.type !== "custom")
|
|
38
|
+
return record;
|
|
39
|
+
const { format, ...rest } = record;
|
|
40
|
+
const codeMode = guidance.codeMode && record.name === "exec" && (namespace === undefined || namespace === "functions");
|
|
41
|
+
const description = `Raw input for ${String(record.name)}.${format === undefined ? "" : ` The input must follow this format definition: ${JSON.stringify(format)}`}`;
|
|
42
|
+
return { ...rest, type: "function", parameters: { type: "object", properties: { input: {
|
|
43
|
+
type: "string", description: codeMode ? (0, tool_guidance_1.codeModeInputDescription)(description) : description,
|
|
44
|
+
} }, required: ["input"], additionalProperties: false } };
|
|
45
|
+
};
|
|
46
|
+
const choice = (value) => {
|
|
47
|
+
if (!value || typeof value !== "object")
|
|
48
|
+
return value;
|
|
49
|
+
const record = (0, types_1.object)(value, "tool_choice");
|
|
50
|
+
if (record.type === "custom")
|
|
51
|
+
return { ...record, type: "function" };
|
|
52
|
+
if (record.type === "allowed_tools")
|
|
53
|
+
return { ...record, tools: (0, types_1.list)(record.tools, "allowed tools").map(choice) };
|
|
54
|
+
return record;
|
|
55
|
+
};
|
|
56
|
+
return { ...publicBody,
|
|
57
|
+
...(guidance.codeMode ? { instructions: (0, tool_guidance_1.appendToolInstructions)(typeof body.instructions === "string" ? body.instructions : "", [tool_guidance_1.CODE_MODE_RESULT_ECHO_SENTENCE, tool_guidance_1.CODE_MODE_HOST_CONTRACT_SENTENCE]) } : {}),
|
|
58
|
+
...(body.tools === undefined ? {} : { tools: (0, types_1.list)(body.tools, "tools").map(value => tool(value)) }),
|
|
59
|
+
...(body.tool_choice === undefined ? {} : { tool_choice: choice(body.tool_choice) }),
|
|
60
|
+
...(Array.isArray(body.input) ? { input: body.input.map(value => {
|
|
61
|
+
const item = (0, types_1.object)(value, "input item");
|
|
62
|
+
if (item.type === "custom_tool_call") {
|
|
63
|
+
const { input, id: _id, ...rest } = item;
|
|
64
|
+
return { ...rest, type: "function_call", arguments: JSON.stringify({ input: (0, types_1.string)(input, "custom tool input") }) };
|
|
65
|
+
}
|
|
66
|
+
if (item.type === "custom_tool_call_output") {
|
|
67
|
+
const { id: _id, ...rest } = item;
|
|
68
|
+
return { ...rest, type: "function_call_output" };
|
|
69
|
+
}
|
|
70
|
+
return item;
|
|
71
|
+
}) } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CodexResponsesTools = void 0;
|
|
4
|
+
const apply_patch_envelope_1 = require("./apply-patch-envelope");
|
|
5
|
+
const types_1 = require("../adapters/types");
|
|
6
|
+
/** Restore only tools explicitly declared as freeform in the client's request. */
|
|
7
|
+
class CodexResponsesTools {
|
|
8
|
+
tools = [];
|
|
9
|
+
calls = new Map();
|
|
10
|
+
constructor(body) {
|
|
11
|
+
const collect = (tools, namespace) => {
|
|
12
|
+
for (const raw of tools) {
|
|
13
|
+
const tool = (0, types_1.object)(raw, "tool");
|
|
14
|
+
if (tool.type === "namespace")
|
|
15
|
+
collect((0, types_1.list)(tool.tools, "namespace tools"), (0, types_1.string)(tool.name, "namespace"));
|
|
16
|
+
else if (tool.type === "function" || tool.type === "custom") {
|
|
17
|
+
const name = (0, types_1.string)(tool.name, "tool name");
|
|
18
|
+
if (this.tools.some(tool => tool.name === name && tool.namespace === namespace))
|
|
19
|
+
throw new types_1.AdapterError("Responses 工具宣告的 namespace/name 識別重複。");
|
|
20
|
+
this.tools.push({ name, namespace, custom: tool.type === "custom" });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
if (body.tools !== undefined)
|
|
25
|
+
collect((0, types_1.list)(body.tools, "tools"));
|
|
26
|
+
}
|
|
27
|
+
get hasCustomTools() { return this.tools.some(tool => tool.custom); }
|
|
28
|
+
identity(item) {
|
|
29
|
+
if ((item.type !== "function_call" && item.type !== "custom_tool_call") || typeof item.name !== "string")
|
|
30
|
+
return;
|
|
31
|
+
let matches = this.tools.filter(tool => tool.name === item.name && tool.namespace === item.namespace);
|
|
32
|
+
if (!matches.length && item.namespace === undefined)
|
|
33
|
+
matches = this.tools.filter(tool => tool.name === item.name || (tool.namespace && `${tool.namespace}.${tool.name}` === item.name));
|
|
34
|
+
if (matches.length > 1)
|
|
35
|
+
throw new types_1.AdapterError("上游工具識別不明確;缺少可唯一判定的 namespace。", 502);
|
|
36
|
+
return matches.length === 1 && matches[0].custom ? matches[0] : undefined;
|
|
37
|
+
}
|
|
38
|
+
convertItem(value) {
|
|
39
|
+
const item = (0, types_1.object)(value, "Responses output item");
|
|
40
|
+
const tool = this.identity(item);
|
|
41
|
+
if (!tool)
|
|
42
|
+
return item;
|
|
43
|
+
const { arguments: args, input, ...rest } = item;
|
|
44
|
+
return { ...rest, type: "custom_tool_call", name: tool.name,
|
|
45
|
+
...(tool.namespace ? { namespace: tool.namespace } : {}), input: (0, apply_patch_envelope_1.repairFreeformToolInput)((0, types_1.string)(item.type === "custom_tool_call" ? input : args, "tool input"), tool.name, tool.namespace) };
|
|
46
|
+
}
|
|
47
|
+
response(body) {
|
|
48
|
+
return Array.isArray(body.output) ? { ...body, output: body.output.map(item => this.convertItem(item)) } : body;
|
|
49
|
+
}
|
|
50
|
+
events(data) {
|
|
51
|
+
if (data.type === "response.output_item.added") {
|
|
52
|
+
const item = (0, types_1.object)(data.item, "output item");
|
|
53
|
+
if (this.identity(item)) {
|
|
54
|
+
this.calls.set((0, types_1.string)(item.id, "item id"), { item, args: typeof (item.arguments ?? item.input) === "string" ? String(item.arguments ?? item.input) : "", finished: false });
|
|
55
|
+
return [{ ...data, item: this.convertItem({ ...item, arguments: "", input: "" }) }];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const state = typeof data.item_id === "string" ? this.calls.get(data.item_id) : undefined;
|
|
59
|
+
if (state && (data.type === "response.function_call_arguments.delta" || data.type === "response.custom_tool_call_input.delta")) {
|
|
60
|
+
state.args += (0, types_1.string)(data.delta, "arguments delta");
|
|
61
|
+
return []; // Hold the JSON wrapper until the complete freeform input is known.
|
|
62
|
+
}
|
|
63
|
+
if (state && (data.type === "response.function_call_arguments.done" || data.type === "response.custom_tool_call_input.done")) {
|
|
64
|
+
const input = this.convertItem({ ...state.item, arguments: data.arguments ?? state.args, input: data.input ?? state.args }).input;
|
|
65
|
+
state.finished = true;
|
|
66
|
+
const { arguments: _args, input: _input, ...rest } = data;
|
|
67
|
+
return [{ ...rest, type: "response.custom_tool_call_input.delta", delta: input },
|
|
68
|
+
{ ...rest, type: "response.custom_tool_call_input.done", input }];
|
|
69
|
+
}
|
|
70
|
+
if (data.type === "response.output_item.done") {
|
|
71
|
+
const item = (0, types_1.object)(data.item, "output item"), converted = this.convertItem(item);
|
|
72
|
+
if (converted !== item) {
|
|
73
|
+
const state = typeof item.id === "string" ? this.calls.get(item.id) : undefined;
|
|
74
|
+
const prefix = state?.finished ? [] : [
|
|
75
|
+
{ type: "response.custom_tool_call_input.delta", item_id: item.id, output_index: data.output_index, delta: converted.input },
|
|
76
|
+
{ type: "response.custom_tool_call_input.done", item_id: item.id, output_index: data.output_index, input: converted.input },
|
|
77
|
+
];
|
|
78
|
+
if (state)
|
|
79
|
+
state.finished = true;
|
|
80
|
+
return [...prefix, { ...data, item: converted }];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if ((data.type === "response.completed" || data.type === "response.incomplete") && data.response)
|
|
84
|
+
return [{ ...data, response: this.response((0, types_1.object)(data.response, "response")) }];
|
|
85
|
+
return [data];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.CodexResponsesTools = CodexResponsesTools;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toolCallArgumentsUsable = toolCallArgumentsUsable;
|
|
4
|
+
exports.toolCallArgumentsCouldBeJson = toolCallArgumentsCouldBeJson;
|
|
5
|
+
/**
|
|
6
|
+
* Whether assembled function-call arguments are usable JSON.
|
|
7
|
+
* An empty buffer is valid (no-arg tools send no deltas). Non-empty must parse.
|
|
8
|
+
*/
|
|
9
|
+
function toolCallArgumentsUsable(args) {
|
|
10
|
+
if (args.length === 0)
|
|
11
|
+
return true;
|
|
12
|
+
const trimmed = args.trim();
|
|
13
|
+
if (!trimmed)
|
|
14
|
+
return false;
|
|
15
|
+
try {
|
|
16
|
+
JSON.parse(args);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Whether an in-progress function-call argument buffer could still become valid JSON.
|
|
25
|
+
* The first non-whitespace byte must be one that can begin a JSON value.
|
|
26
|
+
*/
|
|
27
|
+
function toolCallArgumentsCouldBeJson(args) {
|
|
28
|
+
const first = args.trimStart().charAt(0);
|
|
29
|
+
if (first === "")
|
|
30
|
+
return true;
|
|
31
|
+
return first === "{"
|
|
32
|
+
|| first === "["
|
|
33
|
+
|| first === "\""
|
|
34
|
+
|| first === "-"
|
|
35
|
+
|| (first >= "0" && first <= "9")
|
|
36
|
+
|| first === "t"
|
|
37
|
+
|| first === "f"
|
|
38
|
+
|| first === "n";
|
|
39
|
+
}
|