agently 4.0.7.1__py3-none-any.whl → 4.0.7.2__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.
Files changed (34) hide show
  1. agently/_default_init.py +4 -0
  2. agently/_default_settings.yaml +1 -0
  3. agently/base.py +2 -0
  4. agently/builtins/agent_extensions/ChatSessionExtension.py +2 -2
  5. agently/builtins/agent_extensions/SessionExtension.py +294 -0
  6. agently/builtins/agent_extensions/__init__.py +1 -0
  7. agently/builtins/plugins/PromptGenerator/AgentlyPromptGenerator.py +36 -12
  8. agently/builtins/plugins/Session/AgentlyMemoSession.py +652 -0
  9. agently/builtins/tools/Browse.py +11 -3
  10. agently/builtins/tools/Cmd.py +112 -0
  11. agently/builtins/tools/Search.py +27 -1
  12. agently/builtins/tools/__init__.py +1 -0
  13. agently/core/Agent.py +7 -7
  14. agently/core/ModelRequest.py +0 -4
  15. agently/core/Prompt.py +1 -1
  16. agently/core/Session.py +85 -0
  17. agently/integrations/chromadb.py +4 -4
  18. agently/types/data/__init__.py +2 -0
  19. agently/types/data/prompt.py +6 -1
  20. agently/types/data/tool.py +9 -0
  21. agently/types/plugins/BuiltInTool.py +22 -0
  22. agently/types/plugins/Session.py +159 -0
  23. agently/types/plugins/__init__.py +21 -0
  24. agently/types/plugins/base.py +1 -1
  25. agently/utils/AGENT_UTILS_GUIDE.md +175 -0
  26. agently/utils/DataFormatter.py +6 -2
  27. agently/utils/FunctionShifter.py +3 -2
  28. agently/utils/TimeInfo.py +22 -0
  29. agently/utils/__init__.py +1 -0
  30. agently-4.0.7.2.dist-info/METADATA +433 -0
  31. {agently-4.0.7.1.dist-info → agently-4.0.7.2.dist-info}/RECORD +33 -25
  32. {agently-4.0.7.1.dist-info → agently-4.0.7.2.dist-info}/WHEEL +1 -1
  33. agently-4.0.7.1.dist-info/METADATA +0 -194
  34. {agently-4.0.7.1.dist-info → agently-4.0.7.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,652 @@
1
+ # Copyright 2023-2025 AgentEra(Agently.Tech)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any, Literal, TYPE_CHECKING, cast
19
+ import inspect
20
+ import json
21
+ import uuid
22
+ import yaml
23
+
24
+ from agently.utils import Settings, DataFormatter, DataLocator, FunctionShifter
25
+ from agently.core import ModelRequest
26
+ from agently.types.data import ChatMessage
27
+
28
+ if TYPE_CHECKING:
29
+ from agently.base import Agent
30
+ from agently.types.data import SerializableData, ChatMessageDict
31
+ from agently.types.plugins import (
32
+ MemoResizePolicyHandler,
33
+ MemoResizePolicyAsyncHandler,
34
+ MemoResizeHandler,
35
+ MemoResizeAsyncHandler,
36
+ MemoResizeDecision,
37
+ MemoResizePolicyResult,
38
+ AttachmentSummaryHandler,
39
+ AttachmentSummaryAsyncHandler,
40
+ SessionMode,
41
+ SessionLimit,
42
+ MemoUpdateHandler,
43
+ MemoUpdateAsyncHandler,
44
+ )
45
+
46
+
47
+ class AgentlyMemoSession:
48
+ name = "AgentlyMemoSession"
49
+ DEFAULT_SETTINGS: dict[str, Any] = {}
50
+
51
+ @staticmethod
52
+ def _on_register():
53
+ pass
54
+
55
+ @staticmethod
56
+ def _on_unregister():
57
+ pass
58
+
59
+ def __init__(
60
+ self,
61
+ *,
62
+ policy_handler: "MemoResizePolicyHandler | None" = None,
63
+ resize_handlers: "dict[Literal['lite', 'deep'] | str, MemoResizeHandler] | None" = None,
64
+ attachment_summary_handler: "AttachmentSummaryHandler | None" = None,
65
+ memo_update_handler: "MemoUpdateHandler | None" = None,
66
+ parent_settings: Settings | None = None,
67
+ agent: "Agent | None" = None,
68
+ ):
69
+ self.id = uuid.uuid4().hex
70
+ self.full_chat_history = []
71
+ self.current_chat_history = []
72
+ self.memo = {}
73
+ self._turns = 0
74
+ self._last_resize_turn = 0
75
+ self._memo_cursor = 0
76
+
77
+ self._policy_handler: MemoResizePolicyAsyncHandler = cast(
78
+ "MemoResizePolicyAsyncHandler",
79
+ FunctionShifter.asyncify(policy_handler or self._default_policy_handler),
80
+ )
81
+ if resize_handlers is None:
82
+ resize_handlers = {
83
+ "lite": self._default_lite_resize_handler,
84
+ "deep": self._default_deep_resize_handler,
85
+ }
86
+ self._resize_handlers: dict[Literal["lite", "deep"] | str, MemoResizeAsyncHandler] = {
87
+ key: cast("MemoResizeAsyncHandler", FunctionShifter.asyncify(handler))
88
+ for key, handler in resize_handlers.items()
89
+ }
90
+ self._attachment_summary_handler: AttachmentSummaryAsyncHandler = cast(
91
+ "AttachmentSummaryAsyncHandler",
92
+ FunctionShifter.asyncify(attachment_summary_handler or self._default_attachment_summary_handler),
93
+ )
94
+ self._memo_update_handler: MemoUpdateAsyncHandler = cast(
95
+ "MemoUpdateAsyncHandler",
96
+ FunctionShifter.asyncify(memo_update_handler or self._default_memo_update_handler),
97
+ )
98
+ self.settings = Settings(parent=parent_settings)
99
+ self._agent = agent
100
+
101
+ self.settings.setdefault("session.resize.every_n_turns", 8)
102
+ self.settings.setdefault("session.resize.max_messages_text_length", 12_000)
103
+ self.settings.setdefault("session.resize.max_keep_messages_count", None)
104
+ self.settings.setdefault("session.mode", "lite")
105
+ self.settings.setdefault(
106
+ "session.memo.instruct",
107
+ [
108
+ "Update the memo dictionary based on the provided messages.",
109
+ "Keep stable preferences, constraints, and facts that help future turns.",
110
+ "Add new keys or update existing ones as needed.",
111
+ "Return the updated memo dictionary.",
112
+ ],
113
+ )
114
+
115
+ self.set_settings = self.settings.set_settings
116
+ self.judge_resize = FunctionShifter.syncify(self.async_judge_resize)
117
+ self.resize = FunctionShifter.syncify(self.async_resize)
118
+
119
+ def _is_memo_enabled(self) -> bool:
120
+ enabled = self.settings.get("session.memo.enabled", None)
121
+ if enabled is not None:
122
+ return bool(enabled)
123
+ return str(self.settings.get("session.mode", "lite")) == "memo"
124
+
125
+ def configure(
126
+ self,
127
+ *,
128
+ mode: "SessionMode | None" = None,
129
+ limit: "SessionLimit | None" = None,
130
+ every_n_turns: int | None = None,
131
+ ):
132
+ if mode is not None:
133
+ self.settings.set("session.mode", str(mode))
134
+ if str(mode) == "memo":
135
+ self.settings.set("session.memo.enabled", True)
136
+ elif str(mode) == "lite":
137
+ self.settings.set("session.memo.enabled", False)
138
+ if limit is not None:
139
+ self.settings.set("session.limit", DataFormatter.sanitize(limit))
140
+ if isinstance(limit, dict):
141
+ if "chars" in limit:
142
+ self.settings.set("session.resize.max_messages_text_length", limit["chars"])
143
+ if "messages" in limit:
144
+ self.settings.set("session.resize.max_keep_messages_count", limit["messages"])
145
+ if every_n_turns is not None:
146
+ self.settings.set("session.resize.every_n_turns", every_n_turns)
147
+ return self
148
+
149
+ def set_limit(
150
+ self,
151
+ *,
152
+ chars: int | None = None,
153
+ messages: int | None = None,
154
+ ):
155
+ limit: "SessionLimit" = {}
156
+ if chars is not None:
157
+ limit["chars"] = chars
158
+ if messages is not None:
159
+ limit["messages"] = messages
160
+ if limit:
161
+ return self.configure(limit=limit)
162
+ return self
163
+
164
+ def use_lite(
165
+ self,
166
+ *,
167
+ chars: int | None = None,
168
+ messages: int | None = None,
169
+ every_n_turns: int | None = None,
170
+ ):
171
+ limit: "SessionLimit" = {}
172
+ if chars is not None:
173
+ limit["chars"] = chars
174
+ if messages is not None:
175
+ limit["messages"] = messages
176
+ return self.configure(
177
+ mode="lite",
178
+ limit=limit if limit else None,
179
+ every_n_turns=every_n_turns,
180
+ )
181
+
182
+ def use_memo(
183
+ self,
184
+ *,
185
+ chars: int | None = None,
186
+ messages: int | None = None,
187
+ every_n_turns: int | None = None,
188
+ ):
189
+ limit: "SessionLimit" = {}
190
+ if chars is not None:
191
+ limit["chars"] = chars
192
+ if messages is not None:
193
+ limit["messages"] = messages
194
+ return self.configure(
195
+ mode="memo",
196
+ limit=limit if limit else None,
197
+ every_n_turns=every_n_turns,
198
+ )
199
+
200
+ def _approx_message_chars(self, message: "ChatMessage") -> int:
201
+ content = message.content
202
+ if isinstance(content, str):
203
+ return len(content)
204
+ if isinstance(content, list):
205
+ total = 0
206
+ for part in content:
207
+ if isinstance(part, str):
208
+ total += len(part)
209
+ elif isinstance(part, dict):
210
+ if "text" in part and isinstance(part["text"], str):
211
+ total += len(part["text"])
212
+ else:
213
+ total += len(json.dumps(part, ensure_ascii=False, default=str))
214
+ else:
215
+ total += len(str(part))
216
+ return total
217
+ return len(str(content))
218
+
219
+ def _approx_chat_history_chars(self, chat_history: "list[ChatMessage]") -> int:
220
+ return sum(len(m.role) + 1 + self._approx_message_chars(m) for m in chat_history)
221
+
222
+ def _get_limits(self) -> tuple[Any, Any]:
223
+ max_current_chars = self.settings.get("session.resize.max_messages_text_length", 12_000)
224
+ max_keep_messages_count = self.settings.get("session.resize.max_keep_messages_count", None)
225
+ limit = self.settings.get("session.limit", None)
226
+ if isinstance(limit, dict):
227
+ if "chars" in limit:
228
+ max_current_chars = limit["chars"]
229
+ if "messages" in limit:
230
+ max_keep_messages_count = limit["messages"]
231
+ return max_current_chars, max_keep_messages_count
232
+
233
+ def _get_model_requester(self):
234
+ if self._agent:
235
+ return ModelRequest(
236
+ self._agent.plugin_manager,
237
+ agent_name=self._agent.name,
238
+ parent_settings=self.settings,
239
+ )
240
+ from agently.base import plugin_manager
241
+
242
+ return ModelRequest(
243
+ plugin_manager,
244
+ parent_settings=self.settings,
245
+ )
246
+
247
+ def _serialize_chat_history(self, chat_history: "list[ChatMessage]") -> list[dict[str, Any]]:
248
+ return [DataFormatter.sanitize(message.model_dump()) for message in chat_history]
249
+
250
+ def _dump_data(self) -> dict[str, Any]:
251
+ return {
252
+ "id": self.id,
253
+ "memo": DataFormatter.sanitize(self.memo),
254
+ "full_chat_history": self._serialize_chat_history(self.full_chat_history),
255
+ "current_chat_history": self._serialize_chat_history(self.current_chat_history),
256
+ "settings": DataFormatter.sanitize(self.settings.get()),
257
+ "turns": self._turns,
258
+ "last_resize_turn": self._last_resize_turn,
259
+ "memo_cursor": self._memo_cursor,
260
+ }
261
+
262
+ def to_json(self) -> str:
263
+ return json.dumps(self._dump_data(), ensure_ascii=True)
264
+
265
+ def to_yaml(self) -> str:
266
+ return yaml.safe_dump(self._dump_data(), allow_unicode=False)
267
+
268
+ def _load_data(self, data: dict[str, Any]):
269
+ from agently.types.data.prompt import validate_chat_history
270
+
271
+ if not isinstance(data, dict):
272
+ raise TypeError("Session.load expects a dictionary data object.")
273
+ if "id" in data:
274
+ self.id = str(data["id"])
275
+ if "memo" in data:
276
+ self.memo = data["memo"]
277
+ if "full_chat_history" in data:
278
+ self.full_chat_history = validate_chat_history(data["full_chat_history"])
279
+ if "current_chat_history" in data:
280
+ self.current_chat_history = validate_chat_history(data["current_chat_history"])
281
+ if "settings" in data and isinstance(data["settings"], dict):
282
+ self.settings.update(data["settings"])
283
+ if "turns" in data:
284
+ self._turns = int(data["turns"])
285
+ if "last_resize_turn" in data:
286
+ self._last_resize_turn = int(data["last_resize_turn"])
287
+ if "memo_cursor" in data:
288
+ self._memo_cursor = int(data["memo_cursor"])
289
+ return self
290
+
291
+ def _ensure_list(self, value: Any) -> list[Any]:
292
+ if value is None:
293
+ return []
294
+ if isinstance(value, list):
295
+ return value
296
+ if isinstance(value, tuple):
297
+ return list(value)
298
+ return [value]
299
+
300
+ def _get_memo_instruct(self) -> list[str]:
301
+ base = self._ensure_list(self.settings.get("session.memo.instruct", []))
302
+ return [str(item) for item in base if item is not None]
303
+
304
+ async def _default_memo_update_handler(
305
+ self,
306
+ memo: dict[str, Any],
307
+ messages: "list[ChatMessage]",
308
+ attachments: list[dict[str, Any]],
309
+ settings: Settings,
310
+ ) -> dict[str, Any]:
311
+ requester = self._get_model_requester()
312
+ prompt_input = {
313
+ "current_memo": memo,
314
+ "messages": self._serialize_chat_history(messages),
315
+ "attachments": attachments,
316
+ }
317
+ output_schema = {"memo": (dict[str, Any], "Updated memo dictionary")}
318
+ result = requester.input(prompt_input).instruct(self._get_memo_instruct()).output(output_schema)
319
+ data = await result.async_get_data()
320
+ if isinstance(data, dict) and isinstance(data.get("memo"), dict):
321
+ return data["memo"]
322
+ if isinstance(data, dict):
323
+ return data
324
+ return memo
325
+
326
+ async def _update_memo(
327
+ self,
328
+ memo: dict[str, Any],
329
+ messages: "list[ChatMessage]",
330
+ ) -> dict[str, Any]:
331
+ if not self._is_memo_enabled():
332
+ return memo
333
+ if not messages:
334
+ return memo
335
+ attachments = await self._collect_attachment_refs(messages)
336
+ result = await self._memo_update_handler(memo, messages, attachments, self.settings)
337
+ if inspect.isawaitable(result):
338
+ result = await result
339
+ if isinstance(result, dict):
340
+ return result
341
+ return memo
342
+
343
+ def _chunk_by_max_chars(
344
+ self,
345
+ chat_history: "list[ChatMessage]",
346
+ max_chars: int,
347
+ ) -> list["list[ChatMessage]"]:
348
+ if max_chars <= 0:
349
+ return [chat_history] if chat_history else []
350
+ chunks: list[list[ChatMessage]] = []
351
+ current: list[ChatMessage] = []
352
+ current_chars = 0
353
+ for message in chat_history:
354
+ message_chars = len(message.role) + 1 + self._approx_message_chars(message)
355
+ if current and current_chars + message_chars > max_chars:
356
+ chunks.append(current)
357
+ current = [message]
358
+ current_chars = message_chars
359
+ else:
360
+ current.append(message)
361
+ current_chars += message_chars
362
+ if current:
363
+ chunks.append(current)
364
+ return chunks
365
+
366
+ def load_json(self, value: str):
367
+ self._load_data(json.loads(value))
368
+ return self
369
+
370
+ def load_yaml(self, value: str):
371
+ self._load_data(yaml.safe_load(value))
372
+ return self
373
+
374
+ def _split_by_max_chars(
375
+ self,
376
+ chat_history: "list[ChatMessage]",
377
+ max_chars: int,
378
+ ) -> tuple["list[ChatMessage]", "list[ChatMessage]"]:
379
+ if max_chars <= 0 or not chat_history:
380
+ return [], chat_history
381
+
382
+ kept: list[ChatMessage] = []
383
+ kept_chars = 0
384
+ for message in reversed(chat_history):
385
+ message_chars = len(message.role) + 1 + self._approx_message_chars(message)
386
+ if kept and kept_chars + message_chars > max_chars:
387
+ break
388
+ kept.append(message)
389
+ kept_chars += message_chars
390
+
391
+ kept.reverse()
392
+ pruned = chat_history[: len(chat_history) - len(kept)]
393
+ return pruned, kept
394
+
395
+ def _default_attachment_summary_handler(self, message: "ChatMessage") -> list[dict[str, Any]]:
396
+ content = message.content
397
+ if not isinstance(content, list):
398
+ return []
399
+ summaries: list[dict[str, Any]] = []
400
+ for part in content:
401
+ if isinstance(part, dict):
402
+ payload = part
403
+ elif hasattr(part, "model_dump"):
404
+ payload = part.model_dump()
405
+ else:
406
+ continue
407
+ payload = DataFormatter.sanitize(payload)
408
+ part_type = payload.get("type")
409
+ if not part_type or part_type == "text":
410
+ continue
411
+ ref = None
412
+ for key in ("file", "url", "path", "id", "name"):
413
+ value = DataLocator.locate_path_in_dict(payload, key, default=None)
414
+ if value:
415
+ ref = value
416
+ break
417
+ summaries.append(
418
+ {
419
+ "type": part_type,
420
+ "ref": ref,
421
+ "meta": DataFormatter.sanitize(
422
+ {
423
+ key: payload.get(key)
424
+ for key in ("name", "mime_type", "size", "width", "height", "duration")
425
+ if key in payload
426
+ }
427
+ ),
428
+ }
429
+ )
430
+ return summaries
431
+
432
+ async def _collect_attachment_refs(self, messages: "list[ChatMessage]") -> list[dict[str, Any]]:
433
+ refs: list[dict[str, Any]] = []
434
+ for message in messages:
435
+ result = await self._attachment_summary_handler(message)
436
+ if inspect.isawaitable(result):
437
+ result = await result
438
+ refs.extend(cast(list[dict[str, Any]], result))
439
+ return refs
440
+
441
+ def _ensure_decision(self, result: "MemoResizePolicyResult") -> "MemoResizeDecision | None":
442
+ if result is None:
443
+ return None
444
+ if isinstance(result, str):
445
+ return {"type": result}
446
+ if isinstance(result, dict) and "type" in result:
447
+ return result
448
+ raise TypeError(f"Invalid policy result: {type(result)} {result}")
449
+
450
+ async def _default_policy_handler(
451
+ self,
452
+ full_chat_history: "list[ChatMessage]",
453
+ current_chat_history: "list[ChatMessage]",
454
+ settings: Settings,
455
+ ):
456
+ max_current_chars, max_keep_messages_count = self._get_limits()
457
+ every_n_turns = settings.get("session.resize.every_n_turns", 8)
458
+
459
+ try:
460
+ max_current_chars_int = int(max_current_chars) # type: ignore
461
+ except Exception:
462
+ max_current_chars_int = 12_000
463
+ try:
464
+ every_n_turns_int = int(every_n_turns) # type: ignore
465
+ except Exception:
466
+ every_n_turns_int = 8
467
+
468
+ if max_current_chars_int > 0 and self._approx_chat_history_chars(current_chat_history) >= max_current_chars_int:
469
+ return {"type": "deep", "reason": "max_messages_text_length", "severity": 100}
470
+
471
+ try:
472
+ max_keep_messages_count_int = int(max_keep_messages_count) # type: ignore[arg-type]
473
+ except Exception:
474
+ max_keep_messages_count_int = None
475
+ if max_keep_messages_count_int and len(current_chat_history) > max_keep_messages_count_int:
476
+ return {"type": "lite", "reason": "max_keep_messages_count", "severity": 50}
477
+
478
+ turns_since_last_resize = self._turns - self._last_resize_turn
479
+ if every_n_turns_int > 0 and turns_since_last_resize >= every_n_turns_int:
480
+ return {"type": "lite", "reason": "every_n_turns", "severity": 10}
481
+
482
+ return None
483
+
484
+ async def _default_lite_resize_handler(
485
+ self,
486
+ full_chat_history: "list[ChatMessage]",
487
+ current_chat_history: "list[ChatMessage]",
488
+ memo: "SerializableData",
489
+ settings: Settings,
490
+ ):
491
+ memo_dict: dict[str, Any] = memo if isinstance(memo, dict) else {}
492
+ delta_messages = full_chat_history[self._memo_cursor :]
493
+ if delta_messages:
494
+ memo_dict = await self._update_memo(memo_dict, delta_messages)
495
+ self._memo_cursor = len(full_chat_history)
496
+
497
+ max_current_chars, keep_last_messages = self._get_limits()
498
+ try:
499
+ keep_last_messages_int = max(0, int(keep_last_messages)) # type: ignore[arg-type]
500
+ except Exception:
501
+ keep_last_messages_int = None
502
+ try:
503
+ max_current_chars_int = int(max_current_chars) # type: ignore[arg-type]
504
+ except Exception:
505
+ max_current_chars_int = 12_000
506
+
507
+ if keep_last_messages_int is None:
508
+ pruned, remaining = self._split_by_max_chars(current_chat_history, max_current_chars_int)
509
+ else:
510
+ if keep_last_messages_int == 0 or len(current_chat_history) <= keep_last_messages_int:
511
+ memo_dict["last_resize"] = {"type": "lite", "turn": self._turns, "reason": "lite_resize"}
512
+ return full_chat_history, current_chat_history, memo_dict
513
+ pruned = current_chat_history[:-keep_last_messages_int]
514
+ remaining = current_chat_history[-keep_last_messages_int:]
515
+ if max_current_chars_int > 0:
516
+ extra_pruned, remaining = self._split_by_max_chars(remaining, max_current_chars_int)
517
+ if extra_pruned:
518
+ pruned = pruned + extra_pruned
519
+
520
+ memo_dict["last_resize"] = {"type": "lite", "turn": self._turns, "reason": "lite_resize"}
521
+
522
+ return full_chat_history, remaining, memo_dict
523
+
524
+ async def _default_deep_resize_handler(
525
+ self,
526
+ full_chat_history: "list[ChatMessage]",
527
+ current_chat_history: "list[ChatMessage]",
528
+ memo: "SerializableData",
529
+ settings: Settings,
530
+ ):
531
+ memo_dict: dict[str, Any] = memo if isinstance(memo, dict) else {}
532
+ max_current_chars, keep_last_messages = self._get_limits()
533
+ try:
534
+ max_current_chars_int = int(max_current_chars) # type: ignore[arg-type]
535
+ except Exception:
536
+ max_current_chars_int = 12_000
537
+ for batch in self._chunk_by_max_chars(full_chat_history, max_current_chars_int):
538
+ memo_dict = await self._update_memo(memo_dict, batch)
539
+ self._memo_cursor = len(full_chat_history)
540
+
541
+ try:
542
+ keep_last_messages_int = max(0, int(keep_last_messages)) # type: ignore[arg-type]
543
+ except Exception:
544
+ keep_last_messages_int = None
545
+
546
+ if keep_last_messages_int is None:
547
+ pruned, remaining = self._split_by_max_chars(current_chat_history, max_current_chars_int)
548
+ else:
549
+ if keep_last_messages_int == 0:
550
+ pruned = current_chat_history
551
+ remaining = []
552
+ elif len(current_chat_history) <= keep_last_messages_int:
553
+ pruned = []
554
+ remaining = current_chat_history
555
+ else:
556
+ pruned = current_chat_history[:-keep_last_messages_int]
557
+ remaining = current_chat_history[-keep_last_messages_int:]
558
+ if max_current_chars_int > 0 and remaining:
559
+ extra_pruned, remaining = self._split_by_max_chars(remaining, max_current_chars_int)
560
+ if extra_pruned:
561
+ pruned = pruned + extra_pruned
562
+
563
+ memo_dict["last_resize"] = {"type": "deep", "turn": self._turns, "reason": "deep_resize"}
564
+
565
+ return full_chat_history, remaining, memo_dict
566
+
567
+ def set_policy_handler(self, policy_handler: "MemoResizePolicyHandler"):
568
+ self._policy_handler = cast("MemoResizePolicyAsyncHandler", FunctionShifter.asyncify(policy_handler))
569
+ return self
570
+
571
+ def set_attachment_summary_handler(self, attachment_summary_handler: "AttachmentSummaryHandler"):
572
+ self._attachment_summary_handler = cast(
573
+ "AttachmentSummaryAsyncHandler",
574
+ FunctionShifter.asyncify(attachment_summary_handler),
575
+ )
576
+ return self
577
+
578
+ def set_memo_update_handler(self, memo_update_handler: "MemoUpdateHandler"):
579
+ self._memo_update_handler = cast(
580
+ "MemoUpdateAsyncHandler",
581
+ FunctionShifter.asyncify(memo_update_handler),
582
+ )
583
+ return self
584
+
585
+ def set_resize_handlers(self, resize_type: Literal["lite", "deep"] | str, resize_handler: "MemoResizeHandler"):
586
+ self._resize_handlers[resize_type] = cast(
587
+ "MemoResizeAsyncHandler",
588
+ FunctionShifter.asyncify(resize_handler),
589
+ )
590
+ return self
591
+
592
+ def append_message(self, message: "ChatMessage | ChatMessageDict"):
593
+ if isinstance(message, dict):
594
+ message = ChatMessage(
595
+ role=message["role"],
596
+ content=message["content"],
597
+ )
598
+ self.full_chat_history.append(message)
599
+ self.current_chat_history.append(message)
600
+ if message.role == "assistant":
601
+ self._turns += 1
602
+ return self
603
+
604
+ async def async_judge_resize(self, force: Literal["lite", "deep", False, None] | str = False):
605
+ if force:
606
+ decision: "MemoResizeDecision | None" = {
607
+ "type": str(force) if isinstance(force, str) else "deep",
608
+ "reason": "force",
609
+ "severity": 1000,
610
+ }
611
+ else:
612
+ policy_result = await self._policy_handler(
613
+ self.full_chat_history,
614
+ self.current_chat_history,
615
+ self.settings,
616
+ )
617
+ if inspect.isawaitable(policy_result):
618
+ policy_result = await policy_result
619
+ decision = self._ensure_decision(cast("MemoResizePolicyResult", policy_result))
620
+
621
+ if decision is None:
622
+ return None
623
+
624
+ return decision
625
+
626
+ async def async_resize(self, force: Literal["lite", "deep", False, None] | str = False):
627
+ decision = await self.async_judge_resize(force=force)
628
+ if decision is None:
629
+ return self.current_chat_history
630
+
631
+ resize_type = decision["type"]
632
+ handler = self._resize_handlers.get(resize_type)
633
+ if handler is None:
634
+ raise KeyError(f"Missing resize handler for type: {resize_type}")
635
+
636
+ handler_result = await handler(
637
+ self.full_chat_history,
638
+ self.current_chat_history,
639
+ self.memo,
640
+ self.settings,
641
+ )
642
+ if inspect.isawaitable(handler_result):
643
+ handler_result = await handler_result
644
+ self.full_chat_history, self.current_chat_history, self.memo = handler_result
645
+ if isinstance(self.memo, dict) and isinstance(decision, dict) and decision.get("reason"):
646
+ last_resize = self.memo.get("last_resize")
647
+ if not isinstance(last_resize, dict):
648
+ last_resize = {}
649
+ self.memo["last_resize"] = last_resize
650
+ last_resize["reason"] = decision["reason"] if "reason" in decision else ""
651
+ self._last_resize_turn = self._turns
652
+ return self.current_chat_history
@@ -12,18 +12,26 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
-
15
+ from agently.types.plugins import BuiltInTool
16
16
  from agently.utils import LazyImport
17
17
 
18
18
 
19
- class Browse:
20
-
19
+ class Browse(BuiltInTool):
21
20
  def __init__(
22
21
  self,
23
22
  proxy: str | None = None,
24
23
  timeout: int | None = None,
25
24
  headers: dict[str, str] | None = None,
26
25
  ):
26
+ self.tool_info_list = [
27
+ {
28
+ "name": "browse",
29
+ "desc": "Browse the page at {url}",
30
+ "kwargs": {"url": ("str", "Accessible URL")},
31
+ "func": self.browse,
32
+ }
33
+ ]
34
+
27
35
  LazyImport.import_package("httpx")
28
36
  LazyImport.import_package("bs4", install_name="beautifulsoup4")
29
37
  self.proxy = proxy