feat(agents): research loop with budget guards, compaction, depth nudge, finalize

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016DvdRZkyCy9NJpHYDdPqmS
This commit is contained in:
Aryan SinghandClaude Opus 4.8 committed 2026-07-01 16:29:29 -05:00
1 parent 63085113ed
commit b2ae3d6ced
3 files changed
+439

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
"""Rolling message-history compaction — the context safety net.
We let the research history grow into the model's full context window (131k+ on gpt-oss,
absorbed by oMLX's paged SSD KV cache) and only compact when the prompt actually approaches
that limit. When it does, we fold the older turns into a running text summary and keep the
most recent turns verbatim.
"""
from __future__ import annotations
from langchain_core.messages import HumanMessage, SystemMessage
COMPACT_RATIO = 0.9 # compact once the prompt reaches this fraction of the context window
KEEP_LAST = 20 # verbatim recent turns to retain after compaction
_CHARS_PER_TOKEN = 4 # rough fallback when the server doesn't report token counts
def estimate_tokens(messages: list) -> int:
chars = sum(
len(m.content if isinstance(m.content, str) else str(m.content)) for m in messages
)
return chars // _CHARS_PER_TOKEN
def should_compact(prompt_tokens: int, context_length: int, ratio: float = COMPACT_RATIO) -> bool:
return prompt_tokens >= ratio * context_length
def _render(messages: list) -> str:
out = []
for m in messages:
role = m.__class__.__name__.replace("Message", "")
content = m.content if isinstance(m.content, str) else str(m.content)
out.append(f"{role}: {content[:800]}")
return "\n".join(out)
def compact(summarizer_llm, messages: list, running_summary: str, keep_last: int = KEEP_LAST):
"""Return (new_messages, new_summary). Keeps system[0] + a rolling summary + recent turns."""
if len(messages) <= keep_last + 1:
return messages, running_summary
system = messages[0]
older = messages[1:-keep_last]
recent = messages[-keep_last:]
if not older:
return messages, running_summary
prompt = (
"Summarize the research progress so far into concise bullet points. Preserve tickers, "
"prices, key numbers, findings, and any tentative picks.\n\n"
f"Previous summary:\n{running_summary or '(none)'}\n\nNew activity:\n{_render(older)}"
)
try:
resp = summarizer_llm.invoke(
[
SystemMessage(content="You compress research notes faithfully and briefly."),
HumanMessage(content=prompt),
]
)
summary = resp.content if isinstance(resp.content, str) else str(resp.content)
except Exception:
summary = running_summary # on failure, keep the old summary and just trim
new_messages = [system, SystemMessage(content=f"Research summary so far:\n{summary}")] + recent
return new_messages, summary