agent-runtime-core 0.7.1__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.
@@ -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 = []