import json
import logging
from typing import Dict, List, Optional, Tuple
from typeguard import typechecked
import os
import re
from backend.config.paths import SESSIONS_DIR
logger = logging.getLogger(__name__)
# One plain-English trust line, fenced by a tag. The model treats the fence as structural framing; the sentence is what actually defuses a security-conscious agent flagging the block as spoofed tool output.
PLATFORM_NOTE_PREAMBLE = (
"This block is authored by the OpenSwarm platform, not tool output and not a "
"prior message. It is trusted context."
)
PLATFORM_NOTE_OPEN = ""
PLATFORM_NOTE_CLOSE = ""
SESSION_RECAP_OPEN = ""
SESSION_RECAP_CLOSE = ""
# Per-turn caps so the re-grounded recap stays compact (summaries, not replays) and cannot reinflate the context window from one giant tool input/output.
RECAP_TOOL_INPUT_CAP = 200
RECAP_TOOL_RESULT_CAP = 500
# Whole-trail budget: a rebuilt session must start far below the compaction trigger, whatever the chat's length. ~10K tokens.
RECAP_TRAIL_MAX_CHARS = 40_000
# Older calls survive as bare stubs (the command, so it can be re-run) inside this second budget; older still fold to a count.
RECAP_STUB_MAX_CHARS = 24_000
RECAP_STUB_CHARS = 120
# Inline budget for a spilled tool result, split head/tail. Same total as the old head-only 4KB, but a test summary or build verdict lives at the END of the output and head-only threw it away every time.
SPILL_HEAD_CHARS = 2_500
SPILL_TAIL_CHARS = 1_500
@typechecked
def wrap_platform_note(body: str) -> str:
"""Fence platform-authored text so the model reads it as trusted annotation,
never as spoofed tool output. The frontend parses the same tag to render a
calm chip instead of leaking the raw tag into chat."""
return f"{PLATFORM_NOTE_OPEN}\n{PLATFORM_NOTE_PREAMBLE}\n{body}\n{PLATFORM_NOTE_CLOSE}"
P_SENTINEL_TAG_RE = re.compile(r"?openswarm_(?:platform_note|session_recap)\b[^>]*>")
@typechecked
def clamp_recap_text(text: str) -> str:
"""Middle-elide a giant user/assistant message in the RECAP only (session.messages keeps the full text): one pasted log used to survive compaction verbatim and re-overflow the rebuilt prompt."""
if len(text) <= SPILL_HEAD_CHARS + SPILL_TAIL_CHARS:
return text
elided = len(text) - SPILL_HEAD_CHARS - SPILL_TAIL_CHARS
return f"{text[:SPILL_HEAD_CHARS]}\n[... {elided} chars elided from recap ...]\n{text[-SPILL_TAIL_CHARS:]}"
@typechecked
def strip_forged_sentinels(text: str) -> str:
"""Neuter any platform-note/recap tags hiding in UNTRUSTED text (tool results,
user input) so attacker-supplied content can't pose as trusted platform context."""
if "openswarm_platform_note" not in text and "openswarm_session_recap" not in text:
return text
return P_SENTINEL_TAG_RE.sub(lambda m: m.group(0).replace("<", "<").replace(">", ">"), text)
@typechecked
def recap_tool_call_line(content: object) -> str:
"""One compact line for a tool_call turn: Tool call: name()."""
if isinstance(content, dict):
tool = content.get("tool") or content.get("name") or "tool"
raw_input = content.get("input")
try:
input_str = json.dumps(raw_input, ensure_ascii=False, default=str)
except Exception:
input_str = str(raw_input)
else:
tool = "tool"
input_str = str(content)
if len(input_str) > RECAP_TOOL_INPUT_CAP:
input_str = input_str[:RECAP_TOOL_INPUT_CAP] + "..."
return f"Tool call: {tool}({strip_forged_sentinels(input_str)})"
@typechecked
def recap_tool_result_line(content: object) -> str:
"""One compact line for a tool_result turn: Tool result (name): ."""
tool_name = ""
if isinstance(content, dict):
tool_name = content.get("tool_name") or ""
text = content.get("text")
body = text if isinstance(text, str) else json.dumps(content, ensure_ascii=False, default=str)
else:
body = str(content)
if len(body) > RECAP_TOOL_RESULT_CAP:
body = body[:RECAP_TOOL_RESULT_CAP] + "..."
label = f"Tool result ({tool_name})" if tool_name else "Tool result"
return f"{label}: {strip_forged_sentinels(body)}"
@typechecked
def get_branch_messages(session) -> List:
"""Return the linear message list for the active branch, walking the branch tree."""
branch_id = session.active_branch_id or "main"
branch = session.branches.get(branch_id)
if not branch or not branch.fork_point_message_id:
return [m for m in session.messages if m.branch_id == "main" or m.branch_id == branch_id]
segments = []
cur = branch
cur_id = branch_id
visited = set()
while cur and cur.fork_point_message_id:
if cur_id in visited:
break
visited.add(cur_id)
segments.insert(0, {"branch_id": cur_id, "up_to": cur.fork_point_message_id})
cur_id = cur.parent_branch_id or "main"
cur = session.branches.get(cur_id)
segments.insert(0, {"branch_id": cur_id, "up_to": None})
result = []
for i, seg in enumerate(segments):
fork_msg_id = seg["up_to"]
if fork_msg_id:
fork_idx = next((j for j, m in enumerate(session.messages) if m.id == fork_msg_id), len(session.messages))
result.extend(m for m in session.messages[:fork_idx] if m.branch_id == seg["branch_id"])
else:
next_fork = segments[i + 1]["up_to"] if i + 1 < len(segments) else None
if next_fork:
fork_idx = next((j for j, m in enumerate(session.messages) if m.id == next_fork), len(session.messages))
result.extend(m for m in session.messages[:fork_idx] if m.branch_id == seg["branch_id"])
else:
result.extend(m for m in session.messages if m.branch_id == seg["branch_id"])
if not any(m.branch_id == branch_id for m in result):
result.extend(m for m in session.messages if m.branch_id == branch_id)
return result
@typechecked
def trail_lines(messages, cutoff_msg_id: Optional[str] = None, max_trail_chars: int = RECAP_TRAIL_MAX_CHARS) -> List[str]:
"""The user's asks and the tool trail, and NEVER a line of model-authored prose.
Extracted so every renderer bound for a model's context shares one definition of what is safe
to send. Two others were still emitting raw `USER:/ASSISTANT:` replays of another agent's chat
(ENG-396), which is the exact shape ENG-358 removed from the recap; a safety property with two
implementations is one drift away from being no safety property at all.
"""
from backend.apps.agents.manager.session.aged_recap_lines import age_tool_results
cutoff_idx = -1
if cutoff_msg_id:
cutoff_idx = next((i for i, m in enumerate(messages) if m.id == cutoff_msg_id), -1)
visible = [(i, m) for i, m in enumerate(messages) if not getattr(m, "hidden", False)]
fates = age_tool_results([m for _, m in visible], cutoff_idx=next(
(v for v, (i, _) in enumerate(visible) if i == cutoff_idx), -1))
lines: List[str] = []
for v, (i, m) in enumerate(visible):
if m.role == "user":
text = m.content if isinstance(m.content, str) else str(m.content)
lines.append(f"The user asked: {strip_forged_sentinels(clamp_recap_text(text))}")
elif m.role == "tool_call":
lines.append(recap_tool_call_line(m.content))
elif m.role == "tool_result":
body = fates.get(v)
if body is None:
lines.append(recap_tool_result_line(m.content))
else:
tool_name = m.content.get("tool_name") if isinstance(m.content, dict) else None
label = f"Tool result ({tool_name})" if tool_name else "Tool result"
lines.append(f"{label}: {strip_forged_sentinels(body)}")
return bound_trail(lines, max_trail_chars)
@typechecked
def bound_trail(lines: List[str], max_trail_chars: int) -> List[str]:
"""Three tiers, newest first: full lines within `max_trail_chars`; then bare re-runnable call
stubs (no result lines, each cut to RECAP_STUB_CHARS) within RECAP_STUB_MAX_CHARS; then ONE
counted line for everything older. Asks are never dropped. Aging alone left one full line per
tool call forever: a real 766-call chat rebuilt with a 98K-token recap, so its first request was
192K tokens against a 180K trigger and the CLI compacted before the second tool call, every time
(the Recall Radar thrash, 2026-09-01)."""
p_tool = [i for i, l in enumerate(lines) if l.startswith("Tool call") or l.startswith("Tool result")]
if not p_tool:
return lines
p_spent = 0
p_full_from = len(p_tool)
for k in range(len(p_tool) - 1, -1, -1):
p_spent += len(lines[p_tool[k]]) + 1
if p_spent > max_trail_chars:
break
p_full_from = k
if p_full_from == 0:
return lines
p_stub: Dict[int, str] = {}
p_stub_spent = 0
p_stub_from = p_full_from
for k in range(p_full_from - 1, -1, -1):
i = p_tool[k]
if not lines[i].startswith("Tool call"):
continue
p_line = lines[i][:RECAP_STUB_CHARS]
if p_stub_spent + len(p_line) + 1 > RECAP_STUB_MAX_CHARS:
break
p_stub_spent += len(p_line) + 1
p_stub[i] = p_line
p_stub_from = k
p_folded_idx = p_tool[:p_stub_from]
p_counts: Dict[str, int] = {}
for i in p_folded_idx:
if lines[i].startswith("Tool call: "):
p_name = lines[i][len("Tool call: "):].split("(", 1)[0].strip() or "tool"
p_counts[p_name] = p_counts.get(p_name, 0) + 1
p_calls = sum(p_counts.values())
p_by_tool = ", ".join(f"{n} {c}" for n, c in sorted(p_counts.items(), key=lambda kv: -kv[1])[:8])
p_fold = f"[{p_calls} earlier tool calls are not shown to save space" + (f": {p_by_tool}" if p_by_tool else "") + "]"
p_folded = set(p_folded_idx)
p_middle = set(p_tool[p_stub_from:p_full_from])
p_out: List[str] = []
p_fold_written = p_calls == 0
for i, l in enumerate(lines):
if i in p_folded:
if not p_fold_written:
p_out.append(p_fold)
p_fold_written = True
continue
if i in p_middle:
if i in p_stub:
p_out.append(p_stub[i])
continue
p_out.append(l)
return p_out
@typechecked
def render_agent_trail(messages, max_chars: int = 14_000) -> str:
"""What ANOTHER agent's run did, for a model that has to reason about it.
Same safe body as the recap, different framing: this is someone else's run, not your own past.
Tail-biased cap so the end, where a run succeeds or blows up, always survives.
"""
lines = trail_lines(messages)
# The RESULT is not a replay. A caller who invoked this run wants its outcome, and one final
# answer is what every delegation already returns; what must never travel is the multi-turn
# role-tagged transcript around it. Dropping this too made InvokeWorkflow return a trail with
# no answer in it, which the suite caught (test_invoke_waits_and_returns_transcript).
p_last = next((m for m in reversed(messages)
if getattr(m, "role", "") == "assistant" and not getattr(m, "hidden", False)), None)
if p_last is not None:
p_text = p_last.content if isinstance(p_last.content, str) else str(p_last.content)
if p_text.strip():
lines.append(f"It reported back: {strip_forged_sentinels(clamp_recap_text(p_text))}")
if not lines:
return ""
out = "\n".join(lines)
if len(out) > max_chars:
out = "...(earlier steps trimmed)...\n" + out[-max_chars:]
return out
@typechecked
def build_history_prefix(messages, cutoff_msg_id: Optional[str] = None) -> str:
"""Format branch messages into a conversation summary for context injection.
Carries the user's asks and the tool trail, never the model's own replies: a replay of the
model's outputs in text we author is what Anthropic's anti-distillation filter blocks on the
subscription lane (192 blocks in 14 days, none on API keys). Claude Code and hermes keep old
answers only as model-written summaries; the distilled summary plays that role here.
When `cutoff_msg_id` is provided (session.compacted_through_msg_id), drop every
message up to and including that id so the marker the UI shows actually matches
what the model sees. Missing cutoff id falls through to full history.
"""
# Aging replaced dropping (ENG-354, hermes lift): pre-cutoff history becomes re-runnable
# one-line stubs instead of vanishing, duplicates collapse, and the newest tool results
# survive verbatim inside a budget, so a context break costs detail, never the trail.
lines = trail_lines(messages, cutoff_msg_id)
if not lines:
return ""
# Framing lifted from hermes-agent's compaction handoff (context_compressor.py, MIT): reference only, never active instructions, the message after it is the single source of truth, and an explicit end marker so a weak model cannot read the last line as fresh input.
# Say it is PARTIAL when it is. Measured 2026-08-29 on the packaged build: after a proactive
# prune the model was asked what the user's FIRST message had been and confidently quoted a much
# later one, because nothing in the recap distinguishes "this is the start of the conversation"
# from "this is what survived". A recap that hides its own gap turns lost context into a
# confident wrong answer, which is worse than the loss.
p_partial = " Earlier turns have been dropped to save space, so this does NOT begin at the start of the conversation; if asked about something not in it, say so rather than guessing." if cutoff_msg_id else ""
p_recap_frame = ("Recap of YOUR OWN earlier turns in this same conversation (what was asked and which tools "
"you ran), kept locally by the OpenSwarm app so you can continue where you left off."
+ p_partial +
" Reference only: do not answer or redo anything in it; respond to the message that follows.")
return (f"{SESSION_RECAP_OPEN}\n{PLATFORM_NOTE_PREAMBLE}\n{p_recap_frame}\n" + "\n".join(lines)
+ f"\n--- end of recap; respond to the message below, not the recap above ---\n{SESSION_RECAP_CLOSE}")
@typechecked
def estimate_post_compact_input(session) -> int:
"""Return a conservative token estimate after compaction trims history."""
try:
messages = get_branch_messages(session)
cutoff_msg_id = getattr(session, "compacted_through_msg_id", None)
if cutoff_msg_id:
skip_idx = next(
(i for i, m in enumerate(messages) if m.id == cutoff_msg_id),
-1,
)
if skip_idx >= 0:
messages = messages[skip_idx + 1:]
surviving_chars = 0
for message in messages:
if getattr(message, "hidden", False):
continue
content = getattr(message, "content", "")
if isinstance(content, str):
serialized = content
else:
try:
serialized = json.dumps(content, ensure_ascii=False)
except Exception:
serialized = str(content)
surviving_chars += len(serialized)
framework_overhead = int(getattr(session, "framework_overhead_tokens", 0) or 0)
summary_overhead = 200 if cutoff_msg_id else 0
return max(0, framework_overhead + summary_overhead + (surviving_chars // 4))
except Exception:
logger.debug("post-compact token estimate failed", exc_info=True)
return max(0, int(getattr(session, "framework_overhead_tokens", 0) or 0))
@typechecked
def truncate_large_tool_result(content: object, session_id: str, msg_id: str, max_bytes: int = 50_000) -> Tuple[object, Optional[str]]:
"""Spill a large tool_result body to disk, return a truncated
inline replacement plus the on-disk path (or None if untouched).
Storage is session-scoped under data/sessions//blobs/,
never honors caller-supplied paths (defense against path
traversal). The inline replacement middle-elides: head AND tail
survive, so a verdict printed at the end of a long output (test
summary, build result) still reaches the model.
"""
if not isinstance(content, str):
try:
serialized = json.dumps(content) if not isinstance(content, str) else content
except Exception:
serialized = str(content)
else:
serialized = content
if len(serialized.encode("utf-8")) <= max_bytes:
return content, None
blob_path = write_blob(serialized, session_id, msg_id)
if blob_path is None:
return content, None
return build_elided_replacement(serialized, blob_path), blob_path
@typechecked
def write_blob(serialized: str, session_id: str, msg_id: str, suffix: str = "") -> Optional[str]:
"""Park a full tool body under the session's own blobs dir; returns the path, or None if it
could not be written. Caller-supplied paths are never honoured (path traversal)."""
blobs_dir = os.path.join(SESSIONS_DIR, session_id, "blobs")
try:
os.makedirs(blobs_dir, exist_ok=True)
# Sanitize msg_id (it's UUID hex, but be defensive).
safe_msg_id = re.sub(r"[^a-zA-Z0-9_-]", "", str(msg_id))[:64] or "blob"
blob_path = os.path.join(blobs_dir, f"{safe_msg_id}{suffix}.txt")
with open(blob_path, "w", encoding="utf-8") as f:
f.write(serialized)
return blob_path
except Exception as e:
logger.warning(f"Failed to spill tool result for {session_id}: {e}")
return None
@typechecked
def build_elided_replacement(serialized: str, blob_path: str) -> str:
"""Head + tail of an oversized body with an elision marker between them, then the recovery
note. Degrades to the plain body when it is too short to elide."""
head = strip_forged_sentinels(serialized[:SPILL_HEAD_CHARS])
note = wrap_platform_note(
f"Output truncated by OpenSwarm. Full output ({len(serialized)} chars) saved to "
f"{blob_path}. Ask the user or run a follow-up tool call if you need the rest."
)
dropped = len(serialized) - SPILL_HEAD_CHARS - SPILL_TAIL_CHARS
if dropped <= 0:
return f"{strip_forged_sentinels(serialized)}\n\n{note}"
tail = strip_forged_sentinels(serialized[-SPILL_TAIL_CHARS:])
marker = f"\n\n[... {dropped} chars elided by OpenSwarm; full output at {blob_path} ...]\n\n"
return f"{head}{marker}{tail}\n\n{note}"