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,394 @@
1
+ """Memory compaction manager.
2
+
3
+ Handles automatic compaction of conversation history when token count
4
+ exceeds threshold. Ejected messages are summarized and stored in memory.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import TYPE_CHECKING, Any, Protocol, Callable
10
+ from abc import abstractmethod
11
+
12
+ from ..core.logging import memory_logger as logger
13
+
14
+ if TYPE_CHECKING:
15
+ from .manager import MemoryManager
16
+
17
+
18
+ class TokenCounter(Protocol):
19
+ """Protocol for counting tokens in content."""
20
+
21
+ @abstractmethod
22
+ def count(self, content: str) -> int:
23
+ """Count tokens in content."""
24
+ ...
25
+
26
+ @abstractmethod
27
+ def count_messages(self, messages: list[dict[str, Any]]) -> int:
28
+ """Count tokens in messages."""
29
+ ...
30
+
31
+
32
+ class SimpleTokenCounter:
33
+ """Simple word-based token estimator.
34
+
35
+ Approximates 1 token ≈ 4 characters for English text.
36
+ Good enough for threshold checking, not for billing.
37
+ """
38
+
39
+ def __init__(self, chars_per_token: float = 4.0):
40
+ self.chars_per_token = chars_per_token
41
+
42
+ def count(self, content: str) -> int:
43
+ return int(len(content) / self.chars_per_token)
44
+
45
+ def count_messages(self, messages: list[dict[str, Any]]) -> int:
46
+ total = 0
47
+ for msg in messages:
48
+ # Base overhead for role, etc
49
+ total += 4
50
+
51
+ content = msg.get("content", "")
52
+ if isinstance(content, str):
53
+ total += self.count(content)
54
+ elif isinstance(content, list):
55
+ # Multi-part content
56
+ for part in content:
57
+ if isinstance(part, dict):
58
+ if part.get("type") == "text":
59
+ total += self.count(part.get("text", ""))
60
+ elif part.get("type") == "image_url":
61
+ total += 85 # Rough estimate for image token
62
+ return total
63
+
64
+
65
+ class Summarizer(Protocol):
66
+ """Protocol for summarizing content."""
67
+
68
+ @abstractmethod
69
+ async def summarize(
70
+ self,
71
+ messages: list[dict[str, Any]],
72
+ existing_summary: str | None = None,
73
+ ) -> str:
74
+ """Summarize messages, optionally incorporating existing summary."""
75
+ ...
76
+
77
+
78
+ class SimpleSummarizer:
79
+ """Simple extractive summarizer.
80
+
81
+ Extracts key information without LLM call.
82
+ For production, replace with LLM-based summarizer.
83
+ """
84
+
85
+ def __init__(self, max_length: int = 500):
86
+ self.max_length = max_length
87
+
88
+ async def summarize(
89
+ self,
90
+ messages: list[dict[str, Any]],
91
+ existing_summary: str | None = None,
92
+ ) -> str:
93
+ parts = []
94
+
95
+ # Include existing summary
96
+ if existing_summary:
97
+ parts.append(f"Previous: {existing_summary}")
98
+
99
+ # Extract key points from messages
100
+ for msg in messages:
101
+ role = msg.get("role", "unknown")
102
+ content = msg.get("content", "")
103
+
104
+ if isinstance(content, list):
105
+ # Extract text parts
106
+ text_parts = [
107
+ p.get("text", "")
108
+ for p in content
109
+ if isinstance(p, dict) and p.get("type") == "text"
110
+ ]
111
+ content = " ".join(text_parts)
112
+
113
+ # Skip empty content
114
+ if not content.strip():
115
+ continue
116
+
117
+ # Truncate long content
118
+ if len(content) > 200:
119
+ content = content[:200] + "..."
120
+
121
+ parts.append(f"{role}: {content}")
122
+
123
+ result = "\n".join(parts)
124
+
125
+ # Final truncation
126
+ if len(result) > self.max_length:
127
+ result = result[:self.max_length] + "..."
128
+
129
+ return result
130
+
131
+
132
+ @dataclass
133
+ class CompactionConfig:
134
+ """Configuration for compaction behavior."""
135
+
136
+ # Token thresholds
137
+ token_threshold: int = 8000 # Start compaction when exceeded
138
+ target_tokens: int = 4000 # Target after compaction
139
+
140
+ # Message handling
141
+ keep_system: bool = True # Always keep system messages
142
+ keep_recent: int = 4 # Always keep N most recent messages
143
+
144
+ # Summary behavior
145
+ update_summary: bool = True # Update session summary
146
+ store_ejected: bool = True # Store ejected messages in memory
147
+
148
+ # Extra configuration
149
+ metadata: dict[str, Any] = field(default_factory=dict)
150
+
151
+
152
+ @dataclass
153
+ class CompactionResult:
154
+ """Result of a compaction operation."""
155
+
156
+ compacted: bool # Whether compaction occurred
157
+ messages_before: int
158
+ messages_after: int
159
+ tokens_before: int
160
+ tokens_after: int
161
+ ejected_count: int
162
+ summary: str | None = None
163
+ error: str | None = None
164
+
165
+ @classmethod
166
+ def no_action(cls, messages: int, tokens: int) -> "CompactionResult":
167
+ """Create result for no compaction needed."""
168
+ return cls(
169
+ compacted=False,
170
+ messages_before=messages,
171
+ messages_after=messages,
172
+ tokens_before=tokens,
173
+ tokens_after=tokens,
174
+ ejected_count=0,
175
+ )
176
+
177
+ @classmethod
178
+ def with_error(cls, error: str, messages: int, tokens: int) -> "CompactionResult":
179
+ """Create result for error case."""
180
+ return cls(
181
+ compacted=False,
182
+ messages_before=messages,
183
+ messages_after=messages,
184
+ tokens_before=tokens,
185
+ tokens_after=tokens,
186
+ ejected_count=0,
187
+ error=error,
188
+ )
189
+
190
+
191
+ class CompactionManager:
192
+ """Manages compaction of conversation history.
193
+
194
+ When token count exceeds threshold:
195
+ 1. Identifies messages to eject (keeping system + recent)
196
+ 2. Summarizes ejected messages
197
+ 3. Stores ejected content in memory
198
+ 4. Returns compacted message list with summary injected
199
+
200
+ Usage:
201
+ compaction = CompactionManager(
202
+ config=CompactionConfig(token_threshold=8000),
203
+ memory_manager=memory,
204
+ )
205
+
206
+ # Check and compact if needed
207
+ result, messages = await compaction.check_and_compact(
208
+ messages=messages,
209
+ session_id=session_id,
210
+ invocation_id=invocation_id,
211
+ )
212
+
213
+ if result.compacted:
214
+ print(f"Compacted {result.ejected_count} messages")
215
+ """
216
+
217
+ def __init__(
218
+ self,
219
+ config: CompactionConfig | None = None,
220
+ memory_manager: MemoryManager | None = None,
221
+ token_counter: TokenCounter | None = None,
222
+ summarizer: Summarizer | None = None,
223
+ ):
224
+ self.config = config or CompactionConfig()
225
+ self.memory = memory_manager
226
+ self.token_counter = token_counter or SimpleTokenCounter()
227
+ self.summarizer = summarizer or SimpleSummarizer()
228
+
229
+ async def check_and_compact(
230
+ self,
231
+ messages: list[dict[str, Any]],
232
+ session_id: str | None = None,
233
+ invocation_id: str | None = None,
234
+ ) -> tuple[CompactionResult, list[dict[str, Any]]]:
235
+ """Check if compaction needed and perform if so.
236
+
237
+ Args:
238
+ messages: Current message list
239
+ session_id: Session ID for memory storage
240
+ invocation_id: Invocation ID for tracking
241
+
242
+ Returns:
243
+ Tuple of (result, possibly_compacted_messages)
244
+ """
245
+ tokens_before = self.token_counter.count_messages(messages)
246
+ messages_before = len(messages)
247
+
248
+ # Check threshold
249
+ if tokens_before <= self.config.token_threshold:
250
+ return CompactionResult.no_action(messages_before, tokens_before), messages
251
+
252
+ logger.info(
253
+ f"Compaction triggered: {tokens_before} tokens > {self.config.token_threshold}"
254
+ )
255
+
256
+ try:
257
+ # Perform compaction
258
+ compacted, ejected, summary = await self._compact(
259
+ messages,
260
+ session_id,
261
+ invocation_id,
262
+ )
263
+
264
+ tokens_after = self.token_counter.count_messages(compacted)
265
+
266
+ result = CompactionResult(
267
+ compacted=True,
268
+ messages_before=messages_before,
269
+ messages_after=len(compacted),
270
+ tokens_before=tokens_before,
271
+ tokens_after=tokens_after,
272
+ ejected_count=len(ejected),
273
+ summary=summary,
274
+ )
275
+
276
+ return result, compacted
277
+
278
+ except Exception as e:
279
+ logger.error(f"Compaction failed: {e}")
280
+ return CompactionResult.with_error(str(e), messages_before, tokens_before), messages
281
+
282
+ async def _compact(
283
+ self,
284
+ messages: list[dict[str, Any]],
285
+ session_id: str | None,
286
+ invocation_id: str | None,
287
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], str | None]:
288
+ """Perform compaction.
289
+
290
+ Returns:
291
+ Tuple of (compacted_messages, ejected_messages, summary)
292
+ """
293
+ # Separate system messages
294
+ system_msgs = []
295
+ non_system_msgs = []
296
+
297
+ for msg in messages:
298
+ if msg.get("role") == "system" and self.config.keep_system:
299
+ system_msgs.append(msg)
300
+ else:
301
+ non_system_msgs.append(msg)
302
+
303
+ # Determine what to keep vs eject
304
+ keep_recent = self.config.keep_recent
305
+
306
+ if len(non_system_msgs) <= keep_recent:
307
+ # Nothing to eject
308
+ return messages, [], None
309
+
310
+ # Calculate how many to eject to reach target
311
+ target_eject = 0
312
+ cumulative_tokens = self.token_counter.count_messages(system_msgs)
313
+
314
+ # Start from most recent and work backwards to find cut point
315
+ kept_msgs = []
316
+ to_eject = []
317
+
318
+ # We need to keep at least keep_recent messages
319
+ for i, msg in enumerate(reversed(non_system_msgs)):
320
+ msg_tokens = self.token_counter.count_messages([msg])
321
+
322
+ if i < keep_recent:
323
+ # Always keep recent messages
324
+ kept_msgs.insert(0, msg)
325
+ cumulative_tokens += msg_tokens
326
+ elif cumulative_tokens + msg_tokens > self.config.target_tokens:
327
+ # Eject this message
328
+ to_eject.insert(0, msg)
329
+ else:
330
+ # Can still fit, keep it
331
+ kept_msgs.insert(0, msg)
332
+ cumulative_tokens += msg_tokens
333
+
334
+ if not to_eject:
335
+ return messages, [], None
336
+
337
+ # Get existing summary for incremental update
338
+ existing_summary = None
339
+ if self.memory and self.config.update_summary:
340
+ summary_obj = await self.memory.get_summary(session_id) if session_id else None
341
+ existing_summary = summary_obj.content if summary_obj else None
342
+
343
+ # Summarize ejected messages
344
+ summary = await self.summarizer.summarize(to_eject, existing_summary)
345
+
346
+ # Store ejected messages in memory
347
+ if self.memory and self.config.store_ejected and session_id:
348
+ await self.memory.on_compress(
349
+ session_id=session_id,
350
+ invocation_id=invocation_id or "",
351
+ ejected_messages=to_eject,
352
+ )
353
+
354
+ # Update session summary
355
+ if self.config.update_summary:
356
+ await self.memory.update_summary(
357
+ session_id=session_id,
358
+ content=summary,
359
+ last_invocation_id=invocation_id or "",
360
+ )
361
+
362
+ # Build compacted messages
363
+ # Inject summary as a system message
364
+ compacted = list(system_msgs)
365
+
366
+ if summary:
367
+ compacted.append({
368
+ "role": "system",
369
+ "content": f"[Conversation history summary]\n{summary}",
370
+ })
371
+
372
+ compacted.extend(kept_msgs)
373
+
374
+ return compacted, to_eject, summary
375
+
376
+ def should_compact(self, messages: list[dict[str, Any]]) -> bool:
377
+ """Quick check if compaction might be needed."""
378
+ tokens = self.token_counter.count_messages(messages)
379
+ return tokens > self.config.token_threshold
380
+
381
+ def estimate_tokens(self, messages: list[dict[str, Any]]) -> int:
382
+ """Estimate token count for messages."""
383
+ return self.token_counter.count_messages(messages)
384
+
385
+
386
+ __all__ = [
387
+ "TokenCounter",
388
+ "SimpleTokenCounter",
389
+ "Summarizer",
390
+ "SimpleSummarizer",
391
+ "CompactionConfig",
392
+ "CompactionResult",
393
+ "CompactionManager",
394
+ ]