@bolt-foundry/gambit 0.6.8 → 0.8.0

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 (39) hide show
  1. package/CHANGELOG.md +6 -37
  2. package/README.md +76 -25
  3. package/esm/gambit/simulator-ui/dist/bundle.js +1010 -333
  4. package/esm/gambit/simulator-ui/dist/bundle.js.map +3 -3
  5. package/esm/mod.d.ts +4 -4
  6. package/esm/mod.d.ts.map +1 -1
  7. package/esm/mod.js +2 -2
  8. package/esm/src/cli_utils.d.ts +4 -5
  9. package/esm/src/cli_utils.d.ts.map +1 -1
  10. package/esm/src/cli_utils.js +27 -7
  11. package/esm/src/compat/openai.d.ts +2 -0
  12. package/esm/src/compat/openai.d.ts.map +1 -0
  13. package/esm/src/compat/openai.js +1 -0
  14. package/esm/src/providers/openrouter.d.ts +9 -0
  15. package/esm/src/providers/openrouter.d.ts.map +1 -0
  16. package/esm/src/providers/openrouter.js +595 -0
  17. package/esm/src/server.d.ts.map +1 -1
  18. package/esm/src/server.js +261 -6
  19. package/esm/src/trace.d.ts.map +1 -1
  20. package/esm/src/trace.js +4 -1
  21. package/package.json +3 -2
  22. package/script/gambit/simulator-ui/dist/bundle.js +1010 -333
  23. package/script/gambit/simulator-ui/dist/bundle.js.map +3 -3
  24. package/script/mod.d.ts +4 -4
  25. package/script/mod.d.ts.map +1 -1
  26. package/script/mod.js +6 -6
  27. package/script/src/cli_utils.d.ts +4 -5
  28. package/script/src/cli_utils.d.ts.map +1 -1
  29. package/script/src/cli_utils.js +29 -9
  30. package/script/src/compat/openai.d.ts +2 -0
  31. package/script/src/compat/openai.d.ts.map +1 -0
  32. package/script/src/compat/openai.js +5 -0
  33. package/script/src/providers/openrouter.d.ts +9 -0
  34. package/script/src/providers/openrouter.d.ts.map +1 -0
  35. package/script/src/providers/openrouter.js +634 -0
  36. package/script/src/server.d.ts.map +1 -1
  37. package/script/src/server.js +261 -6
  38. package/script/src/trace.d.ts.map +1 -1
  39. package/script/src/trace.js +4 -1
@@ -98,6 +98,87 @@ function randomId(prefix) {
98
98
  const suffix = crypto.randomUUID().replace(/-/g, "").slice(0, 24);
99
99
  return `${prefix}-${suffix}`;
100
100
  }
101
+ async function parseOpenResponseRequest(req) {
102
+ try {
103
+ return await req.json();
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ }
109
+ function formatOpenResponseSseEvent(event) {
110
+ return `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`;
111
+ }
112
+ function formatOpenResponseDoneEvent() {
113
+ return "data: [DONE]\n\n";
114
+ }
115
+ function createOpenResponseStream(req, provider, payload) {
116
+ const encoder = new TextEncoder();
117
+ const stream = new ReadableStream({
118
+ start(controller) {
119
+ let closed = false;
120
+ let completed = false;
121
+ const close = () => {
122
+ if (closed)
123
+ return;
124
+ closed = true;
125
+ controller.close();
126
+ };
127
+ const send = (chunk) => {
128
+ if (closed)
129
+ return;
130
+ controller.enqueue(encoder.encode(chunk));
131
+ };
132
+ const sendDone = () => {
133
+ if (closed)
134
+ return;
135
+ send(formatOpenResponseDoneEvent());
136
+ close();
137
+ };
138
+ const sendEvent = (event) => {
139
+ if (closed)
140
+ return;
141
+ send(formatOpenResponseSseEvent(event));
142
+ if (event.type === "response.completed") {
143
+ completed = true;
144
+ sendDone();
145
+ }
146
+ };
147
+ req.signal.addEventListener("abort", () => {
148
+ close();
149
+ });
150
+ (async () => {
151
+ try {
152
+ const response = await provider.responses({
153
+ ...payload,
154
+ stream: true,
155
+ onStreamEvent: sendEvent,
156
+ });
157
+ if (!completed) {
158
+ sendEvent({ type: "response.completed", response });
159
+ sendDone();
160
+ }
161
+ }
162
+ catch (err) {
163
+ const message = err instanceof Error ? err.message : String(err);
164
+ sendEvent({
165
+ type: "error",
166
+ error: { code: "server_error", message },
167
+ });
168
+ sendDone();
169
+ }
170
+ })();
171
+ },
172
+ });
173
+ return new Response(stream, {
174
+ status: 200,
175
+ headers: {
176
+ "Content-Type": "text/event-stream",
177
+ "Cache-Control": "no-cache",
178
+ "Connection": "keep-alive",
179
+ },
180
+ });
181
+ }
101
182
  function resolveDefaultValue(raw) {
102
183
  if (typeof raw === "function") {
103
184
  try {
@@ -565,6 +646,12 @@ function startWebSocketSimulator(opts) {
565
646
  return "";
566
647
  if (typeof value === "string")
567
648
  return value;
649
+ if (Array.isArray(value)) {
650
+ return value
651
+ .map((part) => typeof part === "string" ? part : part.text ??
652
+ "")
653
+ .join("");
654
+ }
568
655
  try {
569
656
  return JSON.stringify(value);
570
657
  }
@@ -624,7 +711,8 @@ function startWebSocketSimulator(opts) {
624
711
  const fallbackToolInserts = [];
625
712
  for (let i = 0; i < rawMessages.length; i++) {
626
713
  const msg = rawMessages[i];
627
- if (msg?.role === "assistant" || msg?.role === "user") {
714
+ if (msg?.type === "message" &&
715
+ (msg.role === "assistant" || msg.role === "user")) {
628
716
  const content = stringifyContent(msg.content).trim();
629
717
  if (!content)
630
718
  continue;
@@ -637,7 +725,7 @@ function startWebSocketSimulator(opts) {
637
725
  });
638
726
  continue;
639
727
  }
640
- if (msg?.role === "tool") {
728
+ if (msg?.type === "message" && msg.role === "tool") {
641
729
  const actionCallId = typeof msg.tool_call_id === "string"
642
730
  ? msg.tool_call_id
643
731
  : undefined;
@@ -773,7 +861,7 @@ function startWebSocketSimulator(opts) {
773
861
  const getLastAssistantMessage = (history) => {
774
862
  for (let i = history.length - 1; i >= 0; i--) {
775
863
  const msg = history[i];
776
- if (msg?.role === "assistant") {
864
+ if (msg?.type === "message" && msg.role === "assistant") {
777
865
  return stringifyContent(msg.content);
778
866
  }
779
867
  }
@@ -1036,6 +1124,43 @@ function startWebSocketSimulator(opts) {
1036
1124
  }
1037
1125
  const server = dntShim.Deno.serve({ port, signal: opts.signal, onListen: () => { } }, async (req) => {
1038
1126
  const url = new URL(req.url);
1127
+ if (url.pathname === "/v1/responses") {
1128
+ if (req.method !== "POST") {
1129
+ return new Response("Method not allowed", { status: 405 });
1130
+ }
1131
+ const payload = await parseOpenResponseRequest(req);
1132
+ if (!payload) {
1133
+ return new Response("Invalid JSON payload", { status: 400 });
1134
+ }
1135
+ const model = payload.model ?? opts.model;
1136
+ if (!model) {
1137
+ return new Response("Missing model", { status: 400 });
1138
+ }
1139
+ const requestPayload = {
1140
+ ...payload,
1141
+ model,
1142
+ input: payload.input ?? null,
1143
+ };
1144
+ if (payload.stream) {
1145
+ return createOpenResponseStream(req, opts.modelProvider, requestPayload);
1146
+ }
1147
+ try {
1148
+ const response = await opts.modelProvider.responses({
1149
+ ...requestPayload,
1150
+ stream: false,
1151
+ });
1152
+ return new Response(JSON.stringify(response), {
1153
+ headers: { "content-type": "application/json" },
1154
+ });
1155
+ }
1156
+ catch (err) {
1157
+ const message = err instanceof Error ? err.message : String(err);
1158
+ return new Response(JSON.stringify({ error: message }), {
1159
+ status: 500,
1160
+ headers: { "content-type": "application/json" },
1161
+ });
1162
+ }
1163
+ }
1039
1164
  if (url.pathname.startsWith("/api/durable-streams/stream/")) {
1040
1165
  return (0, durable_streams_js_1.handleDurableStreamRequest)(req);
1041
1166
  }
@@ -1086,7 +1211,9 @@ function startWebSocketSimulator(opts) {
1086
1211
  })();
1087
1212
  const sessionPayload = {
1088
1213
  messages: Array.isArray(sessionState.messages)
1089
- ? sessionState.messages.map((msg) => ({
1214
+ ? sessionState.messages
1215
+ .filter((msg) => msg.type === "message")
1216
+ .map((msg) => ({
1090
1217
  role: msg.role,
1091
1218
  content: msg.content,
1092
1219
  name: msg.name,
@@ -1241,6 +1368,124 @@ function startWebSocketSimulator(opts) {
1241
1368
  }), { status: 400, headers: { "content-type": "application/json" } });
1242
1369
  }
1243
1370
  }
1371
+ if (url.pathname === "/api/calibrate/flag") {
1372
+ if (req.method !== "POST") {
1373
+ return new Response("Method not allowed", { status: 405 });
1374
+ }
1375
+ try {
1376
+ const body = await req.json();
1377
+ if (!body.sessionId || !body.refId) {
1378
+ throw new Error("Missing sessionId or refId");
1379
+ }
1380
+ const state = readSessionState(body.sessionId);
1381
+ if (!state) {
1382
+ throw new Error("Session not found");
1383
+ }
1384
+ const meta = (state.meta && typeof state.meta === "object")
1385
+ ? { ...state.meta }
1386
+ : {};
1387
+ const existingFlags = Array.isArray(meta.gradingFlags)
1388
+ ? (meta.gradingFlags)
1389
+ : [];
1390
+ const flagIndex = existingFlags.findIndex((flag) => flag?.refId === body.refId);
1391
+ let nextFlags;
1392
+ let flagged = false;
1393
+ if (flagIndex >= 0) {
1394
+ nextFlags = existingFlags.filter((_, idx) => idx !== flagIndex);
1395
+ flagged = false;
1396
+ }
1397
+ else {
1398
+ const now = new Date().toISOString();
1399
+ nextFlags = [
1400
+ ...existingFlags,
1401
+ {
1402
+ id: randomId("flag"),
1403
+ refId: body.refId,
1404
+ runId: body.runId,
1405
+ turnIndex: body.turnIndex,
1406
+ reason: body.reason?.trim() || undefined,
1407
+ createdAt: now,
1408
+ },
1409
+ ];
1410
+ flagged = true;
1411
+ }
1412
+ const updated = persistSessionState({
1413
+ ...state,
1414
+ meta: {
1415
+ ...meta,
1416
+ gradingFlags: nextFlags,
1417
+ },
1418
+ });
1419
+ const sessionMeta = buildSessionMeta(body.sessionId, updated);
1420
+ (0, durable_streams_js_1.appendDurableStreamEvent)(CALIBRATE_STREAM_ID, {
1421
+ type: "calibrateSession",
1422
+ sessionId: body.sessionId,
1423
+ session: sessionMeta,
1424
+ });
1425
+ return new Response(JSON.stringify({
1426
+ sessionId: body.sessionId,
1427
+ flagged,
1428
+ flags: nextFlags,
1429
+ }), { headers: { "content-type": "application/json" } });
1430
+ }
1431
+ catch (err) {
1432
+ return new Response(JSON.stringify({
1433
+ error: err instanceof Error ? err.message : String(err),
1434
+ }), { status: 400, headers: { "content-type": "application/json" } });
1435
+ }
1436
+ }
1437
+ if (url.pathname === "/api/calibrate/flag/reason") {
1438
+ if (req.method !== "POST") {
1439
+ return new Response("Method not allowed", { status: 405 });
1440
+ }
1441
+ try {
1442
+ const body = await req.json();
1443
+ if (!body.sessionId || !body.refId) {
1444
+ throw new Error("Missing sessionId or refId");
1445
+ }
1446
+ const state = readSessionState(body.sessionId);
1447
+ if (!state) {
1448
+ throw new Error("Session not found");
1449
+ }
1450
+ const meta = (state.meta && typeof state.meta === "object")
1451
+ ? { ...state.meta }
1452
+ : {};
1453
+ const existingFlags = Array.isArray(meta.gradingFlags)
1454
+ ? (meta.gradingFlags)
1455
+ : [];
1456
+ const flagIndex = existingFlags.findIndex((flag) => flag?.refId === body.refId);
1457
+ if (flagIndex < 0) {
1458
+ throw new Error("Flag not found");
1459
+ }
1460
+ const updatedFlag = {
1461
+ ...existingFlags[flagIndex],
1462
+ reason: body.reason?.trim() || undefined,
1463
+ };
1464
+ const nextFlags = existingFlags.map((flag, idx) => idx === flagIndex ? updatedFlag : flag);
1465
+ const updated = persistSessionState({
1466
+ ...state,
1467
+ meta: {
1468
+ ...meta,
1469
+ gradingFlags: nextFlags,
1470
+ },
1471
+ });
1472
+ const sessionMeta = buildSessionMeta(body.sessionId, updated);
1473
+ (0, durable_streams_js_1.appendDurableStreamEvent)(CALIBRATE_STREAM_ID, {
1474
+ type: "calibrateSession",
1475
+ sessionId: body.sessionId,
1476
+ session: sessionMeta,
1477
+ });
1478
+ return new Response(JSON.stringify({
1479
+ sessionId: body.sessionId,
1480
+ flags: nextFlags,
1481
+ }), { headers: { "content-type": "application/json" } });
1482
+ }
1483
+ catch (err) {
1484
+ return new Response(JSON.stringify({
1485
+ error: err instanceof Error ? err.message : String(err),
1486
+ }), { status: 400, headers: { "content-type": "application/json" } });
1487
+ }
1488
+ }
1244
1489
  if (url.pathname === "/api/grading/reference") {
1245
1490
  if (req.method !== "POST") {
1246
1491
  return new Response("Method not allowed", { status: 405 });
@@ -1385,7 +1630,10 @@ function startWebSocketSimulator(opts) {
1385
1630
  if (typeof body.maxTurns === "number" && Number.isFinite(body.maxTurns)) {
1386
1631
  maxTurnsOverride = body.maxTurns;
1387
1632
  }
1388
- deckInput = body.init;
1633
+ deckInput = body.context ?? body.init;
1634
+ if (body.init !== undefined && body.context === undefined) {
1635
+ logger.warn('[gambit] Received deprecated "init" field in test-bot API; use "context" instead.');
1636
+ }
1389
1637
  botInput = body.botInput;
1390
1638
  await deckLoadPromise.catch(() => null);
1391
1639
  if (typeof body.botDeckPath === "string") {
@@ -1829,6 +2077,7 @@ function startWebSocketSimulator(opts) {
1829
2077
  sessionId,
1830
2078
  messages: state.messages,
1831
2079
  messageRefs: state.messageRefs,
2080
+ feedback: state.feedback,
1832
2081
  traces: state.traces,
1833
2082
  notes: state.notes,
1834
2083
  meta: state.meta,
@@ -1992,7 +2241,13 @@ function startWebSocketSimulator(opts) {
1992
2241
  Array.isArray(state.messages)) {
1993
2242
  const idx = state.messageRefs.findIndex((ref) => ref?.id === messageRefId);
1994
2243
  if (idx >= 0) {
1995
- messageContent = state.messages[idx]?.content;
2244
+ const item = state.messages[idx];
2245
+ if (item?.type === "message") {
2246
+ messageContent = stringifyContent(item.content);
2247
+ }
2248
+ else {
2249
+ messageContent = undefined;
2250
+ }
1996
2251
  }
1997
2252
  }
1998
2253
  items.push({
@@ -1 +1 @@
1
- {"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../../src/src/trace.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAI5D,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAQ7E;AAED,wBAAgB,iBAAiB,IAAI,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAmI/D"}
1
+ {"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../../src/src/trace.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAI5D,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAQ7E;AAED,wBAAgB,iBAAiB,IAAI,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAsI/D"}
@@ -68,7 +68,10 @@ function makeConsoleTracer() {
68
68
  break;
69
69
  }
70
70
  case "message.user": {
71
- logger.log(`[trace] message.user runId=${event.runId} actionCallId=${event.actionCallId} deck=${event.deckPath} content=${JSON.stringify(event.message?.content ?? "")}`);
71
+ const messageContent = event.message?.type === "message"
72
+ ? event.message.content
73
+ : event.message;
74
+ logger.log(`[trace] message.user runId=${event.runId} actionCallId=${event.actionCallId} deck=${event.deckPath} content=${JSON.stringify(messageContent ?? "")}`);
72
75
  break;
73
76
  }
74
77
  case "deck.start":