from __future__ import annotations
import asyncio
import html
import json
import re
from common import PROTOCOL, ROOT, LiveClient, clip, digest, tokens, write_json
from prepare import history_batches, render_history
SUMMARY = (ROOT / "prompts" / "deerflow-default-summary.txt").read_text()
NOTES = (ROOT / "prompts" / "notes.txt").read_text()
async def build_memory(case: dict, client: LiveClient, *, forced_stages: list[list[dict]] | None = None) -> dict:
cid = case["id"]
records = case["records"]
if forced_stages is None:
# Preserve recent records separately, and compact only the old prefix.
recent, count = [], 0
for record in reversed(records):
n = tokens(render_history([record]))
if recent and count + n > PROTOCOL["recent_tail_tokens"]:
break
recent.insert(0, record)
count += n
old = records[:-len(recent)] if recent else records
batches = history_batches(old, PROTOCOL["history_batch_tokens"])
tail = clip(render_history(recent), PROTOCOL["recent_tail_tokens"], tail=True)
else:
stages_copy = [list(stage) for stage in forced_stages]
recent, count = [], 0
for record in reversed(stages_copy[-1]):
n = tokens(render_history([record]))
if recent and count + n > PROTOCOL["recent_tail_tokens"]:
break
recent.insert(0, record)
count += n
if recent:
stages_copy[-1] = stages_copy[-1][:-len(recent)]
batches = [render_history(stage) for stage in stages_copy]
tail = clip(render_history(recent), PROTOCOL["recent_tail_tokens"], tail=True)
signature = digest({"records": records, "batches": batches, "protocol": PROTOCOL,
"summary_prompt": SUMMARY, "notes_prompt": NOTES})
dest = ROOT / "memory" / f"{cid}.json"
previous_summary, previous_notes, stages = "", "", []
if dest.exists():
saved = json.loads(dest.read_text())
if saved.get("signature") == signature:
stages = saved["stages"]
if stages:
previous_summary = stages[-1]["summary"]
previous_notes = stages[-1]["notes"]
if len(stages) == len(batches):
return saved
for i, batch in enumerate(batches):
if i < len(stages):
continue
wrapped = ""
if previous_summary:
wrapped += "\n" + html.escape(previous_summary, quote=False) + "\n\n"
wrapped += "\n" + html.escape(batch, quote=False) + "\n"
summary_prompt = SUMMARY.format(messages=wrapped)
notes_prompt = NOTES.format(previous=previous_notes, history=batch)
summary_result, notes_result = await asyncio.gather(
client.chat([{"role": "user", "content": summary_prompt}], max_tokens=PROTOCOL["summary_max_output_tokens"], tag=f"{cid}:summary:{i}"),
client.chat([{"role": "user", "content": notes_prompt}], max_tokens=PROTOCOL["notes_max_output_tokens"], tag=f"{cid}:notes:{i}"))
previous_summary = summary_result["message"].get("content") or ""
previous_notes = notes_result["message"].get("content") or ""
known_ids = set(re.findall(r"\br\d{5}\b", "\n".join(batches[:i + 1])))
referenced = set(re.findall(r"\br\d{5}\b", previous_notes))
stages.append({"index": i, "input_tokens_proxy": tokens(batch), "summary": previous_summary,
"notes": previous_notes, "invalid_note_refs": sorted(referenced - known_ids),
"summary_request": summary_result["request_hash"], "notes_request": notes_result["request_hash"],
"summary_metrics": summary_result["metrics"], "notes_metrics": notes_result["metrics"]})
write_json(dest, {"id": cid, "signature": signature, "stages": stages, "recent_tail": tail,
"summary": previous_summary, "notes": previous_notes, "total_batches": len(batches)})
print(f"memory {cid} {i+1}/{len(batches)}", flush=True)
return json.loads(dest.read_text())
def reader_context(memory: dict, arm: str, hits: list[dict] | None = None) -> str:
parts = ["\n" + memory["summary"] + "\n"]
if arm != "A":
parts.append("\n" + clip(memory["notes"], PROTOCOL["notes_context_tokens"]) + "\n")
if hits:
parts.append("\n" + "\n\n".join(c["rendered"] for c in hits) + "\n")
if memory["recent_tail"]:
parts.append("\n" + memory["recent_tail"] + "\n")
context = "\n\n".join(parts)
if tokens(context) > PROTOCOL["reader_context_limit_tokens"]:
raise ValueError("Reader memory exceeds preregistered hard cap")
return context