@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/esm/src/server.js
CHANGED
|
@@ -6,6 +6,7 @@ import { makeConsoleTracer } from "./trace.js";
|
|
|
6
6
|
import { defaultSessionRoot } from "./cli_utils.js";
|
|
7
7
|
import { loadDeck } from "@bolt-foundry/gambit-core";
|
|
8
8
|
import { appendDurableStreamEvent, handleDurableStreamRequest, } from "./durable_streams.js";
|
|
9
|
+
const GAMBIT_TOOL_RESPOND = "gambit_respond";
|
|
9
10
|
const logger = console;
|
|
10
11
|
const moduleLocation = (() => {
|
|
11
12
|
const directoryFromUrl = (url) => {
|
|
@@ -47,11 +48,12 @@ const simulatorBundleSourceMapUrl = (() => {
|
|
|
47
48
|
let cachedRemoteBundle = null;
|
|
48
49
|
let cachedRemoteBundleSourceMap = null;
|
|
49
50
|
const simulatorBundlePath = path.resolve(moduleDir, "..", "simulator-ui", "dist", "bundle.js");
|
|
50
|
-
const simulatorUiEntryPath = path.resolve(moduleDir, "..", "simulator-ui", "src", "main.tsx");
|
|
51
51
|
const simulatorBundleSourceMapPath = path.resolve(moduleDir, "..", "simulator-ui", "dist", "bundle.js.map");
|
|
52
|
+
const simulatorFaviconDistPath = path.resolve(moduleDir, "..", "simulator-ui", "dist", "favicon.ico");
|
|
53
|
+
const simulatorFaviconSrcPath = path.resolve(moduleDir, "..", "simulator-ui", "src", "favicon.ico");
|
|
52
54
|
const SIMULATOR_STREAM_ID = "gambit-simulator";
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
+
const GRADE_STREAM_ID = "gambit-grade";
|
|
56
|
+
const TEST_STREAM_ID = "gambit-test";
|
|
55
57
|
let availableTestDecks = [];
|
|
56
58
|
const testDeckByPath = new Map();
|
|
57
59
|
const testDeckById = new Map();
|
|
@@ -307,6 +309,162 @@ function deriveInitialFromSchema(schema) {
|
|
|
307
309
|
return undefined;
|
|
308
310
|
}
|
|
309
311
|
}
|
|
312
|
+
function getPathValue(value, path) {
|
|
313
|
+
let current = value;
|
|
314
|
+
for (const segment of path) {
|
|
315
|
+
if (!current || typeof current !== "object" ||
|
|
316
|
+
!(segment in current)) {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
current = current[segment];
|
|
320
|
+
}
|
|
321
|
+
return current;
|
|
322
|
+
}
|
|
323
|
+
function setPathValue(value, path, nextValue) {
|
|
324
|
+
if (path.length === 0)
|
|
325
|
+
return nextValue;
|
|
326
|
+
const root = value && typeof value === "object"
|
|
327
|
+
? cloneValue(value)
|
|
328
|
+
: {};
|
|
329
|
+
let cursor = root;
|
|
330
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
331
|
+
const segment = path[i];
|
|
332
|
+
const existing = cursor[segment];
|
|
333
|
+
const next = existing && typeof existing === "object"
|
|
334
|
+
? cloneValue(existing)
|
|
335
|
+
: {};
|
|
336
|
+
cursor[segment] = next;
|
|
337
|
+
cursor = next;
|
|
338
|
+
}
|
|
339
|
+
const last = path[path.length - 1];
|
|
340
|
+
if (nextValue === undefined) {
|
|
341
|
+
delete cursor[last];
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
cursor[last] = nextValue;
|
|
345
|
+
}
|
|
346
|
+
return root;
|
|
347
|
+
}
|
|
348
|
+
function findMissingRequiredFields(schema, value, prefix = []) {
|
|
349
|
+
if (!schema)
|
|
350
|
+
return [];
|
|
351
|
+
if (schema.optional)
|
|
352
|
+
return [];
|
|
353
|
+
if (schema.kind === "object" && schema.fields) {
|
|
354
|
+
if (value !== undefined && value !== null &&
|
|
355
|
+
(typeof value !== "object" || Array.isArray(value))) {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
const asObj = value && typeof value === "object"
|
|
359
|
+
? value
|
|
360
|
+
: undefined;
|
|
361
|
+
const missing = [];
|
|
362
|
+
for (const [key, child] of Object.entries(schema.fields)) {
|
|
363
|
+
missing.push(...findMissingRequiredFields(child, asObj ? asObj[key] : undefined, [...prefix, key]));
|
|
364
|
+
}
|
|
365
|
+
return missing;
|
|
366
|
+
}
|
|
367
|
+
const key = prefix.join(".") || "(root)";
|
|
368
|
+
if (value === undefined || value === null) {
|
|
369
|
+
return schema.defaultValue !== undefined ? [] : [key];
|
|
370
|
+
}
|
|
371
|
+
if (schema.kind === "string" || schema.kind === "enum") {
|
|
372
|
+
return typeof value === "string" && value.trim() === "" ? [key] : [];
|
|
373
|
+
}
|
|
374
|
+
if (schema.kind === "array") {
|
|
375
|
+
return Array.isArray(value) && value.length === 0 ? [key] : [];
|
|
376
|
+
}
|
|
377
|
+
if (schema.kind === "number") {
|
|
378
|
+
return typeof value === "number" && Number.isFinite(value) ? [] : [key];
|
|
379
|
+
}
|
|
380
|
+
if (schema.kind === "boolean") {
|
|
381
|
+
return typeof value === "boolean" ? [] : [key];
|
|
382
|
+
}
|
|
383
|
+
return [];
|
|
384
|
+
}
|
|
385
|
+
function getSchemaAtPath(schema, path) {
|
|
386
|
+
let current = schema;
|
|
387
|
+
for (const segment of path) {
|
|
388
|
+
if (!current || current.kind !== "object" || !current.fields)
|
|
389
|
+
return;
|
|
390
|
+
current = current.fields[segment];
|
|
391
|
+
}
|
|
392
|
+
return current;
|
|
393
|
+
}
|
|
394
|
+
function buildInitFillPrompt(args) {
|
|
395
|
+
const schemaHints = args.missing.map((path) => {
|
|
396
|
+
const segments = path === "(root)" ? [] : path.split(".");
|
|
397
|
+
const leaf = getSchemaAtPath(args.schema, segments);
|
|
398
|
+
return {
|
|
399
|
+
path,
|
|
400
|
+
kind: leaf?.kind,
|
|
401
|
+
description: leaf?.description,
|
|
402
|
+
enumValues: leaf?.enumValues,
|
|
403
|
+
};
|
|
404
|
+
});
|
|
405
|
+
const payload = {
|
|
406
|
+
type: "gambit_test_bot_init_fill",
|
|
407
|
+
missing: args.missing,
|
|
408
|
+
current: args.current ?? null,
|
|
409
|
+
schemaHints,
|
|
410
|
+
};
|
|
411
|
+
return [
|
|
412
|
+
"You are filling missing required init fields for a Gambit Test Bot run.",
|
|
413
|
+
"Return ONLY valid JSON that includes values for the missing fields.",
|
|
414
|
+
"Do not include any fields that are not listed as missing.",
|
|
415
|
+
"If the only missing path is '(root)', return the full init JSON value.",
|
|
416
|
+
"",
|
|
417
|
+
JSON.stringify(payload, null, 2),
|
|
418
|
+
].join("\n");
|
|
419
|
+
}
|
|
420
|
+
function unwrapRespondPayload(output) {
|
|
421
|
+
if (!output || typeof output !== "object")
|
|
422
|
+
return output;
|
|
423
|
+
const record = output;
|
|
424
|
+
if ("payload" in record) {
|
|
425
|
+
return record.payload;
|
|
426
|
+
}
|
|
427
|
+
return output;
|
|
428
|
+
}
|
|
429
|
+
function parseInitFillOutput(output) {
|
|
430
|
+
if (output === null || output === undefined) {
|
|
431
|
+
return { error: "Persona returned empty init fill output." };
|
|
432
|
+
}
|
|
433
|
+
if (typeof output === "object") {
|
|
434
|
+
return { data: unwrapRespondPayload(output) };
|
|
435
|
+
}
|
|
436
|
+
if (typeof output === "string") {
|
|
437
|
+
const text = output.trim();
|
|
438
|
+
if (!text)
|
|
439
|
+
return { error: "Persona returned empty init fill output." };
|
|
440
|
+
try {
|
|
441
|
+
const parsed = JSON.parse(text);
|
|
442
|
+
return { data: unwrapRespondPayload(parsed) };
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
return {
|
|
446
|
+
error: `Persona returned invalid JSON for init fill: ${err instanceof Error ? err.message : String(err)}`,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return { error: "Persona returned unsupported init fill output." };
|
|
451
|
+
}
|
|
452
|
+
function validateInitInput(schema, value) {
|
|
453
|
+
if (!schema)
|
|
454
|
+
return value;
|
|
455
|
+
if (typeof schema.safeParse !== "function") {
|
|
456
|
+
throw new Error("Init schema missing safeParse");
|
|
457
|
+
}
|
|
458
|
+
const result = schema.safeParse(value);
|
|
459
|
+
if (!result.success) {
|
|
460
|
+
const issue = result.error.issues?.[0];
|
|
461
|
+
const message = issue
|
|
462
|
+
? `${issue.path.join(".") || "(root)"}: ${issue.message}`
|
|
463
|
+
: result.error.message;
|
|
464
|
+
throw new Error(`Schema validation failed: ${message}`);
|
|
465
|
+
}
|
|
466
|
+
return result.data;
|
|
467
|
+
}
|
|
310
468
|
/**
|
|
311
469
|
* Start the WebSocket simulator server used by the Gambit debug UI.
|
|
312
470
|
*/
|
|
@@ -345,9 +503,10 @@ export function startWebSocketSimulator(opts) {
|
|
|
345
503
|
};
|
|
346
504
|
const testBotRuns = new Map();
|
|
347
505
|
const broadcastTestBot = (payload) => {
|
|
348
|
-
appendDurableStreamEvent(
|
|
506
|
+
appendDurableStreamEvent(TEST_STREAM_ID, payload);
|
|
349
507
|
};
|
|
350
508
|
let deckSlug = deckSlugFromPath(resolvedDeckPath);
|
|
509
|
+
let deckLabel = undefined;
|
|
351
510
|
const enrichStateWithSession = (state) => {
|
|
352
511
|
const meta = { ...(state.meta ?? {}) };
|
|
353
512
|
const now = new Date();
|
|
@@ -539,6 +698,59 @@ export function startWebSocketSimulator(opts) {
|
|
|
539
698
|
return String(value);
|
|
540
699
|
}
|
|
541
700
|
};
|
|
701
|
+
const safeParseJson = (text) => {
|
|
702
|
+
if (typeof text !== "string" || text.trim().length === 0)
|
|
703
|
+
return undefined;
|
|
704
|
+
try {
|
|
705
|
+
return JSON.parse(text);
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
return undefined;
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
const summarizeRespondCall = (message) => {
|
|
712
|
+
if (!message || message.role !== "tool")
|
|
713
|
+
return null;
|
|
714
|
+
const name = typeof message.name === "string" ? message.name : undefined;
|
|
715
|
+
if (name !== GAMBIT_TOOL_RESPOND)
|
|
716
|
+
return null;
|
|
717
|
+
const parsed = safeParseJson(typeof message.content === "string" ? message.content : "");
|
|
718
|
+
const payload = parsed && typeof parsed === "object"
|
|
719
|
+
? ("payload" in parsed
|
|
720
|
+
? parsed.payload
|
|
721
|
+
: parsed)
|
|
722
|
+
: undefined;
|
|
723
|
+
const status = typeof parsed?.status === "number"
|
|
724
|
+
? parsed.status
|
|
725
|
+
: undefined;
|
|
726
|
+
const code = typeof parsed?.code === "string"
|
|
727
|
+
? parsed.code
|
|
728
|
+
: undefined;
|
|
729
|
+
const respondMessage = typeof parsed?.message === "string"
|
|
730
|
+
? parsed.message
|
|
731
|
+
: undefined;
|
|
732
|
+
const meta = parsed && typeof parsed.meta === "object"
|
|
733
|
+
? parsed.meta
|
|
734
|
+
: undefined;
|
|
735
|
+
const summary = {};
|
|
736
|
+
if (status !== undefined)
|
|
737
|
+
summary.status = status;
|
|
738
|
+
if (code !== undefined)
|
|
739
|
+
summary.code = code;
|
|
740
|
+
if (respondMessage !== undefined)
|
|
741
|
+
summary.message = respondMessage;
|
|
742
|
+
if (meta !== undefined)
|
|
743
|
+
summary.meta = meta;
|
|
744
|
+
summary.payload = payload ?? null;
|
|
745
|
+
return {
|
|
746
|
+
status,
|
|
747
|
+
code,
|
|
748
|
+
message: respondMessage,
|
|
749
|
+
meta,
|
|
750
|
+
payload,
|
|
751
|
+
displayText: JSON.stringify(summary, null, 2),
|
|
752
|
+
};
|
|
753
|
+
};
|
|
542
754
|
const updateTestDeckRegistry = (list) => {
|
|
543
755
|
testDeckByPath.clear();
|
|
544
756
|
testDeckById.clear();
|
|
@@ -591,11 +803,11 @@ export function startWebSocketSimulator(opts) {
|
|
|
591
803
|
const fallbackToolInserts = [];
|
|
592
804
|
for (let i = 0; i < rawMessages.length; i++) {
|
|
593
805
|
const msg = rawMessages[i];
|
|
806
|
+
const refId = refs[i]?.id;
|
|
594
807
|
if (msg?.role === "assistant" || msg?.role === "user") {
|
|
595
808
|
const content = stringifyContent(msg.content).trim();
|
|
596
809
|
if (!content)
|
|
597
810
|
continue;
|
|
598
|
-
const refId = refs[i]?.id;
|
|
599
811
|
messages.push({
|
|
600
812
|
role: msg.role,
|
|
601
813
|
content,
|
|
@@ -604,6 +816,21 @@ export function startWebSocketSimulator(opts) {
|
|
|
604
816
|
});
|
|
605
817
|
continue;
|
|
606
818
|
}
|
|
819
|
+
const respondSummary = summarizeRespondCall(msg);
|
|
820
|
+
if (respondSummary) {
|
|
821
|
+
messages.push({
|
|
822
|
+
role: "assistant",
|
|
823
|
+
content: respondSummary.displayText,
|
|
824
|
+
messageRefId: refId,
|
|
825
|
+
feedback: refId ? feedbackByRef.get(refId) : undefined,
|
|
826
|
+
respondStatus: respondSummary.status,
|
|
827
|
+
respondCode: respondSummary.code,
|
|
828
|
+
respondMessage: respondSummary.message,
|
|
829
|
+
respondPayload: respondSummary.payload,
|
|
830
|
+
respondMeta: respondSummary.meta,
|
|
831
|
+
});
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
607
834
|
if (msg?.role === "tool") {
|
|
608
835
|
const actionCallId = typeof msg.tool_call_id === "string"
|
|
609
836
|
? msg.tool_call_id
|
|
@@ -624,6 +851,33 @@ export function startWebSocketSimulator(opts) {
|
|
|
624
851
|
: fallbackToolInserts,
|
|
625
852
|
};
|
|
626
853
|
};
|
|
854
|
+
const buildConversationMessages = (state) => {
|
|
855
|
+
const rawMessages = state.messages ?? [];
|
|
856
|
+
const conversation = [];
|
|
857
|
+
for (const msg of rawMessages) {
|
|
858
|
+
if (msg?.role === "assistant" || msg?.role === "user") {
|
|
859
|
+
const content = stringifyContent(msg.content).trim();
|
|
860
|
+
if (!content)
|
|
861
|
+
continue;
|
|
862
|
+
conversation.push({
|
|
863
|
+
role: msg.role,
|
|
864
|
+
content,
|
|
865
|
+
name: msg.name,
|
|
866
|
+
tool_calls: msg.tool_calls,
|
|
867
|
+
});
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
const respondSummary = summarizeRespondCall(msg);
|
|
871
|
+
if (respondSummary) {
|
|
872
|
+
conversation.push({
|
|
873
|
+
role: "assistant",
|
|
874
|
+
content: respondSummary.displayText,
|
|
875
|
+
name: GAMBIT_TOOL_RESPOND,
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return conversation;
|
|
880
|
+
};
|
|
627
881
|
const deriveToolInsertsFromTraces = (state, messageCount) => {
|
|
628
882
|
const traces = Array.isArray(state.traces) ? state.traces : [];
|
|
629
883
|
if (!traces.length)
|
|
@@ -668,6 +922,10 @@ export function startWebSocketSimulator(opts) {
|
|
|
668
922
|
: undefined;
|
|
669
923
|
if (sessionId)
|
|
670
924
|
run.sessionId = sessionId;
|
|
925
|
+
const initFill = state.meta
|
|
926
|
+
?.testBotInitFill;
|
|
927
|
+
if (initFill)
|
|
928
|
+
run.initFill = initFill;
|
|
671
929
|
run.traces = Array.isArray(state.traces) ? [...state.traces] : undefined;
|
|
672
930
|
};
|
|
673
931
|
const startTestBotRun = (runOpts = {}) => {
|
|
@@ -705,9 +963,27 @@ export function startWebSocketSimulator(opts) {
|
|
|
705
963
|
};
|
|
706
964
|
testBotRuns.set(runId, entry);
|
|
707
965
|
const run = entry.run;
|
|
966
|
+
if (runOpts.initFill)
|
|
967
|
+
run.initFill = runOpts.initFill;
|
|
708
968
|
let savedState = undefined;
|
|
709
969
|
let lastCount = 0;
|
|
710
970
|
const capturedTraces = [];
|
|
971
|
+
if (runOpts.initFillTrace) {
|
|
972
|
+
const actionCallId = randomId("initfill");
|
|
973
|
+
capturedTraces.push({
|
|
974
|
+
type: "tool.call",
|
|
975
|
+
runId,
|
|
976
|
+
actionCallId,
|
|
977
|
+
name: "gambit_test_bot_init_fill",
|
|
978
|
+
args: runOpts.initFillTrace.args,
|
|
979
|
+
}, {
|
|
980
|
+
type: "tool.result",
|
|
981
|
+
runId,
|
|
982
|
+
actionCallId,
|
|
983
|
+
name: "gambit_test_bot_init_fill",
|
|
984
|
+
result: runOpts.initFillTrace.result,
|
|
985
|
+
});
|
|
986
|
+
}
|
|
711
987
|
const setSessionId = (state) => {
|
|
712
988
|
const sessionId = typeof state?.meta?.sessionId === "string"
|
|
713
989
|
? state.meta.sessionId
|
|
@@ -763,6 +1039,7 @@ export function startWebSocketSimulator(opts) {
|
|
|
763
1039
|
},
|
|
764
1040
|
stream: Boolean(streamOpts?.onStreamText),
|
|
765
1041
|
onStreamText: streamOpts?.onStreamText,
|
|
1042
|
+
responsesMode: opts.responsesMode,
|
|
766
1043
|
});
|
|
767
1044
|
if (isGambitEndSignal(result)) {
|
|
768
1045
|
sessionEnded = true;
|
|
@@ -786,6 +1063,7 @@ export function startWebSocketSimulator(opts) {
|
|
|
786
1063
|
state: savedState,
|
|
787
1064
|
allowRootStringInput: true,
|
|
788
1065
|
initialUserMessage: initialUserMessage || undefined,
|
|
1066
|
+
responsesMode: opts.responsesMode,
|
|
789
1067
|
onStateUpdate: (state) => {
|
|
790
1068
|
const nextMeta = {
|
|
791
1069
|
...(savedState?.meta ?? {}),
|
|
@@ -794,6 +1072,7 @@ export function startWebSocketSimulator(opts) {
|
|
|
794
1072
|
testBotRunId: runId,
|
|
795
1073
|
testBotConfigPath: botConfigPath,
|
|
796
1074
|
testBotName,
|
|
1075
|
+
...(run.initFill ? { testBotInitFill: run.initFill } : {}),
|
|
797
1076
|
};
|
|
798
1077
|
const enriched = persistSessionState({
|
|
799
1078
|
...state,
|
|
@@ -845,6 +1124,7 @@ export function startWebSocketSimulator(opts) {
|
|
|
845
1124
|
state: savedState,
|
|
846
1125
|
allowRootStringInput: true,
|
|
847
1126
|
initialUserMessage: userMessage,
|
|
1127
|
+
responsesMode: opts.responsesMode,
|
|
848
1128
|
onStateUpdate: (state) => {
|
|
849
1129
|
const nextMeta = {
|
|
850
1130
|
...(savedState?.meta ?? {}),
|
|
@@ -853,6 +1133,7 @@ export function startWebSocketSimulator(opts) {
|
|
|
853
1133
|
testBotRunId: runId,
|
|
854
1134
|
testBotConfigPath: botConfigPath,
|
|
855
1135
|
testBotName,
|
|
1136
|
+
...(run.initFill ? { testBotInitFill: run.initFill } : {}),
|
|
856
1137
|
};
|
|
857
1138
|
const enriched = persistSessionState({
|
|
858
1139
|
...state,
|
|
@@ -911,10 +1192,60 @@ export function startWebSocketSimulator(opts) {
|
|
|
911
1192
|
broadcastTestBot({ type: "testBotStatus", run });
|
|
912
1193
|
return run;
|
|
913
1194
|
};
|
|
1195
|
+
const persistFailedInitFill = (args) => {
|
|
1196
|
+
const failedRunId = randomId("testbot");
|
|
1197
|
+
const testBotName = path.basename(args.botDeckPath).replace(/\.deck\.(md|ts)$/i, "");
|
|
1198
|
+
const actionCallId = randomId("initfill");
|
|
1199
|
+
const traces = [
|
|
1200
|
+
{
|
|
1201
|
+
type: "tool.call",
|
|
1202
|
+
runId: failedRunId,
|
|
1203
|
+
actionCallId,
|
|
1204
|
+
name: "gambit_test_bot_init_fill",
|
|
1205
|
+
args: { missing: args.initFill?.requested ?? [] },
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
type: "tool.result",
|
|
1209
|
+
runId: failedRunId,
|
|
1210
|
+
actionCallId,
|
|
1211
|
+
name: "gambit_test_bot_init_fill",
|
|
1212
|
+
result: {
|
|
1213
|
+
error: args.error,
|
|
1214
|
+
provided: args.initFill?.provided,
|
|
1215
|
+
},
|
|
1216
|
+
},
|
|
1217
|
+
];
|
|
1218
|
+
const failedState = persistSessionState({
|
|
1219
|
+
runId: failedRunId,
|
|
1220
|
+
messages: [],
|
|
1221
|
+
traces,
|
|
1222
|
+
meta: {
|
|
1223
|
+
testBot: true,
|
|
1224
|
+
testBotRunId: failedRunId,
|
|
1225
|
+
testBotConfigPath: args.botDeckPath,
|
|
1226
|
+
testBotName,
|
|
1227
|
+
testBotInitFill: args.initFill,
|
|
1228
|
+
testBotInitFillError: args.error,
|
|
1229
|
+
},
|
|
1230
|
+
});
|
|
1231
|
+
const sessionId = typeof failedState.meta?.sessionId === "string"
|
|
1232
|
+
? failedState.meta.sessionId
|
|
1233
|
+
: undefined;
|
|
1234
|
+
const sessionPath = typeof failedState.meta?.sessionStatePath === "string"
|
|
1235
|
+
? failedState.meta.sessionStatePath
|
|
1236
|
+
: undefined;
|
|
1237
|
+
if (sessionPath) {
|
|
1238
|
+
logger.warn(`[sim] init fill failed; session saved to ${sessionPath}`);
|
|
1239
|
+
}
|
|
1240
|
+
return { sessionId, sessionPath };
|
|
1241
|
+
};
|
|
914
1242
|
const deckLoadPromise = loadDeck(resolvedDeckPath)
|
|
915
1243
|
.then((deck) => {
|
|
916
1244
|
resolvedDeckPath = deck.path;
|
|
917
1245
|
deckSlug = deckSlugFromPath(resolvedDeckPath);
|
|
1246
|
+
deckLabel = typeof deck.label === "string"
|
|
1247
|
+
? deck.label
|
|
1248
|
+
: toDeckLabel(deck.path);
|
|
918
1249
|
availableTestDecks = (deck.testDecks ?? []).map((testDeck, index) => {
|
|
919
1250
|
const label = testDeck.label && typeof testDeck.label === "string"
|
|
920
1251
|
? testDeck.label
|
|
@@ -978,14 +1309,21 @@ export function startWebSocketSimulator(opts) {
|
|
|
978
1309
|
const wantsSourceMap = Boolean(opts.sourceMap);
|
|
979
1310
|
const bundlePlatform = opts.bundlePlatform ?? "deno";
|
|
980
1311
|
const autoBundle = opts.autoBundle ?? true;
|
|
1312
|
+
const forceBundle = opts.forceBundle ?? false;
|
|
981
1313
|
const needsBundle = !hasReactBundle() ||
|
|
982
1314
|
(wantsSourceMap && !hasReactBundleSourceMap()) ||
|
|
983
1315
|
isReactBundleStale();
|
|
984
|
-
const shouldAutoBundle = autoBundle && moduleLocation.isLocal &&
|
|
1316
|
+
const shouldAutoBundle = autoBundle && moduleLocation.isLocal &&
|
|
1317
|
+
(forceBundle || needsBundle);
|
|
985
1318
|
if (autoBundle && !moduleLocation.isLocal && opts.verbose) {
|
|
986
1319
|
logger.log("[sim] auto-bundle disabled for remote package; using packaged bundle.");
|
|
987
1320
|
}
|
|
1321
|
+
if (autoBundle && moduleLocation.isLocal && !shouldAutoBundle) {
|
|
1322
|
+
logger.log("[sim] auto-bundle enabled; bundle already up to date.");
|
|
1323
|
+
}
|
|
988
1324
|
if (shouldAutoBundle) {
|
|
1325
|
+
logger.log(`[sim] auto-bundle enabled; rebuilding simulator UI (${forceBundle ? "forced" : "stale"})...`);
|
|
1326
|
+
logger.log(`[sim] bundling simulator UI (${forceBundle ? "forced" : "stale"})...`);
|
|
989
1327
|
try {
|
|
990
1328
|
const p = new dntShim.Deno.Command("deno", {
|
|
991
1329
|
args: [
|
|
@@ -1012,6 +1350,28 @@ export function startWebSocketSimulator(opts) {
|
|
|
1012
1350
|
if (url.pathname.startsWith("/api/durable-streams/stream/")) {
|
|
1013
1351
|
return handleDurableStreamRequest(req);
|
|
1014
1352
|
}
|
|
1353
|
+
if (url.pathname === "/favicon.ico") {
|
|
1354
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1355
|
+
return new Response("Method not allowed", { status: 405 });
|
|
1356
|
+
}
|
|
1357
|
+
try {
|
|
1358
|
+
const data = await dntShim.Deno.readFile(simulatorFaviconDistPath);
|
|
1359
|
+
return new Response(req.method === "HEAD" ? null : data, {
|
|
1360
|
+
headers: { "content-type": "image/x-icon" },
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
catch {
|
|
1364
|
+
try {
|
|
1365
|
+
const data = await dntShim.Deno.readFile(simulatorFaviconSrcPath);
|
|
1366
|
+
return new Response(req.method === "HEAD" ? null : data, {
|
|
1367
|
+
headers: { "content-type": "image/x-icon" },
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
catch {
|
|
1371
|
+
return new Response("Not found", { status: 404 });
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1015
1375
|
if (url.pathname === "/api/calibrate") {
|
|
1016
1376
|
if (req.method !== "GET") {
|
|
1017
1377
|
return new Response("Method not allowed", { status: 405 });
|
|
@@ -1057,9 +1417,10 @@ export function startWebSocketSimulator(opts) {
|
|
|
1057
1417
|
delete next.gradingRuns;
|
|
1058
1418
|
return next;
|
|
1059
1419
|
})();
|
|
1420
|
+
const conversationMessages = buildConversationMessages(sessionState);
|
|
1060
1421
|
const sessionPayload = {
|
|
1061
|
-
messages:
|
|
1062
|
-
?
|
|
1422
|
+
messages: conversationMessages.length > 0
|
|
1423
|
+
? conversationMessages.map((msg) => ({
|
|
1063
1424
|
role: msg.role,
|
|
1064
1425
|
content: msg.content,
|
|
1065
1426
|
name: msg.name,
|
|
@@ -1092,7 +1453,7 @@ export function startWebSocketSimulator(opts) {
|
|
|
1092
1453
|
},
|
|
1093
1454
|
});
|
|
1094
1455
|
const sessionMeta = buildSessionMeta(sessionId, nextState);
|
|
1095
|
-
appendDurableStreamEvent(
|
|
1456
|
+
appendDurableStreamEvent(GRADE_STREAM_ID, {
|
|
1096
1457
|
type: "calibrateSession",
|
|
1097
1458
|
sessionId,
|
|
1098
1459
|
run: nextEntry,
|
|
@@ -1122,6 +1483,7 @@ export function startWebSocketSimulator(opts) {
|
|
|
1122
1483
|
allowRootStringInput: false,
|
|
1123
1484
|
initialUserMessage: undefined,
|
|
1124
1485
|
stream: false,
|
|
1486
|
+
responsesMode: opts.responsesMode,
|
|
1125
1487
|
});
|
|
1126
1488
|
}
|
|
1127
1489
|
const messages = sessionPayload.messages ?? [];
|
|
@@ -1161,6 +1523,7 @@ export function startWebSocketSimulator(opts) {
|
|
|
1161
1523
|
allowRootStringInput: false,
|
|
1162
1524
|
initialUserMessage: undefined,
|
|
1163
1525
|
stream: false,
|
|
1526
|
+
responsesMode: opts.responsesMode,
|
|
1164
1527
|
});
|
|
1165
1528
|
turns.push({
|
|
1166
1529
|
index: idx,
|
|
@@ -1263,7 +1626,7 @@ export function startWebSocketSimulator(opts) {
|
|
|
1263
1626
|
},
|
|
1264
1627
|
});
|
|
1265
1628
|
const sessionMeta = buildSessionMeta(body.sessionId, updated);
|
|
1266
|
-
appendDurableStreamEvent(
|
|
1629
|
+
appendDurableStreamEvent(GRADE_STREAM_ID, {
|
|
1267
1630
|
type: "calibrateSession",
|
|
1268
1631
|
sessionId: body.sessionId,
|
|
1269
1632
|
session: sessionMeta,
|
|
@@ -1316,7 +1679,7 @@ export function startWebSocketSimulator(opts) {
|
|
|
1316
1679
|
},
|
|
1317
1680
|
});
|
|
1318
1681
|
const sessionMeta = buildSessionMeta(body.sessionId, updated);
|
|
1319
|
-
appendDurableStreamEvent(
|
|
1682
|
+
appendDurableStreamEvent(GRADE_STREAM_ID, {
|
|
1320
1683
|
type: "calibrateSession",
|
|
1321
1684
|
sessionId: body.sessionId,
|
|
1322
1685
|
session: sessionMeta,
|
|
@@ -1403,7 +1766,7 @@ export function startWebSocketSimulator(opts) {
|
|
|
1403
1766
|
},
|
|
1404
1767
|
});
|
|
1405
1768
|
const sessionMeta = buildSessionMeta(body.sessionId, nextState);
|
|
1406
|
-
appendDurableStreamEvent(
|
|
1769
|
+
appendDurableStreamEvent(GRADE_STREAM_ID, {
|
|
1407
1770
|
type: "calibrateSession",
|
|
1408
1771
|
sessionId: body.sessionId,
|
|
1409
1772
|
run: nextRun,
|
|
@@ -1421,7 +1784,7 @@ export function startWebSocketSimulator(opts) {
|
|
|
1421
1784
|
}), { status: 400, headers: { "content-type": "application/json" } });
|
|
1422
1785
|
}
|
|
1423
1786
|
}
|
|
1424
|
-
if (url.pathname === "/api/test
|
|
1787
|
+
if (url.pathname === "/api/test") {
|
|
1425
1788
|
if (req.method === "GET") {
|
|
1426
1789
|
await deckLoadPromise.catch(() => null);
|
|
1427
1790
|
const requestedDeck = url.searchParams.get("deckPath");
|
|
@@ -1462,7 +1825,7 @@ export function startWebSocketSimulator(opts) {
|
|
|
1462
1825
|
}
|
|
1463
1826
|
return new Response("Method not allowed", { status: 405 });
|
|
1464
1827
|
}
|
|
1465
|
-
if (url.pathname === "/api/test
|
|
1828
|
+
if (url.pathname === "/api/test/run") {
|
|
1466
1829
|
if (req.method !== "POST") {
|
|
1467
1830
|
return new Response("Method not allowed", { status: 405 });
|
|
1468
1831
|
}
|
|
@@ -1471,17 +1834,28 @@ export function startWebSocketSimulator(opts) {
|
|
|
1471
1834
|
let botInput = undefined;
|
|
1472
1835
|
let initialUserMessage = undefined;
|
|
1473
1836
|
let botDeckSelection;
|
|
1837
|
+
let inheritBotInput = false;
|
|
1838
|
+
let userProvidedDeckInput = false;
|
|
1839
|
+
let initFillRequestMissing = undefined;
|
|
1474
1840
|
try {
|
|
1475
1841
|
const body = await req.json();
|
|
1476
1842
|
if (typeof body.maxTurns === "number" && Number.isFinite(body.maxTurns)) {
|
|
1477
1843
|
maxTurnsOverride = body.maxTurns;
|
|
1478
1844
|
}
|
|
1479
1845
|
deckInput = body.context ?? body.init;
|
|
1846
|
+
if (body.context !== undefined || body.init !== undefined) {
|
|
1847
|
+
userProvidedDeckInput = true;
|
|
1848
|
+
}
|
|
1480
1849
|
if (body.init !== undefined && body.context === undefined) {
|
|
1481
|
-
logger.warn('[gambit] Received deprecated "init" field in test
|
|
1850
|
+
logger.warn('[gambit] Received deprecated "init" field in test API; use "context" instead.');
|
|
1482
1851
|
}
|
|
1483
1852
|
botInput = body.botInput;
|
|
1484
|
-
|
|
1853
|
+
if (typeof body.inheritBotInput === "boolean") {
|
|
1854
|
+
inheritBotInput = body.inheritBotInput;
|
|
1855
|
+
}
|
|
1856
|
+
if (body.initFill && Array.isArray(body.initFill.missing)) {
|
|
1857
|
+
initFillRequestMissing = body.initFill.missing.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
1858
|
+
}
|
|
1485
1859
|
if (typeof body.botDeckPath === "string") {
|
|
1486
1860
|
const resolved = resolveTestDeck(body.botDeckPath);
|
|
1487
1861
|
if (!resolved) {
|
|
@@ -1514,19 +1888,175 @@ export function startWebSocketSimulator(opts) {
|
|
|
1514
1888
|
// ignore; keep undefined
|
|
1515
1889
|
}
|
|
1516
1890
|
}
|
|
1891
|
+
if (!userProvidedDeckInput && inheritBotInput && botInput !== undefined) {
|
|
1892
|
+
deckInput = cloneValue(botInput);
|
|
1893
|
+
}
|
|
1517
1894
|
if (!botDeckSelection) {
|
|
1518
1895
|
return new Response(JSON.stringify({ error: "No test decks configured" }), { status: 400, headers: { "content-type": "application/json" } });
|
|
1519
1896
|
}
|
|
1897
|
+
let initFillInfo;
|
|
1898
|
+
let initFillTrace;
|
|
1899
|
+
try {
|
|
1900
|
+
const rootDeck = await deckLoadPromise.catch(() => null);
|
|
1901
|
+
const rootSchema = rootDeck?.contextSchema ?? rootDeck?.inputSchema;
|
|
1902
|
+
const normalizedSchema = rootSchema
|
|
1903
|
+
? normalizeSchema(rootSchema)
|
|
1904
|
+
: undefined;
|
|
1905
|
+
const missing = normalizedSchema
|
|
1906
|
+
? findMissingRequiredFields(normalizedSchema, deckInput)
|
|
1907
|
+
: [];
|
|
1908
|
+
const requested = initFillRequestMissing?.length
|
|
1909
|
+
? missing.filter((entry) => initFillRequestMissing?.includes(entry))
|
|
1910
|
+
: missing;
|
|
1911
|
+
if (requested.length > 0) {
|
|
1912
|
+
const fillPrompt = buildInitFillPrompt({
|
|
1913
|
+
missing: requested,
|
|
1914
|
+
current: deckInput,
|
|
1915
|
+
schema: normalizedSchema,
|
|
1916
|
+
});
|
|
1917
|
+
const fillOutput = await runDeckWithFallback({
|
|
1918
|
+
path: botDeckSelection.path,
|
|
1919
|
+
input: botInput,
|
|
1920
|
+
inputProvided: botInput !== undefined,
|
|
1921
|
+
modelProvider: opts.modelProvider,
|
|
1922
|
+
allowRootStringInput: true,
|
|
1923
|
+
initialUserMessage: fillPrompt,
|
|
1924
|
+
responsesMode: opts.responsesMode,
|
|
1925
|
+
});
|
|
1926
|
+
const parsed = parseInitFillOutput(fillOutput);
|
|
1927
|
+
if (parsed.error) {
|
|
1928
|
+
initFillInfo = {
|
|
1929
|
+
requested,
|
|
1930
|
+
provided: fillOutput,
|
|
1931
|
+
error: parsed.error,
|
|
1932
|
+
};
|
|
1933
|
+
const failure = persistFailedInitFill({
|
|
1934
|
+
error: parsed.error,
|
|
1935
|
+
initFill: initFillInfo,
|
|
1936
|
+
botDeckPath: botDeckSelection.path,
|
|
1937
|
+
});
|
|
1938
|
+
return new Response(JSON.stringify({
|
|
1939
|
+
error: parsed.error,
|
|
1940
|
+
initFill: initFillInfo,
|
|
1941
|
+
sessionId: failure.sessionId,
|
|
1942
|
+
sessionPath: failure.sessionPath,
|
|
1943
|
+
}), {
|
|
1944
|
+
status: 400,
|
|
1945
|
+
headers: { "content-type": "application/json" },
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
let appliedObject = {};
|
|
1949
|
+
let appliedRoot = undefined;
|
|
1950
|
+
let nextInput = deckInput;
|
|
1951
|
+
for (const pathKey of requested) {
|
|
1952
|
+
const segments = pathKey === "(root)" ? [] : pathKey.split(".");
|
|
1953
|
+
const leafSchema = getSchemaAtPath(normalizedSchema, segments);
|
|
1954
|
+
const currentValue = getPathValue(nextInput, segments);
|
|
1955
|
+
if (currentValue !== undefined && currentValue !== null &&
|
|
1956
|
+
!(typeof currentValue === "string" &&
|
|
1957
|
+
(leafSchema?.kind === "string" ||
|
|
1958
|
+
leafSchema?.kind === "enum") &&
|
|
1959
|
+
currentValue.trim() === "") &&
|
|
1960
|
+
!(Array.isArray(currentValue) && leafSchema?.kind === "array" &&
|
|
1961
|
+
currentValue.length === 0)) {
|
|
1962
|
+
continue;
|
|
1963
|
+
}
|
|
1964
|
+
const fillValue = getPathValue(parsed.data, segments);
|
|
1965
|
+
if (fillValue === undefined)
|
|
1966
|
+
continue;
|
|
1967
|
+
if (segments.length === 0) {
|
|
1968
|
+
nextInput = fillValue;
|
|
1969
|
+
appliedRoot = fillValue;
|
|
1970
|
+
continue;
|
|
1971
|
+
}
|
|
1972
|
+
nextInput = setPathValue(nextInput, segments, fillValue);
|
|
1973
|
+
const appliedValue = setPathValue(appliedObject, segments, fillValue);
|
|
1974
|
+
if (appliedValue && typeof appliedValue === "object") {
|
|
1975
|
+
appliedObject = appliedValue;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
const validated = validateInitInput(rootSchema, nextInput);
|
|
1979
|
+
deckInput = validated;
|
|
1980
|
+
const remainingMissing = normalizedSchema
|
|
1981
|
+
? findMissingRequiredFields(normalizedSchema, deckInput)
|
|
1982
|
+
: [];
|
|
1983
|
+
if (remainingMissing.length > 0) {
|
|
1984
|
+
const message = `Init fill incomplete: missing ${remainingMissing.join(", ")}`;
|
|
1985
|
+
initFillInfo = {
|
|
1986
|
+
requested,
|
|
1987
|
+
applied: appliedRoot !== undefined
|
|
1988
|
+
? appliedRoot
|
|
1989
|
+
: Object.keys(appliedObject).length
|
|
1990
|
+
? appliedObject
|
|
1991
|
+
: undefined,
|
|
1992
|
+
provided: parsed.data,
|
|
1993
|
+
error: message,
|
|
1994
|
+
};
|
|
1995
|
+
const failure = persistFailedInitFill({
|
|
1996
|
+
error: message,
|
|
1997
|
+
initFill: initFillInfo,
|
|
1998
|
+
botDeckPath: botDeckSelection.path,
|
|
1999
|
+
});
|
|
2000
|
+
return new Response(JSON.stringify({
|
|
2001
|
+
error: message,
|
|
2002
|
+
initFill: initFillInfo,
|
|
2003
|
+
sessionId: failure.sessionId,
|
|
2004
|
+
sessionPath: failure.sessionPath,
|
|
2005
|
+
}), {
|
|
2006
|
+
status: 400,
|
|
2007
|
+
headers: { "content-type": "application/json" },
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
initFillInfo = {
|
|
2011
|
+
requested,
|
|
2012
|
+
applied: appliedRoot !== undefined
|
|
2013
|
+
? appliedRoot
|
|
2014
|
+
: Object.keys(appliedObject).length
|
|
2015
|
+
? appliedObject
|
|
2016
|
+
: undefined,
|
|
2017
|
+
provided: parsed.data,
|
|
2018
|
+
};
|
|
2019
|
+
initFillTrace = {
|
|
2020
|
+
args: {
|
|
2021
|
+
missing: requested,
|
|
2022
|
+
},
|
|
2023
|
+
result: {
|
|
2024
|
+
applied: initFillInfo.applied,
|
|
2025
|
+
provided: initFillInfo.provided,
|
|
2026
|
+
},
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
catch (err) {
|
|
2031
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2032
|
+
initFillInfo = initFillInfo ?? {
|
|
2033
|
+
requested: [],
|
|
2034
|
+
};
|
|
2035
|
+
initFillInfo.error = message;
|
|
2036
|
+
const failure = persistFailedInitFill({
|
|
2037
|
+
error: message,
|
|
2038
|
+
initFill: initFillInfo,
|
|
2039
|
+
botDeckPath: botDeckSelection.path,
|
|
2040
|
+
});
|
|
2041
|
+
return new Response(JSON.stringify({
|
|
2042
|
+
error: message,
|
|
2043
|
+
initFill: initFillInfo,
|
|
2044
|
+
sessionId: failure.sessionId,
|
|
2045
|
+
sessionPath: failure.sessionPath,
|
|
2046
|
+
}), { status: 400, headers: { "content-type": "application/json" } });
|
|
2047
|
+
}
|
|
1520
2048
|
const run = startTestBotRun({
|
|
1521
2049
|
maxTurnsOverride,
|
|
1522
2050
|
deckInput,
|
|
1523
2051
|
botInput,
|
|
1524
2052
|
initialUserMessage,
|
|
1525
2053
|
botDeckPath: botDeckSelection.path,
|
|
2054
|
+
initFill: initFillInfo,
|
|
2055
|
+
initFillTrace,
|
|
1526
2056
|
});
|
|
1527
2057
|
return new Response(JSON.stringify({ run }), { headers: { "content-type": "application/json" } });
|
|
1528
2058
|
}
|
|
1529
|
-
if (url.pathname === "/api/test
|
|
2059
|
+
if (url.pathname === "/api/test/status") {
|
|
1530
2060
|
const runId = url.searchParams.get("runId") ?? undefined;
|
|
1531
2061
|
const sessionId = url.searchParams.get("sessionId") ?? undefined;
|
|
1532
2062
|
let entry = runId ? testBotRuns.get(runId) : undefined;
|
|
@@ -1599,7 +2129,7 @@ export function startWebSocketSimulator(opts) {
|
|
|
1599
2129
|
testDecks: availableTestDecks,
|
|
1600
2130
|
}), { headers: { "content-type": "application/json" } });
|
|
1601
2131
|
}
|
|
1602
|
-
if (url.pathname === "/api/test
|
|
2132
|
+
if (url.pathname === "/api/test/stop") {
|
|
1603
2133
|
if (req.method !== "POST") {
|
|
1604
2134
|
return new Response("Method not allowed", { status: 405 });
|
|
1605
2135
|
}
|
|
@@ -1700,6 +2230,7 @@ export function startWebSocketSimulator(opts) {
|
|
|
1700
2230
|
trace: tracer,
|
|
1701
2231
|
stream,
|
|
1702
2232
|
state: simulatorSavedState,
|
|
2233
|
+
responsesMode: opts.responsesMode,
|
|
1703
2234
|
onStateUpdate: (state) => {
|
|
1704
2235
|
const nextMeta = {
|
|
1705
2236
|
...(simulatorSavedState?.meta ?? {}),
|
|
@@ -2163,20 +2694,28 @@ export function startWebSocketSimulator(opts) {
|
|
|
2163
2694
|
url.pathname.startsWith("/debug") ||
|
|
2164
2695
|
url.pathname.startsWith("/editor") ||
|
|
2165
2696
|
url.pathname.startsWith("/docs") ||
|
|
2166
|
-
url.pathname.startsWith("/test
|
|
2167
|
-
url.pathname.startsWith("/
|
|
2697
|
+
url.pathname.startsWith("/test") ||
|
|
2698
|
+
url.pathname.startsWith("/grade")) {
|
|
2168
2699
|
const hasBundle = await canServeReactBundle();
|
|
2169
2700
|
if (!hasBundle) {
|
|
2170
2701
|
return new Response("Simulator UI bundle missing. Run `deno task bundle:sim` (or start with `--bundle`).", { status: 500 });
|
|
2171
2702
|
}
|
|
2172
|
-
|
|
2703
|
+
await deckLoadPromise.catch(() => null);
|
|
2704
|
+
const resolvedLabel = deckLabel ?? toDeckLabel(resolvedDeckPath);
|
|
2705
|
+
return new Response(simulatorReactHtml(resolvedDeckPath, resolvedLabel), {
|
|
2173
2706
|
headers: { "content-type": "text/html; charset=utf-8" },
|
|
2174
2707
|
});
|
|
2175
2708
|
}
|
|
2176
2709
|
if (url.pathname === "/schema") {
|
|
2177
2710
|
const desc = await schemaPromise;
|
|
2711
|
+
const deck = await deckLoadPromise.catch(() => null);
|
|
2712
|
+
const startMode = deck &&
|
|
2713
|
+
(deck.startMode === "assistant" || deck.startMode === "user")
|
|
2714
|
+
? deck.startMode
|
|
2715
|
+
: undefined;
|
|
2178
2716
|
return new Response(JSON.stringify({
|
|
2179
2717
|
deck: resolvedDeckPath,
|
|
2718
|
+
startMode,
|
|
2180
2719
|
...desc,
|
|
2181
2720
|
}), {
|
|
2182
2721
|
headers: { "content-type": "application/json; charset=utf-8" },
|
|
@@ -2270,18 +2809,58 @@ function hasReactBundleSourceMap() {
|
|
|
2270
2809
|
return false;
|
|
2271
2810
|
}
|
|
2272
2811
|
}
|
|
2812
|
+
function newestMtimeInDir(dirPath) {
|
|
2813
|
+
const stack = [dirPath];
|
|
2814
|
+
let newest = undefined;
|
|
2815
|
+
while (stack.length > 0) {
|
|
2816
|
+
const current = stack.pop();
|
|
2817
|
+
if (!current)
|
|
2818
|
+
continue;
|
|
2819
|
+
let entries;
|
|
2820
|
+
try {
|
|
2821
|
+
entries = Array.from(dntShim.Deno.readDirSync(current));
|
|
2822
|
+
}
|
|
2823
|
+
catch {
|
|
2824
|
+
continue;
|
|
2825
|
+
}
|
|
2826
|
+
for (const entry of entries) {
|
|
2827
|
+
const entryPath = path.join(current, entry.name);
|
|
2828
|
+
if (entry.isDirectory) {
|
|
2829
|
+
stack.push(entryPath);
|
|
2830
|
+
continue;
|
|
2831
|
+
}
|
|
2832
|
+
if (!entry.isFile)
|
|
2833
|
+
continue;
|
|
2834
|
+
try {
|
|
2835
|
+
const stat = dntShim.Deno.statSync(entryPath);
|
|
2836
|
+
if (!stat.isFile)
|
|
2837
|
+
continue;
|
|
2838
|
+
const mtime = stat.mtime?.getTime();
|
|
2839
|
+
if (typeof mtime !== "number")
|
|
2840
|
+
continue;
|
|
2841
|
+
newest = newest === undefined ? mtime : Math.max(newest, mtime);
|
|
2842
|
+
}
|
|
2843
|
+
catch {
|
|
2844
|
+
continue;
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
return newest;
|
|
2849
|
+
}
|
|
2273
2850
|
function isReactBundleStale() {
|
|
2274
2851
|
try {
|
|
2275
2852
|
const bundleStat = dntShim.Deno.statSync(simulatorBundlePath);
|
|
2276
|
-
|
|
2277
|
-
if (!bundleStat.isFile || !entryStat.isFile)
|
|
2853
|
+
if (!bundleStat.isFile)
|
|
2278
2854
|
return false;
|
|
2279
2855
|
const bundleTime = bundleStat.mtime?.getTime();
|
|
2280
|
-
|
|
2281
|
-
if (typeof bundleTime !== "number" || typeof entryTime !== "number") {
|
|
2856
|
+
if (typeof bundleTime !== "number") {
|
|
2282
2857
|
return false;
|
|
2283
2858
|
}
|
|
2284
|
-
|
|
2859
|
+
const srcRoot = path.resolve(moduleDir, "..", "simulator-ui", "src");
|
|
2860
|
+
const newestSource = newestMtimeInDir(srcRoot);
|
|
2861
|
+
if (typeof newestSource !== "number")
|
|
2862
|
+
return false;
|
|
2863
|
+
return newestSource > bundleTime;
|
|
2285
2864
|
}
|
|
2286
2865
|
catch {
|
|
2287
2866
|
return false;
|
|
@@ -2340,8 +2919,9 @@ async function readRemoteBundle(url, kind) {
|
|
|
2340
2919
|
return null;
|
|
2341
2920
|
}
|
|
2342
2921
|
}
|
|
2343
|
-
function simulatorReactHtml(deckPath) {
|
|
2344
|
-
const
|
|
2922
|
+
function simulatorReactHtml(deckPath, deckLabel) {
|
|
2923
|
+
const safeDeckPath = deckPath.replaceAll("<", "<").replaceAll(">", ">");
|
|
2924
|
+
const safeDeckLabel = deckLabel?.replaceAll("<", "<").replaceAll(">", ">") ?? null;
|
|
2345
2925
|
const bundleStamp = (() => {
|
|
2346
2926
|
try {
|
|
2347
2927
|
const stat = dntShim.Deno.statSync(simulatorBundlePath);
|
|
@@ -2369,7 +2949,8 @@ function simulatorReactHtml(deckPath) {
|
|
|
2369
2949
|
<body>
|
|
2370
2950
|
<div id="root"></div>
|
|
2371
2951
|
<script>
|
|
2372
|
-
window.__GAMBIT_DECK_PATH__ = ${JSON.stringify(
|
|
2952
|
+
window.__GAMBIT_DECK_PATH__ = ${JSON.stringify(safeDeckPath)};
|
|
2953
|
+
window.__GAMBIT_DECK_LABEL__ = ${JSON.stringify(safeDeckLabel)};
|
|
2373
2954
|
</script>
|
|
2374
2955
|
<script type="module" src="${bundleUrl}"></script>
|
|
2375
2956
|
</body>
|
|
@@ -2413,6 +2994,7 @@ async function runDeckWithFallback(args) {
|
|
|
2413
2994
|
onStateUpdate: args.onStateUpdate,
|
|
2414
2995
|
stream: args.stream,
|
|
2415
2996
|
onStreamText: args.onStreamText,
|
|
2997
|
+
responsesMode: args.responsesMode,
|
|
2416
2998
|
});
|
|
2417
2999
|
}
|
|
2418
3000
|
catch (error) {
|
|
@@ -2428,6 +3010,7 @@ async function runDeckWithFallback(args) {
|
|
|
2428
3010
|
onStateUpdate: args.onStateUpdate,
|
|
2429
3011
|
stream: args.stream,
|
|
2430
3012
|
onStreamText: args.onStreamText,
|
|
3013
|
+
responsesMode: args.responsesMode,
|
|
2431
3014
|
});
|
|
2432
3015
|
}
|
|
2433
3016
|
throw error;
|