@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +82 -2
  2. package/README.md +31 -9
  3. package/esm/gambit/simulator-ui/dist/bundle.js +4744 -4360
  4. package/esm/gambit/simulator-ui/dist/bundle.js.map +4 -4
  5. package/esm/gambit/simulator-ui/dist/favicon.ico +0 -0
  6. package/esm/mod.d.ts +7 -3
  7. package/esm/mod.d.ts.map +1 -1
  8. package/esm/mod.js +5 -1
  9. package/esm/src/cli_utils.d.ts +3 -2
  10. package/esm/src/cli_utils.d.ts.map +1 -1
  11. package/esm/src/cli_utils.js +43 -27
  12. package/esm/src/openai_compat.d.ts +63 -0
  13. package/esm/src/openai_compat.d.ts.map +1 -0
  14. package/esm/src/openai_compat.js +277 -0
  15. package/esm/src/providers/google.d.ts +16 -0
  16. package/esm/src/providers/google.d.ts.map +1 -0
  17. package/esm/src/providers/google.js +352 -0
  18. package/esm/src/providers/ollama.d.ts +17 -0
  19. package/esm/src/providers/ollama.d.ts.map +1 -0
  20. package/esm/src/providers/ollama.js +509 -0
  21. package/esm/src/providers/openrouter.d.ts +14 -1
  22. package/esm/src/providers/openrouter.d.ts.map +1 -1
  23. package/esm/src/providers/openrouter.js +460 -463
  24. package/esm/src/server.d.ts +4 -0
  25. package/esm/src/server.d.ts.map +1 -1
  26. package/esm/src/server.js +623 -164
  27. package/esm/src/trace.d.ts.map +1 -1
  28. package/esm/src/trace.js +3 -6
  29. package/package.json +2 -2
  30. package/script/gambit/simulator-ui/dist/bundle.js +4744 -4360
  31. package/script/gambit/simulator-ui/dist/bundle.js.map +4 -4
  32. package/script/gambit/simulator-ui/dist/favicon.ico +0 -0
  33. package/script/mod.d.ts +7 -3
  34. package/script/mod.d.ts.map +1 -1
  35. package/script/mod.js +9 -3
  36. package/script/src/cli_utils.d.ts +3 -2
  37. package/script/src/cli_utils.d.ts.map +1 -1
  38. package/script/src/cli_utils.js +42 -26
  39. package/script/src/openai_compat.d.ts +63 -0
  40. package/script/src/openai_compat.d.ts.map +1 -0
  41. package/script/src/openai_compat.js +281 -0
  42. package/script/src/providers/google.d.ts +16 -0
  43. package/script/src/providers/google.d.ts.map +1 -0
  44. package/script/src/providers/google.js +359 -0
  45. package/script/src/providers/ollama.d.ts +17 -0
  46. package/script/src/providers/ollama.d.ts.map +1 -0
  47. package/script/src/providers/ollama.js +551 -0
  48. package/script/src/providers/openrouter.d.ts +14 -1
  49. package/script/src/providers/openrouter.d.ts.map +1 -1
  50. package/script/src/providers/openrouter.js +461 -463
  51. package/script/src/server.d.ts +4 -0
  52. package/script/src/server.d.ts.map +1 -1
  53. package/script/src/server.js +623 -164
  54. package/script/src/trace.d.ts.map +1 -1
  55. package/script/src/trace.js +3 -6
  56. package/esm/src/compat/openai.d.ts +0 -2
  57. package/esm/src/compat/openai.d.ts.map +0 -1
  58. package/esm/src/compat/openai.js +0 -1
  59. package/script/src/compat/openai.d.ts +0 -2
  60. package/script/src/compat/openai.d.ts.map +0 -1
  61. package/script/src/compat/openai.js +0 -5
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 TEST_BOT_STREAM_ID = "gambit-test-bot";
54
- const CALIBRATE_STREAM_ID = "gambit-calibrate";
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();
@@ -62,87 +64,6 @@ function randomId(prefix) {
62
64
  const suffix = crypto.randomUUID().replace(/-/g, "").slice(0, 24);
63
65
  return `${prefix}-${suffix}`;
64
66
  }
65
- async function parseOpenResponseRequest(req) {
66
- try {
67
- return await req.json();
68
- }
69
- catch {
70
- return null;
71
- }
72
- }
73
- function formatOpenResponseSseEvent(event) {
74
- return `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`;
75
- }
76
- function formatOpenResponseDoneEvent() {
77
- return "data: [DONE]\n\n";
78
- }
79
- function createOpenResponseStream(req, provider, payload) {
80
- const encoder = new TextEncoder();
81
- const stream = new ReadableStream({
82
- start(controller) {
83
- let closed = false;
84
- let completed = false;
85
- const close = () => {
86
- if (closed)
87
- return;
88
- closed = true;
89
- controller.close();
90
- };
91
- const send = (chunk) => {
92
- if (closed)
93
- return;
94
- controller.enqueue(encoder.encode(chunk));
95
- };
96
- const sendDone = () => {
97
- if (closed)
98
- return;
99
- send(formatOpenResponseDoneEvent());
100
- close();
101
- };
102
- const sendEvent = (event) => {
103
- if (closed)
104
- return;
105
- send(formatOpenResponseSseEvent(event));
106
- if (event.type === "response.completed") {
107
- completed = true;
108
- sendDone();
109
- }
110
- };
111
- req.signal.addEventListener("abort", () => {
112
- close();
113
- });
114
- (async () => {
115
- try {
116
- const response = await provider.responses({
117
- ...payload,
118
- stream: true,
119
- onStreamEvent: sendEvent,
120
- });
121
- if (!completed) {
122
- sendEvent({ type: "response.completed", response });
123
- sendDone();
124
- }
125
- }
126
- catch (err) {
127
- const message = err instanceof Error ? err.message : String(err);
128
- sendEvent({
129
- type: "error",
130
- error: { code: "server_error", message },
131
- });
132
- sendDone();
133
- }
134
- })();
135
- },
136
- });
137
- return new Response(stream, {
138
- status: 200,
139
- headers: {
140
- "Content-Type": "text/event-stream",
141
- "Cache-Control": "no-cache",
142
- "Connection": "keep-alive",
143
- },
144
- });
145
- }
146
67
  function resolveDefaultValue(raw) {
147
68
  if (typeof raw === "function") {
148
69
  try {
@@ -388,11 +309,170 @@ function deriveInitialFromSchema(schema) {
388
309
  return undefined;
389
310
  }
390
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
+ }
391
468
  /**
392
469
  * Start the WebSocket simulator server used by the Gambit debug UI.
393
470
  */
394
471
  export function startWebSocketSimulator(opts) {
395
472
  const port = opts.port ?? 8000;
473
+ const initialContext = opts.initialContext;
474
+ const hasInitialContext = opts.contextProvided ??
475
+ (initialContext !== undefined);
396
476
  const consoleTracer = opts.verbose ? makeConsoleTracer() : undefined;
397
477
  let resolvedDeckPath = resolveDeckPath(opts.deckPath);
398
478
  const sessionsRoot = (() => {
@@ -423,9 +503,10 @@ export function startWebSocketSimulator(opts) {
423
503
  };
424
504
  const testBotRuns = new Map();
425
505
  const broadcastTestBot = (payload) => {
426
- appendDurableStreamEvent(TEST_BOT_STREAM_ID, payload);
506
+ appendDurableStreamEvent(TEST_STREAM_ID, payload);
427
507
  };
428
508
  let deckSlug = deckSlugFromPath(resolvedDeckPath);
509
+ let deckLabel = undefined;
429
510
  const enrichStateWithSession = (state) => {
430
511
  const meta = { ...(state.meta ?? {}) };
431
512
  const now = new Date();
@@ -610,12 +691,6 @@ export function startWebSocketSimulator(opts) {
610
691
  return "";
611
692
  if (typeof value === "string")
612
693
  return value;
613
- if (Array.isArray(value)) {
614
- return value
615
- .map((part) => typeof part === "string" ? part : part.text ??
616
- "")
617
- .join("");
618
- }
619
694
  try {
620
695
  return JSON.stringify(value);
621
696
  }
@@ -623,6 +698,59 @@ export function startWebSocketSimulator(opts) {
623
698
  return String(value);
624
699
  }
625
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
+ };
626
754
  const updateTestDeckRegistry = (list) => {
627
755
  testDeckByPath.clear();
628
756
  testDeckById.clear();
@@ -675,12 +803,11 @@ export function startWebSocketSimulator(opts) {
675
803
  const fallbackToolInserts = [];
676
804
  for (let i = 0; i < rawMessages.length; i++) {
677
805
  const msg = rawMessages[i];
678
- if (msg?.type === "message" &&
679
- (msg.role === "assistant" || msg.role === "user")) {
806
+ const refId = refs[i]?.id;
807
+ if (msg?.role === "assistant" || msg?.role === "user") {
680
808
  const content = stringifyContent(msg.content).trim();
681
809
  if (!content)
682
810
  continue;
683
- const refId = refs[i]?.id;
684
811
  messages.push({
685
812
  role: msg.role,
686
813
  content,
@@ -689,7 +816,22 @@ export function startWebSocketSimulator(opts) {
689
816
  });
690
817
  continue;
691
818
  }
692
- if (msg?.type === "message" && msg.role === "tool") {
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
+ }
834
+ if (msg?.role === "tool") {
693
835
  const actionCallId = typeof msg.tool_call_id === "string"
694
836
  ? msg.tool_call_id
695
837
  : undefined;
@@ -709,6 +851,33 @@ export function startWebSocketSimulator(opts) {
709
851
  : fallbackToolInserts,
710
852
  };
711
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
+ };
712
881
  const deriveToolInsertsFromTraces = (state, messageCount) => {
713
882
  const traces = Array.isArray(state.traces) ? state.traces : [];
714
883
  if (!traces.length)
@@ -753,6 +922,10 @@ export function startWebSocketSimulator(opts) {
753
922
  : undefined;
754
923
  if (sessionId)
755
924
  run.sessionId = sessionId;
925
+ const initFill = state.meta
926
+ ?.testBotInitFill;
927
+ if (initFill)
928
+ run.initFill = initFill;
756
929
  run.traces = Array.isArray(state.traces) ? [...state.traces] : undefined;
757
930
  };
758
931
  const startTestBotRun = (runOpts = {}) => {
@@ -790,9 +963,27 @@ export function startWebSocketSimulator(opts) {
790
963
  };
791
964
  testBotRuns.set(runId, entry);
792
965
  const run = entry.run;
966
+ if (runOpts.initFill)
967
+ run.initFill = runOpts.initFill;
793
968
  let savedState = undefined;
794
969
  let lastCount = 0;
795
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
+ }
796
987
  const setSessionId = (state) => {
797
988
  const sessionId = typeof state?.meta?.sessionId === "string"
798
989
  ? state.meta.sessionId
@@ -825,7 +1016,7 @@ export function startWebSocketSimulator(opts) {
825
1016
  const getLastAssistantMessage = (history) => {
826
1017
  for (let i = history.length - 1; i >= 0; i--) {
827
1018
  const msg = history[i];
828
- if (msg?.type === "message" && msg.role === "assistant") {
1019
+ if (msg?.role === "assistant") {
829
1020
  return stringifyContent(msg.content);
830
1021
  }
831
1022
  }
@@ -848,6 +1039,7 @@ export function startWebSocketSimulator(opts) {
848
1039
  },
849
1040
  stream: Boolean(streamOpts?.onStreamText),
850
1041
  onStreamText: streamOpts?.onStreamText,
1042
+ responsesMode: opts.responsesMode,
851
1043
  });
852
1044
  if (isGambitEndSignal(result)) {
853
1045
  sessionEnded = true;
@@ -871,6 +1063,7 @@ export function startWebSocketSimulator(opts) {
871
1063
  state: savedState,
872
1064
  allowRootStringInput: true,
873
1065
  initialUserMessage: initialUserMessage || undefined,
1066
+ responsesMode: opts.responsesMode,
874
1067
  onStateUpdate: (state) => {
875
1068
  const nextMeta = {
876
1069
  ...(savedState?.meta ?? {}),
@@ -879,6 +1072,7 @@ export function startWebSocketSimulator(opts) {
879
1072
  testBotRunId: runId,
880
1073
  testBotConfigPath: botConfigPath,
881
1074
  testBotName,
1075
+ ...(run.initFill ? { testBotInitFill: run.initFill } : {}),
882
1076
  };
883
1077
  const enriched = persistSessionState({
884
1078
  ...state,
@@ -930,6 +1124,7 @@ export function startWebSocketSimulator(opts) {
930
1124
  state: savedState,
931
1125
  allowRootStringInput: true,
932
1126
  initialUserMessage: userMessage,
1127
+ responsesMode: opts.responsesMode,
933
1128
  onStateUpdate: (state) => {
934
1129
  const nextMeta = {
935
1130
  ...(savedState?.meta ?? {}),
@@ -938,6 +1133,7 @@ export function startWebSocketSimulator(opts) {
938
1133
  testBotRunId: runId,
939
1134
  testBotConfigPath: botConfigPath,
940
1135
  testBotName,
1136
+ ...(run.initFill ? { testBotInitFill: run.initFill } : {}),
941
1137
  };
942
1138
  const enriched = persistSessionState({
943
1139
  ...state,
@@ -996,10 +1192,60 @@ export function startWebSocketSimulator(opts) {
996
1192
  broadcastTestBot({ type: "testBotStatus", run });
997
1193
  return run;
998
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
+ };
999
1242
  const deckLoadPromise = loadDeck(resolvedDeckPath)
1000
1243
  .then((deck) => {
1001
1244
  resolvedDeckPath = deck.path;
1002
1245
  deckSlug = deckSlugFromPath(resolvedDeckPath);
1246
+ deckLabel = typeof deck.label === "string"
1247
+ ? deck.label
1248
+ : toDeckLabel(deck.path);
1003
1249
  availableTestDecks = (deck.testDecks ?? []).map((testDeck, index) => {
1004
1250
  const label = testDeck.label && typeof testDeck.label === "string"
1005
1251
  ? testDeck.label
@@ -1046,8 +1292,14 @@ export function startWebSocketSimulator(opts) {
1046
1292
  return null;
1047
1293
  });
1048
1294
  const schemaPromise = deckLoadPromise
1049
- .then((deck) => deck ? describeZodSchema(deck.inputSchema) : {
1050
- error: "Deck failed to load",
1295
+ .then((deck) => {
1296
+ const desc = deck ? describeZodSchema(deck.inputSchema) : {
1297
+ error: "Deck failed to load",
1298
+ };
1299
+ if (hasInitialContext) {
1300
+ return { ...desc, defaults: initialContext };
1301
+ }
1302
+ return desc;
1051
1303
  })
1052
1304
  .catch((err) => {
1053
1305
  const message = err instanceof Error ? err.message : String(err);
@@ -1057,14 +1309,21 @@ export function startWebSocketSimulator(opts) {
1057
1309
  const wantsSourceMap = Boolean(opts.sourceMap);
1058
1310
  const bundlePlatform = opts.bundlePlatform ?? "deno";
1059
1311
  const autoBundle = opts.autoBundle ?? true;
1312
+ const forceBundle = opts.forceBundle ?? false;
1060
1313
  const needsBundle = !hasReactBundle() ||
1061
1314
  (wantsSourceMap && !hasReactBundleSourceMap()) ||
1062
1315
  isReactBundleStale();
1063
- const shouldAutoBundle = autoBundle && moduleLocation.isLocal && needsBundle;
1316
+ const shouldAutoBundle = autoBundle && moduleLocation.isLocal &&
1317
+ (forceBundle || needsBundle);
1064
1318
  if (autoBundle && !moduleLocation.isLocal && opts.verbose) {
1065
1319
  logger.log("[sim] auto-bundle disabled for remote package; using packaged bundle.");
1066
1320
  }
1321
+ if (autoBundle && moduleLocation.isLocal && !shouldAutoBundle) {
1322
+ logger.log("[sim] auto-bundle enabled; bundle already up to date.");
1323
+ }
1067
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"})...`);
1068
1327
  try {
1069
1328
  const p = new dntShim.Deno.Command("deno", {
1070
1329
  args: [
@@ -1088,46 +1347,31 @@ export function startWebSocketSimulator(opts) {
1088
1347
  }
1089
1348
  const server = dntShim.Deno.serve({ port, signal: opts.signal, onListen: () => { } }, async (req) => {
1090
1349
  const url = new URL(req.url);
1091
- if (url.pathname === "/v1/responses") {
1092
- if (req.method !== "POST") {
1350
+ if (url.pathname.startsWith("/api/durable-streams/stream/")) {
1351
+ return handleDurableStreamRequest(req);
1352
+ }
1353
+ if (url.pathname === "/favicon.ico") {
1354
+ if (req.method !== "GET" && req.method !== "HEAD") {
1093
1355
  return new Response("Method not allowed", { status: 405 });
1094
1356
  }
1095
- const payload = await parseOpenResponseRequest(req);
1096
- if (!payload) {
1097
- return new Response("Invalid JSON payload", { status: 400 });
1098
- }
1099
- const model = payload.model ?? opts.model;
1100
- if (!model) {
1101
- return new Response("Missing model", { status: 400 });
1102
- }
1103
- const requestPayload = {
1104
- ...payload,
1105
- model,
1106
- input: payload.input ?? null,
1107
- };
1108
- if (payload.stream) {
1109
- return createOpenResponseStream(req, opts.modelProvider, requestPayload);
1110
- }
1111
1357
  try {
1112
- const response = await opts.modelProvider.responses({
1113
- ...requestPayload,
1114
- stream: false,
1115
- });
1116
- return new Response(JSON.stringify(response), {
1117
- headers: { "content-type": "application/json" },
1358
+ const data = await dntShim.Deno.readFile(simulatorFaviconDistPath);
1359
+ return new Response(req.method === "HEAD" ? null : data, {
1360
+ headers: { "content-type": "image/x-icon" },
1118
1361
  });
1119
1362
  }
1120
- catch (err) {
1121
- const message = err instanceof Error ? err.message : String(err);
1122
- return new Response(JSON.stringify({ error: message }), {
1123
- status: 500,
1124
- headers: { "content-type": "application/json" },
1125
- });
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
+ }
1126
1373
  }
1127
1374
  }
1128
- if (url.pathname.startsWith("/api/durable-streams/stream/")) {
1129
- return handleDurableStreamRequest(req);
1130
- }
1131
1375
  if (url.pathname === "/api/calibrate") {
1132
1376
  if (req.method !== "GET") {
1133
1377
  return new Response("Method not allowed", { status: 405 });
@@ -1173,11 +1417,10 @@ export function startWebSocketSimulator(opts) {
1173
1417
  delete next.gradingRuns;
1174
1418
  return next;
1175
1419
  })();
1420
+ const conversationMessages = buildConversationMessages(sessionState);
1176
1421
  const sessionPayload = {
1177
- messages: Array.isArray(sessionState.messages)
1178
- ? sessionState.messages
1179
- .filter((msg) => msg.type === "message")
1180
- .map((msg) => ({
1422
+ messages: conversationMessages.length > 0
1423
+ ? conversationMessages.map((msg) => ({
1181
1424
  role: msg.role,
1182
1425
  content: msg.content,
1183
1426
  name: msg.name,
@@ -1210,7 +1453,7 @@ export function startWebSocketSimulator(opts) {
1210
1453
  },
1211
1454
  });
1212
1455
  const sessionMeta = buildSessionMeta(sessionId, nextState);
1213
- appendDurableStreamEvent(CALIBRATE_STREAM_ID, {
1456
+ appendDurableStreamEvent(GRADE_STREAM_ID, {
1214
1457
  type: "calibrateSession",
1215
1458
  sessionId,
1216
1459
  run: nextEntry,
@@ -1240,6 +1483,7 @@ export function startWebSocketSimulator(opts) {
1240
1483
  allowRootStringInput: false,
1241
1484
  initialUserMessage: undefined,
1242
1485
  stream: false,
1486
+ responsesMode: opts.responsesMode,
1243
1487
  });
1244
1488
  }
1245
1489
  const messages = sessionPayload.messages ?? [];
@@ -1279,6 +1523,7 @@ export function startWebSocketSimulator(opts) {
1279
1523
  allowRootStringInput: false,
1280
1524
  initialUserMessage: undefined,
1281
1525
  stream: false,
1526
+ responsesMode: opts.responsesMode,
1282
1527
  });
1283
1528
  turns.push({
1284
1529
  index: idx,
@@ -1381,7 +1626,7 @@ export function startWebSocketSimulator(opts) {
1381
1626
  },
1382
1627
  });
1383
1628
  const sessionMeta = buildSessionMeta(body.sessionId, updated);
1384
- appendDurableStreamEvent(CALIBRATE_STREAM_ID, {
1629
+ appendDurableStreamEvent(GRADE_STREAM_ID, {
1385
1630
  type: "calibrateSession",
1386
1631
  sessionId: body.sessionId,
1387
1632
  session: sessionMeta,
@@ -1434,7 +1679,7 @@ export function startWebSocketSimulator(opts) {
1434
1679
  },
1435
1680
  });
1436
1681
  const sessionMeta = buildSessionMeta(body.sessionId, updated);
1437
- appendDurableStreamEvent(CALIBRATE_STREAM_ID, {
1682
+ appendDurableStreamEvent(GRADE_STREAM_ID, {
1438
1683
  type: "calibrateSession",
1439
1684
  sessionId: body.sessionId,
1440
1685
  session: sessionMeta,
@@ -1521,7 +1766,7 @@ export function startWebSocketSimulator(opts) {
1521
1766
  },
1522
1767
  });
1523
1768
  const sessionMeta = buildSessionMeta(body.sessionId, nextState);
1524
- appendDurableStreamEvent(CALIBRATE_STREAM_ID, {
1769
+ appendDurableStreamEvent(GRADE_STREAM_ID, {
1525
1770
  type: "calibrateSession",
1526
1771
  sessionId: body.sessionId,
1527
1772
  run: nextRun,
@@ -1539,7 +1784,7 @@ export function startWebSocketSimulator(opts) {
1539
1784
  }), { status: 400, headers: { "content-type": "application/json" } });
1540
1785
  }
1541
1786
  }
1542
- if (url.pathname === "/api/test-bot") {
1787
+ if (url.pathname === "/api/test") {
1543
1788
  if (req.method === "GET") {
1544
1789
  await deckLoadPromise.catch(() => null);
1545
1790
  const requestedDeck = url.searchParams.get("deckPath");
@@ -1580,7 +1825,7 @@ export function startWebSocketSimulator(opts) {
1580
1825
  }
1581
1826
  return new Response("Method not allowed", { status: 405 });
1582
1827
  }
1583
- if (url.pathname === "/api/test-bot/run") {
1828
+ if (url.pathname === "/api/test/run") {
1584
1829
  if (req.method !== "POST") {
1585
1830
  return new Response("Method not allowed", { status: 405 });
1586
1831
  }
@@ -1589,17 +1834,28 @@ export function startWebSocketSimulator(opts) {
1589
1834
  let botInput = undefined;
1590
1835
  let initialUserMessage = undefined;
1591
1836
  let botDeckSelection;
1837
+ let inheritBotInput = false;
1838
+ let userProvidedDeckInput = false;
1839
+ let initFillRequestMissing = undefined;
1592
1840
  try {
1593
1841
  const body = await req.json();
1594
1842
  if (typeof body.maxTurns === "number" && Number.isFinite(body.maxTurns)) {
1595
1843
  maxTurnsOverride = body.maxTurns;
1596
1844
  }
1597
1845
  deckInput = body.context ?? body.init;
1846
+ if (body.context !== undefined || body.init !== undefined) {
1847
+ userProvidedDeckInput = true;
1848
+ }
1598
1849
  if (body.init !== undefined && body.context === undefined) {
1599
- logger.warn('[gambit] Received deprecated "init" field in test-bot API; use "context" instead.');
1850
+ logger.warn('[gambit] Received deprecated "init" field in test API; use "context" instead.');
1600
1851
  }
1601
1852
  botInput = body.botInput;
1602
- await deckLoadPromise.catch(() => null);
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
+ }
1603
1859
  if (typeof body.botDeckPath === "string") {
1604
1860
  const resolved = resolveTestDeck(body.botDeckPath);
1605
1861
  if (!resolved) {
@@ -1632,19 +1888,175 @@ export function startWebSocketSimulator(opts) {
1632
1888
  // ignore; keep undefined
1633
1889
  }
1634
1890
  }
1891
+ if (!userProvidedDeckInput && inheritBotInput && botInput !== undefined) {
1892
+ deckInput = cloneValue(botInput);
1893
+ }
1635
1894
  if (!botDeckSelection) {
1636
1895
  return new Response(JSON.stringify({ error: "No test decks configured" }), { status: 400, headers: { "content-type": "application/json" } });
1637
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
+ }
1638
2048
  const run = startTestBotRun({
1639
2049
  maxTurnsOverride,
1640
2050
  deckInput,
1641
2051
  botInput,
1642
2052
  initialUserMessage,
1643
2053
  botDeckPath: botDeckSelection.path,
2054
+ initFill: initFillInfo,
2055
+ initFillTrace,
1644
2056
  });
1645
2057
  return new Response(JSON.stringify({ run }), { headers: { "content-type": "application/json" } });
1646
2058
  }
1647
- if (url.pathname === "/api/test-bot/status") {
2059
+ if (url.pathname === "/api/test/status") {
1648
2060
  const runId = url.searchParams.get("runId") ?? undefined;
1649
2061
  const sessionId = url.searchParams.get("sessionId") ?? undefined;
1650
2062
  let entry = runId ? testBotRuns.get(runId) : undefined;
@@ -1717,7 +2129,7 @@ export function startWebSocketSimulator(opts) {
1717
2129
  testDecks: availableTestDecks,
1718
2130
  }), { headers: { "content-type": "application/json" } });
1719
2131
  }
1720
- if (url.pathname === "/api/test-bot/stop") {
2132
+ if (url.pathname === "/api/test/stop") {
1721
2133
  if (req.method !== "POST") {
1722
2134
  return new Response("Method not allowed", { status: 405 });
1723
2135
  }
@@ -1818,6 +2230,7 @@ export function startWebSocketSimulator(opts) {
1818
2230
  trace: tracer,
1819
2231
  stream,
1820
2232
  state: simulatorSavedState,
2233
+ responsesMode: opts.responsesMode,
1821
2234
  onStateUpdate: (state) => {
1822
2235
  const nextMeta = {
1823
2236
  ...(simulatorSavedState?.meta ?? {}),
@@ -2205,13 +2618,7 @@ export function startWebSocketSimulator(opts) {
2205
2618
  Array.isArray(state.messages)) {
2206
2619
  const idx = state.messageRefs.findIndex((ref) => ref?.id === messageRefId);
2207
2620
  if (idx >= 0) {
2208
- const item = state.messages[idx];
2209
- if (item?.type === "message") {
2210
- messageContent = stringifyContent(item.content);
2211
- }
2212
- else {
2213
- messageContent = undefined;
2214
- }
2621
+ messageContent = state.messages[idx]?.content;
2215
2622
  }
2216
2623
  }
2217
2624
  items.push({
@@ -2287,20 +2694,28 @@ export function startWebSocketSimulator(opts) {
2287
2694
  url.pathname.startsWith("/debug") ||
2288
2695
  url.pathname.startsWith("/editor") ||
2289
2696
  url.pathname.startsWith("/docs") ||
2290
- url.pathname.startsWith("/test-bot") ||
2291
- url.pathname.startsWith("/calibrate")) {
2697
+ url.pathname.startsWith("/test") ||
2698
+ url.pathname.startsWith("/grade")) {
2292
2699
  const hasBundle = await canServeReactBundle();
2293
2700
  if (!hasBundle) {
2294
2701
  return new Response("Simulator UI bundle missing. Run `deno task bundle:sim` (or start with `--bundle`).", { status: 500 });
2295
2702
  }
2296
- return new Response(simulatorReactHtml(resolvedDeckPath), {
2703
+ await deckLoadPromise.catch(() => null);
2704
+ const resolvedLabel = deckLabel ?? toDeckLabel(resolvedDeckPath);
2705
+ return new Response(simulatorReactHtml(resolvedDeckPath, resolvedLabel), {
2297
2706
  headers: { "content-type": "text/html; charset=utf-8" },
2298
2707
  });
2299
2708
  }
2300
2709
  if (url.pathname === "/schema") {
2301
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;
2302
2716
  return new Response(JSON.stringify({
2303
2717
  deck: resolvedDeckPath,
2718
+ startMode,
2304
2719
  ...desc,
2305
2720
  }), {
2306
2721
  headers: { "content-type": "application/json; charset=utf-8" },
@@ -2394,18 +2809,58 @@ function hasReactBundleSourceMap() {
2394
2809
  return false;
2395
2810
  }
2396
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
+ }
2397
2850
  function isReactBundleStale() {
2398
2851
  try {
2399
2852
  const bundleStat = dntShim.Deno.statSync(simulatorBundlePath);
2400
- const entryStat = dntShim.Deno.statSync(simulatorUiEntryPath);
2401
- if (!bundleStat.isFile || !entryStat.isFile)
2853
+ if (!bundleStat.isFile)
2402
2854
  return false;
2403
2855
  const bundleTime = bundleStat.mtime?.getTime();
2404
- const entryTime = entryStat.mtime?.getTime();
2405
- if (typeof bundleTime !== "number" || typeof entryTime !== "number") {
2856
+ if (typeof bundleTime !== "number") {
2406
2857
  return false;
2407
2858
  }
2408
- return entryTime > bundleTime;
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;
2409
2864
  }
2410
2865
  catch {
2411
2866
  return false;
@@ -2464,8 +2919,9 @@ async function readRemoteBundle(url, kind) {
2464
2919
  return null;
2465
2920
  }
2466
2921
  }
2467
- function simulatorReactHtml(deckPath) {
2468
- const deckLabel = deckPath.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
2922
+ function simulatorReactHtml(deckPath, deckLabel) {
2923
+ const safeDeckPath = deckPath.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
2924
+ const safeDeckLabel = deckLabel?.replaceAll("<", "&lt;").replaceAll(">", "&gt;") ?? null;
2469
2925
  const bundleStamp = (() => {
2470
2926
  try {
2471
2927
  const stat = dntShim.Deno.statSync(simulatorBundlePath);
@@ -2493,7 +2949,8 @@ function simulatorReactHtml(deckPath) {
2493
2949
  <body>
2494
2950
  <div id="root"></div>
2495
2951
  <script>
2496
- window.__GAMBIT_DECK_PATH__ = ${JSON.stringify(deckLabel)};
2952
+ window.__GAMBIT_DECK_PATH__ = ${JSON.stringify(safeDeckPath)};
2953
+ window.__GAMBIT_DECK_LABEL__ = ${JSON.stringify(safeDeckLabel)};
2497
2954
  </script>
2498
2955
  <script type="module" src="${bundleUrl}"></script>
2499
2956
  </body>
@@ -2537,6 +2994,7 @@ async function runDeckWithFallback(args) {
2537
2994
  onStateUpdate: args.onStateUpdate,
2538
2995
  stream: args.stream,
2539
2996
  onStreamText: args.onStreamText,
2997
+ responsesMode: args.responsesMode,
2540
2998
  });
2541
2999
  }
2542
3000
  catch (error) {
@@ -2552,6 +3010,7 @@ async function runDeckWithFallback(args) {
2552
3010
  onStateUpdate: args.onStateUpdate,
2553
3011
  stream: args.stream,
2554
3012
  onStreamText: args.onStreamText,
3013
+ responsesMode: args.responsesMode,
2555
3014
  });
2556
3015
  }
2557
3016
  throw error;