aury-agent 0.0.4__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 (149) hide show
  1. aury/__init__.py +2 -0
  2. aury/agents/__init__.py +55 -0
  3. aury/agents/a2a/__init__.py +168 -0
  4. aury/agents/backends/__init__.py +196 -0
  5. aury/agents/backends/artifact/__init__.py +9 -0
  6. aury/agents/backends/artifact/memory.py +130 -0
  7. aury/agents/backends/artifact/types.py +133 -0
  8. aury/agents/backends/code/__init__.py +65 -0
  9. aury/agents/backends/file/__init__.py +11 -0
  10. aury/agents/backends/file/local.py +66 -0
  11. aury/agents/backends/file/types.py +40 -0
  12. aury/agents/backends/invocation/__init__.py +8 -0
  13. aury/agents/backends/invocation/memory.py +81 -0
  14. aury/agents/backends/invocation/types.py +110 -0
  15. aury/agents/backends/memory/__init__.py +8 -0
  16. aury/agents/backends/memory/memory.py +179 -0
  17. aury/agents/backends/memory/types.py +136 -0
  18. aury/agents/backends/message/__init__.py +9 -0
  19. aury/agents/backends/message/memory.py +122 -0
  20. aury/agents/backends/message/types.py +124 -0
  21. aury/agents/backends/sandbox.py +275 -0
  22. aury/agents/backends/session/__init__.py +8 -0
  23. aury/agents/backends/session/memory.py +93 -0
  24. aury/agents/backends/session/types.py +124 -0
  25. aury/agents/backends/shell/__init__.py +11 -0
  26. aury/agents/backends/shell/local.py +110 -0
  27. aury/agents/backends/shell/types.py +55 -0
  28. aury/agents/backends/shell.py +209 -0
  29. aury/agents/backends/snapshot/__init__.py +19 -0
  30. aury/agents/backends/snapshot/git.py +95 -0
  31. aury/agents/backends/snapshot/hybrid.py +125 -0
  32. aury/agents/backends/snapshot/memory.py +86 -0
  33. aury/agents/backends/snapshot/types.py +59 -0
  34. aury/agents/backends/state/__init__.py +29 -0
  35. aury/agents/backends/state/composite.py +49 -0
  36. aury/agents/backends/state/file.py +57 -0
  37. aury/agents/backends/state/memory.py +52 -0
  38. aury/agents/backends/state/sqlite.py +262 -0
  39. aury/agents/backends/state/types.py +178 -0
  40. aury/agents/backends/subagent/__init__.py +165 -0
  41. aury/agents/cli/__init__.py +41 -0
  42. aury/agents/cli/chat.py +239 -0
  43. aury/agents/cli/config.py +236 -0
  44. aury/agents/cli/extensions.py +460 -0
  45. aury/agents/cli/main.py +189 -0
  46. aury/agents/cli/session.py +337 -0
  47. aury/agents/cli/workflow.py +276 -0
  48. aury/agents/context_providers/__init__.py +66 -0
  49. aury/agents/context_providers/artifact.py +299 -0
  50. aury/agents/context_providers/base.py +177 -0
  51. aury/agents/context_providers/memory.py +70 -0
  52. aury/agents/context_providers/message.py +130 -0
  53. aury/agents/context_providers/skill.py +50 -0
  54. aury/agents/context_providers/subagent.py +46 -0
  55. aury/agents/context_providers/tool.py +68 -0
  56. aury/agents/core/__init__.py +83 -0
  57. aury/agents/core/base.py +573 -0
  58. aury/agents/core/context.py +797 -0
  59. aury/agents/core/context_builder.py +303 -0
  60. aury/agents/core/event_bus/__init__.py +15 -0
  61. aury/agents/core/event_bus/bus.py +203 -0
  62. aury/agents/core/factory.py +169 -0
  63. aury/agents/core/isolator.py +97 -0
  64. aury/agents/core/logging.py +95 -0
  65. aury/agents/core/parallel.py +194 -0
  66. aury/agents/core/runner.py +139 -0
  67. aury/agents/core/services/__init__.py +5 -0
  68. aury/agents/core/services/file_session.py +144 -0
  69. aury/agents/core/services/message.py +53 -0
  70. aury/agents/core/services/session.py +53 -0
  71. aury/agents/core/signals.py +109 -0
  72. aury/agents/core/state.py +363 -0
  73. aury/agents/core/types/__init__.py +107 -0
  74. aury/agents/core/types/action.py +176 -0
  75. aury/agents/core/types/artifact.py +135 -0
  76. aury/agents/core/types/block.py +736 -0
  77. aury/agents/core/types/message.py +350 -0
  78. aury/agents/core/types/recall.py +144 -0
  79. aury/agents/core/types/session.py +257 -0
  80. aury/agents/core/types/subagent.py +154 -0
  81. aury/agents/core/types/tool.py +205 -0
  82. aury/agents/eval/__init__.py +331 -0
  83. aury/agents/hitl/__init__.py +57 -0
  84. aury/agents/hitl/ask_user.py +242 -0
  85. aury/agents/hitl/compaction.py +230 -0
  86. aury/agents/hitl/exceptions.py +87 -0
  87. aury/agents/hitl/permission.py +617 -0
  88. aury/agents/hitl/revert.py +216 -0
  89. aury/agents/llm/__init__.py +31 -0
  90. aury/agents/llm/adapter.py +367 -0
  91. aury/agents/llm/openai.py +294 -0
  92. aury/agents/llm/provider.py +476 -0
  93. aury/agents/mcp/__init__.py +153 -0
  94. aury/agents/memory/__init__.py +46 -0
  95. aury/agents/memory/compaction.py +394 -0
  96. aury/agents/memory/manager.py +465 -0
  97. aury/agents/memory/processor.py +177 -0
  98. aury/agents/memory/store.py +187 -0
  99. aury/agents/memory/types.py +137 -0
  100. aury/agents/messages/__init__.py +40 -0
  101. aury/agents/messages/config.py +47 -0
  102. aury/agents/messages/raw_store.py +224 -0
  103. aury/agents/messages/store.py +118 -0
  104. aury/agents/messages/types.py +88 -0
  105. aury/agents/middleware/__init__.py +31 -0
  106. aury/agents/middleware/base.py +341 -0
  107. aury/agents/middleware/chain.py +342 -0
  108. aury/agents/middleware/message.py +129 -0
  109. aury/agents/middleware/message_container.py +126 -0
  110. aury/agents/middleware/raw_message.py +153 -0
  111. aury/agents/middleware/truncation.py +139 -0
  112. aury/agents/middleware/types.py +81 -0
  113. aury/agents/plugin.py +162 -0
  114. aury/agents/react/__init__.py +4 -0
  115. aury/agents/react/agent.py +1923 -0
  116. aury/agents/sandbox/__init__.py +23 -0
  117. aury/agents/sandbox/local.py +239 -0
  118. aury/agents/sandbox/remote.py +200 -0
  119. aury/agents/sandbox/types.py +115 -0
  120. aury/agents/skill/__init__.py +16 -0
  121. aury/agents/skill/loader.py +180 -0
  122. aury/agents/skill/types.py +83 -0
  123. aury/agents/tool/__init__.py +39 -0
  124. aury/agents/tool/builtin/__init__.py +23 -0
  125. aury/agents/tool/builtin/ask_user.py +155 -0
  126. aury/agents/tool/builtin/bash.py +107 -0
  127. aury/agents/tool/builtin/delegate.py +726 -0
  128. aury/agents/tool/builtin/edit.py +121 -0
  129. aury/agents/tool/builtin/plan.py +277 -0
  130. aury/agents/tool/builtin/read.py +91 -0
  131. aury/agents/tool/builtin/thinking.py +111 -0
  132. aury/agents/tool/builtin/yield_result.py +130 -0
  133. aury/agents/tool/decorator.py +252 -0
  134. aury/agents/tool/set.py +204 -0
  135. aury/agents/usage/__init__.py +12 -0
  136. aury/agents/usage/tracker.py +236 -0
  137. aury/agents/workflow/__init__.py +85 -0
  138. aury/agents/workflow/adapter.py +268 -0
  139. aury/agents/workflow/dag.py +116 -0
  140. aury/agents/workflow/dsl.py +575 -0
  141. aury/agents/workflow/executor.py +659 -0
  142. aury/agents/workflow/expression.py +136 -0
  143. aury/agents/workflow/parser.py +182 -0
  144. aury/agents/workflow/state.py +145 -0
  145. aury/agents/workflow/types.py +86 -0
  146. aury_agent-0.0.4.dist-info/METADATA +90 -0
  147. aury_agent-0.0.4.dist-info/RECORD +149 -0
  148. aury_agent-0.0.4.dist-info/WHEEL +4 -0
  149. aury_agent-0.0.4.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,303 @@
1
+ """Context builder for LLM communication.
2
+
3
+ ContextBuilder constructs the full context to send to LLM, combining:
4
+ - System prompt
5
+ - Knowledge (user-defined, via Middleware)
6
+ - Summary (compressed history)
7
+ - Recalls (session key points)
8
+ - Recent messages
9
+ - Current input
10
+
11
+ Context is runtime-built, not stored.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass, field
17
+ from typing import Any, Protocol, TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING:
20
+ from .types.message import Message, PromptInput
21
+ from .types.recall import Recall, Summary
22
+ from ..backends.state import StateBackend
23
+
24
+
25
+ @dataclass
26
+ class ContextConfig:
27
+ """Configuration for context building."""
28
+
29
+ # Token limits
30
+ max_tokens: int = 8000
31
+
32
+ # Message control
33
+ max_recent_messages: int = 50
34
+
35
+ # Memory control
36
+ include_summary: bool = True
37
+ include_recalls: bool = True
38
+ recall_limit: int = 20
39
+
40
+ # Knowledge control (user-defined via Middleware)
41
+ include_knowledge: bool = True
42
+ knowledge_limit: int = 10
43
+
44
+ # Compression
45
+ enable_compression: bool = True
46
+ compression_threshold: int = 6000
47
+
48
+
49
+ @dataclass
50
+ class LLMContext:
51
+ """Context prepared for LLM.
52
+
53
+ This is the final object sent to LLM, not stored.
54
+ """
55
+
56
+ # Final messages (LLM API format)
57
+ messages: list[dict[str, Any]] = field(default_factory=list)
58
+
59
+ # Sources (for debugging/tracing)
60
+ source_summary: "Summary | None" = None
61
+ source_recalls: list["Recall"] = field(default_factory=list)
62
+ source_knowledge: list[dict[str, Any]] = field(default_factory=list) # User-defined structure
63
+ source_messages: list["Message"] = field(default_factory=list)
64
+
65
+ # Token stats
66
+ estimated_tokens: int = 0
67
+
68
+ def to_llm_messages(self) -> list[dict[str, Any]]:
69
+ """Get messages in LLM API format."""
70
+ return self.messages
71
+
72
+
73
+ class ContextBuilder(Protocol):
74
+ """Protocol for context building.
75
+
76
+ Users can implement custom builders for specialized context management.
77
+ """
78
+
79
+ async def build(
80
+ self,
81
+ session_id: str,
82
+ invocation_id: str,
83
+ current_input: "PromptInput",
84
+ branch: str | None = None,
85
+ config: ContextConfig | None = None,
86
+ ) -> LLMContext:
87
+ """Build context for LLM.
88
+
89
+ Args:
90
+ session_id: Current session ID
91
+ invocation_id: Current invocation ID
92
+ current_input: User input for this turn
93
+ branch: SubAgent branch (for isolation)
94
+ config: Context configuration
95
+
96
+ Returns:
97
+ LLMContext ready for LLM API call
98
+ """
99
+ ...
100
+
101
+
102
+ class DefaultContextBuilder:
103
+ """Default implementation of context builder.
104
+
105
+ Build order:
106
+ 1. System prompt
107
+ 2. Knowledge (injected via Middleware)
108
+ 3. Summary (compressed history)
109
+ 4. Recalls (key points)
110
+ 5. Recent messages
111
+ 6. Current input
112
+ """
113
+
114
+ def __init__(
115
+ self,
116
+ storage: "StateBackend",
117
+ system_prompt: str | None = None,
118
+ ):
119
+ """Initialize context builder.
120
+
121
+ Args:
122
+ storage: State backend for loading messages/recalls
123
+ system_prompt: System prompt template (can include {var} for state.vars)
124
+ """
125
+ self._storage = storage
126
+ self._system_prompt = system_prompt
127
+
128
+ async def build(
129
+ self,
130
+ session_id: str,
131
+ invocation_id: str,
132
+ current_input: "PromptInput",
133
+ branch: str | None = None,
134
+ config: ContextConfig | None = None,
135
+ *,
136
+ state_vars: dict[str, Any] | None = None,
137
+ ) -> LLMContext:
138
+ """Build context for LLM.
139
+
140
+ Args:
141
+ session_id: Current session ID
142
+ invocation_id: Current invocation ID
143
+ current_input: User input for this turn
144
+ branch: SubAgent branch (for isolation)
145
+ config: Context configuration
146
+ state_vars: Variables for prompt formatting (from state.vars)
147
+ """
148
+ from .types.message import Message, MessageRole
149
+ from .types.recall import Recall, Summary
150
+
151
+ cfg = config or ContextConfig()
152
+ context = LLMContext()
153
+ messages: list[dict[str, Any]] = []
154
+
155
+ # 1. System prompt
156
+ if self._system_prompt:
157
+ system_text = self._system_prompt
158
+ if state_vars:
159
+ try:
160
+ system_text = system_text.format(**state_vars)
161
+ except KeyError:
162
+ pass # Ignore missing vars
163
+ messages.append(
164
+ {
165
+ "role": "system",
166
+ "content": system_text,
167
+ }
168
+ )
169
+
170
+ # 2. Summary (compressed history)
171
+ if cfg.include_summary:
172
+ summary = await self._load_summary(session_id)
173
+ if summary:
174
+ context.source_summary = summary
175
+ messages.append(
176
+ {
177
+ "role": "system",
178
+ "content": f"[Previous Conversation Summary]\n{summary.content}",
179
+ }
180
+ )
181
+
182
+ # 3. Recalls (key points)
183
+ if cfg.include_recalls:
184
+ recalls = await self._load_recalls(session_id, branch, cfg.recall_limit)
185
+ if recalls:
186
+ context.source_recalls = recalls
187
+ recalls_text = "\n".join([f"- {r.content}" for r in recalls])
188
+ messages.append(
189
+ {
190
+ "role": "system",
191
+ "content": f"[Key Information]\n{recalls_text}",
192
+ }
193
+ )
194
+
195
+ # 4. Recent messages
196
+ recent_messages = await self._load_messages(
197
+ session_id,
198
+ branch,
199
+ cfg.max_recent_messages,
200
+ )
201
+ context.source_messages = recent_messages
202
+
203
+ for msg in recent_messages:
204
+ llm_msg = msg.to_llm_format()
205
+ messages.append(llm_msg)
206
+
207
+ # 5. Current input
208
+ input_msg = current_input.to_message(session_id, invocation_id)
209
+ messages.append(input_msg.to_llm_format())
210
+
211
+ context.messages = messages
212
+ context.estimated_tokens = self._estimate_tokens(messages)
213
+
214
+ return context
215
+
216
+ async def _load_summary(self, session_id: str) -> "Summary | None":
217
+ """Load session summary."""
218
+ from .types.recall import Summary
219
+
220
+ data = await self._storage.get("summaries", session_id)
221
+ if data:
222
+ return Summary.from_dict(data)
223
+ return None
224
+
225
+ async def _load_recalls(
226
+ self,
227
+ session_id: str,
228
+ branch: str | None,
229
+ limit: int,
230
+ ) -> list["Recall"]:
231
+ """Load session recalls."""
232
+ from .types.recall import Recall
233
+
234
+ # Load all recalls for session
235
+ keys = await self._storage.list("recalls", prefix=session_id)
236
+ recalls: list[Recall] = []
237
+
238
+ for key in keys[: limit * 2]: # Load extra to filter by branch
239
+ data = await self._storage.get("recalls", key)
240
+ if data:
241
+ recall = Recall.from_dict(data)
242
+ # Filter by branch
243
+ if branch is None or recall.branch is None or recall.branch == branch:
244
+ recalls.append(recall)
245
+ if len(recalls) >= limit:
246
+ break
247
+
248
+ # Sort by importance
249
+ recalls.sort(key=lambda r: r.importance, reverse=True)
250
+ return recalls[:limit]
251
+
252
+ async def _load_messages(
253
+ self,
254
+ session_id: str,
255
+ branch: str | None,
256
+ limit: int,
257
+ ) -> list["Message"]:
258
+ """Load recent messages."""
259
+ from .types.message import Message
260
+
261
+ # Load all message keys for session
262
+ keys = await self._storage.list("messages", prefix=session_id)
263
+ messages: list[Message] = []
264
+
265
+ # Load in reverse (most recent first)
266
+ for key in reversed(keys):
267
+ if len(messages) >= limit:
268
+ break
269
+
270
+ data = await self._storage.get("messages", key)
271
+ if data:
272
+ msg = Message.from_dict(data)
273
+ # Filter by branch
274
+ if branch is None or msg.branch is None or msg.branch == branch:
275
+ messages.append(msg)
276
+
277
+ # Reverse to chronological order
278
+ messages.reverse()
279
+ return messages
280
+
281
+ def _estimate_tokens(self, messages: list[dict[str, Any]]) -> int:
282
+ """Estimate token count (rough approximation)."""
283
+ total_chars = 0
284
+ for msg in messages:
285
+ content = msg.get("content", "")
286
+ if isinstance(content, str):
287
+ total_chars += len(content)
288
+ elif isinstance(content, list):
289
+ for part in content:
290
+ if isinstance(part, dict):
291
+ text = part.get("text", "") or part.get("content", "")
292
+ total_chars += len(str(text))
293
+
294
+ # Rough estimate: 4 chars per token
295
+ return total_chars // 4
296
+
297
+
298
+ __all__ = [
299
+ "ContextConfig",
300
+ "LLMContext",
301
+ "ContextBuilder",
302
+ "DefaultContextBuilder",
303
+ ]
@@ -0,0 +1,15 @@
1
+ """Event bus for pub/sub messaging."""
2
+ from .bus import (
3
+ Events,
4
+ EventHandler,
5
+ EventBus,
6
+ EventCollector,
7
+ )
8
+
9
+
10
+ __all__ = [
11
+ "Events",
12
+ "EventHandler",
13
+ "EventBus",
14
+ "EventCollector",
15
+ ]
@@ -0,0 +1,203 @@
1
+ """Event bus for pub/sub messaging."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ from typing import Any, Callable, Awaitable
6
+ from enum import Enum
7
+
8
+ from ..logging import bus_logger as logger
9
+
10
+
11
+ class Events:
12
+ """Predefined event types."""
13
+
14
+ # Session lifecycle
15
+ SESSION_CREATED = "session.created"
16
+ SESSION_UPDATED = "session.updated"
17
+ SESSION_ENDED = "session.ended"
18
+
19
+ # Invocation lifecycle
20
+ INVOCATION_START = "invocation.start"
21
+ INVOCATION_END = "invocation.end"
22
+ INVOCATION_ERROR = "invocation.error"
23
+ INVOCATION_CANCELLED = "invocation.cancelled"
24
+
25
+ # Unified block event (used by ctx.emit())
26
+ BLOCK = "block" # Main streaming event for all block outputs
27
+
28
+ # Block lifecycle events (for persistence/logging)
29
+ BLOCK_CREATED = "block.created"
30
+ BLOCK_UPDATED = "block.updated"
31
+ BLOCK_DELTA = "block.delta"
32
+ BLOCK_CLOSED = "block.closed"
33
+
34
+ # Tool events
35
+ TOOL_START = "tool.start"
36
+ TOOL_END = "tool.end"
37
+ TOOL_ERROR = "tool.error"
38
+
39
+ # Workflow node events
40
+ NODE_START = "node.start"
41
+ NODE_END = "node.end"
42
+ NODE_ERROR = "node.error"
43
+ NODE_SKIPPED = "node.skipped"
44
+
45
+ # LLM events
46
+ LLM_START = "llm.start"
47
+ LLM_END = "llm.end"
48
+ LLM_STREAM = "llm.stream"
49
+
50
+ # Memory events
51
+ MEMORY_ADD = "memory.add"
52
+ MEMORY_SEARCH = "memory.search"
53
+
54
+ # Usage events
55
+ USAGE_RECORDED = "usage.recorded"
56
+
57
+ # Permission events
58
+ PERMISSION_REQUESTED = "permission.requested"
59
+ PERMISSION_RESOLVED = "permission.resolved"
60
+
61
+ # State events
62
+ STATE_CHANGED = "state.changed"
63
+ STATE_REVERTED = "state.reverted"
64
+
65
+
66
+ EventHandler = Callable[[str, Any], Awaitable[None]] | Callable[[str, Any], None]
67
+
68
+
69
+ class EventBus:
70
+ """Event bus with pub/sub support.
71
+
72
+ Features:
73
+ - Async and sync handlers
74
+ - Wildcard subscription with "*"
75
+ - Handler errors don't block other handlers
76
+ """
77
+
78
+ def __init__(self) -> None:
79
+ self._subscriptions: dict[str, list[EventHandler]] = {}
80
+ self._lock = asyncio.Lock()
81
+
82
+ async def publish(self, event_type: str, payload: Any) -> None:
83
+ """Publish event to all subscribers.
84
+
85
+ Args:
86
+ event_type: Event type string
87
+ payload: Event payload (any data)
88
+ """
89
+ handlers: list[EventHandler] = []
90
+
91
+ async with self._lock:
92
+ # Exact match
93
+ handlers.extend(self._subscriptions.get(event_type, []))
94
+ # Wildcard match
95
+ handlers.extend(self._subscriptions.get("*", []))
96
+
97
+ logger.debug("Publishing event", extra={"event_type": event_type})
98
+
99
+ for handler in handlers:
100
+ try:
101
+ if asyncio.iscoroutinefunction(handler):
102
+ await handler(event_type, payload)
103
+ else:
104
+ handler(event_type, payload)
105
+ except Exception as e:
106
+ logger.error(
107
+ "Event handler error",
108
+ extra={"event_type": event_type, "error": str(e)},
109
+ exc_info=True,
110
+ )
111
+
112
+ def subscribe(self, event_type: str, handler: EventHandler) -> Callable[[], None]:
113
+ """Subscribe to event type.
114
+
115
+ Args:
116
+ event_type: Event type to subscribe to, or "*" for all
117
+ handler: Callback function (async or sync)
118
+
119
+ Returns:
120
+ Unsubscribe function
121
+ """
122
+ if event_type not in self._subscriptions:
123
+ self._subscriptions[event_type] = []
124
+
125
+ self._subscriptions[event_type].append(handler)
126
+
127
+ def unsubscribe() -> None:
128
+ if event_type in self._subscriptions:
129
+ try:
130
+ self._subscriptions[event_type].remove(handler)
131
+ except ValueError:
132
+ pass
133
+
134
+ return unsubscribe
135
+
136
+ def on(self, event_type: str) -> Callable[[EventHandler], EventHandler]:
137
+ """Decorator for subscribing to events.
138
+
139
+ Usage:
140
+ @bus.on("session.created")
141
+ async def handler(event_type, payload):
142
+ ...
143
+ """
144
+ def decorator(handler: EventHandler) -> EventHandler:
145
+ self.subscribe(event_type, handler)
146
+ return handler
147
+ return decorator
148
+
149
+ def clear(self) -> None:
150
+ """Clear all subscriptions."""
151
+ self._subscriptions.clear()
152
+
153
+ def subscriber_count(self, event_type: str | None = None) -> int:
154
+ """Get number of subscribers.
155
+
156
+ Args:
157
+ event_type: Specific event type, or None for total count
158
+ """
159
+ if event_type:
160
+ return len(self._subscriptions.get(event_type, []))
161
+ return sum(len(handlers) for handlers in self._subscriptions.values())
162
+
163
+
164
+ class EventCollector:
165
+ """Collects events for testing/debugging.
166
+
167
+ Usage:
168
+ collector = EventCollector(bus)
169
+ collector.start()
170
+ # ... do stuff ...
171
+ events = collector.stop()
172
+ """
173
+
174
+ def __init__(self, bus: EventBus, event_types: list[str] | None = None):
175
+ self.bus = bus
176
+ self.event_types = event_types # None means all
177
+ self.events: list[tuple[str, Any]] = []
178
+ self._unsubscribers: list[Callable[[], None]] = []
179
+
180
+ def _handler(self, event_type: str, payload: Any) -> None:
181
+ if self.event_types is None or event_type in self.event_types:
182
+ self.events.append((event_type, payload))
183
+
184
+ def start(self) -> None:
185
+ """Start collecting events."""
186
+ if self.event_types:
187
+ for event_type in self.event_types:
188
+ unsub = self.bus.subscribe(event_type, self._handler)
189
+ self._unsubscribers.append(unsub)
190
+ else:
191
+ unsub = self.bus.subscribe("*", self._handler)
192
+ self._unsubscribers.append(unsub)
193
+
194
+ def stop(self) -> list[tuple[str, Any]]:
195
+ """Stop collecting and return events."""
196
+ for unsub in self._unsubscribers:
197
+ unsub()
198
+ self._unsubscribers.clear()
199
+ return self.events
200
+
201
+ def clear(self) -> None:
202
+ """Clear collected events."""
203
+ self.events.clear()
@@ -0,0 +1,169 @@
1
+ """Agent factory for creating agent instances.
2
+
3
+ Provides unified creation of both ReactAgent and WorkflowAgent:
4
+ factory = AgentFactory()
5
+ factory.register("researcher", ResearcherAgent) # ReactAgent subclass
6
+ factory.register("pipeline", PipelineWorkflow) # WorkflowAgent subclass
7
+
8
+ agent = factory.create("researcher", ctx) # Works for both types
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, TYPE_CHECKING
13
+
14
+ from .base import AgentConfig
15
+
16
+ if TYPE_CHECKING:
17
+ from .base import BaseAgent
18
+ from .context import InvocationContext
19
+
20
+
21
+ class AgentFactory:
22
+ """Factory for creating agent instances.
23
+
24
+ Unified factory for both ReactAgent and WorkflowAgent.
25
+ All agents use the same constructor:
26
+ __init__(self, ctx: InvocationContext, config: AgentConfig | None = None)
27
+
28
+ Usage:
29
+ factory = AgentFactory()
30
+
31
+ # Register agent classes
32
+ factory.register("researcher", ResearcherAgent)
33
+ factory.register("coder", CoderAgent)
34
+ factory.register("pipeline", PipelineWorkflow)
35
+
36
+ # Create agents
37
+ agent = factory.create("researcher", ctx)
38
+ agent = factory.create("researcher", ctx, config=custom_config)
39
+
40
+ # Auto-register from class
41
+ factory.register_class(ResearcherAgent) # Uses class.name
42
+ """
43
+
44
+ def __init__(self):
45
+ self._registry: dict[str, type["BaseAgent"]] = {}
46
+
47
+ def register(self, name: str, agent_class: type["BaseAgent"]) -> None:
48
+ """Register an agent class with a name.
49
+
50
+ Args:
51
+ name: Name to register under
52
+ agent_class: Agent class (must have unified constructor)
53
+ """
54
+ self._registry[name] = agent_class
55
+
56
+ def register_class(self, agent_class: type["BaseAgent"]) -> None:
57
+ """Register an agent class using its class-level name.
58
+
59
+ Args:
60
+ agent_class: Agent class with 'name' class attribute
61
+ """
62
+ name = getattr(agent_class, 'name', agent_class.__name__)
63
+ self._registry[name] = agent_class
64
+
65
+ def register_all(self, *agent_classes: type["BaseAgent"]) -> None:
66
+ """Register multiple agent classes.
67
+
68
+ Args:
69
+ agent_classes: Agent classes with 'name' class attribute
70
+ """
71
+ for agent_class in agent_classes:
72
+ self.register_class(agent_class)
73
+
74
+ def create(
75
+ self,
76
+ agent_type: str,
77
+ ctx: "InvocationContext",
78
+ config: AgentConfig | None = None,
79
+ ) -> "BaseAgent":
80
+ """Create an agent instance.
81
+
82
+ All agents are created with the same signature:
83
+ agent = AgentClass(ctx, config)
84
+
85
+ Args:
86
+ agent_type: Registered agent type name
87
+ ctx: InvocationContext with all services
88
+ config: Agent configuration (optional)
89
+
90
+ Returns:
91
+ Agent instance (ReactAgent or WorkflowAgent)
92
+
93
+ Raises:
94
+ KeyError: If agent type not registered
95
+ """
96
+ if agent_type not in self._registry:
97
+ available = ", ".join(self._registry.keys()) or "none"
98
+ raise KeyError(
99
+ f"Unknown agent type: {agent_type}. Available: {available}"
100
+ )
101
+
102
+ agent_class = self._registry[agent_type]
103
+ return agent_class(ctx, config)
104
+
105
+ def create_subagent(
106
+ self,
107
+ agent_type: str,
108
+ parent_ctx: "InvocationContext",
109
+ mode: str = "delegated",
110
+ config: AgentConfig | None = None,
111
+ ) -> "BaseAgent":
112
+ """Create a sub-agent with child context.
113
+
114
+ Convenience method that creates child context and agent.
115
+
116
+ Args:
117
+ agent_type: Registered agent type name
118
+ parent_ctx: Parent's InvocationContext
119
+ mode: Execution mode ('delegated' or 'embedded')
120
+ config: Agent configuration (optional)
121
+
122
+ Returns:
123
+ Agent instance with child context
124
+ """
125
+ child_ctx = parent_ctx.create_child(agent_id=agent_type, mode=mode)
126
+ return self.create(agent_type, child_ctx, config)
127
+
128
+ def list_types(self) -> list[str]:
129
+ """List registered agent types."""
130
+ return list(self._registry.keys())
131
+
132
+ def get_class(self, agent_type: str) -> type["BaseAgent"] | None:
133
+ """Get agent class by type name."""
134
+ return self._registry.get(agent_type)
135
+
136
+ def is_registered(self, agent_type: str) -> bool:
137
+ """Check if agent type is registered."""
138
+ return agent_type in self._registry
139
+
140
+ def get_info(self, agent_type: str) -> dict[str, Any] | None:
141
+ """Get agent info (name, description, type).
142
+
143
+ Returns:
144
+ Dict with name, description, agent_type, or None if not found
145
+ """
146
+ agent_class = self._registry.get(agent_type)
147
+ if agent_class is None:
148
+ return None
149
+
150
+ return {
151
+ "name": getattr(agent_class, 'name', agent_type),
152
+ "description": getattr(agent_class, 'description', ''),
153
+ "agent_type": getattr(agent_class, 'agent_type', 'react'),
154
+ "sub_agents": [
155
+ getattr(sa, 'name', sa.__name__)
156
+ for sa in getattr(agent_class, 'sub_agents', [])
157
+ ],
158
+ }
159
+
160
+ def list_info(self) -> list[dict[str, Any]]:
161
+ """List info for all registered agents."""
162
+ return [
163
+ self.get_info(name)
164
+ for name in self._registry.keys()
165
+ if self.get_info(name) is not None
166
+ ]
167
+
168
+
169
+ __all__ = ["AgentFactory"]