groundswell 0.0.3 → 1.0.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/LICENSE +21 -0
- package/README.md +26 -9
- package/dist/cache/cache-key.d.ts +20 -0
- package/dist/cache/cache-key.d.ts.map +1 -1
- package/dist/cache/cache-key.js +9 -0
- package/dist/cache/cache-key.js.map +1 -1
- package/dist/core/agent.d.ts +120 -29
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +584 -177
- package/dist/core/agent.js.map +1 -1
- package/dist/core/mcp-handler.d.ts +63 -5
- package/dist/core/mcp-handler.d.ts.map +1 -1
- package/dist/core/mcp-handler.js +184 -4
- package/dist/core/mcp-handler.js.map +1 -1
- package/dist/core/workflow-context.d.ts +6 -2
- package/dist/core/workflow-context.d.ts.map +1 -1
- package/dist/core/workflow-context.js +99 -4
- package/dist/core/workflow-context.js.map +1 -1
- package/dist/core/workflow.d.ts +315 -13
- package/dist/core/workflow.d.ts.map +1 -1
- package/dist/core/workflow.js +552 -30
- package/dist/core/workflow.js.map +1 -1
- package/dist/debugger/event-replayer.d.ts +422 -0
- package/dist/debugger/event-replayer.d.ts.map +1 -0
- package/dist/debugger/event-replayer.js +639 -0
- package/dist/debugger/event-replayer.js.map +1 -0
- package/dist/debugger/tree-debugger.d.ts +170 -1
- package/dist/debugger/tree-debugger.d.ts.map +1 -1
- package/dist/debugger/tree-debugger.js +423 -1
- package/dist/debugger/tree-debugger.js.map +1 -1
- package/dist/decorators/step.d.ts.map +1 -1
- package/dist/decorators/step.js +129 -47
- package/dist/decorators/step.js.map +1 -1
- package/dist/harnesses/claude-code-harness.d.ts +391 -0
- package/dist/harnesses/claude-code-harness.d.ts.map +1 -0
- package/dist/harnesses/claude-code-harness.js +1076 -0
- package/dist/harnesses/claude-code-harness.js.map +1 -0
- package/dist/harnesses/harness-registry.d.ts +440 -0
- package/dist/harnesses/harness-registry.d.ts.map +1 -0
- package/dist/harnesses/harness-registry.js +543 -0
- package/dist/harnesses/harness-registry.js.map +1 -0
- package/dist/harnesses/index.d.ts +12 -0
- package/dist/harnesses/index.d.ts.map +1 -0
- package/dist/harnesses/index.js +11 -0
- package/dist/harnesses/index.js.map +1 -0
- package/dist/harnesses/pi-harness.d.ts +219 -0
- package/dist/harnesses/pi-harness.d.ts.map +1 -0
- package/dist/harnesses/pi-harness.js +676 -0
- package/dist/harnesses/pi-harness.js.map +1 -0
- package/dist/harnesses/pi-schema-converter.d.ts +24 -0
- package/dist/harnesses/pi-schema-converter.d.ts.map +1 -0
- package/dist/harnesses/pi-schema-converter.js +81 -0
- package/dist/harnesses/pi-schema-converter.js.map +1 -0
- package/dist/harnesses/register-defaults.d.ts +24 -0
- package/dist/harnesses/register-defaults.d.ts.map +1 -0
- package/dist/harnesses/register-defaults.js +40 -0
- package/dist/harnesses/register-defaults.js.map +1 -0
- package/dist/harnesses/session-store.d.ts +201 -0
- package/dist/harnesses/session-store.d.ts.map +1 -0
- package/dist/harnesses/session-store.js +254 -0
- package/dist/harnesses/session-store.js.map +1 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/reflection/reflection.d.ts.map +1 -1
- package/dist/reflection/reflection.js +19 -4
- package/dist/reflection/reflection.js.map +1 -1
- package/dist/types/agent.d.ts +1253 -2
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/agent.js +418 -1
- package/dist/types/agent.js.map +1 -1
- package/dist/types/decorators.d.ts +10 -1
- package/dist/types/decorators.d.ts.map +1 -1
- package/dist/types/events.d.ts +26 -0
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/harnesses.d.ts +474 -0
- package/dist/types/harnesses.d.ts.map +1 -0
- package/dist/types/harnesses.js +2 -0
- package/dist/types/harnesses.js.map +1 -0
- package/dist/types/index.d.ts +9 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/providers.d.ts +691 -0
- package/dist/types/providers.d.ts.map +1 -0
- package/dist/types/providers.js +14 -0
- package/dist/types/providers.js.map +1 -0
- package/dist/types/restart.d.ts +132 -0
- package/dist/types/restart.d.ts.map +1 -0
- package/dist/types/restart.js +2 -0
- package/dist/types/restart.js.map +1 -0
- package/dist/types/streaming.d.ts +194 -0
- package/dist/types/streaming.d.ts.map +1 -0
- package/dist/types/streaming.js +67 -0
- package/dist/types/streaming.js.map +1 -0
- package/dist/types/workflow-context.d.ts +137 -1
- package/dist/types/workflow-context.d.ts.map +1 -1
- package/dist/utils/agent-validation.d.ts +88 -0
- package/dist/utils/agent-validation.d.ts.map +1 -0
- package/dist/utils/agent-validation.js +87 -0
- package/dist/utils/agent-validation.js.map +1 -0
- package/dist/utils/delay.d.ts +7 -0
- package/dist/utils/delay.d.ts.map +1 -0
- package/dist/utils/delay.js +9 -0
- package/dist/utils/delay.js.map +1 -0
- package/dist/utils/harness-config.d.ts +180 -0
- package/dist/utils/harness-config.d.ts.map +1 -0
- package/dist/utils/harness-config.js +311 -0
- package/dist/utils/harness-config.js.map +1 -0
- package/dist/utils/index.d.ts +9 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +8 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/model-spec.d.ts +110 -0
- package/dist/utils/model-spec.d.ts.map +1 -0
- package/dist/utils/model-spec.js +149 -0
- package/dist/utils/model-spec.js.map +1 -0
- package/dist/utils/provider-config.d.ts +10 -0
- package/dist/utils/provider-config.d.ts.map +1 -0
- package/dist/utils/provider-config.js +10 -0
- package/dist/utils/provider-config.js.map +1 -0
- package/dist/utils/restart-analysis.d.ts +202 -0
- package/dist/utils/restart-analysis.d.ts.map +1 -0
- package/dist/utils/restart-analysis.js +426 -0
- package/dist/utils/restart-analysis.js.map +1 -0
- package/dist/utils/session-serialization.d.ts +118 -0
- package/dist/utils/session-serialization.d.ts.map +1 -0
- package/dist/utils/session-serialization.js +217 -0
- package/dist/utils/session-serialization.js.map +1 -0
- package/package.json +31 -5
- package/CHANGELOG.md +0 -188
- package/dist/__tests__/adversarial/attachChild-performance.test.d.ts +0 -16
- package/dist/__tests__/adversarial/attachChild-performance.test.d.ts.map +0 -1
- package/dist/__tests__/adversarial/attachChild-performance.test.js +0 -187
- package/dist/__tests__/adversarial/attachChild-performance.test.js.map +0 -1
- package/dist/__tests__/adversarial/circular-reference.test.d.ts +0 -13
- package/dist/__tests__/adversarial/circular-reference.test.d.ts.map +0 -1
- package/dist/__tests__/adversarial/circular-reference.test.js +0 -92
- package/dist/__tests__/adversarial/circular-reference.test.js.map +0 -1
- package/dist/__tests__/adversarial/complex-circular-reference.test.d.ts +0 -16
- package/dist/__tests__/adversarial/complex-circular-reference.test.d.ts.map +0 -1
- package/dist/__tests__/adversarial/complex-circular-reference.test.js +0 -127
- package/dist/__tests__/adversarial/complex-circular-reference.test.js.map +0 -1
- package/dist/__tests__/adversarial/concurrent-task-failures.test.d.ts +0 -21
- package/dist/__tests__/adversarial/concurrent-task-failures.test.d.ts.map +0 -1
- package/dist/__tests__/adversarial/concurrent-task-failures.test.js +0 -667
- package/dist/__tests__/adversarial/concurrent-task-failures.test.js.map +0 -1
- package/dist/__tests__/adversarial/deep-analysis.test.d.ts +0 -6
- package/dist/__tests__/adversarial/deep-analysis.test.d.ts.map +0 -1
- package/dist/__tests__/adversarial/deep-analysis.test.js +0 -877
- package/dist/__tests__/adversarial/deep-analysis.test.js.map +0 -1
- package/dist/__tests__/adversarial/deep-hierarchy-stress.test.d.ts +0 -13
- package/dist/__tests__/adversarial/deep-hierarchy-stress.test.d.ts.map +0 -1
- package/dist/__tests__/adversarial/deep-hierarchy-stress.test.js +0 -186
- package/dist/__tests__/adversarial/deep-hierarchy-stress.test.js.map +0 -1
- package/dist/__tests__/adversarial/e2e-prd-validation.test.d.ts +0 -6
- package/dist/__tests__/adversarial/e2e-prd-validation.test.d.ts.map +0 -1
- package/dist/__tests__/adversarial/e2e-prd-validation.test.js +0 -626
- package/dist/__tests__/adversarial/e2e-prd-validation.test.js.map +0 -1
- package/dist/__tests__/adversarial/edge-case.test.d.ts +0 -6
- package/dist/__tests__/adversarial/edge-case.test.d.ts.map +0 -1
- package/dist/__tests__/adversarial/edge-case.test.js +0 -857
- package/dist/__tests__/adversarial/edge-case.test.js.map +0 -1
- package/dist/__tests__/adversarial/error-merge-strategy.test.d.ts +0 -20
- package/dist/__tests__/adversarial/error-merge-strategy.test.d.ts.map +0 -1
- package/dist/__tests__/adversarial/error-merge-strategy.test.js +0 -907
- package/dist/__tests__/adversarial/error-merge-strategy.test.js.map +0 -1
- package/dist/__tests__/adversarial/incremental-performance.test.d.ts +0 -2
- package/dist/__tests__/adversarial/incremental-performance.test.d.ts.map +0 -1
- package/dist/__tests__/adversarial/incremental-performance.test.js +0 -113
- package/dist/__tests__/adversarial/incremental-performance.test.js.map +0 -1
- package/dist/__tests__/adversarial/node-map-update-benchmarks.test.d.ts +0 -22
- package/dist/__tests__/adversarial/node-map-update-benchmarks.test.d.ts.map +0 -1
- package/dist/__tests__/adversarial/node-map-update-benchmarks.test.js +0 -383
- package/dist/__tests__/adversarial/node-map-update-benchmarks.test.js.map +0 -1
- package/dist/__tests__/adversarial/observer-propagation.test.d.ts +0 -21
- package/dist/__tests__/adversarial/observer-propagation.test.d.ts.map +0 -1
- package/dist/__tests__/adversarial/observer-propagation.test.js +0 -404
- package/dist/__tests__/adversarial/observer-propagation.test.js.map +0 -1
- package/dist/__tests__/adversarial/parent-validation.test.d.ts +0 -13
- package/dist/__tests__/adversarial/parent-validation.test.d.ts.map +0 -1
- package/dist/__tests__/adversarial/parent-validation.test.js +0 -128
- package/dist/__tests__/adversarial/parent-validation.test.js.map +0 -1
- package/dist/__tests__/adversarial/prd-12-2-compliance.test.d.ts +0 -20
- package/dist/__tests__/adversarial/prd-12-2-compliance.test.d.ts.map +0 -1
- package/dist/__tests__/adversarial/prd-12-2-compliance.test.js +0 -482
- package/dist/__tests__/adversarial/prd-12-2-compliance.test.js.map +0 -1
- package/dist/__tests__/adversarial/prd-compliance.test.d.ts +0 -6
- package/dist/__tests__/adversarial/prd-compliance.test.d.ts.map +0 -1
- package/dist/__tests__/adversarial/prd-compliance.test.js +0 -886
- package/dist/__tests__/adversarial/prd-compliance.test.js.map +0 -1
- package/dist/__tests__/compatibility/backward-compatibility.test.d.ts +0 -22
- package/dist/__tests__/compatibility/backward-compatibility.test.d.ts.map +0 -1
- package/dist/__tests__/compatibility/backward-compatibility.test.js +0 -1843
- package/dist/__tests__/compatibility/backward-compatibility.test.js.map +0 -1
- package/dist/__tests__/helpers/index.d.ts +0 -10
- package/dist/__tests__/helpers/index.d.ts.map +0 -1
- package/dist/__tests__/helpers/index.js +0 -10
- package/dist/__tests__/helpers/index.js.map +0 -1
- package/dist/__tests__/helpers/tree-verification.d.ts +0 -90
- package/dist/__tests__/helpers/tree-verification.d.ts.map +0 -1
- package/dist/__tests__/helpers/tree-verification.js +0 -202
- package/dist/__tests__/helpers/tree-verification.js.map +0 -1
- package/dist/__tests__/integration/agent-workflow.test.d.ts +0 -2
- package/dist/__tests__/integration/agent-workflow.test.d.ts.map +0 -1
- package/dist/__tests__/integration/agent-workflow.test.js +0 -256
- package/dist/__tests__/integration/agent-workflow.test.js.map +0 -1
- package/dist/__tests__/integration/bidirectional-consistency.test.d.ts +0 -14
- package/dist/__tests__/integration/bidirectional-consistency.test.d.ts.map +0 -1
- package/dist/__tests__/integration/bidirectional-consistency.test.js +0 -668
- package/dist/__tests__/integration/bidirectional-consistency.test.js.map +0 -1
- package/dist/__tests__/integration/observer-logging.test.d.ts +0 -2
- package/dist/__tests__/integration/observer-logging.test.d.ts.map +0 -1
- package/dist/__tests__/integration/observer-logging.test.js +0 -517
- package/dist/__tests__/integration/observer-logging.test.js.map +0 -1
- package/dist/__tests__/integration/tree-mirroring.test.d.ts +0 -2
- package/dist/__tests__/integration/tree-mirroring.test.d.ts.map +0 -1
- package/dist/__tests__/integration/tree-mirroring.test.js +0 -117
- package/dist/__tests__/integration/tree-mirroring.test.js.map +0 -1
- package/dist/__tests__/integration/workflow-reparenting.test.d.ts +0 -12
- package/dist/__tests__/integration/workflow-reparenting.test.d.ts.map +0 -1
- package/dist/__tests__/integration/workflow-reparenting.test.js +0 -239
- package/dist/__tests__/integration/workflow-reparenting.test.js.map +0 -1
- package/dist/__tests__/unit/agent.test.d.ts +0 -2
- package/dist/__tests__/unit/agent.test.d.ts.map +0 -1
- package/dist/__tests__/unit/agent.test.js +0 -143
- package/dist/__tests__/unit/agent.test.js.map +0 -1
- package/dist/__tests__/unit/cache-key.test.d.ts +0 -5
- package/dist/__tests__/unit/cache-key.test.d.ts.map +0 -1
- package/dist/__tests__/unit/cache-key.test.js +0 -145
- package/dist/__tests__/unit/cache-key.test.js.map +0 -1
- package/dist/__tests__/unit/cache.test.d.ts +0 -5
- package/dist/__tests__/unit/cache.test.d.ts.map +0 -1
- package/dist/__tests__/unit/cache.test.js +0 -132
- package/dist/__tests__/unit/cache.test.js.map +0 -1
- package/dist/__tests__/unit/context.test.d.ts +0 -2
- package/dist/__tests__/unit/context.test.d.ts.map +0 -1
- package/dist/__tests__/unit/context.test.js +0 -220
- package/dist/__tests__/unit/context.test.js.map +0 -1
- package/dist/__tests__/unit/decorators.test.d.ts +0 -2
- package/dist/__tests__/unit/decorators.test.d.ts.map +0 -1
- package/dist/__tests__/unit/decorators.test.js +0 -162
- package/dist/__tests__/unit/decorators.test.js.map +0 -1
- package/dist/__tests__/unit/introspection-tools.test.d.ts +0 -5
- package/dist/__tests__/unit/introspection-tools.test.d.ts.map +0 -1
- package/dist/__tests__/unit/introspection-tools.test.js +0 -191
- package/dist/__tests__/unit/introspection-tools.test.js.map +0 -1
- package/dist/__tests__/unit/logger.test.d.ts +0 -2
- package/dist/__tests__/unit/logger.test.d.ts.map +0 -1
- package/dist/__tests__/unit/logger.test.js +0 -241
- package/dist/__tests__/unit/logger.test.js.map +0 -1
- package/dist/__tests__/unit/observable.test.d.ts +0 -2
- package/dist/__tests__/unit/observable.test.d.ts.map +0 -1
- package/dist/__tests__/unit/observable.test.js +0 -251
- package/dist/__tests__/unit/observable.test.js.map +0 -1
- package/dist/__tests__/unit/prompt.test.d.ts +0 -2
- package/dist/__tests__/unit/prompt.test.d.ts.map +0 -1
- package/dist/__tests__/unit/prompt.test.js +0 -113
- package/dist/__tests__/unit/prompt.test.js.map +0 -1
- package/dist/__tests__/unit/reflection.test.d.ts +0 -5
- package/dist/__tests__/unit/reflection.test.d.ts.map +0 -1
- package/dist/__tests__/unit/reflection.test.js +0 -160
- package/dist/__tests__/unit/reflection.test.js.map +0 -1
- package/dist/__tests__/unit/tree-debugger-incremental.test.d.ts +0 -2
- package/dist/__tests__/unit/tree-debugger-incremental.test.d.ts.map +0 -1
- package/dist/__tests__/unit/tree-debugger-incremental.test.js +0 -136
- package/dist/__tests__/unit/tree-debugger-incremental.test.js.map +0 -1
- package/dist/__tests__/unit/tree-debugger.test.d.ts +0 -2
- package/dist/__tests__/unit/tree-debugger.test.d.ts.map +0 -1
- package/dist/__tests__/unit/tree-debugger.test.js +0 -69
- package/dist/__tests__/unit/tree-debugger.test.js.map +0 -1
- package/dist/__tests__/unit/utils/workflow-error-utils.test.d.ts +0 -2
- package/dist/__tests__/unit/utils/workflow-error-utils.test.d.ts.map +0 -1
- package/dist/__tests__/unit/utils/workflow-error-utils.test.js +0 -154
- package/dist/__tests__/unit/utils/workflow-error-utils.test.js.map +0 -1
- package/dist/__tests__/unit/workflow-detachChild.test.d.ts +0 -2
- package/dist/__tests__/unit/workflow-detachChild.test.d.ts.map +0 -1
- package/dist/__tests__/unit/workflow-detachChild.test.js +0 -76
- package/dist/__tests__/unit/workflow-detachChild.test.js.map +0 -1
- package/dist/__tests__/unit/workflow-emitEvent-childDetached.test.d.ts +0 -2
- package/dist/__tests__/unit/workflow-emitEvent-childDetached.test.d.ts.map +0 -1
- package/dist/__tests__/unit/workflow-emitEvent-childDetached.test.js +0 -122
- package/dist/__tests__/unit/workflow-emitEvent-childDetached.test.js.map +0 -1
- package/dist/__tests__/unit/workflow-isDescendantOf.test.d.ts +0 -2
- package/dist/__tests__/unit/workflow-isDescendantOf.test.d.ts.map +0 -1
- package/dist/__tests__/unit/workflow-isDescendantOf.test.js +0 -140
- package/dist/__tests__/unit/workflow-isDescendantOf.test.js.map +0 -1
- package/dist/__tests__/unit/workflow.test.d.ts +0 -2
- package/dist/__tests__/unit/workflow.test.d.ts.map +0 -1
- package/dist/__tests__/unit/workflow.test.js +0 -330
- package/dist/__tests__/unit/workflow.test.js.map +0 -1
package/dist/core/workflow.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { generateId } from '../utils/id.js';
|
|
3
|
+
import { validateAgentResponse } from '../utils/agent-validation.js';
|
|
4
|
+
import { analyzeErrorForRestart } from '../utils/restart-analysis.js';
|
|
5
|
+
import { mergeWorkflowErrors } from '../utils/workflow-error-utils.js';
|
|
2
6
|
import { WorkflowLogger } from './logger.js';
|
|
3
7
|
import { getObservedState } from '../decorators/observed-state.js';
|
|
4
8
|
import { createWorkflowContext } from './workflow-context.js';
|
|
@@ -46,16 +50,25 @@ export class Workflow {
|
|
|
46
50
|
executor;
|
|
47
51
|
/** Workflow configuration */
|
|
48
52
|
config;
|
|
53
|
+
/** Error collection state for workflow-level error merge */
|
|
54
|
+
collectedErrors = [];
|
|
55
|
+
/** Event history entries with insertion timestamps for replay functionality (ES2022 private field) */
|
|
56
|
+
#eventHistory = [];
|
|
57
|
+
/** Total operations count for error merge context */
|
|
58
|
+
totalOperations = 0;
|
|
59
|
+
/** Operation counter for error merge context */
|
|
60
|
+
operationCounter = 0;
|
|
49
61
|
/**
|
|
50
62
|
* Create a new workflow instance
|
|
51
63
|
*
|
|
52
|
-
* @overload Class-based pattern
|
|
53
|
-
* @
|
|
54
|
-
* @param
|
|
64
|
+
* @overload Class-based pattern: constructor(name?: string, parent?: Workflow)
|
|
65
|
+
* @overload Functional pattern: constructor(config: WorkflowConfig, executor?: WorkflowExecutor)
|
|
66
|
+
* @param name For class-based pattern, human-readable name. Allowed characters: alphanumeric (a-z, A-Z, 0-9), spaces, hyphens (-), underscores (_). (default: class name).
|
|
67
|
+
* For functional pattern, config object with workflow settings.
|
|
68
|
+
* @param parentOrExecutor For class-based pattern, optional parent workflow.
|
|
69
|
+
* For functional pattern, executor function.
|
|
55
70
|
*
|
|
56
|
-
* @
|
|
57
|
-
* @param config Workflow configuration
|
|
58
|
-
* @param executor Executor function
|
|
71
|
+
* @remarks Security validation rejects names containing control characters, HTML tags, JavaScript patterns, path traversal sequences (..), and file system special characters (/ \ : * ? " < > |). This prevents XSS attacks, injection attacks, and path traversal vulnerabilities.
|
|
59
72
|
*/
|
|
60
73
|
constructor(name, parentOrExecutor) {
|
|
61
74
|
this.id = generateId();
|
|
@@ -80,6 +93,32 @@ export class Workflow {
|
|
|
80
93
|
if (this.config.name.length > 100) {
|
|
81
94
|
throw new Error('Workflow name cannot exceed 100 characters');
|
|
82
95
|
}
|
|
96
|
+
// Shared message for all security validations below.
|
|
97
|
+
const invalidNameMessage = 'Invalid workflow name. Names may contain letters, numbers, spaces, hyphens, underscores, and emoji.';
|
|
98
|
+
// Security validation: control characters (ASCII 0x00-0x1F, 0x7F)
|
|
99
|
+
if (/[\x00-\x1F\x7F]/.test(trimmedName)) {
|
|
100
|
+
throw new Error(invalidNameMessage);
|
|
101
|
+
}
|
|
102
|
+
// Security validation: HTML/JavaScript injection patterns
|
|
103
|
+
if (/<[^>]*>/.test(trimmedName) || /javascript:/i.test(trimmedName)) {
|
|
104
|
+
throw new Error(invalidNameMessage);
|
|
105
|
+
}
|
|
106
|
+
// Security validation: path traversal patterns
|
|
107
|
+
if (/\.\./.test(trimmedName)) {
|
|
108
|
+
throw new Error(invalidNameMessage);
|
|
109
|
+
}
|
|
110
|
+
// Security validation: file system special characters
|
|
111
|
+
if (/[\/\\:*?"<>|]/.test(trimmedName)) {
|
|
112
|
+
throw new Error(invalidNameMessage);
|
|
113
|
+
}
|
|
114
|
+
// Security validation: allowed characters (allowlist - defense in depth)
|
|
115
|
+
// Unicode-aware: permits letters (\p{L}), numbers (\p{N}), and emoji
|
|
116
|
+
// (including pictographic bases, variation selectors U+FE0F, and ZWJ
|
|
117
|
+
// sequences U+200D) from any script, while still blocking exotic or
|
|
118
|
+
// symbolic characters not covered by the targeted checks above.
|
|
119
|
+
if (!/^[\p{L}\p{N}\p{Emoji_Presentation}\p{Extended_Pictographic}\u200D\uFE0F _-]+$/u.test(trimmedName)) {
|
|
120
|
+
throw new Error(invalidNameMessage);
|
|
121
|
+
}
|
|
83
122
|
}
|
|
84
123
|
// Create the node representation
|
|
85
124
|
this.node = {
|
|
@@ -118,6 +157,64 @@ export class Workflow {
|
|
|
118
157
|
}
|
|
119
158
|
return root.observers;
|
|
120
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Check if event history is enabled for this workflow
|
|
162
|
+
*
|
|
163
|
+
* @returns true if event history is enabled, false otherwise
|
|
164
|
+
*/
|
|
165
|
+
isEventHistoryEnabled() {
|
|
166
|
+
return this.config.eventHistory?.enabled === true;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get event history configuration with defaults applied
|
|
170
|
+
*
|
|
171
|
+
* @returns Configuration object with all required fields populated
|
|
172
|
+
*/
|
|
173
|
+
getEventHistoryConfig() {
|
|
174
|
+
return {
|
|
175
|
+
enabled: this.config.eventHistory?.enabled ?? false,
|
|
176
|
+
maxEvents: this.config.eventHistory?.maxEvents ?? 1000,
|
|
177
|
+
maxAgeMs: this.config.eventHistory?.maxAgeMs ?? 3600000,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Trim event history based on configuration
|
|
182
|
+
*
|
|
183
|
+
* Uses lazy trimming for performance:
|
|
184
|
+
* - Only trims when at least 1.5x over the maxEvents limit
|
|
185
|
+
* - Applies both count and age constraints
|
|
186
|
+
* - Uses slice() for efficiency (not shift())
|
|
187
|
+
*
|
|
188
|
+
* @remarks
|
|
189
|
+
* Lazy trimming reduces the number of trim operations by only trimming
|
|
190
|
+
* when the history is significantly over the limit (1.5x). This provides
|
|
191
|
+
* better performance for high-frequency event emission.
|
|
192
|
+
*/
|
|
193
|
+
trimEventHistory() {
|
|
194
|
+
const config = this.getEventHistoryConfig();
|
|
195
|
+
// Lazy trimming: only trim when significantly over limit
|
|
196
|
+
const trimThreshold = Math.floor(config.maxEvents * 1.5);
|
|
197
|
+
if (this.#eventHistory.length < trimThreshold) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
const ageCutoff = now - config.maxAgeMs;
|
|
202
|
+
// Age-based trimming: find first entry within age limit
|
|
203
|
+
let keepFromIndex = 0;
|
|
204
|
+
for (let i = 0; i < this.#eventHistory.length; i++) {
|
|
205
|
+
if (this.#eventHistory[i].insertedAt > ageCutoff) {
|
|
206
|
+
keepFromIndex = i;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Count-based trimming: remove excess events
|
|
211
|
+
const countBasedIndex = Math.max(0, this.#eventHistory.length - config.maxEvents);
|
|
212
|
+
// Use the more aggressive constraint
|
|
213
|
+
const finalIndex = Math.max(keepFromIndex, countBasedIndex);
|
|
214
|
+
if (finalIndex > 0) {
|
|
215
|
+
this.#eventHistory = this.#eventHistory.slice(finalIndex);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
121
218
|
/**
|
|
122
219
|
* Check if this workflow is a descendant of the given ancestor workflow.
|
|
123
220
|
*
|
|
@@ -126,12 +223,11 @@ export class Workflow {
|
|
|
126
223
|
* a convenient way to check workflow hierarchy relationships without manually
|
|
127
224
|
* traversing the parent chain.
|
|
128
225
|
*
|
|
129
|
-
* @
|
|
130
|
-
* application exposes workflows via an API, ensure you implement proper
|
|
131
|
-
* access control to prevent unauthorized topology discovery. Note that
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* accessible.
|
|
226
|
+
* @remarks SECURITY WARNING: This method reveals workflow hierarchy information.
|
|
227
|
+
* If your application exposes workflows via an API, ensure you implement proper
|
|
228
|
+
* access control to prevent unauthorized topology discovery. Note that the parent
|
|
229
|
+
* and children properties are already public, so this method does not expose any
|
|
230
|
+
* new information beyond what is currently accessible.
|
|
135
231
|
*
|
|
136
232
|
* **Time Complexity**: O(d) where d is the depth of the hierarchy
|
|
137
233
|
* **Space Complexity**: O(d) for the visited Set in worst case (cycle detection)
|
|
@@ -203,6 +299,8 @@ export class Workflow {
|
|
|
203
299
|
/**
|
|
204
300
|
* Add an observer to this workflow (must be root)
|
|
205
301
|
* @throws Error if called on non-root workflow
|
|
302
|
+
* @side effects Adds observer to internal observers array for root workflows.
|
|
303
|
+
* Observers will receive notifications for workflow events.
|
|
206
304
|
*/
|
|
207
305
|
addObserver(observer) {
|
|
208
306
|
if (this.parent) {
|
|
@@ -233,6 +331,7 @@ export class Workflow {
|
|
|
233
331
|
* - Sets child.parent = this (workflow tree)
|
|
234
332
|
* - Sets child.node.parent = this.node (node tree)
|
|
235
333
|
* - Emits childAttached event to notify observers
|
|
334
|
+
* - Emits treeUpdated event to trigger tree debugger rebuild
|
|
236
335
|
*
|
|
237
336
|
* **Invariants Maintained:**
|
|
238
337
|
* - Single-parent rule: A workflow can only have one parent
|
|
@@ -247,6 +346,8 @@ export class Workflow {
|
|
|
247
346
|
* @throws {Error} If the child is already attached to this workflow
|
|
248
347
|
* @throws {Error} If the child already has a different parent (use detachChild() first for reparenting)
|
|
249
348
|
* @throws {Error} If the child is an ancestor of this parent (would create circular reference)
|
|
349
|
+
* @side effects Modifies workflow tree structure, emits childAttached event,
|
|
350
|
+
* and triggers treeUpdated event for debugger.
|
|
250
351
|
*
|
|
251
352
|
* @example
|
|
252
353
|
* ```ts
|
|
@@ -298,7 +399,7 @@ export class Workflow {
|
|
|
298
399
|
}
|
|
299
400
|
this.children.push(child);
|
|
300
401
|
this.node.children.push(child.node);
|
|
301
|
-
// Emit child attached event
|
|
402
|
+
// Emit child attached event (triggers onTreeChanged via emitEvent)
|
|
302
403
|
this.emitEvent({
|
|
303
404
|
type: 'childAttached',
|
|
304
405
|
parentId: this.id,
|
|
@@ -312,10 +413,14 @@ export class Workflow {
|
|
|
312
413
|
* the node tree (this.node.children), clears the child's parent reference,
|
|
313
414
|
* and emits a childDetached event to notify observers.
|
|
314
415
|
*
|
|
416
|
+
* Also emits treeUpdated event to trigger tree debugger rebuild.
|
|
417
|
+
*
|
|
315
418
|
* This enables reparenting workflows: oldParent.detachChild(child); newParent.attachChild(child);
|
|
316
419
|
*
|
|
317
420
|
* @param child - The child workflow to detach
|
|
318
421
|
* @throws {Error} If the child is not attached to this parent workflow
|
|
422
|
+
* @side effects Modifies workflow tree structure, emits childDetached event,
|
|
423
|
+
* and triggers treeUpdated event for debugger.
|
|
319
424
|
*
|
|
320
425
|
* @example
|
|
321
426
|
* ```ts
|
|
@@ -343,7 +448,7 @@ export class Workflow {
|
|
|
343
448
|
// Clear child's parent reference (both workflow tree and node tree)
|
|
344
449
|
child.parent = null;
|
|
345
450
|
child.node.parent = null; // Maintain 1:1 mirror between workflow tree and node tree
|
|
346
|
-
// Emit childDetached event
|
|
451
|
+
// Emit childDetached event (triggers onTreeChanged via emitEvent)
|
|
347
452
|
this.emitEvent({
|
|
348
453
|
type: 'childDetached',
|
|
349
454
|
parentId: this.id,
|
|
@@ -352,8 +457,15 @@ export class Workflow {
|
|
|
352
457
|
}
|
|
353
458
|
/**
|
|
354
459
|
* Emit an event to all root observers
|
|
460
|
+
* @side effects Pushes event to node.events array and notifies all registered observers.
|
|
461
|
+
* May trigger treeUpdated notifications for specific event types.
|
|
355
462
|
*/
|
|
356
463
|
emitEvent(event) {
|
|
464
|
+
// Store event in history FIRST (for replay functionality) - only if enabled
|
|
465
|
+
if (this.isEventHistoryEnabled()) {
|
|
466
|
+
this.#eventHistory.push({ event, insertedAt: Date.now() });
|
|
467
|
+
this.trimEventHistory();
|
|
468
|
+
}
|
|
357
469
|
this.node.events.push(event);
|
|
358
470
|
const observers = this.getRootObservers();
|
|
359
471
|
for (const obs of observers) {
|
|
@@ -369,8 +481,117 @@ export class Workflow {
|
|
|
369
481
|
}
|
|
370
482
|
}
|
|
371
483
|
}
|
|
484
|
+
/**
|
|
485
|
+
* Replay historical events to an observer
|
|
486
|
+
*
|
|
487
|
+
* **Strategy:**
|
|
488
|
+
* 1. Start with event history array
|
|
489
|
+
* 2. Filter by timestamp if `since` is provided
|
|
490
|
+
* 3. Limit events if `limit` is provided
|
|
491
|
+
* 4. Call observer.onEvent() for each event
|
|
492
|
+
* 5. Handle observer errors gracefully
|
|
493
|
+
*
|
|
494
|
+
* **Performance:** O(n) where n = number of events in history
|
|
495
|
+
*
|
|
496
|
+
* **Timestamp Handling:**
|
|
497
|
+
* - Events with timestamps: stepRetry, stepRestarted, invalidResponse
|
|
498
|
+
* - Events without timestamps: Always included (considered timeless)
|
|
499
|
+
* - Filter applies only to events with timestamp field
|
|
500
|
+
*
|
|
501
|
+
* **Order of Operations:** Filter first, then limit (more efficient)
|
|
502
|
+
*
|
|
503
|
+
* **Use Case:**
|
|
504
|
+
* - Catch up new observers to current state
|
|
505
|
+
* - Debug by replaying events to diagnostic observers
|
|
506
|
+
* - Test scenarios by replaying historical events
|
|
507
|
+
*
|
|
508
|
+
* @param observer - The observer to replay events to
|
|
509
|
+
* @param options - Optional replay configuration
|
|
510
|
+
* @param options.since - Only replay events after this timestamp (ms since epoch)
|
|
511
|
+
* @param options.limit - Maximum number of events to replay
|
|
512
|
+
*
|
|
513
|
+
* @example Replay all events to new observer
|
|
514
|
+
* ```ts
|
|
515
|
+
* const observer = {
|
|
516
|
+
* onLog: () => {},
|
|
517
|
+
* onEvent: (e) => console.log(e.type),
|
|
518
|
+
* onStateUpdated: () => {},
|
|
519
|
+
* onTreeChanged: () => {},
|
|
520
|
+
* };
|
|
521
|
+
* workflow.replayEvents(observer);
|
|
522
|
+
* ```
|
|
523
|
+
*
|
|
524
|
+
* @example Replay last 10 events
|
|
525
|
+
* ```ts
|
|
526
|
+
* workflow.replayEvents(observer, { limit: 10 });
|
|
527
|
+
* ```
|
|
528
|
+
*
|
|
529
|
+
* @example Replay events from last 5 minutes
|
|
530
|
+
* ```ts
|
|
531
|
+
* const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
|
|
532
|
+
* workflow.replayEvents(observer, { since: fiveMinutesAgo });
|
|
533
|
+
* ```
|
|
534
|
+
*/
|
|
535
|
+
replayEvents(observer, options) {
|
|
536
|
+
// Extract events from entries
|
|
537
|
+
let events = this.#eventHistory.map(entry => entry.event);
|
|
538
|
+
// Filter by timestamp if provided
|
|
539
|
+
if (options?.since !== undefined) {
|
|
540
|
+
events = events.filter(event => {
|
|
541
|
+
// Extract timestamp from events that have it
|
|
542
|
+
const timestamp = event.type === 'stepRetry' ? event.timestamp :
|
|
543
|
+
event.type === 'stepRestarted' ? event.timestamp :
|
|
544
|
+
event.type === 'invalidResponse' ? event.timestamp :
|
|
545
|
+
undefined;
|
|
546
|
+
// Include events without timestamp or events after since
|
|
547
|
+
return timestamp === undefined || timestamp >= options.since;
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
// Apply limit if provided
|
|
551
|
+
if (options?.limit !== undefined) {
|
|
552
|
+
events = events.slice(0, options.limit);
|
|
553
|
+
}
|
|
554
|
+
// Replay events to observer
|
|
555
|
+
for (const event of events) {
|
|
556
|
+
try {
|
|
557
|
+
observer.onEvent(event);
|
|
558
|
+
}
|
|
559
|
+
catch (err) {
|
|
560
|
+
this.logger.error('Observer replay error', { error: err, eventType: event.type });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Clear the event history array
|
|
566
|
+
*
|
|
567
|
+
* **Strategy:**
|
|
568
|
+
* - Reassign #eventHistory to empty array
|
|
569
|
+
* - Frees memory by discarding all stored events
|
|
570
|
+
* - Events in node.events are preserved
|
|
571
|
+
*
|
|
572
|
+
* **Use Case:**
|
|
573
|
+
* - Free memory after workflow completes
|
|
574
|
+
* - Reset history between test runs
|
|
575
|
+
* - Prevent memory leaks in long-running workflows
|
|
576
|
+
*
|
|
577
|
+
* **Side Effects:**
|
|
578
|
+
* - Frees memory for discarded events
|
|
579
|
+
* - Future replayEvents() calls will return empty
|
|
580
|
+
* - Does NOT affect node.events array
|
|
581
|
+
*
|
|
582
|
+
* @example Clear history after workflow completes
|
|
583
|
+
* ```ts
|
|
584
|
+
* await workflow.run();
|
|
585
|
+
* workflow.clearEventHistory(); // Free memory
|
|
586
|
+
* ```
|
|
587
|
+
*/
|
|
588
|
+
clearEventHistory() {
|
|
589
|
+
this.#eventHistory = [];
|
|
590
|
+
}
|
|
372
591
|
/**
|
|
373
592
|
* Capture and emit a state snapshot
|
|
593
|
+
* @side effects Updates node.stateSnapshot, notifies observers via onStateUpdated callback,
|
|
594
|
+
* emits snapshot event, and triggers treeUpdated event for debugger.
|
|
374
595
|
*/
|
|
375
596
|
snapshotState() {
|
|
376
597
|
const snapshot = getObservedState(this);
|
|
@@ -393,6 +614,278 @@ export class Workflow {
|
|
|
393
614
|
// Emit treeUpdated event to trigger tree debugger rebuild
|
|
394
615
|
this.emitEvent({ type: 'treeUpdated', root: this.getRoot().node });
|
|
395
616
|
}
|
|
617
|
+
/**
|
|
618
|
+
* Restart a specific step with state restoration and retry tracking
|
|
619
|
+
*
|
|
620
|
+
* This method enables manual step restart from parent workflows. It validates
|
|
621
|
+
* the step exists, checks retry limits, optionally restores state, re-executes
|
|
622
|
+
* the step method, and emits a stepRestarted event for observability.
|
|
623
|
+
*
|
|
624
|
+
* @param stepName - The name of the step method to restart
|
|
625
|
+
* @param options - Optional configuration for the restart attempt
|
|
626
|
+
* @returns The result of the step execution
|
|
627
|
+
* @throws {WorkflowError} When step is not found or max retries exceeded
|
|
628
|
+
*
|
|
629
|
+
* @example Restart a step with default retry tracking
|
|
630
|
+
* ```ts
|
|
631
|
+
* class MyWorkflow extends Workflow {
|
|
632
|
+
* @Step({ restartable: true })
|
|
633
|
+
* async myStep() { return 'result'; }
|
|
634
|
+
*
|
|
635
|
+
* async run() {
|
|
636
|
+
* const result = await this.restartStep('myStep');
|
|
637
|
+
* }
|
|
638
|
+
* }
|
|
639
|
+
* ```
|
|
640
|
+
*
|
|
641
|
+
* @example Restart with explicit retry count and state override
|
|
642
|
+
* ```ts
|
|
643
|
+
* await this.restartStep('failingStep', {
|
|
644
|
+
* retryCount: 1,
|
|
645
|
+
* maxRetries: 3,
|
|
646
|
+
* stateOverride: { counter: 5 }
|
|
647
|
+
* });
|
|
648
|
+
* ```
|
|
649
|
+
*/
|
|
650
|
+
async restartStep(stepName, options) {
|
|
651
|
+
// Calculate the retry count for this attempt
|
|
652
|
+
const retryCount = (options?.retryCount ?? 0) + 1;
|
|
653
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
654
|
+
// Check retry limit
|
|
655
|
+
if (retryCount > maxRetries) {
|
|
656
|
+
const error = {
|
|
657
|
+
message: `Max retries (${maxRetries}) exceeded for step '${stepName}'`,
|
|
658
|
+
original: new Error('Max retries exceeded'),
|
|
659
|
+
workflowId: this.id,
|
|
660
|
+
state: getObservedState(this),
|
|
661
|
+
logs: [...this.node.logs],
|
|
662
|
+
};
|
|
663
|
+
throw error;
|
|
664
|
+
}
|
|
665
|
+
// Verify the step method exists and is callable
|
|
666
|
+
const method = this[stepName];
|
|
667
|
+
if (typeof method !== 'function') {
|
|
668
|
+
const error = {
|
|
669
|
+
message: `Step '${stepName}' not found`,
|
|
670
|
+
original: new Error('Step not found'),
|
|
671
|
+
workflowId: this.id,
|
|
672
|
+
state: getObservedState(this),
|
|
673
|
+
logs: [...this.node.logs],
|
|
674
|
+
};
|
|
675
|
+
throw error;
|
|
676
|
+
}
|
|
677
|
+
// Restore state - use override if provided, otherwise capture current state
|
|
678
|
+
let restoredState;
|
|
679
|
+
if (options?.stateOverride !== undefined) {
|
|
680
|
+
restoredState = options.stateOverride;
|
|
681
|
+
// For state override, we'd ideally restore the state here
|
|
682
|
+
// Since no restoreState() method exists, stateOverride is primarily for the event payload
|
|
683
|
+
// and any future state restoration implementation
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
// Capture current state as the restored state
|
|
687
|
+
this.snapshotState();
|
|
688
|
+
restoredState = this.node.stateSnapshot ?? {};
|
|
689
|
+
}
|
|
690
|
+
// Execute the step method
|
|
691
|
+
const result = await method.call(this);
|
|
692
|
+
// Emit stepRestarted event
|
|
693
|
+
this.emitEvent({
|
|
694
|
+
type: 'stepRestarted',
|
|
695
|
+
node: this.node,
|
|
696
|
+
stepName,
|
|
697
|
+
retryCount,
|
|
698
|
+
restoredState,
|
|
699
|
+
timestamp: Date.now(),
|
|
700
|
+
});
|
|
701
|
+
return result;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Analyze a WorkflowError from a child workflow and determine restart action
|
|
705
|
+
*
|
|
706
|
+
* This method enables parent workflows to make intelligent decisions about child
|
|
707
|
+
* workflow failures by analyzing the error and step metadata to determine whether
|
|
708
|
+
* to retry the child, abort the parent, or rebuild the execution plan.
|
|
709
|
+
*
|
|
710
|
+
* **Analysis Flow:**
|
|
711
|
+
* 1. Check `error.original?.recoverable` - if false, return 'abort'
|
|
712
|
+
* 2. Extract stepName from error metadata (if available)
|
|
713
|
+
* 3. Retrieve step metadata from stepMetadata map (if exists)
|
|
714
|
+
* 4. Check if step is marked as restartable - if not, return 'abort'
|
|
715
|
+
* 5. Use `analyzeErrorForRestart` utility to check retry criteria
|
|
716
|
+
* 6. Return 'retry' if any criteria match, otherwise 'abort'
|
|
717
|
+
*
|
|
718
|
+
* **Integration with restartStep:**
|
|
719
|
+
* This method is designed to be used alongside `restartStep()`:
|
|
720
|
+
* - Call `analyzeError()` to get the decision
|
|
721
|
+
* - If 'retry', call `restartStep(stepName)` to execute
|
|
722
|
+
* - If 'abort', throw the error or return early
|
|
723
|
+
* - If 'rebuild', trigger plan rebuild logic
|
|
724
|
+
*
|
|
725
|
+
* @param error - The WorkflowError to analyze (typically from child workflow)
|
|
726
|
+
* @returns The recommended action: 'retry', 'abort', or 'rebuild'
|
|
727
|
+
*
|
|
728
|
+
* @example Parent workflow error handling
|
|
729
|
+
* ```ts
|
|
730
|
+
* class ParentWorkflow extends Workflow {
|
|
731
|
+
* @Step({ restartable: true, retryOn: [{ code: 'TIMEOUT' }] })
|
|
732
|
+
* async childWorkflow(): Promise<void> {
|
|
733
|
+
* // Child logic that may fail
|
|
734
|
+
* }
|
|
735
|
+
*
|
|
736
|
+
* async run(): Promise<void> {
|
|
737
|
+
* try {
|
|
738
|
+
* await this.childWorkflow();
|
|
739
|
+
* } catch (err) {
|
|
740
|
+
* const error = err as WorkflowError;
|
|
741
|
+
* const action = this.analyzeError(error);
|
|
742
|
+
*
|
|
743
|
+
* if (action === 'retry') {
|
|
744
|
+
* await this.restartStep('childWorkflow');
|
|
745
|
+
* } else if (action === 'abort') {
|
|
746
|
+
* throw error;
|
|
747
|
+
* } else if (action === 'rebuild') {
|
|
748
|
+
* // Trigger plan rebuild logic
|
|
749
|
+
* }
|
|
750
|
+
* }
|
|
751
|
+
* }
|
|
752
|
+
* }
|
|
753
|
+
* ```
|
|
754
|
+
*
|
|
755
|
+
* @example Analyze error from child workflow event
|
|
756
|
+
* ```ts
|
|
757
|
+
* class ParentWorkflow extends Workflow {
|
|
758
|
+
* private lastError: WorkflowError | null = null;
|
|
759
|
+
*
|
|
760
|
+
* async run(): Promise<void> {
|
|
761
|
+
* // Subscribe to error events
|
|
762
|
+
* this.on('error', (event) => {
|
|
763
|
+
* this.lastError = event.error;
|
|
764
|
+
* });
|
|
765
|
+
*
|
|
766
|
+
* // Later, analyze the error
|
|
767
|
+
* if (this.lastError) {
|
|
768
|
+
* const action = this.analyzeError(this.lastError);
|
|
769
|
+
* // Take action based on decision
|
|
770
|
+
* }
|
|
771
|
+
* }
|
|
772
|
+
* }
|
|
773
|
+
* ```
|
|
774
|
+
*
|
|
775
|
+
* @remarks
|
|
776
|
+
* **Known Limitation:**
|
|
777
|
+
* The `stepMetadata` map is not yet populated by the `@Step` decorator.
|
|
778
|
+
* This method will return 'abort' if stepMetadata is not available or the step
|
|
779
|
+
* is not found. This will be improved in a future update to the decorator.
|
|
780
|
+
*
|
|
781
|
+
* **Error Metadata:**
|
|
782
|
+
* The stepName is extracted from `error.state?.stepName`. Ensure child
|
|
783
|
+
* workflows populate this field when creating WorkflowError instances.
|
|
784
|
+
*
|
|
785
|
+
* @see {@link restartStep} - For executing a retry after analysis
|
|
786
|
+
* @see {@link analyzeErrorForRestart} - For the underlying utility function
|
|
787
|
+
*/
|
|
788
|
+
analyzeError(error) {
|
|
789
|
+
// STEP 1: Check recoverable flag
|
|
790
|
+
const original = error.original;
|
|
791
|
+
if (original && 'recoverable' in original && !original.recoverable) {
|
|
792
|
+
return 'abort';
|
|
793
|
+
}
|
|
794
|
+
// STEP 2: Extract stepName from error metadata
|
|
795
|
+
// GOTCHA: WorkflowError doesn't have stepName property
|
|
796
|
+
// Check if error.state or error.original has step information
|
|
797
|
+
const stepName = error.state?.stepName;
|
|
798
|
+
if (!stepName) {
|
|
799
|
+
return 'abort'; // Can't determine which step failed
|
|
800
|
+
}
|
|
801
|
+
// STEP 3: Get step metadata with graceful handling
|
|
802
|
+
// CRITICAL: stepMetadata may not exist yet (decorator doesn't store it)
|
|
803
|
+
if (!('stepMetadata' in this)) {
|
|
804
|
+
return 'abort'; // No metadata available
|
|
805
|
+
}
|
|
806
|
+
const stepMeta = this.stepMetadata.get(stepName);
|
|
807
|
+
if (!stepMeta) {
|
|
808
|
+
return 'abort'; // Step not found in metadata
|
|
809
|
+
}
|
|
810
|
+
// STEP 4: Check if step is restartable
|
|
811
|
+
if (!stepMeta.options?.restartable) {
|
|
812
|
+
return 'abort'; // Step not marked as restartable
|
|
813
|
+
}
|
|
814
|
+
// STEP 5: Use analyzeErrorForRestart for criterion matching
|
|
815
|
+
const analysis = analyzeErrorForRestart(error, stepMeta.options);
|
|
816
|
+
if (analysis.shouldRestart) {
|
|
817
|
+
return 'retry';
|
|
818
|
+
}
|
|
819
|
+
// STEP 6: Default to abort
|
|
820
|
+
return 'abort';
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Validate an agent response at the workflow level
|
|
824
|
+
*
|
|
825
|
+
* This method enables parent workflows to validate agent responses
|
|
826
|
+
* before processing them. It follows the same validation pattern as
|
|
827
|
+
* Agent.validateResponse() but emits events and creates WorkflowError
|
|
828
|
+
* for workflow-level error handling.
|
|
829
|
+
*
|
|
830
|
+
* @template T - The type of response data
|
|
831
|
+
* @param response - The AgentResponse to validate
|
|
832
|
+
* @param agentId - Identifier of the agent that produced the response
|
|
833
|
+
* @param dataSchema - Optional Zod schema for response data (defaults to z.unknown())
|
|
834
|
+
* @returns true if validation passes, false if validation fails
|
|
835
|
+
*
|
|
836
|
+
* @example Validate response from child workflow
|
|
837
|
+
* ```ts
|
|
838
|
+
* class ParentWorkflow extends Workflow {
|
|
839
|
+
* @Step()
|
|
840
|
+
* async processChildResult() {
|
|
841
|
+
* const response = await this.childWorkflow.run();
|
|
842
|
+
*
|
|
843
|
+
* if (!this.validateAgentResponse(response, this.childWorkflow.agent.id)) {
|
|
844
|
+
* // Handle validation failure
|
|
845
|
+
* const action = this.analyzeError(this.lastError);
|
|
846
|
+
* if (action === 'retry') {
|
|
847
|
+
* return await this.restartStep('processChildResult');
|
|
848
|
+
* }
|
|
849
|
+
* }
|
|
850
|
+
*
|
|
851
|
+
* // Process valid response
|
|
852
|
+
* return response.data;
|
|
853
|
+
* }
|
|
854
|
+
* }
|
|
855
|
+
* ```
|
|
856
|
+
*/
|
|
857
|
+
validateAgentResponse(response, agentId, dataSchema = z.unknown()) {
|
|
858
|
+
// Call shared utility for validation
|
|
859
|
+
const result = validateAgentResponse(response, dataSchema);
|
|
860
|
+
if (result.valid) {
|
|
861
|
+
// Response is valid
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
// Validation failed - emit event and create error
|
|
865
|
+
const zodError = result.errors; // Safe: errors exists when valid is false
|
|
866
|
+
// Emit invalidResponse event
|
|
867
|
+
this.emitEvent({
|
|
868
|
+
type: 'invalidResponse',
|
|
869
|
+
node: this.node,
|
|
870
|
+
response,
|
|
871
|
+
agentId,
|
|
872
|
+
errors: zodError,
|
|
873
|
+
timestamp: Date.now(),
|
|
874
|
+
});
|
|
875
|
+
// Create WorkflowError with INVALID_RESPONSE_FORMAT context
|
|
876
|
+
const validationError = {
|
|
877
|
+
message: `Agent response validation failed for agent '${agentId}'`,
|
|
878
|
+
original: zodError,
|
|
879
|
+
workflowId: this.id,
|
|
880
|
+
stack: zodError.stack,
|
|
881
|
+
state: getObservedState(this),
|
|
882
|
+
logs: [...this.node.logs],
|
|
883
|
+
};
|
|
884
|
+
// Store error for potential use by analyzeError/restartStep
|
|
885
|
+
// Note: Consider adding lastError property to Workflow class if not exists
|
|
886
|
+
// For now, emit event and return false
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
396
889
|
/**
|
|
397
890
|
* Update workflow status and sync with node
|
|
398
891
|
*/
|
|
@@ -431,10 +924,34 @@ export class Workflow {
|
|
|
431
924
|
}
|
|
432
925
|
const startTime = Date.now();
|
|
433
926
|
this.setStatus('running');
|
|
434
|
-
//
|
|
435
|
-
|
|
927
|
+
// Reset error collection state
|
|
928
|
+
this.collectedErrors = [];
|
|
929
|
+
this.operationCounter = 0;
|
|
930
|
+
// Create workflow context with error merge strategy
|
|
931
|
+
const ctx = createWorkflowContext(this, this.parent?.id, this.config.enableReflection ? { enabled: true } : undefined, this.config.autoValidateResponses ?? true, this.config.errorMergeStrategy);
|
|
436
932
|
try {
|
|
437
933
|
const result = await this.executor(ctx);
|
|
934
|
+
// Check if we should merge collected errors
|
|
935
|
+
if (this.collectedErrors.length > 0) {
|
|
936
|
+
if (this.config.errorMergeStrategy?.enabled) {
|
|
937
|
+
// Merge errors
|
|
938
|
+
const mergedError = this.config.errorMergeStrategy?.combine
|
|
939
|
+
? this.config.errorMergeStrategy.combine(this.collectedErrors)
|
|
940
|
+
: mergeWorkflowErrors(this.collectedErrors, this.config.name || this.id, this.id, this.operationCounter);
|
|
941
|
+
// Emit error event with merged error
|
|
942
|
+
this.emitEvent({
|
|
943
|
+
type: 'error',
|
|
944
|
+
node: this.node,
|
|
945
|
+
error: mergedError,
|
|
946
|
+
});
|
|
947
|
+
// Throw merged error
|
|
948
|
+
throw mergedError;
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
// Throw first error (backward compatibility)
|
|
952
|
+
throw this.collectedErrors[0];
|
|
953
|
+
}
|
|
954
|
+
}
|
|
438
955
|
this.setStatus('completed');
|
|
439
956
|
return {
|
|
440
957
|
data: result,
|
|
@@ -443,20 +960,25 @@ export class Workflow {
|
|
|
443
960
|
};
|
|
444
961
|
}
|
|
445
962
|
catch (error) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
963
|
+
// Handle errors thrown directly (not collected)
|
|
964
|
+
if (!this.config.errorMergeStrategy?.enabled) {
|
|
965
|
+
this.setStatus('failed');
|
|
966
|
+
this.emitEvent({
|
|
967
|
+
type: 'error',
|
|
968
|
+
node: this.node,
|
|
969
|
+
error: {
|
|
970
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
971
|
+
original: error,
|
|
972
|
+
workflowId: this.id,
|
|
973
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
974
|
+
state: getObservedState(this),
|
|
975
|
+
logs: [...this.node.logs],
|
|
976
|
+
},
|
|
977
|
+
});
|
|
978
|
+
throw error;
|
|
979
|
+
}
|
|
980
|
+
// If in collection mode, error should have been collected already
|
|
981
|
+
// Re-throw if it somehow escaped collection
|
|
460
982
|
throw error;
|
|
461
983
|
}
|
|
462
984
|
}
|