Files
openai--openai-agents-python/src/agents/sandbox/memory/phase_one.py
T
2026-07-13 12:39:17 +08:00

127 lines
4.5 KiB
Python

from __future__ import annotations
import json
import re
from pathlib import Path
from ...run_config import RunConfig
from ..config import MemoryGenerateConfig
from ..sandbox_agent import SandboxAgent
from ..util.token_truncation import TruncationPolicy, truncate_text
from .interface import RolloutExtractionArtifacts
from .prompts import (
render_rollout_extraction_prompt,
render_rollout_extraction_user_prompt,
)
_ROLLOUT_SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,79}$")
_ROLLOUT_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
_PHASE_ONE_ROLLOUT_TOKEN_LIMIT = 150_000
_PHASE_ONE_ROLLOUT_OMISSION_MARKER_TEMPLATE = (
"\n\n"
"[rollout content omitted: this phase-one memory prompt contains a truncated view of "
"the saved rollout. original_chars={original_chars}; rendered_chars={rendered_chars}. "
"Do not assume the rendered rollout below is complete.]"
"\n\n"
)
def normalize_rollout_slug(value: str) -> str:
slug = value.strip()
if slug.endswith(".md"):
slug = slug[:-3]
if not _ROLLOUT_SLUG_RE.fullmatch(slug):
raise ValueError(f"Invalid rollout_slug: {value!r}")
return slug
def rollout_id_from_rollout_path(value: str) -> str:
rollout_id = Path(Path(value).name.strip()).stem
if not rollout_id or not _ROLLOUT_ID_RE.fullmatch(rollout_id):
raise ValueError(f"Invalid rollout id for memory: {value!r}")
return rollout_id
def render_phase_one_prompt(*, rollout_contents: str) -> str:
payloads = [json.loads(line) for line in rollout_contents.splitlines() if line.strip()]
if not payloads:
raise ValueError("rollout_contents must contain at least one JSONL record")
payload = payloads[-1]
if len(payloads) == 1:
terminal_metadata: object = payload.get("terminal_metadata", {})
else:
terminal_metadata = {
"segment_count": len(payloads),
"final_terminal_metadata": payload.get("terminal_metadata", {}),
"terminal_states": [
item.get("terminal_metadata", {}).get("terminal_state", "unknown")
for item in payloads
if isinstance(item, dict)
],
}
terminal_metadata_json = json.dumps(
terminal_metadata,
sort_keys=True,
separators=(",", ":"),
indent=2,
)
# TODO: Replace this fixed cap with 70% of the phase-one model's effective
# context window once model metadata is available in the SDK.
truncated_rollout_contents = truncate_text(
rollout_contents,
TruncationPolicy.tokens(_PHASE_ONE_ROLLOUT_TOKEN_LIMIT),
)
if truncated_rollout_contents != rollout_contents:
marker = _PHASE_ONE_ROLLOUT_OMISSION_MARKER_TEMPLATE.format(
original_chars=len(rollout_contents),
rendered_chars=len(truncated_rollout_contents),
)
truncated_rollout_contents = marker + truncated_rollout_contents
return render_rollout_extraction_user_prompt(
terminal_metadata_json=terminal_metadata_json,
rollout_contents=truncated_rollout_contents,
)
def validate_rollout_artifacts(artifacts: RolloutExtractionArtifacts) -> bool:
if (
artifacts.rollout_slug.strip() == ""
and artifacts.rollout_summary.strip() == ""
and artifacts.raw_memory.strip() == ""
):
return False
if (
not artifacts.rollout_slug.strip()
or not artifacts.rollout_summary.strip()
or not artifacts.raw_memory.strip()
):
raise ValueError("Phase 1 returned partially-empty memory artifacts.")
return True
async def run_phase_one(
*,
config: MemoryGenerateConfig,
prompt: str,
run_config: RunConfig,
) -> RolloutExtractionArtifacts:
from ...run import Runner
if config.phase_one_model_settings is None:
agent = SandboxAgent(
name="sandbox-memory-phase-one",
instructions=render_rollout_extraction_prompt(extra_prompt=config.extra_prompt),
output_type=RolloutExtractionArtifacts,
model=config.phase_one_model,
)
else:
agent = SandboxAgent(
name="sandbox-memory-phase-one",
instructions=render_rollout_extraction_prompt(extra_prompt=config.extra_prompt),
output_type=RolloutExtractionArtifacts,
model=config.phase_one_model,
model_settings=config.phase_one_model_settings,
)
result = await Runner.run(agent, prompt, run_config=run_config)
return result.final_output_as(RolloutExtractionArtifacts, raise_if_incorrect_type=True)