feat: configure include_contents="none" in subagents and implement history pruning callback for plan, implement and autotune agents - #83
Conversation
…story pruning callback for plan, implement and autotune agents
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to prune intermediate tool history from LLM requests to optimize context window usage. It adds a helper function prune_intermediate_tool_history in callbacks.py and integrates it across various subagents, alongside setting include_contents="none" for specific agents. The review feedback highlights potential runtime errors, such as AttributeError when a part's text attribute is None and TypeError if parts is None. It provides actionable suggestions to safely handle these cases and recommends using dynamic class instantiation (c.__class__) to avoid importing google.genai.types directly.
| parts = getattr(c, "parts", []) | ||
| # Remove tool call/response parts (both raw attributes and serialized logs) | ||
| new_parts = [p for p in parts if not _is_tool_part(p)] | ||
| # Keep the turn if there is at least one meaningful part remaining | ||
| # (not just an orphaned 'For context:' header left after removing tool logs) | ||
| if any(getattr(p, "text", "").strip() != "For context:" for p in new_parts): | ||
| filtered_history.append(types.Content(role=c.role, parts=new_parts)) |
There was a problem hiding this comment.
There are three key improvements here:
- Avoid AttributeError: In the Google GenAI SDK,
Partobjects for non-text components (such as function calls or inline data) can have theirtextattribute set toNone. Callinggetattr(p, "text", "").strip()will returnNone(since the attribute exists but isNone), leading to anAttributeError: 'NoneType' object has no attribute 'strip'at runtime. Using(getattr(p, "text", None) or "").strip()safely handles this. - Dynamic Class Instantiation: Reconstructing the content using
types.Contentintroduces an unnecessary dependency ongoogle.genai.typesand might cause class mismatch issues if the upstream ADK uses a different wrapper class. Usingc.__class__dynamically preserves the original class type and allows removing the unused import oftypeson line 11. - Safety Fallback: Adding
or []togetattr(c, "parts", [])prevents potentialTypeErrorcrashes ifpartsisNone.
| parts = getattr(c, "parts", []) | |
| # Remove tool call/response parts (both raw attributes and serialized logs) | |
| new_parts = [p for p in parts if not _is_tool_part(p)] | |
| # Keep the turn if there is at least one meaningful part remaining | |
| # (not just an orphaned 'For context:' header left after removing tool logs) | |
| if any(getattr(p, "text", "").strip() != "For context:" for p in new_parts): | |
| filtered_history.append(types.Content(role=c.role, parts=new_parts)) | |
| parts = getattr(c, "parts", []) or [] | |
| # Remove tool call/response parts (both raw attributes and serialized logs) | |
| new_parts = [p for p in parts if not _is_tool_part(p)] | |
| # Keep the turn if there is at least one meaningful part remaining | |
| # (not just an orphaned 'For context:' header left after removing tool logs) | |
| if any((getattr(p, "text", None) or "").strip() not in ("", "For context:") for p in new_parts): | |
| filtered_history.append(c.__class__(role=getattr(c, "role", None), parts=new_parts)) |
| parts = getattr(last_content, "parts", []) | ||
| if any(_is_tool_part(p) for p in parts): |
There was a problem hiding this comment.
If parts is explicitly set to None or is missing and defaults to None in the content object, getattr(last_content, "parts", []) might return None. This will cause a TypeError: 'NoneType' object is not iterable when evaluated in the any() generator expression. Ensuring a fallback to an empty list or [] prevents this potential crash.
| parts = getattr(last_content, "parts", []) | |
| if any(_is_tool_part(p) for p in parts): | |
| parts = getattr(last_content, "parts", []) or [] | |
| if any(_is_tool_part(p) for p in parts): |
No description provided.