Skip to content

Commit e0b84c2

Browse files
committed
fix(agents): preserve both assistant and function messages during snapshot clean for Gemini API
Fixes #3759 When `enable_snapshot_clean` is enabled, the `_clean_snapshot_in_memory` method previously only recreated the FUNCTION message (tool result) but removed both the ASSISTANT message (tool call request) and FUNCTION message from memory. This breaks Gemini API which requires both messages to exist together - the tool call request must be present alongside the tool call result. Without the ASSISTANT message, Gemini returns the error: `GenerateContentRequest.contents[0].parts[0].function_response.name: Name cannot be empty` Changes: - Added `args` and `extra_content` fields to `_ToolOutputHistoryEntry` to preserve tool call context for later reconstruction - Modified `_register_tool_output_for_cache` to accept and store args/extra_content - Modified `_clean_snapshot_in_memory` to recreate BOTH the ASSISTANT message (with tool_calls) and the FUNCTION message (with result) when cleaning snapshots - Updated `_record_tool_calling` to pass args and extra_content to the cache registration function This ensures proper tool call message reconstruction that is compatible with Gemini and other APIs that require paired tool call request/response messages.
1 parent fe757da commit e0b84c2

File tree

1 file changed

+75
-22
lines changed

1 file changed

+75
-22
lines changed

camel/agents/chat_agent.py

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ class _ToolOutputHistoryEntry:
169169
result_text: str
170170
record_uuids: List[str]
171171
record_timestamps: List[float]
172+
args: Optional[Dict[str, Any]] = None
173+
extra_content: Optional[Dict[str, Any]] = None
172174
cached: bool = False
173175

174176

@@ -1445,6 +1447,8 @@ def _register_tool_output_for_cache(
14451447
tool_call_id: str,
14461448
result_text: str,
14471449
records: List[MemoryRecord],
1450+
args: Optional[Dict[str, Any]] = None,
1451+
extra_content: Optional[Dict[str, Any]] = None,
14481452
) -> None:
14491453
if not records:
14501454
return
@@ -1455,6 +1459,8 @@ def _register_tool_output_for_cache(
14551459
result_text=result_text,
14561460
record_uuids=[str(record.uuid) for record in records],
14571461
record_timestamps=[record.timestamp for record in records],
1462+
args=args,
1463+
extra_content=extra_content,
14581464
)
14591465
self._tool_output_history.append(entry)
14601466
self._process_tool_output_cache()
@@ -1480,22 +1486,6 @@ def _clean_snapshot_in_memory(
14801486
if '- ' in result_text and '[ref=' in result_text:
14811487
cleaned_result = self._clean_snapshot_content(result_text)
14821488

1483-
# Update the message in memory storage
1484-
timestamp = (
1485-
entry.record_timestamps[0]
1486-
if entry.record_timestamps
1487-
else time.time_ns() / 1_000_000_000
1488-
)
1489-
cleaned_message = FunctionCallingMessage(
1490-
role_name=self.role_name,
1491-
role_type=self.role_type,
1492-
meta_dict={},
1493-
content="",
1494-
func_name=entry.tool_name,
1495-
result=cleaned_result,
1496-
tool_call_id=entry.tool_call_id,
1497-
)
1498-
14991489
chat_history_block = getattr(
15001490
self.memory, "_chat_history_block", None
15011491
)
@@ -1504,18 +1494,74 @@ def _clean_snapshot_in_memory(
15041494
return
15051495

15061496
existing_records = storage.load()
1497+
1498+
# Remove records by UUID
15071499
updated_records = [
15081500
record
15091501
for record in existing_records
15101502
if record["uuid"] not in entry.record_uuids
15111503
]
1512-
new_record = MemoryRecord(
1513-
message=cleaned_message,
1504+
1505+
# Recreate both assistant and function messages
1506+
# For Gemini API, the tool call request needs to exist along with
1507+
# the tool call result.
1508+
assist_args = entry.args if entry.args is not None else {}
1509+
1510+
# Use timestamps from entry, ensuring proper ordering
1511+
if len(entry.record_timestamps) >= 2:
1512+
assist_timestamp = entry.record_timestamps[0]
1513+
func_timestamp = entry.record_timestamps[1]
1514+
elif entry.record_timestamps:
1515+
# If only one timestamp, use it for function and ensure
1516+
# assist comes before
1517+
func_timestamp = entry.record_timestamps[0]
1518+
assist_timestamp = func_timestamp - 1e-6
1519+
else:
1520+
# No timestamps, use current time with proper ordering
1521+
assist_timestamp = time.time_ns() / 1_000_000_000
1522+
func_timestamp = assist_timestamp + 1e-6
1523+
1524+
# Recreate assistant message (tool call request)
1525+
cleaned_assist_message = FunctionCallingMessage(
1526+
role_name=self.role_name,
1527+
role_type=self.role_type,
1528+
meta_dict={},
1529+
content="",
1530+
func_name=entry.tool_name,
1531+
args=assist_args,
1532+
tool_call_id=entry.tool_call_id,
1533+
extra_content=entry.extra_content,
1534+
)
1535+
1536+
# Recreate function message (tool result)
1537+
cleaned_func_message = FunctionCallingMessage(
1538+
role_name=self.role_name,
1539+
role_type=self.role_type,
1540+
meta_dict={},
1541+
content="",
1542+
func_name=entry.tool_name,
1543+
result=cleaned_result,
1544+
tool_call_id=entry.tool_call_id,
1545+
extra_content=entry.extra_content,
1546+
)
1547+
1548+
# Create new records for both assistant and function messages
1549+
new_assist_record = MemoryRecord(
1550+
message=cleaned_assist_message,
1551+
role_at_backend=OpenAIBackendRole.ASSISTANT,
1552+
timestamp=assist_timestamp,
1553+
agent_id=self.agent_id,
1554+
)
1555+
new_func_record = MemoryRecord(
1556+
message=cleaned_func_message,
15141557
role_at_backend=OpenAIBackendRole.FUNCTION,
1515-
timestamp=timestamp,
1558+
timestamp=func_timestamp,
15161559
agent_id=self.agent_id,
15171560
)
1518-
updated_records.append(new_record.to_dict())
1561+
1562+
# Add both records back
1563+
updated_records.append(new_assist_record.to_dict())
1564+
updated_records.append(new_func_record.to_dict())
15191565
updated_records.sort(key=lambda record: record["timestamp"])
15201566
storage.clear()
15211567
storage.save(updated_records)
@@ -1527,8 +1573,11 @@ def _clean_snapshot_in_memory(
15271573
)
15281574

15291575
entry.cached = True
1530-
entry.record_uuids = [str(new_record.uuid)]
1531-
entry.record_timestamps = [timestamp]
1576+
entry.record_uuids = [
1577+
str(new_assist_record.uuid),
1578+
str(new_func_record.uuid),
1579+
]
1580+
entry.record_timestamps = [assist_timestamp, func_timestamp]
15321581

15331582
def add_external_tool(
15341583
self, tool: Union[FunctionTool, Callable, Dict[str, Any]]
@@ -4079,13 +4128,17 @@ def _record_tool_calling(
40794128
)
40804129

40814130
# Register tool output for snapshot cleaning if enabled
4131+
# Include args and extra_content so both the assistant message
4132+
# (tool call) and function message (tool result) can be recreated
40824133
if self._enable_snapshot_clean and not mask_output and func_records:
40834134
serialized_result = self._serialize_tool_result(result_for_memory)
40844135
self._register_tool_output_for_cache(
40854136
func_name,
40864137
tool_call_id,
40874138
serialized_result,
40884139
cast(List[MemoryRecord], func_records),
4140+
args=args,
4141+
extra_content=extra_content,
40894142
)
40904143

40914144
if isinstance(result, ToolResult) and result.images:

0 commit comments

Comments
 (0)