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.
- package/.claude/settings.local.json +9 -0
- package/.claude/system_prompts/task-breakdown.md +100 -0
- package/PRPs/001-hierarchical-workflow-engine.md +2438 -0
- package/PRPs/PRDs/001-hierarchical-workflow-engine.md +543 -0
- package/PRPs/PRDs/002-agent-prompt.md +390 -0
- package/PRPs/PRDs/003-agent-prompt.md +943 -0
- package/PRPs/PRDs/004-agent-prompt.md +1136 -0
- package/PRPs/PRDs/tasks-001.json +492 -0
- package/PRPs/README.md +83 -0
- package/PRPs/templates/prp_base.md +222 -0
- package/README.md +218 -0
- package/docs/agent.md +422 -0
- package/docs/prompt.md +419 -0
- package/docs/workflow.md +600 -0
- package/examples/README.md +244 -0
- package/examples/examples/01-basic-workflow.ts +100 -0
- package/examples/examples/02-decorator-options.ts +217 -0
- package/examples/examples/03-parent-child.ts +241 -0
- package/examples/examples/04-observers-debugger.ts +340 -0
- package/examples/examples/05-error-handling.ts +387 -0
- package/examples/examples/06-concurrent-tasks.ts +352 -0
- package/examples/examples/07-agent-loops.ts +432 -0
- package/examples/examples/08-sdk-features.ts +667 -0
- package/examples/examples/09-reflection.ts +573 -0
- package/examples/examples/10-introspection.ts +550 -0
- package/examples/index.ts +143 -0
- package/examples/utils/helpers.ts +57 -0
- package/llms_full.txt +5890 -0
- package/package.json +63 -0
- package/plan/P1P2/PRP.md +527 -0
- package/plan/P1P2/research/LRU_CACHE_BEST_PRACTICES.md +1929 -0
- package/plan/P1P2/research/LRU_CACHE_CODE_PATTERNS.md +857 -0
- package/plan/P1P2/research/LRU_CACHE_INTEGRATION_GUIDE.md +738 -0
- package/plan/P1P2/research/LRU_CACHE_RESEARCH_INDEX.md +424 -0
- package/plan/P1P2/research/REFLECTION_INDEX.md +291 -0
- package/plan/P1P2/research/REFLECTION_RESEARCH_REPORT.md +1342 -0
- package/plan/P1P2/research/RESEARCH_SUMMARY.md +342 -0
- package/plan/P1P2/research/anthropic-sdk.md +174 -0
- package/plan/P1P2/research/async-local-storage.md +200 -0
- package/plan/P1P2/research/reflection-code-patterns.md +1205 -0
- package/plan/P1P2/research/reflection-decision-matrix.md +421 -0
- package/plan/P1P2/research/reflection-implementation-guide.md +1341 -0
- package/plan/P1P2/research/reflection-integration-guide.md +834 -0
- package/plan/P1P2/research/reflection-patterns.md +1468 -0
- package/plan/P1P2/research/reflection-quick-reference.md +558 -0
- package/plan/P1P2/research/zod-schema.md +152 -0
- package/plan/P3P4/PRP.md +1388 -0
- package/plan/P3P4/research/caching-lru.md +116 -0
- package/plan/P3P4/research/introspection-tools.md +177 -0
- package/plan/P3P4/research/reflection-patterns.md +117 -0
- package/plan/P4P5/PRP.md +1136 -0
- package/plan/P4P5/research/RESEARCH_SUMMARY.md +151 -0
- package/plan/architecture/external_deps.md +358 -0
- package/plan/architecture/system_context.md +242 -0
- package/plan/backlog.json +867 -0
- package/plan/research/INTROSPECTION_RESEARCH_SUMMARY.md +378 -0
- package/plan/research/README-INTROSPECTION.md +352 -0
- package/plan/research/agent-introspection-patterns.md +1085 -0
- package/plan/research/introspection-security-guide.md +928 -0
- package/plan/research/introspection-tool-examples.md +875 -0
- package/scripts/generate-llms-full.ts +206 -0
- package/src/__tests__/integration/agent-workflow.test.ts +256 -0
- package/src/__tests__/integration/tree-mirroring.test.ts +114 -0
- package/src/__tests__/unit/agent.test.ts +169 -0
- package/src/__tests__/unit/cache-key.test.ts +182 -0
- package/src/__tests__/unit/cache.test.ts +172 -0
- package/src/__tests__/unit/context.test.ts +138 -0
- package/src/__tests__/unit/decorators.test.ts +100 -0
- package/src/__tests__/unit/introspection-tools.test.ts +277 -0
- package/src/__tests__/unit/prompt.test.ts +135 -0
- package/src/__tests__/unit/reflection.test.ts +210 -0
- package/src/__tests__/unit/tree-debugger.test.ts +85 -0
- package/src/__tests__/unit/workflow.test.ts +81 -0
- package/src/cache/cache-key.ts +244 -0
- package/src/cache/cache.ts +236 -0
- package/src/cache/index.ts +8 -0
- package/src/core/agent.ts +573 -0
- package/src/core/context.ts +119 -0
- package/src/core/event-tree.ts +260 -0
- package/src/core/factory.ts +123 -0
- package/src/core/index.ts +17 -0
- package/src/core/logger.ts +87 -0
- package/src/core/mcp-handler.ts +184 -0
- package/src/core/prompt.ts +150 -0
- package/src/core/workflow-context.ts +349 -0
- package/src/core/workflow.ts +302 -0
- package/src/debugger/index.ts +1 -0
- package/src/debugger/tree-debugger.ts +210 -0
- package/src/decorators/index.ts +3 -0
- package/src/decorators/observed-state.ts +95 -0
- package/src/decorators/step.ts +139 -0
- package/src/decorators/task.ts +96 -0
- package/src/examples/index.ts +2 -0
- package/src/examples/tdd-orchestrator.ts +65 -0
- package/src/examples/test-cycle-workflow.ts +64 -0
- package/src/index.ts +140 -0
- package/src/reflection/index.ts +5 -0
- package/src/reflection/reflection.ts +407 -0
- package/src/tools/index.ts +36 -0
- package/src/tools/introspection.ts +464 -0
- package/src/types/agent.ts +90 -0
- package/src/types/decorators.ts +25 -0
- package/src/types/error-strategy.ts +13 -0
- package/src/types/error.ts +20 -0
- package/src/types/events.ts +74 -0
- package/src/types/index.ts +55 -0
- package/src/types/logging.ts +24 -0
- package/src/types/observer.ts +18 -0
- package/src/types/prompt.ts +40 -0
- package/src/types/reflection.ts +117 -0
- package/src/types/sdk-primitives.ts +128 -0
- package/src/types/snapshot.ts +14 -0
- package/src/types/workflow-context.ts +163 -0
- package/src/types/workflow.ts +37 -0
- package/src/utils/id.ts +11 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/observable.ts +77 -0
- package/tasks.json +0 -0
- package/tsconfig.json +22 -0
- 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,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
|
+
}
|