@bolt-foundry/gambit 0.8.1 → 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 +78 -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 +8 -4
- package/esm/mod.d.ts.map +1 -1
- package/esm/mod.js +6 -2
- package/esm/src/cli_utils.d.ts.map +1 -1
- package/esm/src/cli_utils.js +39 -3
- 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 +22 -0
- package/esm/src/providers/openrouter.d.ts.map +1 -0
- package/esm/src/providers/openrouter.js +592 -0
- package/esm/src/server.d.ts +2 -0
- package/esm/src/server.d.ts.map +1 -1
- package/esm/src/server.js +612 -29
- package/esm/src/trace.d.ts.map +1 -1
- package/esm/src/trace.js +2 -2
- package/package.json +3 -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 +8 -4
- package/script/mod.d.ts.map +1 -1
- package/script/mod.js +13 -7
- package/script/src/cli_utils.d.ts.map +1 -1
- package/script/src/cli_utils.js +38 -2
- 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 +22 -0
- package/script/src/providers/openrouter.d.ts.map +1 -0
- package/script/src/providers/openrouter.js +632 -0
- package/script/src/server.d.ts +2 -0
- package/script/src/server.d.ts.map +1 -1
- package/script/src/server.js +612 -29
- package/script/src/trace.d.ts.map +1 -1
- package/script/src/trace.js +2 -2
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();
|
|
@@ -343,6 +345,162 @@ function deriveInitialFromSchema(schema) {
|
|
|
343
345
|
return undefined;
|
|
344
346
|
}
|
|
345
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
|
+
}
|
|
346
504
|
/**
|
|
347
505
|
* Start the WebSocket simulator server used by the Gambit debug UI.
|
|
348
506
|
*/
|
|
@@ -381,9 +539,10 @@ function startWebSocketSimulator(opts) {
|
|
|
381
539
|
};
|
|
382
540
|
const testBotRuns = new Map();
|
|
383
541
|
const broadcastTestBot = (payload) => {
|
|
384
|
-
(0, durable_streams_js_1.appendDurableStreamEvent)(
|
|
542
|
+
(0, durable_streams_js_1.appendDurableStreamEvent)(TEST_STREAM_ID, payload);
|
|
385
543
|
};
|
|
386
544
|
let deckSlug = deckSlugFromPath(resolvedDeckPath);
|
|
545
|
+
let deckLabel = undefined;
|
|
387
546
|
const enrichStateWithSession = (state) => {
|
|
388
547
|
const meta = { ...(state.meta ?? {}) };
|
|
389
548
|
const now = new Date();
|
|
@@ -575,6 +734,59 @@ function startWebSocketSimulator(opts) {
|
|
|
575
734
|
return String(value);
|
|
576
735
|
}
|
|
577
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
|
+
};
|
|
578
790
|
const updateTestDeckRegistry = (list) => {
|
|
579
791
|
testDeckByPath.clear();
|
|
580
792
|
testDeckById.clear();
|
|
@@ -627,11 +839,11 @@ function startWebSocketSimulator(opts) {
|
|
|
627
839
|
const fallbackToolInserts = [];
|
|
628
840
|
for (let i = 0; i < rawMessages.length; i++) {
|
|
629
841
|
const msg = rawMessages[i];
|
|
842
|
+
const refId = refs[i]?.id;
|
|
630
843
|
if (msg?.role === "assistant" || msg?.role === "user") {
|
|
631
844
|
const content = stringifyContent(msg.content).trim();
|
|
632
845
|
if (!content)
|
|
633
846
|
continue;
|
|
634
|
-
const refId = refs[i]?.id;
|
|
635
847
|
messages.push({
|
|
636
848
|
role: msg.role,
|
|
637
849
|
content,
|
|
@@ -640,6 +852,21 @@ function startWebSocketSimulator(opts) {
|
|
|
640
852
|
});
|
|
641
853
|
continue;
|
|
642
854
|
}
|
|
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
|
+
}
|
|
643
870
|
if (msg?.role === "tool") {
|
|
644
871
|
const actionCallId = typeof msg.tool_call_id === "string"
|
|
645
872
|
? msg.tool_call_id
|
|
@@ -660,6 +887,33 @@ function startWebSocketSimulator(opts) {
|
|
|
660
887
|
: fallbackToolInserts,
|
|
661
888
|
};
|
|
662
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
|
+
};
|
|
663
917
|
const deriveToolInsertsFromTraces = (state, messageCount) => {
|
|
664
918
|
const traces = Array.isArray(state.traces) ? state.traces : [];
|
|
665
919
|
if (!traces.length)
|
|
@@ -704,6 +958,10 @@ function startWebSocketSimulator(opts) {
|
|
|
704
958
|
: undefined;
|
|
705
959
|
if (sessionId)
|
|
706
960
|
run.sessionId = sessionId;
|
|
961
|
+
const initFill = state.meta
|
|
962
|
+
?.testBotInitFill;
|
|
963
|
+
if (initFill)
|
|
964
|
+
run.initFill = initFill;
|
|
707
965
|
run.traces = Array.isArray(state.traces) ? [...state.traces] : undefined;
|
|
708
966
|
};
|
|
709
967
|
const startTestBotRun = (runOpts = {}) => {
|
|
@@ -741,9 +999,27 @@ function startWebSocketSimulator(opts) {
|
|
|
741
999
|
};
|
|
742
1000
|
testBotRuns.set(runId, entry);
|
|
743
1001
|
const run = entry.run;
|
|
1002
|
+
if (runOpts.initFill)
|
|
1003
|
+
run.initFill = runOpts.initFill;
|
|
744
1004
|
let savedState = undefined;
|
|
745
1005
|
let lastCount = 0;
|
|
746
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
|
+
}
|
|
747
1023
|
const setSessionId = (state) => {
|
|
748
1024
|
const sessionId = typeof state?.meta?.sessionId === "string"
|
|
749
1025
|
? state.meta.sessionId
|
|
@@ -799,6 +1075,7 @@ function startWebSocketSimulator(opts) {
|
|
|
799
1075
|
},
|
|
800
1076
|
stream: Boolean(streamOpts?.onStreamText),
|
|
801
1077
|
onStreamText: streamOpts?.onStreamText,
|
|
1078
|
+
responsesMode: opts.responsesMode,
|
|
802
1079
|
});
|
|
803
1080
|
if ((0, gambit_core_1.isGambitEndSignal)(result)) {
|
|
804
1081
|
sessionEnded = true;
|
|
@@ -822,6 +1099,7 @@ function startWebSocketSimulator(opts) {
|
|
|
822
1099
|
state: savedState,
|
|
823
1100
|
allowRootStringInput: true,
|
|
824
1101
|
initialUserMessage: initialUserMessage || undefined,
|
|
1102
|
+
responsesMode: opts.responsesMode,
|
|
825
1103
|
onStateUpdate: (state) => {
|
|
826
1104
|
const nextMeta = {
|
|
827
1105
|
...(savedState?.meta ?? {}),
|
|
@@ -830,6 +1108,7 @@ function startWebSocketSimulator(opts) {
|
|
|
830
1108
|
testBotRunId: runId,
|
|
831
1109
|
testBotConfigPath: botConfigPath,
|
|
832
1110
|
testBotName,
|
|
1111
|
+
...(run.initFill ? { testBotInitFill: run.initFill } : {}),
|
|
833
1112
|
};
|
|
834
1113
|
const enriched = persistSessionState({
|
|
835
1114
|
...state,
|
|
@@ -881,6 +1160,7 @@ function startWebSocketSimulator(opts) {
|
|
|
881
1160
|
state: savedState,
|
|
882
1161
|
allowRootStringInput: true,
|
|
883
1162
|
initialUserMessage: userMessage,
|
|
1163
|
+
responsesMode: opts.responsesMode,
|
|
884
1164
|
onStateUpdate: (state) => {
|
|
885
1165
|
const nextMeta = {
|
|
886
1166
|
...(savedState?.meta ?? {}),
|
|
@@ -889,6 +1169,7 @@ function startWebSocketSimulator(opts) {
|
|
|
889
1169
|
testBotRunId: runId,
|
|
890
1170
|
testBotConfigPath: botConfigPath,
|
|
891
1171
|
testBotName,
|
|
1172
|
+
...(run.initFill ? { testBotInitFill: run.initFill } : {}),
|
|
892
1173
|
};
|
|
893
1174
|
const enriched = persistSessionState({
|
|
894
1175
|
...state,
|
|
@@ -947,10 +1228,60 @@ function startWebSocketSimulator(opts) {
|
|
|
947
1228
|
broadcastTestBot({ type: "testBotStatus", run });
|
|
948
1229
|
return run;
|
|
949
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
|
+
};
|
|
950
1278
|
const deckLoadPromise = (0, gambit_core_2.loadDeck)(resolvedDeckPath)
|
|
951
1279
|
.then((deck) => {
|
|
952
1280
|
resolvedDeckPath = deck.path;
|
|
953
1281
|
deckSlug = deckSlugFromPath(resolvedDeckPath);
|
|
1282
|
+
deckLabel = typeof deck.label === "string"
|
|
1283
|
+
? deck.label
|
|
1284
|
+
: toDeckLabel(deck.path);
|
|
954
1285
|
availableTestDecks = (deck.testDecks ?? []).map((testDeck, index) => {
|
|
955
1286
|
const label = testDeck.label && typeof testDeck.label === "string"
|
|
956
1287
|
? testDeck.label
|
|
@@ -1014,14 +1345,21 @@ function startWebSocketSimulator(opts) {
|
|
|
1014
1345
|
const wantsSourceMap = Boolean(opts.sourceMap);
|
|
1015
1346
|
const bundlePlatform = opts.bundlePlatform ?? "deno";
|
|
1016
1347
|
const autoBundle = opts.autoBundle ?? true;
|
|
1348
|
+
const forceBundle = opts.forceBundle ?? false;
|
|
1017
1349
|
const needsBundle = !hasReactBundle() ||
|
|
1018
1350
|
(wantsSourceMap && !hasReactBundleSourceMap()) ||
|
|
1019
1351
|
isReactBundleStale();
|
|
1020
|
-
const shouldAutoBundle = autoBundle && moduleLocation.isLocal &&
|
|
1352
|
+
const shouldAutoBundle = autoBundle && moduleLocation.isLocal &&
|
|
1353
|
+
(forceBundle || needsBundle);
|
|
1021
1354
|
if (autoBundle && !moduleLocation.isLocal && opts.verbose) {
|
|
1022
1355
|
logger.log("[sim] auto-bundle disabled for remote package; using packaged bundle.");
|
|
1023
1356
|
}
|
|
1357
|
+
if (autoBundle && moduleLocation.isLocal && !shouldAutoBundle) {
|
|
1358
|
+
logger.log("[sim] auto-bundle enabled; bundle already up to date.");
|
|
1359
|
+
}
|
|
1024
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"})...`);
|
|
1025
1363
|
try {
|
|
1026
1364
|
const p = new dntShim.Deno.Command("deno", {
|
|
1027
1365
|
args: [
|
|
@@ -1048,6 +1386,28 @@ function startWebSocketSimulator(opts) {
|
|
|
1048
1386
|
if (url.pathname.startsWith("/api/durable-streams/stream/")) {
|
|
1049
1387
|
return (0, durable_streams_js_1.handleDurableStreamRequest)(req);
|
|
1050
1388
|
}
|
|
1389
|
+
if (url.pathname === "/favicon.ico") {
|
|
1390
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1391
|
+
return new Response("Method not allowed", { status: 405 });
|
|
1392
|
+
}
|
|
1393
|
+
try {
|
|
1394
|
+
const data = await dntShim.Deno.readFile(simulatorFaviconDistPath);
|
|
1395
|
+
return new Response(req.method === "HEAD" ? null : data, {
|
|
1396
|
+
headers: { "content-type": "image/x-icon" },
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
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
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1051
1411
|
if (url.pathname === "/api/calibrate") {
|
|
1052
1412
|
if (req.method !== "GET") {
|
|
1053
1413
|
return new Response("Method not allowed", { status: 405 });
|
|
@@ -1093,9 +1453,10 @@ function startWebSocketSimulator(opts) {
|
|
|
1093
1453
|
delete next.gradingRuns;
|
|
1094
1454
|
return next;
|
|
1095
1455
|
})();
|
|
1456
|
+
const conversationMessages = buildConversationMessages(sessionState);
|
|
1096
1457
|
const sessionPayload = {
|
|
1097
|
-
messages:
|
|
1098
|
-
?
|
|
1458
|
+
messages: conversationMessages.length > 0
|
|
1459
|
+
? conversationMessages.map((msg) => ({
|
|
1099
1460
|
role: msg.role,
|
|
1100
1461
|
content: msg.content,
|
|
1101
1462
|
name: msg.name,
|
|
@@ -1128,7 +1489,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1128
1489
|
},
|
|
1129
1490
|
});
|
|
1130
1491
|
const sessionMeta = buildSessionMeta(sessionId, nextState);
|
|
1131
|
-
(0, durable_streams_js_1.appendDurableStreamEvent)(
|
|
1492
|
+
(0, durable_streams_js_1.appendDurableStreamEvent)(GRADE_STREAM_ID, {
|
|
1132
1493
|
type: "calibrateSession",
|
|
1133
1494
|
sessionId,
|
|
1134
1495
|
run: nextEntry,
|
|
@@ -1158,6 +1519,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1158
1519
|
allowRootStringInput: false,
|
|
1159
1520
|
initialUserMessage: undefined,
|
|
1160
1521
|
stream: false,
|
|
1522
|
+
responsesMode: opts.responsesMode,
|
|
1161
1523
|
});
|
|
1162
1524
|
}
|
|
1163
1525
|
const messages = sessionPayload.messages ?? [];
|
|
@@ -1197,6 +1559,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1197
1559
|
allowRootStringInput: false,
|
|
1198
1560
|
initialUserMessage: undefined,
|
|
1199
1561
|
stream: false,
|
|
1562
|
+
responsesMode: opts.responsesMode,
|
|
1200
1563
|
});
|
|
1201
1564
|
turns.push({
|
|
1202
1565
|
index: idx,
|
|
@@ -1299,7 +1662,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1299
1662
|
},
|
|
1300
1663
|
});
|
|
1301
1664
|
const sessionMeta = buildSessionMeta(body.sessionId, updated);
|
|
1302
|
-
(0, durable_streams_js_1.appendDurableStreamEvent)(
|
|
1665
|
+
(0, durable_streams_js_1.appendDurableStreamEvent)(GRADE_STREAM_ID, {
|
|
1303
1666
|
type: "calibrateSession",
|
|
1304
1667
|
sessionId: body.sessionId,
|
|
1305
1668
|
session: sessionMeta,
|
|
@@ -1352,7 +1715,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1352
1715
|
},
|
|
1353
1716
|
});
|
|
1354
1717
|
const sessionMeta = buildSessionMeta(body.sessionId, updated);
|
|
1355
|
-
(0, durable_streams_js_1.appendDurableStreamEvent)(
|
|
1718
|
+
(0, durable_streams_js_1.appendDurableStreamEvent)(GRADE_STREAM_ID, {
|
|
1356
1719
|
type: "calibrateSession",
|
|
1357
1720
|
sessionId: body.sessionId,
|
|
1358
1721
|
session: sessionMeta,
|
|
@@ -1439,7 +1802,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1439
1802
|
},
|
|
1440
1803
|
});
|
|
1441
1804
|
const sessionMeta = buildSessionMeta(body.sessionId, nextState);
|
|
1442
|
-
(0, durable_streams_js_1.appendDurableStreamEvent)(
|
|
1805
|
+
(0, durable_streams_js_1.appendDurableStreamEvent)(GRADE_STREAM_ID, {
|
|
1443
1806
|
type: "calibrateSession",
|
|
1444
1807
|
sessionId: body.sessionId,
|
|
1445
1808
|
run: nextRun,
|
|
@@ -1457,7 +1820,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1457
1820
|
}), { status: 400, headers: { "content-type": "application/json" } });
|
|
1458
1821
|
}
|
|
1459
1822
|
}
|
|
1460
|
-
if (url.pathname === "/api/test
|
|
1823
|
+
if (url.pathname === "/api/test") {
|
|
1461
1824
|
if (req.method === "GET") {
|
|
1462
1825
|
await deckLoadPromise.catch(() => null);
|
|
1463
1826
|
const requestedDeck = url.searchParams.get("deckPath");
|
|
@@ -1498,7 +1861,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1498
1861
|
}
|
|
1499
1862
|
return new Response("Method not allowed", { status: 405 });
|
|
1500
1863
|
}
|
|
1501
|
-
if (url.pathname === "/api/test
|
|
1864
|
+
if (url.pathname === "/api/test/run") {
|
|
1502
1865
|
if (req.method !== "POST") {
|
|
1503
1866
|
return new Response("Method not allowed", { status: 405 });
|
|
1504
1867
|
}
|
|
@@ -1507,17 +1870,28 @@ function startWebSocketSimulator(opts) {
|
|
|
1507
1870
|
let botInput = undefined;
|
|
1508
1871
|
let initialUserMessage = undefined;
|
|
1509
1872
|
let botDeckSelection;
|
|
1873
|
+
let inheritBotInput = false;
|
|
1874
|
+
let userProvidedDeckInput = false;
|
|
1875
|
+
let initFillRequestMissing = undefined;
|
|
1510
1876
|
try {
|
|
1511
1877
|
const body = await req.json();
|
|
1512
1878
|
if (typeof body.maxTurns === "number" && Number.isFinite(body.maxTurns)) {
|
|
1513
1879
|
maxTurnsOverride = body.maxTurns;
|
|
1514
1880
|
}
|
|
1515
1881
|
deckInput = body.context ?? body.init;
|
|
1882
|
+
if (body.context !== undefined || body.init !== undefined) {
|
|
1883
|
+
userProvidedDeckInput = true;
|
|
1884
|
+
}
|
|
1516
1885
|
if (body.init !== undefined && body.context === undefined) {
|
|
1517
|
-
logger.warn('[gambit] Received deprecated "init" field in test
|
|
1886
|
+
logger.warn('[gambit] Received deprecated "init" field in test API; use "context" instead.');
|
|
1518
1887
|
}
|
|
1519
1888
|
botInput = body.botInput;
|
|
1520
|
-
|
|
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
|
+
}
|
|
1521
1895
|
if (typeof body.botDeckPath === "string") {
|
|
1522
1896
|
const resolved = resolveTestDeck(body.botDeckPath);
|
|
1523
1897
|
if (!resolved) {
|
|
@@ -1550,19 +1924,175 @@ function startWebSocketSimulator(opts) {
|
|
|
1550
1924
|
// ignore; keep undefined
|
|
1551
1925
|
}
|
|
1552
1926
|
}
|
|
1927
|
+
if (!userProvidedDeckInput && inheritBotInput && botInput !== undefined) {
|
|
1928
|
+
deckInput = cloneValue(botInput);
|
|
1929
|
+
}
|
|
1553
1930
|
if (!botDeckSelection) {
|
|
1554
1931
|
return new Response(JSON.stringify({ error: "No test decks configured" }), { status: 400, headers: { "content-type": "application/json" } });
|
|
1555
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
|
+
}
|
|
1556
2084
|
const run = startTestBotRun({
|
|
1557
2085
|
maxTurnsOverride,
|
|
1558
2086
|
deckInput,
|
|
1559
2087
|
botInput,
|
|
1560
2088
|
initialUserMessage,
|
|
1561
2089
|
botDeckPath: botDeckSelection.path,
|
|
2090
|
+
initFill: initFillInfo,
|
|
2091
|
+
initFillTrace,
|
|
1562
2092
|
});
|
|
1563
2093
|
return new Response(JSON.stringify({ run }), { headers: { "content-type": "application/json" } });
|
|
1564
2094
|
}
|
|
1565
|
-
if (url.pathname === "/api/test
|
|
2095
|
+
if (url.pathname === "/api/test/status") {
|
|
1566
2096
|
const runId = url.searchParams.get("runId") ?? undefined;
|
|
1567
2097
|
const sessionId = url.searchParams.get("sessionId") ?? undefined;
|
|
1568
2098
|
let entry = runId ? testBotRuns.get(runId) : undefined;
|
|
@@ -1635,7 +2165,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1635
2165
|
testDecks: availableTestDecks,
|
|
1636
2166
|
}), { headers: { "content-type": "application/json" } });
|
|
1637
2167
|
}
|
|
1638
|
-
if (url.pathname === "/api/test
|
|
2168
|
+
if (url.pathname === "/api/test/stop") {
|
|
1639
2169
|
if (req.method !== "POST") {
|
|
1640
2170
|
return new Response("Method not allowed", { status: 405 });
|
|
1641
2171
|
}
|
|
@@ -1736,6 +2266,7 @@ function startWebSocketSimulator(opts) {
|
|
|
1736
2266
|
trace: tracer,
|
|
1737
2267
|
stream,
|
|
1738
2268
|
state: simulatorSavedState,
|
|
2269
|
+
responsesMode: opts.responsesMode,
|
|
1739
2270
|
onStateUpdate: (state) => {
|
|
1740
2271
|
const nextMeta = {
|
|
1741
2272
|
...(simulatorSavedState?.meta ?? {}),
|
|
@@ -2199,20 +2730,28 @@ function startWebSocketSimulator(opts) {
|
|
|
2199
2730
|
url.pathname.startsWith("/debug") ||
|
|
2200
2731
|
url.pathname.startsWith("/editor") ||
|
|
2201
2732
|
url.pathname.startsWith("/docs") ||
|
|
2202
|
-
url.pathname.startsWith("/test
|
|
2203
|
-
url.pathname.startsWith("/
|
|
2733
|
+
url.pathname.startsWith("/test") ||
|
|
2734
|
+
url.pathname.startsWith("/grade")) {
|
|
2204
2735
|
const hasBundle = await canServeReactBundle();
|
|
2205
2736
|
if (!hasBundle) {
|
|
2206
2737
|
return new Response("Simulator UI bundle missing. Run `deno task bundle:sim` (or start with `--bundle`).", { status: 500 });
|
|
2207
2738
|
}
|
|
2208
|
-
|
|
2739
|
+
await deckLoadPromise.catch(() => null);
|
|
2740
|
+
const resolvedLabel = deckLabel ?? toDeckLabel(resolvedDeckPath);
|
|
2741
|
+
return new Response(simulatorReactHtml(resolvedDeckPath, resolvedLabel), {
|
|
2209
2742
|
headers: { "content-type": "text/html; charset=utf-8" },
|
|
2210
2743
|
});
|
|
2211
2744
|
}
|
|
2212
2745
|
if (url.pathname === "/schema") {
|
|
2213
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;
|
|
2214
2752
|
return new Response(JSON.stringify({
|
|
2215
2753
|
deck: resolvedDeckPath,
|
|
2754
|
+
startMode,
|
|
2216
2755
|
...desc,
|
|
2217
2756
|
}), {
|
|
2218
2757
|
headers: { "content-type": "application/json; charset=utf-8" },
|
|
@@ -2306,18 +2845,58 @@ function hasReactBundleSourceMap() {
|
|
|
2306
2845
|
return false;
|
|
2307
2846
|
}
|
|
2308
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
|
+
}
|
|
2309
2886
|
function isReactBundleStale() {
|
|
2310
2887
|
try {
|
|
2311
2888
|
const bundleStat = dntShim.Deno.statSync(simulatorBundlePath);
|
|
2312
|
-
|
|
2313
|
-
if (!bundleStat.isFile || !entryStat.isFile)
|
|
2889
|
+
if (!bundleStat.isFile)
|
|
2314
2890
|
return false;
|
|
2315
2891
|
const bundleTime = bundleStat.mtime?.getTime();
|
|
2316
|
-
|
|
2317
|
-
if (typeof bundleTime !== "number" || typeof entryTime !== "number") {
|
|
2892
|
+
if (typeof bundleTime !== "number") {
|
|
2318
2893
|
return false;
|
|
2319
2894
|
}
|
|
2320
|
-
|
|
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;
|
|
2321
2900
|
}
|
|
2322
2901
|
catch {
|
|
2323
2902
|
return false;
|
|
@@ -2376,8 +2955,9 @@ async function readRemoteBundle(url, kind) {
|
|
|
2376
2955
|
return null;
|
|
2377
2956
|
}
|
|
2378
2957
|
}
|
|
2379
|
-
function simulatorReactHtml(deckPath) {
|
|
2380
|
-
const
|
|
2958
|
+
function simulatorReactHtml(deckPath, deckLabel) {
|
|
2959
|
+
const safeDeckPath = deckPath.replaceAll("<", "<").replaceAll(">", ">");
|
|
2960
|
+
const safeDeckLabel = deckLabel?.replaceAll("<", "<").replaceAll(">", ">") ?? null;
|
|
2381
2961
|
const bundleStamp = (() => {
|
|
2382
2962
|
try {
|
|
2383
2963
|
const stat = dntShim.Deno.statSync(simulatorBundlePath);
|
|
@@ -2405,7 +2985,8 @@ function simulatorReactHtml(deckPath) {
|
|
|
2405
2985
|
<body>
|
|
2406
2986
|
<div id="root"></div>
|
|
2407
2987
|
<script>
|
|
2408
|
-
window.__GAMBIT_DECK_PATH__ = ${JSON.stringify(
|
|
2988
|
+
window.__GAMBIT_DECK_PATH__ = ${JSON.stringify(safeDeckPath)};
|
|
2989
|
+
window.__GAMBIT_DECK_LABEL__ = ${JSON.stringify(safeDeckLabel)};
|
|
2409
2990
|
</script>
|
|
2410
2991
|
<script type="module" src="${bundleUrl}"></script>
|
|
2411
2992
|
</body>
|
|
@@ -2449,6 +3030,7 @@ async function runDeckWithFallback(args) {
|
|
|
2449
3030
|
onStateUpdate: args.onStateUpdate,
|
|
2450
3031
|
stream: args.stream,
|
|
2451
3032
|
onStreamText: args.onStreamText,
|
|
3033
|
+
responsesMode: args.responsesMode,
|
|
2452
3034
|
});
|
|
2453
3035
|
}
|
|
2454
3036
|
catch (error) {
|
|
@@ -2464,6 +3046,7 @@ async function runDeckWithFallback(args) {
|
|
|
2464
3046
|
onStateUpdate: args.onStateUpdate,
|
|
2465
3047
|
stream: args.stream,
|
|
2466
3048
|
onStreamText: args.onStreamText,
|
|
3049
|
+
responsesMode: args.responsesMode,
|
|
2467
3050
|
});
|
|
2468
3051
|
}
|
|
2469
3052
|
throw error;
|