pi-system-one 1.2.1 → 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", () => {
@@ -1167,7 +1223,7 @@ describe("system_one tool", () => {
1167
1223
  instructions: "Rate?",
1168
1224
  criteria: ["1", "5"],
1169
1225
  },
1170
- b: { type: "noul", instructions: "OK?", extra: 1 },
1226
+ b: { type: "noul", instructions: "OK?" },
1171
1227
  },
1172
1228
  });
1173
1229
  });
@@ -1466,18 +1522,92 @@ describe("system_one tool", () => {
1466
1522
  assert.equal(calls(), 0);
1467
1523
  });
1468
1524
 
1469
- 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 () => {
1470
1600
  const { tool, calls } = stubTool();
1471
1601
  await assert.rejects(
1472
1602
  tool.execute(
1473
- "id-min-8",
1603
+ "id-fold-score-single",
1474
1604
  {
1475
1605
  state: "hi",
1476
1606
  questions: {
1477
1607
  s: {
1478
1608
  type: "score",
1479
1609
  instructions: "How?",
1480
- criteria: { low: "bad", high: "good" },
1610
+ criteria: { only: "meh" },
1481
1611
  },
1482
1612
  },
1483
1613
  } as never,
@@ -1490,40 +1620,803 @@ describe("system_one tool", () => {
1490
1620
  assert.equal(calls(), 0);
1491
1621
  });
1492
1622
 
1493
- it("rejects choice questions with non-string array criteria", async () => {
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",
1635
+ },
1636
+ },
1637
+ } as never,
1638
+ undefined,
1639
+ undefined,
1640
+ {} as never,
1641
+ );
1642
+ assert.deepEqual(request().questions.n, {
1643
+ type: "noul",
1644
+ instructions: "Is it?",
1645
+ criteria: { true: "it is", false: "it is not" },
1646
+ });
1647
+ });
1648
+
1649
+ it("merges choice labels spilled out of criteria", async () => {
1650
+ const { tool, request } = recordingTool();
1651
+ await tool.execute(
1652
+ "id-spilled-choice",
1653
+ {
1654
+ state: "hi",
1655
+ questions: {
1656
+ ownership: {
1657
+ type: "choice",
1658
+ instructions: "Who?",
1659
+ criteria: { sre_oncall: "" },
1660
+ account_team: "",
1661
+ ingest_service_owner: "",
1662
+ },
1663
+ },
1664
+ } as never,
1665
+ undefined,
1666
+ undefined,
1667
+ {} as never,
1668
+ );
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 () => {
1494
1825
  const { tool, calls } = stubTool();
1495
1826
  await assert.rejects(
1496
1827
  tool.execute(
1497
- "id-min-9",
1828
+ "id-all-strings",
1498
1829
  {
1499
1830
  state: "hi",
1500
- questions: {
1501
- t: { type: "choice", instructions: "W?", criteria: [1, 2] },
1502
- },
1831
+ questions: { deny: "refund it", allow: "let it go" },
1503
1832
  } as never,
1504
1833
  undefined,
1505
1834
  undefined,
1506
1835
  {} as never,
1507
1836
  ),
1508
- /choice question "t" needs criteria as an object/,
1837
+ /must be an object with "type"/,
1509
1838
  );
1510
1839
  assert.equal(calls(), 0);
1511
1840
  });
1512
1841
 
1513
- it("lets boundary-valid questions through (1 choice, 2 levels)", async () => {
1514
- const { tool, calls } = stubTool();
1842
+ it("merges a name-to-array question map", async () => {
1843
+ const { tool, request } = recordingTool();
1515
1844
  await tool.execute(
1516
- "id-min-5",
1845
+ "id-array-map",
1517
1846
  {
1518
1847
  state: "hi",
1519
1848
  questions: {
1520
- c: { type: "choice", instructions: "W?", criteria: { a: null } },
1521
- s: {
1522
- type: "score",
1523
- instructions: "How?",
1524
- criteria: ["low", "high"],
1525
- },
1526
- n: { type: "noul", instructions: "Is it?" },
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?" },
1527
2420
  },
1528
2421
  } as never,
1529
2422
  undefined,
@@ -1534,3 +2427,72 @@ describe("system_one tool", () => {
1534
2427
  });
1535
2428
  });
1536
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?" } },
2495
+ });
2496
+ assert.doesNotMatch(text, /NOTE:/);
2497
+ });
2498
+ });