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,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"]