camel-ai 0.2.77__py3-none-any.whl → 0.2.79a0__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.

Potentially problematic release.


This version of camel-ai might be problematic. Click here for more details.

@@ -30,7 +30,6 @@ import threading
30
30
  import time
31
31
  import uuid
32
32
  import warnings
33
- from dataclasses import dataclass
34
33
  from datetime import datetime
35
34
  from pathlib import Path
36
35
  from typing import (
@@ -72,7 +71,6 @@ from camel.memories import (
72
71
  MemoryRecord,
73
72
  ScoreBasedContextCreator,
74
73
  )
75
- from camel.memories.blocks.chat_history_block import EmptyMemoryWarning
76
74
  from camel.messages import (
77
75
  BaseMessage,
78
76
  FunctionCallingMessage,
@@ -160,53 +158,6 @@ SIMPLE_FORMAT_PROMPT = TextPrompt(
160
158
  )
161
159
 
162
160
 
163
- @dataclass
164
- class _ToolOutputHistoryEntry:
165
- tool_name: str
166
- tool_call_id: str
167
- result_text: str
168
- record_uuids: List[str]
169
- record_timestamps: List[float]
170
- preview_text: str
171
- cached: bool = False
172
- cache_id: Optional[str] = None
173
-
174
-
175
- class _ToolOutputCacheManager:
176
- r"""Minimal persistent store for caching verbose tool outputs."""
177
-
178
- def __init__(self, base_dir: Union[str, Path]) -> None:
179
- self.base_dir = Path(base_dir).expanduser().resolve()
180
- self.base_dir.mkdir(parents=True, exist_ok=True)
181
-
182
- def save(
183
- self,
184
- tool_name: str,
185
- tool_call_id: str,
186
- content: str,
187
- ) -> Tuple[str, Path]:
188
- cache_id = uuid.uuid4().hex
189
- filename = f"{cache_id}.txt"
190
- path = self.base_dir / filename
191
- header = (
192
- f"# Cached tool output\n"
193
- f"tool_name: {tool_name}\n"
194
- f"tool_call_id: {tool_call_id}\n"
195
- f"cache_id: {cache_id}\n"
196
- f"---\n"
197
- )
198
- path.write_text(f"{header}{content}", encoding="utf-8")
199
- return cache_id, path
200
-
201
- def load(self, cache_id: str) -> str:
202
- path = self.base_dir / f"{cache_id}.txt"
203
- if not path.exists():
204
- raise FileNotFoundError(
205
- f"Cached tool output {cache_id} not found at {path}"
206
- )
207
- return path.read_text(encoding="utf-8")
208
-
209
-
210
161
  class StreamContentAccumulator:
211
162
  r"""Manages content accumulation across streaming responses to ensure
212
163
  all responses contain complete cumulative content."""
@@ -452,19 +403,6 @@ class ChatAgent(BaseAgent):
452
403
  usage. When enabled, removes FUNCTION/TOOL role messages and
453
404
  ASSISTANT messages with tool_calls after each step.
454
405
  (default: :obj:`False`)
455
- enable_tool_output_cache (bool, optional): Whether to offload verbose
456
- historical tool outputs to a local cache and replace them with
457
- lightweight references in memory. Only older tool results whose
458
- payload length exceeds ``tool_output_cache_threshold`` are cached.
459
- (default: :obj:`False`)
460
- tool_output_cache_threshold (int, optional): Minimum character length
461
- of a tool result before it becomes eligible for caching. Values
462
- below or equal to zero disable caching regardless of the toggle.
463
- (default: :obj:`2000`)
464
- tool_output_cache_dir (Optional[Union[str, Path]], optional): Target
465
- directory for cached tool outputs. When omitted, a ``tool_cache``
466
- directory relative to the current working directory is used.
467
- (default: :obj:`None`)
468
406
  retry_attempts (int, optional): Maximum number of retry attempts for
469
407
  rate limit errors. (default: :obj:`3`)
470
408
  retry_delay (float, optional): Initial delay in seconds between
@@ -516,9 +454,6 @@ class ChatAgent(BaseAgent):
516
454
  mask_tool_output: bool = False,
517
455
  pause_event: Optional[Union[threading.Event, asyncio.Event]] = None,
518
456
  prune_tool_calls_from_memory: bool = False,
519
- enable_tool_output_cache: bool = False,
520
- tool_output_cache_threshold: int = 2000,
521
- tool_output_cache_dir: Optional[Union[str, Path]] = None,
522
457
  retry_attempts: int = 3,
523
458
  retry_delay: float = 1.0,
524
459
  step_timeout: Optional[float] = None,
@@ -538,28 +473,6 @@ class ChatAgent(BaseAgent):
538
473
  # Assign unique ID
539
474
  self.agent_id = agent_id if agent_id else str(uuid.uuid4())
540
475
 
541
- self._tool_output_cache_enabled = (
542
- enable_tool_output_cache and tool_output_cache_threshold > 0
543
- )
544
- self._tool_output_cache_threshold = max(0, tool_output_cache_threshold)
545
- self._tool_output_cache_dir: Optional[Path]
546
- self._tool_output_cache_manager: Optional[_ToolOutputCacheManager]
547
- if self._tool_output_cache_enabled:
548
- cache_dir = (
549
- Path(tool_output_cache_dir).expanduser()
550
- if tool_output_cache_dir is not None
551
- else Path("tool_cache")
552
- )
553
- self._tool_output_cache_dir = cache_dir
554
- self._tool_output_cache_manager = _ToolOutputCacheManager(
555
- cache_dir
556
- )
557
- else:
558
- self._tool_output_cache_dir = None
559
- self._tool_output_cache_manager = None
560
- self._tool_output_history: List[_ToolOutputHistoryEntry] = []
561
- self._cache_lookup_tool_name = "retrieve_cached_tool_output"
562
-
563
476
  # Set up memory
564
477
  context_creator = ScoreBasedContextCreator(
565
478
  self.model_backend.token_counter,
@@ -606,8 +519,6 @@ class ChatAgent(BaseAgent):
606
519
  convert_to_function_tool(tool) for tool in (tools or [])
607
520
  ]
608
521
  }
609
- if self._tool_output_cache_enabled:
610
- self._ensure_tool_cache_lookup_tool()
611
522
 
612
523
  # Register agent with toolkits that have RegisteredAgentToolkit mixin
613
524
  if toolkits_to_register_agent:
@@ -644,8 +555,6 @@ class ChatAgent(BaseAgent):
644
555
  r"""Resets the :obj:`ChatAgent` to its initial state."""
645
556
  self.terminated = False
646
557
  self.init_messages()
647
- if self._tool_output_cache_enabled:
648
- self._tool_output_history.clear()
649
558
  for terminator in self.response_terminators:
650
559
  terminator.reset()
651
560
 
@@ -866,178 +775,6 @@ class ChatAgent(BaseAgent):
866
775
  for tool in tools:
867
776
  self.add_tool(tool)
868
777
 
869
- def retrieve_cached_tool_output(self, cache_id: str) -> str:
870
- r"""Load a cached tool output by its cache identifier.
871
-
872
- Args:
873
- cache_id (str): Identifier provided in cached tool messages.
874
-
875
- Returns:
876
- str: The cached content or an explanatory error message.
877
- """
878
- if not self._tool_output_cache_manager:
879
- return "Tool output caching is disabled for this agent instance."
880
-
881
- normalized_cache_id = cache_id.strip()
882
- if not normalized_cache_id:
883
- return "Please provide a non-empty cache_id."
884
-
885
- try:
886
- return self._tool_output_cache_manager.load(normalized_cache_id)
887
- except FileNotFoundError:
888
- return (
889
- f"Cache entry '{normalized_cache_id}' was not found. "
890
- "Verify the identifier and try again."
891
- )
892
-
893
- def _ensure_tool_cache_lookup_tool(self) -> None:
894
- if not self._tool_output_cache_enabled:
895
- return
896
- lookup_name = self._cache_lookup_tool_name
897
- if lookup_name in self._internal_tools:
898
- return
899
- lookup_tool = convert_to_function_tool(
900
- self.retrieve_cached_tool_output
901
- )
902
- self._internal_tools[lookup_tool.get_function_name()] = lookup_tool
903
-
904
- def _serialize_tool_result(self, result: Any) -> str:
905
- if isinstance(result, str):
906
- return result
907
- try:
908
- return json.dumps(result, ensure_ascii=False)
909
- except (TypeError, ValueError):
910
- return str(result)
911
-
912
- def _summarize_tool_result(self, text: str, limit: int = 160) -> str:
913
- normalized = re.sub(r"\s+", " ", text).strip()
914
- if len(normalized) <= limit:
915
- return normalized
916
- return normalized[: max(0, limit - 3)].rstrip() + "..."
917
-
918
- def _register_tool_output_for_cache(
919
- self,
920
- func_name: str,
921
- tool_call_id: str,
922
- result_text: str,
923
- records: List[MemoryRecord],
924
- ) -> None:
925
- if not records:
926
- return
927
-
928
- entry = _ToolOutputHistoryEntry(
929
- tool_name=func_name,
930
- tool_call_id=tool_call_id,
931
- result_text=result_text,
932
- record_uuids=[str(record.uuid) for record in records],
933
- record_timestamps=[record.timestamp for record in records],
934
- preview_text=self._summarize_tool_result(result_text),
935
- )
936
- self._tool_output_history.append(entry)
937
- self._process_tool_output_cache()
938
-
939
- def _process_tool_output_cache(self) -> None:
940
- if (
941
- not self._tool_output_cache_enabled
942
- or not self._tool_output_history
943
- or self._tool_output_cache_manager is None
944
- ):
945
- return
946
-
947
- # Only cache older results; keep the latest expanded for immediate use.
948
- for entry in self._tool_output_history[:-1]:
949
- if entry.cached:
950
- continue
951
- if len(entry.result_text) < self._tool_output_cache_threshold:
952
- continue
953
- self._cache_tool_output_entry(entry)
954
-
955
- def _cache_tool_output_entry(self, entry: _ToolOutputHistoryEntry) -> None:
956
- if self._tool_output_cache_manager is None or not entry.record_uuids:
957
- return
958
-
959
- try:
960
- cache_id, cache_path = self._tool_output_cache_manager.save(
961
- entry.tool_name,
962
- entry.tool_call_id,
963
- entry.result_text,
964
- )
965
- except Exception as exc: # pragma: no cover - defensive
966
- logger.warning(
967
- "Failed to persist cached tool output for %s (%s): %s",
968
- entry.tool_name,
969
- entry.tool_call_id,
970
- exc,
971
- )
972
- return
973
-
974
- timestamp = (
975
- entry.record_timestamps[0]
976
- if entry.record_timestamps
977
- else time.time_ns() / 1_000_000_000
978
- )
979
- reference_message = FunctionCallingMessage(
980
- role_name=self.role_name,
981
- role_type=self.role_type,
982
- meta_dict={
983
- "cache_id": cache_id,
984
- "cached_preview": entry.preview_text,
985
- "cached_tool_output_path": str(cache_path),
986
- },
987
- content="",
988
- func_name=entry.tool_name,
989
- result=self._build_cache_reference_text(entry, cache_id),
990
- tool_call_id=entry.tool_call_id,
991
- )
992
-
993
- chat_history_block = getattr(self.memory, "_chat_history_block", None)
994
- storage = getattr(chat_history_block, "storage", None)
995
- if storage is None:
996
- return
997
-
998
- existing_records = storage.load()
999
- updated_records = [
1000
- record
1001
- for record in existing_records
1002
- if record["uuid"] not in entry.record_uuids
1003
- ]
1004
- new_record = MemoryRecord(
1005
- message=reference_message,
1006
- role_at_backend=OpenAIBackendRole.FUNCTION,
1007
- timestamp=timestamp,
1008
- agent_id=self.agent_id,
1009
- )
1010
- updated_records.append(new_record.to_dict())
1011
- updated_records.sort(key=lambda record: record["timestamp"])
1012
- storage.clear()
1013
- storage.save(updated_records)
1014
-
1015
- logger.info(
1016
- "Cached tool output '%s' (%s) to %s with cache_id=%s",
1017
- entry.tool_name,
1018
- entry.tool_call_id,
1019
- cache_path,
1020
- cache_id,
1021
- )
1022
-
1023
- entry.cached = True
1024
- entry.cache_id = cache_id
1025
- entry.record_uuids = [str(new_record.uuid)]
1026
- entry.record_timestamps = [timestamp]
1027
-
1028
- def _build_cache_reference_text(
1029
- self, entry: _ToolOutputHistoryEntry, cache_id: str
1030
- ) -> str:
1031
- preview = entry.preview_text or "[no preview available]"
1032
- return (
1033
- "[cached tool output]\n"
1034
- f"tool: {entry.tool_name}\n"
1035
- f"cache_id: {cache_id}\n"
1036
- f"preview: {preview}\n"
1037
- f"Use `{self._cache_lookup_tool_name}` with this cache_id to "
1038
- "retrieve the full content."
1039
- )
1040
-
1041
778
  def add_external_tool(
1042
779
  self, tool: Union[FunctionTool, Callable, Dict[str, Any]]
1043
780
  ) -> None:
@@ -1082,8 +819,7 @@ class ChatAgent(BaseAgent):
1082
819
  message: BaseMessage,
1083
820
  role: OpenAIBackendRole,
1084
821
  timestamp: Optional[float] = None,
1085
- return_records: bool = False,
1086
- ) -> Optional[List[MemoryRecord]]:
822
+ ) -> None:
1087
823
  r"""Updates the agent memory with a new message.
1088
824
 
1089
825
  If the single *message* exceeds the model's context window, it will
@@ -1103,29 +839,21 @@ class ChatAgent(BaseAgent):
1103
839
  timestamp (Optional[float], optional): Custom timestamp for the
1104
840
  memory record. If `None`, the current time will be used.
1105
841
  (default: :obj:`None`)
1106
- return_records (bool, optional): When ``True`` the method returns
1107
- the list of :class:`MemoryRecord` objects written to memory.
1108
- (default: :obj:`False`)
1109
-
1110
- Returns:
1111
- Optional[List[MemoryRecord]]: The records that were written when
1112
- ``return_records`` is ``True``; otherwise ``None``.
842
+ (default: obj:`None`)
1113
843
  """
1114
844
 
1115
- written_records: List[MemoryRecord] = []
1116
-
1117
845
  # 1. Helper to write a record to memory
1118
846
  def _write_single_record(
1119
847
  message: BaseMessage, role: OpenAIBackendRole, timestamp: float
1120
848
  ):
1121
- record = MemoryRecord(
1122
- message=message,
1123
- role_at_backend=role,
1124
- timestamp=timestamp,
1125
- agent_id=self.agent_id,
849
+ self.memory.write_record(
850
+ MemoryRecord(
851
+ message=message,
852
+ role_at_backend=role,
853
+ timestamp=timestamp,
854
+ agent_id=self.agent_id,
855
+ )
1126
856
  )
1127
- written_records.append(record)
1128
- self.memory.write_record(record)
1129
857
 
1130
858
  base_ts = (
1131
859
  timestamp
@@ -1140,7 +868,7 @@ class ChatAgent(BaseAgent):
1140
868
  token_limit = context_creator.token_limit
1141
869
  except AttributeError:
1142
870
  _write_single_record(message, role, base_ts)
1143
- return written_records if return_records else None
871
+ return
1144
872
 
1145
873
  # 3. Check if slicing is necessary
1146
874
  try:
@@ -1148,22 +876,20 @@ class ChatAgent(BaseAgent):
1148
876
  [message.to_openai_message(role)]
1149
877
  )
1150
878
 
1151
- with warnings.catch_warnings():
1152
- warnings.filterwarnings("ignore", category=EmptyMemoryWarning)
1153
- _, ctx_tokens = self.memory.get_context()
879
+ _, ctx_tokens = self.memory.get_context()
1154
880
 
1155
881
  remaining_budget = max(0, token_limit - ctx_tokens)
1156
882
 
1157
883
  if current_tokens <= remaining_budget:
1158
884
  _write_single_record(message, role, base_ts)
1159
- return written_records if return_records else None
885
+ return
1160
886
  except Exception as e:
1161
887
  logger.warning(
1162
888
  f"Token calculation failed before chunking, "
1163
889
  f"writing message as-is. Error: {e}"
1164
890
  )
1165
891
  _write_single_record(message, role, base_ts)
1166
- return written_records if return_records else None
892
+ return
1167
893
 
1168
894
  # 4. Perform slicing
1169
895
  logger.warning(
@@ -1184,18 +910,18 @@ class ChatAgent(BaseAgent):
1184
910
 
1185
911
  if not text_to_chunk or not text_to_chunk.strip():
1186
912
  _write_single_record(message, role, base_ts)
1187
- return written_records if return_records else None
913
+ return
1188
914
  # Encode the entire text to get a list of all token IDs
1189
915
  try:
1190
916
  all_token_ids = token_counter.encode(text_to_chunk)
1191
917
  except Exception as e:
1192
918
  logger.error(f"Failed to encode text for chunking: {e}")
1193
919
  _write_single_record(message, role, base_ts) # Fallback
1194
- return written_records if return_records else None
920
+ return
1195
921
 
1196
922
  if not all_token_ids:
1197
923
  _write_single_record(message, role, base_ts) # Nothing to chunk
1198
- return written_records if return_records else None
924
+ return
1199
925
 
1200
926
  # 1. Base chunk size: one-tenth of the smaller of (a) total token
1201
927
  # limit and (b) current remaining budget. This prevents us from
@@ -1261,8 +987,6 @@ class ChatAgent(BaseAgent):
1261
987
  # Increment timestamp slightly to maintain order
1262
988
  _write_single_record(new_msg, role, base_ts + i * 1e-6)
1263
989
 
1264
- return written_records if return_records else None
1265
-
1266
990
  def load_memory(self, memory: AgentMemory) -> None:
1267
991
  r"""Load the provided memory into the agent.
1268
992
 
@@ -1350,6 +1074,10 @@ class ChatAgent(BaseAgent):
1350
1074
  r"""Summarize the agent's current conversation context and persist it
1351
1075
  to a markdown file.
1352
1076
 
1077
+ .. deprecated:: 0.2.80
1078
+ Use :meth:`asummarize` for async/await support and better
1079
+ performance in parallel summarization workflows.
1080
+
1353
1081
  Args:
1354
1082
  filename (Optional[str]): The base filename (without extension) to
1355
1083
  use for the markdown file. Defaults to a timestamped name when
@@ -1369,8 +1097,18 @@ class ChatAgent(BaseAgent):
1369
1097
  Dict[str, Any]: A dictionary containing the summary text, file
1370
1098
  path, status message, and optionally structured_summary if
1371
1099
  response_format was provided.
1100
+
1101
+ See Also:
1102
+ :meth:`asummarize`: Async version for non-blocking LLM calls.
1372
1103
  """
1373
1104
 
1105
+ warnings.warn(
1106
+ "summarize() is synchronous. Consider using asummarize() "
1107
+ "for async/await support and better performance.",
1108
+ DeprecationWarning,
1109
+ stacklevel=2,
1110
+ )
1111
+
1374
1112
  result: Dict[str, Any] = {
1375
1113
  "summary": "",
1376
1114
  "file_path": None,
@@ -1520,11 +1258,29 @@ class ChatAgent(BaseAgent):
1520
1258
  result["status"] = status_message
1521
1259
  return result
1522
1260
 
1523
- base_filename = (
1524
- filename
1525
- if filename
1526
- else f"context_summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}" # noqa: E501
1527
- )
1261
+ # handle structured output if response_format was provided
1262
+ structured_output = None
1263
+ if response_format and response.msgs[-1].parsed:
1264
+ structured_output = response.msgs[-1].parsed
1265
+
1266
+ # determine filename: use provided filename, or extract from
1267
+ # structured output, or generate timestamp
1268
+ if filename:
1269
+ base_filename = filename
1270
+ elif structured_output and hasattr(
1271
+ structured_output, 'task_title'
1272
+ ):
1273
+ # use task_title from structured output for filename
1274
+ task_title = structured_output.task_title
1275
+ clean_title = ContextUtility.sanitize_workflow_filename(
1276
+ task_title
1277
+ )
1278
+ base_filename = (
1279
+ f"{clean_title}_workflow" if clean_title else "workflow"
1280
+ )
1281
+ else:
1282
+ base_filename = f"context_summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}" # noqa: E501
1283
+
1528
1284
  base_filename = Path(base_filename).with_suffix("").name
1529
1285
 
1530
1286
  metadata = context_util.get_session_metadata()
@@ -1535,11 +1291,274 @@ class ChatAgent(BaseAgent):
1535
1291
  }
1536
1292
  )
1537
1293
 
1538
- # Handle structured output if response_format was provided
1294
+ # convert structured output to custom markdown if present
1295
+ if structured_output:
1296
+ # convert structured output to custom markdown
1297
+ summary_content = context_util.structured_output_to_markdown(
1298
+ structured_data=structured_output, metadata=metadata
1299
+ )
1300
+
1301
+ # Save the markdown (either custom structured or default)
1302
+ save_status = context_util.save_markdown_file(
1303
+ base_filename,
1304
+ summary_content,
1305
+ title="Conversation Summary"
1306
+ if not structured_output
1307
+ else None,
1308
+ metadata=metadata if not structured_output else None,
1309
+ )
1310
+
1311
+ file_path = (
1312
+ context_util.get_working_directory() / f"{base_filename}.md"
1313
+ )
1314
+
1315
+ # Prepare result dictionary
1316
+ result_dict = {
1317
+ "summary": summary_content,
1318
+ "file_path": str(file_path),
1319
+ "status": save_status,
1320
+ "structured_summary": structured_output,
1321
+ }
1322
+
1323
+ result.update(result_dict)
1324
+ logger.info("Conversation summary saved to %s", file_path)
1325
+ return result
1326
+
1327
+ except Exception as exc:
1328
+ error_message = f"Failed to summarize conversation context: {exc}"
1329
+ logger.error(error_message)
1330
+ result["status"] = error_message
1331
+ return result
1332
+
1333
+ async def asummarize(
1334
+ self,
1335
+ filename: Optional[str] = None,
1336
+ summary_prompt: Optional[str] = None,
1337
+ response_format: Optional[Type[BaseModel]] = None,
1338
+ working_directory: Optional[Union[str, Path]] = None,
1339
+ ) -> Dict[str, Any]:
1340
+ r"""Asynchronously summarize the agent's current conversation context
1341
+ and persist it to a markdown file.
1342
+
1343
+ This is the async version of summarize() that uses astep() for
1344
+ non-blocking LLM calls, enabling parallel summarization of multiple
1345
+ agents.
1346
+
1347
+ Args:
1348
+ filename (Optional[str]): The base filename (without extension) to
1349
+ use for the markdown file. Defaults to a timestamped name when
1350
+ not provided.
1351
+ summary_prompt (Optional[str]): Custom prompt for the summarizer.
1352
+ When omitted, a default prompt highlighting key decisions,
1353
+ action items, and open questions is used.
1354
+ response_format (Optional[Type[BaseModel]]): A Pydantic model
1355
+ defining the expected structure of the response. If provided,
1356
+ the summary will be generated as structured output and included
1357
+ in the result.
1358
+ working_directory (Optional[str|Path]): Optional directory to save
1359
+ the markdown summary file. If provided, overrides the default
1360
+ directory used by ContextUtility.
1361
+
1362
+ Returns:
1363
+ Dict[str, Any]: A dictionary containing the summary text, file
1364
+ path, status message, and optionally structured_summary if
1365
+ response_format was provided.
1366
+ """
1367
+
1368
+ result: Dict[str, Any] = {
1369
+ "summary": "",
1370
+ "file_path": None,
1371
+ "status": "",
1372
+ }
1373
+
1374
+ try:
1375
+ # Use external context if set, otherwise create local one
1376
+ if self._context_utility is None:
1377
+ if working_directory is not None:
1378
+ self._context_utility = ContextUtility(
1379
+ working_directory=str(working_directory)
1380
+ )
1381
+ else:
1382
+ self._context_utility = ContextUtility()
1383
+ context_util = self._context_utility
1384
+
1385
+ # Get conversation directly from agent's memory
1386
+ messages, _ = self.memory.get_context()
1387
+
1388
+ if not messages:
1389
+ status_message = (
1390
+ "No conversation context available to summarize."
1391
+ )
1392
+ result["status"] = status_message
1393
+ return result
1394
+
1395
+ # Convert messages to conversation text
1396
+ conversation_lines = []
1397
+ for message in messages:
1398
+ role = message.get('role', 'unknown')
1399
+ content = message.get('content', '')
1400
+
1401
+ # Handle tool call messages (assistant calling tools)
1402
+ tool_calls = message.get('tool_calls')
1403
+ if tool_calls and isinstance(tool_calls, (list, tuple)):
1404
+ for tool_call in tool_calls:
1405
+ # Handle both dict and object formats
1406
+ if isinstance(tool_call, dict):
1407
+ func_name = tool_call.get('function', {}).get(
1408
+ 'name', 'unknown_tool'
1409
+ )
1410
+ func_args_str = tool_call.get('function', {}).get(
1411
+ 'arguments', '{}'
1412
+ )
1413
+ else:
1414
+ # Handle object format (Pydantic or similar)
1415
+ func_name = getattr(
1416
+ getattr(tool_call, 'function', None),
1417
+ 'name',
1418
+ 'unknown_tool',
1419
+ )
1420
+ func_args_str = getattr(
1421
+ getattr(tool_call, 'function', None),
1422
+ 'arguments',
1423
+ '{}',
1424
+ )
1425
+
1426
+ # Parse and format arguments for readability
1427
+ try:
1428
+ import json
1429
+
1430
+ args_dict = json.loads(func_args_str)
1431
+ args_formatted = ', '.join(
1432
+ f"{k}={v}" for k, v in args_dict.items()
1433
+ )
1434
+ except (json.JSONDecodeError, ValueError, TypeError):
1435
+ args_formatted = func_args_str
1436
+
1437
+ conversation_lines.append(
1438
+ f"[TOOL CALL] {func_name}({args_formatted})"
1439
+ )
1440
+
1441
+ # Handle tool response messages
1442
+ elif role == 'tool':
1443
+ tool_name = message.get('name', 'unknown_tool')
1444
+ if not content:
1445
+ content = str(message.get('content', ''))
1446
+ conversation_lines.append(
1447
+ f"[TOOL RESULT] {tool_name} → {content}"
1448
+ )
1449
+
1450
+ # Handle regular content messages (user/assistant/system)
1451
+ elif content:
1452
+ conversation_lines.append(f"{role}: {content}")
1453
+
1454
+ conversation_text = "\n".join(conversation_lines).strip()
1455
+
1456
+ if not conversation_text:
1457
+ status_message = (
1458
+ "Conversation context is empty; skipping summary."
1459
+ )
1460
+ result["status"] = status_message
1461
+ return result
1462
+
1463
+ if self._context_summary_agent is None:
1464
+ self._context_summary_agent = ChatAgent(
1465
+ system_message=(
1466
+ "You are a helpful assistant that summarizes "
1467
+ "conversations"
1468
+ ),
1469
+ model=self.model_backend,
1470
+ agent_id=f"{self.agent_id}_context_summarizer",
1471
+ )
1472
+ else:
1473
+ self._context_summary_agent.reset()
1474
+
1475
+ if summary_prompt:
1476
+ prompt_text = (
1477
+ f"{summary_prompt.rstrip()}\n\n"
1478
+ f"AGENT CONVERSATION TO BE SUMMARIZED:\n"
1479
+ f"{conversation_text}"
1480
+ )
1481
+ else:
1482
+ prompt_text = (
1483
+ "Summarize the context information in concise markdown "
1484
+ "bullet points highlighting key decisions, action items.\n"
1485
+ f"Context information:\n{conversation_text}"
1486
+ )
1487
+
1488
+ try:
1489
+ # Use structured output if response_format is provided
1490
+ if response_format:
1491
+ response = await self._context_summary_agent.astep(
1492
+ prompt_text, response_format=response_format
1493
+ )
1494
+ else:
1495
+ response = await self._context_summary_agent.astep(
1496
+ prompt_text
1497
+ )
1498
+
1499
+ # Handle streaming response
1500
+ if isinstance(response, AsyncStreamingChatAgentResponse):
1501
+ # Collect final response
1502
+ final_response = await response
1503
+ response = final_response
1504
+
1505
+ except Exception as step_exc:
1506
+ error_message = (
1507
+ f"Failed to generate summary using model: {step_exc}"
1508
+ )
1509
+ logger.error(error_message)
1510
+ result["status"] = error_message
1511
+ return result
1512
+
1513
+ if not response.msgs:
1514
+ status_message = (
1515
+ "Failed to generate summary from model response."
1516
+ )
1517
+ result["status"] = status_message
1518
+ return result
1519
+
1520
+ summary_content = response.msgs[-1].content.strip()
1521
+ if not summary_content:
1522
+ status_message = "Generated summary is empty."
1523
+ result["status"] = status_message
1524
+ return result
1525
+
1526
+ # handle structured output if response_format was provided
1539
1527
  structured_output = None
1540
1528
  if response_format and response.msgs[-1].parsed:
1541
1529
  structured_output = response.msgs[-1].parsed
1542
- # Convert structured output to custom markdown
1530
+
1531
+ # determine filename: use provided filename, or extract from
1532
+ # structured output, or generate timestamp
1533
+ if filename:
1534
+ base_filename = filename
1535
+ elif structured_output and hasattr(
1536
+ structured_output, 'task_title'
1537
+ ):
1538
+ # use task_title from structured output for filename
1539
+ task_title = structured_output.task_title
1540
+ clean_title = ContextUtility.sanitize_workflow_filename(
1541
+ task_title
1542
+ )
1543
+ base_filename = (
1544
+ f"{clean_title}_workflow" if clean_title else "workflow"
1545
+ )
1546
+ else:
1547
+ base_filename = f"context_summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}" # noqa: E501
1548
+
1549
+ base_filename = Path(base_filename).with_suffix("").name
1550
+
1551
+ metadata = context_util.get_session_metadata()
1552
+ metadata.update(
1553
+ {
1554
+ "agent_id": self.agent_id,
1555
+ "message_count": len(messages),
1556
+ }
1557
+ )
1558
+
1559
+ # convert structured output to custom markdown if present
1560
+ if structured_output:
1561
+ # convert structured output to custom markdown
1543
1562
  summary_content = context_util.structured_output_to_markdown(
1544
1563
  structured_data=structured_output, metadata=metadata
1545
1564
  )
@@ -1583,8 +1602,6 @@ class ChatAgent(BaseAgent):
1583
1602
  None
1584
1603
  """
1585
1604
  self.memory.clear()
1586
- if self._tool_output_cache_enabled:
1587
- self._tool_output_history.clear()
1588
1605
 
1589
1606
  if self.system_message is not None:
1590
1607
  self.update_memory(self.system_message, OpenAIBackendRole.SYSTEM)
@@ -1622,7 +1639,7 @@ class ChatAgent(BaseAgent):
1622
1639
  message.
1623
1640
  """
1624
1641
  self.memory.clear()
1625
- # avoid UserWarning: The `ChatHistoryMemory` is empty.
1642
+ # Write system message to memory if provided
1626
1643
  if self.system_message is not None:
1627
1644
  self.memory.write_record(
1628
1645
  MemoryRecord(
@@ -3015,18 +3032,14 @@ class ChatAgent(BaseAgent):
3015
3032
  base_timestamp = current_time_ns / 1_000_000_000 # Convert to seconds
3016
3033
 
3017
3034
  self.update_memory(
3018
- assist_msg,
3019
- OpenAIBackendRole.ASSISTANT,
3020
- timestamp=base_timestamp,
3021
- return_records=self._tool_output_cache_enabled,
3035
+ assist_msg, OpenAIBackendRole.ASSISTANT, timestamp=base_timestamp
3022
3036
  )
3023
3037
 
3024
3038
  # Add minimal increment to ensure function message comes after
3025
- func_records = self.update_memory(
3039
+ self.update_memory(
3026
3040
  func_msg,
3027
3041
  OpenAIBackendRole.FUNCTION,
3028
3042
  timestamp=base_timestamp + 1e-6,
3029
- return_records=self._tool_output_cache_enabled,
3030
3043
  )
3031
3044
 
3032
3045
  # Record information about this tool call
@@ -3037,20 +3050,6 @@ class ChatAgent(BaseAgent):
3037
3050
  tool_call_id=tool_call_id,
3038
3051
  )
3039
3052
 
3040
- if (
3041
- self._tool_output_cache_enabled
3042
- and not mask_output
3043
- and func_records
3044
- and self._tool_output_cache_manager is not None
3045
- ):
3046
- serialized_result = self._serialize_tool_result(result)
3047
- self._register_tool_output_for_cache(
3048
- func_name,
3049
- tool_call_id,
3050
- serialized_result,
3051
- cast(List[MemoryRecord], func_records),
3052
- )
3053
-
3054
3053
  return tool_record
3055
3054
 
3056
3055
  def _stream(
@@ -4402,9 +4401,6 @@ class ChatAgent(BaseAgent):
4402
4401
  tool_execution_timeout=self.tool_execution_timeout,
4403
4402
  pause_event=self.pause_event,
4404
4403
  prune_tool_calls_from_memory=self.prune_tool_calls_from_memory,
4405
- enable_tool_output_cache=self._tool_output_cache_enabled,
4406
- tool_output_cache_threshold=self._tool_output_cache_threshold,
4407
- tool_output_cache_dir=self._tool_output_cache_dir,
4408
4404
  stream_accumulate=self.stream_accumulate,
4409
4405
  )
4410
4406