@bolt-foundry/gambit 0.8.0 → 0.8.3
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/CHANGELOG.md +82 -2
- package/README.md +31 -9
- package/esm/gambit/simulator-ui/dist/bundle.js +4744 -4360
- package/esm/gambit/simulator-ui/dist/bundle.js.map +4 -4
- package/esm/gambit/simulator-ui/dist/favicon.ico +0 -0
- package/esm/mod.d.ts +7 -3
- package/esm/mod.d.ts.map +1 -1
- package/esm/mod.js +5 -1
- package/esm/src/cli_utils.d.ts +3 -2
- package/esm/src/cli_utils.d.ts.map +1 -1
- package/esm/src/cli_utils.js +43 -27
- package/esm/src/openai_compat.d.ts +63 -0
- package/esm/src/openai_compat.d.ts.map +1 -0
- package/esm/src/openai_compat.js +277 -0
- package/esm/src/providers/google.d.ts +16 -0
- package/esm/src/providers/google.d.ts.map +1 -0
- package/esm/src/providers/google.js +352 -0
- package/esm/src/providers/ollama.d.ts +17 -0
- package/esm/src/providers/ollama.d.ts.map +1 -0
- package/esm/src/providers/ollama.js +509 -0
- package/esm/src/providers/openrouter.d.ts +14 -1
- package/esm/src/providers/openrouter.d.ts.map +1 -1
- package/esm/src/providers/openrouter.js +460 -463
- package/esm/src/server.d.ts +4 -0
- package/esm/src/server.d.ts.map +1 -1
- package/esm/src/server.js +623 -164
- package/esm/src/trace.d.ts.map +1 -1
- package/esm/src/trace.js +3 -6
- package/package.json +2 -2
- package/script/gambit/simulator-ui/dist/bundle.js +4744 -4360
- package/script/gambit/simulator-ui/dist/bundle.js.map +4 -4
- package/script/gambit/simulator-ui/dist/favicon.ico +0 -0
- package/script/mod.d.ts +7 -3
- package/script/mod.d.ts.map +1 -1
- package/script/mod.js +9 -3
- package/script/src/cli_utils.d.ts +3 -2
- package/script/src/cli_utils.d.ts.map +1 -1
- package/script/src/cli_utils.js +42 -26
- package/script/src/openai_compat.d.ts +63 -0
- package/script/src/openai_compat.d.ts.map +1 -0
- package/script/src/openai_compat.js +281 -0
- package/script/src/providers/google.d.ts +16 -0
- package/script/src/providers/google.d.ts.map +1 -0
- package/script/src/providers/google.js +359 -0
- package/script/src/providers/ollama.d.ts +17 -0
- package/script/src/providers/ollama.d.ts.map +1 -0
- package/script/src/providers/ollama.js +551 -0
- package/script/src/providers/openrouter.d.ts +14 -1
- package/script/src/providers/openrouter.d.ts.map +1 -1
- package/script/src/providers/openrouter.js +461 -463
- package/script/src/server.d.ts +4 -0
- package/script/src/server.d.ts.map +1 -1
- package/script/src/server.js +623 -164
- package/script/src/trace.d.ts.map +1 -1
- package/script/src/trace.js +3 -6
- package/esm/src/compat/openai.d.ts +0 -2
- package/esm/src/compat/openai.d.ts.map +0 -1
- package/esm/src/compat/openai.js +0 -1
- package/script/src/compat/openai.d.ts +0 -2
- package/script/src/compat/openai.d.ts.map +0 -1
- package/script/src/compat/openai.js +0 -5
package/script/src/server.js
CHANGED
|
@@ -42,6 +42,7 @@ const trace_js_1 = require("./trace.js");
|
|
|
42
42
|
const cli_utils_js_1 = require("./cli_utils.js");
|
|
43
43
|
const gambit_core_2 = require("@bolt-foundry/gambit-core");
|
|
44
44
|
const durable_streams_js_1 = require("./durable_streams.js");
|
|
45
|
+
const GAMBIT_TOOL_RESPOND = "gambit_respond";
|
|
45
46
|
const logger = console;
|
|
46
47
|
const moduleLocation = (() => {
|
|
47
48
|
const directoryFromUrl = (url) => {
|
|
@@ -83,11 +84,12 @@ const simulatorBundleSourceMapUrl = (() => {
|
|
|
83
84
|
let cachedRemoteBundle = null;
|
|
84
85
|
let cachedRemoteBundleSourceMap = null;
|
|
85
86
|
const simulatorBundlePath = path.resolve(moduleDir, "..", "simulator-ui", "dist", "bundle.js");
|
|
86
|
-
const simulatorUiEntryPath = path.resolve(moduleDir, "..", "simulator-ui", "src", "main.tsx");
|
|
87
87
|
const simulatorBundleSourceMapPath = path.resolve(moduleDir, "..", "simulator-ui", "dist", "bundle.js.map");
|
|
88
|
+
const simulatorFaviconDistPath = path.resolve(moduleDir, "..", "simulator-ui", "dist", "favicon.ico");
|
|
89
|
+
const simulatorFaviconSrcPath = path.resolve(moduleDir, "..", "simulator-ui", "src", "favicon.ico");
|
|
88
90
|
const SIMULATOR_STREAM_ID = "gambit-simulator";
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
+
const GRADE_STREAM_ID = "gambit-grade";
|
|
92
|
+
const TEST_STREAM_ID = "gambit-test";
|
|
91
93
|
let availableTestDecks = [];
|
|
92
94
|
const testDeckByPath = new Map();
|
|
93
95
|
const testDeckById = new Map();
|
|
@@ -98,87 +100,6 @@ function randomId(prefix) {
|
|
|
98
100
|
const suffix = crypto.randomUUID().replace(/-/g, "").slice(0, 24);
|
|
99
101
|
return `${prefix}-${suffix}`;
|
|
100
102
|
}
|
|
101
|
-
async function parseOpenResponseRequest(req) {
|
|
102
|
-
try {
|
|
103
|
-
return await req.json();
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
function formatOpenResponseSseEvent(event) {
|
|
110
|
-
return `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`;
|
|
111
|
-
}
|
|
112
|
-
function formatOpenResponseDoneEvent() {
|
|
113
|
-
return "data: [DONE]\n\n";
|
|
114
|
-
}
|
|
115
|
-
function createOpenResponseStream(req, provider, payload) {
|
|
116
|
-
const encoder = new TextEncoder();
|
|
117
|
-
const stream = new ReadableStream({
|
|
118
|
-
start(controller) {
|
|
119
|
-
let closed = false;
|
|
120
|
-
let completed = false;
|
|
121
|
-
const close = () => {
|
|
122
|
-
if (closed)
|
|
123
|
-
return;
|
|
124
|
-
closed = true;
|
|
125
|
-
controller.close();
|
|
126
|
-
};
|
|
127
|
-
const send = (chunk) => {
|
|
128
|
-
if (closed)
|
|
129
|
-
return;
|
|
130
|
-
controller.enqueue(encoder.encode(chunk));
|
|
131
|
-
};
|
|
132
|
-
const sendDone = () => {
|
|
133
|
-
if (closed)
|
|
134
|
-
return;
|
|
135
|
-
send(formatOpenResponseDoneEvent());
|
|
136
|
-
close();
|
|
137
|
-
};
|
|
138
|
-
const sendEvent = (event) => {
|
|
139
|
-
if (closed)
|
|
140
|
-
return;
|
|
141
|
-
send(formatOpenResponseSseEvent(event));
|
|
142
|
-
if (event.type === "response.completed") {
|
|
143
|
-
completed = true;
|
|
144
|
-
sendDone();
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
req.signal.addEventListener("abort", () => {
|
|
148
|
-
close();
|
|
149
|
-
});
|
|
150
|
-
(async () => {
|
|
151
|
-
try {
|
|
152
|
-
const response = await provider.responses({
|
|
153
|
-
...payload,
|
|
154
|
-
stream: true,
|
|
155
|
-
onStreamEvent: sendEvent,
|
|
156
|
-
});
|
|
157
|
-
if (!completed) {
|
|
158
|
-
sendEvent({ type: "response.completed", response });
|
|
159
|
-
sendDone();
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
catch (err) {
|
|
163
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
164
|
-
sendEvent({
|
|
165
|
-
type: "error",
|
|
166
|
-
error: { code: "server_error", message },
|
|
167
|
-
});
|
|
168
|
-
sendDone();
|
|
169
|
-
}
|
|
170
|
-
})();
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
return new Response(stream, {
|
|
174
|
-
status: 200,
|
|
175
|
-
headers: {
|
|
176
|
-
"Content-Type": "text/event-stream",
|
|
177
|
-
"Cache-Control": "no-cache",
|
|
178
|
-
"Connection": "keep-alive",
|
|
179
|
-
},
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
103
|
function resolveDefaultValue(raw) {
|
|
183
104
|
if (typeof raw === "function") {
|
|
184
105
|
try {
|
|
@@ -424,11 +345,170 @@ function deriveInitialFromSchema(schema) {
|
|
|
424
345
|
return undefined;
|
|
425
346
|
}
|
|
426
347
|
}
|
|
348
|
+
function getPathValue(value, path) {
|
|
349
|
+
let current = value;
|
|
350
|
+
for (const segment of path) {
|
|
351
|
+
if (!current || typeof current !== "object" ||
|
|
352
|
+
!(segment in current)) {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
current = current[segment];
|
|
356
|
+
}
|
|
357
|
+
return current;
|
|
358
|
+
}
|
|
359
|
+
function setPathValue(value, path, nextValue) {
|
|
360
|
+
if (path.length === 0)
|
|
361
|
+
return nextValue;
|
|
362
|
+
const root = value && typeof value === "object"
|
|
363
|
+
? cloneValue(value)
|
|
364
|
+
: {};
|
|
365
|
+
let cursor = root;
|
|
366
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
367
|
+
const segment = path[i];
|
|
368
|
+
const existing = cursor[segment];
|
|
369
|
+
const next = existing && typeof existing === "object"
|
|
370
|
+
? cloneValue(existing)
|
|
371
|
+
: {};
|
|
372
|
+
cursor[segment] = next;
|
|
373
|
+
cursor = next;
|
|
374
|
+
}
|
|
375
|
+
const last = path[path.length - 1];
|
|
376
|
+
if (nextValue === undefined) {
|
|
377
|
+
delete cursor[last];
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
cursor[last] = nextValue;
|
|
381
|
+
}
|
|
382
|
+
return root;
|
|
383
|
+
}
|
|
384
|
+
function findMissingRequiredFields(schema, value, prefix = []) {
|
|
385
|
+
if (!schema)
|
|
386
|
+
return [];
|
|
387
|
+
if (schema.optional)
|
|
388
|
+
return [];
|
|
389
|
+
if (schema.kind === "object" && schema.fields) {
|
|
390
|
+
if (value !== undefined && value !== null &&
|
|
391
|
+
(typeof value !== "object" || Array.isArray(value))) {
|
|
392
|
+
return [];
|
|
393
|
+
}
|
|
394
|
+
const asObj = value && typeof value === "object"
|
|
395
|
+
? value
|
|
396
|
+
: undefined;
|
|
397
|
+
const missing = [];
|
|
398
|
+
for (const [key, child] of Object.entries(schema.fields)) {
|
|
399
|
+
missing.push(...findMissingRequiredFields(child, asObj ? asObj[key] : undefined, [...prefix, key]));
|
|
400
|
+
}
|
|
401
|
+
return missing;
|
|
402
|
+
}
|
|
403
|
+
const key = prefix.join(".") || "(root)";
|
|
404
|
+
if (value === undefined || value === null) {
|
|
405
|
+
return schema.defaultValue !== undefined ? [] : [key];
|
|
406
|
+
}
|
|
407
|
+
if (schema.kind === "string" || schema.kind === "enum") {
|
|
408
|
+
return typeof value === "string" && value.trim() === "" ? [key] : [];
|
|
409
|
+
}
|
|
410
|
+
if (schema.kind === "array") {
|
|
411
|
+
return Array.isArray(value) && value.length === 0 ? [key] : [];
|
|
412
|
+
}
|
|
413
|
+
if (schema.kind === "number") {
|
|
414
|
+
return typeof value === "number" && Number.isFinite(value) ? [] : [key];
|
|
415
|
+
}
|
|
416
|
+
if (schema.kind === "boolean") {
|
|
417
|
+
return typeof value === "boolean" ? [] : [key];
|
|
418
|
+
}
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
function getSchemaAtPath(schema, path) {
|
|
422
|
+
let current = schema;
|
|
423
|
+
for (const segment of path) {
|
|
424
|
+
if (!current || current.kind !== "object" || !current.fields)
|
|
425
|
+
return;
|
|
426
|
+
current = current.fields[segment];
|
|
427
|
+
}
|
|
428
|
+
return current;
|
|
429
|
+
}
|
|
430
|
+
function buildInitFillPrompt(args) {
|
|
431
|
+
const schemaHints = args.missing.map((path) => {
|
|
432
|
+
const segments = path === "(root)" ? [] : path.split(".");
|
|
433
|
+
const leaf = getSchemaAtPath(args.schema, segments);
|
|
434
|
+
return {
|
|
435
|
+
path,
|
|
436
|
+
kind: leaf?.kind,
|
|
437
|
+
description: leaf?.description,
|
|
438
|
+
enumValues: leaf?.enumValues,
|
|
439
|
+
};
|
|
440
|
+
});
|
|
441
|
+
const payload = {
|
|
442
|
+
type: "gambit_test_bot_init_fill",
|
|
443
|
+
missing: args.missing,
|
|
444
|
+
current: args.current ?? null,
|
|
445
|
+
schemaHints,
|
|
446
|
+
};
|
|
447
|
+
return [
|
|
448
|
+
"You are filling missing required init fields for a Gambit Test Bot run.",
|
|
449
|
+
"Return ONLY valid JSON that includes values for the missing fields.",
|
|
450
|
+
"Do not include any fields that are not listed as missing.",
|
|
451
|
+
"If the only missing path is '(root)', return the full init JSON value.",
|
|
452
|
+
"",
|
|
453
|
+
JSON.stringify(payload, null, 2),
|
|
454
|
+
].join("\n");
|
|
455
|
+
}
|
|
456
|
+
function unwrapRespondPayload(output) {
|
|
457
|
+
if (!output || typeof output !== "object")
|
|
458
|
+
return output;
|
|
459
|
+
const record = output;
|
|
460
|
+
if ("payload" in record) {
|
|
461
|
+
return record.payload;
|
|
462
|
+
}
|
|
463
|
+
return output;
|
|
464
|
+
}
|
|
465
|
+
function parseInitFillOutput(output) {
|
|
466
|
+
if (output === null || output === undefined) {
|
|
467
|
+
return { error: "Persona returned empty init fill output." };
|
|
468
|
+
}
|
|
469
|
+
if (typeof output === "object") {
|
|
470
|
+
return { data: unwrapRespondPayload(output) };
|
|
471
|
+
}
|
|
472
|
+
if (typeof output === "string") {
|
|
473
|
+
const text = output.trim();
|
|
474
|
+
if (!text)
|
|
475
|
+
return { error: "Persona returned empty init fill output." };
|
|
476
|
+
try {
|
|
477
|
+
const parsed = JSON.parse(text);
|
|
478
|
+
return { data: unwrapRespondPayload(parsed) };
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
return {
|
|
482
|
+
error: `Persona returned invalid JSON for init fill: ${err instanceof Error ? err.message : String(err)}`,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return { error: "Persona returned unsupported init fill output." };
|
|
487
|
+
}
|
|
488
|
+
function validateInitInput(schema, value) {
|
|
489
|
+
if (!schema)
|
|
490
|
+
return value;
|
|
491
|
+
if (typeof schema.safeParse !== "function") {
|
|
492
|
+
throw new Error("Init schema missing safeParse");
|
|
493
|
+
}
|
|
494
|
+
const result = schema.safeParse(value);
|
|
495
|
+
if (!result.success) {
|
|
496
|
+
const issue = result.error.issues?.[0];
|
|
497
|
+
const message = issue
|
|
498
|
+
? `${issue.path.join(".") || "(root)"}: ${issue.message}`
|
|
499
|
+
: result.error.message;
|
|
500
|
+
throw new Error(`Schema validation failed: ${message}`);
|
|
501
|
+
}
|
|
502
|
+
return result.data;
|
|
503
|
+
}
|
|
427
504
|
/**
|
|
428
505
|
* Start the WebSocket simulator server used by the Gambit debug UI.
|
|
429
506
|
*/
|
|
430
507
|
function startWebSocketSimulator(opts) {
|
|
431
508
|
const port = opts.port ?? 8000;
|
|
509
|
+
const initialContext = opts.initialContext;
|
|
510
|
+
const hasInitialContext = opts.contextProvided ??
|
|
511
|
+
(initialContext !== undefined);
|
|
432
512
|
const consoleTracer = opts.verbose ? (0, trace_js_1.makeConsoleTracer)() : undefined;
|
|
433
513
|
let resolvedDeckPath = resolveDeckPath(opts.deckPath);
|
|
434
514
|
const sessionsRoot = (() => {
|
|
@@ -459,9 +539,10 @@ function startWebSocketSimulator(opts) {
|
|
|
459
539
|
};
|
|
460
540
|
const testBotRuns = new Map();
|
|
461
541
|
const broadcastTestBot = (payload) => {
|
|
462
|
-
(0, durable_streams_js_1.appendDurableStreamEvent)(
|
|
542
|
+
(0, durable_streams_js_1.appendDurableStreamEvent)(TEST_STREAM_ID, payload);
|
|
463
543
|
};
|
|
464
544
|
let deckSlug = deckSlugFromPath(resolvedDeckPath);
|
|
545
|
+
let deckLabel = undefined;
|
|
465
546
|
const enrichStateWithSession = (state) => {
|
|
466
547
|
const meta = { ...(state.meta ?? {}) };
|
|
467
548
|
const now = new Date();
|
|
@@ -646,12 +727,6 @@ function startWebSocketSimulator(opts) {
|
|
|
646
727
|
return "";
|
|
647
728
|
if (typeof value === "string")
|
|
648
729
|
return value;
|
|
649
|
-
if (Array.isArray(value)) {
|
|
650
|
-
return value
|
|
651
|
-
.map((part) => typeof part === "string" ? part : part.text ??
|
|
652
|
-
"")
|
|
653
|
-
.join("");
|
|
654
|
-
}
|
|
655
730
|
try {
|
|
656
731
|
return JSON.stringify(value);
|
|
657
732
|
}
|
|
@@ -659,6 +734,59 @@ function startWebSocketSimulator(opts) {
|
|
|
659
734
|
return String(value);
|
|
660
735
|
}
|
|
661
736
|
};
|
|
737
|
+
const safeParseJson = (text) => {
|
|
738
|
+
if (typeof text !== "string" || text.trim().length === 0)
|
|
739
|
+
return undefined;
|
|
740
|
+
try {
|
|
741
|
+
return JSON.parse(text);
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
return undefined;
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
const summarizeRespondCall = (message) => {
|
|
748
|
+
if (!message || message.role !== "tool")
|
|
749
|
+
return null;
|
|
750
|
+
const name = typeof message.name === "string" ? message.name : undefined;
|
|
751
|
+
if (name !== GAMBIT_TOOL_RESPOND)
|
|
752
|
+
return null;
|
|
753
|
+
const parsed = safeParseJson(typeof message.content === "string" ? message.content : "");
|
|
754
|
+
const payload = parsed && typeof parsed === "object"
|
|
755
|
+
? ("payload" in parsed
|
|
756
|
+
? parsed.payload
|
|
757
|
+
: parsed)
|
|
758
|
+
: undefined;
|
|
759
|
+
const status = typeof parsed?.status === "number"
|
|
760
|
+
? parsed.status
|
|
761
|
+
: undefined;
|
|
762
|
+
const code = typeof parsed?.code === "string"
|
|
763
|
+
? parsed.code
|
|
764
|
+
: undefined;
|
|
765
|
+
const respondMessage = typeof parsed?.message === "string"
|
|
766
|
+
? parsed.message
|
|
767
|
+
: undefined;
|
|
768
|
+
const meta = parsed && typeof parsed.meta === "object"
|
|
769
|
+
? parsed.meta
|
|
770
|
+
: undefined;
|
|
771
|
+
const summary = {};
|
|
772
|
+
if (status !== undefined)
|
|
773
|
+
summary.status = status;
|
|
774
|
+
if (code !== undefined)
|
|
775
|
+
summary.code = code;
|
|
776
|
+
if (respondMessage !== undefined)
|
|
777
|
+
summary.message = respondMessage;
|
|
778
|
+
if (meta !== undefined)
|
|
779
|
+
summary.meta = meta;
|
|
780
|
+
summary.payload = payload ?? null;
|
|
781
|
+
return {
|
|
782
|
+
status,
|
|
783
|
+
code,
|
|
784
|
+
message: respondMessage,
|
|
785
|
+
meta,
|
|
786
|
+
payload,
|
|
787
|
+
displayText: JSON.stringify(summary, null, 2),
|
|
788
|
+
};
|
|
789
|
+
};
|
|
662
790
|
const updateTestDeckRegistry = (list) => {
|
|
663
791
|
testDeckByPath.clear();
|
|
664
792
|
testDeckById.clear();
|
|
@@ -711,12 +839,11 @@ function startWebSocketSimulator(opts) {
|
|
|
711
839
|
const fallbackToolInserts = [];
|
|
712
840
|
for (let i = 0; i < rawMessages.length; i++) {
|
|
713
841
|
const msg = rawMessages[i];
|
|
714
|
-
|
|
715
|
-
|
|
842
|
+
const refId = refs[i]?.id;
|
|
843
|
+
if (msg?.role === "assistant" || msg?.role === "user") {
|
|
716
844
|
const content = stringifyContent(msg.content).trim();
|
|
717
845
|
if (!content)
|
|
718
846
|
continue;
|
|
719
|
-
const refId = refs[i]?.id;
|
|
720
847
|
messages.push({
|
|
721
848
|
role: msg.role,
|
|
722
849
|
content,
|
|
@@ -725,7 +852,22 @@ function startWebSocketSimulator(opts) {
|
|
|
725
852
|
});
|
|
726
853
|
continue;
|
|
727
854
|
}
|
|
728
|
-
|
|
855
|
+
const respondSummary = summarizeRespondCall(msg);
|
|
856
|
+
if (respondSummary) {
|
|
857
|
+
messages.push({
|
|
858
|
+
role: "assistant",
|
|
859
|
+
content: respondSummary.displayText,
|
|
860
|
+
messageRefId: refId,
|
|
861
|
+
feedback: refId ? feedbackByRef.get(refId) : undefined,
|
|
862
|
+
respondStatus: respondSummary.status,
|
|
863
|
+
respondCode: respondSummary.code,
|
|
864
|
+
respondMessage: respondSummary.message,
|
|
865
|
+
respondPayload: respondSummary.payload,
|
|
866
|
+
respondMeta: respondSummary.meta,
|
|
867
|
+
});
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
if (msg?.role === "tool") {
|
|
729
871
|
const actionCallId = typeof msg.tool_call_id === "string"
|
|
730
872
|
? msg.tool_call_id
|
|
731
873
|
: undefined;
|
|
@@ -745,6 +887,33 @@ function startWebSocketSimulator(opts) {
|
|
|
745
887
|
: fallbackToolInserts,
|
|
746
888
|
};
|
|
747
889
|
};
|
|
890
|
+
const buildConversationMessages = (state) => {
|
|
891
|
+
const rawMessages = state.messages ?? [];
|
|
892
|
+
const conversation = [];
|
|
893
|
+
for (const msg of rawMessages) {
|
|
894
|
+
if (msg?.role === "assistant" || msg?.role === "user") {
|
|
895
|
+
const content = stringifyContent(msg.content).trim();
|
|
896
|
+
if (!content)
|
|
897
|
+
continue;
|
|
898
|
+
conversation.push({
|
|
899
|
+
role: msg.role,
|
|
900
|
+
content,
|
|
901
|
+
name: msg.name,
|
|
902
|
+
tool_calls: msg.tool_calls,
|
|
903
|
+
});
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
const respondSummary = summarizeRespondCall(msg);
|
|
907
|
+
if (respondSummary) {
|
|
908
|
+
conversation.push({
|
|
909
|
+
role: "assistant",
|
|
910
|
+
content: respondSummary.displayText,
|
|
911
|
+
name: GAMBIT_TOOL_RESPOND,
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return conversation;
|
|
916
|
+
};
|
|
748
917
|
const deriveToolInsertsFromTraces = (state, messageCount) => {
|
|
749
918
|
const traces = Array.isArray(state.traces) ? state.traces : [];
|
|
750
919
|
if (!traces.length)
|
|
@@ -789,6 +958,10 @@ function startWebSocketSimulator(opts) {
|
|
|
789
958
|
: undefined;
|
|
790
959
|
if (sessionId)
|
|
791
960
|
run.sessionId = sessionId;
|
|
961
|
+
const initFill = state.meta
|
|
962
|
+
?.testBotInitFill;
|
|
963
|
+
if (initFill)
|
|
964
|
+
run.initFill = initFill;
|
|
792
965
|
run.traces = Array.isArray(state.traces) ? [...state.traces] : undefined;
|
|
793
966
|
};
|
|
794
967
|
const startTestBotRun = (runOpts = {}) => {
|
|
@@ -826,9 +999,27 @@ function startWebSocketSimulator(opts) {
|
|
|
826
999
|
};
|
|
827
1000
|
testBotRuns.set(runId, entry);
|
|
828
1001
|
const run = entry.run;
|
|
1002
|
+
if (runOpts.initFill)
|
|
1003
|
+
run.initFill = runOpts.initFill;
|
|
829
1004
|
let savedState = undefined;
|
|
830
1005
|
let lastCount = 0;
|
|
831
1006
|
const capturedTraces = [];
|
|
1007
|
+
if (runOpts.initFillTrace) {
|
|
1008
|
+
const actionCallId = randomId("initfill");
|
|
1009
|
+
capturedTraces.push({
|
|
1010
|
+
type: "tool.call",
|
|
1011
|
+
runId,
|
|
1012
|
+
actionCallId,
|
|
1013
|
+
name: "gambit_test_bot_init_fill",
|
|
1014
|
+
args: runOpts.initFillTrace.args,
|
|
1015
|
+
}, {
|
|
1016
|
+
type: "tool.result",
|
|
1017
|
+
runId,
|
|
1018
|
+
actionCallId,
|
|
1019
|
+
name: "gambit_test_bot_init_fill",
|
|
1020
|
+
result: runOpts.initFillTrace.result,
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
832
1023
|
const setSessionId = (state) => {
|
|
833
1024
|
const sessionId = typeof state?.meta?.sessionId === "string"
|
|
834
1025
|
? state.meta.sessionId
|
|
@@ -861,7 +1052,7 @@ function startWebSocketSimulator(opts) {
|
|
|
861
1052
|
const getLastAssistantMessage = (history) => {
|
|
862
1053
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
863
1054
|
const msg = history[i];
|
|
864
|
-
if (msg?.
|
|
1055
|
+
if (msg?.role === "assistant") {
|
|
865
1056
|
return stringifyContent(msg.content);
|
|
866
1057
|
}
|
|
867
1058
|
}
|
|
@@ -884,6 +1075,7 @@ function startWebSocketSimulator(opts) {
|
|
|
884
1075
|
},
|
|
885
1076
|
stream: Boolean(streamOpts?.onStreamText),
|
|
886
1077
|
onStreamText: streamOpts?.onStreamText,
|
|
1078
|
+
responsesMode: opts.responsesMode,
|
|
887
1079
|
});
|
|
888
1080
|
if ((0, gambit_core_1.isGambitEndSignal)(result)) {
|
|
889
1081
|
sessionEnded = true;
|
|
@@ -907,6 +1099,7 @@ function startWebSocketSimulator(opts) {
|
|
|
907
1099
|
state: savedState,
|
|
908
1100
|
allowRootStringInput: true,
|
|
909
1101
|
initialUserMessage: initialUserMessage || undefined,
|
|
1102
|
+
responsesMode: opts.responsesMode,
|
|
910
1103
|
onStateUpdate: (state) => {
|
|
911
1104
|
const nextMeta = {
|
|
912
1105
|
...(savedState?.meta ?? {}),
|
|
@@ -915,6 +1108,7 @@ function startWebSocketSimulator(opts) {
|
|
|
915
1108
|
testBotRunId: runId,
|
|
916
1109
|
testBotConfigPath: botConfigPath,
|
|
917
1110
|
testBotName,
|
|
1111
|
+
...(run.initFill ? { testBotInitFill: run.initFill } : {}),
|
|
918
1112
|
};
|
|
919
1113
|
const enriched = persistSessionState({
|
|
920
1114
|
...state,
|
|
@@ -966,6 +1160,7 @@ function startWebSocketSimulator(opts) {
|
|
|
966
1160
|
state: savedState,
|
|
967
1161
|
allowRootStringInput: true,
|
|
968
1162
|
initialUserMessage: userMessage,
|
|
1163
|
+
responsesMode: opts.responsesMode,
|
|
969
1164
|
onStateUpdate: (state) => {
|
|
970
1165
|
const nextMeta = {
|
|
971
1166
|
...(savedState?.meta ?? {}),
|
|
@@ -974,6 +1169,7 @@ function startWebSocketSimulator(opts) {
|
|
|
974
1169
|
testBotRunId: runId,
|
|
975
1170
|
testBotConfigPath: botConfigPath,
|
|
976
1171
|
testBotName,
|
|
1172
|
+
...(run.initFill ? { testBotInitFill: run.initFill } : {}),
|
|
977
1173
|
};
|
|
978
1174
|
const enriched = persistSessionState({
|
|
979
1175
|
...state,
|
|
@@ -1032,10 +1228,60 @@ function startWebSocketSimulator(opts) {
|
|
|
1032
1228
|
broadcastTestBot({ type: "testBotStatus", run });
|
|
1033
1229
|
return run;
|
|
1034
1230
|
};
|
|
1231
|
+
const persistFailedInitFill = (args) => {
|
|
1232
|
+
const failedRunId = randomId("testbot");
|
|
1233
|
+
const testBotName = path.basename(args.botDeckPath).replace(/\.deck\.(md|ts)$/i, "");
|
|
1234
|
+
const actionCallId = randomId("initfill");
|
|
1235
|
+
const traces = [
|
|
1236
|
+
{
|
|
1237
|
+
type: "tool.call",
|
|
1238
|
+
runId: failedRunId,
|
|
1239
|
+
actionCallId,
|
|
1240
|
+
name: "gambit_test_bot_init_fill",
|
|
1241
|
+
args: { missing: args.initFill?.requested ?? [] },
|
|
1242
|
+
},
|
|
1243
|
+
{
|
|
1244
|
+
type: "tool.result",
|
|
1245
|
+
runId: failedRunId,
|
|
1246
|
+
actionCallId,
|
|
1247
|
+
name: "gambit_test_bot_init_fill",
|
|
1248
|
+
result: {
|
|
1249
|
+
error: args.error,
|
|
1250
|
+
provided: args.initFill?.provided,
|
|
1251
|
+
},
|
|
1252
|
+
},
|
|
1253
|
+
];
|
|
1254
|
+
const failedState = persistSessionState({
|
|
1255
|
+
runId: failedRunId,
|
|
1256
|
+
messages: [],
|
|
1257
|
+
traces,
|
|
1258
|
+
meta: {
|
|
1259
|
+
testBot: true,
|
|
1260
|
+
testBotRunId: failedRunId,
|
|
1261
|
+
testBotConfigPath: args.botDeckPath,
|
|
1262
|
+
testBotName,
|
|
1263
|
+
testBotInitFill: args.initFill,
|
|
1264
|
+
testBotInitFillError: args.error,
|
|
1265
|
+
},
|
|
1266
|
+
});
|
|
1267
|
+
const sessionId = typeof failedState.meta?.sessionId === "string"
|
|
1268
|
+
? failedState.meta.sessionId
|
|
1269
|
+
: undefined;
|
|
1270
|
+
const sessionPath = typeof failedState.meta?.sessionStatePath === "string"
|
|
1271
|
+
? failedState.meta.sessionStatePath
|
|
1272
|
+
: undefined;
|
|
1273
|
+
if (sessionPath) {
|
|
1274
|
+
logger.warn(`[sim] init fill failed; session saved to ${sessionPath}`);
|
|
1275
|
+
}
|
|
1276
|
+
return { sessionId, sessionPath };
|
|
1277
|
+
};
|
|
1035
1278
|
const deckLoadPromise = (0, gambit_core_2.loadDeck)(resolvedDeckPath)
|
|
1036
1279
|
.then((deck) => {
|
|
1037
1280
|
resolvedDeckPath = deck.path;
|
|
1038
1281
|
deckSlug = deckSlugFromPath(resolvedDeckPath);
|
|
1282
|
+
deckLabel = typeof deck.label === "string"
|
|
1283
|
+
? deck.label
|
|
1284
|
+
: toDeckLabel(deck.path);
|
|
1039
1285
|
availableTestDecks = (deck.testDecks ?? []).map((testDeck, index) => {
|
|
1040
1286
|
const label = testDeck.label && typeof testDeck.label === "string"
|
|
1041
1287
|
? testDeck.label
|
|
@@ -1082,8 +1328,14 @@ function startWebSocketSimulator(opts) {
|
|
|
1082
1328
|
return null;
|
|
1083
1329
|
});
|
|
1084
1330
|
const schemaPromise = deckLoadPromise
|
|
1085
|
-
.then((deck) =>
|
|
1086
|
-
|
|
1331
|
+
.then((deck) => {
|
|
1332
|
+
const desc = deck ? describeZodSchema(deck.inputSchema) : {
|
|
1333
|
+
error: "Deck failed to load",
|
|
1334
|
+
};
|
|
1335
|
+
if (hasInitialContext) {
|
|
1336
|
+
return { ...desc, defaults: initialContext };
|
|
1337
|
+
}
|
|
1338
|
+
return desc;
|
|
1087
1339
|
})
|
|
1088
1340
|
.catch((err) => {
|
|
1089
1341
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1093,14 +1345,21 @@ function startWebSocketSimulator(opts) {
|
|
|
1093
1345
|
const wantsSourceMap = Boolean(opts.sourceMap);
|
|
1094
1346
|
const bundlePlatform = opts.bundlePlatform ?? "deno";
|
|
1095
1347
|
const autoBundle = opts.autoBundle ?? true;
|
|
1348
|
+
const forceBundle = opts.forceBundle ?? false;
|
|
1096
1349
|
const needsBundle = !hasReactBundle() ||
|
|
1097
1350
|
(wantsSourceMap && !hasReactBundleSourceMap()) ||
|
|
1098
1351
|
isReactBundleStale();
|
|
1099
|
-
const shouldAutoBundle = autoBundle && moduleLocation.isLocal &&
|
|
1352
|
+
const shouldAutoBundle = autoBundle && moduleLocation.isLocal &&
|
|
1353
|
+
(forceBundle || needsBundle);
|
|
1100
1354
|
if (autoBundle && !moduleLocation.isLocal && opts.verbose) {
|
|
1101
1355
|
logger.log("[sim] auto-bundle disabled for remote package; using packaged bundle.");
|
|
1102
1356
|
}
|
|
1357
|
+
if (autoBundle && moduleLocation.isLocal && !shouldAutoBundle) {
|
|
1358
|
+
logger.log("[sim] auto-bundle enabled; bundle already up to date.");
|
|
1359
|
+
}
|
|
1103
1360
|
if (shouldAutoBundle) {
|
|
1361
|
+
logger.log(`[sim] auto-bundle enabled; rebuilding simulator UI (${forceBundle ? "forced" : "stale"})...`);
|
|
1362
|
+
logger.log(`[sim] bundling simulator UI (${forceBundle ? "forced" : "stale"})...`);
|
|
1104
1363
|
try {
|
|
1105
1364
|
const p = new dntShim.Deno.Command("deno", {
|
|
1106
1365
|
args: [
|
|
@@ -1124,46 +1383,31 @@ function startWebSocketSimulator(opts) {
|
|
|
1124
1383
|
}
|
|
1125
1384
|
const server = dntShim.Deno.serve({ port, signal: opts.signal, onListen: () => { } }, async (req) => {
|
|
1126
1385
|
const url = new URL(req.url);
|
|
1127
|
-
if (url.pathname
|
|
1128
|
-
|
|
1386
|
+
if (url.pathname.startsWith("/api/durable-streams/stream/")) {
|
|
1387
|
+
return (0, durable_streams_js_1.handleDurableStreamRequest)(req);
|
|
1388
|
+
}
|
|
1389
|
+
if (url.pathname === "/favicon.ico") {
|
|
1390
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1129
1391
|
return new Response("Method not allowed", { status: 405 });
|
|
1130
1392
|
}
|
|
1131
|
-
const payload = await parseOpenResponseRequest(req);
|
|
1132
|
-
if (!payload) {
|
|
1133
|
-
return new Response("Invalid JSON payload", { status: 400 });
|
|
1134
|
-
}
|
|
1135
|
-
const model = payload.model ?? opts.model;
|
|
1136
|
-
if (!model) {
|
|
1137
|
-
return new Response("Missing model", { status: 400 });
|
|
1138
|
-
}
|
|
1139
|
-
const requestPayload = {
|
|
1140
|
-
...payload,
|
|
1141
|
-
model,
|
|
1142
|
-
input: payload.input ?? null,
|
|
1143
|
-
};
|
|
1144
|
-
if (payload.stream) {
|
|
1145
|
-
return createOpenResponseStream(req, opts.modelProvider, requestPayload);
|
|
1146
|
-
}
|
|
1147
1393
|
try {
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
});
|
|
1152
|
-
return new Response(JSON.stringify(response), {
|
|
1153
|
-
headers: { "content-type": "application/json" },
|
|
1394
|
+
const data = await dntShim.Deno.readFile(simulatorFaviconDistPath);
|
|
1395
|
+
return new Response(req.method === "HEAD" ? null : data, {
|
|
1396
|
+
headers: { "content-type": "image/x-icon" },
|
|
1154
1397
|
});
|
|
1155
1398
|
}
|
|
1156
|
-
catch
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1399
|
+
catch {
|
|
1400
|
+
try {
|
|
1401
|
+
const data = await dntShim.Deno.readFile(simulatorFaviconSrcPath);
|
|
1402
|
+
return new Response(req.method === "HEAD" ? null : data, {
|
|
1403
|
+
headers: { "content-type": "image/x-icon" },
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
catch {
|
|
1407
|
+
return new Response("Not found", { status: 404 });
|
|
1408
|
+
}
|
|
1162
1409
|
}
|
|
1163
1410
|
}
|
|
1164
|
-
if (url.pathname.startsWith("/api/durable-streams/stream/")) {
|
|
1165
|
-
return (0, durable_streams_js_1.handleDurableStreamRequest)(req);
|
|
1166
|
-
}
|
|
1167
1411
|
if (url.pathname === "/api/calibrate") {
|
|
1168
1412
|
if (req.method !== "GET") {
|
|
1169
1413
|
return new Response("Method not allowed", { status: 405 });
|
|
@@ -1209,11 +1453,10 @@ function startWebSocketSimulator(opts) {
|
|
|
1209
1453
|
delete next.gradingRuns;
|
|
1210
1454
|
return next;
|
|
1211
1455
|
})();
|
|
1456
|
+
const conversationMessages = buildConversationMessages(sessionState);
|
|
1212
1457
|
const sessionPayload = {
|
|
1213
|
-
messages:
|
|
1214
|
-
?
|
|
1215
|
-
.filter((msg) => msg.type === "message")
|
|
1216
|
-
.map((msg) => ({
|
|
1458
|
+
messages: conversationMessages.length > 0
|
|
1459
|
+
? conversationMessages.map((msg) => ({
|
|
1217
1460
|
role: msg.role,
|
|
1218
1461
|
content: msg.content,
|
|
1219
1462
|
name: msg.name,
|
|
@@ -1246,7 +1489,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1246
1489
|
},
|
|
1247
1490
|
});
|
|
1248
1491
|
const sessionMeta = buildSessionMeta(sessionId, nextState);
|
|
1249
|
-
(0, durable_streams_js_1.appendDurableStreamEvent)(
|
|
1492
|
+
(0, durable_streams_js_1.appendDurableStreamEvent)(GRADE_STREAM_ID, {
|
|
1250
1493
|
type: "calibrateSession",
|
|
1251
1494
|
sessionId,
|
|
1252
1495
|
run: nextEntry,
|
|
@@ -1276,6 +1519,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1276
1519
|
allowRootStringInput: false,
|
|
1277
1520
|
initialUserMessage: undefined,
|
|
1278
1521
|
stream: false,
|
|
1522
|
+
responsesMode: opts.responsesMode,
|
|
1279
1523
|
});
|
|
1280
1524
|
}
|
|
1281
1525
|
const messages = sessionPayload.messages ?? [];
|
|
@@ -1315,6 +1559,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1315
1559
|
allowRootStringInput: false,
|
|
1316
1560
|
initialUserMessage: undefined,
|
|
1317
1561
|
stream: false,
|
|
1562
|
+
responsesMode: opts.responsesMode,
|
|
1318
1563
|
});
|
|
1319
1564
|
turns.push({
|
|
1320
1565
|
index: idx,
|
|
@@ -1417,7 +1662,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1417
1662
|
},
|
|
1418
1663
|
});
|
|
1419
1664
|
const sessionMeta = buildSessionMeta(body.sessionId, updated);
|
|
1420
|
-
(0, durable_streams_js_1.appendDurableStreamEvent)(
|
|
1665
|
+
(0, durable_streams_js_1.appendDurableStreamEvent)(GRADE_STREAM_ID, {
|
|
1421
1666
|
type: "calibrateSession",
|
|
1422
1667
|
sessionId: body.sessionId,
|
|
1423
1668
|
session: sessionMeta,
|
|
@@ -1470,7 +1715,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1470
1715
|
},
|
|
1471
1716
|
});
|
|
1472
1717
|
const sessionMeta = buildSessionMeta(body.sessionId, updated);
|
|
1473
|
-
(0, durable_streams_js_1.appendDurableStreamEvent)(
|
|
1718
|
+
(0, durable_streams_js_1.appendDurableStreamEvent)(GRADE_STREAM_ID, {
|
|
1474
1719
|
type: "calibrateSession",
|
|
1475
1720
|
sessionId: body.sessionId,
|
|
1476
1721
|
session: sessionMeta,
|
|
@@ -1557,7 +1802,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1557
1802
|
},
|
|
1558
1803
|
});
|
|
1559
1804
|
const sessionMeta = buildSessionMeta(body.sessionId, nextState);
|
|
1560
|
-
(0, durable_streams_js_1.appendDurableStreamEvent)(
|
|
1805
|
+
(0, durable_streams_js_1.appendDurableStreamEvent)(GRADE_STREAM_ID, {
|
|
1561
1806
|
type: "calibrateSession",
|
|
1562
1807
|
sessionId: body.sessionId,
|
|
1563
1808
|
run: nextRun,
|
|
@@ -1575,7 +1820,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1575
1820
|
}), { status: 400, headers: { "content-type": "application/json" } });
|
|
1576
1821
|
}
|
|
1577
1822
|
}
|
|
1578
|
-
if (url.pathname === "/api/test
|
|
1823
|
+
if (url.pathname === "/api/test") {
|
|
1579
1824
|
if (req.method === "GET") {
|
|
1580
1825
|
await deckLoadPromise.catch(() => null);
|
|
1581
1826
|
const requestedDeck = url.searchParams.get("deckPath");
|
|
@@ -1616,7 +1861,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1616
1861
|
}
|
|
1617
1862
|
return new Response("Method not allowed", { status: 405 });
|
|
1618
1863
|
}
|
|
1619
|
-
if (url.pathname === "/api/test
|
|
1864
|
+
if (url.pathname === "/api/test/run") {
|
|
1620
1865
|
if (req.method !== "POST") {
|
|
1621
1866
|
return new Response("Method not allowed", { status: 405 });
|
|
1622
1867
|
}
|
|
@@ -1625,17 +1870,28 @@ function startWebSocketSimulator(opts) {
|
|
|
1625
1870
|
let botInput = undefined;
|
|
1626
1871
|
let initialUserMessage = undefined;
|
|
1627
1872
|
let botDeckSelection;
|
|
1873
|
+
let inheritBotInput = false;
|
|
1874
|
+
let userProvidedDeckInput = false;
|
|
1875
|
+
let initFillRequestMissing = undefined;
|
|
1628
1876
|
try {
|
|
1629
1877
|
const body = await req.json();
|
|
1630
1878
|
if (typeof body.maxTurns === "number" && Number.isFinite(body.maxTurns)) {
|
|
1631
1879
|
maxTurnsOverride = body.maxTurns;
|
|
1632
1880
|
}
|
|
1633
1881
|
deckInput = body.context ?? body.init;
|
|
1882
|
+
if (body.context !== undefined || body.init !== undefined) {
|
|
1883
|
+
userProvidedDeckInput = true;
|
|
1884
|
+
}
|
|
1634
1885
|
if (body.init !== undefined && body.context === undefined) {
|
|
1635
|
-
logger.warn('[gambit] Received deprecated "init" field in test
|
|
1886
|
+
logger.warn('[gambit] Received deprecated "init" field in test API; use "context" instead.');
|
|
1636
1887
|
}
|
|
1637
1888
|
botInput = body.botInput;
|
|
1638
|
-
|
|
1889
|
+
if (typeof body.inheritBotInput === "boolean") {
|
|
1890
|
+
inheritBotInput = body.inheritBotInput;
|
|
1891
|
+
}
|
|
1892
|
+
if (body.initFill && Array.isArray(body.initFill.missing)) {
|
|
1893
|
+
initFillRequestMissing = body.initFill.missing.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
1894
|
+
}
|
|
1639
1895
|
if (typeof body.botDeckPath === "string") {
|
|
1640
1896
|
const resolved = resolveTestDeck(body.botDeckPath);
|
|
1641
1897
|
if (!resolved) {
|
|
@@ -1668,19 +1924,175 @@ function startWebSocketSimulator(opts) {
|
|
|
1668
1924
|
// ignore; keep undefined
|
|
1669
1925
|
}
|
|
1670
1926
|
}
|
|
1927
|
+
if (!userProvidedDeckInput && inheritBotInput && botInput !== undefined) {
|
|
1928
|
+
deckInput = cloneValue(botInput);
|
|
1929
|
+
}
|
|
1671
1930
|
if (!botDeckSelection) {
|
|
1672
1931
|
return new Response(JSON.stringify({ error: "No test decks configured" }), { status: 400, headers: { "content-type": "application/json" } });
|
|
1673
1932
|
}
|
|
1933
|
+
let initFillInfo;
|
|
1934
|
+
let initFillTrace;
|
|
1935
|
+
try {
|
|
1936
|
+
const rootDeck = await deckLoadPromise.catch(() => null);
|
|
1937
|
+
const rootSchema = rootDeck?.contextSchema ?? rootDeck?.inputSchema;
|
|
1938
|
+
const normalizedSchema = rootSchema
|
|
1939
|
+
? normalizeSchema(rootSchema)
|
|
1940
|
+
: undefined;
|
|
1941
|
+
const missing = normalizedSchema
|
|
1942
|
+
? findMissingRequiredFields(normalizedSchema, deckInput)
|
|
1943
|
+
: [];
|
|
1944
|
+
const requested = initFillRequestMissing?.length
|
|
1945
|
+
? missing.filter((entry) => initFillRequestMissing?.includes(entry))
|
|
1946
|
+
: missing;
|
|
1947
|
+
if (requested.length > 0) {
|
|
1948
|
+
const fillPrompt = buildInitFillPrompt({
|
|
1949
|
+
missing: requested,
|
|
1950
|
+
current: deckInput,
|
|
1951
|
+
schema: normalizedSchema,
|
|
1952
|
+
});
|
|
1953
|
+
const fillOutput = await runDeckWithFallback({
|
|
1954
|
+
path: botDeckSelection.path,
|
|
1955
|
+
input: botInput,
|
|
1956
|
+
inputProvided: botInput !== undefined,
|
|
1957
|
+
modelProvider: opts.modelProvider,
|
|
1958
|
+
allowRootStringInput: true,
|
|
1959
|
+
initialUserMessage: fillPrompt,
|
|
1960
|
+
responsesMode: opts.responsesMode,
|
|
1961
|
+
});
|
|
1962
|
+
const parsed = parseInitFillOutput(fillOutput);
|
|
1963
|
+
if (parsed.error) {
|
|
1964
|
+
initFillInfo = {
|
|
1965
|
+
requested,
|
|
1966
|
+
provided: fillOutput,
|
|
1967
|
+
error: parsed.error,
|
|
1968
|
+
};
|
|
1969
|
+
const failure = persistFailedInitFill({
|
|
1970
|
+
error: parsed.error,
|
|
1971
|
+
initFill: initFillInfo,
|
|
1972
|
+
botDeckPath: botDeckSelection.path,
|
|
1973
|
+
});
|
|
1974
|
+
return new Response(JSON.stringify({
|
|
1975
|
+
error: parsed.error,
|
|
1976
|
+
initFill: initFillInfo,
|
|
1977
|
+
sessionId: failure.sessionId,
|
|
1978
|
+
sessionPath: failure.sessionPath,
|
|
1979
|
+
}), {
|
|
1980
|
+
status: 400,
|
|
1981
|
+
headers: { "content-type": "application/json" },
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
let appliedObject = {};
|
|
1985
|
+
let appliedRoot = undefined;
|
|
1986
|
+
let nextInput = deckInput;
|
|
1987
|
+
for (const pathKey of requested) {
|
|
1988
|
+
const segments = pathKey === "(root)" ? [] : pathKey.split(".");
|
|
1989
|
+
const leafSchema = getSchemaAtPath(normalizedSchema, segments);
|
|
1990
|
+
const currentValue = getPathValue(nextInput, segments);
|
|
1991
|
+
if (currentValue !== undefined && currentValue !== null &&
|
|
1992
|
+
!(typeof currentValue === "string" &&
|
|
1993
|
+
(leafSchema?.kind === "string" ||
|
|
1994
|
+
leafSchema?.kind === "enum") &&
|
|
1995
|
+
currentValue.trim() === "") &&
|
|
1996
|
+
!(Array.isArray(currentValue) && leafSchema?.kind === "array" &&
|
|
1997
|
+
currentValue.length === 0)) {
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
2000
|
+
const fillValue = getPathValue(parsed.data, segments);
|
|
2001
|
+
if (fillValue === undefined)
|
|
2002
|
+
continue;
|
|
2003
|
+
if (segments.length === 0) {
|
|
2004
|
+
nextInput = fillValue;
|
|
2005
|
+
appliedRoot = fillValue;
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
nextInput = setPathValue(nextInput, segments, fillValue);
|
|
2009
|
+
const appliedValue = setPathValue(appliedObject, segments, fillValue);
|
|
2010
|
+
if (appliedValue && typeof appliedValue === "object") {
|
|
2011
|
+
appliedObject = appliedValue;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
const validated = validateInitInput(rootSchema, nextInput);
|
|
2015
|
+
deckInput = validated;
|
|
2016
|
+
const remainingMissing = normalizedSchema
|
|
2017
|
+
? findMissingRequiredFields(normalizedSchema, deckInput)
|
|
2018
|
+
: [];
|
|
2019
|
+
if (remainingMissing.length > 0) {
|
|
2020
|
+
const message = `Init fill incomplete: missing ${remainingMissing.join(", ")}`;
|
|
2021
|
+
initFillInfo = {
|
|
2022
|
+
requested,
|
|
2023
|
+
applied: appliedRoot !== undefined
|
|
2024
|
+
? appliedRoot
|
|
2025
|
+
: Object.keys(appliedObject).length
|
|
2026
|
+
? appliedObject
|
|
2027
|
+
: undefined,
|
|
2028
|
+
provided: parsed.data,
|
|
2029
|
+
error: message,
|
|
2030
|
+
};
|
|
2031
|
+
const failure = persistFailedInitFill({
|
|
2032
|
+
error: message,
|
|
2033
|
+
initFill: initFillInfo,
|
|
2034
|
+
botDeckPath: botDeckSelection.path,
|
|
2035
|
+
});
|
|
2036
|
+
return new Response(JSON.stringify({
|
|
2037
|
+
error: message,
|
|
2038
|
+
initFill: initFillInfo,
|
|
2039
|
+
sessionId: failure.sessionId,
|
|
2040
|
+
sessionPath: failure.sessionPath,
|
|
2041
|
+
}), {
|
|
2042
|
+
status: 400,
|
|
2043
|
+
headers: { "content-type": "application/json" },
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
initFillInfo = {
|
|
2047
|
+
requested,
|
|
2048
|
+
applied: appliedRoot !== undefined
|
|
2049
|
+
? appliedRoot
|
|
2050
|
+
: Object.keys(appliedObject).length
|
|
2051
|
+
? appliedObject
|
|
2052
|
+
: undefined,
|
|
2053
|
+
provided: parsed.data,
|
|
2054
|
+
};
|
|
2055
|
+
initFillTrace = {
|
|
2056
|
+
args: {
|
|
2057
|
+
missing: requested,
|
|
2058
|
+
},
|
|
2059
|
+
result: {
|
|
2060
|
+
applied: initFillInfo.applied,
|
|
2061
|
+
provided: initFillInfo.provided,
|
|
2062
|
+
},
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
catch (err) {
|
|
2067
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2068
|
+
initFillInfo = initFillInfo ?? {
|
|
2069
|
+
requested: [],
|
|
2070
|
+
};
|
|
2071
|
+
initFillInfo.error = message;
|
|
2072
|
+
const failure = persistFailedInitFill({
|
|
2073
|
+
error: message,
|
|
2074
|
+
initFill: initFillInfo,
|
|
2075
|
+
botDeckPath: botDeckSelection.path,
|
|
2076
|
+
});
|
|
2077
|
+
return new Response(JSON.stringify({
|
|
2078
|
+
error: message,
|
|
2079
|
+
initFill: initFillInfo,
|
|
2080
|
+
sessionId: failure.sessionId,
|
|
2081
|
+
sessionPath: failure.sessionPath,
|
|
2082
|
+
}), { status: 400, headers: { "content-type": "application/json" } });
|
|
2083
|
+
}
|
|
1674
2084
|
const run = startTestBotRun({
|
|
1675
2085
|
maxTurnsOverride,
|
|
1676
2086
|
deckInput,
|
|
1677
2087
|
botInput,
|
|
1678
2088
|
initialUserMessage,
|
|
1679
2089
|
botDeckPath: botDeckSelection.path,
|
|
2090
|
+
initFill: initFillInfo,
|
|
2091
|
+
initFillTrace,
|
|
1680
2092
|
});
|
|
1681
2093
|
return new Response(JSON.stringify({ run }), { headers: { "content-type": "application/json" } });
|
|
1682
2094
|
}
|
|
1683
|
-
if (url.pathname === "/api/test
|
|
2095
|
+
if (url.pathname === "/api/test/status") {
|
|
1684
2096
|
const runId = url.searchParams.get("runId") ?? undefined;
|
|
1685
2097
|
const sessionId = url.searchParams.get("sessionId") ?? undefined;
|
|
1686
2098
|
let entry = runId ? testBotRuns.get(runId) : undefined;
|
|
@@ -1753,7 +2165,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1753
2165
|
testDecks: availableTestDecks,
|
|
1754
2166
|
}), { headers: { "content-type": "application/json" } });
|
|
1755
2167
|
}
|
|
1756
|
-
if (url.pathname === "/api/test
|
|
2168
|
+
if (url.pathname === "/api/test/stop") {
|
|
1757
2169
|
if (req.method !== "POST") {
|
|
1758
2170
|
return new Response("Method not allowed", { status: 405 });
|
|
1759
2171
|
}
|
|
@@ -1854,6 +2266,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1854
2266
|
trace: tracer,
|
|
1855
2267
|
stream,
|
|
1856
2268
|
state: simulatorSavedState,
|
|
2269
|
+
responsesMode: opts.responsesMode,
|
|
1857
2270
|
onStateUpdate: (state) => {
|
|
1858
2271
|
const nextMeta = {
|
|
1859
2272
|
...(simulatorSavedState?.meta ?? {}),
|
|
@@ -2241,13 +2654,7 @@ function startWebSocketSimulator(opts) {
|
|
|
2241
2654
|
Array.isArray(state.messages)) {
|
|
2242
2655
|
const idx = state.messageRefs.findIndex((ref) => ref?.id === messageRefId);
|
|
2243
2656
|
if (idx >= 0) {
|
|
2244
|
-
|
|
2245
|
-
if (item?.type === "message") {
|
|
2246
|
-
messageContent = stringifyContent(item.content);
|
|
2247
|
-
}
|
|
2248
|
-
else {
|
|
2249
|
-
messageContent = undefined;
|
|
2250
|
-
}
|
|
2657
|
+
messageContent = state.messages[idx]?.content;
|
|
2251
2658
|
}
|
|
2252
2659
|
}
|
|
2253
2660
|
items.push({
|
|
@@ -2323,20 +2730,28 @@ function startWebSocketSimulator(opts) {
|
|
|
2323
2730
|
url.pathname.startsWith("/debug") ||
|
|
2324
2731
|
url.pathname.startsWith("/editor") ||
|
|
2325
2732
|
url.pathname.startsWith("/docs") ||
|
|
2326
|
-
url.pathname.startsWith("/test
|
|
2327
|
-
url.pathname.startsWith("/
|
|
2733
|
+
url.pathname.startsWith("/test") ||
|
|
2734
|
+
url.pathname.startsWith("/grade")) {
|
|
2328
2735
|
const hasBundle = await canServeReactBundle();
|
|
2329
2736
|
if (!hasBundle) {
|
|
2330
2737
|
return new Response("Simulator UI bundle missing. Run `deno task bundle:sim` (or start with `--bundle`).", { status: 500 });
|
|
2331
2738
|
}
|
|
2332
|
-
|
|
2739
|
+
await deckLoadPromise.catch(() => null);
|
|
2740
|
+
const resolvedLabel = deckLabel ?? toDeckLabel(resolvedDeckPath);
|
|
2741
|
+
return new Response(simulatorReactHtml(resolvedDeckPath, resolvedLabel), {
|
|
2333
2742
|
headers: { "content-type": "text/html; charset=utf-8" },
|
|
2334
2743
|
});
|
|
2335
2744
|
}
|
|
2336
2745
|
if (url.pathname === "/schema") {
|
|
2337
2746
|
const desc = await schemaPromise;
|
|
2747
|
+
const deck = await deckLoadPromise.catch(() => null);
|
|
2748
|
+
const startMode = deck &&
|
|
2749
|
+
(deck.startMode === "assistant" || deck.startMode === "user")
|
|
2750
|
+
? deck.startMode
|
|
2751
|
+
: undefined;
|
|
2338
2752
|
return new Response(JSON.stringify({
|
|
2339
2753
|
deck: resolvedDeckPath,
|
|
2754
|
+
startMode,
|
|
2340
2755
|
...desc,
|
|
2341
2756
|
}), {
|
|
2342
2757
|
headers: { "content-type": "application/json; charset=utf-8" },
|
|
@@ -2430,18 +2845,58 @@ function hasReactBundleSourceMap() {
|
|
|
2430
2845
|
return false;
|
|
2431
2846
|
}
|
|
2432
2847
|
}
|
|
2848
|
+
function newestMtimeInDir(dirPath) {
|
|
2849
|
+
const stack = [dirPath];
|
|
2850
|
+
let newest = undefined;
|
|
2851
|
+
while (stack.length > 0) {
|
|
2852
|
+
const current = stack.pop();
|
|
2853
|
+
if (!current)
|
|
2854
|
+
continue;
|
|
2855
|
+
let entries;
|
|
2856
|
+
try {
|
|
2857
|
+
entries = Array.from(dntShim.Deno.readDirSync(current));
|
|
2858
|
+
}
|
|
2859
|
+
catch {
|
|
2860
|
+
continue;
|
|
2861
|
+
}
|
|
2862
|
+
for (const entry of entries) {
|
|
2863
|
+
const entryPath = path.join(current, entry.name);
|
|
2864
|
+
if (entry.isDirectory) {
|
|
2865
|
+
stack.push(entryPath);
|
|
2866
|
+
continue;
|
|
2867
|
+
}
|
|
2868
|
+
if (!entry.isFile)
|
|
2869
|
+
continue;
|
|
2870
|
+
try {
|
|
2871
|
+
const stat = dntShim.Deno.statSync(entryPath);
|
|
2872
|
+
if (!stat.isFile)
|
|
2873
|
+
continue;
|
|
2874
|
+
const mtime = stat.mtime?.getTime();
|
|
2875
|
+
if (typeof mtime !== "number")
|
|
2876
|
+
continue;
|
|
2877
|
+
newest = newest === undefined ? mtime : Math.max(newest, mtime);
|
|
2878
|
+
}
|
|
2879
|
+
catch {
|
|
2880
|
+
continue;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
return newest;
|
|
2885
|
+
}
|
|
2433
2886
|
function isReactBundleStale() {
|
|
2434
2887
|
try {
|
|
2435
2888
|
const bundleStat = dntShim.Deno.statSync(simulatorBundlePath);
|
|
2436
|
-
|
|
2437
|
-
if (!bundleStat.isFile || !entryStat.isFile)
|
|
2889
|
+
if (!bundleStat.isFile)
|
|
2438
2890
|
return false;
|
|
2439
2891
|
const bundleTime = bundleStat.mtime?.getTime();
|
|
2440
|
-
|
|
2441
|
-
if (typeof bundleTime !== "number" || typeof entryTime !== "number") {
|
|
2892
|
+
if (typeof bundleTime !== "number") {
|
|
2442
2893
|
return false;
|
|
2443
2894
|
}
|
|
2444
|
-
|
|
2895
|
+
const srcRoot = path.resolve(moduleDir, "..", "simulator-ui", "src");
|
|
2896
|
+
const newestSource = newestMtimeInDir(srcRoot);
|
|
2897
|
+
if (typeof newestSource !== "number")
|
|
2898
|
+
return false;
|
|
2899
|
+
return newestSource > bundleTime;
|
|
2445
2900
|
}
|
|
2446
2901
|
catch {
|
|
2447
2902
|
return false;
|
|
@@ -2500,8 +2955,9 @@ async function readRemoteBundle(url, kind) {
|
|
|
2500
2955
|
return null;
|
|
2501
2956
|
}
|
|
2502
2957
|
}
|
|
2503
|
-
function simulatorReactHtml(deckPath) {
|
|
2504
|
-
const
|
|
2958
|
+
function simulatorReactHtml(deckPath, deckLabel) {
|
|
2959
|
+
const safeDeckPath = deckPath.replaceAll("<", "<").replaceAll(">", ">");
|
|
2960
|
+
const safeDeckLabel = deckLabel?.replaceAll("<", "<").replaceAll(">", ">") ?? null;
|
|
2505
2961
|
const bundleStamp = (() => {
|
|
2506
2962
|
try {
|
|
2507
2963
|
const stat = dntShim.Deno.statSync(simulatorBundlePath);
|
|
@@ -2529,7 +2985,8 @@ function simulatorReactHtml(deckPath) {
|
|
|
2529
2985
|
<body>
|
|
2530
2986
|
<div id="root"></div>
|
|
2531
2987
|
<script>
|
|
2532
|
-
window.__GAMBIT_DECK_PATH__ = ${JSON.stringify(
|
|
2988
|
+
window.__GAMBIT_DECK_PATH__ = ${JSON.stringify(safeDeckPath)};
|
|
2989
|
+
window.__GAMBIT_DECK_LABEL__ = ${JSON.stringify(safeDeckLabel)};
|
|
2533
2990
|
</script>
|
|
2534
2991
|
<script type="module" src="${bundleUrl}"></script>
|
|
2535
2992
|
</body>
|
|
@@ -2573,6 +3030,7 @@ async function runDeckWithFallback(args) {
|
|
|
2573
3030
|
onStateUpdate: args.onStateUpdate,
|
|
2574
3031
|
stream: args.stream,
|
|
2575
3032
|
onStreamText: args.onStreamText,
|
|
3033
|
+
responsesMode: args.responsesMode,
|
|
2576
3034
|
});
|
|
2577
3035
|
}
|
|
2578
3036
|
catch (error) {
|
|
@@ -2588,6 +3046,7 @@ async function runDeckWithFallback(args) {
|
|
|
2588
3046
|
onStateUpdate: args.onStateUpdate,
|
|
2589
3047
|
stream: args.stream,
|
|
2590
3048
|
onStreamText: args.onStreamText,
|
|
3049
|
+
responsesMode: args.responsesMode,
|
|
2591
3050
|
});
|
|
2592
3051
|
}
|
|
2593
3052
|
throw error;
|