agent-runtime-core 0.8.0__py3-none-any.whl → 0.9.0__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.
- agent_runtime_core/__init__.py +65 -3
- agent_runtime_core/agentic_loop.py +275 -15
- agent_runtime_core/config.py +4 -0
- agent_runtime_core/contexts.py +72 -4
- agent_runtime_core/llm/anthropic.py +83 -0
- agent_runtime_core/multi_agent.py +1408 -16
- agent_runtime_core/persistence/__init__.py +8 -0
- agent_runtime_core/persistence/base.py +318 -1
- agent_runtime_core/persistence/file.py +226 -2
- agent_runtime_core/privacy.py +250 -0
- {agent_runtime_core-0.8.0.dist-info → agent_runtime_core-0.9.0.dist-info}/METADATA +2 -1
- {agent_runtime_core-0.8.0.dist-info → agent_runtime_core-0.9.0.dist-info}/RECORD +14 -13
- {agent_runtime_core-0.8.0.dist-info → agent_runtime_core-0.9.0.dist-info}/WHEEL +0 -0
- {agent_runtime_core-0.8.0.dist-info → agent_runtime_core-0.9.0.dist-info}/licenses/LICENSE +0 -0
agent_runtime_core/contexts.py
CHANGED
|
@@ -24,11 +24,15 @@ import json
|
|
|
24
24
|
import os
|
|
25
25
|
from datetime import datetime
|
|
26
26
|
from pathlib import Path
|
|
27
|
-
from typing import Any, Callable, Optional
|
|
27
|
+
from typing import Any, Callable, Optional, TYPE_CHECKING
|
|
28
28
|
from uuid import UUID, uuid4
|
|
29
29
|
|
|
30
30
|
from agent_runtime_core.interfaces import EventType, Message, ToolRegistry
|
|
31
31
|
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from agent_runtime_core.multi_agent import SystemContext
|
|
34
|
+
from agent_runtime_core.privacy import PrivacyConfig, UserContext
|
|
35
|
+
|
|
32
36
|
|
|
33
37
|
class InMemoryRunContext:
|
|
34
38
|
"""
|
|
@@ -65,10 +69,13 @@ class InMemoryRunContext:
|
|
|
65
69
|
metadata: Optional[dict] = None,
|
|
66
70
|
tool_registry: Optional[ToolRegistry] = None,
|
|
67
71
|
on_event: Optional[Callable[[str, dict], None]] = None,
|
|
72
|
+
system_context: Optional["SystemContext"] = None,
|
|
73
|
+
user_context: Optional["UserContext"] = None,
|
|
74
|
+
privacy_config: Optional["PrivacyConfig"] = None,
|
|
68
75
|
):
|
|
69
76
|
"""
|
|
70
77
|
Initialize an in-memory run context.
|
|
71
|
-
|
|
78
|
+
|
|
72
79
|
Args:
|
|
73
80
|
run_id: Unique identifier for this run (auto-generated if not provided)
|
|
74
81
|
conversation_id: Associated conversation ID (optional)
|
|
@@ -77,6 +84,9 @@ class InMemoryRunContext:
|
|
|
77
84
|
metadata: Run metadata
|
|
78
85
|
tool_registry: Registry of available tools
|
|
79
86
|
on_event: Optional callback for events (for testing/debugging)
|
|
87
|
+
system_context: Optional SystemContext for multi-agent systems with shared knowledge
|
|
88
|
+
user_context: Optional UserContext for user isolation and privacy
|
|
89
|
+
privacy_config: Optional PrivacyConfig for privacy settings (defaults to max privacy)
|
|
80
90
|
"""
|
|
81
91
|
self._run_id = run_id or uuid4()
|
|
82
92
|
self._conversation_id = conversation_id
|
|
@@ -88,7 +98,18 @@ class InMemoryRunContext:
|
|
|
88
98
|
self._state: Optional[dict] = None
|
|
89
99
|
self._events: list[dict] = []
|
|
90
100
|
self._on_event = on_event
|
|
91
|
-
|
|
101
|
+
self._system_context = system_context
|
|
102
|
+
|
|
103
|
+
# Import here to avoid circular imports
|
|
104
|
+
from agent_runtime_core.privacy import (
|
|
105
|
+
DEFAULT_PRIVACY_CONFIG,
|
|
106
|
+
ANONYMOUS_USER,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Default to secure settings: anonymous user + strict privacy
|
|
110
|
+
self._user_context = user_context if user_context is not None else ANONYMOUS_USER
|
|
111
|
+
self._privacy_config = privacy_config if privacy_config is not None else DEFAULT_PRIVACY_CONFIG
|
|
112
|
+
|
|
92
113
|
@property
|
|
93
114
|
def run_id(self) -> UUID:
|
|
94
115
|
"""Unique identifier for this run."""
|
|
@@ -118,7 +139,22 @@ class InMemoryRunContext:
|
|
|
118
139
|
def tool_registry(self) -> ToolRegistry:
|
|
119
140
|
"""Registry of available tools for this agent."""
|
|
120
141
|
return self._tool_registry
|
|
121
|
-
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def system_context(self) -> Optional["SystemContext"]:
|
|
145
|
+
"""System context for multi-agent systems with shared knowledge."""
|
|
146
|
+
return self._system_context
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def user_context(self) -> "UserContext":
|
|
150
|
+
"""User context for privacy and data isolation. Defaults to ANONYMOUS_USER."""
|
|
151
|
+
return self._user_context
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def privacy_config(self) -> "PrivacyConfig":
|
|
155
|
+
"""Privacy configuration for this run. Defaults to DEFAULT_PRIVACY_CONFIG (strict)."""
|
|
156
|
+
return self._privacy_config
|
|
157
|
+
|
|
122
158
|
async def emit(self, event_type: EventType | str, payload: dict) -> None:
|
|
123
159
|
"""Emit an event (stored in memory)."""
|
|
124
160
|
event_type_str = event_type.value if hasattr(event_type, 'value') else str(event_type)
|
|
@@ -195,6 +231,9 @@ class FileRunContext:
|
|
|
195
231
|
metadata: Optional[dict] = None,
|
|
196
232
|
tool_registry: Optional[ToolRegistry] = None,
|
|
197
233
|
on_event: Optional[Callable[[str, dict], None]] = None,
|
|
234
|
+
system_context: Optional["SystemContext"] = None,
|
|
235
|
+
user_context: Optional["UserContext"] = None,
|
|
236
|
+
privacy_config: Optional["PrivacyConfig"] = None,
|
|
198
237
|
):
|
|
199
238
|
"""
|
|
200
239
|
Initialize a file-based run context.
|
|
@@ -208,6 +247,9 @@ class FileRunContext:
|
|
|
208
247
|
metadata: Run metadata
|
|
209
248
|
tool_registry: Registry of available tools
|
|
210
249
|
on_event: Optional callback for events
|
|
250
|
+
system_context: Optional SystemContext for multi-agent systems with shared knowledge
|
|
251
|
+
user_context: Optional UserContext for user isolation and privacy
|
|
252
|
+
privacy_config: Optional PrivacyConfig for privacy settings (defaults to max privacy)
|
|
211
253
|
"""
|
|
212
254
|
self._run_id = run_id or uuid4()
|
|
213
255
|
self._checkpoint_dir = Path(checkpoint_dir)
|
|
@@ -219,6 +261,17 @@ class FileRunContext:
|
|
|
219
261
|
self._cancelled = False
|
|
220
262
|
self._on_event = on_event
|
|
221
263
|
self._state_cache: Optional[dict] = None
|
|
264
|
+
self._system_context = system_context
|
|
265
|
+
|
|
266
|
+
# Import here to avoid circular imports
|
|
267
|
+
from agent_runtime_core.privacy import (
|
|
268
|
+
DEFAULT_PRIVACY_CONFIG,
|
|
269
|
+
ANONYMOUS_USER,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Default to secure settings: anonymous user + strict privacy
|
|
273
|
+
self._user_context = user_context if user_context is not None else ANONYMOUS_USER
|
|
274
|
+
self._privacy_config = privacy_config if privacy_config is not None else DEFAULT_PRIVACY_CONFIG
|
|
222
275
|
|
|
223
276
|
# Ensure checkpoint directory exists
|
|
224
277
|
self._checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -253,6 +306,21 @@ class FileRunContext:
|
|
|
253
306
|
"""Registry of available tools for this agent."""
|
|
254
307
|
return self._tool_registry
|
|
255
308
|
|
|
309
|
+
@property
|
|
310
|
+
def system_context(self) -> Optional["SystemContext"]:
|
|
311
|
+
"""System context for multi-agent systems with shared knowledge."""
|
|
312
|
+
return self._system_context
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def user_context(self) -> "UserContext":
|
|
316
|
+
"""User context for privacy and data isolation. Defaults to ANONYMOUS_USER."""
|
|
317
|
+
return self._user_context
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def privacy_config(self) -> "PrivacyConfig":
|
|
321
|
+
"""Privacy configuration for this run. Defaults to DEFAULT_PRIVACY_CONFIG (strict)."""
|
|
322
|
+
return self._privacy_config
|
|
323
|
+
|
|
256
324
|
def _checkpoint_path(self) -> Path:
|
|
257
325
|
"""Get the path to the checkpoint file for this run."""
|
|
258
326
|
return self._checkpoint_dir / f"{self._run_id}.json"
|
|
@@ -90,6 +90,83 @@ class AnthropicClient(LLMClient):
|
|
|
90
90
|
|
|
91
91
|
return os.environ.get("ANTHROPIC_API_KEY")
|
|
92
92
|
|
|
93
|
+
def _validate_tool_call_pairs(self, messages: list[Message]) -> list[Message]:
|
|
94
|
+
"""
|
|
95
|
+
Validate and repair tool_use/tool_result pairing in message history.
|
|
96
|
+
|
|
97
|
+
Anthropic requires that every tool_use block has a corresponding tool_result
|
|
98
|
+
block immediately after. This can be violated if a run fails mid-way through
|
|
99
|
+
tool execution (e.g., timeout, crash, API error during parallel tool calls).
|
|
100
|
+
|
|
101
|
+
This method removes orphaned tool_use blocks (assistant messages with tool_calls
|
|
102
|
+
that don't have corresponding tool results).
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
messages: List of messages in framework-neutral format
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Cleaned list of messages with orphaned tool_use blocks removed
|
|
109
|
+
"""
|
|
110
|
+
if not messages:
|
|
111
|
+
return messages
|
|
112
|
+
|
|
113
|
+
# First pass: collect all tool_call_ids that have results
|
|
114
|
+
tool_result_ids = set()
|
|
115
|
+
for msg in messages:
|
|
116
|
+
if msg.get("role") == "tool" and msg.get("tool_call_id"):
|
|
117
|
+
tool_result_ids.add(msg["tool_call_id"])
|
|
118
|
+
|
|
119
|
+
# Second pass: check each assistant message with tool_calls
|
|
120
|
+
cleaned_messages = []
|
|
121
|
+
orphaned_count = 0
|
|
122
|
+
|
|
123
|
+
for msg in messages:
|
|
124
|
+
if msg.get("role") == "assistant" and msg.get("tool_calls"):
|
|
125
|
+
# Check which tool_calls have results
|
|
126
|
+
tool_calls = msg.get("tool_calls", [])
|
|
127
|
+
valid_tool_calls = []
|
|
128
|
+
orphaned_ids = []
|
|
129
|
+
|
|
130
|
+
for tc in tool_calls:
|
|
131
|
+
# Handle both formats: {"id": ...} and {"function": {...}, "id": ...}
|
|
132
|
+
tc_id = tc.get("id")
|
|
133
|
+
if tc_id in tool_result_ids:
|
|
134
|
+
valid_tool_calls.append(tc)
|
|
135
|
+
else:
|
|
136
|
+
orphaned_ids.append(tc_id)
|
|
137
|
+
|
|
138
|
+
if orphaned_ids:
|
|
139
|
+
orphaned_count += len(orphaned_ids)
|
|
140
|
+
print(
|
|
141
|
+
f"[anthropic] Removing {len(orphaned_ids)} orphaned tool_use blocks "
|
|
142
|
+
f"without results: {orphaned_ids[:3]}{'...' if len(orphaned_ids) > 3 else ''}",
|
|
143
|
+
flush=True,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if valid_tool_calls:
|
|
147
|
+
# Keep the message but only with valid tool_calls
|
|
148
|
+
cleaned_msg = msg.copy()
|
|
149
|
+
cleaned_msg["tool_calls"] = valid_tool_calls
|
|
150
|
+
cleaned_messages.append(cleaned_msg)
|
|
151
|
+
elif msg.get("content"):
|
|
152
|
+
# No valid tool_calls but has text content - keep as regular message
|
|
153
|
+
cleaned_msg = {
|
|
154
|
+
"role": "assistant",
|
|
155
|
+
"content": msg["content"],
|
|
156
|
+
}
|
|
157
|
+
cleaned_messages.append(cleaned_msg)
|
|
158
|
+
# else: skip the message entirely (no valid tool_calls, no content)
|
|
159
|
+
else:
|
|
160
|
+
cleaned_messages.append(msg)
|
|
161
|
+
|
|
162
|
+
if orphaned_count > 0:
|
|
163
|
+
print(
|
|
164
|
+
f"[anthropic] Cleaned {orphaned_count} orphaned tool_use blocks from message history",
|
|
165
|
+
flush=True,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return cleaned_messages
|
|
169
|
+
|
|
93
170
|
async def generate(
|
|
94
171
|
self,
|
|
95
172
|
messages: list[Message],
|
|
@@ -104,6 +181,9 @@ class AnthropicClient(LLMClient):
|
|
|
104
181
|
"""Generate a completion from Anthropic."""
|
|
105
182
|
model = model or self.default_model
|
|
106
183
|
|
|
184
|
+
# Validate and repair message history before processing
|
|
185
|
+
messages = self._validate_tool_call_pairs(messages)
|
|
186
|
+
|
|
107
187
|
# Extract system message and convert other messages
|
|
108
188
|
system_message = None
|
|
109
189
|
converted_messages = []
|
|
@@ -156,6 +236,9 @@ class AnthropicClient(LLMClient):
|
|
|
156
236
|
"""Stream a completion from Anthropic."""
|
|
157
237
|
model = model or self.default_model
|
|
158
238
|
|
|
239
|
+
# Validate and repair message history before processing
|
|
240
|
+
messages = self._validate_tool_call_pairs(messages)
|
|
241
|
+
|
|
159
242
|
# Extract system message and convert other messages
|
|
160
243
|
system_message = None
|
|
161
244
|
converted_messages = []
|