@hasna/loops 0.4.14 → 0.4.22

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 (48) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +119 -29
  3. package/dist/api/index.d.ts +1 -0
  4. package/dist/api/index.js +825 -7
  5. package/dist/cli/index.js +5419 -2619
  6. package/dist/daemon/index.js +478 -84
  7. package/dist/generated/storage-kit/index.d.ts +1 -1
  8. package/dist/index.js +2521 -226
  9. package/dist/lib/cloud/mode.d.ts +17 -0
  10. package/dist/lib/cloud/resolve.d.ts +16 -0
  11. package/dist/lib/cloud/storage.d.ts +82 -0
  12. package/dist/lib/cloud/transport.d.ts +148 -0
  13. package/dist/lib/format.d.ts +2 -1
  14. package/dist/lib/migration.d.ts +40 -0
  15. package/dist/lib/mode.js +3 -3
  16. package/dist/lib/route/index.d.ts +2 -0
  17. package/dist/lib/route/policies.d.ts +51 -0
  18. package/dist/lib/route/provider-admission.d.ts +37 -0
  19. package/dist/lib/route/throttle.d.ts +4 -0
  20. package/dist/lib/route/types.d.ts +8 -0
  21. package/dist/lib/run-receipts.d.ts +14 -0
  22. package/dist/lib/storage/contract.d.ts +7 -1
  23. package/dist/lib/storage/index.js +710 -31
  24. package/dist/lib/storage/postgres-loop-storage.d.ts +6 -0
  25. package/dist/lib/storage/postgres-schema.js +29 -0
  26. package/dist/lib/storage/postgres.js +29 -0
  27. package/dist/lib/storage/sqlite.d.ts +6 -0
  28. package/dist/lib/storage/sqlite.js +412 -18
  29. package/dist/lib/store/index.d.ts +268 -0
  30. package/dist/lib/store.d.ts +48 -1
  31. package/dist/lib/store.js +396 -18
  32. package/dist/lib/template-kit.d.ts +25 -0
  33. package/dist/lib/templates.d.ts +16 -1
  34. package/dist/mcp/http.d.ts +16 -0
  35. package/dist/mcp/index.js +1658 -168
  36. package/dist/runner/index.js +76 -9
  37. package/dist/sdk/http.d.ts +67 -0
  38. package/dist/sdk/http.js +21 -0
  39. package/dist/sdk/index.d.ts +50 -17
  40. package/dist/sdk/index.js +1509 -132
  41. package/dist/serve/index.js +1598 -122
  42. package/dist/types.d.ts +49 -0
  43. package/docs/AUTOMATION_RUNTIME_DESIGN.md +442 -1
  44. package/docs/CUTOVER-RUNBOOK.md +73 -56
  45. package/docs/DEPLOYMENT_MODES.md +35 -18
  46. package/docs/RUNTIME_BOUNDARY.md +203 -0
  47. package/docs/USAGE.md +145 -27
  48. package/package.json +3 -3
package/dist/api/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@hasna/loops",
6
- version: "0.4.14",
6
+ version: "0.4.22",
7
7
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
8
8
  type: "module",
9
9
  main: "dist/index.js",
@@ -113,7 +113,6 @@ var package_default = {
113
113
  bun: ">=1.0.0"
114
114
  },
115
115
  dependencies: {
116
- "@hasna/contracts": "^0.4.1",
117
116
  "@hasna/events": "^0.1.9",
118
117
  "@modelcontextprotocol/sdk": "^1.29.0",
119
118
  "@openrouter/ai-sdk-provider": "2.9.1",
@@ -123,7 +122,8 @@ var package_default = {
123
122
  zod: "4.4.3"
124
123
  },
125
124
  optionalDependencies: {
126
- "@hasna/machines": "0.0.49"
125
+ "@hasna/machines": "0.0.49",
126
+ "@hasna/contracts": "^0.4.2"
127
127
  },
128
128
  devDependencies: {
129
129
  "@types/bun": "latest",
@@ -438,6 +438,9 @@ function publicRun(run, showOutput = false, opts = {}) {
438
438
  error: opts.redactError ? redact(run.error) : run.error
439
439
  };
440
440
  }
441
+ function publicRunReceipt(receipt) {
442
+ return { ...receipt };
443
+ }
441
444
  function publicExecutorResult(result, showOutput = false) {
442
445
  return {
443
446
  ...result,
@@ -989,6 +992,50 @@ var loops_default = {
989
992
  }
990
993
  }
991
994
  },
995
+ "/v1/loops/count": {
996
+ get: {
997
+ operationId: "countLoops",
998
+ summary: "Count loops (total-row verification)",
999
+ parameters: [
1000
+ {
1001
+ name: "status",
1002
+ in: "query",
1003
+ required: false,
1004
+ schema: {
1005
+ type: "string"
1006
+ }
1007
+ },
1008
+ {
1009
+ name: "includeArchived",
1010
+ in: "query",
1011
+ required: false,
1012
+ schema: {
1013
+ type: "boolean"
1014
+ }
1015
+ },
1016
+ {
1017
+ name: "archived",
1018
+ in: "query",
1019
+ required: false,
1020
+ schema: {
1021
+ type: "boolean"
1022
+ }
1023
+ }
1024
+ ],
1025
+ responses: {
1026
+ "200": {
1027
+ description: "count",
1028
+ content: {
1029
+ "application/json": {
1030
+ schema: {
1031
+ $ref: "#/components/schemas/CountResponse"
1032
+ }
1033
+ }
1034
+ }
1035
+ }
1036
+ }
1037
+ }
1038
+ },
992
1039
  "/v1/loops/{id}": {
993
1040
  get: {
994
1041
  operationId: "getLoop",
@@ -1179,6 +1226,34 @@ var loops_default = {
1179
1226
  }
1180
1227
  }
1181
1228
  },
1229
+ "/v1/runs/count": {
1230
+ get: {
1231
+ operationId: "countRuns",
1232
+ summary: "Count runs (total-row verification)",
1233
+ parameters: [
1234
+ {
1235
+ name: "status",
1236
+ in: "query",
1237
+ required: false,
1238
+ schema: {
1239
+ type: "string"
1240
+ }
1241
+ }
1242
+ ],
1243
+ responses: {
1244
+ "200": {
1245
+ description: "count",
1246
+ content: {
1247
+ "application/json": {
1248
+ schema: {
1249
+ $ref: "#/components/schemas/CountResponse"
1250
+ }
1251
+ }
1252
+ }
1253
+ }
1254
+ }
1255
+ }
1256
+ },
1182
1257
  "/v1/runs/{id}": {
1183
1258
  get: {
1184
1259
  operationId: "getRun",
@@ -1206,6 +1281,157 @@ var loops_default = {
1206
1281
  }
1207
1282
  }
1208
1283
  }
1284
+ },
1285
+ "/v1/import": {
1286
+ post: {
1287
+ operationId: "importRows",
1288
+ summary: "Bulk id-preserving import (self-hosted backfill)",
1289
+ description: "Upsert full workflow/loop/run rows by id (idempotent; ON CONFLICT(id) DO UPDATE). Preserves id, status, archived state, and timestamps. Applied FK-safe: workflows, then loops, then runs. Volatile running runs are skipped and reported.",
1290
+ requestBody: {
1291
+ required: true,
1292
+ content: {
1293
+ "application/json": {
1294
+ schema: {
1295
+ $ref: "#/components/schemas/ImportInput"
1296
+ }
1297
+ }
1298
+ }
1299
+ },
1300
+ responses: {
1301
+ "200": {
1302
+ description: "import counts",
1303
+ content: {
1304
+ "application/json": {
1305
+ schema: {
1306
+ $ref: "#/components/schemas/ImportResponse"
1307
+ }
1308
+ }
1309
+ }
1310
+ }
1311
+ }
1312
+ }
1313
+ },
1314
+ "/v1/receipts": {
1315
+ get: {
1316
+ operationId: "listRunReceipts",
1317
+ summary: "List run receipts",
1318
+ parameters: [
1319
+ {
1320
+ name: "loopId",
1321
+ in: "query",
1322
+ required: false,
1323
+ schema: {
1324
+ type: "string"
1325
+ }
1326
+ },
1327
+ {
1328
+ name: "repo",
1329
+ in: "query",
1330
+ required: false,
1331
+ schema: {
1332
+ type: "string"
1333
+ }
1334
+ },
1335
+ {
1336
+ name: "taskId",
1337
+ in: "query",
1338
+ required: false,
1339
+ schema: {
1340
+ type: "string"
1341
+ }
1342
+ },
1343
+ {
1344
+ name: "knowledgeId",
1345
+ in: "query",
1346
+ required: false,
1347
+ schema: {
1348
+ type: "string"
1349
+ }
1350
+ },
1351
+ {
1352
+ name: "status",
1353
+ in: "query",
1354
+ required: false,
1355
+ schema: {
1356
+ type: "string"
1357
+ }
1358
+ },
1359
+ {
1360
+ name: "limit",
1361
+ in: "query",
1362
+ required: false,
1363
+ schema: {
1364
+ type: "integer"
1365
+ }
1366
+ }
1367
+ ],
1368
+ responses: {
1369
+ "200": {
1370
+ description: "run receipts",
1371
+ content: {
1372
+ "application/json": {
1373
+ schema: {
1374
+ $ref: "#/components/schemas/RunReceiptListResponse"
1375
+ }
1376
+ }
1377
+ }
1378
+ }
1379
+ }
1380
+ },
1381
+ post: {
1382
+ operationId: "writeRunReceipt",
1383
+ summary: "Write a run receipt",
1384
+ requestBody: {
1385
+ required: true,
1386
+ content: {
1387
+ "application/json": {
1388
+ schema: {
1389
+ $ref: "#/components/schemas/WriteRunReceiptInput"
1390
+ }
1391
+ }
1392
+ }
1393
+ },
1394
+ responses: {
1395
+ "201": {
1396
+ description: "run receipt",
1397
+ content: {
1398
+ "application/json": {
1399
+ schema: {
1400
+ $ref: "#/components/schemas/RunReceiptResponse"
1401
+ }
1402
+ }
1403
+ }
1404
+ }
1405
+ }
1406
+ }
1407
+ },
1408
+ "/v1/receipts/{runId}": {
1409
+ get: {
1410
+ operationId: "getRunReceipt",
1411
+ summary: "Get a run receipt by run id",
1412
+ parameters: [
1413
+ {
1414
+ name: "runId",
1415
+ in: "path",
1416
+ required: true,
1417
+ schema: {
1418
+ type: "string"
1419
+ }
1420
+ }
1421
+ ],
1422
+ responses: {
1423
+ "200": {
1424
+ description: "run receipt",
1425
+ content: {
1426
+ "application/json": {
1427
+ schema: {
1428
+ $ref: "#/components/schemas/RunReceiptResponse"
1429
+ }
1430
+ }
1431
+ }
1432
+ }
1433
+ }
1434
+ }
1209
1435
  }
1210
1436
  },
1211
1437
  components: {
@@ -1439,6 +1665,334 @@ var loops_default = {
1439
1665
  "ok",
1440
1666
  "deleted"
1441
1667
  ]
1668
+ },
1669
+ RunReceiptMachine: {
1670
+ oneOf: [
1671
+ {
1672
+ type: "string"
1673
+ },
1674
+ {
1675
+ type: "object",
1676
+ additionalProperties: true
1677
+ }
1678
+ ]
1679
+ },
1680
+ RunReceiptSummary: {
1681
+ type: "object",
1682
+ required: [
1683
+ "stdout_bytes",
1684
+ "stderr_bytes"
1685
+ ],
1686
+ properties: {
1687
+ text: {
1688
+ type: "string"
1689
+ },
1690
+ stdout_bytes: {
1691
+ type: "integer",
1692
+ minimum: 0
1693
+ },
1694
+ stderr_bytes: {
1695
+ type: "integer",
1696
+ minimum: 0
1697
+ },
1698
+ stdout_excerpt: {
1699
+ type: "string"
1700
+ },
1701
+ stderr_excerpt: {
1702
+ type: "string"
1703
+ },
1704
+ error: {
1705
+ type: "string"
1706
+ },
1707
+ duration_ms: {
1708
+ type: "integer",
1709
+ minimum: 0
1710
+ }
1711
+ },
1712
+ additionalProperties: false
1713
+ },
1714
+ RunReceipt: {
1715
+ type: "object",
1716
+ required: [
1717
+ "loop_id",
1718
+ "run_id",
1719
+ "machine",
1720
+ "repo",
1721
+ "task_ids",
1722
+ "knowledge_ids",
1723
+ "digest_id",
1724
+ "started_at",
1725
+ "finished_at",
1726
+ "status",
1727
+ "exit_code",
1728
+ "summary",
1729
+ "evidence_paths",
1730
+ "created_at",
1731
+ "updated_at"
1732
+ ],
1733
+ properties: {
1734
+ loop_id: {
1735
+ type: "string"
1736
+ },
1737
+ run_id: {
1738
+ type: "string"
1739
+ },
1740
+ machine: {
1741
+ $ref: "#/components/schemas/RunReceiptMachine"
1742
+ },
1743
+ repo: {
1744
+ type: "string"
1745
+ },
1746
+ task_ids: {
1747
+ type: "array",
1748
+ items: {
1749
+ type: "string"
1750
+ }
1751
+ },
1752
+ knowledge_ids: {
1753
+ type: "array",
1754
+ items: {
1755
+ type: "string"
1756
+ }
1757
+ },
1758
+ digest_id: {
1759
+ type: "string"
1760
+ },
1761
+ started_at: {
1762
+ type: "string",
1763
+ format: "date-time",
1764
+ nullable: true
1765
+ },
1766
+ finished_at: {
1767
+ type: "string",
1768
+ format: "date-time",
1769
+ nullable: true
1770
+ },
1771
+ status: {
1772
+ type: "string"
1773
+ },
1774
+ exit_code: {
1775
+ type: "integer",
1776
+ nullable: true
1777
+ },
1778
+ summary: {
1779
+ $ref: "#/components/schemas/RunReceiptSummary"
1780
+ },
1781
+ evidence_paths: {
1782
+ type: "array",
1783
+ items: {
1784
+ type: "string"
1785
+ }
1786
+ },
1787
+ created_at: {
1788
+ type: "string",
1789
+ format: "date-time"
1790
+ },
1791
+ updated_at: {
1792
+ type: "string",
1793
+ format: "date-time"
1794
+ }
1795
+ },
1796
+ additionalProperties: false
1797
+ },
1798
+ WriteRunReceiptInput: {
1799
+ type: "object",
1800
+ required: [
1801
+ "run_id"
1802
+ ],
1803
+ properties: {
1804
+ loop_id: {
1805
+ type: "string"
1806
+ },
1807
+ run_id: {
1808
+ type: "string"
1809
+ },
1810
+ machine: {
1811
+ $ref: "#/components/schemas/RunReceiptMachine"
1812
+ },
1813
+ repo: {
1814
+ type: "string"
1815
+ },
1816
+ task_ids: {
1817
+ type: "array",
1818
+ items: {
1819
+ type: "string"
1820
+ }
1821
+ },
1822
+ knowledge_ids: {
1823
+ type: "array",
1824
+ items: {
1825
+ type: "string"
1826
+ }
1827
+ },
1828
+ digest_id: {
1829
+ type: "string"
1830
+ },
1831
+ started_at: {
1832
+ type: "string",
1833
+ format: "date-time",
1834
+ nullable: true
1835
+ },
1836
+ finished_at: {
1837
+ type: "string",
1838
+ format: "date-time",
1839
+ nullable: true
1840
+ },
1841
+ status: {
1842
+ type: "string"
1843
+ },
1844
+ exit_code: {
1845
+ type: "integer",
1846
+ nullable: true
1847
+ },
1848
+ summary: {
1849
+ oneOf: [
1850
+ {
1851
+ type: "string"
1852
+ },
1853
+ {
1854
+ $ref: "#/components/schemas/RunReceiptSummary"
1855
+ },
1856
+ {
1857
+ type: "null"
1858
+ }
1859
+ ]
1860
+ },
1861
+ evidence_paths: {
1862
+ type: "array",
1863
+ items: {
1864
+ type: "string"
1865
+ }
1866
+ },
1867
+ stdout: {
1868
+ type: "string"
1869
+ },
1870
+ stderr: {
1871
+ type: "string"
1872
+ },
1873
+ error: {
1874
+ type: "string"
1875
+ },
1876
+ duration_ms: {
1877
+ type: "integer",
1878
+ minimum: 0
1879
+ }
1880
+ },
1881
+ additionalProperties: false
1882
+ },
1883
+ RunReceiptResponse: {
1884
+ type: "object",
1885
+ properties: {
1886
+ ok: {
1887
+ type: "boolean"
1888
+ },
1889
+ receipt: {
1890
+ $ref: "#/components/schemas/RunReceipt"
1891
+ }
1892
+ },
1893
+ required: [
1894
+ "ok",
1895
+ "receipt"
1896
+ ]
1897
+ },
1898
+ RunReceiptListResponse: {
1899
+ type: "object",
1900
+ properties: {
1901
+ ok: {
1902
+ type: "boolean"
1903
+ },
1904
+ receipts: {
1905
+ type: "array",
1906
+ items: {
1907
+ $ref: "#/components/schemas/RunReceipt"
1908
+ }
1909
+ }
1910
+ },
1911
+ required: [
1912
+ "ok",
1913
+ "receipts"
1914
+ ]
1915
+ },
1916
+ ImportInput: {
1917
+ type: "object",
1918
+ description: "Batches of full rows to upsert by id. Any array may be omitted.",
1919
+ properties: {
1920
+ workflows: {
1921
+ type: "array",
1922
+ items: {
1923
+ type: "object",
1924
+ additionalProperties: true
1925
+ }
1926
+ },
1927
+ loops: {
1928
+ type: "array",
1929
+ items: {
1930
+ type: "object",
1931
+ additionalProperties: true
1932
+ }
1933
+ },
1934
+ runs: {
1935
+ type: "array",
1936
+ items: {
1937
+ type: "object",
1938
+ additionalProperties: true
1939
+ }
1940
+ },
1941
+ replace: {
1942
+ type: "boolean",
1943
+ description: "Update existing rows whose id matches (default false: existing rows are left unchanged)."
1944
+ }
1945
+ }
1946
+ },
1947
+ ImportResponse: {
1948
+ type: "object",
1949
+ properties: {
1950
+ ok: {
1951
+ type: "boolean"
1952
+ },
1953
+ imported: {
1954
+ type: "object",
1955
+ properties: {
1956
+ workflows: {
1957
+ type: "integer"
1958
+ },
1959
+ loops: {
1960
+ type: "integer"
1961
+ },
1962
+ runs: {
1963
+ type: "integer"
1964
+ }
1965
+ },
1966
+ required: [
1967
+ "workflows",
1968
+ "loops",
1969
+ "runs"
1970
+ ]
1971
+ },
1972
+ skippedRunning: {
1973
+ type: "integer"
1974
+ }
1975
+ },
1976
+ required: [
1977
+ "ok",
1978
+ "imported",
1979
+ "skippedRunning"
1980
+ ]
1981
+ },
1982
+ CountResponse: {
1983
+ type: "object",
1984
+ properties: {
1985
+ ok: {
1986
+ type: "boolean"
1987
+ },
1988
+ count: {
1989
+ type: "integer"
1990
+ }
1991
+ },
1992
+ required: [
1993
+ "ok",
1994
+ "count"
1995
+ ]
1442
1996
  }
1443
1997
  },
1444
1998
  securitySchemes: {
@@ -1463,6 +2017,7 @@ function openApiDocument() {
1463
2017
  var program = new Command;
1464
2018
  var DEFAULT_BODY_LIMIT_BYTES = 64 * 1024;
1465
2019
  var DEFAULT_EVIDENCE_LIMIT_BYTES = 256 * 1024;
2020
+ var DEFAULT_IMPORT_LIMIT_BYTES = 32 * 1024 * 1024;
1466
2021
  var MIN_RUNNER_LEASE_MS = 1000;
1467
2022
  program.name("loops-api").description("OpenLoops self-hosted control-plane API foundation").version(packageVersion()).option("-j, --json", "print JSON");
1468
2023
  function wantsJson(opts) {
@@ -1573,6 +2128,7 @@ function createLoopsApiServer(opts = {}) {
1573
2128
  storage: opts.storage,
1574
2129
  bodyLimitBytes: opts.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES,
1575
2130
  evidenceLimitBytes: opts.evidenceLimitBytes ?? DEFAULT_EVIDENCE_LIMIT_BYTES,
2131
+ importLimitBytes: opts.importLimitBytes ?? DEFAULT_IMPORT_LIMIT_BYTES,
1576
2132
  now: opts.now ?? (() => new Date)
1577
2133
  });
1578
2134
  }
@@ -1587,10 +2143,28 @@ async function handleV1Request(ctx) {
1587
2143
  if (ctx.request.method === "GET" && segments[1] === "status")
1588
2144
  return Response.json(apiStatus());
1589
2145
  try {
2146
+ if (segments[1] === "import")
2147
+ return await handleImportRequest(ctx, segments.slice(2));
1590
2148
  if (segments[1] === "loops")
1591
2149
  return await handleLoopsRequest(ctx, segments.slice(2));
1592
2150
  if (segments[1] === "runs")
1593
2151
  return await handleRunsRequest(ctx, segments.slice(2));
2152
+ if (segments[1] === "receipts")
2153
+ return await handleReceiptsRequest(ctx, segments.slice(2));
2154
+ if (segments[1] === "workflows")
2155
+ return await handleWorkflowsRequest(ctx, segments.slice(2));
2156
+ if (segments[1] === "workflow-runs")
2157
+ return await handleWorkflowRunsRequest(ctx, segments.slice(2));
2158
+ if (segments[1] === "work-items")
2159
+ return await handleWorkItemsRequest(ctx, segments.slice(2));
2160
+ if (segments[1] === "invocations")
2161
+ return await handleInvocationsRequest(ctx, segments.slice(2));
2162
+ if (segments[1] === "goals")
2163
+ return await handleGoalsRequest(ctx, segments.slice(2));
2164
+ if (segments[1] === "goal-runs")
2165
+ return await handleGoalRunsRequest(ctx, segments.slice(2));
2166
+ if (segments[1] === "history")
2167
+ return await handleHistoryRequest(ctx, segments.slice(2));
1594
2168
  if (segments[1] === "runners")
1595
2169
  return await handleRunnerRequest(ctx, segments.slice(2));
1596
2170
  if (segments[1] === "leases" && segments[2] === "recover" && ctx.request.method === "POST") {
@@ -1601,14 +2175,45 @@ async function handleV1Request(ctx) {
1601
2175
  return errorResponse(error);
1602
2176
  }
1603
2177
  }
2178
+ async function handleImportRequest(ctx, segments) {
2179
+ if (segments.length !== 0 || ctx.request.method !== "POST")
2180
+ return fail("not_found", 404);
2181
+ const storage = requireStorage(ctx.storage);
2182
+ const body = await readJsonBody(ctx.request, ctx.importLimitBytes);
2183
+ const replace = body.replace === true;
2184
+ const workflows = Array.isArray(body.workflows) ? body.workflows : [];
2185
+ const loops = Array.isArray(body.loops) ? body.loops : [];
2186
+ const runs = Array.isArray(body.runs) ? body.runs : [];
2187
+ const imported = { workflows: 0, loops: 0, runs: 0 };
2188
+ let skippedRunning = 0;
2189
+ for (const workflow of workflows) {
2190
+ await storage.upsertMigrationWorkflow(workflow, { replace });
2191
+ imported.workflows += 1;
2192
+ }
2193
+ for (const loop of loops) {
2194
+ await storage.upsertMigrationLoop(loop, { replace });
2195
+ imported.loops += 1;
2196
+ }
2197
+ for (const run of runs) {
2198
+ if (run.status === "running") {
2199
+ skippedRunning += 1;
2200
+ continue;
2201
+ }
2202
+ await storage.upsertMigrationRun(run, { replace });
2203
+ imported.runs += 1;
2204
+ }
2205
+ return ok({ imported, skippedRunning });
2206
+ }
1604
2207
  async function handleLoopsRequest(ctx, segments) {
1605
2208
  const storage = requireStorage(ctx.storage);
1606
2209
  if (segments.length === 0 && ctx.request.method === "GET") {
1607
2210
  const loops = await storage.listLoops({
1608
2211
  status: optionalEnum(ctx.url.searchParams.get("status"), ["active", "paused", "stopped", "expired"]),
1609
2212
  limit: optionalLimit(ctx.url.searchParams.get("limit")),
2213
+ offset: optionalOffset(ctx.url.searchParams.get("offset")),
1610
2214
  includeArchived: optionalBoolean(ctx.url.searchParams.get("includeArchived")),
1611
- archived: optionalBoolean(ctx.url.searchParams.get("archived"))
2215
+ archived: optionalBoolean(ctx.url.searchParams.get("archived")),
2216
+ name: optionalString(ctx.url.searchParams.get("name"))
1612
2217
  });
1613
2218
  return ok({ loops: loops.map(publicLoop) });
1614
2219
  }
@@ -1617,6 +2222,13 @@ async function handleLoopsRequest(ctx, segments) {
1617
2222
  const loop = await storage.createLoop(body);
1618
2223
  return ok({ loop: publicLoop(loop) }, { status: 201 });
1619
2224
  }
2225
+ if (segments.length === 1 && segments[0] === "count" && ctx.request.method === "GET") {
2226
+ const count = await storage.countLoops(optionalEnum(ctx.url.searchParams.get("status"), ["active", "paused", "stopped", "expired"]), {
2227
+ includeArchived: optionalBoolean(ctx.url.searchParams.get("includeArchived")),
2228
+ archived: optionalBoolean(ctx.url.searchParams.get("archived"))
2229
+ });
2230
+ return ok({ count });
2231
+ }
1620
2232
  const id = segments[0];
1621
2233
  if (!id)
1622
2234
  return fail("not_found", 404);
@@ -1649,6 +2261,170 @@ async function handleLoopsRequest(ctx, segments) {
1649
2261
  if (segments.length === 2 && segments[1] === "unarchive" && ctx.request.method === "POST") {
1650
2262
  return ok({ loop: publicLoop(await storage.unarchiveLoop(id)) });
1651
2263
  }
2264
+ if (segments.length === 2 && segments[1] === "rename" && ctx.request.method === "POST") {
2265
+ const body = await readJsonBody(ctx.request, ctx.bodyLimitBytes);
2266
+ const name = requiredString(body.name, "name");
2267
+ return ok({ loop: publicLoop(await storage.renameLoop(id, name)) });
2268
+ }
2269
+ return fail("not_found", 404);
2270
+ }
2271
+ async function handleWorkflowsRequest(ctx, segments) {
2272
+ const storage = requireStorage(ctx.storage);
2273
+ if (segments.length === 0 && ctx.request.method === "GET") {
2274
+ const workflows = await storage.listWorkflows({
2275
+ status: optionalEnum(ctx.url.searchParams.get("status"), ["active", "archived"]),
2276
+ limit: optionalLimit(ctx.url.searchParams.get("limit")),
2277
+ offset: optionalOffset(ctx.url.searchParams.get("offset"))
2278
+ });
2279
+ return ok({ workflows: workflows.map(publicWorkflow) });
2280
+ }
2281
+ if (segments.length === 0 && ctx.request.method === "POST") {
2282
+ const body = await readJsonBody(ctx.request, ctx.bodyLimitBytes);
2283
+ return ok({ workflow: publicWorkflow(await storage.createWorkflow(body)) }, { status: 201 });
2284
+ }
2285
+ if (segments.length === 1 && segments[0] === "count" && ctx.request.method === "GET") {
2286
+ const count = await storage.countWorkflows({
2287
+ status: optionalEnum(ctx.url.searchParams.get("status"), ["active", "archived"])
2288
+ });
2289
+ return ok({ count });
2290
+ }
2291
+ const id = segments[0];
2292
+ if (!id)
2293
+ return fail("not_found", 404);
2294
+ if (segments.length === 1 && ctx.request.method === "GET") {
2295
+ const workflow = await storage.getWorkflow(id);
2296
+ if (!workflow)
2297
+ return fail("workflow_not_found", 404);
2298
+ return ok({ workflow: publicWorkflow(workflow) });
2299
+ }
2300
+ if (segments.length === 2 && segments[1] === "archive" && ctx.request.method === "POST") {
2301
+ return ok({ workflow: publicWorkflow(await storage.archiveWorkflow(id)) });
2302
+ }
2303
+ return fail("not_found", 404);
2304
+ }
2305
+ async function handleWorkflowRunsRequest(ctx, segments) {
2306
+ const storage = requireStorage(ctx.storage);
2307
+ if (segments.length === 0 && ctx.request.method === "GET") {
2308
+ const runs = await storage.listWorkflowRuns({
2309
+ workflowId: ctx.url.searchParams.get("workflowId") ?? undefined,
2310
+ loopRunId: ctx.url.searchParams.get("loopRunId") ?? undefined,
2311
+ limit: optionalLimit(ctx.url.searchParams.get("limit"))
2312
+ });
2313
+ return ok({ workflowRuns: runs.map(publicWorkflowRun) });
2314
+ }
2315
+ const id = segments[0];
2316
+ if (!id)
2317
+ return fail("not_found", 404);
2318
+ if (segments.length === 1 && ctx.request.method === "GET") {
2319
+ const run = await storage.getWorkflowRun(id);
2320
+ if (!run)
2321
+ return fail("workflow_run_not_found", 404);
2322
+ return ok({ workflowRun: publicWorkflowRun(run) });
2323
+ }
2324
+ if (segments.length === 2 && segments[1] === "steps" && ctx.request.method === "GET") {
2325
+ const steps = await storage.listWorkflowStepRuns(id);
2326
+ return ok({ steps: steps.map((step) => publicWorkflowStepRun(step)) });
2327
+ }
2328
+ if (segments.length === 2 && segments[1] === "events" && ctx.request.method === "GET") {
2329
+ const events = await storage.listWorkflowEvents(id, optionalLimit(ctx.url.searchParams.get("limit")) ?? 200);
2330
+ return ok({ events: events.map(publicWorkflowEvent) });
2331
+ }
2332
+ if (segments.length === 2 && segments[1] === "recover" && ctx.request.method === "POST") {
2333
+ const body = await readJsonBody(ctx.request, ctx.bodyLimitBytes);
2334
+ const reason = optionalText(body.reason);
2335
+ const result = reason === undefined ? await storage.recoverWorkflowRun(id) : await storage.recoverWorkflowRun(id, reason);
2336
+ return ok({
2337
+ workflowRun: publicWorkflowRun(result.run),
2338
+ recoveredSteps: result.recoveredSteps.map((step) => publicWorkflowStepRun(step))
2339
+ });
2340
+ }
2341
+ return fail("not_found", 404);
2342
+ }
2343
+ async function handleWorkItemsRequest(ctx, segments) {
2344
+ const storage = requireStorage(ctx.storage);
2345
+ if (segments.length === 0 && ctx.request.method === "GET") {
2346
+ const items = await storage.listWorkflowWorkItems({
2347
+ status: optionalString(ctx.url.searchParams.get("status")),
2348
+ routeKey: ctx.url.searchParams.get("routeKey") ?? undefined,
2349
+ limit: optionalLimit(ctx.url.searchParams.get("limit"))
2350
+ });
2351
+ return ok({ workItems: items.map(publicWorkflowWorkItem) });
2352
+ }
2353
+ const id = segments[0];
2354
+ if (!id)
2355
+ return fail("not_found", 404);
2356
+ if (segments.length === 1 && ctx.request.method === "GET") {
2357
+ const item = await storage.getWorkflowWorkItem(id);
2358
+ if (!item)
2359
+ return fail("work_item_not_found", 404);
2360
+ return ok({ workItem: publicWorkflowWorkItem(item) });
2361
+ }
2362
+ return fail("not_found", 404);
2363
+ }
2364
+ async function handleInvocationsRequest(ctx, segments) {
2365
+ const storage = requireStorage(ctx.storage);
2366
+ if (segments.length === 0 && ctx.request.method === "GET") {
2367
+ const invocations = await storage.listWorkflowInvocations({ limit: optionalLimit(ctx.url.searchParams.get("limit")) });
2368
+ return ok({ invocations: invocations.map(publicWorkflowInvocation) });
2369
+ }
2370
+ const id = segments[0];
2371
+ if (!id)
2372
+ return fail("not_found", 404);
2373
+ if (segments.length === 1 && ctx.request.method === "GET") {
2374
+ const invocation = await storage.getWorkflowInvocation(id);
2375
+ if (!invocation)
2376
+ return fail("invocation_not_found", 404);
2377
+ return ok({ invocation: publicWorkflowInvocation(invocation) });
2378
+ }
2379
+ return fail("not_found", 404);
2380
+ }
2381
+ async function handleGoalsRequest(ctx, segments) {
2382
+ const storage = requireStorage(ctx.storage);
2383
+ if (segments.length === 0 && ctx.request.method === "GET") {
2384
+ const goals = await storage.listGoals({
2385
+ status: optionalString(ctx.url.searchParams.get("status")),
2386
+ limit: optionalLimit(ctx.url.searchParams.get("limit"))
2387
+ });
2388
+ return ok({ goals: goals.map(publicGoal) });
2389
+ }
2390
+ const id = segments[0];
2391
+ if (!id)
2392
+ return fail("not_found", 404);
2393
+ if (segments.length === 1 && ctx.request.method === "GET") {
2394
+ const goal = await storage.getGoal(id);
2395
+ if (!goal)
2396
+ return fail("goal_not_found", 404);
2397
+ return ok({ goal: publicGoal(goal) });
2398
+ }
2399
+ if (segments.length === 2 && segments[1] === "plan-nodes" && ctx.request.method === "GET") {
2400
+ const nodes = await storage.listGoalPlanNodes(id);
2401
+ return ok({ nodes });
2402
+ }
2403
+ return fail("not_found", 404);
2404
+ }
2405
+ async function handleGoalRunsRequest(ctx, segments) {
2406
+ const storage = requireStorage(ctx.storage);
2407
+ if (segments.length === 0 && ctx.request.method === "GET") {
2408
+ const runs = await storage.listGoalRuns({
2409
+ goalId: ctx.url.searchParams.get("goalId") ?? undefined,
2410
+ runId: ctx.url.searchParams.get("runId") ?? undefined,
2411
+ limit: optionalLimit(ctx.url.searchParams.get("limit"))
2412
+ });
2413
+ return ok({ goalRuns: runs.map(publicGoalRun) });
2414
+ }
2415
+ return fail("not_found", 404);
2416
+ }
2417
+ async function handleHistoryRequest(ctx, segments) {
2418
+ const storage = requireStorage(ctx.storage);
2419
+ if (segments.length === 1 && segments[0] === "prune" && ctx.request.method === "POST") {
2420
+ const body = await readJsonBody(ctx.request, ctx.bodyLimitBytes);
2421
+ const history = await storage.pruneHistory({
2422
+ maxAgeDays: optionalInteger(body.maxAgeDays),
2423
+ keepPerLoop: optionalInteger(body.keepPerLoop),
2424
+ dryRun: body.dryRun === undefined ? undefined : Boolean(body.dryRun)
2425
+ });
2426
+ return ok({ history });
2427
+ }
1652
2428
  return fail("not_found", 404);
1653
2429
  }
1654
2430
  async function handleRunsRequest(ctx, segments) {
@@ -1684,11 +2460,16 @@ async function handleRunsRequest(ctx, segments) {
1684
2460
  }
1685
2461
  const storage = requireStorage(ctx.storage);
1686
2462
  const showOutput = optionalBoolean(ctx.url.searchParams.get("showOutput")) ?? false;
2463
+ if (segments.length === 1 && id === "count" && ctx.request.method === "GET") {
2464
+ const count = await storage.countRuns(optionalEnum(ctx.url.searchParams.get("status"), ["running", "succeeded", "failed", "timed_out", "abandoned", "skipped"]));
2465
+ return ok({ count });
2466
+ }
1687
2467
  if (segments.length === 0 && ctx.request.method === "GET") {
1688
2468
  const runs = await storage.listRuns({
1689
2469
  loopId: ctx.url.searchParams.get("loopId") ?? undefined,
1690
2470
  status: optionalEnum(ctx.url.searchParams.get("status"), ["running", "succeeded", "failed", "timed_out", "abandoned", "skipped"]),
1691
- limit: optionalLimit(ctx.url.searchParams.get("limit"))
2471
+ limit: optionalLimit(ctx.url.searchParams.get("limit")),
2472
+ offset: optionalOffset(ctx.url.searchParams.get("offset"))
1692
2473
  });
1693
2474
  return ok({ runs: runs.map((run) => publicRun(run, showOutput, { redactError: true })) });
1694
2475
  }
@@ -1702,6 +2483,34 @@ async function handleRunsRequest(ctx, segments) {
1702
2483
  }
1703
2484
  return fail("not_found", 404);
1704
2485
  }
2486
+ async function handleReceiptsRequest(ctx, segments) {
2487
+ const storage = requireStorage(ctx.storage);
2488
+ const id = segments[0];
2489
+ if (segments.length === 0 && ctx.request.method === "GET") {
2490
+ const receipts = await storage.listRunReceipts({
2491
+ loopId: ctx.url.searchParams.get("loopId") ?? undefined,
2492
+ repo: ctx.url.searchParams.get("repo") ?? undefined,
2493
+ taskId: ctx.url.searchParams.get("taskId") ?? undefined,
2494
+ knowledgeId: ctx.url.searchParams.get("knowledgeId") ?? undefined,
2495
+ status: ctx.url.searchParams.get("status") ?? undefined,
2496
+ limit: optionalLimit(ctx.url.searchParams.get("limit"))
2497
+ });
2498
+ return ok({ receipts: receipts.map(publicRunReceipt) });
2499
+ }
2500
+ if (segments.length === 0 && ctx.request.method === "POST") {
2501
+ const body = await readJsonBody(ctx.request, ctx.evidenceLimitBytes);
2502
+ return ok({ receipt: publicRunReceipt(await storage.writeRunReceipt(body)) }, { status: 201 });
2503
+ }
2504
+ if (!id)
2505
+ return fail("not_found", 404);
2506
+ if (segments.length === 1 && ctx.request.method === "GET") {
2507
+ const receipt = await storage.getRunReceipt(id);
2508
+ if (!receipt)
2509
+ return fail("run_receipt_not_found", 404);
2510
+ return ok({ receipt: publicRunReceipt(receipt) });
2511
+ }
2512
+ return fail("not_found", 404);
2513
+ }
1705
2514
  async function handleRunnerRequest(ctx, segments) {
1706
2515
  if (ctx.request.method !== "POST")
1707
2516
  return fail("not_found", 404);
@@ -1913,13 +2722,22 @@ async function readBodyText(request, limitBytes) {
1913
2722
  function runnerProtocolPending(message) {
1914
2723
  return fail("runner_protocol_pending", 501, { message });
1915
2724
  }
2725
+ var MAX_PAGE_LIMIT = 1000;
1916
2726
  function optionalLimit(value) {
1917
2727
  if (value == null || value === "")
1918
2728
  return;
1919
2729
  const limit = Number(value);
1920
- if (!Number.isInteger(limit) || limit < 1 || limit > 1000)
2730
+ if (!Number.isInteger(limit) || limit < 1)
1921
2731
  throw Object.assign(new Error("invalid_limit"), { status: 422 });
1922
- return limit;
2732
+ return Math.min(limit, MAX_PAGE_LIMIT);
2733
+ }
2734
+ function optionalOffset(value) {
2735
+ if (value == null || value === "")
2736
+ return;
2737
+ const offset = Number(value);
2738
+ if (!Number.isInteger(offset) || offset < 0)
2739
+ throw Object.assign(new Error("invalid_offset"), { status: 422 });
2740
+ return offset;
1923
2741
  }
1924
2742
  function optionalString(value) {
1925
2743
  if (value === undefined || value === null)