pi-system-one 1.2.0 → 1.2.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.
@@ -11,6 +11,25 @@ import {
11
11
  } from "../src/tool.ts";
12
12
 
13
13
  describe("system_one tool", () => {
14
+ /**
15
+ * A tool whose provider records the request it was handed, so a test can
16
+ * assert the exact shape that reached the wire after normalization.
17
+ */
18
+ function recordingTool() {
19
+ let captured: any = null;
20
+ const stub = {
21
+ id: "stub",
22
+ async evaluate(request: any) {
23
+ captured = request;
24
+ return { answers: {}, metadata: { provider: "stub" } };
25
+ },
26
+ };
27
+ return {
28
+ tool: buildSystemOneTool({ provider: stub as never }),
29
+ request: () => captured,
30
+ };
31
+ }
32
+
14
33
  it("delegates a mixed batch and preserves details", async () => {
15
34
  const tool = buildSystemOneTool({
16
35
  provider: new MockSystemOneProvider({
@@ -303,6 +322,66 @@ describe("system_one tool", () => {
303
322
  });
304
323
  }
305
324
 
325
+ it("decodes questions serialized as a JSON string (provider quirk)", () => {
326
+ const raw = {
327
+ state: "hi",
328
+ questions: JSON.stringify({
329
+ t: { type: "choice", instructions: "W?", criteria: { a: null } },
330
+ }),
331
+ };
332
+ const prepared = prepareSystemOneArgs(raw);
333
+ assert.deepEqual(prepared, {
334
+ state: "hi",
335
+ questions: {
336
+ t: { type: "choice", instructions: "W?", criteria: { a: null } },
337
+ },
338
+ });
339
+ });
340
+
341
+ it("decodes a question serialized as a JSON string inside an array", () => {
342
+ const q = { type: "choice", instructions: "W?", criteria: { a: null } };
343
+ // Every entry stringified, and the whole argument stringified too:
344
+ // both layers decode before normalization.
345
+ for (const questions of [[JSON.stringify(q)], JSON.stringify([q])]) {
346
+ assert.deepEqual(prepareSystemOneArgs({ state: "hi", questions }), {
347
+ state: "hi",
348
+ questions: { q1: q },
349
+ });
350
+ }
351
+ });
352
+
353
+ it("still rejects strings that are not JSON objects or arrays", async () => {
354
+ const tool = buildSystemOneTool({
355
+ provider: {
356
+ id: "stub",
357
+ async evaluate() {
358
+ return { answers: {}, metadata: { provider: "stub" } };
359
+ },
360
+ } as never,
361
+ });
362
+ for (const questions of [
363
+ "foo",
364
+ "{not json",
365
+ "{{{{",
366
+ "[[[",
367
+ "123",
368
+ "true",
369
+ "null",
370
+ '"hello"',
371
+ ]) {
372
+ await assert.rejects(
373
+ tool.execute(
374
+ "id-rej-string",
375
+ { state: "hi", questions } as never,
376
+ undefined,
377
+ undefined,
378
+ {} as never,
379
+ ),
380
+ /"questions" must resolve to a question map/,
381
+ );
382
+ }
383
+ });
384
+
306
385
  it("folds options array to null-valued criteria record", () => {
307
386
  assert.deepEqual(
308
387
  prepareSystemOneArgs({
@@ -348,7 +427,7 @@ describe("system_one tool", () => {
348
427
  );
349
428
  });
350
429
 
351
- it("unwraps single questions with extra fields so execute reports the field", async () => {
430
+ it("unwraps a single question and drops its extra fields", async () => {
352
431
  const prepared = prepareSystemOneArgs({
353
432
  state: "hi",
354
433
  questions: {
@@ -365,12 +444,12 @@ describe("system_one tool", () => {
365
444
  type: "choice",
366
445
  instructions: "W?",
367
446
  criteria: { a: null },
368
- foo: "bar",
369
447
  },
370
448
  },
371
449
  });
372
450
  // The permissive boundary schema lets this through (older Pi must be
373
- // able to reach execute); the semantic gate rejects it precisely.
451
+ // able to reach execute), and the extra field is dropped rather than
452
+ // turned into a retry the model has to learn from.
374
453
  assert.equal(Value.Check(systemOneParams, prepared), true);
375
454
  let calls = 0;
376
455
  const tool = buildSystemOneTool({
@@ -382,17 +461,14 @@ describe("system_one tool", () => {
382
461
  },
383
462
  } as never,
384
463
  });
385
- await assert.rejects(
386
- tool.execute(
387
- "id-foo",
388
- prepared as never,
389
- undefined,
390
- undefined,
391
- {} as never,
392
- ),
393
- /question "q" has unknown field "foo"/,
464
+ await tool.execute(
465
+ "id-foo",
466
+ prepared as never,
467
+ undefined,
468
+ undefined,
469
+ {} as never,
394
470
  );
395
- assert.equal(calls, 0);
471
+ assert.equal(calls, 1);
396
472
  });
397
473
 
398
474
  it("lets unknown types through the schema so execute rejects them clearly", async () => {
@@ -861,21 +937,6 @@ describe("system_one tool", () => {
861
937
  { state: "hi", questions: null },
862
938
  /questions is required/,
863
939
  ],
864
- [
865
- "unknown field",
866
- {
867
- state: "hi",
868
- questions: {
869
- t: {
870
- type: "choice",
871
- instructions: "W?",
872
- criteria: { a: null },
873
- confidence: 0.9,
874
- },
875
- },
876
- },
877
- /question "t" has unknown field "confidence"/,
878
- ],
879
940
  [
880
941
  "noul criteria as array",
881
942
  {
@@ -1044,11 +1105,6 @@ describe("system_one tool", () => {
1044
1105
  provider: new MockSystemOneProvider({ answers: {} }),
1045
1106
  });
1046
1107
  assert.ok(tool.promptSnippet && tool.promptSnippet.length > 0);
1047
- for (const guideline of tool.promptGuidelines ?? []) {
1048
- assert.match(guideline, /system_one/);
1049
- }
1050
- assert.match(tool.description, /instead of answering directly/);
1051
- assert.match(tool.description, /calibrated probabilities/);
1052
1108
  });
1053
1109
 
1054
1110
  it("keeps discovery and type selection mechanical in one layer", () => {
@@ -1075,6 +1131,245 @@ describe("system_one tool", () => {
1075
1131
  });
1076
1132
  });
1077
1133
 
1134
+ describe("copy-on-write normalization", () => {
1135
+ const canonicalRequest = () => ({
1136
+ state: { incident: "latency" },
1137
+ questions: {
1138
+ rollback: { type: "noul", instructions: "Roll back?" },
1139
+ owner: {
1140
+ type: "choice",
1141
+ instructions: "Which team?",
1142
+ criteria: { frontend: null, backend: null },
1143
+ },
1144
+ },
1145
+ });
1146
+
1147
+ it("returns canonical requests, maps, and questions by reference", () => {
1148
+ const args = canonicalRequest();
1149
+ const out = prepareSystemOneArgs(args) as typeof args;
1150
+ assert.equal(out, args);
1151
+ assert.equal(out.questions, args.questions);
1152
+ assert.equal(out.questions.rollback, args.questions.rollback);
1153
+ assert.equal(out.questions.owner, args.questions.owner);
1154
+ // Key order is not canonicalized: identity means "already final".
1155
+ const reordered = {
1156
+ state: "s",
1157
+ questions: {
1158
+ q: { criteria: { a: null }, instructions: "I?", type: "choice" },
1159
+ },
1160
+ };
1161
+ assert.equal(prepareSystemOneArgs(reordered), reordered);
1162
+ });
1163
+
1164
+ it("structurally shares unaffected siblings on partial repair", () => {
1165
+ const args = {
1166
+ state: "s",
1167
+ questions: {
1168
+ fine: { type: "noul", instructions: "OK?" },
1169
+ sloppy: { type: "Choice", prompt: "Pick?", options: ["a", "b"] },
1170
+ },
1171
+ };
1172
+ const out = prepareSystemOneArgs(args) as typeof args;
1173
+ assert.notEqual(out, args);
1174
+ assert.notEqual(out.questions, args.questions);
1175
+ assert.notEqual(out.questions.sloppy, args.questions.sloppy);
1176
+ assert.equal(out.questions.fine, args.questions.fine);
1177
+ assert.deepEqual(out.questions.sloppy, {
1178
+ type: "choice",
1179
+ instructions: "Pick?",
1180
+ criteria: { a: null, b: null },
1181
+ });
1182
+ });
1183
+
1184
+ it("is idempotent and allocates nothing new on the second pass", () => {
1185
+ const once = prepareSystemOneArgs(canonicalRequest()) as Record<
1186
+ string,
1187
+ unknown
1188
+ >;
1189
+ const twice = prepareSystemOneArgs(once);
1190
+ assert.deepEqual(twice, once);
1191
+ assert.equal(twice, once);
1192
+ // Repair once, then the repaired output is itself canonical.
1193
+ const repairedOnce = prepareSystemOneArgs({
1194
+ state: "s",
1195
+ questions: [{ name: "q", type: "boolean", prompt: "Y?" }],
1196
+ });
1197
+ assert.equal(prepareSystemOneArgs(repairedOnce), repairedOnce);
1198
+ });
1199
+
1200
+ it("never mutates its input", () => {
1201
+ const deepFreeze = (v: unknown): void => {
1202
+ if (typeof v === "object" && v !== null && !Object.isFrozen(v)) {
1203
+ Object.freeze(v);
1204
+ for (const k of Object.keys(v)) {
1205
+ deepFreeze((v as Record<string, unknown>)[k]);
1206
+ }
1207
+ }
1208
+ };
1209
+ const sloppy = {
1210
+ context: { incident: "latency" },
1211
+ question: [
1212
+ { name: "a", type: "Rating", prompt: "Rate?", levels: ["1", "5"] },
1213
+ { id: "b", type: "noul", instructions: "OK?", extra: 1 },
1214
+ ],
1215
+ };
1216
+ deepFreeze(sloppy);
1217
+ const out = prepareSystemOneArgs(sloppy);
1218
+ assert.deepEqual(out, {
1219
+ state: { incident: "latency" },
1220
+ questions: {
1221
+ a: {
1222
+ type: "score",
1223
+ instructions: "Rate?",
1224
+ criteria: ["1", "5"],
1225
+ },
1226
+ b: { type: "noul", instructions: "OK?" },
1227
+ },
1228
+ });
1229
+ });
1230
+
1231
+ it("repairs every non-canonical shape exactly (no false sharing)", () => {
1232
+ // [input, expected output, sharesInputRef]: the last two entries are
1233
+ // already builder fixed points, so sharing them is correct.
1234
+ const cases: Array<[unknown, unknown, boolean]> = [
1235
+ // Alias type spelling.
1236
+ [
1237
+ {
1238
+ state: "s",
1239
+ questions: { q: { type: "Yes/No", instructions: "Y?" } },
1240
+ },
1241
+ {
1242
+ state: "s",
1243
+ questions: { q: { type: "noul", instructions: "Y?" } },
1244
+ },
1245
+ false,
1246
+ ],
1247
+ // Alias field names.
1248
+ [
1249
+ {
1250
+ state: "s",
1251
+ questions: { q: { type: "choice", prompt: "P?", choices: ["a"] } },
1252
+ },
1253
+ {
1254
+ state: "s",
1255
+ questions: {
1256
+ q: { type: "choice", instructions: "P?", criteria: { a: null } },
1257
+ },
1258
+ },
1259
+ false,
1260
+ ],
1261
+ // Junk keys dropped.
1262
+ [
1263
+ {
1264
+ state: "s",
1265
+ questions: {
1266
+ q: {
1267
+ type: "noul",
1268
+ instructions: "Y?",
1269
+ description: "d",
1270
+ title: "t",
1271
+ },
1272
+ },
1273
+ },
1274
+ {
1275
+ state: "s",
1276
+ questions: { q: { type: "noul", instructions: "Y?" } },
1277
+ },
1278
+ false,
1279
+ ],
1280
+ // Undefined-valued lingering alias key is still dropped.
1281
+ [
1282
+ {
1283
+ state: "s",
1284
+ questions: {
1285
+ q: { type: "noul", instructions: "Y?", prompt: undefined },
1286
+ },
1287
+ },
1288
+ {
1289
+ state: "s",
1290
+ questions: { q: { type: "noul", instructions: "Y?" } },
1291
+ },
1292
+ false,
1293
+ ],
1294
+ // Own undefined instructions key is preserved (builder copies it).
1295
+ [
1296
+ {
1297
+ state: "s",
1298
+ questions: { q: { type: "noul", instructions: undefined } },
1299
+ },
1300
+ {
1301
+ state: "s",
1302
+ questions: { q: { type: "noul", instructions: undefined } },
1303
+ },
1304
+ true,
1305
+ ],
1306
+ // noul null criteria reads as omitted.
1307
+ [
1308
+ {
1309
+ state: "s",
1310
+ questions: {
1311
+ q: { type: "noul", instructions: "Y?", criteria: null },
1312
+ },
1313
+ },
1314
+ {
1315
+ state: "s",
1316
+ questions: { q: { type: "noul", instructions: "Y?" } },
1317
+ },
1318
+ false,
1319
+ ],
1320
+ // Singular question alias + array form + naming metadata.
1321
+ [
1322
+ { context: "s", question: { type: "boolean", prompt: "Y?" } },
1323
+ {
1324
+ state: "s",
1325
+ questions: { q: { type: "noul", instructions: "Y?" } },
1326
+ },
1327
+ false,
1328
+ ],
1329
+ // Unknown top-level keys dropped; missing state preserved as-is.
1330
+ [
1331
+ {
1332
+ state: "s",
1333
+ questions: { q: { type: "noul", instructions: "Y?" } },
1334
+ extra: 1,
1335
+ },
1336
+ {
1337
+ state: "s",
1338
+ questions: { q: { type: "noul", instructions: "Y?" } },
1339
+ },
1340
+ false,
1341
+ ],
1342
+ // Empty choice array still folds (every() on [] is true).
1343
+ [
1344
+ {
1345
+ state: "s",
1346
+ questions: {
1347
+ q: { type: "choice", instructions: "P?", criteria: [] },
1348
+ },
1349
+ },
1350
+ {
1351
+ state: "s",
1352
+ questions: {
1353
+ q: { type: "choice", instructions: "P?", criteria: {} },
1354
+ },
1355
+ },
1356
+ false,
1357
+ ],
1358
+ // Non-string type passes through untouched (fixed point).
1359
+ [
1360
+ { state: "s", questions: { q: { type: 5, instructions: "Y?" } } },
1361
+ { state: "s", questions: { q: { type: 5, instructions: "Y?" } } },
1362
+ true,
1363
+ ],
1364
+ ];
1365
+ for (const [input, expected, sharesRef] of cases) {
1366
+ const out = prepareSystemOneArgs(input);
1367
+ assert.deepEqual(out, expected);
1368
+ assert.equal(out === input, sharesRef);
1369
+ }
1370
+ });
1371
+ });
1372
+
1078
1373
  describe("backend minima guardrails", () => {
1079
1374
  function stubTool() {
1080
1375
  let calls = 0;
@@ -1227,18 +1522,92 @@ describe("system_one tool", () => {
1227
1522
  assert.equal(calls(), 0);
1228
1523
  });
1229
1524
 
1230
- it("rejects score questions with object criteria", async () => {
1525
+ it("folds object score criteria into an ordered rubric", async () => {
1526
+ const { tool, request } = recordingTool();
1527
+ await tool.execute(
1528
+ "id-fold-score",
1529
+ {
1530
+ state: "hi",
1531
+ questions: {
1532
+ s: {
1533
+ type: "score",
1534
+ instructions: "How?",
1535
+ criteria: { low: "bad", high: "good" },
1536
+ },
1537
+ },
1538
+ } as never,
1539
+ undefined,
1540
+ undefined,
1541
+ {} as never,
1542
+ );
1543
+ assert.deepEqual(request().questions.s.criteria, [
1544
+ "low: bad",
1545
+ "high: good",
1546
+ ]);
1547
+ });
1548
+
1549
+ it("keeps a score level's label and description without duplicating", async () => {
1550
+ const { tool, request } = recordingTool();
1551
+ await tool.execute(
1552
+ "id-level-self-describing",
1553
+ {
1554
+ state: "hi",
1555
+ questions: {
1556
+ s: {
1557
+ type: "score",
1558
+ instructions: "How bad?",
1559
+ criteria: {
1560
+ low: "Bad: minor and reversible",
1561
+ high: "",
1562
+ moderate: "moderate",
1563
+ },
1564
+ },
1565
+ },
1566
+ } as never,
1567
+ undefined,
1568
+ undefined,
1569
+ {} as never,
1570
+ );
1571
+ assert.deepEqual(request().questions.s.criteria, [
1572
+ "low: Bad: minor and reversible",
1573
+ "high",
1574
+ "moderate",
1575
+ ]);
1576
+ });
1577
+
1578
+ it("unwraps a single wrapper key holding the rubric", async () => {
1579
+ const { tool, request } = recordingTool();
1580
+ await tool.execute(
1581
+ "id-fold-score-wrapper",
1582
+ {
1583
+ state: "hi",
1584
+ questions: {
1585
+ s: {
1586
+ type: "score",
1587
+ instructions: "How?",
1588
+ criteria: { item: ["low", "high"] },
1589
+ },
1590
+ },
1591
+ } as never,
1592
+ undefined,
1593
+ undefined,
1594
+ {} as never,
1595
+ );
1596
+ assert.deepEqual(request().questions.s.criteria, ["low", "high"]);
1597
+ });
1598
+
1599
+ it("rejects a one-level object rubric rather than unwrapping it", async () => {
1231
1600
  const { tool, calls } = stubTool();
1232
1601
  await assert.rejects(
1233
1602
  tool.execute(
1234
- "id-min-8",
1603
+ "id-fold-score-single",
1235
1604
  {
1236
1605
  state: "hi",
1237
1606
  questions: {
1238
1607
  s: {
1239
1608
  type: "score",
1240
1609
  instructions: "How?",
1241
- criteria: { low: "bad", high: "good" },
1610
+ criteria: { only: "meh" },
1242
1611
  },
1243
1612
  },
1244
1613
  } as never,
@@ -1251,47 +1620,879 @@ describe("system_one tool", () => {
1251
1620
  assert.equal(calls(), 0);
1252
1621
  });
1253
1622
 
1254
- it("rejects choice questions with non-string array criteria", async () => {
1255
- const { tool, calls } = stubTool();
1256
- await assert.rejects(
1257
- tool.execute(
1258
- "id-min-9",
1259
- {
1260
- state: "hi",
1261
- questions: {
1262
- t: { type: "choice", instructions: "W?", criteria: [1, 2] },
1623
+ it("lifts bare noul outcomes into criteria", async () => {
1624
+ const { tool, request } = recordingTool();
1625
+ await tool.execute(
1626
+ "id-lift-noul",
1627
+ {
1628
+ state: "hi",
1629
+ questions: {
1630
+ n: {
1631
+ type: "noul",
1632
+ instructions: "Is it?",
1633
+ true: "it is",
1634
+ false: "it is not",
1263
1635
  },
1264
- } as never,
1265
- undefined,
1266
- undefined,
1267
- {} as never,
1268
- ),
1269
- /choice question "t" needs criteria as an object/,
1636
+ },
1637
+ } as never,
1638
+ undefined,
1639
+ undefined,
1640
+ {} as never,
1270
1641
  );
1271
- assert.equal(calls(), 0);
1642
+ assert.deepEqual(request().questions.n, {
1643
+ type: "noul",
1644
+ instructions: "Is it?",
1645
+ criteria: { true: "it is", false: "it is not" },
1646
+ });
1272
1647
  });
1273
1648
 
1274
- it("lets boundary-valid questions through (1 choice, 2 levels)", async () => {
1275
- const { tool, calls } = stubTool();
1649
+ it("merges choice labels spilled out of criteria", async () => {
1650
+ const { tool, request } = recordingTool();
1276
1651
  await tool.execute(
1277
- "id-min-5",
1652
+ "id-spilled-choice",
1278
1653
  {
1279
1654
  state: "hi",
1280
1655
  questions: {
1281
- c: { type: "choice", instructions: "W?", criteria: { a: null } },
1282
- s: {
1283
- type: "score",
1284
- instructions: "How?",
1285
- criteria: ["low", "high"],
1656
+ ownership: {
1657
+ type: "choice",
1658
+ instructions: "Who?",
1659
+ criteria: { sre_oncall: "" },
1660
+ account_team: "",
1661
+ ingest_service_owner: "",
1286
1662
  },
1287
- n: { type: "noul", instructions: "Is it?" },
1288
1663
  },
1289
1664
  } as never,
1290
1665
  undefined,
1291
1666
  undefined,
1292
1667
  {} as never,
1293
1668
  );
1294
- assert.equal(calls(), 1);
1669
+ assert.deepEqual(Object.keys(request().questions.ownership.criteria), [
1670
+ "sre_oncall",
1671
+ "account_team",
1672
+ "ingest_service_owner",
1673
+ ]);
1674
+ });
1675
+
1676
+ it("uses spilled labels as criteria when criteria is absent", async () => {
1677
+ const { tool, request } = recordingTool();
1678
+ await tool.execute(
1679
+ "id-spilled-only",
1680
+ {
1681
+ state: "hi",
1682
+ questions: {
1683
+ pick: {
1684
+ type: "choice",
1685
+ instructions: "Which?",
1686
+ billing: null,
1687
+ onboarding: null,
1688
+ },
1689
+ },
1690
+ } as never,
1691
+ undefined,
1692
+ undefined,
1693
+ {} as never,
1694
+ );
1695
+ assert.deepEqual(request().questions.pick.criteria, {
1696
+ billing: null,
1697
+ onboarding: null,
1698
+ });
1699
+ });
1700
+
1701
+ it("drops an empty sibling that is a noul question's own name", async () => {
1702
+ const { tool, request } = recordingTool();
1703
+ await tool.execute(
1704
+ "id-noul-selfname",
1705
+ {
1706
+ state: "hi",
1707
+ questions: {
1708
+ q: {
1709
+ type: "noul",
1710
+ instructions: "Is it?",
1711
+ is_coordinated_credential_stuffing_run: "",
1712
+ },
1713
+ },
1714
+ } as never,
1715
+ undefined,
1716
+ undefined,
1717
+ {} as never,
1718
+ );
1719
+ assert.deepEqual(request().questions.q, {
1720
+ type: "noul",
1721
+ instructions: "Is it?",
1722
+ });
1723
+ });
1724
+
1725
+ it("drops a NON-empty extra field rather than failing the call", async () => {
1726
+ const { tool, request } = recordingTool();
1727
+ await tool.execute(
1728
+ "id-nonempty-extra",
1729
+ {
1730
+ state: "hi",
1731
+ questions: {
1732
+ q: {
1733
+ type: "noul",
1734
+ instructions: "Is it?",
1735
+ confidence: 0.9,
1736
+ },
1737
+ },
1738
+ } as never,
1739
+ undefined,
1740
+ undefined,
1741
+ {} as never,
1742
+ );
1743
+ assert.deepEqual(request().questions.q, {
1744
+ type: "noul",
1745
+ instructions: "Is it?",
1746
+ });
1747
+ });
1748
+
1749
+ it("salvages the type from an extra key that names the kind", async () => {
1750
+ const { tool, request } = recordingTool();
1751
+ await tool.execute(
1752
+ "id-salvage-type",
1753
+ {
1754
+ state: "hi",
1755
+ questions: {
1756
+ q: {
1757
+ approach: "yes_no",
1758
+ name: "finishes_in_window",
1759
+ instructions: "Will it fit?",
1760
+ },
1761
+ },
1762
+ } as never,
1763
+ undefined,
1764
+ undefined,
1765
+ {} as never,
1766
+ );
1767
+ assert.deepEqual(request().questions.q, {
1768
+ type: "noul",
1769
+ instructions: "Will it fit?",
1770
+ });
1771
+ });
1772
+
1773
+ it("unwraps a single-key wrapper around an array of questions", async () => {
1774
+ const { tool, request } = recordingTool();
1775
+ await tool.execute(
1776
+ "id-wrapped-array",
1777
+ {
1778
+ state: "hi",
1779
+ questions: {
1780
+ item: [
1781
+ { name: "plan_it", type: "noul", instructions: "Should we?" },
1782
+ {
1783
+ name: "how_soon",
1784
+ type: "score",
1785
+ instructions: "How soon?",
1786
+ criteria: ["now", "later"],
1787
+ },
1788
+ ],
1789
+ },
1790
+ } as never,
1791
+ undefined,
1792
+ undefined,
1793
+ {} as never,
1794
+ );
1795
+ assert.deepEqual(Object.keys(request().questions), [
1796
+ "plan_it",
1797
+ "how_soon",
1798
+ ]);
1799
+ });
1800
+
1801
+ it("drops string entries beside a real question", async () => {
1802
+ const { tool, request } = recordingTool();
1803
+ await tool.execute(
1804
+ "id-string-entry",
1805
+ {
1806
+ state: "hi",
1807
+ questions: {
1808
+ refund_decision: {
1809
+ type: "choice",
1810
+ instructions: "Which?",
1811
+ criteria: { deny: null },
1812
+ },
1813
+ deny: "refund it",
1814
+ "PR #419 (feat: add metrics endpoint)": "merge it",
1815
+ },
1816
+ } as never,
1817
+ undefined,
1818
+ undefined,
1819
+ {} as never,
1820
+ );
1821
+ assert.deepEqual(Object.keys(request().questions), ["refund_decision"]);
1822
+ });
1823
+
1824
+ it("still rejects a question map of nothing but strings", async () => {
1825
+ const { tool, calls } = stubTool();
1826
+ await assert.rejects(
1827
+ tool.execute(
1828
+ "id-all-strings",
1829
+ {
1830
+ state: "hi",
1831
+ questions: { deny: "refund it", allow: "let it go" },
1832
+ } as never,
1833
+ undefined,
1834
+ undefined,
1835
+ {} as never,
1836
+ ),
1837
+ /must be an object with "type"/,
1838
+ );
1839
+ assert.equal(calls(), 0);
1840
+ });
1841
+
1842
+ it("merges a name-to-array question map", async () => {
1843
+ const { tool, request } = recordingTool();
1844
+ await tool.execute(
1845
+ "id-array-map",
1846
+ {
1847
+ state: "hi",
1848
+ questions: {
1849
+ fixes: [
1850
+ { type: "noul", instructions: "Is it safe?" },
1851
+ { type: "noul", instructions: "Is it cheap?" },
1852
+ ],
1853
+ risks: [
1854
+ {
1855
+ type: "score",
1856
+ instructions: "How bad?",
1857
+ criteria: ["low", "high"],
1858
+ },
1859
+ ],
1860
+ },
1861
+ } as never,
1862
+ undefined,
1863
+ undefined,
1864
+ {} as never,
1865
+ );
1866
+ assert.deepEqual(Object.keys(request().questions), [
1867
+ "fixes",
1868
+ "fixes_2",
1869
+ "risks",
1870
+ ]);
1871
+ });
1872
+
1873
+ it("unwraps one question filed under the wrapper's own name", async () => {
1874
+ const { tool, request } = recordingTool();
1875
+ await tool.execute(
1876
+ "id-named-questions",
1877
+ {
1878
+ state: "hi",
1879
+ questions: {
1880
+ questions: {
1881
+ type: "score",
1882
+ instructions: "How clear?",
1883
+ criteria: ["low", "high"],
1884
+ },
1885
+ },
1886
+ } as never,
1887
+ undefined,
1888
+ undefined,
1889
+ {} as never,
1890
+ );
1891
+ assert.deepEqual(Object.keys(request().questions), ["q"]);
1892
+ assert.equal(request().questions.q.type, "score");
1893
+ });
1894
+
1895
+ it("peels a wrapper nested more than one level deep", async () => {
1896
+ const { tool, request } = recordingTool();
1897
+ await tool.execute(
1898
+ "id-nested-wrapper",
1899
+ {
1900
+ state: "hi",
1901
+ questions: {
1902
+ impact: {
1903
+ type: "score",
1904
+ instructions: "How bad?",
1905
+ criteria: {
1906
+ item: { item: ["negligible", "moderate", "severe"] },
1907
+ },
1908
+ },
1909
+ },
1910
+ } as never,
1911
+ undefined,
1912
+ undefined,
1913
+ {} as never,
1914
+ );
1915
+ assert.deepEqual(request().questions.impact.criteria, [
1916
+ "negligible",
1917
+ "moderate",
1918
+ "severe",
1919
+ ]);
1920
+ });
1921
+
1922
+ it("still rejects a one-level rubric nested in a wrapper", async () => {
1923
+ const { tool, calls } = stubTool();
1924
+ await assert.rejects(
1925
+ tool.execute(
1926
+ "id-nested-one-level",
1927
+ {
1928
+ state: "hi",
1929
+ questions: {
1930
+ impact: {
1931
+ type: "score",
1932
+ instructions: "How bad?",
1933
+ criteria: { item: { only: "meh" } },
1934
+ },
1935
+ },
1936
+ } as never,
1937
+ undefined,
1938
+ undefined,
1939
+ {} as never,
1940
+ ),
1941
+ /score question "impact" needs criteria as an ordered array/,
1942
+ );
1943
+ assert.equal(calls(), 0);
1944
+ });
1945
+
1946
+ it("transposes parallel arrays into named questions", async () => {
1947
+ const { tool, request } = recordingTool();
1948
+ await tool.execute(
1949
+ "id-parallel-arrays",
1950
+ {
1951
+ state: "hi",
1952
+ questions: {
1953
+ item: ["refund", "risk", "followup"],
1954
+ type: ["choice", "score", "noul"],
1955
+ instructions: [
1956
+ "Which action?",
1957
+ "How risky?",
1958
+ "Will they follow up?",
1959
+ ],
1960
+ criteria: [{ refund: null, deny: null }, ["low", "high"], null],
1961
+ },
1962
+ } as never,
1963
+ undefined,
1964
+ undefined,
1965
+ {} as never,
1966
+ );
1967
+ const asked = request().questions;
1968
+ assert.deepEqual(Object.keys(asked), ["refund", "risk", "followup"]);
1969
+ assert.equal(asked.refund.type, "choice");
1970
+ assert.deepEqual(asked.risk.criteria, ["low", "high"]);
1971
+ assert.equal(asked.followup.type, "noul");
1972
+ });
1973
+
1974
+ it("unwraps a question under the wrapper name beside other junk keys", async () => {
1975
+ const { tool, request } = recordingTool();
1976
+ await tool.execute(
1977
+ "id-alias-siblings",
1978
+ {
1979
+ state: "hi",
1980
+ questions: {
1981
+ policy: "",
1982
+ questions: {
1983
+ type: "choice",
1984
+ instructions: "Which policy?",
1985
+ criteria: { a: null, b: null },
1986
+ },
1987
+ },
1988
+ } as never,
1989
+ undefined,
1990
+ undefined,
1991
+ {} as never,
1992
+ );
1993
+ assert.deepEqual(Object.keys(request().questions), ["q"]);
1994
+ assert.equal(request().questions.q.type, "choice");
1995
+ });
1996
+
1997
+ it("drops a question FIELD that landed in the name slot", async () => {
1998
+ const { tool, request } = recordingTool();
1999
+ await tool.execute(
2000
+ "id-field-in-name-slot",
2001
+ {
2002
+ state: "hi",
2003
+ questions: {
2004
+ pick: {
2005
+ type: "choice",
2006
+ instructions: "Which library?",
2007
+ criteria: { date_fns: null, dayjs: null },
2008
+ },
2009
+ criteria: { stray: true },
2010
+ instructions: "stray",
2011
+ },
2012
+ } as never,
2013
+ undefined,
2014
+ undefined,
2015
+ {} as never,
2016
+ );
2017
+ assert.deepEqual(Object.keys(request().questions), ["pick"]);
2018
+ });
2019
+
2020
+ it("splices a question map that landed in a name slot", async () => {
2021
+ const { tool, request } = recordingTool();
2022
+ await tool.execute(
2023
+ "id-nested-map",
2024
+ {
2025
+ state: "hi",
2026
+ questions: {
2027
+ "Which single library should this CLI adopt?": {
2028
+ which_date_library: {
2029
+ type: "choice",
2030
+ instructions: "Which one?",
2031
+ criteria: { date_fns: null, dayjs: null },
2032
+ },
2033
+ },
2034
+ },
2035
+ } as never,
2036
+ undefined,
2037
+ undefined,
2038
+ {} as never,
2039
+ );
2040
+ assert.deepEqual(Object.keys(request().questions), [
2041
+ "which_date_library",
2042
+ ]);
2043
+ assert.equal(request().questions.which_date_library.type, "choice");
2044
+ });
2045
+
2046
+ it("does not mistake a question for a nested map", async () => {
2047
+ const { tool, request } = recordingTool();
2048
+ await tool.execute(
2049
+ "id-not-nested",
2050
+ {
2051
+ state: "hi",
2052
+ questions: {
2053
+ pick: {
2054
+ type: "choice",
2055
+ instructions: "Which one?",
2056
+ criteria: { a: null, b: null },
2057
+ },
2058
+ },
2059
+ } as never,
2060
+ undefined,
2061
+ undefined,
2062
+ {} as never,
2063
+ );
2064
+ assert.deepEqual(Object.keys(request().questions), ["pick"]);
2065
+ });
2066
+
2067
+ it("takes description as the instructions when there are none", async () => {
2068
+ const { tool, request } = recordingTool();
2069
+ await tool.execute(
2070
+ "id-description-instructions",
2071
+ {
2072
+ state: "hi",
2073
+ questions: {
2074
+ q: {
2075
+ type: "score",
2076
+ description: "Rate the documentation quality.",
2077
+ criteria: ["low", "high"],
2078
+ },
2079
+ },
2080
+ } as never,
2081
+ undefined,
2082
+ undefined,
2083
+ {} as never,
2084
+ );
2085
+ assert.equal(
2086
+ request().questions.q.instructions,
2087
+ "Rate the documentation quality.",
2088
+ );
2089
+ });
2090
+
2091
+ it("keeps real instructions ahead of a description", async () => {
2092
+ const { tool, request } = recordingTool();
2093
+ await tool.execute(
2094
+ "id-description-loser",
2095
+ {
2096
+ state: "hi",
2097
+ questions: {
2098
+ q: {
2099
+ type: "noul",
2100
+ instructions: "Is it?",
2101
+ description: "some metadata",
2102
+ },
2103
+ },
2104
+ } as never,
2105
+ undefined,
2106
+ undefined,
2107
+ {} as never,
2108
+ );
2109
+ assert.deepEqual(request().questions.q, {
2110
+ type: "noul",
2111
+ instructions: "Is it?",
2112
+ });
2113
+ });
2114
+
2115
+ it("splices a question map nested more than one level deep", async () => {
2116
+ const { tool, request } = recordingTool();
2117
+ await tool.execute(
2118
+ "id-nested-two-deep",
2119
+ {
2120
+ state: "hi",
2121
+ questions: {
2122
+ no_audit: {
2123
+ no_audit: {
2124
+ q1: {
2125
+ type: "choice",
2126
+ instructions: "Which action?",
2127
+ criteria: { keep: null, replace: null },
2128
+ },
2129
+ },
2130
+ },
2131
+ },
2132
+ } as never,
2133
+ undefined,
2134
+ undefined,
2135
+ {} as never,
2136
+ );
2137
+ assert.deepEqual(Object.keys(request().questions), ["q1"]);
2138
+ assert.equal(request().questions.q1.type, "choice");
2139
+ });
2140
+
2141
+ it("lifts a state nested inside a question to the top level", async () => {
2142
+ const { tool, request } = recordingTool();
2143
+ await tool.execute(
2144
+ "id-lift-state",
2145
+ {
2146
+ questions: [
2147
+ {
2148
+ type: "score",
2149
+ instructions: "How clear?",
2150
+ criteria: ["low", "high"],
2151
+ state: { readme: "## Usage\nRun it." },
2152
+ },
2153
+ ],
2154
+ } as never,
2155
+ undefined,
2156
+ undefined,
2157
+ {} as never,
2158
+ );
2159
+ assert.deepEqual(request().state, { readme: "## Usage\nRun it." });
2160
+ assert.equal(
2161
+ Object.hasOwn(request().questions.q1, "state"),
2162
+ false,
2163
+ "the state must be removed from the question it was found in",
2164
+ );
2165
+ });
2166
+
2167
+ it("lifts context and evidence aliases too", async () => {
2168
+ const { tool, request } = recordingTool();
2169
+ await tool.execute(
2170
+ "id-lift-evidence",
2171
+ {
2172
+ questions: {
2173
+ q: {
2174
+ type: "noul",
2175
+ instructions: "Is it?",
2176
+ evidence: { signal: "weak" },
2177
+ },
2178
+ },
2179
+ } as never,
2180
+ undefined,
2181
+ undefined,
2182
+ {} as never,
2183
+ );
2184
+ assert.deepEqual(request().state, { signal: "weak" });
2185
+ });
2186
+
2187
+ it("keeps a top-level state ahead of a nested one", async () => {
2188
+ const { tool, request } = recordingTool();
2189
+ await tool.execute(
2190
+ "id-lift-state-priority",
2191
+ {
2192
+ state: "the real state",
2193
+ questions: {
2194
+ q: { type: "noul", instructions: "Is it?", state: "stray" },
2195
+ },
2196
+ } as never,
2197
+ undefined,
2198
+ undefined,
2199
+ {} as never,
2200
+ );
2201
+ assert.equal(request().state, "the real state");
2202
+ });
2203
+
2204
+ it("leaves a criteria key named state alone — that is an option label", async () => {
2205
+ const { tool, request } = recordingTool();
2206
+ await tool.execute(
2207
+ "id-criteria-state-label",
2208
+ {
2209
+ state: "s",
2210
+ questions: {
2211
+ q: {
2212
+ type: "choice",
2213
+ instructions: "Which level?",
2214
+ criteria: { state: null, federal: null },
2215
+ },
2216
+ },
2217
+ } as never,
2218
+ undefined,
2219
+ undefined,
2220
+ {} as never,
2221
+ );
2222
+ assert.deepEqual(Object.keys(request().questions.q.criteria), [
2223
+ "state",
2224
+ "federal",
2225
+ ]);
2226
+ });
2227
+
2228
+ it("does not mutate its input when lifting", () => {
2229
+ const deepFreeze = (v: unknown): void => {
2230
+ if (typeof v === "object" && v !== null && !Object.isFrozen(v)) {
2231
+ Object.freeze(v);
2232
+ for (const k of Object.keys(v)) {
2233
+ deepFreeze((v as Record<string, unknown>)[k]);
2234
+ }
2235
+ }
2236
+ };
2237
+ const input = {
2238
+ questions: {
2239
+ q: { type: "noul", instructions: "Is it?", state: "s" },
2240
+ },
2241
+ };
2242
+ deepFreeze(input);
2243
+ const out = prepareSystemOneArgs(input);
2244
+ assert.deepEqual(out, {
2245
+ state: "s",
2246
+ questions: { q: { type: "noul", instructions: "Is it?" } },
2247
+ });
2248
+ });
2249
+
2250
+ it("strips a nested state even when a top-level one is supplied", async () => {
2251
+ // Regression: the lift used to early-return when a real top-level state
2252
+ // existed, leaving `state` inside the question where the provider could
2253
+ // see it. A question must never carry what it is judged against.
2254
+ const { tool, request } = recordingTool();
2255
+ await tool.execute(
2256
+ "id-lift-strip-always",
2257
+ {
2258
+ state: "the real state",
2259
+ questions: {
2260
+ q: { type: "noul", instructions: "Is it?", state: { stray: 1 } },
2261
+ },
2262
+ } as never,
2263
+ undefined,
2264
+ undefined,
2265
+ {} as never,
2266
+ );
2267
+ assert.equal(request().state, "the real state");
2268
+ assert.deepEqual(request().questions.q, {
2269
+ type: "noul",
2270
+ instructions: "Is it?",
2271
+ });
2272
+ });
2273
+
2274
+ it("strips every state alias in one pass (idempotent)", async () => {
2275
+ const freeze = (v: unknown): void => {
2276
+ if (typeof v === "object" && v !== null && !Object.isFrozen(v)) {
2277
+ Object.freeze(v);
2278
+ for (const k of Object.keys(v)) {
2279
+ freeze((v as Record<string, unknown>)[k]);
2280
+ }
2281
+ }
2282
+ };
2283
+ const input = {
2284
+ questions: {
2285
+ q: {
2286
+ type: "noul",
2287
+ instructions: "Is it?",
2288
+ state: { a: 1 },
2289
+ context: { b: 2 },
2290
+ evidence: { c: 3 },
2291
+ },
2292
+ },
2293
+ };
2294
+ freeze(input);
2295
+ const once = prepareSystemOneArgs(input);
2296
+ const twice = prepareSystemOneArgs(once);
2297
+ assert.deepEqual(twice, once);
2298
+ assert.deepEqual(once, {
2299
+ state: { a: 1 },
2300
+ questions: { q: { type: "noul", instructions: "Is it?" } },
2301
+ });
2302
+ });
2303
+
2304
+ it("drops true/false from a question that is not a noul", async () => {
2305
+ // Regression: they are lifted into `criteria` on a noul question and
2306
+ // mean nothing elsewhere, but the copy-on-write fast path used to ship
2307
+ // them to the provider on a choice.
2308
+ const { tool, request } = recordingTool();
2309
+ await tool.execute(
2310
+ "id-true-false-choice",
2311
+ {
2312
+ state: "hi",
2313
+ questions: {
2314
+ q: {
2315
+ type: "choice",
2316
+ instructions: "Which?",
2317
+ criteria: { a: null },
2318
+ true: "yes",
2319
+ false: "no",
2320
+ },
2321
+ },
2322
+ } as never,
2323
+ undefined,
2324
+ undefined,
2325
+ {} as never,
2326
+ );
2327
+ assert.deepEqual(request().questions.q, {
2328
+ type: "choice",
2329
+ instructions: "Which?",
2330
+ criteria: { a: null },
2331
+ });
2332
+ });
2333
+
2334
+ it("stays idempotent when an array mixes junk with questions", () => {
2335
+ // Regression: the array branch kept non-question entries while the
2336
+ // record branch dropped them, so `["junk", {a question}]` normalized to
2337
+ // different maps on the first and second pass.
2338
+ const input = {
2339
+ state: "S",
2340
+ questions: ["junk", { type: "noul", instructions: "Is it?" }, "junk"],
2341
+ };
2342
+ assert.deepEqual(prepareSystemOneArgs(input), {
2343
+ state: "S",
2344
+ questions: { q2: { type: "noul", instructions: "Is it?" } },
2345
+ });
2346
+ const once = prepareSystemOneArgs(input);
2347
+ assert.deepEqual(prepareSystemOneArgs(once), once);
2348
+ });
2349
+
2350
+ it("does not unwrap an array of non-questions", () => {
2351
+ // Regression: `{q1: [...]}` is a question map whose one entry is not a
2352
+ // question. Unwrapping it renamed the junk on every pass.
2353
+ const input = { state: "S", questions: { q1: ["text", "text"] } };
2354
+ const once = prepareSystemOneArgs(input);
2355
+ assert.deepEqual(prepareSystemOneArgs(once), once);
2356
+ assert.deepEqual(once, {
2357
+ state: "S",
2358
+ questions: { q1: ["text", "text"] },
2359
+ });
2360
+ });
2361
+
2362
+ it("rejects score questions with a one-element array rubric", async () => {
2363
+ const { tool, calls } = stubTool();
2364
+ await assert.rejects(
2365
+ tool.execute(
2366
+ "id-min-8",
2367
+ {
2368
+ state: "hi",
2369
+ questions: {
2370
+ s: {
2371
+ type: "score",
2372
+ instructions: "How?",
2373
+ criteria: ["lonely"],
2374
+ },
2375
+ },
2376
+ } as never,
2377
+ undefined,
2378
+ undefined,
2379
+ {} as never,
2380
+ ),
2381
+ /score question "s" needs criteria as an ordered array/,
2382
+ );
2383
+ assert.equal(calls(), 0);
2384
+ });
2385
+
2386
+ it("rejects choice questions with non-string array criteria", async () => {
2387
+ const { tool, calls } = stubTool();
2388
+ await assert.rejects(
2389
+ tool.execute(
2390
+ "id-min-9",
2391
+ {
2392
+ state: "hi",
2393
+ questions: {
2394
+ t: { type: "choice", instructions: "W?", criteria: [1, 2] },
2395
+ },
2396
+ } as never,
2397
+ undefined,
2398
+ undefined,
2399
+ {} as never,
2400
+ ),
2401
+ /choice question "t" needs criteria as an object/,
2402
+ );
2403
+ assert.equal(calls(), 0);
2404
+ });
2405
+
2406
+ it("lets boundary-valid questions through (1 choice, 2 levels)", async () => {
2407
+ const { tool, calls } = stubTool();
2408
+ await tool.execute(
2409
+ "id-min-5",
2410
+ {
2411
+ state: "hi",
2412
+ questions: {
2413
+ c: { type: "choice", instructions: "W?", criteria: { a: null } },
2414
+ s: {
2415
+ type: "score",
2416
+ instructions: "How?",
2417
+ criteria: ["low", "high"],
2418
+ },
2419
+ n: { type: "noul", instructions: "Is it?" },
2420
+ },
2421
+ } as never,
2422
+ undefined,
2423
+ undefined,
2424
+ {} as never,
2425
+ );
2426
+ assert.equal(calls(), 1);
2427
+ });
2428
+ });
2429
+ });
2430
+
2431
+ describe("discarded question reporting", () => {
2432
+ function toolWithAnswers(answers: Record<string, unknown>) {
2433
+ const stub = {
2434
+ id: "stub",
2435
+ async evaluate() {
2436
+ return { answers, metadata: { provider: "stub" } };
2437
+ },
2438
+ };
2439
+ return buildSystemOneTool({ provider: stub as never });
2440
+ }
2441
+
2442
+ async function renderOf(args: unknown): Promise<string> {
2443
+ const tool = toolWithAnswers({
2444
+ kept: { type: "noul", noul: 0.7 },
2445
+ });
2446
+ const result = await tool.execute(
2447
+ "id",
2448
+ args as never,
2449
+ undefined,
2450
+ undefined,
2451
+ {} as never,
2452
+ );
2453
+ return (result.content as Array<{ text: string }>)[0].text;
2454
+ }
2455
+
2456
+ // A real defect the scenario suite could not see: the model sends a
2457
+ // two-question batch, one of which is filed under the wrapper's own name.
2458
+ // The unwrap returned that question and dropped its sibling silently, so
2459
+ // the model received a confident answer to half of what it asked.
2460
+ it("reports a question abandoned by the wrapper-alias unwrap", async () => {
2461
+ const text = await renderOf({
2462
+ state: "an outage and a refund",
2463
+ questions: {
2464
+ questions: {
2465
+ type: "choice",
2466
+ instructions: "Which?",
2467
+ criteria: { a: null },
2468
+ },
2469
+ risk: { type: "noul", instructions: "Is there a risk?" },
2470
+ },
2471
+ });
2472
+ assert.match(text, /NOTE:/);
2473
+ assert.match(text, /discarded/);
2474
+ assert.match(text, /risk/);
2475
+ // The kept answer is still complete and still first.
2476
+ assert.match(text, /kept:[\s\S]*noul: 0\.7/);
2477
+ });
2478
+
2479
+ it("reports a name-slot entry dropped beside a real question", async () => {
2480
+ const text = await renderOf({
2481
+ state: "S",
2482
+ questions: {
2483
+ q1: { type: "noul", instructions: "Is it?" },
2484
+ stray_label: "refund it",
2485
+ },
2486
+ });
2487
+ assert.match(text, /NOTE:/);
2488
+ assert.match(text, /stray_label/);
2489
+ });
2490
+
2491
+ it("adds no note when nothing was discarded", async () => {
2492
+ const text = await renderOf({
2493
+ state: "S",
2494
+ questions: { q1: { type: "noul", instructions: "Is it?" } },
1295
2495
  });
2496
+ assert.doesNotMatch(text, /NOTE:/);
1296
2497
  });
1297
2498
  });