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,206 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Generates llms_full.txt by combining:
4
+ * 1. README.md and its linked markdown documentation files
5
+ * 2. Examples README and all example source files
6
+ *
7
+ * Future-proof design:
8
+ * - Auto-discovers markdown files linked from README.md (docs/*.md pattern)
9
+ * - Auto-discovers all example files matching examples/examples/*.ts
10
+ * - New docs and examples are automatically included without script changes
11
+ */
12
+
13
+ import * as fs from "fs";
14
+ import * as path from "path";
15
+ import { fileURLToPath } from "url";
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = path.dirname(__filename);
19
+ const ROOT_DIR = path.resolve(__dirname, "..");
20
+ const OUTPUT_FILE = path.join(ROOT_DIR, "llms_full.txt");
21
+
22
+ interface Section {
23
+ title: string;
24
+ content: string;
25
+ filePath?: string;
26
+ }
27
+
28
+ function readFile(filePath: string): string {
29
+ try {
30
+ return fs.readFileSync(filePath, "utf-8");
31
+ } catch (error) {
32
+ console.warn(`Warning: Could not read ${filePath}`);
33
+ return "";
34
+ }
35
+ }
36
+
37
+ function extractMarkdownLinks(content: string, baseDir: string): string[] {
38
+ // Match markdown links: [text](path.md) - only .md files
39
+ const linkRegex = /\[([^\]]+)\]\(([^)]+\.md)\)/g;
40
+ const links: string[] = [];
41
+ let match;
42
+
43
+ while ((match = linkRegex.exec(content)) !== null) {
44
+ const linkPath = match[2];
45
+ // Only include relative links (not http/https)
46
+ if (!linkPath.startsWith("http://") && !linkPath.startsWith("https://")) {
47
+ const absolutePath = path.resolve(baseDir, linkPath);
48
+ if (fs.existsSync(absolutePath) && !links.includes(absolutePath)) {
49
+ links.push(absolutePath);
50
+ }
51
+ }
52
+ }
53
+
54
+ return links;
55
+ }
56
+
57
+ function discoverDocsMarkdown(): string[] {
58
+ const docsDir = path.join(ROOT_DIR, "docs");
59
+ if (!fs.existsSync(docsDir)) {
60
+ return [];
61
+ }
62
+
63
+ return fs
64
+ .readdirSync(docsDir)
65
+ .filter((file) => file.endsWith(".md"))
66
+ .map((file) => path.join(docsDir, file))
67
+ .sort();
68
+ }
69
+
70
+ function discoverExamples(): string[] {
71
+ const examplesDir = path.join(ROOT_DIR, "examples", "examples");
72
+ if (!fs.existsSync(examplesDir)) {
73
+ return [];
74
+ }
75
+
76
+ return fs
77
+ .readdirSync(examplesDir)
78
+ .filter((file) => file.endsWith(".ts"))
79
+ .sort() // Sorting ensures NN- prefixed files are in order
80
+ .map((file) => path.join(examplesDir, file));
81
+ }
82
+
83
+ function formatSection(section: Section): string {
84
+ const separator = "=".repeat(80);
85
+ const relativePath = section.filePath
86
+ ? path.relative(ROOT_DIR, section.filePath)
87
+ : "";
88
+
89
+ let header = `${separator}\n${section.title}`;
90
+ if (relativePath) {
91
+ header += `\nFile: ${relativePath}`;
92
+ }
93
+ header += `\n${separator}\n\n`;
94
+
95
+ return header + section.content.trim() + "\n\n";
96
+ }
97
+
98
+ function generateLlmsFull(): void {
99
+ const sections: Section[] = [];
100
+ const processedFiles = new Set<string>();
101
+
102
+ // 1. Main README
103
+ const readmePath = path.join(ROOT_DIR, "README.md");
104
+ const readmeContent = readFile(readmePath);
105
+ if (readmeContent) {
106
+ sections.push({
107
+ title: "PROJECT README",
108
+ content: readmeContent,
109
+ filePath: readmePath,
110
+ });
111
+ processedFiles.add(readmePath);
112
+ }
113
+
114
+ // 2. Documentation files from docs/ directory
115
+ // First, get links from README to preserve intended order
116
+ const linkedDocs = extractMarkdownLinks(readmeContent, ROOT_DIR);
117
+ // Then discover all docs to catch any not linked
118
+ const allDocs = discoverDocsMarkdown();
119
+
120
+ // Process linked docs first (preserves README order)
121
+ for (const docPath of linkedDocs) {
122
+ if (
123
+ docPath.includes("/docs/") &&
124
+ !processedFiles.has(docPath) &&
125
+ fs.existsSync(docPath)
126
+ ) {
127
+ const content = readFile(docPath);
128
+ const fileName = path.basename(docPath, ".md").toUpperCase();
129
+ sections.push({
130
+ title: `DOCUMENTATION: ${fileName}`,
131
+ content,
132
+ filePath: docPath,
133
+ });
134
+ processedFiles.add(docPath);
135
+ }
136
+ }
137
+
138
+ // Add any docs not linked from README
139
+ for (const docPath of allDocs) {
140
+ if (!processedFiles.has(docPath)) {
141
+ const content = readFile(docPath);
142
+ const fileName = path.basename(docPath, ".md").toUpperCase();
143
+ sections.push({
144
+ title: `DOCUMENTATION: ${fileName}`,
145
+ content,
146
+ filePath: docPath,
147
+ });
148
+ processedFiles.add(docPath);
149
+ }
150
+ }
151
+
152
+ // 3. Examples README
153
+ const examplesReadmePath = path.join(ROOT_DIR, "examples", "README.md");
154
+ if (fs.existsSync(examplesReadmePath)) {
155
+ const content = readFile(examplesReadmePath);
156
+ sections.push({
157
+ title: "EXAMPLES OVERVIEW",
158
+ content,
159
+ filePath: examplesReadmePath,
160
+ });
161
+ processedFiles.add(examplesReadmePath);
162
+ }
163
+
164
+ // 4. All example source files
165
+ const examples = discoverExamples();
166
+ for (const examplePath of examples) {
167
+ const content = readFile(examplePath);
168
+ const fileName = path.basename(examplePath);
169
+ // Extract example number and name from filename (e.g., "01-basic-workflow.ts")
170
+ const match = fileName.match(/^(\d+)-(.+)\.ts$/);
171
+ const exampleName = match
172
+ ? `Example ${parseInt(match[1])}: ${match[2].replace(/-/g, " ")}`
173
+ : fileName;
174
+
175
+ sections.push({
176
+ title: `EXAMPLE: ${exampleName.toUpperCase()}`,
177
+ content,
178
+ filePath: examplePath,
179
+ });
180
+ }
181
+
182
+ // Build output
183
+ const header = `GROUNDSWELL - LLM DOCUMENTATION
184
+ Generated: ${new Date().toISOString()}
185
+ ${"=".repeat(80)}
186
+
187
+ This file contains the complete documentation and examples for the Groundswell project.
188
+ It is auto-generated by scripts/generate-llms-full.ts
189
+
190
+ Contents:
191
+ ${sections.map((s, i) => ` ${i + 1}. ${s.title}`).join("\n")}
192
+
193
+ `;
194
+
195
+ const output =
196
+ header + sections.map((section) => formatSection(section)).join("\n");
197
+
198
+ // Write output
199
+ fs.writeFileSync(OUTPUT_FILE, output);
200
+ console.log(`Generated ${path.relative(process.cwd(), OUTPUT_FILE)}`);
201
+ console.log(`Total sections: ${sections.length}`);
202
+ console.log(`Total size: ${(output.length / 1024).toFixed(1)} KB`);
203
+ }
204
+
205
+ // Run
206
+ generateLlmsFull();
@@ -0,0 +1,256 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { z } from 'zod';
3
+ import {
4
+ Workflow,
5
+ Prompt,
6
+ Step,
7
+ WorkflowObserver,
8
+ WorkflowEvent,
9
+ runInContext,
10
+ type AgentExecutionContext,
11
+ createEventTreeHandle,
12
+ } from '../../index.js';
13
+
14
+ // Mock workflow that simulates agent prompt execution within steps
15
+ class MockAgentWorkflow extends Workflow {
16
+ public events: WorkflowEvent[] = [];
17
+
18
+ @Step({ name: 'step1' })
19
+ async executeStep1(): Promise<string> {
20
+ return 'step1-result';
21
+ }
22
+
23
+ @Step({ name: 'step2' })
24
+ async executeStep2(): Promise<string> {
25
+ return 'step2-result';
26
+ }
27
+
28
+ async run(): Promise<string> {
29
+ this.setStatus('running');
30
+ await this.executeStep1();
31
+ await this.executeStep2();
32
+ this.setStatus('completed');
33
+ return 'done';
34
+ }
35
+ }
36
+
37
+ describe('Agent-Workflow Integration', () => {
38
+ it('should establish context in @Step decorated methods', async () => {
39
+ const workflow = new MockAgentWorkflow('TestWorkflow');
40
+ const events: WorkflowEvent[] = [];
41
+
42
+ const observer: WorkflowObserver = {
43
+ onLog: () => {},
44
+ onEvent: (event) => events.push(event),
45
+ onStateUpdated: () => {},
46
+ onTreeChanged: () => {},
47
+ };
48
+
49
+ workflow.addObserver(observer);
50
+ await workflow.run();
51
+
52
+ // Should have step start/end events
53
+ const stepStarts = events.filter((e) => e.type === 'stepStart');
54
+ const stepEnds = events.filter((e) => e.type === 'stepEnd');
55
+
56
+ expect(stepStarts).toHaveLength(2);
57
+ expect(stepEnds).toHaveLength(2);
58
+ });
59
+
60
+ it('should track events emitted from within step context', async () => {
61
+ const emittedEvents: WorkflowEvent[] = [];
62
+
63
+ class ContextTrackingWorkflow extends Workflow {
64
+ @Step({ name: 'tracked-step' })
65
+ async trackedStep(): Promise<void> {
66
+ // This simulates what happens when Agent.prompt() is called
67
+ // The context should be available
68
+ }
69
+
70
+ async run(): Promise<void> {
71
+ this.setStatus('running');
72
+ await this.trackedStep();
73
+ this.setStatus('completed');
74
+ }
75
+ }
76
+
77
+ const workflow = new ContextTrackingWorkflow('ContextTest');
78
+ const observer: WorkflowObserver = {
79
+ onLog: () => {},
80
+ onEvent: (event) => emittedEvents.push(event),
81
+ onStateUpdated: () => {},
82
+ onTreeChanged: () => {},
83
+ };
84
+
85
+ workflow.addObserver(observer);
86
+ await workflow.run();
87
+
88
+ // Verify step events were emitted
89
+ expect(emittedEvents.some((e) => e.type === 'stepStart')).toBe(true);
90
+ expect(emittedEvents.some((e) => e.type === 'stepEnd')).toBe(true);
91
+ });
92
+
93
+ it('should support functional workflow pattern with step()', async () => {
94
+ const events: WorkflowEvent[] = [];
95
+
96
+ const workflow = new Workflow<string>({ name: 'FunctionalWorkflow' }, async (ctx) => {
97
+ await ctx.step('step-a', async () => {
98
+ return 'a';
99
+ });
100
+
101
+ await ctx.step('step-b', async () => {
102
+ return 'b';
103
+ });
104
+
105
+ return 'completed';
106
+ });
107
+
108
+ const observer: WorkflowObserver = {
109
+ onLog: () => {},
110
+ onEvent: (event) => events.push(event),
111
+ onStateUpdated: () => {},
112
+ onTreeChanged: () => {},
113
+ };
114
+
115
+ workflow.addObserver(observer);
116
+ const result = await workflow.run();
117
+
118
+ expect(result).toEqual({
119
+ data: 'completed',
120
+ node: expect.any(Object),
121
+ duration: expect.any(Number),
122
+ });
123
+
124
+ const stepStarts = events.filter((e) => e.type === 'stepStart');
125
+ const stepEnds = events.filter((e) => e.type === 'stepEnd');
126
+
127
+ expect(stepStarts).toHaveLength(2);
128
+ expect(stepEnds).toHaveLength(2);
129
+ });
130
+
131
+ it('should nest step events under workflow in tree', async () => {
132
+ const workflow = new Workflow<string>({ name: 'TreeTestWorkflow' }, async (ctx) => {
133
+ await ctx.step('nested-step', async () => {
134
+ return 'nested';
135
+ });
136
+
137
+ return 'done';
138
+ });
139
+
140
+ const result = await workflow.run();
141
+ const node = workflow.getNode();
142
+
143
+ // Check that the workflow node has children (the step nodes)
144
+ expect(node.children.length).toBeGreaterThan(0);
145
+ });
146
+
147
+ it('should propagate context through async boundaries', async () => {
148
+ let contextWasAvailable = false;
149
+
150
+ const workflow = new Workflow<boolean>({ name: 'AsyncContextWorkflow' }, async (ctx) => {
151
+ await ctx.step('async-step', async () => {
152
+ // Simulate async operation
153
+ await new Promise((resolve) => setTimeout(resolve, 10));
154
+
155
+ // The context should still be available after async boundary
156
+ // This is verified by the step completing successfully
157
+ contextWasAvailable = true;
158
+ return 'async-result';
159
+ });
160
+
161
+ return contextWasAvailable;
162
+ });
163
+
164
+ const result = await workflow.run();
165
+ // For functional workflows, result is WorkflowResult<T>
166
+ expect((result as { data: boolean }).data).toBe(true);
167
+ });
168
+
169
+ it('should create EventTreeHandle from workflow', async () => {
170
+ const workflow = new Workflow<void>({ name: 'EventTreeWorkflow' }, async (ctx) => {
171
+ await ctx.step('tree-step', async () => {});
172
+ });
173
+
174
+ await workflow.run();
175
+
176
+ const treeHandle = createEventTreeHandle(workflow.getNode());
177
+ expect(treeHandle.root).toBeDefined();
178
+ expect(treeHandle.root.name).toBe('EventTreeWorkflow');
179
+ });
180
+
181
+ it('should handle errors in steps', async () => {
182
+ const events: WorkflowEvent[] = [];
183
+
184
+ const workflow = new Workflow<void>({ name: 'ErrorWorkflow' }, async (ctx) => {
185
+ await ctx.step('failing-step', async () => {
186
+ throw new Error('Step failed');
187
+ });
188
+ });
189
+
190
+ const observer: WorkflowObserver = {
191
+ onLog: () => {},
192
+ onEvent: (event) => events.push(event),
193
+ onStateUpdated: () => {},
194
+ onTreeChanged: () => {},
195
+ };
196
+
197
+ workflow.addObserver(observer);
198
+
199
+ await expect(workflow.run()).rejects.toThrow('Step failed');
200
+
201
+ const errorEvents = events.filter((e) => e.type === 'error');
202
+ // Error is emitted both from the step context and from the workflow
203
+ expect(errorEvents.length).toBeGreaterThanOrEqual(1);
204
+ expect(workflow.status).toBe('failed');
205
+ });
206
+ });
207
+
208
+ describe('Prompt Integration', () => {
209
+ it('should create type-safe prompts with Zod schemas', () => {
210
+ const responseSchema = z.object({
211
+ answer: z.string(),
212
+ confidence: z.number().min(0).max(1),
213
+ });
214
+
215
+ const prompt = new Prompt({
216
+ user: 'What is 2 + 2?',
217
+ responseFormat: responseSchema,
218
+ });
219
+
220
+ // Valid response
221
+ const valid = prompt.validateResponse({ answer: '4', confidence: 0.99 });
222
+ expect(valid).toEqual({ answer: '4', confidence: 0.99 });
223
+
224
+ // Invalid response should throw
225
+ expect(() => prompt.validateResponse({ answer: '4' })).toThrow();
226
+ });
227
+
228
+ it('should support complex nested schemas', () => {
229
+ const schema = z.object({
230
+ items: z.array(
231
+ z.object({
232
+ id: z.number(),
233
+ name: z.string(),
234
+ tags: z.array(z.string()),
235
+ })
236
+ ),
237
+ metadata: z.object({
238
+ total: z.number(),
239
+ page: z.number(),
240
+ }),
241
+ });
242
+
243
+ const prompt = new Prompt({
244
+ user: 'List items',
245
+ responseFormat: schema,
246
+ });
247
+
248
+ const result = prompt.validateResponse({
249
+ items: [{ id: 1, name: 'Item 1', tags: ['a', 'b'] }],
250
+ metadata: { total: 1, page: 1 },
251
+ });
252
+
253
+ expect(result.items).toHaveLength(1);
254
+ expect(result.metadata.total).toBe(1);
255
+ });
256
+ });
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ TDDOrchestrator,
4
+ WorkflowTreeDebugger,
5
+ WorkflowEvent,
6
+ WorkflowObserver,
7
+ } from '../../index.js';
8
+
9
+ describe('Tree Mirroring Integration', () => {
10
+ it('should create 1:1 tree mirror of workflow execution', async () => {
11
+ const orchestrator = new TDDOrchestrator('TDDOrchestrator');
12
+ (orchestrator as any).maxCycles = 1; // Limit to one cycle for test
13
+
14
+ const debugger_ = new WorkflowTreeDebugger(orchestrator);
15
+ const events: WorkflowEvent[] = [];
16
+
17
+ debugger_.events.subscribe({
18
+ next: (event) => events.push(event),
19
+ });
20
+
21
+ try {
22
+ await orchestrator.run();
23
+ } catch {
24
+ // May fail due to random test failures, that's ok
25
+ }
26
+
27
+ // Verify tree structure
28
+ const tree = debugger_.getTree();
29
+ expect(tree.name).toBe('TDDOrchestrator');
30
+
31
+ // Should have at least one child (the cycle)
32
+ expect(tree.children.length).toBeGreaterThanOrEqual(1);
33
+
34
+ // Child should be named Cycle-N
35
+ const cycleChild = tree.children.find((c) => c.name.startsWith('Cycle-'));
36
+ expect(cycleChild).toBeDefined();
37
+
38
+ // Verify events were captured
39
+ expect(events.some((e) => e.type === 'stepStart')).toBe(true);
40
+ expect(events.some((e) => e.type === 'taskStart')).toBe(true);
41
+ });
42
+
43
+ it('should propagate events to root observer', async () => {
44
+ const orchestrator = new TDDOrchestrator('Root');
45
+ (orchestrator as any).maxCycles = 1;
46
+
47
+ const allEvents: WorkflowEvent[] = [];
48
+ const allLogs: any[] = [];
49
+
50
+ const observer: WorkflowObserver = {
51
+ onLog: (entry) => allLogs.push(entry),
52
+ onEvent: (event) => allEvents.push(event),
53
+ onStateUpdated: () => {},
54
+ onTreeChanged: () => {},
55
+ };
56
+
57
+ orchestrator.addObserver(observer);
58
+
59
+ try {
60
+ await orchestrator.run();
61
+ } catch {
62
+ // May fail
63
+ }
64
+
65
+ // Events from child workflows should reach root
66
+ expect(allLogs.length).toBeGreaterThan(0);
67
+ expect(allEvents.length).toBeGreaterThan(0);
68
+
69
+ // Should have events from both parent and child
70
+ const parentEvents = allEvents.filter(
71
+ (e) => 'node' in e && e.node.name === 'Root'
72
+ );
73
+ const childEvents = allEvents.filter(
74
+ (e) => 'node' in e && e.node.name.startsWith('Cycle-')
75
+ );
76
+
77
+ expect(parentEvents.length).toBeGreaterThan(0);
78
+ expect(childEvents.length).toBeGreaterThan(0);
79
+ });
80
+
81
+ it('should include state snapshot on error', async () => {
82
+ const orchestrator = new TDDOrchestrator('ErrorTest');
83
+ (orchestrator as any).maxCycles = 1;
84
+
85
+ const errorEvents: WorkflowEvent[] = [];
86
+
87
+ orchestrator.addObserver({
88
+ onLog: () => {},
89
+ onEvent: (event) => {
90
+ if (event.type === 'error') {
91
+ errorEvents.push(event);
92
+ }
93
+ },
94
+ onStateUpdated: () => {},
95
+ onTreeChanged: () => {},
96
+ });
97
+
98
+ try {
99
+ await orchestrator.run();
100
+ } catch {
101
+ // Expected
102
+ }
103
+
104
+ // If there was an error, it should have state
105
+ if (errorEvents.length > 0) {
106
+ const errEvent = errorEvents[0];
107
+ if (errEvent.type === 'error') {
108
+ expect(errEvent.error.state).toBeDefined();
109
+ expect(errEvent.error.logs).toBeDefined();
110
+ expect(errEvent.error.workflowId).toBeDefined();
111
+ }
112
+ }
113
+ });
114
+ });