@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.
- package/README.md +150 -0
- package/dist/adapters/adapter-utils.d.ts +14 -0
- package/dist/adapters/adapter-utils.d.ts.map +1 -0
- package/dist/adapters/adapter-utils.js +18 -0
- package/dist/adapters/adapter-utils.js.map +1 -0
- package/dist/adapters/airtable-adapter.d.ts +51 -0
- package/dist/adapters/airtable-adapter.d.ts.map +1 -0
- package/dist/adapters/airtable-adapter.js +425 -0
- package/dist/adapters/airtable-adapter.js.map +1 -0
- package/dist/adapters/file-adapter.d.ts +14 -0
- package/dist/adapters/file-adapter.d.ts.map +1 -0
- package/dist/adapters/file-adapter.js +89 -0
- package/dist/adapters/file-adapter.js.map +1 -0
- package/dist/adapters/github-adapter.d.ts +40 -0
- package/dist/adapters/github-adapter.d.ts.map +1 -0
- package/dist/adapters/github-adapter.js +286 -0
- package/dist/adapters/github-adapter.js.map +1 -0
- package/dist/adapters/gorgias-adapter.d.ts +40 -0
- package/dist/adapters/gorgias-adapter.d.ts.map +1 -0
- package/dist/adapters/gorgias-adapter.js +300 -0
- package/dist/adapters/gorgias-adapter.js.map +1 -0
- package/dist/adapters/http-adapter.d.ts +28 -0
- package/dist/adapters/http-adapter.d.ts.map +1 -0
- package/dist/adapters/http-adapter.js +97 -0
- package/dist/adapters/http-adapter.js.map +1 -0
- package/dist/adapters/mock-adapter.d.ts +16 -0
- package/dist/adapters/mock-adapter.d.ts.map +1 -0
- package/dist/adapters/mock-adapter.js +61 -0
- package/dist/adapters/mock-adapter.js.map +1 -0
- package/dist/adapters/notion-adapter.d.ts +54 -0
- package/dist/adapters/notion-adapter.d.ts.map +1 -0
- package/dist/adapters/notion-adapter.js +751 -0
- package/dist/adapters/notion-adapter.js.map +1 -0
- package/dist/adapters/parcelpanel-adapter.d.ts +60 -0
- package/dist/adapters/parcelpanel-adapter.d.ts.map +1 -0
- package/dist/adapters/parcelpanel-adapter.js +251 -0
- package/dist/adapters/parcelpanel-adapter.js.map +1 -0
- package/dist/adapters/rate-limiter.d.ts +26 -0
- package/dist/adapters/rate-limiter.d.ts.map +1 -0
- package/dist/adapters/rate-limiter.js +3 -0
- package/dist/adapters/rate-limiter.js.map +1 -0
- package/dist/adapters/shopify-adapter.d.ts +92 -0
- package/dist/adapters/shopify-adapter.d.ts.map +1 -0
- package/dist/adapters/shopify-adapter.js +415 -0
- package/dist/adapters/shopify-adapter.js.map +1 -0
- package/dist/adapters/slack-adapter.d.ts +18 -0
- package/dist/adapters/slack-adapter.d.ts.map +1 -0
- package/dist/adapters/slack-adapter.js +81 -0
- package/dist/adapters/slack-adapter.js.map +1 -0
- package/dist/adapters/token-bucket.d.ts +27 -0
- package/dist/adapters/token-bucket.d.ts.map +1 -0
- package/dist/adapters/token-bucket.js +109 -0
- package/dist/adapters/token-bucket.js.map +1 -0
- package/dist/config/secrets.d.ts +15 -0
- package/dist/config/secrets.d.ts.map +1 -0
- package/dist/config/secrets.js +33 -0
- package/dist/config/secrets.js.map +1 -0
- package/dist/engine/eligibility.d.ts +50 -0
- package/dist/engine/eligibility.d.ts.map +1 -0
- package/dist/engine/eligibility.js +267 -0
- package/dist/engine/eligibility.js.map +1 -0
- package/dist/engine/error-resolution.d.ts +20 -0
- package/dist/engine/error-resolution.d.ts.map +1 -0
- package/dist/engine/error-resolution.js +32 -0
- package/dist/engine/error-resolution.js.map +1 -0
- package/dist/engine/execution-loop.d.ts +101 -0
- package/dist/engine/execution-loop.d.ts.map +1 -0
- package/dist/engine/execution-loop.js +1156 -0
- package/dist/engine/execution-loop.js.map +1 -0
- package/dist/engine/lifecycle.d.ts +14 -0
- package/dist/engine/lifecycle.d.ts.map +1 -0
- package/dist/engine/lifecycle.js +17 -0
- package/dist/engine/lifecycle.js.map +1 -0
- package/dist/engine/precondition.d.ts +30 -0
- package/dist/engine/precondition.d.ts.map +1 -0
- package/dist/engine/precondition.js +125 -0
- package/dist/engine/precondition.js.map +1 -0
- package/dist/engine/prompt-template.d.ts +25 -0
- package/dist/engine/prompt-template.d.ts.map +1 -0
- package/dist/engine/prompt-template.js +66 -0
- package/dist/engine/prompt-template.js.map +1 -0
- package/dist/engine/render-template.d.ts +52 -0
- package/dist/engine/render-template.d.ts.map +1 -0
- package/dist/engine/render-template.js +548 -0
- package/dist/engine/render-template.js.map +1 -0
- package/dist/engine/state-guard.d.ts +15 -0
- package/dist/engine/state-guard.d.ts.map +1 -0
- package/dist/engine/state-guard.js +40 -0
- package/dist/engine/state-guard.js.map +1 -0
- package/dist/engine/trace-normalizer.d.ts +36 -0
- package/dist/engine/trace-normalizer.d.ts.map +1 -0
- package/dist/engine/trace-normalizer.js +146 -0
- package/dist/engine/trace-normalizer.js.map +1 -0
- package/dist/engine/trace-policy.d.ts +53 -0
- package/dist/engine/trace-policy.d.ts.map +1 -0
- package/dist/engine/trace-policy.js +35 -0
- package/dist/engine/trace-policy.js.map +1 -0
- package/dist/engine/workflow-context-loader.d.ts +9 -0
- package/dist/engine/workflow-context-loader.d.ts.map +1 -0
- package/dist/engine/workflow-context-loader.js +41 -0
- package/dist/engine/workflow-context-loader.js.map +1 -0
- package/dist/evidence/snapshot.d.ts +38 -0
- package/dist/evidence/snapshot.d.ts.map +1 -0
- package/dist/evidence/snapshot.js +53 -0
- package/dist/evidence/snapshot.js.map +1 -0
- package/dist/extensions/default-registry.d.ts +19 -0
- package/dist/extensions/default-registry.d.ts.map +1 -0
- package/dist/extensions/default-registry.js +31 -0
- package/dist/extensions/default-registry.js.map +1 -0
- package/dist/extensions/processor.d.ts +13 -0
- package/dist/extensions/processor.d.ts.map +1 -0
- package/dist/extensions/processor.js +3 -0
- package/dist/extensions/processor.js.map +1 -0
- package/dist/extensions/registry.d.ts +25 -0
- package/dist/extensions/registry.d.ts.map +1 -0
- package/dist/extensions/registry.js +43 -0
- package/dist/extensions/registry.js.map +1 -0
- package/dist/extensions/service-adapter.d.ts +35 -0
- package/dist/extensions/service-adapter.d.ts.map +1 -0
- package/dist/extensions/service-adapter.js +3 -0
- package/dist/extensions/service-adapter.js.map +1 -0
- package/dist/extensions/step-handler.d.ts +28 -0
- package/dist/extensions/step-handler.d.ts.map +1 -0
- package/dist/extensions/step-handler.js +3 -0
- package/dist/extensions/step-handler.js.map +1 -0
- package/dist/handlers/primitives/compare-strings.d.ts +13 -0
- package/dist/handlers/primitives/compare-strings.d.ts.map +1 -0
- package/dist/handlers/primitives/compare-strings.js +28 -0
- package/dist/handlers/primitives/compare-strings.js.map +1 -0
- package/dist/handlers/primitives/count-results.d.ts +21 -0
- package/dist/handlers/primitives/count-results.d.ts.map +1 -0
- package/dist/handlers/primitives/count-results.js +23 -0
- package/dist/handlers/primitives/count-results.js.map +1 -0
- package/dist/handlers/primitives/partition-by-substring.d.ts +18 -0
- package/dist/handlers/primitives/partition-by-substring.d.ts.map +1 -0
- package/dist/handlers/primitives/partition-by-substring.js +28 -0
- package/dist/handlers/primitives/partition-by-substring.js.map +1 -0
- package/dist/handlers/primitives/resolve-resource.d.ts +13 -0
- package/dist/handlers/primitives/resolve-resource.d.ts.map +1 -0
- package/dist/handlers/primitives/resolve-resource.js +21 -0
- package/dist/handlers/primitives/resolve-resource.js.map +1 -0
- package/dist/handlers/primitives/walk-field.d.ts +11 -0
- package/dist/handlers/primitives/walk-field.d.ts.map +1 -0
- package/dist/handlers/primitives/walk-field.js +31 -0
- package/dist/handlers/primitives/walk-field.js.map +1 -0
- package/dist/handlers/validate-field-match.d.ts +11 -0
- package/dist/handlers/validate-field-match.d.ts.map +1 -0
- package/dist/handlers/validate-field-match.js +39 -0
- package/dist/handlers/validate-field-match.js.map +1 -0
- package/dist/handlers/validate-verbatim-quotes.d.ts +11 -0
- package/dist/handlers/validate-verbatim-quotes.d.ts.map +1 -0
- package/dist/handlers/validate-verbatim-quotes.js +37 -0
- package/dist/handlers/validate-verbatim-quotes.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline/processing-pipeline.d.ts +24 -0
- package/dist/pipeline/processing-pipeline.d.ts.map +1 -0
- package/dist/pipeline/processing-pipeline.js +53 -0
- package/dist/pipeline/processing-pipeline.js.map +1 -0
- package/dist/processors/compute-hash.d.ts +4 -0
- package/dist/processors/compute-hash.d.ts.map +1 -0
- package/dist/processors/compute-hash.js +14 -0
- package/dist/processors/compute-hash.js.map +1 -0
- package/dist/processors/normalize-text.d.ts +8 -0
- package/dist/processors/normalize-text.d.ts.map +1 -0
- package/dist/processors/normalize-text.js +47 -0
- package/dist/processors/normalize-text.js.map +1 -0
- package/dist/store/json-file-store.d.ts +24 -0
- package/dist/store/json-file-store.d.ts.map +1 -0
- package/dist/store/json-file-store.js +210 -0
- package/dist/store/json-file-store.js.map +1 -0
- package/dist/store/store-interface.d.ts +33 -0
- package/dist/store/store-interface.d.ts.map +1 -0
- package/dist/store/store-interface.js +2 -0
- package/dist/store/store-interface.js.map +1 -0
- package/dist/store/trace-buffer-store.d.ts +55 -0
- package/dist/store/trace-buffer-store.d.ts.map +1 -0
- package/dist/store/trace-buffer-store.js +113 -0
- package/dist/store/trace-buffer-store.js.map +1 -0
- package/dist/types/mcp-types.d.ts +17 -0
- package/dist/types/mcp-types.d.ts.map +1 -0
- package/dist/types/mcp-types.js +5 -0
- package/dist/types/mcp-types.js.map +1 -0
- package/dist/types/response-envelope.d.ts +96 -0
- package/dist/types/response-envelope.d.ts.map +1 -0
- package/dist/types/response-envelope.js +2 -0
- package/dist/types/response-envelope.js.map +1 -0
- package/dist/types/run-record.d.ts +169 -0
- package/dist/types/run-record.d.ts.map +1 -0
- package/dist/types/run-record.js +2 -0
- package/dist/types/run-record.js.map +1 -0
- package/dist/types/workflow-definition.d.ts +292 -0
- package/dist/types/workflow-definition.d.ts.map +1 -0
- package/dist/types/workflow-definition.js +2 -0
- package/dist/types/workflow-definition.js.map +1 -0
- package/dist/types/workflow-error.d.ts +26 -0
- package/dist/types/workflow-error.d.ts.map +1 -0
- package/dist/types/workflow-error.js +28 -0
- package/dist/types/workflow-error.js.map +1 -0
- package/dist/utils/schema-skeleton.d.ts +11 -0
- package/dist/utils/schema-skeleton.d.ts.map +1 -0
- package/dist/utils/schema-skeleton.js +40 -0
- package/dist/utils/schema-skeleton.js.map +1 -0
- package/dist/validation/input-schema.d.ts +26 -0
- package/dist/validation/input-schema.d.ts.map +1 -0
- package/dist/validation/input-schema.js +67 -0
- package/dist/validation/input-schema.js.map +1 -0
- package/dist/workflow/registrar.d.ts +20 -0
- package/dist/workflow/registrar.d.ts.map +1 -0
- package/dist/workflow/registrar.js +61 -0
- package/dist/workflow/registrar.js.map +1 -0
- package/dist/workflow/template-resolver.d.ts +25 -0
- package/dist/workflow/template-resolver.d.ts.map +1 -0
- package/dist/workflow/template-resolver.js +112 -0
- package/dist/workflow/template-resolver.js.map +1 -0
- package/dist/workflow/yaml-loader.d.ts +15 -0
- package/dist/workflow/yaml-loader.d.ts.map +1 -0
- package/dist/workflow/yaml-loader.js +408 -0
- package/dist/workflow/yaml-loader.js.map +1 -0
- 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
|