186 lines
7.1 KiB
Python
186 lines
7.1 KiB
Python
"""Persistent, grounded context compaction. No extra GPU generation is needed."""
|
|
|
|
import json
|
|
import math
|
|
import time
|
|
from .bfs_agent_review import text_context
|
|
|
|
|
|
class ContextBudgetError(ValueError):
|
|
pass
|
|
|
|
|
|
def tokens(text):
|
|
# This is a working estimate, not the model tokenizer or measured KV usage.
|
|
return math.ceil(len(text.encode("utf-8")) / 3)
|
|
|
|
|
|
def compact(s, through):
|
|
previous = s.context_summary or {}
|
|
start = min(previous.get("through_event", 0), through)
|
|
facts = list(previous.get("facts", []))
|
|
for event in s.events[start:through]:
|
|
kind = event.get("kind")
|
|
if kind == "action":
|
|
facts.append(
|
|
{
|
|
"type": "requested_tool",
|
|
"tool": event.get("tool"),
|
|
"arguments": text_context(event.get("arguments", {})),
|
|
"note": event.get("summary", "")[:300],
|
|
}
|
|
)
|
|
elif kind == "error":
|
|
facts.append({"type": "failure", "error": event.get("error", "")[:600]})
|
|
elif kind in ("feedback", "reply", "media"):
|
|
facts.append({"type": kind, "note": event.get("summary", "")[:600]})
|
|
elif kind == "observation":
|
|
result = event.get("result", {})
|
|
facts.append(
|
|
{
|
|
"type": "tool_observation",
|
|
"tool": event.get("tool"),
|
|
"result": json.dumps(text_context(result), ensure_ascii=False)[
|
|
:800
|
|
],
|
|
}
|
|
)
|
|
# Older raw events remain available on disk and through inspect_history.
|
|
facts = facts[-16:]
|
|
while len(json.dumps(facts, ensure_ascii=False)) > 5000 and len(facts) > 1:
|
|
facts.pop(0)
|
|
s.context_summary = {
|
|
"facts": facts,
|
|
"through_event": through,
|
|
"compactions": previous.get("compactions", 0) + 1,
|
|
"updated": time.time(),
|
|
"note": "Grounded activity digest; requests are not proof of success. Full events remain saved.",
|
|
}
|
|
s.event(
|
|
"context",
|
|
summary="Condensed older activity; task, feedback, live plan and saved evidence remain available.",
|
|
through_event=through,
|
|
compactions=s.context_summary["compactions"],
|
|
)
|
|
|
|
|
|
def prepare_context(s, system, tools, image_count):
|
|
info = s.vlm_info or {}
|
|
requested = int(s.settings.get("context_window", 32768))
|
|
backend = info.get("context_tokens")
|
|
capacity = (
|
|
min(requested, backend)
|
|
if isinstance(backend, int) and backend > 0
|
|
else requested
|
|
)
|
|
source = (
|
|
"backend configuration + working limit"
|
|
if backend
|
|
else "working limit; backend maximum unknown"
|
|
)
|
|
vision = (
|
|
min(24, image_count) * 512
|
|
) # heuristic for 512px letterboxed images; backend-dependent
|
|
reserve = max(1024, int(capacity * 0.08))
|
|
output = s.settings["max_tokens"]
|
|
text_room = capacity - output - vision - reserve
|
|
state = {
|
|
"revision": s.revision,
|
|
"rendered_revision": s.rendered_revision,
|
|
"current_plan_rendered": s.revision == s.rendered_revision,
|
|
"renders": s.renders,
|
|
"render_limit": s.settings["max_renders"],
|
|
"actions": sum(e["kind"] == "action" for e in s.events),
|
|
"action_limit": s.settings["max_actions"],
|
|
"review_fps": s.settings["review_fps"],
|
|
"review_coverage": {
|
|
k: {a: b for a, b in v.items() if a != "seen"}
|
|
for k, v in s.review_coverage.items()
|
|
},
|
|
}
|
|
# Keep the latest user instruction intact; older corrections stay in the digest/source history.
|
|
applied = [m for m in s.messages if m.get("status") == "applied"]
|
|
latest = applied[-1:] or s.feedback[-1:]
|
|
base = (
|
|
system
|
|
+ "\nTOOLS:\n"
|
|
+ json.dumps(tools, ensure_ascii=False)
|
|
+ "\nTASK:\n"
|
|
+ s.task
|
|
+ "\nLATEST USER INPUT:\n"
|
|
+ json.dumps(text_context(latest), ensure_ascii=False)
|
|
+ "\nGENERATION STATE:\n"
|
|
+ json.dumps(state, ensure_ascii=False)
|
|
)
|
|
if text_room <= tokens(base):
|
|
s.context_usage = {
|
|
"capacity": capacity,
|
|
"capacity_source": source,
|
|
"estimated": True,
|
|
"text_tokens": tokens(base),
|
|
"image_tokens": vision,
|
|
"output_tokens": output,
|
|
"safety_tokens": reserve,
|
|
"total_tokens": tokens(base) + vision + output + reserve,
|
|
"blocked": True,
|
|
}
|
|
raise ContextBudgetError(
|
|
"Working context is too small for instructions, images and the decision budget. Increase Context window only within the model's supported limit, or reduce decision tokens/images/task length."
|
|
)
|
|
through = (s.context_summary or {}).get("through_event", 0)
|
|
recent = s.events[through:]
|
|
raw_size = tokens(json.dumps(text_context(recent), ensure_ascii=False))
|
|
if len(recent) > 24 or raw_size > max(500, (text_room - tokens(base)) // 3):
|
|
cutoff = max(through, len(s.events) - 6)
|
|
if cutoff > through:
|
|
compact(s, cutoff)
|
|
recent = s.events[cutoff:]
|
|
plan = {
|
|
k: v
|
|
for k, v in s.plan.items()
|
|
if k not in ("auto_prompts", "ref_details", "picture_details", "cast")
|
|
}
|
|
if len(plan.get("segs", [])) > 8:
|
|
plan["segs"] = plan["segs"][:8]
|
|
plan["note"] = "More overrides saved; use inspect_plan for resolved shots."
|
|
sections = [
|
|
("LATEST OBSERVATION", text_context(s.observation), 0.29),
|
|
("CURRENT PLAN", text_context(plan), 0.23),
|
|
("PERSISTENT SUMMARY", s.context_summary, 0.18),
|
|
("CHECKLIST", s.checklist, 0.10),
|
|
("EARLIER USER FEEDBACK", s.feedback[-8:], 0.10),
|
|
("RECENT HISTORY", text_context(recent), 0.10),
|
|
]
|
|
remaining = max(0, (text_room - tokens(base) - 200) * 3)
|
|
prompt = base
|
|
for name, value, share in sections:
|
|
content = json.dumps(value, ensure_ascii=False)
|
|
limit = max(80, int(remaining * share))
|
|
if len(content.encode("utf-8")) > limit:
|
|
content = (
|
|
content.encode("utf-8")[:limit].decode("utf-8", errors="ignore")
|
|
+ " [excerpt; inspect_history/inspect_plan for details]"
|
|
)
|
|
prompt += "\n" + name + ":\n" + content
|
|
prompt += (
|
|
"\nImages follow the observation order. Return one registered tool action JSON."
|
|
)
|
|
estimate = tokens(prompt)
|
|
s.context_usage = {
|
|
"capacity": capacity,
|
|
"capacity_source": source,
|
|
"estimated": True,
|
|
"text_tokens": estimate,
|
|
"image_tokens": vision,
|
|
"output_tokens": output,
|
|
"safety_tokens": reserve,
|
|
"total_tokens": estimate + vision + output + reserve,
|
|
"compactions": (s.context_summary or {}).get("compactions", 0),
|
|
"note": "Text bytes/3 and 512 tokens/image estimates; actual template, image and KV usage may differ.",
|
|
}
|
|
if s.context_usage["total_tokens"] > capacity:
|
|
raise ContextBudgetError(
|
|
"Working context estimate still exceeds the limit after compaction. Reduce the decision budget or images; the full history is saved."
|
|
)
|
|
return prompt
|