attune-ai 2.1.5__py3-none-any.whl → 2.2.0__py3-none-any.whl

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. attune/cli/__init__.py +3 -59
  2. attune/cli/commands/batch.py +4 -12
  3. attune/cli/commands/cache.py +7 -15
  4. attune/cli/commands/provider.py +17 -0
  5. attune/cli/commands/routing.py +3 -1
  6. attune/cli/commands/setup.py +122 -0
  7. attune/cli/commands/tier.py +1 -3
  8. attune/cli/commands/workflow.py +31 -0
  9. attune/cli/parsers/cache.py +1 -0
  10. attune/cli/parsers/help.py +1 -3
  11. attune/cli/parsers/provider.py +7 -0
  12. attune/cli/parsers/routing.py +1 -3
  13. attune/cli/parsers/setup.py +7 -0
  14. attune/cli/parsers/status.py +1 -3
  15. attune/cli/parsers/tier.py +1 -3
  16. attune/cli_minimal.py +9 -3
  17. attune/cli_router.py +9 -7
  18. attune/cli_unified.py +3 -0
  19. attune/dashboard/app.py +3 -1
  20. attune/dashboard/simple_server.py +3 -1
  21. attune/dashboard/standalone_server.py +7 -3
  22. attune/mcp/server.py +54 -102
  23. attune/memory/long_term.py +0 -2
  24. attune/memory/short_term/__init__.py +84 -0
  25. attune/memory/short_term/base.py +467 -0
  26. attune/memory/short_term/batch.py +219 -0
  27. attune/memory/short_term/caching.py +227 -0
  28. attune/memory/short_term/conflicts.py +265 -0
  29. attune/memory/short_term/cross_session.py +122 -0
  30. attune/memory/short_term/facade.py +655 -0
  31. attune/memory/short_term/pagination.py +215 -0
  32. attune/memory/short_term/patterns.py +271 -0
  33. attune/memory/short_term/pubsub.py +286 -0
  34. attune/memory/short_term/queues.py +244 -0
  35. attune/memory/short_term/security.py +300 -0
  36. attune/memory/short_term/sessions.py +250 -0
  37. attune/memory/short_term/streams.py +249 -0
  38. attune/memory/short_term/timelines.py +234 -0
  39. attune/memory/short_term/transactions.py +186 -0
  40. attune/memory/short_term/working.py +252 -0
  41. attune/meta_workflows/cli_commands/__init__.py +3 -0
  42. attune/meta_workflows/cli_commands/agent_commands.py +0 -4
  43. attune/meta_workflows/cli_commands/analytics_commands.py +0 -6
  44. attune/meta_workflows/cli_commands/config_commands.py +0 -5
  45. attune/meta_workflows/cli_commands/memory_commands.py +0 -5
  46. attune/meta_workflows/cli_commands/template_commands.py +0 -5
  47. attune/meta_workflows/cli_commands/workflow_commands.py +0 -6
  48. attune/models/adaptive_routing.py +4 -8
  49. attune/models/auth_cli.py +3 -9
  50. attune/models/auth_strategy.py +2 -4
  51. attune/models/telemetry/analytics.py +0 -2
  52. attune/models/telemetry/backend.py +0 -3
  53. attune/models/telemetry/storage.py +0 -2
  54. attune/orchestration/_strategies/__init__.py +156 -0
  55. attune/orchestration/_strategies/base.py +231 -0
  56. attune/orchestration/_strategies/conditional_strategies.py +373 -0
  57. attune/orchestration/_strategies/conditions.py +369 -0
  58. attune/orchestration/_strategies/core_strategies.py +491 -0
  59. attune/orchestration/_strategies/data_classes.py +64 -0
  60. attune/orchestration/_strategies/nesting.py +233 -0
  61. attune/orchestration/execution_strategies.py +58 -1567
  62. attune/orchestration/meta_orchestrator.py +1 -3
  63. attune/project_index/scanner.py +1 -3
  64. attune/project_index/scanner_parallel.py +7 -5
  65. attune/socratic_router.py +1 -3
  66. attune/telemetry/agent_coordination.py +9 -3
  67. attune/telemetry/agent_tracking.py +16 -3
  68. attune/telemetry/approval_gates.py +22 -5
  69. attune/telemetry/cli.py +1 -3
  70. attune/telemetry/commands/dashboard_commands.py +24 -8
  71. attune/telemetry/event_streaming.py +8 -2
  72. attune/telemetry/feedback_loop.py +10 -2
  73. attune/tools.py +1 -0
  74. attune/workflow_commands.py +1 -3
  75. attune/workflows/__init__.py +53 -10
  76. attune/workflows/autonomous_test_gen.py +158 -102
  77. attune/workflows/base.py +48 -672
  78. attune/workflows/batch_processing.py +1 -3
  79. attune/workflows/compat.py +156 -0
  80. attune/workflows/cost_mixin.py +141 -0
  81. attune/workflows/data_classes.py +92 -0
  82. attune/workflows/document_gen/workflow.py +11 -14
  83. attune/workflows/history.py +62 -37
  84. attune/workflows/llm_base.py +1 -3
  85. attune/workflows/migration.py +422 -0
  86. attune/workflows/output.py +2 -7
  87. attune/workflows/parsing_mixin.py +427 -0
  88. attune/workflows/perf_audit.py +3 -1
  89. attune/workflows/progress.py +9 -11
  90. attune/workflows/release_prep.py +5 -1
  91. attune/workflows/routing.py +0 -2
  92. attune/workflows/secure_release.py +2 -1
  93. attune/workflows/security_audit.py +19 -14
  94. attune/workflows/security_audit_phase3.py +28 -22
  95. attune/workflows/seo_optimization.py +27 -27
  96. attune/workflows/test_gen/test_templates.py +1 -4
  97. attune/workflows/test_gen/workflow.py +0 -2
  98. attune/workflows/test_gen_behavioral.py +6 -19
  99. attune/workflows/test_gen_parallel.py +6 -4
  100. {attune_ai-2.1.5.dist-info → attune_ai-2.2.0.dist-info}/METADATA +4 -3
  101. {attune_ai-2.1.5.dist-info → attune_ai-2.2.0.dist-info}/RECORD +116 -91
  102. {attune_ai-2.1.5.dist-info → attune_ai-2.2.0.dist-info}/entry_points.txt +0 -2
  103. attune_healthcare/monitors/monitoring/__init__.py +9 -9
  104. attune_llm/agent_factory/__init__.py +6 -6
  105. attune_llm/commands/__init__.py +10 -10
  106. attune_llm/commands/models.py +3 -3
  107. attune_llm/config/__init__.py +8 -8
  108. attune_llm/learning/__init__.py +3 -3
  109. attune_llm/learning/extractor.py +5 -3
  110. attune_llm/learning/storage.py +5 -3
  111. attune_llm/security/__init__.py +17 -17
  112. attune_llm/utils/tokens.py +3 -1
  113. attune/cli_legacy.py +0 -3978
  114. attune/memory/short_term.py +0 -2192
  115. attune/workflows/manage_docs.py +0 -87
  116. attune/workflows/test5.py +0 -125
  117. {attune_ai-2.1.5.dist-info → attune_ai-2.2.0.dist-info}/WHEEL +0 -0
  118. {attune_ai-2.1.5.dist-info → attune_ai-2.2.0.dist-info}/licenses/LICENSE +0 -0
  119. {attune_ai-2.1.5.dist-info → attune_ai-2.2.0.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
  120. {attune_ai-2.1.5.dist-info → attune_ai-2.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,231 @@
1
+ """Base class for agent composition strategies.
2
+
3
+ This module defines the ExecutionStrategy abstract base class that all
4
+ strategy implementations must inherit from.
5
+
6
+ Security:
7
+ - All agent outputs validated before passing to next agent
8
+ - No eval() or exec() usage
9
+ - Timeout enforcement at strategy level
10
+
11
+ Copyright 2025 Smart-AI-Memory
12
+ Licensed under Fair Source License 0.9
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from abc import ABC, abstractmethod
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ from .data_classes import AgentResult, StrategyResult
22
+
23
+ if TYPE_CHECKING:
24
+ from ..agent_templates import AgentTemplate
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class ExecutionStrategy(ABC):
30
+ """Base class for agent composition strategies.
31
+
32
+ All strategies must implement execute() method to define
33
+ how agents are coordinated and results aggregated.
34
+ """
35
+
36
+ @abstractmethod
37
+ async def execute(
38
+ self, agents: list[AgentTemplate], context: dict[str, Any]
39
+ ) -> StrategyResult:
40
+ """Execute agents using this strategy.
41
+
42
+ Args:
43
+ agents: List of agent templates to execute
44
+ context: Initial context for execution
45
+
46
+ Returns:
47
+ StrategyResult with aggregated outputs
48
+
49
+ Raises:
50
+ ValueError: If agents list is empty
51
+ TimeoutError: If execution exceeds timeout
52
+ """
53
+ pass
54
+
55
+ async def _execute_agent(
56
+ self, agent: AgentTemplate, context: dict[str, Any]
57
+ ) -> AgentResult:
58
+ """Execute a single agent with real analysis tools.
59
+
60
+ Maps agent capabilities to real tool implementations and executes them.
61
+
62
+ Args:
63
+ agent: Agent template to execute
64
+ context: Execution context
65
+
66
+ Returns:
67
+ AgentResult with execution outcome
68
+ """
69
+ import time
70
+
71
+ from ..real_tools import (
72
+ RealCodeQualityAnalyzer,
73
+ RealCoverageAnalyzer,
74
+ RealDocumentationAnalyzer,
75
+ RealSecurityAuditor,
76
+ )
77
+
78
+ logger.info(f"Executing agent: {agent.id} ({agent.role})")
79
+ start_time = time.perf_counter()
80
+
81
+ # Get project root from context
82
+ project_root = context.get("project_root", ".")
83
+ target_path = context.get("target_path", "src")
84
+
85
+ try:
86
+ # Map agent ID to real tool implementation
87
+ if agent.id == "security_auditor" or "security" in agent.role.lower():
88
+ auditor = RealSecurityAuditor(project_root)
89
+ report = auditor.audit(target_path)
90
+
91
+ output = {
92
+ "agent_role": agent.role,
93
+ "total_issues": report.total_issues,
94
+ "critical_issues": report.critical_count, # Match workflow field name
95
+ "high_issues": report.high_count, # Match workflow field name
96
+ "medium_issues": report.medium_count, # Match workflow field name
97
+ "passed": report.passed,
98
+ "issues_by_file": report.issues_by_file,
99
+ }
100
+ success = report.passed
101
+ confidence = 1.0 if report.total_issues == 0 else 0.7
102
+
103
+ elif agent.id == "test_coverage_analyzer" or "coverage" in agent.role.lower():
104
+ analyzer = RealCoverageAnalyzer(project_root)
105
+ report = analyzer.analyze() # Analyzes all packages automatically
106
+
107
+ output = {
108
+ "agent_role": agent.role,
109
+ "coverage_percent": report.total_coverage, # Match workflow field name
110
+ "total_coverage": report.total_coverage, # Keep for compatibility
111
+ "files_analyzed": report.files_analyzed,
112
+ "uncovered_files": report.uncovered_files,
113
+ "passed": report.total_coverage >= 80.0,
114
+ }
115
+ success = report.total_coverage >= 80.0
116
+ confidence = min(report.total_coverage / 100.0, 1.0)
117
+
118
+ elif agent.id == "code_reviewer" or "quality" in agent.role.lower():
119
+ analyzer = RealCodeQualityAnalyzer(project_root)
120
+ report = analyzer.analyze(target_path)
121
+
122
+ output = {
123
+ "agent_role": agent.role,
124
+ "quality_score": report.quality_score,
125
+ "ruff_issues": report.ruff_issues,
126
+ "mypy_issues": report.mypy_issues,
127
+ "total_files": report.total_files,
128
+ "passed": report.passed,
129
+ }
130
+ success = report.passed
131
+ confidence = report.quality_score / 10.0
132
+
133
+ elif agent.id == "documentation_writer" or "documentation" in agent.role.lower():
134
+ analyzer = RealDocumentationAnalyzer(project_root)
135
+ report = analyzer.analyze(target_path)
136
+
137
+ output = {
138
+ "agent_role": agent.role,
139
+ "completeness": report.completeness_percentage,
140
+ "coverage_percent": report.completeness_percentage, # Match Release Prep field name
141
+ "total_functions": report.total_functions,
142
+ "documented_functions": report.documented_functions,
143
+ "total_classes": report.total_classes,
144
+ "documented_classes": report.documented_classes,
145
+ "missing_docstrings": report.missing_docstrings,
146
+ "passed": report.passed,
147
+ }
148
+ success = report.passed
149
+ confidence = report.completeness_percentage / 100.0
150
+
151
+ elif agent.id == "performance_optimizer" or "performance" in agent.role.lower():
152
+ # Performance analysis placeholder - mark as passed for now
153
+ # TODO: Implement real performance profiling
154
+ logger.warning("Performance analysis not yet implemented, returning placeholder")
155
+ output = {
156
+ "agent_role": agent.role,
157
+ "message": "Performance analysis not yet implemented",
158
+ "passed": True,
159
+ "placeholder": True,
160
+ }
161
+ success = True
162
+ confidence = 1.0
163
+
164
+ elif agent.id == "test_generator":
165
+ # Test generation requires different handling (LLM-based)
166
+ logger.info("Test generation requires manual invocation, returning placeholder")
167
+ output = {
168
+ "agent_role": agent.role,
169
+ "message": "Test generation requires manual invocation",
170
+ "passed": True,
171
+ }
172
+ success = True
173
+ confidence = 0.8
174
+
175
+ else:
176
+ # Unknown agent type - log warning and return placeholder
177
+ logger.warning(f"Unknown agent type: {agent.id}, returning placeholder")
178
+ output = {
179
+ "agent_role": agent.role,
180
+ "agent_id": agent.id,
181
+ "message": "Unknown agent type - no real implementation",
182
+ "passed": True,
183
+ }
184
+ success = True
185
+ confidence = 0.5
186
+
187
+ duration = time.perf_counter() - start_time
188
+
189
+ logger.info(
190
+ f"Agent {agent.id} completed: success={success}, "
191
+ f"confidence={confidence:.2f}, duration={duration:.2f}s"
192
+ )
193
+
194
+ return AgentResult(
195
+ agent_id=agent.id,
196
+ success=success,
197
+ output=output,
198
+ confidence=confidence,
199
+ duration_seconds=duration,
200
+ )
201
+
202
+ except Exception as e:
203
+ duration = time.perf_counter() - start_time
204
+ logger.error(f"Agent {agent.id} failed: {e}")
205
+
206
+ return AgentResult(
207
+ agent_id=agent.id,
208
+ success=False,
209
+ output={"agent_role": agent.role, "error_details": str(e)},
210
+ error=str(e),
211
+ confidence=0.0,
212
+ duration_seconds=duration,
213
+ )
214
+
215
+ def _aggregate_results(self, results: list[AgentResult]) -> dict[str, Any]:
216
+ """Aggregate results from multiple agents.
217
+
218
+ Args:
219
+ results: List of agent results
220
+
221
+ Returns:
222
+ Aggregated output dictionary
223
+ """
224
+ return {
225
+ "num_agents": len(results),
226
+ "all_succeeded": all(r.success for r in results),
227
+ "avg_confidence": (
228
+ sum(r.confidence for r in results) / len(results) if results else 0.0
229
+ ),
230
+ "outputs": [r.output for r in results],
231
+ }
@@ -0,0 +1,373 @@
1
+ """Conditional and nested execution strategies.
2
+
3
+ This module contains strategies that implement conditional branching and
4
+ nested workflow composition:
5
+
6
+ 1. ConditionalStrategy - if/then/else branching based on gates
7
+ 2. MultiConditionalStrategy - switch/case pattern
8
+ 3. NestedStrategy - recursive workflow execution
9
+ 4. NestedSequentialStrategy - sequential steps with nested workflow support
10
+
11
+ Security:
12
+ - Condition predicates validated (no code execution)
13
+ - Cycle detection prevents infinite recursion
14
+ - Max depth limits enforced
15
+ - No eval() or exec() usage
16
+
17
+ Copyright 2025 Smart-AI-Memory
18
+ Licensed under Fair Source License 0.9
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+ from dataclasses import dataclass
25
+ from typing import TYPE_CHECKING, Any
26
+
27
+ from .base import ExecutionStrategy
28
+ from .conditions import Condition, ConditionEvaluator
29
+ from .data_classes import AgentResult, StrategyResult
30
+ from .nesting import (
31
+ NestingContext,
32
+ WorkflowReference,
33
+ get_workflow,
34
+ )
35
+
36
+ if TYPE_CHECKING:
37
+ from ..agent_templates import AgentTemplate
38
+ from .conditions import Branch
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ class ConditionalStrategy(ExecutionStrategy):
44
+ """Conditional branching (if X then A else B).
45
+
46
+ The 7th grammar rule enabling dynamic workflow decisions based on gates.
47
+
48
+ Use when:
49
+ - Quality gates determine next steps
50
+ - Error handling requires different paths
51
+ - Agent consensus affects workflow
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ condition: Condition,
57
+ then_branch: Branch,
58
+ else_branch: Branch | None = None,
59
+ ):
60
+ """Initialize conditional strategy."""
61
+ self.condition = condition
62
+ self.then_branch = then_branch
63
+ self.else_branch = else_branch
64
+ self.evaluator = ConditionEvaluator()
65
+
66
+ async def execute(
67
+ self, agents: list[AgentTemplate], context: dict[str, Any]
68
+ ) -> StrategyResult:
69
+ """Execute conditional branching."""
70
+ # Import here to avoid circular import
71
+ from . import get_strategy
72
+
73
+ logger.info(f"Conditional: Evaluating '{self.condition.description or 'condition'}'")
74
+
75
+ condition_met = self.evaluator.evaluate(self.condition, context)
76
+ logger.info(f"Conditional: Condition evaluated to {condition_met}")
77
+
78
+ if condition_met:
79
+ selected_branch = self.then_branch
80
+ branch_label = "then"
81
+ else:
82
+ if self.else_branch is None:
83
+ return StrategyResult(
84
+ success=True,
85
+ outputs=[],
86
+ aggregated_output={"branch_taken": None},
87
+ total_duration=0.0,
88
+ )
89
+ selected_branch = self.else_branch
90
+ branch_label = "else"
91
+
92
+ logger.info(f"Conditional: Taking '{branch_label}' branch")
93
+
94
+ branch_strategy = get_strategy(selected_branch.strategy)
95
+ branch_context = context.copy()
96
+ branch_context["_conditional"] = {"condition_met": condition_met, "branch": branch_label}
97
+
98
+ result = await branch_strategy.execute(selected_branch.agents, branch_context)
99
+ result.aggregated_output["_conditional"] = {
100
+ "condition_met": condition_met,
101
+ "branch_taken": branch_label,
102
+ }
103
+ return result
104
+
105
+
106
+ class MultiConditionalStrategy(ExecutionStrategy):
107
+ """Multiple conditional branches (switch/case pattern)."""
108
+
109
+ def __init__(
110
+ self,
111
+ conditions: list[tuple[Condition, Branch]],
112
+ default_branch: Branch | None = None,
113
+ ):
114
+ """Initialize multi-conditional strategy."""
115
+ self.conditions = conditions
116
+ self.default_branch = default_branch
117
+ self.evaluator = ConditionEvaluator()
118
+
119
+ async def execute(
120
+ self, agents: list[AgentTemplate], context: dict[str, Any]
121
+ ) -> StrategyResult:
122
+ """Execute multi-conditional branching."""
123
+ # Import here to avoid circular import
124
+ from . import get_strategy
125
+
126
+ for i, (condition, branch) in enumerate(self.conditions):
127
+ if self.evaluator.evaluate(condition, context):
128
+ logger.info(f"MultiConditional: Condition {i + 1} matched")
129
+ branch_strategy = get_strategy(branch.strategy)
130
+ result = await branch_strategy.execute(branch.agents, context)
131
+ result.aggregated_output["_matched_index"] = i
132
+ return result
133
+
134
+ if self.default_branch:
135
+ branch_strategy = get_strategy(self.default_branch.strategy)
136
+ return await branch_strategy.execute(self.default_branch.agents, context)
137
+
138
+ return StrategyResult(
139
+ success=True,
140
+ outputs=[],
141
+ aggregated_output={"reason": "No conditions matched"},
142
+ total_duration=0.0,
143
+ )
144
+
145
+
146
+ class NestedStrategy(ExecutionStrategy):
147
+ """Nested workflow execution (sentences within sentences).
148
+
149
+ Enables recursive composition where workflows invoke other workflows.
150
+ Implements the "subordinate clause" pattern in the grammar metaphor.
151
+
152
+ Features:
153
+ - Reference workflows by ID or define inline
154
+ - Configurable max depth (default: 3)
155
+ - Cycle detection prevents infinite recursion
156
+ - Full context inheritance from parent to child
157
+
158
+ Use when:
159
+ - Complex multi-stage pipelines need modular sub-workflows
160
+ - Reusable workflow components should be shared
161
+ - Hierarchical team structures (teams containing sub-teams)
162
+
163
+ Example:
164
+ >>> # Parent workflow with nested sub-workflow
165
+ >>> strategy = NestedStrategy(
166
+ ... workflow_ref=WorkflowReference(workflow_id="security-audit"),
167
+ ... max_depth=3
168
+ ... )
169
+ >>> result = await strategy.execute([], context)
170
+
171
+ Example (inline):
172
+ >>> strategy = NestedStrategy(
173
+ ... workflow_ref=WorkflowReference(
174
+ ... inline=InlineWorkflow(
175
+ ... agents=[analyzer, reviewer],
176
+ ... strategy="parallel"
177
+ ... )
178
+ ... )
179
+ ... )
180
+ """
181
+
182
+ def __init__(
183
+ self,
184
+ workflow_ref: WorkflowReference,
185
+ max_depth: int = NestingContext.DEFAULT_MAX_DEPTH,
186
+ ):
187
+ """Initialize nested strategy.
188
+
189
+ Args:
190
+ workflow_ref: Reference to workflow (by ID or inline)
191
+ max_depth: Maximum nesting depth allowed
192
+ """
193
+ self.workflow_ref = workflow_ref
194
+ self.max_depth = max_depth
195
+
196
+ async def execute(
197
+ self, agents: list[AgentTemplate], context: dict[str, Any]
198
+ ) -> StrategyResult:
199
+ """Execute nested workflow.
200
+
201
+ Args:
202
+ agents: Ignored (workflow_ref defines agents)
203
+ context: Parent execution context (inherited by child)
204
+
205
+ Returns:
206
+ StrategyResult from nested workflow execution
207
+
208
+ Raises:
209
+ RecursionError: If max depth exceeded or cycle detected
210
+ """
211
+ # Import here to avoid circular import
212
+ from . import get_strategy
213
+
214
+ # Get or create nesting context
215
+ nesting = NestingContext.from_context(context)
216
+
217
+ # Resolve workflow
218
+ if self.workflow_ref.workflow_id:
219
+ workflow_id = self.workflow_ref.workflow_id
220
+ workflow = get_workflow(workflow_id)
221
+ workflow_agents = workflow.agents
222
+ strategy_name = workflow.strategy
223
+ else:
224
+ workflow_id = f"inline_{id(self.workflow_ref.inline)}"
225
+ workflow_agents = self.workflow_ref.inline.agents
226
+ strategy_name = self.workflow_ref.inline.strategy
227
+
228
+ # Check nesting limits
229
+ if not nesting.can_nest(workflow_id):
230
+ if nesting.current_depth >= nesting.max_depth:
231
+ error_msg = (
232
+ f"Maximum nesting depth ({nesting.max_depth}) exceeded. "
233
+ f"Current stack: {' → '.join(nesting.workflow_stack)}"
234
+ )
235
+ else:
236
+ error_msg = (
237
+ f"Cycle detected: workflow '{workflow_id}' already in stack. "
238
+ f"Stack: {' → '.join(nesting.workflow_stack)}"
239
+ )
240
+ logger.error(error_msg)
241
+ raise RecursionError(error_msg)
242
+
243
+ logger.info(f"Nested: Entering '{workflow_id}' at depth {nesting.current_depth + 1}")
244
+
245
+ # Create child context with updated nesting
246
+ child_nesting = nesting.enter(workflow_id)
247
+ child_context = child_nesting.to_context(context.copy())
248
+
249
+ # Execute nested workflow
250
+ strategy = get_strategy(strategy_name)
251
+ result = await strategy.execute(workflow_agents, child_context)
252
+
253
+ # Augment result with nesting metadata
254
+ result.aggregated_output["_nested"] = {
255
+ "workflow_id": workflow_id,
256
+ "depth": child_nesting.current_depth,
257
+ "parent_stack": nesting.workflow_stack,
258
+ }
259
+
260
+ # Store result under specified key if provided
261
+ if self.workflow_ref.result_key:
262
+ result.aggregated_output[self.workflow_ref.result_key] = result.aggregated_output.copy()
263
+
264
+ logger.info(f"Nested: Exiting '{workflow_id}'")
265
+
266
+ return result
267
+
268
+
269
+ @dataclass
270
+ class StepDefinition:
271
+ """Definition of a step in NestedSequentialStrategy.
272
+
273
+ Either agent OR workflow_ref must be provided (mutually exclusive).
274
+
275
+ Attributes:
276
+ agent: Agent to execute directly
277
+ workflow_ref: Nested workflow to execute
278
+ """
279
+
280
+ agent: AgentTemplate | None = None
281
+ workflow_ref: WorkflowReference | None = None
282
+
283
+ def __post_init__(self):
284
+ """Validate that exactly one step type is provided."""
285
+ if bool(self.agent) == bool(self.workflow_ref):
286
+ raise ValueError("StepDefinition must have exactly one of: agent or workflow_ref")
287
+
288
+
289
+ class NestedSequentialStrategy(ExecutionStrategy):
290
+ """Sequential execution with nested workflow support.
291
+
292
+ Like SequentialStrategy but steps can be either agents OR workflow references.
293
+ Enables mixing direct agent execution with nested sub-workflows.
294
+
295
+ Example:
296
+ >>> strategy = NestedSequentialStrategy(
297
+ ... steps=[
298
+ ... StepDefinition(agent=analyzer),
299
+ ... StepDefinition(workflow_ref=WorkflowReference(workflow_id="review-team")),
300
+ ... StepDefinition(agent=reporter),
301
+ ... ]
302
+ ... )
303
+ """
304
+
305
+ def __init__(
306
+ self,
307
+ steps: list[StepDefinition],
308
+ max_depth: int = NestingContext.DEFAULT_MAX_DEPTH,
309
+ ):
310
+ """Initialize nested sequential strategy.
311
+
312
+ Args:
313
+ steps: List of step definitions (agents or workflow refs)
314
+ max_depth: Maximum nesting depth
315
+ """
316
+ self.steps = steps
317
+ self.max_depth = max_depth
318
+
319
+ async def execute(
320
+ self, agents: list[AgentTemplate], context: dict[str, Any]
321
+ ) -> StrategyResult:
322
+ """Execute steps sequentially, handling both agents and nested workflows."""
323
+ if not self.steps:
324
+ raise ValueError("steps list cannot be empty")
325
+
326
+ logger.info(f"NestedSequential: Executing {len(self.steps)} steps")
327
+
328
+ results: list[AgentResult] = []
329
+ current_context = context.copy()
330
+ total_duration = 0.0
331
+
332
+ for i, step in enumerate(self.steps):
333
+ logger.info(f"NestedSequential: Step {i + 1}/{len(self.steps)}")
334
+
335
+ if step.agent:
336
+ # Direct agent execution
337
+ result = await self._execute_agent(step.agent, current_context)
338
+ results.append(result)
339
+ total_duration += result.duration_seconds
340
+
341
+ if result.success:
342
+ current_context[f"{step.agent.id}_output"] = result.output
343
+ else:
344
+ # Nested workflow execution
345
+ nested_strategy = NestedStrategy(
346
+ workflow_ref=step.workflow_ref,
347
+ max_depth=self.max_depth,
348
+ )
349
+ nested_result = await nested_strategy.execute([], current_context)
350
+ total_duration += nested_result.total_duration
351
+
352
+ # Convert to AgentResult for consistency
353
+ results.append(
354
+ AgentResult(
355
+ agent_id=f"nested_{step.workflow_ref.workflow_id or 'inline'}",
356
+ success=nested_result.success,
357
+ output=nested_result.aggregated_output,
358
+ confidence=nested_result.aggregated_output.get("avg_confidence", 0.0),
359
+ duration_seconds=nested_result.total_duration,
360
+ )
361
+ )
362
+
363
+ if nested_result.success:
364
+ key = step.workflow_ref.result_key or f"step_{i}_output"
365
+ current_context[key] = nested_result.aggregated_output
366
+
367
+ return StrategyResult(
368
+ success=all(r.success for r in results),
369
+ outputs=results,
370
+ aggregated_output=self._aggregate_results(results),
371
+ total_duration=total_duration,
372
+ errors=[r.error for r in results if not r.success],
373
+ )