+
setExpanded(!expanded)}>
+ {node.internalFlow && (expanded ? 'βΌ' : 'βΆ')}
+ {node.type}
+
+
+ {expanded && node.internalFlow && (
+
+ {node.internalFlow.nodes.map(child => (
+
+ ))}
+
+ )}
+
+ );
+}
+```
+
+---
+
+## 8. Observability Integration
+
+### 8.1 Event Streaming
+
+Events from nested flows automatically include full namespace:
+
+```typescript
+// Event from internal node
+{
+ type: StreamEventType.NODE_START,
+ nodeId: "summary",
+ nodeName: "BaseChatCompletionNode",
+ namespace: "youtube.research.agent.summary", // β
Full path
+ timestamp: 1234567890
+}
+```
+
+**UI can filter by depth:**
+```typescript
+// Show only top-level events
+streamer.on('youtube.research.*', handler); // Depth 1
+
+// Show events from agent's internal flow
+streamer.on('youtube.research.agent.*', handler); // Depth 2
+
+// Show all events
+streamer.on('*', handler); // All depths
+```
+
+### 8.2 Hierarchical Visualization
+
+```typescript
+class FlowVisualizer {
+ start(): void {
+ this.streamer.on('*', (event) => {
+ const depth = event.namespace.split('.').length - 1;
+ const indent = 'β '.repeat(depth);
+
+ console.log(`${indent}βοΈ ${event.nodeName}`);
+ });
+ }
+}
+
+// Output:
+// βοΈ YouTubeResearchAgentNode
+// β βοΈ YouTubeSearchNode
+// β βοΈ DataAnalysisNode
+// β β βοΈ BaseChatCompletionNode
+```
+
+---
+
+## 9. Testing Requirements
+
+### 9.1 Unit Tests
+
+```typescript
+describe('BackpackNode - Composite Pattern', () => {
+ it('should create internal flow with inherited context', () => {
+ const node = new TestCompositeNode(config, context);
+ const internalFlow = node.createInternalFlow();
+
+ expect(internalFlow.namespace).toBe(node.namespace);
+ expect(internalFlow.backpack).toBe(node.backpack);
+ expect(internalFlow.eventStreamer).toBe(node.eventStreamer);
+ });
+
+ it('should throw if createInternalFlow called twice', () => {
+ const node = new TestCompositeNode(config, context);
+ node.createInternalFlow();
+
+ expect(() => node.createInternalFlow()).toThrow();
+ });
+
+ it('should expose internal flow via getter', () => {
+ const node = new TestCompositeNode(config, context);
+ expect(node.internalFlow).toBeUndefined();
+
+ node.createInternalFlow();
+ expect(node.internalFlow).toBeDefined();
+ });
+
+ it('should report composite status correctly', () => {
+ const node = new TestCompositeNode(config, context);
+ expect(node.isComposite()).toBe(false);
+
+ node.createInternalFlow();
+ expect(node.isComposite()).toBe(true);
+ });
+});
+```
+
+### 9.2 Integration Tests
+
+```typescript
+describe('FlowLoader - Nested Flows', () => {
+ it('should serialize nested flows', () => {
+ const flow = new Flow({ namespace: 'test' });
+ const agent = flow.addNode(CompositeNode, { id: 'agent' });
+
+ const config = loader.exportFlow(flow);
+
+ expect(config.nodes).toHaveLength(1);
+ expect(config.nodes[0].internalFlow).toBeDefined();
+ expect(config.nodes[0].internalFlow.nodes).toHaveLength(3);
+ });
+
+ it('should respect depth limit', () => {
+ const config = loader.exportFlow(flow, { depth: 0 });
+
+ expect(config.nodes[0].internalFlow).toBeUndefined();
+ });
+
+ it('should load nested flows', async () => {
+ const config = loader.exportFlow(originalFlow);
+ const loadedFlow = await loader.loadFlow(config, deps);
+
+ const agent = loadedFlow.getAllNodes()[0];
+ expect(agent.internalFlow).toBeDefined();
+ expect(agent.internalFlow.getAllNodes()).toHaveLength(3);
+ });
+
+ it('should flatten nodes correctly', () => {
+ const config = loader.exportFlow(flow);
+ const flat = loader.flattenNodes(config);
+
+ expect(flat).toHaveLength(4); // 1 parent + 3 internal
+ });
+
+ it('should find nodes by path', () => {
+ const config = loader.exportFlow(flow);
+ const node = loader.findNode(config, 'agent.search');
+
+ expect(node).toBeDefined();
+ expect(node.id).toBe('search');
+ });
+});
+```
+
+### 9.3 E2E Tests
+
+```typescript
+describe('YouTube Research Agent - Nested Flow', () => {
+ it('should serialize complete agent structure', async () => {
+ // Create agent
+ const flow = new Flow({ namespace: 'youtube.research' });
+ const agent = flow.addNode(YouTubeResearchAgentNode, { id: 'agent' });
+
+ // Pack input
+ flow.backpack.pack('searchQuery', 'AI productivity');
+
+ // Run (this creates internal flow)
+ await flow.run({});
+
+ // Serialize
+ const config = loader.exportFlow(flow);
+
+ // Verify structure
+ expect(config.nodes[0].internalFlow).toBeDefined();
+ expect(config.nodes[0].internalFlow.nodes).toHaveLength(3);
+ expect(config.nodes[0].internalFlow.edges).toHaveLength(2);
+ });
+
+ it('should emit events from nested flows', async () => {
+ const events: BackpackEvent[] = [];
+ streamer.on('*', (e) => events.push(e));
+
+ await flow.run({});
+
+ // Should have events from all 4 nodes (1 parent + 3 internal)
+ const nodeStartEvents = events.filter(e => e.type === StreamEventType.NODE_START);
+ expect(nodeStartEvents).toHaveLength(4);
+
+ // Verify namespaces
+ expect(nodeStartEvents[0].namespace).toBe('youtube.research.agent');
+ expect(nodeStartEvents[1].namespace).toBe('youtube.research.agent.search');
+ expect(nodeStartEvents[2].namespace).toBe('youtube.research.agent.analysis');
+ expect(nodeStartEvents[3].namespace).toBe('youtube.research.agent.summary');
+ });
+});
+```
+
+---
+
+## 10. Success Criteria
+
+### 10.1 Developer Experience
+
+- β
Single method call to create internal flow: `this.createInternalFlow()`
+- β
Automatic context inheritance (namespace, backpack, eventStreamer)
+- β
Clear error messages if misused
+- β
Type-safe API
+
+### 10.2 Serialization
+
+- β
Nested structure matches code structure
+- β
Complete visibility into composite nodes
+- β
Depth control for optimization
+- β
Round-trip guarantee (export β import β identical structure)
+
+### 10.3 Observability
+
+- β
Events from nested flows include full namespace path
+- β
UI can filter by depth
+- β
Hierarchical visualization possible
+
+### 10.4 UI Integration
+
+- β
Collapse/expand composite nodes
+- β
Visual hierarchy clear
+- β
Query utilities for flat views when needed
+
+---
+
+## 11. Examples
+
+### 11.1 Simple Composite Node
+
+```typescript
+class PipelineNode extends BackpackNode {
+ static namespaceSegment = "pipeline";
+
+ async _exec(input: any) {
+ const flow = this.createInternalFlow();
+
+ const step1 = flow.addNode(Step1Node, { id: 'step1' });
+ const step2 = flow.addNode(Step2Node, { id: 'step2' });
+ const step3 = flow.addNode(Step3Node, { id: 'step3' });
+
+ // Clean linear routing with convenience methods
+ step1.onComplete(step2);
+ step2.onComplete(step3);
+
+ flow.setEntryNode(step1);
+ await flow.run(input);
+ }
+}
+```
+
+### 11.2 Deeply Nested Flow
+
+```typescript
+// Level 1: Main flow
+const mainFlow = new Flow({ namespace: 'app' });
+const orchestrator = mainFlow.addNode(OrchestratorNode, { id: 'orchestrator' });
+
+// Level 2: Inside orchestrator
+class OrchestratorNode extends BackpackNode {
+ async _exec() {
+ const flow = this.createInternalFlow();
+ const agent = flow.addNode(AgentNode, { id: 'agent' });
+ // ...
+ }
+}
+
+// Level 3: Inside agent
+class AgentNode extends BackpackNode {
+ async _exec() {
+ const flow = this.createInternalFlow();
+ const search = flow.addNode(SearchNode, { id: 'search' });
+ // ...
+ }
+}
+
+// Serialize with depth control
+const fullExport = loader.exportFlow(mainFlow); // All 3 levels
+const twoLevels = loader.exportFlow(mainFlow, { depth: 2 }); // Levels 1-2 only
+const topOnly = loader.exportFlow(mainFlow, { depth: 0 }); // Level 1 only
+```
+
+---
+
+## 12. Migration Path
+
+### 12.1 Backward Compatibility
+
+**Old code (no internal flow) still works:**
+```typescript
+class SimpleNode extends BackpackNode {
+ async _exec(input: any) {
+ // No internal flow, works fine
+ return { result: 'success' };
+ }
+}
+```
+
+### 12.2 Gradual Adoption
+
+**Phase 1:** Update BackpackNode with `internalFlow` support
+**Phase 2:** Update FlowLoader with recursive serialization
+**Phase 3:** Refactor existing composite nodes to use pattern
+**Phase 4:** Update documentation and examples
+
+---
+
+## 13. Future Enhancements (v2.1+)
+
+### 13.1 Mutable Internal Flows (If Truly Needed)
+
+**Note:** v2.0 uses immutable flows (create once, run many). If self-modifying agents become a common pattern, we could add:
+
+```typescript
+class BackpackNode {
+ // v2.0: Immutable (default)
+ protected createInternalFlow(): Flow { ... }
+
+ // v2.1+: Mutable (opt-in)
+ protected createMutableInternalFlow(): MutableFlow {
+ return new MutableFlow({
+ namespace: this.namespace,
+ backpack: this.backpack,
+ eventStreamer: this.eventStreamer
+ });
+ }
+}
+
+// Usage
+class SelfModifyingAgentNode extends BackpackNode {
+ async _exec(input: any) {
+ const flow = this.createMutableInternalFlow();
+
+ // Can add/remove nodes after creation
+ flow.addNode(SearchNode, { id: 'search' });
+ await flow.run(input);
+
+ // Modify structure based on results
+ const results = this.backpack.unpack('search_results');
+ if (results.needsAnalysis) {
+ flow.addNode(AnalysisNode, { id: 'analysis' });
+ }
+
+ await flow.run(input);
+ }
+}
+```
+
+**Not implemented in v2.0** because:
+- Node reuse patterns cover most use cases
+- Adds serialization complexity
+- Not a one-way door decision (can add later)
+
+### 13.2 Flow Templates
+
+```typescript
+// Register reusable internal flow templates
+loader.registerTemplate('research-pipeline', {
+ nodes: [...],
+ edges: [...]
+});
+
+// Use template in composite node
+class AgentNode extends BackpackNode {
+ async _exec() {
+ const flow = this.createInternalFlowFromTemplate('research-pipeline');
+ await flow.run(input);
+ }
+}
+```
+
+### 13.3 Cross-Flow Communication
+
+```typescript
+// Enable internal flows to communicate with sibling flows
+class ParallelAgentNode extends BackpackNode {
+ async _exec() {
+ const flow1 = this.createInternalFlow('branch1');
+ const flow2 = this.createInternalFlow('branch2');
+
+ await Promise.all([
+ flow1.run(input),
+ flow2.run(input)
+ ]);
+ }
+}
+```
+
+---
+
+## 14. Design Decisions
+
+**Status:** All key decisions have been made and approved.
+
+### Q1: Should there be a max depth limit?
+**Options:**
+- A) No limit (developer responsibility)
+- B) Default limit of 10 (configurable)
+- C) Warn if depth > 5
+
+**Decision:** B - Default limit of 10 (configurable).
+
+**Reasoning:** Prevents runaway recursion while allowing flexibility for legitimate deep nesting.
+
+### Q2: Should internal flows be mutable after creation?
+**Options:**
+- A) Immutable once created
+- B) Mutable (can add/remove nodes)
+
+**Decision:** A - Immutable after creation.
+
+**Reasoning:**
+1. **Node reuse** - No need to create duplicate nodes. Just run the same node multiple times in a loop.
+2. **Build upfront** - Dynamic structure (e.g., tool selection) happens during initialization, before first run.
+3. **Simpler serialization** - Flow structure is stable and predictable.
+4. **Not a one-way door** - Can add `createMutableInternalFlow()` in v2.1+ if truly needed.
+
+**Pattern:**
+```typescript
+async _exec(input: any) {
+ // 1. Create flow (once only)
+ const flow = this.createInternalFlow();
+
+ // 2. Build structure dynamically (before first run)
+ const searchNode = flow.addNode(SearchNode, { id: 'search' });
+
+ if (input.needsAnalysis) {
+ const analysisNode = flow.addNode(AnalysisNode, { id: 'analysis' });
+ searchNode.onComplete(analysisNode);
+ }
+
+ // 3. Run flow
+ await flow.run(input);
+
+ // 4. Cannot modify flow after this point
+}
+```
+
+**For iteration, reuse nodes instead of creating new ones:**
+```typescript
+// β
Good: Reuse same node
+async _exec(input: any) {
+ const searchNode = new SearchNode(config, this.context);
+
+ for (let i = 0; i < input.maxIterations; i++) {
+ await searchNode._run(this.backpack);
+
+ const results = this.backpack.unpack('search_results');
+ if (!this.needsMoreResearch(results)) break;
+ }
+}
+
+// β Bad: Creating duplicate nodes
+async _exec(input: any) {
+ const flow = this.createInternalFlow();
+ for (let i = 0; i < input.maxIterations; i++) {
+ flow.addNode(SearchNode, { id: `search_${i}` }); // Wasteful!
+ }
+}
+```
+
+### Q3: How to handle circular references?
+**Scenario:** Node A has internal flow with Node B, which has internal flow with Node A.
+
+**Decision:** Detect and throw error during serialization with clear message.
+
+**Implementation:**
+```typescript
+exportFlow(flow: Flow, options?: { depth?: number }): FlowConfig {
+ const visited = new Set