groundswell 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.claude/system_prompts/task-breakdown.md +100 -0
  3. package/PRPs/001-hierarchical-workflow-engine.md +2438 -0
  4. package/PRPs/PRDs/001-hierarchical-workflow-engine.md +543 -0
  5. package/PRPs/PRDs/002-agent-prompt.md +390 -0
  6. package/PRPs/PRDs/003-agent-prompt.md +943 -0
  7. package/PRPs/PRDs/004-agent-prompt.md +1136 -0
  8. package/PRPs/PRDs/tasks-001.json +492 -0
  9. package/PRPs/README.md +83 -0
  10. package/PRPs/templates/prp_base.md +222 -0
  11. package/README.md +218 -0
  12. package/docs/agent.md +422 -0
  13. package/docs/prompt.md +419 -0
  14. package/docs/workflow.md +600 -0
  15. package/examples/README.md +244 -0
  16. package/examples/examples/01-basic-workflow.ts +100 -0
  17. package/examples/examples/02-decorator-options.ts +217 -0
  18. package/examples/examples/03-parent-child.ts +241 -0
  19. package/examples/examples/04-observers-debugger.ts +340 -0
  20. package/examples/examples/05-error-handling.ts +387 -0
  21. package/examples/examples/06-concurrent-tasks.ts +352 -0
  22. package/examples/examples/07-agent-loops.ts +432 -0
  23. package/examples/examples/08-sdk-features.ts +667 -0
  24. package/examples/examples/09-reflection.ts +573 -0
  25. package/examples/examples/10-introspection.ts +550 -0
  26. package/examples/index.ts +143 -0
  27. package/examples/utils/helpers.ts +57 -0
  28. package/llms_full.txt +5890 -0
  29. package/package.json +63 -0
  30. package/plan/P1P2/PRP.md +527 -0
  31. package/plan/P1P2/research/LRU_CACHE_BEST_PRACTICES.md +1929 -0
  32. package/plan/P1P2/research/LRU_CACHE_CODE_PATTERNS.md +857 -0
  33. package/plan/P1P2/research/LRU_CACHE_INTEGRATION_GUIDE.md +738 -0
  34. package/plan/P1P2/research/LRU_CACHE_RESEARCH_INDEX.md +424 -0
  35. package/plan/P1P2/research/REFLECTION_INDEX.md +291 -0
  36. package/plan/P1P2/research/REFLECTION_RESEARCH_REPORT.md +1342 -0
  37. package/plan/P1P2/research/RESEARCH_SUMMARY.md +342 -0
  38. package/plan/P1P2/research/anthropic-sdk.md +174 -0
  39. package/plan/P1P2/research/async-local-storage.md +200 -0
  40. package/plan/P1P2/research/reflection-code-patterns.md +1205 -0
  41. package/plan/P1P2/research/reflection-decision-matrix.md +421 -0
  42. package/plan/P1P2/research/reflection-implementation-guide.md +1341 -0
  43. package/plan/P1P2/research/reflection-integration-guide.md +834 -0
  44. package/plan/P1P2/research/reflection-patterns.md +1468 -0
  45. package/plan/P1P2/research/reflection-quick-reference.md +558 -0
  46. package/plan/P1P2/research/zod-schema.md +152 -0
  47. package/plan/P3P4/PRP.md +1388 -0
  48. package/plan/P3P4/research/caching-lru.md +116 -0
  49. package/plan/P3P4/research/introspection-tools.md +177 -0
  50. package/plan/P3P4/research/reflection-patterns.md +117 -0
  51. package/plan/P4P5/PRP.md +1136 -0
  52. package/plan/P4P5/research/RESEARCH_SUMMARY.md +151 -0
  53. package/plan/architecture/external_deps.md +358 -0
  54. package/plan/architecture/system_context.md +242 -0
  55. package/plan/backlog.json +867 -0
  56. package/plan/research/INTROSPECTION_RESEARCH_SUMMARY.md +378 -0
  57. package/plan/research/README-INTROSPECTION.md +352 -0
  58. package/plan/research/agent-introspection-patterns.md +1085 -0
  59. package/plan/research/introspection-security-guide.md +928 -0
  60. package/plan/research/introspection-tool-examples.md +875 -0
  61. package/scripts/generate-llms-full.ts +206 -0
  62. package/src/__tests__/integration/agent-workflow.test.ts +256 -0
  63. package/src/__tests__/integration/tree-mirroring.test.ts +114 -0
  64. package/src/__tests__/unit/agent.test.ts +169 -0
  65. package/src/__tests__/unit/cache-key.test.ts +182 -0
  66. package/src/__tests__/unit/cache.test.ts +172 -0
  67. package/src/__tests__/unit/context.test.ts +138 -0
  68. package/src/__tests__/unit/decorators.test.ts +100 -0
  69. package/src/__tests__/unit/introspection-tools.test.ts +277 -0
  70. package/src/__tests__/unit/prompt.test.ts +135 -0
  71. package/src/__tests__/unit/reflection.test.ts +210 -0
  72. package/src/__tests__/unit/tree-debugger.test.ts +85 -0
  73. package/src/__tests__/unit/workflow.test.ts +81 -0
  74. package/src/cache/cache-key.ts +244 -0
  75. package/src/cache/cache.ts +236 -0
  76. package/src/cache/index.ts +8 -0
  77. package/src/core/agent.ts +573 -0
  78. package/src/core/context.ts +119 -0
  79. package/src/core/event-tree.ts +260 -0
  80. package/src/core/factory.ts +123 -0
  81. package/src/core/index.ts +17 -0
  82. package/src/core/logger.ts +87 -0
  83. package/src/core/mcp-handler.ts +184 -0
  84. package/src/core/prompt.ts +150 -0
  85. package/src/core/workflow-context.ts +349 -0
  86. package/src/core/workflow.ts +302 -0
  87. package/src/debugger/index.ts +1 -0
  88. package/src/debugger/tree-debugger.ts +210 -0
  89. package/src/decorators/index.ts +3 -0
  90. package/src/decorators/observed-state.ts +95 -0
  91. package/src/decorators/step.ts +139 -0
  92. package/src/decorators/task.ts +96 -0
  93. package/src/examples/index.ts +2 -0
  94. package/src/examples/tdd-orchestrator.ts +65 -0
  95. package/src/examples/test-cycle-workflow.ts +64 -0
  96. package/src/index.ts +140 -0
  97. package/src/reflection/index.ts +5 -0
  98. package/src/reflection/reflection.ts +407 -0
  99. package/src/tools/index.ts +36 -0
  100. package/src/tools/introspection.ts +464 -0
  101. package/src/types/agent.ts +90 -0
  102. package/src/types/decorators.ts +25 -0
  103. package/src/types/error-strategy.ts +13 -0
  104. package/src/types/error.ts +20 -0
  105. package/src/types/events.ts +74 -0
  106. package/src/types/index.ts +55 -0
  107. package/src/types/logging.ts +24 -0
  108. package/src/types/observer.ts +18 -0
  109. package/src/types/prompt.ts +40 -0
  110. package/src/types/reflection.ts +117 -0
  111. package/src/types/sdk-primitives.ts +128 -0
  112. package/src/types/snapshot.ts +14 -0
  113. package/src/types/workflow-context.ts +163 -0
  114. package/src/types/workflow.ts +37 -0
  115. package/src/utils/id.ts +11 -0
  116. package/src/utils/index.ts +3 -0
  117. package/src/utils/observable.ts +77 -0
  118. package/tasks.json +0 -0
  119. package/tsconfig.json +22 -0
  120. package/vitest.config.ts +16 -0
@@ -0,0 +1,302 @@
1
+ import type {
2
+ WorkflowNode,
3
+ WorkflowStatus,
4
+ WorkflowEvent,
5
+ WorkflowObserver,
6
+ } from '../types/index.js';
7
+ import type { WorkflowContext, WorkflowConfig, WorkflowResult } from '../types/workflow-context.js';
8
+ import { generateId } from '../utils/id.js';
9
+ import { WorkflowLogger } from './logger.js';
10
+ import { getObservedState } from '../decorators/observed-state.js';
11
+ import { createWorkflowContext } from './workflow-context.js';
12
+
13
+ /**
14
+ * Executor function type for functional workflows
15
+ */
16
+ export type WorkflowExecutor<T = unknown> = (ctx: WorkflowContext) => Promise<T>;
17
+
18
+ /**
19
+ * Base class for all workflows
20
+ * Supports both class-based (subclass with run()) and functional (executor) patterns
21
+ *
22
+ * @example Class-based pattern:
23
+ * ```ts
24
+ * class MyWorkflow extends Workflow {
25
+ * async run() {
26
+ * this.setStatus('running');
27
+ * // workflow logic
28
+ * this.setStatus('completed');
29
+ * }
30
+ * }
31
+ * ```
32
+ *
33
+ * @example Functional pattern:
34
+ * ```ts
35
+ * const workflow = new Workflow({ name: 'MyWorkflow' }, async (ctx) => {
36
+ * await ctx.step('step1', async () => {
37
+ * // step logic
38
+ * });
39
+ * });
40
+ * await workflow.run();
41
+ * ```
42
+ */
43
+ export class Workflow<T = unknown> {
44
+ /** Unique identifier for this workflow instance */
45
+ public readonly id: string;
46
+
47
+ /** Parent workflow (null for root workflows) */
48
+ public parent: Workflow | null = null;
49
+
50
+ /** Child workflows */
51
+ public children: Workflow[] = [];
52
+
53
+ /** Current execution status */
54
+ public status: WorkflowStatus = 'idle';
55
+
56
+ /** Logger instance for this workflow */
57
+ protected readonly logger: WorkflowLogger;
58
+
59
+ /** The node representation of this workflow */
60
+ protected readonly node: WorkflowNode;
61
+
62
+ /** Observers (only populated on root workflow) */
63
+ private observers: WorkflowObserver[] = [];
64
+
65
+ /** Optional executor function for functional workflows */
66
+ private executor?: WorkflowExecutor<T>;
67
+
68
+ /** Workflow configuration */
69
+ private config: WorkflowConfig;
70
+
71
+ /**
72
+ * Create a new workflow instance
73
+ *
74
+ * @overload Class-based pattern
75
+ * @param name Human-readable name (defaults to class name)
76
+ * @param parent Optional parent workflow
77
+ *
78
+ * @overload Functional pattern
79
+ * @param config Workflow configuration
80
+ * @param executor Executor function
81
+ */
82
+ constructor(name?: string | WorkflowConfig, parentOrExecutor?: Workflow | WorkflowExecutor<T>) {
83
+ this.id = generateId();
84
+
85
+ // Parse overloaded arguments
86
+ if (typeof name === 'object' && name !== null) {
87
+ // Functional pattern: constructor(config, executor)
88
+ this.config = name;
89
+ this.executor = parentOrExecutor as WorkflowExecutor<T>;
90
+ this.parent = null;
91
+ } else {
92
+ // Class-based pattern: constructor(name, parent)
93
+ this.config = { name: name ?? this.constructor.name };
94
+ this.parent = (parentOrExecutor as Workflow) ?? null;
95
+ }
96
+
97
+ // Create the node representation
98
+ this.node = {
99
+ id: this.id,
100
+ name: this.config.name ?? this.constructor.name,
101
+ parent: this.parent?.node ?? null,
102
+ children: [],
103
+ status: 'idle',
104
+ logs: [],
105
+ events: [],
106
+ stateSnapshot: null,
107
+ };
108
+
109
+ // Create logger with root observers
110
+ this.logger = new WorkflowLogger(this.node, this.getRootObservers());
111
+
112
+ // Attach to parent if provided
113
+ if (this.parent) {
114
+ this.parent.attachChild(this);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Get observers from the root workflow
120
+ * Traverses up the tree to find the root
121
+ */
122
+ private getRootObservers(): WorkflowObserver[] {
123
+ if (this.parent) {
124
+ return this.parent.getRootObservers();
125
+ }
126
+ return this.observers;
127
+ }
128
+
129
+ /**
130
+ * Get the root workflow
131
+ */
132
+ protected getRoot(): Workflow {
133
+ if (this.parent) {
134
+ return this.parent.getRoot();
135
+ }
136
+ return this;
137
+ }
138
+
139
+ /**
140
+ * Add an observer to this workflow (must be root)
141
+ * @throws Error if called on non-root workflow
142
+ */
143
+ public addObserver(observer: WorkflowObserver): void {
144
+ if (this.parent) {
145
+ throw new Error('Observers can only be added to root workflows');
146
+ }
147
+ this.observers.push(observer);
148
+ }
149
+
150
+ /**
151
+ * Remove an observer from this workflow
152
+ */
153
+ public removeObserver(observer: WorkflowObserver): void {
154
+ const index = this.observers.indexOf(observer);
155
+ if (index !== -1) {
156
+ this.observers.splice(index, 1);
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Attach a child workflow
162
+ * Called automatically in constructor when parent is provided
163
+ */
164
+ public attachChild(child: Workflow): void {
165
+ this.children.push(child);
166
+ this.node.children.push(child.node);
167
+
168
+ // Emit child attached event
169
+ this.emitEvent({
170
+ type: 'childAttached',
171
+ parentId: this.id,
172
+ child: child.node,
173
+ });
174
+ }
175
+
176
+ /**
177
+ * Emit an event to all root observers
178
+ */
179
+ public emitEvent(event: WorkflowEvent): void {
180
+ this.node.events.push(event);
181
+
182
+ const observers = this.getRootObservers();
183
+ for (const obs of observers) {
184
+ try {
185
+ obs.onEvent(event);
186
+
187
+ // Also notify tree changed for tree update events
188
+ if (event.type === 'treeUpdated' || event.type === 'childAttached') {
189
+ obs.onTreeChanged(this.getRoot().node);
190
+ }
191
+ } catch (err) {
192
+ console.error('Observer onEvent error:', err);
193
+ }
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Capture and emit a state snapshot
199
+ */
200
+ public snapshotState(): void {
201
+ const snapshot = getObservedState(this);
202
+ this.node.stateSnapshot = snapshot;
203
+
204
+ // Notify observers
205
+ const observers = this.getRootObservers();
206
+ for (const obs of observers) {
207
+ try {
208
+ obs.onStateUpdated(this.node);
209
+ } catch (err) {
210
+ console.error('Observer onStateUpdated error:', err);
211
+ }
212
+ }
213
+
214
+ // Emit snapshot event
215
+ this.emitEvent({
216
+ type: 'stateSnapshot',
217
+ node: this.node,
218
+ });
219
+ }
220
+
221
+ /**
222
+ * Update workflow status and sync with node
223
+ */
224
+ public setStatus(status: WorkflowStatus): void {
225
+ this.status = status;
226
+ this.node.status = status;
227
+ }
228
+
229
+ /**
230
+ * Get the node representation of this workflow
231
+ */
232
+ public getNode(): WorkflowNode {
233
+ return this.node;
234
+ }
235
+
236
+ /**
237
+ * Run the workflow
238
+ *
239
+ * For functional workflows (created with executor), runs the executor function.
240
+ * For class-based workflows (subclasses), this should be overridden.
241
+ *
242
+ * @returns Workflow result
243
+ */
244
+ public async run(..._args: unknown[]): Promise<T | WorkflowResult<T>> {
245
+ if (this.executor) {
246
+ return this.runFunctional();
247
+ }
248
+
249
+ // Class-based workflows must override this method
250
+ throw new Error(
251
+ 'Workflow.run() must be overridden in subclass or provide executor in constructor'
252
+ );
253
+ }
254
+
255
+ /**
256
+ * Run a functional workflow with context
257
+ */
258
+ private async runFunctional(): Promise<WorkflowResult<T>> {
259
+ if (!this.executor) {
260
+ throw new Error('No executor provided');
261
+ }
262
+
263
+ const startTime = Date.now();
264
+ this.setStatus('running');
265
+
266
+ // Create workflow context
267
+ const ctx = createWorkflowContext(
268
+ this as unknown as Parameters<typeof createWorkflowContext>[0],
269
+ this.parent?.id,
270
+ this.config.enableReflection ? { enabled: true } : undefined
271
+ );
272
+
273
+ try {
274
+ const result = await this.executor(ctx);
275
+ this.setStatus('completed');
276
+
277
+ return {
278
+ data: result,
279
+ node: this.node,
280
+ duration: Date.now() - startTime,
281
+ };
282
+ } catch (error) {
283
+ this.setStatus('failed');
284
+
285
+ // Emit error event
286
+ this.emitEvent({
287
+ type: 'error',
288
+ node: this.node,
289
+ error: {
290
+ message: error instanceof Error ? error.message : 'Unknown error',
291
+ original: error,
292
+ workflowId: this.id,
293
+ stack: error instanceof Error ? error.stack : undefined,
294
+ state: {},
295
+ logs: [],
296
+ },
297
+ });
298
+
299
+ throw error;
300
+ }
301
+ }
302
+ }
@@ -0,0 +1 @@
1
+ export { WorkflowTreeDebugger } from './tree-debugger.js';
@@ -0,0 +1,210 @@
1
+ import type {
2
+ WorkflowNode,
3
+ WorkflowEvent,
4
+ WorkflowObserver,
5
+ LogEntry,
6
+ } from '../types/index.js';
7
+ import { Observable } from '../utils/observable.js';
8
+ import type { Workflow } from '../core/workflow.js';
9
+
10
+ /**
11
+ * Status symbols for tree visualization
12
+ */
13
+ const STATUS_SYMBOLS: Record<string, string> = {
14
+ idle: '○',
15
+ running: '◐',
16
+ completed: '✓',
17
+ failed: '✗',
18
+ cancelled: '⊘',
19
+ };
20
+
21
+ /**
22
+ * Tree debugger for real-time workflow visualization
23
+ * Implements WorkflowObserver to receive all events
24
+ */
25
+ export class WorkflowTreeDebugger implements WorkflowObserver {
26
+ /** Root node of the workflow tree */
27
+ private root: WorkflowNode;
28
+
29
+ /** Observable stream of workflow events */
30
+ public readonly events: Observable<WorkflowEvent>;
31
+
32
+ /** Node lookup map for quick access */
33
+ private nodeMap: Map<string, WorkflowNode> = new Map();
34
+
35
+ /**
36
+ * Create a tree debugger attached to a workflow
37
+ * @param workflow The root workflow to debug
38
+ */
39
+ constructor(workflow: Workflow) {
40
+ this.root = workflow.getNode();
41
+ this.events = new Observable<WorkflowEvent>();
42
+
43
+ // Build initial node map
44
+ this.buildNodeMap(this.root);
45
+
46
+ // Register as observer on the workflow
47
+ workflow.addObserver(this);
48
+ }
49
+
50
+ /**
51
+ * Build node lookup map recursively
52
+ */
53
+ private buildNodeMap(node: WorkflowNode): void {
54
+ this.nodeMap.set(node.id, node);
55
+ for (const child of node.children) {
56
+ this.buildNodeMap(child);
57
+ }
58
+ }
59
+
60
+ // WorkflowObserver implementation
61
+
62
+ onLog(_entry: LogEntry): void {
63
+ // Events are forwarded through the event stream
64
+ }
65
+
66
+ onEvent(event: WorkflowEvent): void {
67
+ // Rebuild node map on structural changes
68
+ if (event.type === 'childAttached') {
69
+ this.buildNodeMap(event.child);
70
+ }
71
+
72
+ // Forward to event stream
73
+ this.events.next(event);
74
+ }
75
+
76
+ onStateUpdated(_node: WorkflowNode): void {
77
+ // State updates are available through the node
78
+ }
79
+
80
+ onTreeChanged(root: WorkflowNode): void {
81
+ this.root = root;
82
+ this.nodeMap.clear();
83
+ this.buildNodeMap(root);
84
+ }
85
+
86
+ // Public API
87
+
88
+ /**
89
+ * Get the current tree root
90
+ */
91
+ getTree(): WorkflowNode {
92
+ return this.root;
93
+ }
94
+
95
+ /**
96
+ * Get a node by ID
97
+ */
98
+ getNode(id: string): WorkflowNode | undefined {
99
+ return this.nodeMap.get(id);
100
+ }
101
+
102
+ /**
103
+ * Render tree as ASCII string
104
+ * @param node Starting node (defaults to root)
105
+ */
106
+ toTreeString(node?: WorkflowNode): string {
107
+ return this.renderTree(node ?? this.root, '', true, true);
108
+ }
109
+
110
+ /**
111
+ * Recursive tree rendering
112
+ */
113
+ private renderTree(
114
+ node: WorkflowNode,
115
+ prefix: string,
116
+ isLast: boolean,
117
+ isRoot: boolean
118
+ ): string {
119
+ let result = '';
120
+
121
+ // Status symbol and color indicator
122
+ const statusSymbol = STATUS_SYMBOLS[node.status] || '?';
123
+ const nodeInfo = `${statusSymbol} ${node.name} [${node.status}]`;
124
+
125
+ if (isRoot) {
126
+ result += nodeInfo + '\n';
127
+ } else {
128
+ const connector = isLast ? '└── ' : '├── ';
129
+ result += prefix + connector + nodeInfo + '\n';
130
+ }
131
+
132
+ // Render children
133
+ const childCount = node.children.length;
134
+ node.children.forEach((child, index) => {
135
+ const isLastChild = index === childCount - 1;
136
+ const childPrefix = isRoot ? '' : prefix + (isLast ? ' ' : '│ ');
137
+ result += this.renderTree(child, childPrefix, isLastChild, false);
138
+ });
139
+
140
+ return result;
141
+ }
142
+
143
+ /**
144
+ * Render logs as formatted string
145
+ * @param node Starting node (defaults to root, includes descendants)
146
+ */
147
+ toLogString(node?: WorkflowNode): string {
148
+ const logs = this.collectLogs(node ?? this.root);
149
+
150
+ // Sort by timestamp
151
+ logs.sort((a, b) => a.timestamp - b.timestamp);
152
+
153
+ return logs
154
+ .map((log) => {
155
+ const time = new Date(log.timestamp).toISOString();
156
+ const level = log.level.toUpperCase().padEnd(5);
157
+ const nodeRef = this.nodeMap.get(log.workflowId);
158
+ const nodeName = nodeRef?.name ?? log.workflowId;
159
+ return `[${time}] ${level} [${nodeName}] ${log.message}`;
160
+ })
161
+ .join('\n');
162
+ }
163
+
164
+ /**
165
+ * Collect all logs from a node and its descendants
166
+ */
167
+ private collectLogs(node: WorkflowNode): LogEntry[] {
168
+ const logs: LogEntry[] = [...node.logs];
169
+
170
+ for (const child of node.children) {
171
+ logs.push(...this.collectLogs(child));
172
+ }
173
+
174
+ return logs;
175
+ }
176
+
177
+ /**
178
+ * Get summary statistics for the tree
179
+ */
180
+ getStats(): {
181
+ totalNodes: number;
182
+ byStatus: Record<string, number>;
183
+ totalLogs: number;
184
+ totalEvents: number;
185
+ } {
186
+ const stats = {
187
+ totalNodes: 0,
188
+ byStatus: {} as Record<string, number>,
189
+ totalLogs: 0,
190
+ totalEvents: 0,
191
+ };
192
+
193
+ this.collectStats(this.root, stats);
194
+ return stats;
195
+ }
196
+
197
+ private collectStats(
198
+ node: WorkflowNode,
199
+ stats: ReturnType<typeof this.getStats>
200
+ ): void {
201
+ stats.totalNodes++;
202
+ stats.byStatus[node.status] = (stats.byStatus[node.status] || 0) + 1;
203
+ stats.totalLogs += node.logs.length;
204
+ stats.totalEvents += node.events.length;
205
+
206
+ for (const child of node.children) {
207
+ this.collectStats(child, stats);
208
+ }
209
+ }
210
+ }
@@ -0,0 +1,3 @@
1
+ export { ObservedState, getObservedState, isFieldObserved, getFieldMetadata } from './observed-state.js';
2
+ export { Step } from './step.js';
3
+ export { Task } from './task.js';
@@ -0,0 +1,95 @@
1
+ import type { StateFieldMetadata, SerializedWorkflowState } from '../types/index.js';
2
+
3
+ /**
4
+ * WeakMap storing field metadata keyed by class prototype
5
+ * Structure: Map<propertyKey, StateFieldMetadata>
6
+ */
7
+ const OBSERVED_STATE_FIELDS = new WeakMap<object, Map<string, StateFieldMetadata>>();
8
+
9
+ /**
10
+ * @ObservedState decorator
11
+ * Marks a class field for inclusion in state snapshots
12
+ *
13
+ * @example
14
+ * class MyWorkflow extends Workflow {
15
+ * @ObservedState()
16
+ * currentStep!: string;
17
+ *
18
+ * @ObservedState({ redact: true })
19
+ * sensitiveData!: string;
20
+ *
21
+ * @ObservedState({ hidden: true })
22
+ * internalState!: object;
23
+ * }
24
+ */
25
+ export function ObservedState(meta: StateFieldMetadata = {}) {
26
+ return function (
27
+ _value: undefined,
28
+ context: ClassFieldDecoratorContext
29
+ ): void {
30
+ const propertyKey = String(context.name);
31
+
32
+ // Use addInitializer to register field when class is instantiated
33
+ context.addInitializer(function (this: unknown) {
34
+ const instance = this as object;
35
+ const proto = Object.getPrototypeOf(instance);
36
+ let map = OBSERVED_STATE_FIELDS.get(proto);
37
+ if (!map) {
38
+ map = new Map();
39
+ OBSERVED_STATE_FIELDS.set(proto, map);
40
+ }
41
+ map.set(propertyKey, meta);
42
+ });
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Get all observed state from an object instance
48
+ * Applies hidden and redact transformations
49
+ */
50
+ export function getObservedState(obj: object): SerializedWorkflowState {
51
+ const proto = Object.getPrototypeOf(obj);
52
+ const map = OBSERVED_STATE_FIELDS.get(proto);
53
+
54
+ if (!map) {
55
+ return {};
56
+ }
57
+
58
+ const result: SerializedWorkflowState = {};
59
+
60
+ for (const [key, meta] of map) {
61
+ // Skip hidden fields
62
+ if (meta.hidden) {
63
+ continue;
64
+ }
65
+
66
+ let value = (obj as Record<string, unknown>)[key];
67
+
68
+ // Redact sensitive fields
69
+ if (meta.redact) {
70
+ value = '***';
71
+ }
72
+
73
+ result[key] = value;
74
+ }
75
+
76
+ return result;
77
+ }
78
+
79
+ /**
80
+ * Check if a field is observed on an object
81
+ */
82
+ export function isFieldObserved(obj: object, fieldName: string): boolean {
83
+ const proto = Object.getPrototypeOf(obj);
84
+ const map = OBSERVED_STATE_FIELDS.get(proto);
85
+ return map?.has(fieldName) ?? false;
86
+ }
87
+
88
+ /**
89
+ * Get metadata for a specific field
90
+ */
91
+ export function getFieldMetadata(obj: object, fieldName: string): StateFieldMetadata | undefined {
92
+ const proto = Object.getPrototypeOf(obj);
93
+ const map = OBSERVED_STATE_FIELDS.get(proto);
94
+ return map?.get(fieldName);
95
+ }