Initial commit
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
|
||||
MAX_CHARS = 2000
|
||||
LAST_MSG_BUDGET = 4000
|
||||
HEAD_RATIO = 0.5
|
||||
|
||||
|
||||
def compact(content: str, budget: int) -> str:
|
||||
if len(content) <= budget:
|
||||
return content
|
||||
head = int(budget * HEAD_RATIO)
|
||||
tail = budget - head - 50
|
||||
return (
|
||||
content[:head]
|
||||
+ f"\n...[truncated {len(content)-head-tail} chars]...\n"
|
||||
+ content[-tail:]
|
||||
)
|
||||
|
||||
|
||||
def _unwrap_text_content(value: str) -> str:
|
||||
prefix = "@{type=text; text="
|
||||
if value.startswith(prefix) and value.endswith("}"):
|
||||
return value[len(prefix) : -1]
|
||||
return value
|
||||
|
||||
|
||||
def _tool_result_text(event: dict[str, Any]) -> str:
|
||||
parts: list[str] = []
|
||||
content_items = event.get("content")
|
||||
if isinstance(content_items, list):
|
||||
for item in content_items:
|
||||
if not isinstance(item, dict) or item.get("type") != "content":
|
||||
continue
|
||||
value = item.get("content")
|
||||
if isinstance(value, str) and value:
|
||||
parts.append(_unwrap_text_content(value))
|
||||
elif (
|
||||
isinstance(value, dict)
|
||||
and value.get("type") == "text"
|
||||
and isinstance(value.get("text"), str)
|
||||
and value["text"]
|
||||
):
|
||||
parts.append(value["text"])
|
||||
return "\n".join(parts) if parts else "(no output)"
|
||||
|
||||
|
||||
def acp_events_to_state(events: Iterable[dict[str, Any]]) -> list[dict[str, str]]:
|
||||
merged: list[dict[str, str]] = []
|
||||
for event in events:
|
||||
event_type = event.get("type")
|
||||
role: str | None = None
|
||||
content: str | None = None
|
||||
if event_type == "user_message":
|
||||
role, content = "user", event.get("text")
|
||||
elif event_type == "agent_thought":
|
||||
role, content = "assistant", event.get("text")
|
||||
elif event_type == "agent_message":
|
||||
role, content = "assistant", event.get("text")
|
||||
if content == "":
|
||||
continue
|
||||
elif event_type == "tool_call":
|
||||
role = "user"
|
||||
kind = event.get("kind", "other")
|
||||
status = event.get("status", "unknown")
|
||||
content = f"Tool result ({kind}; {status}):\n{_tool_result_text(event)}"
|
||||
elif event_type == "agent_timeout":
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
if not isinstance(content, str):
|
||||
raise ValueError(f"{event_type} event has a non-string text/content value")
|
||||
if merged and merged[-1]["role"] == role:
|
||||
merged[-1]["content"] += "\n" + content
|
||||
else:
|
||||
merged.append({"role": role, "content": content})
|
||||
|
||||
if not merged:
|
||||
raise ValueError("trajectory produced an empty state")
|
||||
if merged[0]["role"] != "user":
|
||||
raise ValueError("state must start with a user message")
|
||||
|
||||
seen: set[str] = set()
|
||||
last_index = len(merged) - 1
|
||||
for index, message in enumerate(merged):
|
||||
budget = LAST_MSG_BUDGET if index == last_index else MAX_CHARS
|
||||
content = compact(message["content"], budget)
|
||||
if len(content) > 200:
|
||||
digest = hashlib.md5(content.encode("utf-8")).hexdigest()
|
||||
if digest in seen:
|
||||
content = "[same as previous tool result]"
|
||||
else:
|
||||
seen.add(digest)
|
||||
message["content"] = content
|
||||
return merged
|
||||
|
||||
|
||||
def read_acp_events(path: Path) -> list[dict[str, Any]]:
|
||||
events: list[dict[str, Any]] = []
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
for line_number, line in enumerate(handle, 1):
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
event = json.loads(line)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"invalid JSON on line {line_number}: {exc.msg}") from exc
|
||||
if not isinstance(event, dict):
|
||||
raise ValueError(f"line {line_number} is not a JSON object")
|
||||
events.append(event)
|
||||
return events
|
||||
|
||||
|
||||
def read_acp_state(path: Path) -> list[dict[str, str]]:
|
||||
return acp_events_to_state(read_acp_events(path))
|
||||
Reference in New Issue
Block a user