Files
tracer-cloud--opensre/core/agent_harness/turns/transcript_compaction.py
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

136 lines
4.1 KiB
Python

"""Runtime/session compaction helpers for long REPL conversations."""
from __future__ import annotations
import os
from dataclasses import dataclass
from typing import Any
DEFAULT_AUTO_COMPACTION_CHARS = 48_000
_KEEP_RECENT_MESSAGES = 8
_SUMMARY_MAX_CHARS = 6_000
@dataclass(frozen=True)
class CompactionResult:
summary: str
before_chars: int
after_chars: int
first_kept_entry_id: str
def _message_chars(messages: list[tuple[str, str]]) -> int:
return sum(len(role) + len(text) + 2 for role, text in messages)
def should_compact(
session: Any,
*,
threshold_chars: int | None = None,
) -> bool:
# Headless / in-memory sessions do not have a persisted ``session.agent``;
# they can never grow past a threshold worth compacting, so treat missing
# attributes as "no compaction needed" rather than raising.
agent = getattr(session, "agent", None)
messages = getattr(agent, "messages", None) if agent is not None else None
if messages is None:
return False
threshold = threshold_chars or _auto_threshold()
return _message_chars(list(messages)) > threshold
def compact_session_branch(
session: Any,
*,
summary: str | None = None,
first_kept_entry_id: str = "",
) -> CompactionResult | None:
"""Compact the live session branch and persist a compaction entry.
The LLM-summary path is intentionally optional at this layer. When callers
do not provide a summary, compaction uses a deterministic fallback so the
shell can always recover space without depending on another provider call.
"""
messages = list(session.agent.messages)
if len(messages) <= _KEEP_RECENT_MESSAGES:
return None
before_chars = _message_chars(messages)
kept = messages[-_KEEP_RECENT_MESSAGES:]
compacted = messages[:-_KEEP_RECENT_MESSAGES]
final_summary = summary or deterministic_summary(compacted)
session.agent.messages = [("assistant", f"Session summary:\n{final_summary}"), *kept]
after_chars = _message_chars(list(session.agent.messages))
session.storage.append_compaction(
session.session_id,
summary=final_summary,
first_kept_entry_id=first_kept_entry_id,
before_chars=before_chars,
after_chars=after_chars,
before_tokens=_estimate_tokens(before_chars),
after_tokens=_estimate_tokens(after_chars),
)
return CompactionResult(
summary=final_summary,
before_chars=before_chars,
after_chars=after_chars,
first_kept_entry_id=first_kept_entry_id,
)
def auto_compact_if_needed(
session: Any,
*,
threshold_chars: int | None = None,
) -> CompactionResult | None:
if not should_compact(session, threshold_chars=threshold_chars):
return None
return compact_session_branch(session)
def deterministic_summary(messages: list[tuple[str, str]]) -> str:
if not messages:
return ""
first = _render_message_excerpt(messages[:4])
recent = _render_message_excerpt(messages[-4:]) if len(messages) > 4 else ""
parts = [
f"Compacted {len(messages)} earlier conversation messages.",
"Earlier context:",
first,
]
if recent:
parts.extend(["Most recent compacted context:", recent])
return "\n".join(part for part in parts if part).strip()[:_SUMMARY_MAX_CHARS]
def _render_message_excerpt(messages: list[tuple[str, str]]) -> str:
lines: list[str] = []
for role, text in messages:
compact = " ".join(str(text).split())
if len(compact) > 700:
compact = compact[:697] + "..."
lines.append(f"- {role}: {compact}")
return "\n".join(lines)
def _estimate_tokens(chars: int) -> int:
return max(1, chars // 4) if chars else 0
def _auto_threshold() -> int:
raw = os.getenv("OPENSRE_SESSION_COMPACTION_CHARS", "").strip()
if raw.isdigit():
return max(1_000, int(raw))
return DEFAULT_AUTO_COMPACTION_CHARS
__all__ = [
"CompactionResult",
"DEFAULT_AUTO_COMPACTION_CHARS",
"auto_compact_if_needed",
"compact_session_branch",
"deterministic_summary",
"should_compact",
]