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,299 @@
|
|
|
1
|
+
"""ArtifactContextProvider - provides artifacts context and tools.
|
|
2
|
+
|
|
3
|
+
Features:
|
|
4
|
+
- Artifact index in system_content
|
|
5
|
+
- ReadArtifactTool for LLM to read artifact content
|
|
6
|
+
- Pluggable Loader system for different artifact types
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
11
|
+
|
|
12
|
+
from .base import BaseContextProvider, AgentContext
|
|
13
|
+
from ..core.types.tool import BaseTool, ToolResult, ToolContext
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ..core.context import InvocationContext
|
|
17
|
+
from ..backends.artifact import ArtifactBackend, StoredArtifact
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ============================================================
|
|
21
|
+
# Loader Protocol and Registry
|
|
22
|
+
# ============================================================
|
|
23
|
+
|
|
24
|
+
@runtime_checkable
|
|
25
|
+
class ArtifactLoader(Protocol):
|
|
26
|
+
"""Protocol for loading artifact content.
|
|
27
|
+
|
|
28
|
+
Implement this to support custom artifact types.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
async def load(self, artifact: "StoredArtifact") -> str:
|
|
32
|
+
"""Load artifact content.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
artifact: Stored artifact with metadata and data
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
String content for LLM context
|
|
39
|
+
"""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DefaultLoader:
|
|
44
|
+
"""Default loader - returns summary or data as string."""
|
|
45
|
+
|
|
46
|
+
async def load(self, artifact: "StoredArtifact") -> str:
|
|
47
|
+
# Try summary first
|
|
48
|
+
if artifact.summary:
|
|
49
|
+
return artifact.summary
|
|
50
|
+
|
|
51
|
+
# Try data
|
|
52
|
+
if artifact.data:
|
|
53
|
+
import json
|
|
54
|
+
return json.dumps(artifact.data, ensure_ascii=False, indent=2)
|
|
55
|
+
|
|
56
|
+
return "No content"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SearchResultsLoader:
|
|
60
|
+
"""Loader for search_results artifacts."""
|
|
61
|
+
|
|
62
|
+
async def load(self, artifact: "StoredArtifact") -> str:
|
|
63
|
+
data = artifact.data or {}
|
|
64
|
+
query = data.get("query", "")
|
|
65
|
+
results = data.get("results", [])
|
|
66
|
+
|
|
67
|
+
if not results:
|
|
68
|
+
return f"Search `{query}` returned no results"
|
|
69
|
+
|
|
70
|
+
lines = [f"Search: {query}", f"Total {len(results)} results:", ""]
|
|
71
|
+
for i, r in enumerate(results, 1):
|
|
72
|
+
lines.append(f"{i}. {r.get('title', 'Untitled')}")
|
|
73
|
+
snippet = r.get('snippet', '')[:150]
|
|
74
|
+
if snippet:
|
|
75
|
+
lines.append(f" {snippet}")
|
|
76
|
+
url = r.get('url', '')
|
|
77
|
+
if url:
|
|
78
|
+
lines.append(f" URL: {url}")
|
|
79
|
+
lines.append("")
|
|
80
|
+
|
|
81
|
+
return "\n".join(lines)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class FileLoader:
|
|
85
|
+
"""Loader for file-based artifacts (file:// URLs)."""
|
|
86
|
+
|
|
87
|
+
async def load(self, artifact: "StoredArtifact") -> str:
|
|
88
|
+
data = artifact.data or {}
|
|
89
|
+
url = data.get("url")
|
|
90
|
+
|
|
91
|
+
if not url:
|
|
92
|
+
return artifact.summary or "No content"
|
|
93
|
+
|
|
94
|
+
# Load from file:// URL
|
|
95
|
+
if url.startswith("file://"):
|
|
96
|
+
file_path = url[7:] # Remove "file://"
|
|
97
|
+
try:
|
|
98
|
+
import aiofiles
|
|
99
|
+
async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
|
|
100
|
+
return await f.read()
|
|
101
|
+
except ImportError:
|
|
102
|
+
# Fallback to sync read
|
|
103
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
104
|
+
return f.read()
|
|
105
|
+
except Exception as e:
|
|
106
|
+
return artifact.summary or f"Failed to read file: {file_path} ({e})"
|
|
107
|
+
|
|
108
|
+
return artifact.summary or f"Unsupported URL scheme: {url}"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Global loader registry
|
|
112
|
+
_LOADERS: dict[str, ArtifactLoader] = {
|
|
113
|
+
"search_results": SearchResultsLoader(),
|
|
114
|
+
"report": FileLoader(),
|
|
115
|
+
"file": FileLoader(),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_DEFAULT_LOADER = DefaultLoader()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def register_loader(kind: str, loader: ArtifactLoader) -> None:
|
|
122
|
+
"""Register a custom loader for artifact kind.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
kind: Artifact kind (e.g., "pdf", "image")
|
|
126
|
+
loader: Loader instance implementing ArtifactLoader protocol
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
class PDFLoader:
|
|
130
|
+
async def load(self, artifact):
|
|
131
|
+
# Custom PDF loading logic
|
|
132
|
+
return extracted_text
|
|
133
|
+
|
|
134
|
+
register_loader("pdf", PDFLoader())
|
|
135
|
+
"""
|
|
136
|
+
_LOADERS[kind] = loader
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_loader(kind: str) -> ArtifactLoader:
|
|
140
|
+
"""Get loader for artifact kind.
|
|
141
|
+
|
|
142
|
+
Returns registered loader or default loader if not found.
|
|
143
|
+
"""
|
|
144
|
+
return _LOADERS.get(kind, _DEFAULT_LOADER)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ============================================================
|
|
148
|
+
# ReadArtifactTool
|
|
149
|
+
# ============================================================
|
|
150
|
+
|
|
151
|
+
class ReadArtifactTool(BaseTool):
|
|
152
|
+
"""Tool for reading artifact content.
|
|
153
|
+
|
|
154
|
+
Uses registered loaders to load content based on artifact kind.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
_name = "read_artifact"
|
|
158
|
+
_description = """Read the full content of an artifact.
|
|
159
|
+
|
|
160
|
+
Use this to get detailed content of artifacts like search results, reports, etc.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
_parameters: dict[str, Any] = {
|
|
164
|
+
"type": "object",
|
|
165
|
+
"properties": {
|
|
166
|
+
"artifact_id": {
|
|
167
|
+
"type": "string",
|
|
168
|
+
"description": "Artifact ID to read",
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
"required": ["artifact_id"],
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
def __init__(self, backend: "ArtifactBackend") -> None:
|
|
175
|
+
"""Initialize with artifact backend.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
backend: Backend for artifact storage/retrieval
|
|
179
|
+
"""
|
|
180
|
+
self._backend = backend
|
|
181
|
+
|
|
182
|
+
async def execute(self, params: dict[str, Any], ctx: ToolContext) -> ToolResult:
|
|
183
|
+
artifact_id = params.get("artifact_id")
|
|
184
|
+
if not artifact_id:
|
|
185
|
+
return ToolResult.error("artifact_id is required")
|
|
186
|
+
|
|
187
|
+
# Fetch artifact
|
|
188
|
+
stored = await self._backend.get(artifact_id)
|
|
189
|
+
if not stored:
|
|
190
|
+
return ToolResult.error(f"Artifact not found: {artifact_id}")
|
|
191
|
+
|
|
192
|
+
# Load content using appropriate loader
|
|
193
|
+
loader = get_loader(stored.kind)
|
|
194
|
+
try:
|
|
195
|
+
content = await loader.load(stored)
|
|
196
|
+
except Exception as e:
|
|
197
|
+
return ToolResult.error(f"Failed to load artifact: {e}")
|
|
198
|
+
|
|
199
|
+
return ToolResult.success(output=content)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ============================================================
|
|
203
|
+
# ArtifactContextProvider
|
|
204
|
+
# ============================================================
|
|
205
|
+
|
|
206
|
+
class ArtifactContextProvider(BaseContextProvider):
|
|
207
|
+
"""Artifact context provider.
|
|
208
|
+
|
|
209
|
+
Provides:
|
|
210
|
+
- system_content: Artifact index for LLM awareness
|
|
211
|
+
- tools: ReadArtifactTool for reading artifact content
|
|
212
|
+
|
|
213
|
+
Example:
|
|
214
|
+
provider = ArtifactContextProvider(
|
|
215
|
+
backend=my_artifact_backend,
|
|
216
|
+
max_summary_items=10,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Register custom loader
|
|
220
|
+
register_loader("pdf", MyPDFLoader())
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
_name = "artifacts"
|
|
224
|
+
|
|
225
|
+
def __init__(
|
|
226
|
+
self,
|
|
227
|
+
backend: "ArtifactBackend",
|
|
228
|
+
max_summary_items: int = 10,
|
|
229
|
+
enable_tools: bool = True,
|
|
230
|
+
):
|
|
231
|
+
"""Initialize ArtifactContextProvider.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
backend: Artifact storage backend
|
|
235
|
+
max_summary_items: Max artifacts to show in summary
|
|
236
|
+
enable_tools: Whether to provide ReadArtifactTool
|
|
237
|
+
"""
|
|
238
|
+
self.backend = backend
|
|
239
|
+
self.max_summary_items = max_summary_items
|
|
240
|
+
self.enable_tools = enable_tools
|
|
241
|
+
|
|
242
|
+
async def fetch(self, ctx: "InvocationContext") -> AgentContext:
|
|
243
|
+
"""Fetch artifact context.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
AgentContext with system_content (index) and ReadArtifactTool
|
|
247
|
+
"""
|
|
248
|
+
# Get artifacts for session
|
|
249
|
+
artifacts = await self.backend.list(session_id=ctx.session.id)
|
|
250
|
+
|
|
251
|
+
if not artifacts:
|
|
252
|
+
# No artifacts, but still provide tool if enabled
|
|
253
|
+
tools: list[BaseTool] = []
|
|
254
|
+
if self.enable_tools:
|
|
255
|
+
tools = [ReadArtifactTool(self.backend)]
|
|
256
|
+
return AgentContext(tools=tools) if tools else AgentContext.empty()
|
|
257
|
+
|
|
258
|
+
# Build summary (artifact index)
|
|
259
|
+
summary = self._build_summary(artifacts[:self.max_summary_items])
|
|
260
|
+
|
|
261
|
+
# Build tools
|
|
262
|
+
tools = []
|
|
263
|
+
if self.enable_tools:
|
|
264
|
+
tools = [ReadArtifactTool(self.backend)]
|
|
265
|
+
|
|
266
|
+
return AgentContext(
|
|
267
|
+
system_content=summary,
|
|
268
|
+
tools=tools,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def _build_summary(self, artifacts: list[Any]) -> str:
|
|
272
|
+
"""Build artifact summary for LLM context."""
|
|
273
|
+
lines = ["## Available Artifacts", ""]
|
|
274
|
+
for a in artifacts:
|
|
275
|
+
title = getattr(a, 'title', 'Untitled')
|
|
276
|
+
summary = getattr(a, 'summary', None)
|
|
277
|
+
artifact_id = getattr(a, 'id', 'unknown')
|
|
278
|
+
kind = getattr(a, 'kind', 'unknown')
|
|
279
|
+
|
|
280
|
+
line = f"- [{artifact_id}] ({kind}) {title}"
|
|
281
|
+
if summary:
|
|
282
|
+
line += f": {summary[:100]}"
|
|
283
|
+
lines.append(line)
|
|
284
|
+
|
|
285
|
+
lines.append("")
|
|
286
|
+
lines.append("Use `read_artifact` tool to get full content.")
|
|
287
|
+
return "\n".join(lines)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
__all__ = [
|
|
291
|
+
"ArtifactContextProvider",
|
|
292
|
+
"ArtifactLoader",
|
|
293
|
+
"ReadArtifactTool",
|
|
294
|
+
"register_loader",
|
|
295
|
+
"get_loader",
|
|
296
|
+
"DefaultLoader",
|
|
297
|
+
"SearchResultsLoader",
|
|
298
|
+
"FileLoader",
|
|
299
|
+
]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Base ContextProvider protocol and AgentContext.
|
|
2
|
+
|
|
3
|
+
ContextProvider is the unified abstraction for providing LLM context.
|
|
4
|
+
All context sources (memory, artifacts, skills, subagents)
|
|
5
|
+
are ContextProviders that implement fetch(ctx) -> AgentContext.
|
|
6
|
+
|
|
7
|
+
Design principle:
|
|
8
|
+
- Providers only provide DATA (content, subagents list, skills list, etc.)
|
|
9
|
+
- Agent consumes this data and decides how to use it (e.g., create DelegateTool)
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ..core.context import InvocationContext
|
|
19
|
+
from ..core.types.tool import BaseTool
|
|
20
|
+
from ..backends.subagent import AgentConfig
|
|
21
|
+
from ..skill import Skill
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class AgentContext:
|
|
26
|
+
"""Context provided by ContextProvider.fetch().
|
|
27
|
+
|
|
28
|
+
Contains all context that a ContextProvider provides:
|
|
29
|
+
- system_content: Injected into system prompt
|
|
30
|
+
- user_content: Injected into user message (before User: input)
|
|
31
|
+
- tools: Tools to register (from MemoryContextProvider, ArtifactContextProvider, etc.)
|
|
32
|
+
- messages: History messages (from MessageContextProvider)
|
|
33
|
+
- subagents: Sub-agent configs (from SubAgentContextProvider) - Agent creates DelegateTool
|
|
34
|
+
- skills: Skill definitions (from SkillContextProvider)
|
|
35
|
+
|
|
36
|
+
Multiple AgentContexts are merged by the Agent before LLM call.
|
|
37
|
+
"""
|
|
38
|
+
# Content injection
|
|
39
|
+
system_content: str | None = None
|
|
40
|
+
user_content: str | None = None
|
|
41
|
+
|
|
42
|
+
# Tools to register (paired tools from providers like MemorySearchTool)
|
|
43
|
+
tools: list["BaseTool"] = field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
# Messages (from MessageContextProvider)
|
|
46
|
+
messages: list[dict[str, Any]] = field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
# SubAgents (from SubAgentContextProvider) - Agent creates DelegateTool from this
|
|
49
|
+
subagents: list["AgentConfig"] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
# Skills (from SkillContextProvider)
|
|
52
|
+
skills: list["Skill"] = field(default_factory=list)
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def empty() -> "AgentContext":
|
|
56
|
+
"""Create an empty AgentContext."""
|
|
57
|
+
return AgentContext()
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def merge(outputs: list["AgentContext"]) -> "AgentContext":
|
|
61
|
+
"""Merge multiple AgentContexts into one.
|
|
62
|
+
|
|
63
|
+
- system_content: Concatenated with newlines
|
|
64
|
+
- user_content: Concatenated with newlines
|
|
65
|
+
- tools: Combined list (deduplicated by tool.name)
|
|
66
|
+
- messages: Combined list
|
|
67
|
+
- subagents: Combined list (deduplicated by key)
|
|
68
|
+
- skills: Combined list (deduplicated by name)
|
|
69
|
+
"""
|
|
70
|
+
system_parts: list[str] = []
|
|
71
|
+
user_parts: list[str] = []
|
|
72
|
+
all_tools: list["BaseTool"] = []
|
|
73
|
+
all_messages: list[dict[str, Any]] = []
|
|
74
|
+
all_subagents: list["AgentConfig"] = []
|
|
75
|
+
all_skills: list["Skill"] = []
|
|
76
|
+
seen_tool_names: set[str] = set()
|
|
77
|
+
seen_agent_keys: set[str] = set()
|
|
78
|
+
seen_skill_names: set[str] = set()
|
|
79
|
+
|
|
80
|
+
for output in outputs:
|
|
81
|
+
if output.system_content:
|
|
82
|
+
system_parts.append(output.system_content)
|
|
83
|
+
if output.user_content:
|
|
84
|
+
user_parts.append(output.user_content)
|
|
85
|
+
|
|
86
|
+
# Deduplicate tools by name
|
|
87
|
+
for tool in output.tools:
|
|
88
|
+
if tool.name not in seen_tool_names:
|
|
89
|
+
seen_tool_names.add(tool.name)
|
|
90
|
+
all_tools.append(tool)
|
|
91
|
+
|
|
92
|
+
all_messages.extend(output.messages)
|
|
93
|
+
|
|
94
|
+
# Deduplicate subagents by key
|
|
95
|
+
for agent in output.subagents:
|
|
96
|
+
if agent.key not in seen_agent_keys:
|
|
97
|
+
seen_agent_keys.add(agent.key)
|
|
98
|
+
all_subagents.append(agent)
|
|
99
|
+
|
|
100
|
+
# Deduplicate skills by name
|
|
101
|
+
for skill in output.skills:
|
|
102
|
+
if skill.name not in seen_skill_names:
|
|
103
|
+
seen_skill_names.add(skill.name)
|
|
104
|
+
all_skills.append(skill)
|
|
105
|
+
|
|
106
|
+
return AgentContext(
|
|
107
|
+
system_content="\n\n".join(system_parts) if system_parts else None,
|
|
108
|
+
user_content="\n\n".join(user_parts) if user_parts else None,
|
|
109
|
+
tools=all_tools,
|
|
110
|
+
messages=all_messages,
|
|
111
|
+
subagents=all_subagents,
|
|
112
|
+
skills=all_skills,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@runtime_checkable
|
|
117
|
+
class ContextProvider(Protocol):
|
|
118
|
+
"""Protocol for all ContextProviders.
|
|
119
|
+
|
|
120
|
+
ContextProviders provide context for LLM calls. Each ContextProvider:
|
|
121
|
+
1. Has a unique name
|
|
122
|
+
2. Implements fetch(ctx) to return AgentContext
|
|
123
|
+
3. Provides DATA only - Agent decides how to consume it
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
- MessageContextProvider: Returns messages list
|
|
127
|
+
- MemoryContextProvider: Returns system_content (summary) + MemorySearchTool
|
|
128
|
+
- ArtifactContextProvider: Returns system_content (index) + ReadArtifactTool
|
|
129
|
+
- SubAgentContextProvider: Returns subagents list (Agent creates DelegateTool)
|
|
130
|
+
- SkillContextProvider: Returns skills list
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def name(self) -> str:
|
|
135
|
+
"""Unique name for this provider."""
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
async def fetch(self, ctx: "InvocationContext") -> AgentContext:
|
|
139
|
+
"""Fetch context for LLM call.
|
|
140
|
+
|
|
141
|
+
Called before each LLM call. Can return dynamic content
|
|
142
|
+
based on current context (state, session, ctx.input, etc.).
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
ctx: Current invocation context (includes ctx.input with runtime vars)
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
AgentContext with content and tools
|
|
149
|
+
"""
|
|
150
|
+
...
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class BaseContextProvider(ABC):
|
|
154
|
+
"""Abstract base class for ContextProviders.
|
|
155
|
+
|
|
156
|
+
Provides default implementation structure. Subclass and implement
|
|
157
|
+
fetch() to create custom providers.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
_name: str = "base"
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def name(self) -> str:
|
|
164
|
+
"""Provider name."""
|
|
165
|
+
return self._name
|
|
166
|
+
|
|
167
|
+
@abstractmethod
|
|
168
|
+
async def fetch(self, ctx: "InvocationContext") -> AgentContext:
|
|
169
|
+
"""Fetch context. Subclasses must implement."""
|
|
170
|
+
...
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
__all__ = [
|
|
174
|
+
"ContextProvider",
|
|
175
|
+
"AgentContext",
|
|
176
|
+
"BaseContextProvider",
|
|
177
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""MemoryContextProvider - provides memory context.
|
|
2
|
+
|
|
3
|
+
This provider ONLY fetches memory context (summary/recalls).
|
|
4
|
+
Memory writing is handled by Middleware (e.g., in on_message_save hook).
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from .base import BaseContextProvider, AgentContext
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..core.context import InvocationContext
|
|
14
|
+
from ..memory import MemoryManager
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MemoryContextProvider(BaseContextProvider):
|
|
18
|
+
"""Memory context provider.
|
|
19
|
+
|
|
20
|
+
Provides:
|
|
21
|
+
- system_content: Memory summary and recalls for LLM awareness
|
|
22
|
+
|
|
23
|
+
Note: Memory WRITING is not done here.
|
|
24
|
+
Use Middleware (on_message_save hook) to write to MemoryManager.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
provider = MemoryContextProvider(
|
|
28
|
+
memory_manager=my_memory_manager,
|
|
29
|
+
recall_limit=10,
|
|
30
|
+
)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
_name = "memory"
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
memory_manager: "MemoryManager",
|
|
38
|
+
recall_limit: int = 10,
|
|
39
|
+
):
|
|
40
|
+
"""Initialize MemoryContextProvider.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
memory_manager: The underlying MemoryManager
|
|
44
|
+
recall_limit: Max recalls to include in context
|
|
45
|
+
"""
|
|
46
|
+
self.memory = memory_manager
|
|
47
|
+
self.recall_limit = recall_limit
|
|
48
|
+
|
|
49
|
+
async def fetch(self, ctx: "InvocationContext") -> AgentContext:
|
|
50
|
+
"""Fetch memory context.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
AgentContext with system_content (memory summary/recalls)
|
|
54
|
+
"""
|
|
55
|
+
# Get memory context
|
|
56
|
+
memory_ctx = await self.memory.get_context(
|
|
57
|
+
session_id=ctx.session.id,
|
|
58
|
+
recall_limit=self.recall_limit,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Build system content
|
|
62
|
+
if not memory_ctx or memory_ctx.is_empty:
|
|
63
|
+
return AgentContext.empty()
|
|
64
|
+
|
|
65
|
+
return AgentContext(
|
|
66
|
+
system_content=memory_ctx.to_system_message(),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = ["MemoryContextProvider"]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""MessageContextProvider - provides conversation history messages.
|
|
2
|
+
|
|
3
|
+
This provider ONLY fetches message history for context.
|
|
4
|
+
Message saving is handled by Middleware (on_message_save hook).
|
|
5
|
+
|
|
6
|
+
Recovery Strategy:
|
|
7
|
+
- Check State for complete messages (from pending/crashed invocation)
|
|
8
|
+
- If found, use State messages (complete, not truncated)
|
|
9
|
+
- Otherwise, load from MessageBackend (truncated historical messages)
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
from .base import BaseContextProvider, AgentContext
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ..core.context import InvocationContext
|
|
20
|
+
from ..backends import MessageBackend
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MessageContextProvider(BaseContextProvider):
|
|
26
|
+
"""Message history context provider.
|
|
27
|
+
|
|
28
|
+
Provides conversation history messages for LLM context.
|
|
29
|
+
Uses ctx.backends.message directly.
|
|
30
|
+
|
|
31
|
+
Features:
|
|
32
|
+
- Load messages via MessageBackend
|
|
33
|
+
- Priority: State (complete) > MessageBackend (truncated)
|
|
34
|
+
- Turn limits handled by backend
|
|
35
|
+
|
|
36
|
+
Note: Message SAVING is not done here.
|
|
37
|
+
Use MessageBackendMiddleware for saving messages.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
_name = "messages"
|
|
41
|
+
|
|
42
|
+
def __init__(self, *, max_messages: int = 100):
|
|
43
|
+
"""Initialize MessageContextProvider.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
max_messages: Max messages to fetch
|
|
47
|
+
"""
|
|
48
|
+
self.max_messages = max_messages
|
|
49
|
+
|
|
50
|
+
async def fetch(self, ctx: "InvocationContext") -> AgentContext:
|
|
51
|
+
"""Fetch conversation history.
|
|
52
|
+
|
|
53
|
+
Priority:
|
|
54
|
+
1. Check State for complete messages (from pending/crashed inv)
|
|
55
|
+
2. Fall back to MessageBackend (truncated historical messages)
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
AgentContext with messages list
|
|
59
|
+
"""
|
|
60
|
+
# Try to get complete messages from State (pending/crashed recovery)
|
|
61
|
+
state_messages = await self._get_messages_from_state(ctx)
|
|
62
|
+
if state_messages:
|
|
63
|
+
logger.debug(
|
|
64
|
+
f"Loaded {len(state_messages)} messages from State (complete)",
|
|
65
|
+
extra={"session_id": ctx.session.id},
|
|
66
|
+
)
|
|
67
|
+
return AgentContext(messages=state_messages)
|
|
68
|
+
|
|
69
|
+
# Fall back to MessageBackend
|
|
70
|
+
messages = await self._fetch_from_backend(ctx)
|
|
71
|
+
logger.debug(
|
|
72
|
+
f"Loaded {len(messages)} messages from backend (may be truncated)",
|
|
73
|
+
extra={"session_id": ctx.session.id},
|
|
74
|
+
)
|
|
75
|
+
return AgentContext(messages=messages)
|
|
76
|
+
|
|
77
|
+
async def _fetch_from_backend(self, ctx: "InvocationContext") -> list[dict[str, Any]]:
|
|
78
|
+
"""Fetch messages from MessageBackend."""
|
|
79
|
+
if ctx.backends is not None and ctx.backends.message is not None:
|
|
80
|
+
messages = await ctx.backends.message.get(
|
|
81
|
+
session_id=ctx.session.id,
|
|
82
|
+
type="truncated",
|
|
83
|
+
limit=self.max_messages,
|
|
84
|
+
)
|
|
85
|
+
# Convert to LLM format
|
|
86
|
+
return [
|
|
87
|
+
{"role": m["role"], "content": m["content"]}
|
|
88
|
+
for m in messages
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
# No backend available
|
|
92
|
+
logger.warning(
|
|
93
|
+
"No message backend available for MessageContextProvider",
|
|
94
|
+
extra={"session_id": ctx.session.id},
|
|
95
|
+
)
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
async def _get_messages_from_state(self, ctx: "InvocationContext") -> list[dict[str, Any]] | None:
|
|
99
|
+
"""Try to get complete messages from State.
|
|
100
|
+
|
|
101
|
+
State stores complete (not truncated) messages during execution.
|
|
102
|
+
This allows recovery from:
|
|
103
|
+
- HITL suspend
|
|
104
|
+
- Process crash
|
|
105
|
+
- Abnormal termination
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
List of message dicts if found, None otherwise
|
|
109
|
+
"""
|
|
110
|
+
if not ctx.state:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
# Check if State has message_history
|
|
114
|
+
messages_data = ctx.state.get("agent.message_history")
|
|
115
|
+
if not messages_data:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
if not isinstance(messages_data, list):
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
# Validate and convert to expected format
|
|
122
|
+
messages: list[dict[str, Any]] = []
|
|
123
|
+
for msg in messages_data:
|
|
124
|
+
if isinstance(msg, dict) and "role" in msg:
|
|
125
|
+
messages.append(msg)
|
|
126
|
+
|
|
127
|
+
return messages if messages else None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
__all__ = ["MessageContextProvider"]
|