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.
@@ -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 = []