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,230 @@
|
|
|
1
|
+
"""Session compaction for context window management."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..llm import LLMProvider, LLMMessage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class CompactionConfig:
|
|
12
|
+
"""Compaction configuration."""
|
|
13
|
+
# Protect recent tool outputs (tokens)
|
|
14
|
+
prune_protect: int = 40_000
|
|
15
|
+
# Minimum tokens to trigger prune
|
|
16
|
+
prune_minimum: int = 20_000
|
|
17
|
+
# Context limit threshold (0-1, fraction of model limit)
|
|
18
|
+
context_threshold: float = 0.8
|
|
19
|
+
# Reserve for output
|
|
20
|
+
output_reserve: int = 32_000
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SessionCompaction:
|
|
24
|
+
"""Handle context compaction for long conversations.
|
|
25
|
+
|
|
26
|
+
Implements two strategies:
|
|
27
|
+
1. Prune: Remove old tool outputs beyond protection window
|
|
28
|
+
2. Summarize: Use LLM to compress conversation history
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
llm: LLMProvider,
|
|
34
|
+
config: CompactionConfig | None = None,
|
|
35
|
+
):
|
|
36
|
+
self._llm = llm
|
|
37
|
+
self._config = config or CompactionConfig()
|
|
38
|
+
|
|
39
|
+
def estimate_tokens(self, text: str | dict | list) -> int:
|
|
40
|
+
"""Estimate token count for content.
|
|
41
|
+
|
|
42
|
+
Simple heuristic: ~4 characters per token.
|
|
43
|
+
"""
|
|
44
|
+
import json
|
|
45
|
+
|
|
46
|
+
if isinstance(text, (dict, list)):
|
|
47
|
+
text = json.dumps(text, ensure_ascii=False)
|
|
48
|
+
|
|
49
|
+
return len(text) // 4
|
|
50
|
+
|
|
51
|
+
def estimate_messages_tokens(self, messages: list[LLMMessage]) -> int:
|
|
52
|
+
"""Estimate total tokens in message list."""
|
|
53
|
+
total = 0
|
|
54
|
+
for msg in messages:
|
|
55
|
+
if isinstance(msg.content, str):
|
|
56
|
+
total += self.estimate_tokens(msg.content)
|
|
57
|
+
else:
|
|
58
|
+
total += self.estimate_tokens(msg.content)
|
|
59
|
+
return total
|
|
60
|
+
|
|
61
|
+
async def is_overflow(
|
|
62
|
+
self,
|
|
63
|
+
messages: list[LLMMessage],
|
|
64
|
+
context_limit: int,
|
|
65
|
+
) -> bool:
|
|
66
|
+
"""Check if context needs compaction."""
|
|
67
|
+
total_tokens = self.estimate_messages_tokens(messages)
|
|
68
|
+
usable = int(context_limit * self._config.context_threshold) - self._config.output_reserve
|
|
69
|
+
return total_tokens > usable
|
|
70
|
+
|
|
71
|
+
async def prune(
|
|
72
|
+
self,
|
|
73
|
+
messages: list[LLMMessage],
|
|
74
|
+
) -> tuple[list[LLMMessage], list[LLMMessage]]:
|
|
75
|
+
"""Prune old tool outputs.
|
|
76
|
+
|
|
77
|
+
Keeps recent tool outputs within prune_protect window.
|
|
78
|
+
Returns (pruned_messages, ejected_messages).
|
|
79
|
+
"""
|
|
80
|
+
# Find tool result messages
|
|
81
|
+
tool_results = []
|
|
82
|
+
for i, msg in enumerate(messages):
|
|
83
|
+
content = msg.content
|
|
84
|
+
if isinstance(content, list):
|
|
85
|
+
for part in content:
|
|
86
|
+
if isinstance(part, dict) and part.get("type") == "tool_result":
|
|
87
|
+
tokens = self.estimate_tokens(part.get("content", ""))
|
|
88
|
+
tool_results.append((i, part, tokens))
|
|
89
|
+
|
|
90
|
+
# Calculate which to prune (from oldest)
|
|
91
|
+
total_tokens = 0
|
|
92
|
+
to_prune = []
|
|
93
|
+
|
|
94
|
+
# Process in reverse (newest first)
|
|
95
|
+
for item in reversed(tool_results):
|
|
96
|
+
total_tokens += item[2]
|
|
97
|
+
if total_tokens > self._config.prune_protect:
|
|
98
|
+
to_prune.append(item)
|
|
99
|
+
|
|
100
|
+
# Check minimum threshold
|
|
101
|
+
prune_tokens = sum(t[2] for t in to_prune)
|
|
102
|
+
if prune_tokens < self._config.prune_minimum:
|
|
103
|
+
return messages, []
|
|
104
|
+
|
|
105
|
+
# Create pruned messages
|
|
106
|
+
pruned = []
|
|
107
|
+
ejected = []
|
|
108
|
+
prune_indices = {t[0] for t in to_prune}
|
|
109
|
+
|
|
110
|
+
for i, msg in enumerate(messages):
|
|
111
|
+
if i in prune_indices:
|
|
112
|
+
# Replace tool result content with placeholder
|
|
113
|
+
if isinstance(msg.content, list):
|
|
114
|
+
new_content = []
|
|
115
|
+
for part in msg.content:
|
|
116
|
+
if isinstance(part, dict) and part.get("type") == "tool_result":
|
|
117
|
+
# Store original for ejection
|
|
118
|
+
ejected.append(LLMMessage(
|
|
119
|
+
role=msg.role,
|
|
120
|
+
content=[part],
|
|
121
|
+
))
|
|
122
|
+
# Replace with placeholder
|
|
123
|
+
new_content.append({
|
|
124
|
+
**part,
|
|
125
|
+
"content": "[Old tool result content cleared]",
|
|
126
|
+
})
|
|
127
|
+
else:
|
|
128
|
+
new_content.append(part)
|
|
129
|
+
pruned.append(LLMMessage(role=msg.role, content=new_content))
|
|
130
|
+
else:
|
|
131
|
+
pruned.append(msg)
|
|
132
|
+
else:
|
|
133
|
+
pruned.append(msg)
|
|
134
|
+
|
|
135
|
+
return pruned, ejected
|
|
136
|
+
|
|
137
|
+
async def summarize(
|
|
138
|
+
self,
|
|
139
|
+
messages: list[LLMMessage],
|
|
140
|
+
max_summary_tokens: int = 4096,
|
|
141
|
+
) -> str:
|
|
142
|
+
"""Generate summary of conversation history."""
|
|
143
|
+
import json
|
|
144
|
+
|
|
145
|
+
# Build summary prompt
|
|
146
|
+
prompt = self._build_compaction_prompt(messages)
|
|
147
|
+
|
|
148
|
+
# Call LLM for summary
|
|
149
|
+
summary_messages = [LLMMessage(role="user", content=prompt)]
|
|
150
|
+
|
|
151
|
+
result_text = ""
|
|
152
|
+
async for event in self._llm.complete(
|
|
153
|
+
messages=summary_messages,
|
|
154
|
+
max_tokens=max_summary_tokens,
|
|
155
|
+
):
|
|
156
|
+
if event.type == "content" and event.delta:
|
|
157
|
+
result_text += event.delta
|
|
158
|
+
|
|
159
|
+
return result_text
|
|
160
|
+
|
|
161
|
+
def _build_compaction_prompt(self, messages: list[LLMMessage]) -> str:
|
|
162
|
+
"""Build prompt for summarization."""
|
|
163
|
+
import json
|
|
164
|
+
|
|
165
|
+
# Format messages
|
|
166
|
+
formatted = []
|
|
167
|
+
for msg in messages:
|
|
168
|
+
if isinstance(msg.content, str):
|
|
169
|
+
formatted.append(f"[{msg.role}]: {msg.content}")
|
|
170
|
+
else:
|
|
171
|
+
formatted.append(f"[{msg.role}]: {json.dumps(msg.content, ensure_ascii=False)}")
|
|
172
|
+
|
|
173
|
+
conversation = "\n\n".join(formatted)
|
|
174
|
+
|
|
175
|
+
return f"""Please summarize the following conversation history, preserving:
|
|
176
|
+
- Key decisions and outcomes
|
|
177
|
+
- Important context and facts
|
|
178
|
+
- Tool execution results (summarized)
|
|
179
|
+
- User preferences and requirements
|
|
180
|
+
|
|
181
|
+
Conversation:
|
|
182
|
+
{conversation}
|
|
183
|
+
|
|
184
|
+
Summary:"""
|
|
185
|
+
|
|
186
|
+
async def compact(
|
|
187
|
+
self,
|
|
188
|
+
messages: list[LLMMessage],
|
|
189
|
+
context_limit: int,
|
|
190
|
+
) -> tuple[list[LLMMessage], dict[str, Any]]:
|
|
191
|
+
"""Full compaction: prune then summarize if needed.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Tuple of (compacted_messages, compaction_info)
|
|
195
|
+
"""
|
|
196
|
+
info = {
|
|
197
|
+
"original_tokens": self.estimate_messages_tokens(messages),
|
|
198
|
+
"pruned": False,
|
|
199
|
+
"summarized": False,
|
|
200
|
+
"ejected_count": 0,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# First try pruning
|
|
204
|
+
pruned, ejected = await self.prune(messages)
|
|
205
|
+
if ejected:
|
|
206
|
+
info["pruned"] = True
|
|
207
|
+
info["ejected_count"] = len(ejected)
|
|
208
|
+
messages = pruned
|
|
209
|
+
|
|
210
|
+
# Check if still over limit
|
|
211
|
+
if await self.is_overflow(messages, context_limit):
|
|
212
|
+
# Summarize older messages
|
|
213
|
+
mid_point = len(messages) // 2
|
|
214
|
+
old_messages = messages[:mid_point]
|
|
215
|
+
recent_messages = messages[mid_point:]
|
|
216
|
+
|
|
217
|
+
summary = await self.summarize(old_messages)
|
|
218
|
+
|
|
219
|
+
# Replace old messages with summary
|
|
220
|
+
summary_message = LLMMessage(
|
|
221
|
+
role="system",
|
|
222
|
+
content=f"[Conversation Summary]\n{summary}\n\n[End Summary]",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
messages = [messages[0], summary_message] + recent_messages
|
|
226
|
+
info["summarized"] = True
|
|
227
|
+
|
|
228
|
+
info["final_tokens"] = self.estimate_messages_tokens(messages)
|
|
229
|
+
|
|
230
|
+
return messages, info
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""HITL (Human-in-the-Loop) exceptions and signals.
|
|
2
|
+
|
|
3
|
+
These control agent execution flow when human input is needed.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
# Re-export from core.signals
|
|
11
|
+
from ..core.signals import HITLSuspend, SuspendSignal
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HITLTimeoutError(Exception):
|
|
15
|
+
"""Raised when HITL request times out."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, request_id: str, timeout: float):
|
|
18
|
+
self.request_id = request_id
|
|
19
|
+
self.timeout = timeout
|
|
20
|
+
super().__init__(f"HITL request {request_id} timed out after {timeout}s")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HITLCancelledError(Exception):
|
|
24
|
+
"""Raised when HITL request is cancelled."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, request_id: str, reason: str = "cancelled"):
|
|
27
|
+
self.request_id = request_id
|
|
28
|
+
self.reason = reason
|
|
29
|
+
super().__init__(f"HITL request {request_id} cancelled: {reason}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class HITLRequest:
|
|
34
|
+
"""A pending HITL request.
|
|
35
|
+
|
|
36
|
+
Stored in invocation for persistence.
|
|
37
|
+
"""
|
|
38
|
+
request_id: str
|
|
39
|
+
request_type: str # ask_user, permission, form, workflow_human
|
|
40
|
+
|
|
41
|
+
# Display
|
|
42
|
+
message: str | None = None
|
|
43
|
+
options: list[str] | None = None
|
|
44
|
+
|
|
45
|
+
# Context
|
|
46
|
+
tool_name: str | None = None # If triggered by tool
|
|
47
|
+
node_id: str | None = None # If triggered by workflow node
|
|
48
|
+
|
|
49
|
+
# Metadata
|
|
50
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict[str, Any]:
|
|
53
|
+
"""Convert to dictionary for serialization."""
|
|
54
|
+
return {
|
|
55
|
+
"request_id": self.request_id,
|
|
56
|
+
"request_type": self.request_type,
|
|
57
|
+
"message": self.message,
|
|
58
|
+
"options": self.options,
|
|
59
|
+
"tool_name": self.tool_name,
|
|
60
|
+
"node_id": self.node_id,
|
|
61
|
+
"metadata": self.metadata,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_dict(cls, data: dict[str, Any]) -> "HITLRequest":
|
|
66
|
+
"""Create from dictionary."""
|
|
67
|
+
return cls(
|
|
68
|
+
request_id=data["request_id"],
|
|
69
|
+
request_type=data.get("request_type", "ask_user"),
|
|
70
|
+
message=data.get("message"),
|
|
71
|
+
options=data.get("options"),
|
|
72
|
+
tool_name=data.get("tool_name"),
|
|
73
|
+
node_id=data.get("node_id"),
|
|
74
|
+
metadata=data.get("metadata", {}),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
# Signals
|
|
80
|
+
"SuspendSignal",
|
|
81
|
+
"HITLSuspend",
|
|
82
|
+
# Exceptions
|
|
83
|
+
"HITLTimeoutError",
|
|
84
|
+
"HITLCancelledError",
|
|
85
|
+
# Types
|
|
86
|
+
"HITLRequest",
|
|
87
|
+
]
|