forge-workflow 0.1.0-beta.6 → 0.1.0-beta.7

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 (156) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/docs/guides/MIGRATION.md +3 -3
  3. package/docs/reference/RELEASE.md +3 -3
  4. package/lib/commands/merge.js +2 -2
  5. package/lib/commands/push.js +13 -4
  6. package/lib/commands/shepherd.js +1 -1
  7. package/lib/commands/validate.js +13 -1
  8. package/lib/kernel/migrations.js +1 -1
  9. package/lib/kernel/sqlite-driver.js +1 -1
  10. package/lib/memory/router.js +1 -1
  11. package/lib/memory/usage-evidence.js +1 -1
  12. package/lib/pr-monitor/flow-monitor.js +2 -2
  13. package/lib/pr-monitor/reconcile-executor.js +1 -1
  14. package/lib/project-memory.js +1 -1
  15. package/lib/validation-receipt.js +190 -0
  16. package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  17. package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  18. package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  19. package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  20. package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  21. package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  22. package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  23. package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  24. package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  25. package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  26. package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  27. package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  28. package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  29. package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  30. package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  31. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  32. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  33. package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  34. package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  35. package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  36. package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  37. package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  38. package/node_modules/@forge/contracts/index.js +32 -0
  39. package/node_modules/@forge/contracts/package.json +35 -0
  40. package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  41. package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  42. package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  43. package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  44. package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  45. package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  46. package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  47. package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  48. package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  49. package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  50. package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  51. package/node_modules/@forge/contracts/src/baseline.js +23 -0
  52. package/node_modules/@forge/contracts/src/canonical.js +151 -0
  53. package/node_modules/@forge/contracts/src/definitions.js +176 -0
  54. package/node_modules/@forge/contracts/src/identity.js +42 -0
  55. package/node_modules/@forge/contracts/src/schema.js +72 -0
  56. package/node_modules/@forge/contracts/src/validate.js +305 -0
  57. package/node_modules/@forge/flow/index.js +114 -0
  58. package/node_modules/@forge/flow/package.json +34 -0
  59. package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
  60. package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
  61. package/node_modules/@forge/flow/src/executor.js +279 -0
  62. package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
  63. package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
  64. package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
  65. package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
  66. package/node_modules/@forge/memory/index.js +331 -0
  67. package/node_modules/@forge/memory/package.json +34 -0
  68. package/node_modules/@forge/memory/src/authority-provider.js +67 -0
  69. package/node_modules/@forge/memory/src/backend-registry.js +166 -0
  70. package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
  71. package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
  72. package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
  73. package/package.json +9 -1
  74. package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  75. package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  76. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  77. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  78. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  79. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  80. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  81. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  82. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  83. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  84. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  85. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  86. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  87. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  88. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  89. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  90. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  91. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  92. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  93. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  94. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  95. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  96. package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
  97. package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
  98. package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  99. package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  100. package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  101. package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  102. package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  103. package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  104. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  105. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  106. package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  107. package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  108. package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  109. package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
  110. package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
  111. package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
  112. package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
  113. package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
  114. package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
  115. package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  116. package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  117. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  118. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  119. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  120. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  121. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  122. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  123. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  124. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  125. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  126. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  127. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  128. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  129. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  130. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  131. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  132. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  133. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  134. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  135. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  136. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  137. package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
  138. package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
  139. package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  140. package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  141. package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  142. package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  143. package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  144. package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  145. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  146. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  147. package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  148. package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  149. package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  150. package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
  151. package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
  152. package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
  153. package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
  154. package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
  155. package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
  156. package/scripts/test.js +1 -1
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+
3
+ const { computeContentHash, validateContractStructure } = require("@forge/contracts");
4
+ const { FlowExecutionError, createWorkPacketExecutor } = require("./src/executor");
5
+ const {
6
+ MonitorRuntimeError,
7
+ createMonitorReceipt,
8
+ createMonitorState,
9
+ reduceMonitor,
10
+ reduceMonitorBatch,
11
+ } = require("./src/monitor-runtime");
12
+ const { EfficiencySupervisor } = require("./src/efficiency-supervisor");
13
+ const { SkillRuntime } = require("./src/skill-runtime");
14
+ const {
15
+ BoundedLoopError,
16
+ createBoundedLoop,
17
+ createBoundedLoopState,
18
+ reduceBoundedLoop,
19
+ } = require("./src/bounded-loop");
20
+ const {
21
+ ProcessLifecycleError,
22
+ createProcessLifecycle,
23
+ createProcessState,
24
+ reduceProcessLifecycle,
25
+ } = require("./src/process-lifecycle");
26
+ const {
27
+ MonitorDurabilityError,
28
+ createMonitorDurabilityBridge,
29
+ } = require("./src/monitor-durability");
30
+
31
+ function requiredString(options, name) {
32
+ const value = options[name];
33
+ if (typeof value !== "string" || value.length === 0) {
34
+ throw new TypeError(`${name} must be a non-empty string`);
35
+ }
36
+ return value;
37
+ }
38
+
39
+ function createRunReceiptSkeleton(workPacket, options = {}) {
40
+ const packetValidation = validateContractStructure(workPacket);
41
+ if (!packetValidation.ok || workPacket.schema_id !== "forge.memory.work-packet.v1") {
42
+ throw new TypeError("WorkPacket is invalid");
43
+ }
44
+
45
+ const objectId = requiredString(options, "objectId");
46
+ const runId = requiredString(options, "runId");
47
+ const attemptId = requiredString(options, "attemptId");
48
+ const createdAt = requiredString(options, "createdAt");
49
+ const producerInstanceId = requiredString(options, "producerInstanceId");
50
+ const payload = workPacket.payload;
51
+ const receipt = {
52
+ schema_id: "forge.memory.run-receipt.v1",
53
+ schema_version: 1,
54
+ object_id: objectId,
55
+ created_at: createdAt,
56
+ producer: {
57
+ product_id: "forge-flow",
58
+ product_version: "0.1.0-beta.6",
59
+ instance_id: producerInstanceId,
60
+ },
61
+ capabilities_used: [],
62
+ provenance: {
63
+ source_kind: "flow-boundary",
64
+ actor_class: "system",
65
+ actor_id: producerInstanceId,
66
+ },
67
+ payload: {
68
+ packet_hash: workPacket.content_hash,
69
+ run_id: runId,
70
+ attempt_id: attemptId,
71
+ exact_head: payload.target_head,
72
+ packet_revision: payload.packet_revision,
73
+ manifest_digest: payload.capability_manifest_digest,
74
+ workflow_config_revision: payload.workflow_config_revision,
75
+ status: "NOT_EXECUTED",
76
+ executor: { product_id: "forge-flow", mode: "contract-boundary" },
77
+ started_at: createdAt,
78
+ ended_at: createdAt,
79
+ evidence_refs: [],
80
+ validation: { status: "NOT_RUN" },
81
+ cleanup: { status: "NOT_REQUIRED" },
82
+ },
83
+ extensions: {},
84
+ };
85
+ receipt.content_hash = computeContentHash(receipt);
86
+ const receiptValidation = validateContractStructure(receipt);
87
+ if (!receiptValidation.ok) {
88
+ throw new TypeError("RunReceipt is invalid");
89
+ }
90
+ return receipt;
91
+ }
92
+
93
+ module.exports = {
94
+ BoundedLoopError,
95
+ EfficiencySupervisor,
96
+ FlowExecutionError,
97
+ MonitorDurabilityError,
98
+ MonitorRuntimeError,
99
+ ProcessLifecycleError,
100
+ SkillRuntime,
101
+ createBoundedLoop,
102
+ createBoundedLoopState,
103
+ createMonitorReceipt,
104
+ createMonitorDurabilityBridge,
105
+ createMonitorState,
106
+ createProcessLifecycle,
107
+ createProcessState,
108
+ createRunReceiptSkeleton,
109
+ createWorkPacketExecutor,
110
+ reduceBoundedLoop,
111
+ reduceMonitor,
112
+ reduceMonitorBatch,
113
+ reduceProcessLifecycle,
114
+ };
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@forge/flow",
3
+ "version": "0.1.0-beta.6",
4
+ "description": "Optional behavior-neutral execution boundary for Forge",
5
+ "main": "index.js",
6
+ "type": "commonjs",
7
+ "files": [
8
+ "index.js",
9
+ "src/"
10
+ ],
11
+ "scripts": {
12
+ "test": "bun test",
13
+ "lint": "eslint . --max-warnings 0"
14
+ },
15
+ "dependencies": {
16
+ "@forge/contracts": "0.1.0-beta.6"
17
+ },
18
+ "license": "MIT",
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/harshanandak/forge.git",
25
+ "directory": "packages/flow"
26
+ },
27
+ "homepage": "https://github.com/harshanandak/forge#readme",
28
+ "bugs": {
29
+ "url": "https://github.com/harshanandak/forge/issues"
30
+ },
31
+ "engines": {
32
+ "node": ">=22.16.0"
33
+ }
34
+ }
@@ -0,0 +1,415 @@
1
+ "use strict";
2
+
3
+ const { createHash } = require("node:crypto");
4
+
5
+ const MAX_ATTEMPTS = 128;
6
+ const MAX_ELAPSED_MS = 86_400_000;
7
+ const MAX_EVENTS = 256;
8
+ const MAX_HISTORY = 256;
9
+ const EVENT_TYPES = new Set([
10
+ "start",
11
+ "attempt",
12
+ "complete",
13
+ "fail",
14
+ "cancel-requested",
15
+ "cancel-acknowledged",
16
+ "tick",
17
+ "timeout",
18
+ ]);
19
+ const TERMINAL_STATUSES = new Set(["PASS", "FAIL", "CANCELLED", "INCOMPLETE"]);
20
+ const STATE_CLONE_LIMITS = Object.freeze({ maxBytes: 4_194_304, maxDepth: 8, maxNodes: 32_768 });
21
+ const MAX_EVENT_ID_LENGTH = 128;
22
+ const EVENT_DIGEST_BYTES = 64;
23
+ const STATE_OVERHEAD_RESERVE_BYTES = 32_768;
24
+ const SEEN_EVENT_ENTRY_OVERHEAD_BYTES = (MAX_EVENT_ID_LENGTH * 6) + 2 + EVENT_DIGEST_BYTES + 2 + 3;
25
+ const EVENT_BYTE_BUDGET = Math.floor((STATE_CLONE_LIMITS.maxBytes - STATE_OVERHEAD_RESERVE_BYTES) / MAX_EVENTS)
26
+ - SEEN_EVENT_ENTRY_OVERHEAD_BYTES;
27
+ const EVENT_CLONE_LIMITS = Object.freeze({ maxBytes: EVENT_BYTE_BUDGET, maxDepth: 8, maxNodes: 256 });
28
+
29
+ class BoundedLoopError extends Error {
30
+ constructor(code, message, details = {}) {
31
+ super(message);
32
+ this.name = "BoundedLoopError";
33
+ this.code = code;
34
+ this.details = details;
35
+ }
36
+ }
37
+
38
+ function fail(code, message, details) {
39
+ throw new BoundedLoopError(code, message, details);
40
+ }
41
+
42
+ function ownData(value, key, name) {
43
+ let descriptor;
44
+ try {
45
+ descriptor = Object.getOwnPropertyDescriptor(value, key);
46
+ } catch {
47
+ fail("INVALID_INPUT", `${name || key} cannot be inspected`);
48
+ }
49
+ if (!descriptor || !Object.hasOwn(descriptor, "value")) {
50
+ fail("INVALID_INPUT", `${name || key} must be a data property`);
51
+ }
52
+ return descriptor.value;
53
+ }
54
+
55
+ function readDescriptor(value, key, name = key) {
56
+ try {
57
+ return Object.getOwnPropertyDescriptor(value, key);
58
+ } catch {
59
+ fail("INVALID_INPUT", `${name} cannot be inspected`);
60
+ }
61
+ }
62
+
63
+ function assertDataProperties(value, name) {
64
+ let keys;
65
+ try {
66
+ keys = Reflect.ownKeys(value);
67
+ } catch {
68
+ fail("INVALID_OPTIONS", `${name} cannot be inspected`);
69
+ }
70
+ for (const key of keys) {
71
+ if (typeof key !== "string") fail("INVALID_OPTIONS", `${name} contains a symbol key`);
72
+ const descriptor = readDescriptor(value, key, `${name}.${key}`);
73
+ if (!descriptor || !Object.hasOwn(descriptor, "value")) fail("INVALID_OPTIONS", `${name}.${key} must be a data property`);
74
+ }
75
+ }
76
+
77
+ function optionalData(value, key) {
78
+ const descriptor = readDescriptor(value, key, key);
79
+ if (!descriptor) return { present: false, value: undefined };
80
+ if (!Object.hasOwn(descriptor, "value")) fail("INVALID_OPTIONS", `${key} must be a data property`);
81
+ return { present: true, value: descriptor.value };
82
+ }
83
+
84
+ function safeClone(value, limits = {}, depth = 0, seen = { nodes: 0 }) {
85
+ const maxDepth = limits.maxDepth ?? 8;
86
+ const maxNodes = limits.maxNodes ?? 256;
87
+ const maxBytes = limits.maxBytes ?? 16_384;
88
+ if (depth > maxDepth) fail("BOUNDED_INPUT", "value exceeds maximum depth");
89
+ seen.nodes += 1;
90
+ if (seen.nodes > maxNodes) fail("BOUNDED_INPUT", "value exceeds maximum nodes");
91
+
92
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
93
+ if (typeof value === "number") {
94
+ if (!Number.isFinite(value)) fail("INVALID_INPUT", "non-finite numbers are not accepted");
95
+ return value;
96
+ }
97
+ if (typeof value !== "object") fail("INVALID_INPUT", "functions, symbols, bigint, and undefined are not accepted");
98
+
99
+ let prototype;
100
+ let keys;
101
+ try {
102
+ prototype = Object.getPrototypeOf(value);
103
+ keys = Reflect.ownKeys(value);
104
+ } catch {
105
+ fail("INVALID_INPUT", "value cannot be inspected");
106
+ }
107
+ if (prototype !== Object.prototype && prototype !== null && !Array.isArray(value)) {
108
+ fail("INVALID_INPUT", "value must be plain data");
109
+ }
110
+ if (keys.some((key) => typeof key !== "string")) fail("INVALID_INPUT", "value contains a non-string key");
111
+
112
+ let clone;
113
+ if (Array.isArray(value)) {
114
+ const lengthDescriptor = readDescriptor(value, "length", "array length");
115
+ if (!lengthDescriptor || !Object.hasOwn(lengthDescriptor, "value")
116
+ || !Number.isSafeInteger(lengthDescriptor.value) || lengthDescriptor.value > maxNodes) {
117
+ fail("INVALID_INPUT", "array length is invalid");
118
+ }
119
+ clone = [];
120
+ for (let index = 0; index < lengthDescriptor.value; index += 1) {
121
+ const descriptor = readDescriptor(value, String(index), `array index ${index}`);
122
+ if (!descriptor || !Object.hasOwn(descriptor, "value")) fail("INVALID_INPUT", "array contains a hole or accessor");
123
+ clone.push(safeClone(descriptor.value, limits, depth + 1, seen));
124
+ }
125
+ if (keys.length !== lengthDescriptor.value + 1) fail("INVALID_INPUT", "array contains unsupported properties");
126
+ } else {
127
+ clone = {};
128
+ for (const key of keys) {
129
+ const descriptor = readDescriptor(value, key);
130
+ if (!descriptor || !Object.hasOwn(descriptor, "value")) fail("INVALID_INPUT", "value contains an accessor");
131
+ Object.defineProperty(clone, key, {
132
+ configurable: true,
133
+ enumerable: true,
134
+ value: safeClone(descriptor.value, limits, depth + 1, seen),
135
+ writable: true,
136
+ });
137
+ }
138
+ }
139
+
140
+ try {
141
+ if (Buffer.byteLength(JSON.stringify(clone), "utf8") > maxBytes) fail("BOUNDED_INPUT", "value exceeds maximum bytes");
142
+ } catch (error) {
143
+ if (error instanceof BoundedLoopError) throw error;
144
+ fail("INVALID_INPUT", "value cannot be serialized");
145
+ }
146
+ return clone;
147
+ }
148
+
149
+ function stableStringify(value) {
150
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
151
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
152
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
153
+ }
154
+
155
+ function eventDigest(event) {
156
+ return createHash("sha256").update(stableStringify(event), "utf8").digest("hex");
157
+ }
158
+
159
+ function deepFreeze(value, seen = new Set()) {
160
+ if (!value || typeof value !== "object" || seen.has(value)) return value;
161
+ seen.add(value);
162
+ for (const key of Reflect.ownKeys(value)) {
163
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
164
+ if (descriptor && Object.hasOwn(descriptor, "value")) deepFreeze(descriptor.value, seen);
165
+ }
166
+ return Object.freeze(value);
167
+ }
168
+
169
+ function assertPlainRecord(value, name) {
170
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail("INVALID_INPUT", `${name} must be an object`);
171
+ try {
172
+ const prototype = Object.getPrototypeOf(value);
173
+ if (prototype !== Object.prototype && prototype !== null) fail("INVALID_INPUT", `${name} must be a plain object`);
174
+ } catch {
175
+ fail("INVALID_INPUT", `${name} cannot be inspected`);
176
+ }
177
+ }
178
+
179
+ function assertLimit(value, name, ceiling) {
180
+ if (!Number.isSafeInteger(value) || value < 1 || value > ceiling) {
181
+ fail("INVALID_LIMIT", `${name} must be a safe integer from 1 to ${ceiling}`);
182
+ }
183
+ }
184
+
185
+ function validateLimits(options) {
186
+ const maxAttempts = ownData(options, "maxAttempts", "maxAttempts");
187
+ const maxElapsedMs = ownData(options, "maxElapsedMs", "maxElapsedMs");
188
+ const maxEvents = ownData(options, "maxEvents", "maxEvents");
189
+ const maxHistory = ownData(options, "maxHistory", "maxHistory");
190
+ assertLimit(maxAttempts, "maxAttempts", MAX_ATTEMPTS);
191
+ assertLimit(maxElapsedMs, "maxElapsedMs", MAX_ELAPSED_MS);
192
+ assertLimit(maxEvents, "maxEvents", MAX_EVENTS);
193
+ assertLimit(maxHistory, "maxHistory", MAX_HISTORY);
194
+ if (maxHistory > maxEvents) fail("INVALID_LIMIT", "maxHistory cannot exceed maxEvents");
195
+ return {
196
+ maxAttempts,
197
+ maxElapsedMs,
198
+ maxEvents,
199
+ maxHistory,
200
+ };
201
+ }
202
+
203
+ function validateOptions(rawOptions) {
204
+ assertPlainRecord(rawOptions, "options");
205
+ assertDataProperties(rawOptions, "options");
206
+ const loopId = ownData(rawOptions, "loopId", "loopId");
207
+ if (typeof loopId !== "string" || loopId.length === 0 || loopId.length > 128) fail("INVALID_OPTIONS", "loopId must be a bounded string");
208
+ const clock = ownData(rawOptions, "clock", "clock");
209
+ if (typeof clock !== "function") fail("INVALID_OPTIONS", "clock must be an injected function");
210
+ const limits = validateLimits({
211
+ maxAttempts: ownData(rawOptions, "maxAttempts", "maxAttempts"),
212
+ maxElapsedMs: ownData(rawOptions, "maxElapsedMs", "maxElapsedMs"),
213
+ maxEvents: ownData(rawOptions, "maxEvents", "maxEvents"),
214
+ maxHistory: ownData(rawOptions, "maxHistory", "maxHistory"),
215
+ });
216
+ return { loopId, clock, ...limits };
217
+ }
218
+
219
+ function readNow(options) {
220
+ let now;
221
+ try {
222
+ now = options.now === undefined ? options.clock() : options.now;
223
+ } catch {
224
+ fail("CLOCK_FAILURE", "injected clock failed");
225
+ }
226
+ if (!Number.isSafeInteger(now) || now < 0) fail("CLOCK_INVALID", "injected clock must return a non-negative safe integer");
227
+ return now;
228
+ }
229
+
230
+ function normalizeEvent(rawEvent) {
231
+ const event = safeClone(rawEvent, EVENT_CLONE_LIMITS);
232
+ assertPlainRecord(event, "event");
233
+ if (typeof event.id !== "string" || event.id.length === 0 || event.id.length > 128) fail("INVALID_EVENT", "event id must be a bounded string");
234
+ if (!EVENT_TYPES.has(event.type)) fail("INVALID_EVENT", `unsupported bounded-loop event: ${String(event.type)}`);
235
+ if (event.amount !== undefined && (!Number.isSafeInteger(event.amount) || event.amount < 1)) {
236
+ fail("INVALID_EVENT", "event amount must be a positive safe integer");
237
+ }
238
+ if (event.status !== undefined && !TERMINAL_STATUSES.has(event.status)) fail("INVALID_EVENT", "event status is invalid");
239
+ if (event.reason !== undefined && (typeof event.reason !== "string" || event.reason.length > 256)) fail("INVALID_EVENT", "event reason is invalid");
240
+ if (event.at !== undefined && (!Number.isSafeInteger(event.at) || event.at < 0)) fail("INVALID_EVENT", "event timestamp is invalid");
241
+ return event;
242
+ }
243
+
244
+ function cloneResult(state, effects) {
245
+ return {
246
+ state: deepFreeze(safeClone(state, STATE_CLONE_LIMITS)),
247
+ effects: deepFreeze(safeClone(effects)),
248
+ };
249
+ }
250
+
251
+ function createState(loopId, now) {
252
+ return {
253
+ loopId,
254
+ phase: "READY",
255
+ status: "PENDING",
256
+ terminal: false,
257
+ terminalReason: null,
258
+ attempts: 0,
259
+ elapsedMs: 0,
260
+ startAt: null,
261
+ lastAt: now,
262
+ eventCount: 0,
263
+ history: [],
264
+ seenEvents: {},
265
+ };
266
+ }
267
+
268
+ function createBoundedLoopState(rawOptions, now = 0) {
269
+ const options = validateOptions(rawOptions);
270
+ if (!Number.isSafeInteger(now) || now < 0) fail("CLOCK_INVALID", "initial time is invalid");
271
+ return createState(options.loopId, now);
272
+ }
273
+
274
+ function terminal(state, status, reason, effect) {
275
+ const next = { ...state, phase: "TERMINAL", status, terminal: true, terminalReason: reason };
276
+ return { state: next, effects: effect ? [effect] : [] };
277
+ }
278
+
279
+ function reduceBoundedLoop(rawState, rawEvent, rawOptions = {}) {
280
+ assertPlainRecord(rawState, "state");
281
+ const event = normalizeEvent(rawEvent);
282
+ const state = safeClone(rawState, STATE_CLONE_LIMITS);
283
+ assertPlainRecord(rawOptions, "options");
284
+ assertDataProperties(rawOptions, "options");
285
+ const limits = validateLimits(rawOptions);
286
+ const suppliedNow = optionalData(rawOptions, "now");
287
+ if (suppliedNow.present && typeof suppliedNow.value !== "number" && suppliedNow.value !== undefined) fail("CLOCK_INVALID", "reducer time is invalid");
288
+ const seen = state.seenEvents || {};
289
+ const digest = eventDigest(event);
290
+ if (Object.hasOwn(seen, event.id)) {
291
+ if (seen[event.id] !== digest) fail("IDENTITY_CONFLICT", "event identity conflict");
292
+ return cloneResult(state, []);
293
+ }
294
+ if (state.terminal || state.phase === "TERMINAL") fail("POST_TERMINAL_EVENT", "post-terminal event is rejected");
295
+ if (!Number.isSafeInteger(state.eventCount) || state.eventCount < 0) fail("INVALID_STATE", "event count is invalid");
296
+ if (state.eventCount >= limits.maxEvents) fail("EVENT_CAP_EXCEEDED", "bounded-loop event cap exceeded");
297
+
298
+ const now = suppliedNow.value === undefined ? (event.at ?? state.lastAt) : suppliedNow.value;
299
+ if (!Number.isSafeInteger(now) || now < 0) fail("CLOCK_INVALID", "reducer time is invalid");
300
+ if (event.at !== undefined && suppliedNow.value !== undefined && event.at !== now) fail("NON_MONOTONIC_INPUT", "event timestamp does not match injected clock");
301
+ if (state.lastAt !== null && (!Number.isSafeInteger(state.lastAt) || now < state.lastAt)) {
302
+ fail("NON_MONOTONIC_CLOCK", "injected clock is non-monotonic");
303
+ }
304
+
305
+ const next = {
306
+ ...state,
307
+ lastAt: now,
308
+ eventCount: state.eventCount + 1,
309
+ history: [...(state.history || []), event].slice(-limits.maxHistory),
310
+ seenEvents: { ...seen, [event.id]: digest },
311
+ };
312
+ if (next.startAt !== null) {
313
+ const elapsed = now - next.startAt;
314
+ if (!Number.isSafeInteger(elapsed) || elapsed < 0) fail("ELAPSED_OVERFLOW", "elapsed time overflow");
315
+ next.elapsedMs = elapsed;
316
+ }
317
+
318
+ const invalidPhase = (expected) => fail("INVALID_TRANSITION", `${event.type} requires ${expected}`);
319
+ const elapsedCapReached = event.type !== "start"
320
+ && next.startAt !== null
321
+ && next.elapsedMs >= limits.maxElapsedMs;
322
+ let result;
323
+ switch (event.type) {
324
+ case "start":
325
+ if (state.phase !== "READY") invalidPhase("READY phase");
326
+ next.phase = "RUNNING";
327
+ next.status = "RUNNING";
328
+ next.startAt = now;
329
+ next.attempts = 1;
330
+ result = { state: next, effects: [{ type: "START" }] };
331
+ break;
332
+ case "attempt":
333
+ if (state.phase !== "RUNNING") invalidPhase("RUNNING phase");
334
+ next.attempts += event.amount ?? 1;
335
+ if (!Number.isSafeInteger(next.attempts) || next.attempts > limits.maxAttempts) {
336
+ result = terminal(next, "INCOMPLETE", "ATTEMPT_CAP", { type: "ATTEMPT_CAP", attempts: next.attempts });
337
+ } else {
338
+ result = { state: next, effects: [{ type: "ATTEMPT", attempts: next.attempts }] };
339
+ }
340
+ break;
341
+ case "complete":
342
+ if (state.phase !== "RUNNING") invalidPhase("RUNNING phase");
343
+ if (event.status !== undefined && !["PASS", "FAIL"].includes(event.status)) fail("INVALID_EVENT", "complete status must be PASS or FAIL");
344
+ result = terminal(next, event.status ?? "PASS", event.reason ?? null, { type: "COMPLETE", status: event.status ?? "PASS" });
345
+ break;
346
+ case "fail":
347
+ if (state.phase !== "RUNNING") invalidPhase("RUNNING phase");
348
+ result = terminal(next, "FAIL", event.reason ?? "FAILURE", { type: "FAIL", reason: event.reason ?? "FAILURE" });
349
+ break;
350
+ case "cancel-requested":
351
+ if (state.phase !== "RUNNING") invalidPhase("RUNNING phase");
352
+ next.phase = "CANCEL_REQUESTED";
353
+ next.status = "CANCELLING";
354
+ result = { state: next, effects: [{ type: "CANCEL" }] };
355
+ break;
356
+ case "cancel-acknowledged":
357
+ if (state.phase !== "CANCEL_REQUESTED") invalidPhase("CANCEL_REQUESTED phase");
358
+ result = terminal(next, "CANCELLED", "CANCEL_ACKNOWLEDGED", { type: "CANCEL_ACKNOWLEDGED" });
359
+ break;
360
+ case "tick":
361
+ case "timeout":
362
+ if (!["RUNNING", "CANCEL_REQUESTED"].includes(state.phase)) invalidPhase("RUNNING or CANCEL_REQUESTED phase");
363
+ if (next.elapsedMs >= limits.maxElapsedMs) {
364
+ result = terminal(next, "INCOMPLETE", "ELAPSED_CAP", { type: "TIMEOUT", elapsedMs: next.elapsedMs });
365
+ } else {
366
+ result = { state: next, effects: [] };
367
+ }
368
+ break;
369
+ default:
370
+ fail("INVALID_EVENT", `unsupported bounded-loop event: ${event.type}`);
371
+ }
372
+ if ((elapsedCapReached && !result.state.terminal) || event.type === "timeout") {
373
+ result = terminal(next, "INCOMPLETE", "ELAPSED_CAP", { type: "TIMEOUT", elapsedMs: next.elapsedMs });
374
+ }
375
+ return cloneResult(result.state, result.effects);
376
+ }
377
+
378
+ function createBoundedLoop(rawOptions) {
379
+ const options = validateOptions(rawOptions);
380
+ // The first injected clock read belongs to the first transition. Construction is inert.
381
+ let current = createState(options.loopId, 0);
382
+ return Object.freeze({
383
+ dispatch(rawEvent) {
384
+ const event = normalizeEvent(rawEvent);
385
+ const digest = eventDigest(event);
386
+ if (Object.hasOwn(current.seenEvents, event.id)) {
387
+ const existing = current.seenEvents[event.id];
388
+ if (existing !== digest) fail("IDENTITY_CONFLICT", "event identity conflict");
389
+ return cloneResult(current, []);
390
+ }
391
+ const now = readNow(options);
392
+ const result = reduceBoundedLoop(current, event, { ...options, now });
393
+ current = result.state;
394
+ return result;
395
+ },
396
+ snapshot() {
397
+ return deepFreeze(safeClone(current, STATE_CLONE_LIMITS));
398
+ },
399
+ serialize() {
400
+ return stableStringify(safeClone(current, STATE_CLONE_LIMITS));
401
+ },
402
+ });
403
+ }
404
+
405
+ module.exports = {
406
+ BoundedLoopError,
407
+ MAX_ATTEMPTS,
408
+ EVENT_BYTE_BUDGET,
409
+ MAX_ELAPSED_MS,
410
+ MAX_EVENTS,
411
+ MAX_HISTORY,
412
+ createBoundedLoop,
413
+ createBoundedLoopState,
414
+ reduceBoundedLoop,
415
+ };
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+
3
+ const THRESHOLDS = Object.freeze([
4
+ Object.freeze({ percent: 20, type: "REPLAN" }),
5
+ Object.freeze({ percent: 30, type: "REPLAN" }),
6
+ Object.freeze({ percent: 35, type: "TERMINAL_PATH" }),
7
+ Object.freeze({ percent: 39, type: "STOP" }),
8
+ ]);
9
+
10
+ class EfficiencySupervisor {
11
+ constructor(options = {}) {
12
+ const { tokenBudget } = options;
13
+ if (!Number.isSafeInteger(tokenBudget) || tokenBudget <= 0) {
14
+ throw new TypeError("tokenBudget must be a positive safe integer");
15
+ }
16
+
17
+ this.tokenBudget = tokenBudget;
18
+ this.lastTrustworthyTokens = 0;
19
+ this.emittedThresholds = new Set();
20
+ this.terminal = false;
21
+ this.terminalPath = null;
22
+ this.status = "CONTINUE";
23
+ }
24
+
25
+ observe(sample = {}) {
26
+ if (this.terminal) {
27
+ return this._result([]);
28
+ }
29
+ if (sample === null || typeof sample !== "object" || Array.isArray(sample)) {
30
+ return this._failClosed("INVALID_USAGE");
31
+ }
32
+
33
+ const { totalTokens } = sample;
34
+ if (!Number.isSafeInteger(totalTokens) || totalTokens < 0) {
35
+ return this._failClosed("INVALID_USAGE");
36
+ }
37
+ if (totalTokens < this.lastTrustworthyTokens) {
38
+ return this._failClosed("REGRESSING_USAGE");
39
+ }
40
+
41
+ this.lastTrustworthyTokens = totalTokens;
42
+ const usagePercent = (totalTokens / this.tokenBudget) * 100;
43
+ const actions = [];
44
+
45
+ for (const threshold of THRESHOLDS) {
46
+ if (this.emittedThresholds.has(threshold.percent)
47
+ || usagePercent < threshold.percent) {
48
+ continue;
49
+ }
50
+
51
+ this.emittedThresholds.add(threshold.percent);
52
+ if (threshold.type === "TERMINAL_PATH") {
53
+ this.terminalPath = sample.outcomeComplete === true ? "COMPLETE" : "HANDOFF";
54
+ this.status = "TERMINAL_PATH";
55
+ actions.push({ type: this.terminalPath, thresholdPercent: threshold.percent });
56
+ continue;
57
+ }
58
+
59
+ actions.push({ type: threshold.type, thresholdPercent: threshold.percent });
60
+ if (threshold.type === "STOP") {
61
+ this.terminal = true;
62
+ this.status = this.terminalPath === "COMPLETE" ? "COMPLETE" : "INCOMPLETE";
63
+ }
64
+ }
65
+
66
+ return this._result(actions);
67
+ }
68
+
69
+ _failClosed(reason) {
70
+ this.terminal = true;
71
+ this.status = "INCOMPLETE";
72
+ return this._result([{ type: "STOP", reason }]);
73
+ }
74
+
75
+ _result(actions) {
76
+ return {
77
+ status: this.status,
78
+ terminal: this.terminal,
79
+ terminalPath: this.terminalPath,
80
+ tokenBudget: this.tokenBudget,
81
+ totalTokens: this.lastTrustworthyTokens,
82
+ lastTrustworthyTokens: this.lastTrustworthyTokens,
83
+ usagePercent: (this.lastTrustworthyTokens * 100) / this.tokenBudget,
84
+ actions,
85
+ };
86
+ }
87
+ }
88
+
89
+ module.exports = { EfficiencySupervisor, THRESHOLDS };