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.
- aury/__init__.py +2 -0
- aury/agents/__init__.py +55 -0
- aury/agents/a2a/__init__.py +168 -0
- aury/agents/backends/__init__.py +196 -0
- aury/agents/backends/artifact/__init__.py +9 -0
- aury/agents/backends/artifact/memory.py +130 -0
- aury/agents/backends/artifact/types.py +133 -0
- aury/agents/backends/code/__init__.py +65 -0
- aury/agents/backends/file/__init__.py +11 -0
- aury/agents/backends/file/local.py +66 -0
- aury/agents/backends/file/types.py +40 -0
- aury/agents/backends/invocation/__init__.py +8 -0
- aury/agents/backends/invocation/memory.py +81 -0
- aury/agents/backends/invocation/types.py +110 -0
- aury/agents/backends/memory/__init__.py +8 -0
- aury/agents/backends/memory/memory.py +179 -0
- aury/agents/backends/memory/types.py +136 -0
- aury/agents/backends/message/__init__.py +9 -0
- aury/agents/backends/message/memory.py +122 -0
- aury/agents/backends/message/types.py +124 -0
- aury/agents/backends/sandbox.py +275 -0
- aury/agents/backends/session/__init__.py +8 -0
- aury/agents/backends/session/memory.py +93 -0
- aury/agents/backends/session/types.py +124 -0
- aury/agents/backends/shell/__init__.py +11 -0
- aury/agents/backends/shell/local.py +110 -0
- aury/agents/backends/shell/types.py +55 -0
- aury/agents/backends/shell.py +209 -0
- aury/agents/backends/snapshot/__init__.py +19 -0
- aury/agents/backends/snapshot/git.py +95 -0
- aury/agents/backends/snapshot/hybrid.py +125 -0
- aury/agents/backends/snapshot/memory.py +86 -0
- aury/agents/backends/snapshot/types.py +59 -0
- aury/agents/backends/state/__init__.py +29 -0
- aury/agents/backends/state/composite.py +49 -0
- aury/agents/backends/state/file.py +57 -0
- aury/agents/backends/state/memory.py +52 -0
- aury/agents/backends/state/sqlite.py +262 -0
- aury/agents/backends/state/types.py +178 -0
- aury/agents/backends/subagent/__init__.py +165 -0
- aury/agents/cli/__init__.py +41 -0
- aury/agents/cli/chat.py +239 -0
- aury/agents/cli/config.py +236 -0
- aury/agents/cli/extensions.py +460 -0
- aury/agents/cli/main.py +189 -0
- aury/agents/cli/session.py +337 -0
- aury/agents/cli/workflow.py +276 -0
- aury/agents/context_providers/__init__.py +66 -0
- aury/agents/context_providers/artifact.py +299 -0
- aury/agents/context_providers/base.py +177 -0
- aury/agents/context_providers/memory.py +70 -0
- aury/agents/context_providers/message.py +130 -0
- aury/agents/context_providers/skill.py +50 -0
- aury/agents/context_providers/subagent.py +46 -0
- aury/agents/context_providers/tool.py +68 -0
- aury/agents/core/__init__.py +83 -0
- aury/agents/core/base.py +573 -0
- aury/agents/core/context.py +797 -0
- aury/agents/core/context_builder.py +303 -0
- aury/agents/core/event_bus/__init__.py +15 -0
- aury/agents/core/event_bus/bus.py +203 -0
- aury/agents/core/factory.py +169 -0
- aury/agents/core/isolator.py +97 -0
- aury/agents/core/logging.py +95 -0
- aury/agents/core/parallel.py +194 -0
- aury/agents/core/runner.py +139 -0
- aury/agents/core/services/__init__.py +5 -0
- aury/agents/core/services/file_session.py +144 -0
- aury/agents/core/services/message.py +53 -0
- aury/agents/core/services/session.py +53 -0
- aury/agents/core/signals.py +109 -0
- aury/agents/core/state.py +363 -0
- aury/agents/core/types/__init__.py +107 -0
- aury/agents/core/types/action.py +176 -0
- aury/agents/core/types/artifact.py +135 -0
- aury/agents/core/types/block.py +736 -0
- aury/agents/core/types/message.py +350 -0
- aury/agents/core/types/recall.py +144 -0
- aury/agents/core/types/session.py +257 -0
- aury/agents/core/types/subagent.py +154 -0
- aury/agents/core/types/tool.py +205 -0
- aury/agents/eval/__init__.py +331 -0
- aury/agents/hitl/__init__.py +57 -0
- aury/agents/hitl/ask_user.py +242 -0
- aury/agents/hitl/compaction.py +230 -0
- aury/agents/hitl/exceptions.py +87 -0
- aury/agents/hitl/permission.py +617 -0
- aury/agents/hitl/revert.py +216 -0
- aury/agents/llm/__init__.py +31 -0
- aury/agents/llm/adapter.py +367 -0
- aury/agents/llm/openai.py +294 -0
- aury/agents/llm/provider.py +476 -0
- aury/agents/mcp/__init__.py +153 -0
- aury/agents/memory/__init__.py +46 -0
- aury/agents/memory/compaction.py +394 -0
- aury/agents/memory/manager.py +465 -0
- aury/agents/memory/processor.py +177 -0
- aury/agents/memory/store.py +187 -0
- aury/agents/memory/types.py +137 -0
- aury/agents/messages/__init__.py +40 -0
- aury/agents/messages/config.py +47 -0
- aury/agents/messages/raw_store.py +224 -0
- aury/agents/messages/store.py +118 -0
- aury/agents/messages/types.py +88 -0
- aury/agents/middleware/__init__.py +31 -0
- aury/agents/middleware/base.py +341 -0
- aury/agents/middleware/chain.py +342 -0
- aury/agents/middleware/message.py +129 -0
- aury/agents/middleware/message_container.py +126 -0
- aury/agents/middleware/raw_message.py +153 -0
- aury/agents/middleware/truncation.py +139 -0
- aury/agents/middleware/types.py +81 -0
- aury/agents/plugin.py +162 -0
- aury/agents/react/__init__.py +4 -0
- aury/agents/react/agent.py +1923 -0
- aury/agents/sandbox/__init__.py +23 -0
- aury/agents/sandbox/local.py +239 -0
- aury/agents/sandbox/remote.py +200 -0
- aury/agents/sandbox/types.py +115 -0
- aury/agents/skill/__init__.py +16 -0
- aury/agents/skill/loader.py +180 -0
- aury/agents/skill/types.py +83 -0
- aury/agents/tool/__init__.py +39 -0
- aury/agents/tool/builtin/__init__.py +23 -0
- aury/agents/tool/builtin/ask_user.py +155 -0
- aury/agents/tool/builtin/bash.py +107 -0
- aury/agents/tool/builtin/delegate.py +726 -0
- aury/agents/tool/builtin/edit.py +121 -0
- aury/agents/tool/builtin/plan.py +277 -0
- aury/agents/tool/builtin/read.py +91 -0
- aury/agents/tool/builtin/thinking.py +111 -0
- aury/agents/tool/builtin/yield_result.py +130 -0
- aury/agents/tool/decorator.py +252 -0
- aury/agents/tool/set.py +204 -0
- aury/agents/usage/__init__.py +12 -0
- aury/agents/usage/tracker.py +236 -0
- aury/agents/workflow/__init__.py +85 -0
- aury/agents/workflow/adapter.py +268 -0
- aury/agents/workflow/dag.py +116 -0
- aury/agents/workflow/dsl.py +575 -0
- aury/agents/workflow/executor.py +659 -0
- aury/agents/workflow/expression.py +136 -0
- aury/agents/workflow/parser.py +182 -0
- aury/agents/workflow/state.py +145 -0
- aury/agents/workflow/types.py +86 -0
- aury_agent-0.0.4.dist-info/METADATA +90 -0
- aury_agent-0.0.4.dist-info/RECORD +149 -0
- aury_agent-0.0.4.dist-info/WHEEL +4 -0
- 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,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"]
|