@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
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();
@@ -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(TEST_BOT_STREAM_ID, payload);
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 && needsBundle;
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: Array.isArray(sessionState.messages)
1062
- ? sessionState.messages.map((msg) => ({
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(CALIBRATE_STREAM_ID, {
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(CALIBRATE_STREAM_ID, {
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(CALIBRATE_STREAM_ID, {
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(CALIBRATE_STREAM_ID, {
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-bot") {
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-bot/run") {
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-bot API; use "context" instead.');
1850
+ logger.warn('[gambit] Received deprecated "init" field in test API; use "context" instead.');
1482
1851
  }
1483
1852
  botInput = body.botInput;
1484
- 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
+ }
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-bot/status") {
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-bot/stop") {
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-bot") ||
2167
- url.pathname.startsWith("/calibrate")) {
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
- return new Response(simulatorReactHtml(resolvedDeckPath), {
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
- const entryStat = dntShim.Deno.statSync(simulatorUiEntryPath);
2277
- if (!bundleStat.isFile || !entryStat.isFile)
2853
+ if (!bundleStat.isFile)
2278
2854
  return false;
2279
2855
  const bundleTime = bundleStat.mtime?.getTime();
2280
- const entryTime = entryStat.mtime?.getTime();
2281
- if (typeof bundleTime !== "number" || typeof entryTime !== "number") {
2856
+ if (typeof bundleTime !== "number") {
2282
2857
  return false;
2283
2858
  }
2284
- 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;
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 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;
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(deckLabel)};
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;