@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +78 -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 +8 -4
  7. package/esm/mod.d.ts.map +1 -1
  8. package/esm/mod.js +6 -2
  9. package/esm/src/cli_utils.d.ts.map +1 -1
  10. package/esm/src/cli_utils.js +39 -3
  11. package/esm/src/openai_compat.d.ts +63 -0
  12. package/esm/src/openai_compat.d.ts.map +1 -0
  13. package/esm/src/openai_compat.js +277 -0
  14. package/esm/src/providers/google.d.ts +16 -0
  15. package/esm/src/providers/google.d.ts.map +1 -0
  16. package/esm/src/providers/google.js +352 -0
  17. package/esm/src/providers/ollama.d.ts +17 -0
  18. package/esm/src/providers/ollama.d.ts.map +1 -0
  19. package/esm/src/providers/ollama.js +509 -0
  20. package/esm/src/providers/openrouter.d.ts +22 -0
  21. package/esm/src/providers/openrouter.d.ts.map +1 -0
  22. package/esm/src/providers/openrouter.js +592 -0
  23. package/esm/src/server.d.ts +2 -0
  24. package/esm/src/server.d.ts.map +1 -1
  25. package/esm/src/server.js +612 -29
  26. package/esm/src/trace.d.ts.map +1 -1
  27. package/esm/src/trace.js +2 -2
  28. package/package.json +3 -2
  29. package/script/gambit/simulator-ui/dist/bundle.js +4744 -4360
  30. package/script/gambit/simulator-ui/dist/bundle.js.map +4 -4
  31. package/script/gambit/simulator-ui/dist/favicon.ico +0 -0
  32. package/script/mod.d.ts +8 -4
  33. package/script/mod.d.ts.map +1 -1
  34. package/script/mod.js +13 -7
  35. package/script/src/cli_utils.d.ts.map +1 -1
  36. package/script/src/cli_utils.js +38 -2
  37. package/script/src/openai_compat.d.ts +63 -0
  38. package/script/src/openai_compat.d.ts.map +1 -0
  39. package/script/src/openai_compat.js +281 -0
  40. package/script/src/providers/google.d.ts +16 -0
  41. package/script/src/providers/google.d.ts.map +1 -0
  42. package/script/src/providers/google.js +359 -0
  43. package/script/src/providers/ollama.d.ts +17 -0
  44. package/script/src/providers/ollama.d.ts.map +1 -0
  45. package/script/src/providers/ollama.js +551 -0
  46. package/script/src/providers/openrouter.d.ts +22 -0
  47. package/script/src/providers/openrouter.d.ts.map +1 -0
  48. package/script/src/providers/openrouter.js +632 -0
  49. package/script/src/server.d.ts +2 -0
  50. package/script/src/server.d.ts.map +1 -1
  51. package/script/src/server.js +612 -29
  52. package/script/src/trace.d.ts.map +1 -1
  53. package/script/src/trace.js +2 -2
@@ -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 TEST_BOT_STREAM_ID = "gambit-test-bot";
90
- const CALIBRATE_STREAM_ID = "gambit-calibrate";
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)(TEST_BOT_STREAM_ID, payload);
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 && needsBundle;
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: Array.isArray(sessionState.messages)
1098
- ? sessionState.messages.map((msg) => ({
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)(CALIBRATE_STREAM_ID, {
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)(CALIBRATE_STREAM_ID, {
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)(CALIBRATE_STREAM_ID, {
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)(CALIBRATE_STREAM_ID, {
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-bot") {
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-bot/run") {
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-bot API; use "context" instead.');
1886
+ logger.warn('[gambit] Received deprecated "init" field in test API; use "context" instead.');
1518
1887
  }
1519
1888
  botInput = body.botInput;
1520
- await deckLoadPromise.catch(() => null);
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-bot/status") {
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-bot/stop") {
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-bot") ||
2203
- url.pathname.startsWith("/calibrate")) {
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
- return new Response(simulatorReactHtml(resolvedDeckPath), {
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
- const entryStat = dntShim.Deno.statSync(simulatorUiEntryPath);
2313
- if (!bundleStat.isFile || !entryStat.isFile)
2889
+ if (!bundleStat.isFile)
2314
2890
  return false;
2315
2891
  const bundleTime = bundleStat.mtime?.getTime();
2316
- const entryTime = entryStat.mtime?.getTime();
2317
- if (typeof bundleTime !== "number" || typeof entryTime !== "number") {
2892
+ if (typeof bundleTime !== "number") {
2318
2893
  return false;
2319
2894
  }
2320
- return entryTime > bundleTime;
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 deckLabel = deckPath.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
2958
+ function simulatorReactHtml(deckPath, deckLabel) {
2959
+ const safeDeckPath = deckPath.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
2960
+ const safeDeckLabel = deckLabel?.replaceAll("<", "&lt;").replaceAll(">", "&gt;") ?? 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(deckLabel)};
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;