Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
159 lines
5.5 KiB
Python
159 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Runtime compatibility for legacy action-arena response cleanup.
|
|
|
|
The pinned upstream prompt asks the model for ``{arena}``, then removes only
|
|
the closing brace. Current models reliably follow that format, leaving an
|
|
invalid leading brace at the spatial-memory boundary. This module wraps only
|
|
``generate_action_arena`` and maps its output back to an arena that the
|
|
persona can access in the selected sector.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
import json
|
|
import os
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Iterable
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ArenaNormalization:
|
|
value: str
|
|
reason: str | None
|
|
fallback: bool
|
|
|
|
|
|
def _strip_response_wrappers(value: Any) -> str:
|
|
"""Remove response-only braces, quotes, and surrounding whitespace."""
|
|
|
|
return str(value).strip(" \t\r\n{}\"'`")
|
|
|
|
|
|
def normalize_action_arena(
|
|
raw_output: Any,
|
|
accessible_arenas: Iterable[str],
|
|
current_arena: str | None = None,
|
|
) -> ArenaNormalization:
|
|
"""Return an exact accessible arena, using a bounded deterministic fallback.
|
|
|
|
Matching is case-insensitive after removing response wrappers. Invalid
|
|
output falls back to the current arena when it is among the target
|
|
sector's accessible arenas; otherwise it uses the first arena in the
|
|
upstream spatial-memory order. The function never invents an arena or
|
|
returns an output outside ``accessible_arenas``.
|
|
"""
|
|
|
|
allowed = [item.strip() for item in accessible_arenas if item.strip()]
|
|
if not allowed:
|
|
raise RuntimeError("action-arena compatibility has no accessible fallback")
|
|
|
|
raw_text = str(raw_output)
|
|
candidate = _strip_response_wrappers(raw_text)
|
|
by_casefold = {item.casefold(): item for item in allowed}
|
|
matched = by_casefold.get(candidate.casefold())
|
|
if matched is not None:
|
|
if raw_text == matched:
|
|
return ArenaNormalization(matched, None, False)
|
|
reason = "case_insensitive_exact_match"
|
|
if candidate == matched:
|
|
reason = "stripped_response_wrappers"
|
|
return ArenaNormalization(matched, reason, False)
|
|
|
|
if current_arena:
|
|
current = by_casefold.get(current_arena.strip().casefold())
|
|
if current is not None:
|
|
return ArenaNormalization(
|
|
current, "invalid_output_current_arena_fallback", True
|
|
)
|
|
return ArenaNormalization(
|
|
allowed[0], "invalid_output_first_accessible_fallback", True
|
|
)
|
|
|
|
|
|
class CorrectionRecorder:
|
|
"""Crash-resistant append-only writer for credential-free corrections."""
|
|
|
|
def __init__(self) -> None:
|
|
self.path: Path | None = None
|
|
|
|
def set_path(self, path: Path) -> None:
|
|
self.path = path
|
|
|
|
def record(self, row: dict[str, Any]) -> None:
|
|
if self.path is None:
|
|
raise RuntimeError("action-arena correction receipt path is unset")
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
payload = (
|
|
json.dumps(row, ensure_ascii=False, separators=(",", ":")) + "\n"
|
|
).encode("utf-8")
|
|
descriptor = os.open(
|
|
self.path, os.O_APPEND | os.O_CREAT | os.O_WRONLY, 0o600
|
|
)
|
|
try:
|
|
written = os.write(descriptor, payload)
|
|
if written != len(payload):
|
|
raise OSError(
|
|
f"short action-arena correction write: {written}/{len(payload)}"
|
|
)
|
|
os.fsync(descriptor)
|
|
finally:
|
|
os.close(descriptor)
|
|
|
|
|
|
def install() -> CorrectionRecorder:
|
|
"""Install the upstream wrapper once and return its mutable recorder."""
|
|
|
|
from persona.cognitive_modules import plan
|
|
|
|
installed = getattr(plan.generate_action_arena, "_exp10_5_compat", None)
|
|
if installed is not None:
|
|
return installed
|
|
|
|
original = plan.generate_action_arena
|
|
recorder = CorrectionRecorder()
|
|
|
|
def generate_action_arena(
|
|
act_desp: str,
|
|
persona: Any,
|
|
maze: Any,
|
|
act_world: str,
|
|
act_sector: str,
|
|
) -> str:
|
|
raw_output = original(act_desp, persona, maze, act_world, act_sector)
|
|
accessible = [
|
|
item.strip()
|
|
for item in persona.s_mem.get_str_accessible_sector_arenas(
|
|
f"{act_world}:{act_sector}"
|
|
).split(",")
|
|
if item.strip()
|
|
]
|
|
tile = maze.access_tile(persona.scratch.curr_tile)
|
|
current_arena = None
|
|
if tile.get("world") == act_world and tile.get("sector") == act_sector:
|
|
current_arena = tile.get("arena")
|
|
result = normalize_action_arena(raw_output, accessible, current_arena)
|
|
if result.reason is not None:
|
|
recorder.record(
|
|
{
|
|
"schema_version": 1,
|
|
"timestamp_utc": dt.datetime.now(dt.timezone.utc).isoformat(),
|
|
"kind": "action_arena_compatibility_correction",
|
|
"persona": persona.scratch.name,
|
|
"action_description": act_desp,
|
|
"world": act_world,
|
|
"sector": act_sector,
|
|
"raw_output": str(raw_output),
|
|
"normalized_output": result.value,
|
|
"accessible_arenas": accessible,
|
|
"reason": result.reason,
|
|
"fallback": result.fallback,
|
|
}
|
|
)
|
|
return result.value
|
|
|
|
generate_action_arena._exp10_5_compat = recorder # type: ignore[attr-defined]
|
|
plan.generate_action_arena = generate_action_arena
|
|
return recorder
|