@sensigo/realm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/README.md +150 -0
  2. package/dist/adapters/adapter-utils.d.ts +14 -0
  3. package/dist/adapters/adapter-utils.d.ts.map +1 -0
  4. package/dist/adapters/adapter-utils.js +18 -0
  5. package/dist/adapters/adapter-utils.js.map +1 -0
  6. package/dist/adapters/airtable-adapter.d.ts +51 -0
  7. package/dist/adapters/airtable-adapter.d.ts.map +1 -0
  8. package/dist/adapters/airtable-adapter.js +425 -0
  9. package/dist/adapters/airtable-adapter.js.map +1 -0
  10. package/dist/adapters/file-adapter.d.ts +14 -0
  11. package/dist/adapters/file-adapter.d.ts.map +1 -0
  12. package/dist/adapters/file-adapter.js +89 -0
  13. package/dist/adapters/file-adapter.js.map +1 -0
  14. package/dist/adapters/github-adapter.d.ts +40 -0
  15. package/dist/adapters/github-adapter.d.ts.map +1 -0
  16. package/dist/adapters/github-adapter.js +286 -0
  17. package/dist/adapters/github-adapter.js.map +1 -0
  18. package/dist/adapters/gorgias-adapter.d.ts +40 -0
  19. package/dist/adapters/gorgias-adapter.d.ts.map +1 -0
  20. package/dist/adapters/gorgias-adapter.js +300 -0
  21. package/dist/adapters/gorgias-adapter.js.map +1 -0
  22. package/dist/adapters/http-adapter.d.ts +28 -0
  23. package/dist/adapters/http-adapter.d.ts.map +1 -0
  24. package/dist/adapters/http-adapter.js +97 -0
  25. package/dist/adapters/http-adapter.js.map +1 -0
  26. package/dist/adapters/mock-adapter.d.ts +16 -0
  27. package/dist/adapters/mock-adapter.d.ts.map +1 -0
  28. package/dist/adapters/mock-adapter.js +61 -0
  29. package/dist/adapters/mock-adapter.js.map +1 -0
  30. package/dist/adapters/notion-adapter.d.ts +54 -0
  31. package/dist/adapters/notion-adapter.d.ts.map +1 -0
  32. package/dist/adapters/notion-adapter.js +751 -0
  33. package/dist/adapters/notion-adapter.js.map +1 -0
  34. package/dist/adapters/parcelpanel-adapter.d.ts +60 -0
  35. package/dist/adapters/parcelpanel-adapter.d.ts.map +1 -0
  36. package/dist/adapters/parcelpanel-adapter.js +251 -0
  37. package/dist/adapters/parcelpanel-adapter.js.map +1 -0
  38. package/dist/adapters/rate-limiter.d.ts +26 -0
  39. package/dist/adapters/rate-limiter.d.ts.map +1 -0
  40. package/dist/adapters/rate-limiter.js +3 -0
  41. package/dist/adapters/rate-limiter.js.map +1 -0
  42. package/dist/adapters/shopify-adapter.d.ts +92 -0
  43. package/dist/adapters/shopify-adapter.d.ts.map +1 -0
  44. package/dist/adapters/shopify-adapter.js +415 -0
  45. package/dist/adapters/shopify-adapter.js.map +1 -0
  46. package/dist/adapters/slack-adapter.d.ts +18 -0
  47. package/dist/adapters/slack-adapter.d.ts.map +1 -0
  48. package/dist/adapters/slack-adapter.js +81 -0
  49. package/dist/adapters/slack-adapter.js.map +1 -0
  50. package/dist/adapters/token-bucket.d.ts +27 -0
  51. package/dist/adapters/token-bucket.d.ts.map +1 -0
  52. package/dist/adapters/token-bucket.js +109 -0
  53. package/dist/adapters/token-bucket.js.map +1 -0
  54. package/dist/config/secrets.d.ts +15 -0
  55. package/dist/config/secrets.d.ts.map +1 -0
  56. package/dist/config/secrets.js +33 -0
  57. package/dist/config/secrets.js.map +1 -0
  58. package/dist/engine/eligibility.d.ts +50 -0
  59. package/dist/engine/eligibility.d.ts.map +1 -0
  60. package/dist/engine/eligibility.js +267 -0
  61. package/dist/engine/eligibility.js.map +1 -0
  62. package/dist/engine/error-resolution.d.ts +20 -0
  63. package/dist/engine/error-resolution.d.ts.map +1 -0
  64. package/dist/engine/error-resolution.js +32 -0
  65. package/dist/engine/error-resolution.js.map +1 -0
  66. package/dist/engine/execution-loop.d.ts +101 -0
  67. package/dist/engine/execution-loop.d.ts.map +1 -0
  68. package/dist/engine/execution-loop.js +1156 -0
  69. package/dist/engine/execution-loop.js.map +1 -0
  70. package/dist/engine/lifecycle.d.ts +14 -0
  71. package/dist/engine/lifecycle.d.ts.map +1 -0
  72. package/dist/engine/lifecycle.js +17 -0
  73. package/dist/engine/lifecycle.js.map +1 -0
  74. package/dist/engine/precondition.d.ts +30 -0
  75. package/dist/engine/precondition.d.ts.map +1 -0
  76. package/dist/engine/precondition.js +125 -0
  77. package/dist/engine/precondition.js.map +1 -0
  78. package/dist/engine/prompt-template.d.ts +25 -0
  79. package/dist/engine/prompt-template.d.ts.map +1 -0
  80. package/dist/engine/prompt-template.js +66 -0
  81. package/dist/engine/prompt-template.js.map +1 -0
  82. package/dist/engine/render-template.d.ts +52 -0
  83. package/dist/engine/render-template.d.ts.map +1 -0
  84. package/dist/engine/render-template.js +548 -0
  85. package/dist/engine/render-template.js.map +1 -0
  86. package/dist/engine/state-guard.d.ts +15 -0
  87. package/dist/engine/state-guard.d.ts.map +1 -0
  88. package/dist/engine/state-guard.js +40 -0
  89. package/dist/engine/state-guard.js.map +1 -0
  90. package/dist/engine/trace-normalizer.d.ts +36 -0
  91. package/dist/engine/trace-normalizer.d.ts.map +1 -0
  92. package/dist/engine/trace-normalizer.js +146 -0
  93. package/dist/engine/trace-normalizer.js.map +1 -0
  94. package/dist/engine/trace-policy.d.ts +53 -0
  95. package/dist/engine/trace-policy.d.ts.map +1 -0
  96. package/dist/engine/trace-policy.js +35 -0
  97. package/dist/engine/trace-policy.js.map +1 -0
  98. package/dist/engine/workflow-context-loader.d.ts +9 -0
  99. package/dist/engine/workflow-context-loader.d.ts.map +1 -0
  100. package/dist/engine/workflow-context-loader.js +41 -0
  101. package/dist/engine/workflow-context-loader.js.map +1 -0
  102. package/dist/evidence/snapshot.d.ts +38 -0
  103. package/dist/evidence/snapshot.d.ts.map +1 -0
  104. package/dist/evidence/snapshot.js +53 -0
  105. package/dist/evidence/snapshot.js.map +1 -0
  106. package/dist/extensions/default-registry.d.ts +19 -0
  107. package/dist/extensions/default-registry.d.ts.map +1 -0
  108. package/dist/extensions/default-registry.js +31 -0
  109. package/dist/extensions/default-registry.js.map +1 -0
  110. package/dist/extensions/processor.d.ts +13 -0
  111. package/dist/extensions/processor.d.ts.map +1 -0
  112. package/dist/extensions/processor.js +3 -0
  113. package/dist/extensions/processor.js.map +1 -0
  114. package/dist/extensions/registry.d.ts +25 -0
  115. package/dist/extensions/registry.d.ts.map +1 -0
  116. package/dist/extensions/registry.js +43 -0
  117. package/dist/extensions/registry.js.map +1 -0
  118. package/dist/extensions/service-adapter.d.ts +35 -0
  119. package/dist/extensions/service-adapter.d.ts.map +1 -0
  120. package/dist/extensions/service-adapter.js +3 -0
  121. package/dist/extensions/service-adapter.js.map +1 -0
  122. package/dist/extensions/step-handler.d.ts +28 -0
  123. package/dist/extensions/step-handler.d.ts.map +1 -0
  124. package/dist/extensions/step-handler.js +3 -0
  125. package/dist/extensions/step-handler.js.map +1 -0
  126. package/dist/handlers/primitives/compare-strings.d.ts +13 -0
  127. package/dist/handlers/primitives/compare-strings.d.ts.map +1 -0
  128. package/dist/handlers/primitives/compare-strings.js +28 -0
  129. package/dist/handlers/primitives/compare-strings.js.map +1 -0
  130. package/dist/handlers/primitives/count-results.d.ts +21 -0
  131. package/dist/handlers/primitives/count-results.d.ts.map +1 -0
  132. package/dist/handlers/primitives/count-results.js +23 -0
  133. package/dist/handlers/primitives/count-results.js.map +1 -0
  134. package/dist/handlers/primitives/partition-by-substring.d.ts +18 -0
  135. package/dist/handlers/primitives/partition-by-substring.d.ts.map +1 -0
  136. package/dist/handlers/primitives/partition-by-substring.js +28 -0
  137. package/dist/handlers/primitives/partition-by-substring.js.map +1 -0
  138. package/dist/handlers/primitives/resolve-resource.d.ts +13 -0
  139. package/dist/handlers/primitives/resolve-resource.d.ts.map +1 -0
  140. package/dist/handlers/primitives/resolve-resource.js +21 -0
  141. package/dist/handlers/primitives/resolve-resource.js.map +1 -0
  142. package/dist/handlers/primitives/walk-field.d.ts +11 -0
  143. package/dist/handlers/primitives/walk-field.d.ts.map +1 -0
  144. package/dist/handlers/primitives/walk-field.js +31 -0
  145. package/dist/handlers/primitives/walk-field.js.map +1 -0
  146. package/dist/handlers/validate-field-match.d.ts +11 -0
  147. package/dist/handlers/validate-field-match.d.ts.map +1 -0
  148. package/dist/handlers/validate-field-match.js +39 -0
  149. package/dist/handlers/validate-field-match.js.map +1 -0
  150. package/dist/handlers/validate-verbatim-quotes.d.ts +11 -0
  151. package/dist/handlers/validate-verbatim-quotes.d.ts.map +1 -0
  152. package/dist/handlers/validate-verbatim-quotes.js +37 -0
  153. package/dist/handlers/validate-verbatim-quotes.js.map +1 -0
  154. package/dist/index.d.ts +57 -0
  155. package/dist/index.d.ts.map +1 -0
  156. package/dist/index.js +45 -0
  157. package/dist/index.js.map +1 -0
  158. package/dist/pipeline/processing-pipeline.d.ts +24 -0
  159. package/dist/pipeline/processing-pipeline.d.ts.map +1 -0
  160. package/dist/pipeline/processing-pipeline.js +53 -0
  161. package/dist/pipeline/processing-pipeline.js.map +1 -0
  162. package/dist/processors/compute-hash.d.ts +4 -0
  163. package/dist/processors/compute-hash.d.ts.map +1 -0
  164. package/dist/processors/compute-hash.js +14 -0
  165. package/dist/processors/compute-hash.js.map +1 -0
  166. package/dist/processors/normalize-text.d.ts +8 -0
  167. package/dist/processors/normalize-text.d.ts.map +1 -0
  168. package/dist/processors/normalize-text.js +47 -0
  169. package/dist/processors/normalize-text.js.map +1 -0
  170. package/dist/store/json-file-store.d.ts +24 -0
  171. package/dist/store/json-file-store.d.ts.map +1 -0
  172. package/dist/store/json-file-store.js +210 -0
  173. package/dist/store/json-file-store.js.map +1 -0
  174. package/dist/store/store-interface.d.ts +33 -0
  175. package/dist/store/store-interface.d.ts.map +1 -0
  176. package/dist/store/store-interface.js +2 -0
  177. package/dist/store/store-interface.js.map +1 -0
  178. package/dist/store/trace-buffer-store.d.ts +55 -0
  179. package/dist/store/trace-buffer-store.d.ts.map +1 -0
  180. package/dist/store/trace-buffer-store.js +113 -0
  181. package/dist/store/trace-buffer-store.js.map +1 -0
  182. package/dist/types/mcp-types.d.ts +17 -0
  183. package/dist/types/mcp-types.d.ts.map +1 -0
  184. package/dist/types/mcp-types.js +5 -0
  185. package/dist/types/mcp-types.js.map +1 -0
  186. package/dist/types/response-envelope.d.ts +96 -0
  187. package/dist/types/response-envelope.d.ts.map +1 -0
  188. package/dist/types/response-envelope.js +2 -0
  189. package/dist/types/response-envelope.js.map +1 -0
  190. package/dist/types/run-record.d.ts +169 -0
  191. package/dist/types/run-record.d.ts.map +1 -0
  192. package/dist/types/run-record.js +2 -0
  193. package/dist/types/run-record.js.map +1 -0
  194. package/dist/types/workflow-definition.d.ts +292 -0
  195. package/dist/types/workflow-definition.d.ts.map +1 -0
  196. package/dist/types/workflow-definition.js +2 -0
  197. package/dist/types/workflow-definition.js.map +1 -0
  198. package/dist/types/workflow-error.d.ts +26 -0
  199. package/dist/types/workflow-error.d.ts.map +1 -0
  200. package/dist/types/workflow-error.js +28 -0
  201. package/dist/types/workflow-error.js.map +1 -0
  202. package/dist/utils/schema-skeleton.d.ts +11 -0
  203. package/dist/utils/schema-skeleton.d.ts.map +1 -0
  204. package/dist/utils/schema-skeleton.js +40 -0
  205. package/dist/utils/schema-skeleton.js.map +1 -0
  206. package/dist/validation/input-schema.d.ts +26 -0
  207. package/dist/validation/input-schema.d.ts.map +1 -0
  208. package/dist/validation/input-schema.js +67 -0
  209. package/dist/validation/input-schema.js.map +1 -0
  210. package/dist/workflow/registrar.d.ts +20 -0
  211. package/dist/workflow/registrar.d.ts.map +1 -0
  212. package/dist/workflow/registrar.js +61 -0
  213. package/dist/workflow/registrar.js.map +1 -0
  214. package/dist/workflow/template-resolver.d.ts +25 -0
  215. package/dist/workflow/template-resolver.d.ts.map +1 -0
  216. package/dist/workflow/template-resolver.js +112 -0
  217. package/dist/workflow/template-resolver.js.map +1 -0
  218. package/dist/workflow/yaml-loader.d.ts +15 -0
  219. package/dist/workflow/yaml-loader.d.ts.map +1 -0
  220. package/dist/workflow/yaml-loader.js +408 -0
  221. package/dist/workflow/yaml-loader.js.map +1 -0
  222. package/package.json +61 -0
@@ -0,0 +1,1156 @@
1
+ import { WorkflowError } from '../types/workflow-error.js';
2
+ import { captureEvidence } from '../evidence/snapshot.js';
3
+ import { validateInputSchema, validateOutputSchema, validateTraceSchema, } from '../validation/input-schema.js';
4
+ import { normalizeTrace } from './trace-normalizer.js';
5
+ import { TERMINAL_PHASES } from './lifecycle.js';
6
+ import { checkPreconditions, evaluateAllPreconditions } from './precondition.js';
7
+ import { ExtensionRegistry } from '../extensions/registry.js';
8
+ import { createDefaultRegistry } from '../extensions/default-registry.js';
9
+ import { resolveSecret } from '../config/secrets.js';
10
+ import { renderTemplate, resolvePath, UnknownFilterError } from './render-template.js';
11
+ import { generateSchemaSkeleton } from '../utils/schema-skeleton.js';
12
+ import { loadWorkflowContext } from './workflow-context-loader.js';
13
+ import { findEligibleSteps, isWorkflowComplete, buildEvidenceByStep, propagateSkips, } from './eligibility.js';
14
+ import { resolvePreExecutionAgentAction, resolvePostDispatchAgentAction, } from './error-resolution.js';
15
+ function delayMs(ms) {
16
+ return new Promise((resolve) => setTimeout(resolve, ms));
17
+ }
18
+ /**
19
+ * Executes `dispatch` with a cancellation signal. If `dispatch` does not complete within `ms`
20
+ * milliseconds, the signal is aborted and a STEP_TIMEOUT WorkflowError is thrown.
21
+ */
22
+ function withTimeout(dispatch, ms, stepName) {
23
+ const controller = new AbortController();
24
+ let timer;
25
+ const timeout = new Promise((_, reject) => {
26
+ timer = setTimeout(() => {
27
+ controller.abort();
28
+ reject(new WorkflowError(`Step '${stepName}' timed out after ${ms}ms`, {
29
+ code: 'STEP_TIMEOUT',
30
+ category: 'ENGINE',
31
+ agentAction: 'report_to_user',
32
+ retryable: false,
33
+ details: { stepName, timeout_ms: ms },
34
+ }));
35
+ }, ms);
36
+ });
37
+ return Promise.race([dispatch(controller.signal), timeout]).finally(() => clearTimeout(timer));
38
+ }
39
+ /**
40
+ * Resolves an input_map declaration into a concrete params object.
41
+ * Falls back to options.input when input_map is absent.
42
+ */
43
+ function resolveInputMap(inputMap, options, pendingRun) {
44
+ if (inputMap === undefined)
45
+ return options.input;
46
+ const evidenceByStep = buildEvidenceByStep(pendingRun);
47
+ const root = {
48
+ run: { params: pendingRun.params },
49
+ context: { resources: evidenceByStep },
50
+ };
51
+ const result = {};
52
+ for (const [key, path] of Object.entries(inputMap)) {
53
+ result[key] = resolvePath(path, root);
54
+ }
55
+ return result;
56
+ }
57
+ /** Computes the delay (ms) before a retry attempt based on the configured backoff strategy. */
58
+ function computeBackoff(config, attemptNum) {
59
+ const backoff = config.backoff ?? 'fixed';
60
+ const base = config.base_delay_ms ?? 0;
61
+ let delay;
62
+ switch (backoff) {
63
+ case 'linear':
64
+ delay = base * attemptNum;
65
+ break;
66
+ case 'exponential':
67
+ delay = base * Math.pow(2, attemptNum - 1);
68
+ break;
69
+ default: // 'fixed'
70
+ delay = base;
71
+ }
72
+ return config.max_delay_ms !== undefined ? Math.min(delay, config.max_delay_ms) : delay;
73
+ }
74
+ /**
75
+ * Resolves and calls the service adapter for an auto step with `uses_service`.
76
+ *
77
+ * @param rateLimiterRegistry - Stable registry for rate-limiter state. Must be the same
78
+ * instance across all retry attempts of this step so that pause/resume coordination
79
+ * is preserved. Created once per executeStep() invocation and shared here.
80
+ */
81
+ async function callAdapter(stepDef, definition, options, pendingRun, rateLimiterRegistry, signal) {
82
+ const serviceName = stepDef.uses_service;
83
+ const serviceDef = definition.services?.[serviceName];
84
+ if (serviceDef === undefined) {
85
+ throw new WorkflowError(`Service '${serviceName}' not found in workflow definition`, {
86
+ code: 'ENGINE_ADAPTER_FAILED',
87
+ category: 'ENGINE',
88
+ agentAction: 'stop',
89
+ retryable: false,
90
+ stepId: options.command,
91
+ });
92
+ }
93
+ // Adapter lookup: use the caller-provided registry for custom adapters; fall back to
94
+ // the built-in default registry (FileSystemAdapter etc.) when none is provided.
95
+ const adapter = (options.registry ?? createDefaultRegistry()).getAdapter(serviceDef.adapter);
96
+ if (adapter === undefined) {
97
+ throw new WorkflowError(`Adapter '${serviceDef.adapter}' for service '${serviceName}' is not registered`, {
98
+ code: 'ENGINE_ADAPTER_FAILED',
99
+ category: 'ENGINE',
100
+ agentAction: 'stop',
101
+ retryable: false,
102
+ stepId: options.command,
103
+ });
104
+ }
105
+ const secrets = options.secrets ?? {};
106
+ const config = { adapter: serviceDef.adapter, trust: serviceDef.trust };
107
+ if (serviceDef.auth?.token_from !== undefined) {
108
+ config['auth'] = { token: resolveSecret(serviceDef.auth.token_from, secrets) };
109
+ }
110
+ const method = stepDef.service_method ?? 'fetch';
111
+ const operation = stepDef.operation ?? options.command;
112
+ const adapterParams = resolveInputMap(stepDef.input_map, options, pendingRun);
113
+ // Guard: `delete` is optional on ServiceAdapter. If the adapter omits it, surface
114
+ // ADAPTER_OP_UNSUPPORTED rather than allowing a TypeError from an undefined call.
115
+ const adapterMethod = adapter[method];
116
+ if (typeof adapterMethod !== 'function') {
117
+ throw new WorkflowError(`Adapter '${serviceDef.adapter}' does not support service_method '${method}'`, {
118
+ code: 'ADAPTER_OP_UNSUPPORTED',
119
+ category: 'ENGINE',
120
+ agentAction: 'report_to_user',
121
+ retryable: false,
122
+ });
123
+ }
124
+ // Proactive rate limiting: acquire a token before calling the service.
125
+ // rateLimiterRegistry is always defined (step-scoped; see executeStep).
126
+ if (serviceDef.rate_limit?.requests_per_second !== undefined) {
127
+ await rateLimiterRegistry
128
+ .getOrCreateRateLimiter(serviceName, serviceDef.rate_limit)
129
+ .acquire(signal);
130
+ }
131
+ let response;
132
+ try {
133
+ response = await adapterMethod.call(adapter, operation, adapterParams, config, signal);
134
+ }
135
+ catch (err) {
136
+ if (err instanceof WorkflowError) {
137
+ if (err.code === 'SERVICE_RATE_LIMITED') {
138
+ // Resolve retry_after through the three-tier fallback chain:
139
+ // 1. Header value (already on err.retry_after from the adapter)
140
+ // 2. rate_limit.fallback_retry_seconds from YAML
141
+ // 3. adapter.defaultRetryAfterSeconds constant
142
+ const resolvedRetryAfter = err.retry_after ??
143
+ serviceDef.rate_limit?.fallback_retry_seconds ??
144
+ adapter?.defaultRetryAfterSeconds;
145
+ // Pause the token bucket for the resolved retry window.
146
+ // The cap (Math.min with max_retry_seconds) is applied before calling pause() so
147
+ // that concurrent callers are never held longer than max_retry_seconds. The pause
148
+ // fires before the fail-fast throw below — this is intentional: even when the
149
+ // current step exits immediately, the bucket is paused to prevent a burst of
150
+ // immediate retries from concurrent steps against an already-overloaded service.
151
+ const maxRetry = serviceDef.rate_limit?.max_retry_seconds;
152
+ if (serviceDef.rate_limit?.requests_per_second !== undefined &&
153
+ resolvedRetryAfter !== undefined) {
154
+ const pauseDuration = maxRetry !== undefined ? Math.min(resolvedRetryAfter, maxRetry) : resolvedRetryAfter;
155
+ if (pauseDuration > 0) {
156
+ rateLimiterRegistry
157
+ .getOrCreateRateLimiter(serviceName, serviceDef.rate_limit)
158
+ .pause(pauseDuration);
159
+ }
160
+ }
161
+ // Fail fast when the retry window exceeds max_retry_seconds.
162
+ // The pause above has already fired with a capped duration — this throw
163
+ // signals the retry loop to stop rather than wait the full retry window.
164
+ if (maxRetry !== undefined &&
165
+ resolvedRetryAfter !== undefined &&
166
+ resolvedRetryAfter > maxRetry) {
167
+ throw new WorkflowError(err.message, {
168
+ code: 'SERVICE_RATE_LIMITED',
169
+ category: 'SERVICE',
170
+ agentAction: 'report_to_user',
171
+ retryable: false,
172
+ retry_after: resolvedRetryAfter,
173
+ ...(Object.keys(err.details).length > 0 ? { details: err.details } : {}),
174
+ });
175
+ }
176
+ // Re-throw with the resolved retry_after (may differ from original).
177
+ if (resolvedRetryAfter !== err.retry_after) {
178
+ throw new WorkflowError(err.message, {
179
+ code: 'SERVICE_RATE_LIMITED',
180
+ category: 'SERVICE',
181
+ agentAction: 'wait_and_proceed',
182
+ retryable: true,
183
+ ...(resolvedRetryAfter !== undefined ? { retry_after: resolvedRetryAfter } : {}),
184
+ ...(Object.keys(err.details).length > 0 ? { details: err.details } : {}),
185
+ });
186
+ }
187
+ }
188
+ throw err;
189
+ }
190
+ const message = err instanceof Error ? err.message : String(err);
191
+ throw new WorkflowError(`Adapter '${serviceDef.adapter}' threw: ${message}`, {
192
+ code: 'ENGINE_ADAPTER_FAILED',
193
+ category: 'ENGINE',
194
+ agentAction: 'stop',
195
+ retryable: false,
196
+ stepId: options.command,
197
+ });
198
+ }
199
+ const output = typeof response.data === 'object' && response.data !== null
200
+ ? response.data
201
+ : { data: response.data, status: response.status };
202
+ return {
203
+ output,
204
+ resolvedParams: stepDef.input_map !== undefined ? adapterParams : undefined,
205
+ };
206
+ }
207
+ /**
208
+ * Resolves and calls the step handler for an auto step with a `handler` reference.
209
+ */
210
+ async function callHandler(stepDef, options, pendingRun, evidenceByStep, signal) {
211
+ const handlerName = stepDef.handler;
212
+ const handler = (options.registry ?? createDefaultRegistry()).getHandler(handlerName);
213
+ if (handler === undefined) {
214
+ throw new WorkflowError(`Handler '${handlerName}' is not registered`, {
215
+ code: 'ENGINE_HANDLER_FAILED',
216
+ category: 'ENGINE',
217
+ agentAction: 'stop',
218
+ retryable: false,
219
+ stepId: options.command,
220
+ });
221
+ }
222
+ let result;
223
+ try {
224
+ result = await handler.execute({ params: options.input }, {
225
+ run_id: options.runId,
226
+ run_params: pendingRun.params,
227
+ config: stepDef.config ?? {},
228
+ resources: evidenceByStep,
229
+ }, signal);
230
+ }
231
+ catch (err) {
232
+ if (err instanceof WorkflowError)
233
+ throw err;
234
+ const message = err instanceof Error ? err.message : String(err);
235
+ throw new WorkflowError(`Handler '${handlerName}' threw: ${message}`, {
236
+ code: 'ENGINE_HANDLER_FAILED',
237
+ category: 'ENGINE',
238
+ agentAction: 'stop',
239
+ retryable: false,
240
+ stepId: options.command,
241
+ });
242
+ }
243
+ return result.data;
244
+ }
245
+ /**
246
+ * Builds a NextAction for a single eligible step, resolving prompt templates.
247
+ */
248
+ function stepToNextAction(stepName, step, context) {
249
+ const resolvedPrompt = step.prompt !== undefined ? renderTemplate(step.prompt, context) : undefined;
250
+ return {
251
+ instruction: step.handler !== undefined
252
+ ? { tool: step.handler, params: {}, call_with: {} }
253
+ : step.execution === 'agent'
254
+ ? {
255
+ tool: 'execute_step',
256
+ params: { run_id: context.runId, command: stepName },
257
+ call_with: {
258
+ run_id: context.runId,
259
+ command: stepName,
260
+ params: step.input_schema !== undefined
261
+ ? generateSchemaSkeleton(step.input_schema)
262
+ : {},
263
+ },
264
+ }
265
+ : null,
266
+ ...(step.execution === 'agent' && step.input_schema !== undefined
267
+ ? { input_schema: step.input_schema }
268
+ : {}),
269
+ human_readable: `Execute step '${stepName}': ${step.description}`,
270
+ orientation: `Run is active. Next step ready: '${stepName}'.`,
271
+ ...(step.timeout_seconds !== undefined ? { expected_timeout: `${step.timeout_seconds}s` } : {}),
272
+ ...(resolvedPrompt !== undefined ? { prompt: resolvedPrompt } : {}),
273
+ };
274
+ }
275
+ /**
276
+ * Returns NextAction objects for all agent-executable eligible steps.
277
+ * Auto steps are excluded — they are executed internally by executeChain.
278
+ */
279
+ export function buildNextActions(definition, run) {
280
+ const eligible = findEligibleSteps(definition, run);
281
+ const evidenceByStep = buildEvidenceByStep(run);
282
+ const context = {
283
+ evidenceByStep,
284
+ runParams: run.params,
285
+ runId: run.id,
286
+ ...(run.workflow_context_snapshots !== undefined
287
+ ? {
288
+ workflowContext: {
289
+ snapshots: run.workflow_context_snapshots,
290
+ wrapper: (definition.context_wrapper ?? 'xml'),
291
+ },
292
+ }
293
+ : {}),
294
+ };
295
+ return eligible
296
+ .filter((name) => definition.steps[name]?.execution === 'agent' ||
297
+ definition.steps[name]?.handler !== undefined)
298
+ .map((name) => stepToNextAction(name, definition.steps[name], context));
299
+ }
300
+ /**
301
+ * Merges call-scoped trace-schema warnings with an optional cleanup warning into
302
+ * a single warnings array. Trace warnings are listed first (deterministic order).
303
+ */
304
+ function mergeWarnings(traceWarnings, cleanupWarning) {
305
+ if (traceWarnings.length === 0 && cleanupWarning === undefined)
306
+ return [];
307
+ return cleanupWarning !== undefined ? [...traceWarnings, cleanupWarning] : [...traceWarnings];
308
+ }
309
+ /** Builds a minimal error ResponseEnvelope from primitive fields. */
310
+ function errorEnvelope(command, runId, runVersion, err, contextHint) {
311
+ return {
312
+ command,
313
+ run_id: runId,
314
+ run_version: runVersion,
315
+ status: 'error',
316
+ data: {},
317
+ evidence: [],
318
+ warnings: [],
319
+ errors: [err.message],
320
+ error_code: err.code,
321
+ ...(Object.keys(err.details).length > 0 ? { error_details: err.details } : {}),
322
+ agent_action: err.agentAction,
323
+ ...(err.retry_after !== undefined ? { retry_after: err.retry_after } : {}),
324
+ context_hint: contextHint ?? `Error during '${command}'.`,
325
+ next_actions: [],
326
+ };
327
+ }
328
+ /**
329
+ * Builds a ResponseEnvelope for errors caught in an MCP tool's outer catch block,
330
+ * before any step execution has occurred. Translates provide_input and
331
+ * resolve_precondition agent actions to report_to_user (pre-execution context).
332
+ */
333
+ export function buildPreExecutionErrorEnvelope(command, runId, runVersion, err, contextHint) {
334
+ const agentAction = resolvePreExecutionAgentAction(err);
335
+ return {
336
+ command,
337
+ run_id: runId,
338
+ run_version: runVersion,
339
+ status: 'error',
340
+ data: {},
341
+ evidence: [],
342
+ warnings: [],
343
+ errors: [err.message],
344
+ error_code: err.code,
345
+ ...(Object.keys(err.details).length > 0 ? { error_details: err.details } : {}),
346
+ agent_action: agentAction,
347
+ ...(err.retry_after !== undefined ? { retry_after: err.retry_after } : {}),
348
+ context_hint: contextHint ?? `Error during '${command}'.`,
349
+ next_actions: [],
350
+ };
351
+ }
352
+ function makeErrorEnvelope(options, run, err, definition, extraWarnings) {
353
+ const hint = run !== null ? `Error during '${options.command}'. Run phase: '${run.run_phase}'.` : undefined;
354
+ const base = errorEnvelope(options.command, options.runId, run !== null ? run.version : 0, err, hint);
355
+ // Apply pre-execution translation: provide_input / resolve_precondition cannot
356
+ // apply before claimStep — translate them to report_to_user.
357
+ const translatedBase = run === null ? { ...base, agent_action: resolvePreExecutionAgentAction(err) } : base;
358
+ const baseWithWarnings = extraWarnings !== undefined && extraWarnings.length > 0
359
+ ? { ...translatedBase, warnings: extraWarnings }
360
+ : translatedBase;
361
+ if (run !== null && definition !== undefined && err.agentAction !== 'stop') {
362
+ return { ...baseWithWarnings, next_actions: buildNextActions(definition, run) };
363
+ }
364
+ return baseWithWarnings;
365
+ }
366
+ /**
367
+ * Validates eligibility, claims the step, executes it through the dispatcher with retry
368
+ * and timeout support, captures evidence, persists the updated run record, and returns
369
+ * a ResponseEnvelope containing the outcome and the next eligible actions.
370
+ */
371
+ export async function executeStep(store, definition, options) {
372
+ // Step 1: Load run.
373
+ let run;
374
+ try {
375
+ run = await store.get(options.runId);
376
+ }
377
+ catch (err) {
378
+ if (err instanceof WorkflowError) {
379
+ return makeErrorEnvelope(options, null, err);
380
+ }
381
+ const internal = new WorkflowError('Failed to load run from store', {
382
+ code: 'ENGINE_STORE_FAILED',
383
+ category: 'ENGINE',
384
+ agentAction: 'stop',
385
+ retryable: false,
386
+ });
387
+ return makeErrorEnvelope(options, null, internal);
388
+ }
389
+ // Step 2: Check eligibility.
390
+ const eligible = findEligibleSteps(definition, run);
391
+ if (!eligible.includes(options.command)) {
392
+ const nextActions = buildNextActions(definition, run);
393
+ return {
394
+ command: options.command,
395
+ run_id: options.runId,
396
+ run_version: run.version,
397
+ status: 'blocked',
398
+ data: {},
399
+ evidence: [],
400
+ warnings: [],
401
+ errors: [],
402
+ agent_action: 'resolve_precondition',
403
+ context_hint: `Step '${options.command}' is not eligible in the current run state.`,
404
+ next_actions: nextActions,
405
+ blocked_reason: nextActions.length > 0
406
+ ? {
407
+ eligible_steps: eligible,
408
+ suggestion: `Call one of the steps indicated in next_actions instead.`,
409
+ }
410
+ : {
411
+ eligible_steps: eligible,
412
+ suggestion: `No eligible steps available. Check run_phase and completed_steps.`,
413
+ },
414
+ };
415
+ }
416
+ const stepDef = definition.steps[options.command];
417
+ const evidenceByStep = buildEvidenceByStep(run);
418
+ // Step 2a: Evaluate preconditions.
419
+ if (stepDef?.preconditions !== undefined && stepDef.preconditions.length > 0) {
420
+ const failed = checkPreconditions(stepDef.preconditions, evidenceByStep);
421
+ if (failed !== null) {
422
+ return {
423
+ command: options.command,
424
+ run_id: options.runId,
425
+ run_version: run.version,
426
+ status: 'blocked',
427
+ data: {},
428
+ evidence: [],
429
+ warnings: [],
430
+ errors: [],
431
+ agent_action: 'stop',
432
+ context_hint: `Precondition failed for step '${options.command}'.`,
433
+ next_actions: [],
434
+ blocked_reason: {
435
+ eligible_steps: eligible,
436
+ suggestion: `Precondition failed: '${failed.expression}'. Resolved value: ${String(failed.resolved_value)}.`,
437
+ },
438
+ };
439
+ }
440
+ }
441
+ const preconditionTrace = evaluateAllPreconditions(stepDef?.preconditions ?? [], evidenceByStep);
442
+ let inputTokenEstimate = Math.ceil(JSON.stringify(options.input).length / 4);
443
+ // Step 2b: Validate input schema.
444
+ if (stepDef?.input_schema !== undefined) {
445
+ try {
446
+ validateInputSchema(options.input, stepDef.input_schema, options.command);
447
+ }
448
+ catch (err) {
449
+ return makeErrorEnvelope(options, run, err, definition);
450
+ }
451
+ }
452
+ // Step 2c: Validate output schema (agent steps only).
453
+ // For agent steps dispatch is a pass-through, so options.input IS the agent's
454
+ // submitted output. Validating here (pre-claim) is equivalent to
455
+ // "post-generation, pre-commit" — the standard output guardrail position.
456
+ if (stepDef?.execution === 'agent' && stepDef.output_schema !== undefined) {
457
+ try {
458
+ validateOutputSchema(options.input, stepDef.output_schema, options.command);
459
+ }
460
+ catch (err) {
461
+ return makeErrorEnvelope(options, run, err, definition);
462
+ }
463
+ }
464
+ // Step 2d: Merge WAL buffer + execute_step trace, normalize, validate (agent steps only, pre-claim).
465
+ // walEntries is declared at this scope so it is in scope at the captureEvidence call site below.
466
+ const traceWarnings = [];
467
+ let preNormalizedTrace;
468
+ let walEntries = [];
469
+ if (stepDef?.execution === 'agent') {
470
+ // Read WAL buffer if a buffer store is configured.
471
+ walEntries =
472
+ options.traceBufferStore !== undefined
473
+ ? await options.traceBufferStore.read(options.runId, options.command)
474
+ : [];
475
+ const hasAnyTrace = walEntries.length > 0 || (options.trace !== undefined && options.trace.length > 0);
476
+ if (hasAnyTrace) {
477
+ // Build merge set: WAL entries carry their _internalTs; execute_step entries
478
+ // receive Date.now() so they sort after all WAL batches (step conclusion ordering).
479
+ const finalTs = Date.now();
480
+ const mergeSet = [
481
+ ...walEntries, // already have _internalTs from buffer store
482
+ ...(options.trace ?? []).map((e) => ({ ...e, _internalTs: finalTs })),
483
+ ];
484
+ // Sort by _internalTs to produce chronological order, then strip the field before
485
+ // passing to normalizeTrace (which operates on plain AgentTraceEntry[]).
486
+ mergeSet.sort((a, b) => a._internalTs - b._internalTs);
487
+ const sortedEntries = mergeSet.map(({ _internalTs: _, ...rest }) => rest);
488
+ // Normalize the merged set once. This is the single canonicalization pass.
489
+ preNormalizedTrace = normalizeTrace(sortedEntries);
490
+ // Validate trace schema if configured (unchanged call site).
491
+ if (stepDef.trace_schema !== undefined) {
492
+ const mode = stepDef.trace_validation_mode ?? 'warn';
493
+ if (mode === 'enforce') {
494
+ try {
495
+ validateTraceSchema(preNormalizedTrace.entries, stepDef.trace_schema, options.command, 'enforce');
496
+ preNormalizedTrace.summary.schema_applied = true;
497
+ preNormalizedTrace.summary.validation_mode = 'enforce';
498
+ preNormalizedTrace.summary.validation_errors = 0;
499
+ }
500
+ catch (err) {
501
+ // On enforce rejection: do NOT delete the WAL — agent retries with WAL preserved.
502
+ return makeErrorEnvelope(options, run, err, definition);
503
+ }
504
+ }
505
+ else {
506
+ const result = validateTraceSchema(preNormalizedTrace.entries, stepDef.trace_schema, options.command, 'warn');
507
+ preNormalizedTrace.summary.schema_applied = true;
508
+ preNormalizedTrace.summary.validation_mode = 'warn';
509
+ preNormalizedTrace.summary.validation_errors = result.errorCount;
510
+ if (result.errorCount > 0) {
511
+ traceWarnings.push(result.warning);
512
+ }
513
+ }
514
+ }
515
+ }
516
+ }
517
+ // Step 3: Claim the step — adds to in_progress_steps under file lock.
518
+ let pendingRun;
519
+ try {
520
+ pendingRun = await store.claimStep(options.runId, options.command, definition);
521
+ }
522
+ catch (err) {
523
+ if (err instanceof WorkflowError) {
524
+ if (err.code === 'STATE_STEP_ALREADY_CLAIMED') {
525
+ const freshRun = await store.get(options.runId).catch(() => run);
526
+ return {
527
+ command: options.command,
528
+ run_id: options.runId,
529
+ run_version: freshRun.version,
530
+ status: 'blocked',
531
+ data: {},
532
+ evidence: [],
533
+ warnings: [],
534
+ errors: [],
535
+ agent_action: 'resolve_precondition',
536
+ context_hint: `Step '${options.command}' was already claimed by another process.`,
537
+ next_actions: buildNextActions(definition, freshRun),
538
+ blocked_reason: {
539
+ eligible_steps: findEligibleSteps(definition, freshRun),
540
+ suggestion: `Step is already in progress. Wait for it to complete.`,
541
+ },
542
+ };
543
+ }
544
+ return makeErrorEnvelope(options, run, err, definition, traceWarnings.length > 0 ? traceWarnings : undefined);
545
+ }
546
+ return makeErrorEnvelope(options, run, new WorkflowError('Failed to claim step', {
547
+ code: 'ENGINE_STORE_FAILED',
548
+ category: 'ENGINE',
549
+ agentAction: 'stop',
550
+ retryable: false,
551
+ }), definition, traceWarnings.length > 0 ? traceWarnings : undefined);
552
+ }
553
+ // Load workflow context once at run start — skip if already populated.
554
+ if (definition.workflow_context !== undefined &&
555
+ Object.keys(definition.workflow_context).length > 0 &&
556
+ pendingRun.workflow_context_snapshots === undefined) {
557
+ const contextSnapshots = await loadWorkflowContext(definition);
558
+ pendingRun = await store.update({
559
+ ...pendingRun,
560
+ workflow_context_snapshots: contextSnapshots,
561
+ });
562
+ }
563
+ // Step 4: Dispatch with retry and timeout.
564
+ const retryConfig = stepDef?.retry;
565
+ const maxAttempts = retryConfig?.max_attempts ?? 1;
566
+ const timeoutMs = stepDef?.timeout_seconds !== undefined ? stepDef.timeout_seconds * 1000 : undefined;
567
+ // Create a stable rate-limiter registry for all retry attempts of this step.
568
+ // Shared state ensures that a pause() triggered on attempt N is still in effect
569
+ // when the proactive acquire() runs on attempt N+1. When the caller provides an
570
+ // explicit registry, it is used directly (also enables cross-step coordination);
571
+ // otherwise a step-scoped fallback is created so rate limiting still works.
572
+ const rateLimiterRegistry = options.registry ?? new ExtensionRegistry();
573
+ let output = {};
574
+ let dispatchError = null;
575
+ let attemptsUsed = 0;
576
+ const allEvidence = [];
577
+ for (let attemptNum = 1; attemptNum <= maxAttempts; attemptNum++) {
578
+ attemptsUsed = attemptNum;
579
+ const startedAt = new Date();
580
+ let attemptOutput = {};
581
+ let attemptError = null;
582
+ let resolvedParams;
583
+ try {
584
+ const makeCall = (signal) => {
585
+ if (stepDef?.execution === 'auto' && stepDef.uses_service !== undefined) {
586
+ return callAdapter(stepDef, definition, options, pendingRun, rateLimiterRegistry, signal);
587
+ }
588
+ else if (stepDef?.execution === 'auto' && stepDef.handler !== undefined) {
589
+ return callHandler(stepDef, options, pendingRun, evidenceByStep, signal).then((result) => ({ output: result, resolvedParams: undefined }));
590
+ }
591
+ else {
592
+ return options
593
+ .dispatcher(options.command, options.input, pendingRun, signal)
594
+ .then((result) => ({ output: result, resolvedParams: undefined }));
595
+ }
596
+ };
597
+ const callResult = timeoutMs !== undefined
598
+ ? await withTimeout((signal) => makeCall(signal), timeoutMs, options.command)
599
+ : await makeCall();
600
+ attemptOutput = callResult.output;
601
+ resolvedParams = callResult.resolvedParams;
602
+ if (resolvedParams !== undefined) {
603
+ inputTokenEstimate = Math.ceil(JSON.stringify(resolvedParams).length / 4);
604
+ }
605
+ }
606
+ catch (err) {
607
+ if (err instanceof WorkflowError) {
608
+ attemptError = err;
609
+ }
610
+ else {
611
+ const message = err instanceof Error ? err.message : String(err);
612
+ attemptError = new WorkflowError(`Dispatcher failed: ${message}`, {
613
+ code: 'ENGINE_INTERNAL',
614
+ category: 'ENGINE',
615
+ agentAction: 'stop',
616
+ retryable: false,
617
+ stepId: options.command,
618
+ });
619
+ }
620
+ }
621
+ const completedAt = new Date();
622
+ const profile = stepDef?.agent_profile;
623
+ const profileData = profile !== undefined ? definition.resolved_profiles?.[profile] : undefined;
624
+ const baseSnap = captureEvidence({
625
+ stepId: options.command,
626
+ startedAt,
627
+ completedAt,
628
+ input: options.input,
629
+ output: attemptOutput,
630
+ ...(attemptError !== null ? { error: attemptError.message } : {}),
631
+ diagnostics: {
632
+ input_token_estimate: inputTokenEstimate,
633
+ precondition_trace: preconditionTrace,
634
+ },
635
+ ...(profileData !== undefined
636
+ ? { agentProfile: profile, agentProfileHash: profileData.content_hash }
637
+ : {}),
638
+ ...(resolvedParams !== undefined ? { resolvedParams } : {}),
639
+ ...(options.stepMeta?.toolCalls !== undefined
640
+ ? { toolCalls: options.stepMeta.toolCalls }
641
+ : {}),
642
+ // Gate trace to agent steps only — drop silently for auto/adapter/handler steps.
643
+ // When pre-normalized (WAL merge + schema validation ran), pass the pre-normalized
644
+ // result to avoid double normalization. Also handle WAL-only case (options.trace may
645
+ // be undefined while walEntries contributed entries via preNormalizedTrace).
646
+ ...(stepDef?.execution === 'agent' && (options.trace !== undefined || walEntries.length > 0)
647
+ ? preNormalizedTrace !== undefined
648
+ ? { normalizedTrace: preNormalizedTrace }
649
+ : { trace: options.trace ?? [] }
650
+ : {}),
651
+ });
652
+ const snap = retryConfig !== undefined ? { ...baseSnap, attempt: attemptNum } : baseSnap;
653
+ allEvidence.push(snap);
654
+ if (attemptError === null) {
655
+ output = attemptOutput;
656
+ dispatchError = null;
657
+ break;
658
+ }
659
+ dispatchError = attemptError;
660
+ const willRetry = retryConfig !== undefined && attemptError.retryable && attemptNum < maxAttempts;
661
+ if (willRetry) {
662
+ const baseBackoff = computeBackoff(retryConfig, attemptNum);
663
+ const retryAfterMs = attemptError instanceof WorkflowError && attemptError.retry_after !== undefined
664
+ ? attemptError.retry_after * 1000
665
+ : 0;
666
+ await delayMs(Math.max(baseBackoff, retryAfterMs));
667
+ }
668
+ else {
669
+ break;
670
+ }
671
+ }
672
+ if (dispatchError !== null && retryConfig !== undefined && attemptsUsed === maxAttempts) {
673
+ const lastError = dispatchError;
674
+ dispatchError = new WorkflowError(`Step '${options.command}' failed after ${attemptsUsed} attempts`, {
675
+ code: 'STEP_RETRY_EXHAUSTED',
676
+ category: 'ENGINE',
677
+ agentAction: 'report_to_user',
678
+ retryable: false,
679
+ details: {
680
+ stepName: options.command,
681
+ attempts: attemptsUsed,
682
+ lastError: lastError.message,
683
+ ...(lastError.retry_after !== undefined ? { retry_after: lastError.retry_after } : {}),
684
+ },
685
+ });
686
+ }
687
+ // Step 5: Handle dispatch failure — move step to failed_steps.
688
+ if (dispatchError !== null) {
689
+ // Pure in-memory derivations — no I/O, no try required.
690
+ const afterFail = {
691
+ ...pendingRun,
692
+ in_progress_steps: pendingRun.in_progress_steps.filter((s) => s !== options.command),
693
+ failed_steps: [...pendingRun.failed_steps, options.command],
694
+ };
695
+ // Propagate skips: mark steps whose trigger_rule can never be satisfied after this failure.
696
+ const withSkippedFail = {
697
+ ...afterFail,
698
+ skipped_steps: propagateSkips(afterFail, definition),
699
+ };
700
+ // A run is terminal when all steps are settled OR when no step will ever become
701
+ // eligible again (safety net for when-condition edge cases not covered by propagateSkips).
702
+ const isComplete = isWorkflowComplete(withSkippedFail, definition) ||
703
+ (withSkippedFail.in_progress_steps.length === 0 &&
704
+ findEligibleSteps(definition, withSkippedFail).length === 0);
705
+ const failedRun = {
706
+ ...withSkippedFail,
707
+ evidence: [...pendingRun.evidence, ...allEvidence],
708
+ terminal_state: isComplete,
709
+ ...(isComplete
710
+ ? { terminal_reason: `Step '${options.command}' failed: ${dispatchError.message}` }
711
+ : {}),
712
+ };
713
+ // Persist run state and WAL cleanup in separate try/catch blocks so a WAL deletion
714
+ // failure does not mask a successful store.update.
715
+ let persistedRun;
716
+ let storeCleanupWarning;
717
+ try {
718
+ persistedRun = await store.update(failedRun);
719
+ }
720
+ catch (storeErr) {
721
+ storeCleanupWarning = `Failed to persist step failure: ${storeErr instanceof Error ? storeErr.message : String(storeErr)}`;
722
+ }
723
+ let walCleanupWarning;
724
+ try {
725
+ // Delete WAL after run state is written for failure — entries are now in evidence.
726
+ await options.traceBufferStore?.delete(options.runId, options.command);
727
+ }
728
+ catch (walErr) {
729
+ walCleanupWarning = `Failed to clean up trace buffer after step failure: ${walErr instanceof Error ? walErr.message : String(walErr)}`;
730
+ }
731
+ // Derive the agent_action from the error semantics and run termination state.
732
+ //
733
+ // Non-terminal 'stop' errors (e.g. auth failure with a recovery branch) are surfaced
734
+ // as 'report_to_user' so the agent knows recovery steps are available. Terminal runs
735
+ // stay 'stop' — no further progress is possible.
736
+ //
737
+ // 'provide_input' and 'resolve_precondition' both imply "retry the same command" which
738
+ // is impossible once a step is in failed_steps; translate both to 'report_to_user'.
739
+ const effectiveAction = resolvePostDispatchAgentAction(dispatchError, (persistedRun ?? failedRun).terminal_state);
740
+ // Mirror makeErrorEnvelope: populate next_actions for any action other than 'stop',
741
+ // but only when store.update succeeded (inconsistent state → no reliable next_actions).
742
+ let nextActions = [];
743
+ if (effectiveAction !== 'stop' && storeCleanupWarning === undefined) {
744
+ try {
745
+ nextActions = buildNextActions(definition, persistedRun ?? failedRun);
746
+ }
747
+ catch {
748
+ // buildNextActions can throw for unresolvable template references; fall back to [].
749
+ }
750
+ }
751
+ const contextHint = effectiveAction === 'stop'
752
+ ? `Step '${options.command}' failed. Run is terminated.`
753
+ : effectiveAction === 'wait_and_proceed'
754
+ ? `Step '${options.command}' was rate-limited. Wait ${dispatchError.retry_after !== undefined ? `${dispatchError.retry_after} second(s)` : 'a moment'} then follow next_actions — no human intervention required.`
755
+ : effectiveAction === 'wait_for_human'
756
+ ? `Step '${options.command}' failed due to external service unavailability. Wait for service recovery, then proceed with the steps in next_actions.`
757
+ : `Step '${options.command}' failed. ${isComplete ? 'Run is terminated.' : 'Recovery steps are available in next_actions.'}`;
758
+ return {
759
+ command: options.command,
760
+ run_id: options.runId,
761
+ run_version: (persistedRun ?? failedRun).version,
762
+ status: 'error',
763
+ data: {},
764
+ evidence: allEvidence,
765
+ warnings: mergeWarnings(traceWarnings, storeCleanupWarning ?? walCleanupWarning),
766
+ errors: [dispatchError.message],
767
+ agent_action: effectiveAction,
768
+ ...(dispatchError.retry_after !== undefined
769
+ ? { retry_after: dispatchError.retry_after }
770
+ : {}),
771
+ context_hint: contextHint,
772
+ next_actions: nextActions,
773
+ };
774
+ }
775
+ // Step 5b: Gate check — if trust requires human confirmation, open a gate and halt.
776
+ if (stepDef.trust === 'human_confirmed' || stepDef.trust === 'human_reviewed') {
777
+ const gate_id = crypto.randomUUID();
778
+ const choicesRaw = stepDef.gate?.choices ?? stepDef.input_schema?.properties?.['choice']?.enum;
779
+ const choices = Array.isArray(choicesRaw) ? choicesRaw : ['approve', 'reject'];
780
+ const step_name = options.command;
781
+ // Resolve gate.message if configured — fail-fast on unresolvable references.
782
+ const gateEvidenceCtxEarly = { ...evidenceByStep, [options.command]: output };
783
+ const wfCtxSpreadEarly = pendingRun.workflow_context_snapshots !== undefined
784
+ ? {
785
+ workflowContext: {
786
+ snapshots: pendingRun.workflow_context_snapshots,
787
+ wrapper: (definition.context_wrapper ?? 'xml'),
788
+ },
789
+ }
790
+ : {};
791
+ let resolvedGateMessage;
792
+ if (stepDef.gate?.message !== undefined) {
793
+ let raw;
794
+ try {
795
+ raw = renderTemplate(stepDef.gate.message, {
796
+ evidenceByStep: gateEvidenceCtxEarly,
797
+ runParams: run.params,
798
+ ...wfCtxSpreadEarly,
799
+ }, { strict: true });
800
+ }
801
+ catch (err) {
802
+ if (err instanceof UnknownFilterError) {
803
+ return makeErrorEnvelope(options, pendingRun, new WorkflowError(`gate.message uses unknown filter '${err.filterName}'`, {
804
+ code: 'FILTER_UNKNOWN',
805
+ category: 'ENGINE',
806
+ agentAction: 'stop',
807
+ retryable: false,
808
+ }), definition, traceWarnings.length > 0 ? traceWarnings : undefined);
809
+ }
810
+ throw err;
811
+ }
812
+ const unresolved = [...raw.matchAll(/\{\{\s*([\w.-]+)\s*\}\}/g)].map((m) => m[1]);
813
+ if (unresolved.length > 0) {
814
+ return makeErrorEnvelope(options, pendingRun, new WorkflowError(`gate.message has unresolvable references: ${unresolved.join(', ')}`, {
815
+ code: 'GATE_MESSAGE_UNRESOLVABLE',
816
+ category: 'ENGINE',
817
+ agentAction: 'stop',
818
+ retryable: false,
819
+ }), definition, traceWarnings.length > 0 ? traceWarnings : undefined);
820
+ }
821
+ resolvedGateMessage = raw;
822
+ }
823
+ const gateConfig = stepDef.gate;
824
+ let gateRun;
825
+ try {
826
+ gateRun = await store.update({
827
+ ...pendingRun,
828
+ // Step stays in in_progress_steps while gate is open — moved to completed on submit.
829
+ evidence: [...pendingRun.evidence, ...allEvidence],
830
+ pending_gate: {
831
+ gate_id,
832
+ step_name,
833
+ preview: output,
834
+ choices,
835
+ opened_at: new Date().toISOString(),
836
+ ...(gateConfig?.owner !== undefined ? { owner: gateConfig.owner } : {}),
837
+ ...(resolvedGateMessage !== undefined ? { resolved_message: resolvedGateMessage } : {}),
838
+ ...(gateConfig?.resolution_messages !== undefined
839
+ ? { resolution_messages: gateConfig.resolution_messages }
840
+ : {}),
841
+ },
842
+ });
843
+ }
844
+ catch (err) {
845
+ if (err instanceof WorkflowError) {
846
+ return makeErrorEnvelope(options, pendingRun, err, definition, traceWarnings.length > 0 ? traceWarnings : undefined);
847
+ }
848
+ return makeErrorEnvelope(options, pendingRun, new WorkflowError('Failed to open gate', {
849
+ code: 'ENGINE_STORE_FAILED',
850
+ category: 'ENGINE',
851
+ agentAction: 'stop',
852
+ retryable: false,
853
+ }), definition, traceWarnings.length > 0 ? traceWarnings : undefined);
854
+ }
855
+ // gate.display fallback chain: gate.message resolved → step.prompt resolved → absent
856
+ const resolvedGateDisplay = resolvedGateMessage !== undefined
857
+ ? resolvedGateMessage
858
+ : stepDef.prompt !== undefined
859
+ ? renderTemplate(stepDef.prompt, {
860
+ evidenceByStep: gateEvidenceCtxEarly,
861
+ runParams: run.params,
862
+ ...wfCtxSpreadEarly,
863
+ })
864
+ : undefined;
865
+ const resolvedGateInstructions = stepDef.instructions !== undefined
866
+ ? renderTemplate(stepDef.instructions, {
867
+ evidenceByStep: gateEvidenceCtxEarly,
868
+ runParams: run.params,
869
+ ...wfCtxSpreadEarly,
870
+ })
871
+ : undefined;
872
+ const gateNextAction = {
873
+ instruction: {
874
+ tool: 'submit_human_response',
875
+ params: { run_id: options.runId, gate_id },
876
+ call_with: {
877
+ run_id: options.runId,
878
+ gate_id,
879
+ choice: `<${choices.join('|')}>`,
880
+ },
881
+ },
882
+ human_readable: `Human review required for step '${options.command}'. Present gate.display to the user, wait for their choice from gate.response_spec.choices, then call submit_human_response.`,
883
+ orientation: `Run is paused at gate '${gate_id}'. Available choices: ${choices.join(', ')}.`,
884
+ };
885
+ return {
886
+ command: options.command,
887
+ run_id: options.runId,
888
+ run_version: gateRun.version,
889
+ status: 'confirm_required',
890
+ data: output,
891
+ evidence: allEvidence,
892
+ warnings: [...traceWarnings],
893
+ errors: [],
894
+ context_hint: `Run is paused at gate '${gate_id}'. Available choices: ${choices.join(', ')}.`,
895
+ next_actions: [gateNextAction],
896
+ gate: {
897
+ gate_id,
898
+ step_name,
899
+ preview: output,
900
+ choices,
901
+ ...(resolvedGateDisplay !== undefined ? { display: resolvedGateDisplay } : {}),
902
+ ...(resolvedGateInstructions !== undefined ? { agent_hint: resolvedGateInstructions } : {}),
903
+ response_spec: { choices },
904
+ },
905
+ };
906
+ }
907
+ // Step 6: Move step from in_progress to completed, compute terminal state.
908
+ const afterComplete = {
909
+ ...pendingRun,
910
+ in_progress_steps: pendingRun.in_progress_steps.filter((s) => s !== options.command),
911
+ completed_steps: [...pendingRun.completed_steps, options.command],
912
+ evidence: [...pendingRun.evidence, ...allEvidence],
913
+ };
914
+ // Propagate skips: completing this step may make some downstream steps permanently ineligible
915
+ // (e.g. all_failed steps whose dep just succeeded, one_failed steps whose last unfailed dep just completed).
916
+ const withSkippedComplete = {
917
+ ...afterComplete,
918
+ skipped_steps: propagateSkips(afterComplete, definition),
919
+ };
920
+ // A run is terminal when all steps are settled OR when no step will ever become
921
+ // eligible again (safety net for when-condition routing not fully covered by propagateSkips).
922
+ const isComplete = isWorkflowComplete(withSkippedComplete, definition) ||
923
+ (withSkippedComplete.in_progress_steps.length === 0 &&
924
+ findEligibleSteps(definition, withSkippedComplete).length === 0);
925
+ const finalRun = {
926
+ ...withSkippedComplete,
927
+ terminal_state: isComplete,
928
+ ...(isComplete ? { terminal_reason: `Workflow completed.` } : {}),
929
+ };
930
+ let savedRun;
931
+ try {
932
+ savedRun = await store.update(finalRun);
933
+ }
934
+ catch (err) {
935
+ if (err instanceof WorkflowError) {
936
+ return makeErrorEnvelope(options, pendingRun, err, definition, traceWarnings.length > 0 ? traceWarnings : undefined);
937
+ }
938
+ const internal = new WorkflowError('Failed to persist run update', {
939
+ code: 'ENGINE_STORE_FAILED',
940
+ category: 'ENGINE',
941
+ agentAction: 'stop',
942
+ retryable: false,
943
+ });
944
+ return makeErrorEnvelope(options, pendingRun, internal, definition, traceWarnings.length > 0 ? traceWarnings : undefined);
945
+ }
946
+ // Delete WAL after successful run update — entries are now in evidence.
947
+ await options.traceBufferStore?.delete(options.runId, options.command);
948
+ // Step 7: Build and return ResponseEnvelope.
949
+ const nextActions = savedRun.terminal_state ? [] : buildNextActions(definition, savedRun);
950
+ const orientation = savedRun.terminal_state
951
+ ? `Run completed (phase: '${savedRun.run_phase}'). Call get_run_state with run_id '${options.runId}' to retrieve the full evidence record.`
952
+ : nextActions.length > 0
953
+ ? `Step '${options.command}' completed. ${nextActions.length} step(s) now available.`
954
+ : `Step '${options.command}' completed. Waiting for other steps to complete.`;
955
+ return {
956
+ command: options.command,
957
+ run_id: options.runId,
958
+ run_version: savedRun.version,
959
+ status: 'ok',
960
+ data: output,
961
+ evidence: allEvidence,
962
+ warnings: traceWarnings,
963
+ errors: [],
964
+ context_hint: orientation,
965
+ next_actions: nextActions,
966
+ };
967
+ }
968
+ /**
969
+ * Submits a human response for a gate-waiting run.
970
+ * Validates the gate_id and choice, then moves the step to completed_steps.
971
+ */
972
+ export async function submitHumanResponse(store, definition, options) {
973
+ // 1. Load run.
974
+ let run;
975
+ try {
976
+ run = await store.get(options.runId);
977
+ }
978
+ catch (err) {
979
+ const e = err instanceof WorkflowError
980
+ ? err
981
+ : new WorkflowError('Failed to load run from store', {
982
+ code: 'ENGINE_STORE_FAILED',
983
+ category: 'ENGINE',
984
+ agentAction: 'stop',
985
+ retryable: false,
986
+ });
987
+ return errorEnvelope('submit_gate', options.runId, 0, e);
988
+ }
989
+ // 2. Verify a gate is open.
990
+ if (run.pending_gate === undefined) {
991
+ return errorEnvelope('submit_gate', options.runId, run.version, new WorkflowError('Run is not waiting at a gate.', {
992
+ code: 'STATE_BLOCKED',
993
+ category: 'STATE',
994
+ agentAction: 'report_to_user',
995
+ retryable: false,
996
+ }), `Run '${options.runId}' has no open gate (phase: '${run.run_phase}').`);
997
+ }
998
+ // 3. Verify gate_id.
999
+ if (run.pending_gate.gate_id !== options.gateId) {
1000
+ return errorEnvelope('submit_gate', options.runId, run.version, new WorkflowError('Gate ID mismatch.', {
1001
+ code: 'STATE_BLOCKED',
1002
+ category: 'STATE',
1003
+ agentAction: 'report_to_user',
1004
+ retryable: false,
1005
+ }), `Gate ID mismatch on run '${options.runId}'.`);
1006
+ }
1007
+ // 4. Validate choice.
1008
+ if (!run.pending_gate.choices.includes(options.choice)) {
1009
+ const expected = run.pending_gate.choices.join(', ');
1010
+ return errorEnvelope(run.pending_gate.step_name, options.runId, run.version, new WorkflowError(`Choice '${options.choice}' is not valid. Expected one of: ${expected}`, {
1011
+ code: 'VALIDATION_INPUT_SCHEMA',
1012
+ category: 'VALIDATION',
1013
+ agentAction: 'report_to_user',
1014
+ retryable: false,
1015
+ }), `Invalid choice '${options.choice}' for gate '${run.pending_gate.step_name}'.`);
1016
+ }
1017
+ // 5. Record gate response evidence and move step to completed_steps.
1018
+ const gateStepName = run.pending_gate.step_name;
1019
+ const respondedAt = new Date();
1020
+ const gateEvidence = captureEvidence({
1021
+ stepId: gateStepName,
1022
+ startedAt: new Date(run.pending_gate.opened_at),
1023
+ completedAt: respondedAt,
1024
+ input: { choice: options.choice },
1025
+ output: { ...run.pending_gate.preview, choice: options.choice },
1026
+ });
1027
+ const gateSnapshot = {
1028
+ ...gateEvidence,
1029
+ kind: 'gate_response',
1030
+ ...(run.pending_gate.resolved_message !== undefined
1031
+ ? { gate_message: run.pending_gate.resolved_message }
1032
+ : {}),
1033
+ };
1034
+ const { pending_gate: _pg, terminal_reason: _tr, ...rest } = run;
1035
+ const afterGate = {
1036
+ ...rest,
1037
+ in_progress_steps: rest.in_progress_steps.filter((s) => s !== gateStepName),
1038
+ completed_steps: [...rest.completed_steps, gateStepName],
1039
+ evidence: [...rest.evidence, gateSnapshot],
1040
+ };
1041
+ // Propagate skips in case resolving the gate completes a dep that makes
1042
+ // some downstream trigger_rules permanently unsatisfiable.
1043
+ const withSkippedGate = {
1044
+ ...afterGate,
1045
+ skipped_steps: propagateSkips(afterGate, definition),
1046
+ };
1047
+ // A run is terminal when all steps are settled OR when no step will ever become
1048
+ // eligible again (safety net for when-condition routing not fully covered by propagateSkips).
1049
+ const isComplete = isWorkflowComplete(withSkippedGate, definition) ||
1050
+ (withSkippedGate.in_progress_steps.length === 0 &&
1051
+ findEligibleSteps(definition, withSkippedGate).length === 0);
1052
+ const finalRun = {
1053
+ ...withSkippedGate,
1054
+ terminal_state: isComplete,
1055
+ ...(isComplete ? { terminal_reason: `Workflow completed.` } : {}),
1056
+ };
1057
+ let savedRun;
1058
+ try {
1059
+ savedRun = await store.update(finalRun);
1060
+ }
1061
+ catch (err) {
1062
+ const e = err instanceof WorkflowError
1063
+ ? err
1064
+ : new WorkflowError('Failed to persist gate response', {
1065
+ code: 'ENGINE_STORE_FAILED',
1066
+ category: 'ENGINE',
1067
+ agentAction: 'stop',
1068
+ retryable: false,
1069
+ });
1070
+ return errorEnvelope(gateStepName, options.runId, run.version, e, `Failed to persist gate response.`);
1071
+ }
1072
+ // 6. Build response.
1073
+ const data = { ...run.pending_gate.preview, choice: options.choice };
1074
+ const nextActions = savedRun.terminal_state ? [] : buildNextActions(definition, savedRun);
1075
+ const orientation = savedRun.terminal_state
1076
+ ? `Run completed (phase: '${savedRun.run_phase}'). Call get_run_state with run_id '${options.runId}' to retrieve the full evidence record.`
1077
+ : `Gate '${gateStepName}' resolved with choice '${options.choice}'. ${nextActions.length} step(s) now available.`;
1078
+ return {
1079
+ command: gateStepName,
1080
+ run_id: options.runId,
1081
+ run_version: savedRun.version,
1082
+ status: 'ok',
1083
+ data,
1084
+ evidence: [],
1085
+ warnings: [],
1086
+ errors: [],
1087
+ context_hint: orientation,
1088
+ next_actions: nextActions,
1089
+ };
1090
+ }
1091
+ const MAX_CHAIN_DEPTH = 50;
1092
+ async function executeChainInternal(store, definition, options, depth, chainedSteps) {
1093
+ if (depth > MAX_CHAIN_DEPTH) {
1094
+ return {
1095
+ command: options.command,
1096
+ run_id: options.runId,
1097
+ run_version: 0,
1098
+ status: 'error',
1099
+ data: {},
1100
+ evidence: [],
1101
+ warnings: [],
1102
+ errors: [
1103
+ 'Auto-execution chain exceeded maximum depth (50). Possible cycle in workflow definition.',
1104
+ ],
1105
+ agent_action: 'stop',
1106
+ context_hint: `Auto-step chain exceeded depth limit (50) for run '${options.runId}'.`,
1107
+ next_actions: [],
1108
+ };
1109
+ }
1110
+ const result = await executeStep(store, definition, options);
1111
+ // Stop chaining on any non-ok result.
1112
+ if (result.status !== 'ok') {
1113
+ return result;
1114
+ }
1115
+ // Load the current run to determine what comes next.
1116
+ let run;
1117
+ try {
1118
+ run = await store.get(options.runId);
1119
+ }
1120
+ catch {
1121
+ return result;
1122
+ }
1123
+ // Record this auto step in the accumulator.
1124
+ if (definition.steps[options.command]?.execution === 'auto') {
1125
+ chainedSteps.push({ step: options.command, run_phase: run.run_phase });
1126
+ }
1127
+ if (run.terminal_state || run.pending_gate !== undefined) {
1128
+ return result;
1129
+ }
1130
+ // Find the next eligible auto step and chain into it.
1131
+ const eligible = findEligibleSteps(definition, run);
1132
+ const nextAutoStep = eligible.find((name) => definition.steps[name]?.execution === 'auto');
1133
+ if (nextAutoStep === undefined) {
1134
+ // Only agent steps or nothing — stop chain, return with latest next_actions.
1135
+ return result;
1136
+ }
1137
+ return executeChainInternal(store, definition, { ...options, command: nextAutoStep, input: {} }, depth + 1, chainedSteps);
1138
+ }
1139
+ /**
1140
+ * Executes a step and automatically chains into subsequent `execution: auto` steps.
1141
+ * Stops at agent steps, gate steps (returning confirm_required), errors, or terminal state.
1142
+ * Returns next_actions containing all eligible agent steps when the auto chain exhausts.
1143
+ */
1144
+ export async function executeChain(store, definition, options) {
1145
+ const effectiveOptions = {
1146
+ ...options,
1147
+ registry: options.registry ?? createDefaultRegistry(),
1148
+ };
1149
+ const chained = [];
1150
+ const result = await executeChainInternal(store, definition, effectiveOptions, 0, chained);
1151
+ const envelope = { ...result, command: options.command };
1152
+ return chained.length > 0 ? { ...envelope, chained_auto_steps: chained } : envelope;
1153
+ }
1154
+ // Re-export TERMINAL_PHASES so existing importers via execution-loop.js still resolve.
1155
+ export { TERMINAL_PHASES as TERMINAL_STATES };
1156
+ //# sourceMappingURL=execution-loop.js.map