Skip to content

fix(strategies): guard foresight sender scan against tool-call items - #373

Open
dani1005 wants to merge 1 commit into
mainfrom
fix/foresight-tool-call-items
Open

fix(strategies): guard foresight sender scan against tool-call items#373
dani1005 wants to merge 1 commit into
mainfrom
fix/foresight-tool-call-items

Conversation

@dani1005

Copy link
Copy Markdown
Collaborator

Problem

extract_foresight opens by listing the user senders in the memcell:

sender_ids = sorted({m.sender_id for m in memcell.items if m.role == "user"})

memcell.items is a union. Only ChatMessage carries a role; ToolCallRequest has a sender but no role, and ToolCallResult has neither. So a memcell cut from an agent trajectory raises AttributeError: 'ToolCallRequest' object has no attribute 'role' on the first tool item, and the strategy dies before it extracts anything. Every session containing a tool call loses foresights for all of its users.

Found while recording a Langfuse trace fixture against 1.2.1: a four-round agent trajectory produced three everos.ome.extract_foresight error spans, one per retry.

Change

Guard the attribute read with getattr, matching how the rest of the codebase already walks this union:

call site existing guard
extract/pipeline/user_memory.py:230 getattr(item, "role", None) != "user" — same job, collecting user senders
extract_agent_case.py:131 getattr(item, "sender_id"/"role"/"kind", None)
service/_boundary.py:467 getattr(item, "sender_id", None)

extract_foresight.py:54 was the only unguarded read left, so this completes the pattern rather than introducing a new one.

Semantics are unchanged: a tool call is issued by an agent, so its sender_id could never be a user sender, and ToolCallResult has no sender at all. The set of user senders is exactly the set of role == "user" chat messages either way.

Why the extractor needs no change

ForesightExtractor is already designed to accept an agent-shaped memcell — its docstring states that non-ChatMessage items in memcell.items are silently skipped (agent → user-memory contract), and it filters internally through everalgo's own chat_messages() helper. The crash was purely in our pre-scan, so the fix does not just move the failure one line down.

That helper is deliberately not imported here: it lives in everalgo/user_memory/_render.py, whose module docstring marks it internal and outside the public API.

Testing

New regression test builds a memcell with a ChatMessage plus ToolCallRequest / ToolCallResult and asserts the strategy still finds the one user sender. It fails with AttributeError against the unpatched line and passes with the guard.

tests/unit 1862 passed; ruff check/format, import-linter and the repo gates clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UyKinsWs1MgoARPoB9R4NW

A memcell cut from an agent trajectory carries ToolCallRequest and
ToolCallResult items alongside ChatMessage. Neither has a role, so the
sender scan's bare m.role raised AttributeError and took extract_foresight
down for every session containing a tool call: no foresight is written for
any user in that conversation.

Guarded with getattr, matching the sibling scans in
extract/pipeline/user_memory.py and service/_boundary.py, which already
walk the same union defensively. This was the only unguarded read left.

The extractor itself needs no change: everalgo documents that user-memory
extractors silently skip non-ChatMessage items, and ForesightExtractor
filters internally, so the crash was purely in our pre-scan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UyKinsWs1MgoARPoB9R4NW
@dani1005
dani1005 requested review from Kendrick-Song and cyfyifanchen and removed request for cyfyifanchen July 29, 2026 19:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant