chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from agents import Runner
|
||||
from agents.run import RunConfig
|
||||
from agents.sandbox import LocalSnapshotSpec, Manifest, SandboxAgent, SandboxRunConfig
|
||||
from agents.sandbox.capabilities import Filesystem, Memory, Shell
|
||||
from agents.sandbox.entries import File
|
||||
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient
|
||||
from agents.sandbox.session.base_sandbox_session import BaseSandboxSession
|
||||
|
||||
if __package__ is None or __package__ == "":
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
|
||||
DEFAULT_MODEL = "gpt-5.6-sol"
|
||||
FIRST_PROMPT = "Inspect workspace and fix invoice total bug in src/acme_metrics/report.py."
|
||||
SECOND_PROMPT = "Add a regression test for the previous bug you fixed."
|
||||
|
||||
|
||||
def _build_manifest() -> Manifest:
|
||||
return Manifest(
|
||||
entries={
|
||||
"README.md": File(
|
||||
content=(
|
||||
b"# Acme Metrics\n\n"
|
||||
b"Small demo package for validating invoice total formatting.\n"
|
||||
)
|
||||
),
|
||||
"pyproject.toml": File(
|
||||
content=(
|
||||
b"[project]\n"
|
||||
b'name = "acme-metrics"\n'
|
||||
b'version = "0.1.0"\n'
|
||||
b'requires-python = ">=3.10"\n'
|
||||
b"\n"
|
||||
b"[tool.pytest.ini_options]\n"
|
||||
b'pythonpath = ["src"]\n'
|
||||
)
|
||||
),
|
||||
"src/acme_metrics/__init__.py": File(
|
||||
content=b"from .report import format_invoice_total\n"
|
||||
),
|
||||
"src/acme_metrics/report.py": File(
|
||||
content=(
|
||||
b"from __future__ import annotations\n\n"
|
||||
b"def format_invoice_total(subtotal: float, tax_rate: float) -> str:\n"
|
||||
b" total = subtotal + tax_rate\n"
|
||||
b' return f"${total:.2f}"\n'
|
||||
)
|
||||
),
|
||||
"tests/test_report.py": File(
|
||||
content=(
|
||||
b"from acme_metrics import format_invoice_total\n\n\n"
|
||||
b"def test_format_invoice_total_applies_tax_rate() -> None:\n"
|
||||
b' assert format_invoice_total(100.0, 0.075) == "$107.50"\n'
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _build_agent(*, model: str, manifest: Manifest) -> SandboxAgent:
|
||||
# This one user-facing agent can read existing memory, update stale memory in place, and
|
||||
# generate new background memories when the sandbox session closes.
|
||||
return SandboxAgent(
|
||||
name="Sandbox Memory Demo",
|
||||
model=model,
|
||||
instructions=(
|
||||
"Answer questions about the sandbox workspace. Inspect files before answering, make "
|
||||
"minimal edits, and keep the response concise. "
|
||||
"Use the shell tool to inspect and validate the workspace. Use apply_patch for text "
|
||||
"edits when it is the clearest option. Use a non-login POSIX shell for commands. "
|
||||
"Make one focused pytest attempt; if the local sandbox blocks Python or toolchain "
|
||||
"access, report that validation was blocked and finish instead of retrying repeatedly. "
|
||||
"Do not invent files you did not read."
|
||||
),
|
||||
default_manifest=manifest,
|
||||
capabilities=[
|
||||
# `Memory()` enables both read and generate behavior with live updates on by default.
|
||||
Memory(),
|
||||
Filesystem(),
|
||||
Shell(),
|
||||
],
|
||||
# `Memory()` is the recommended default. If you need to tune the behavior, you can switch
|
||||
# to an explicit config such as:
|
||||
#
|
||||
# Memory(
|
||||
# layout=MemoryLayoutConfig(memories_dir="agent_memory", sessions_dir="agent_sessions"),
|
||||
# read=MemoryReadConfig(live_update=False),
|
||||
# generate=MemoryGenerateConfig(max_raw_memories_for_consolidation=128),
|
||||
# )
|
||||
#
|
||||
# `generate.max_raw_memories_for_consolidation`: cap how many recent raw memories are
|
||||
# considered during consolidation. Older conversation-specific guidance may be removed from
|
||||
# consolidated memory when the cap is exceeded.
|
||||
#
|
||||
# Multi-turn conversations work best when all turns share the same live sandbox session and
|
||||
# an SDK Session. The SDK session_id groups those runs into one memory conversation. Without
|
||||
# an SDK session, sandbox memory falls back to OpenAI conversation_id, then RunConfig
|
||||
# group_id, then one generated memory conversation for each Runner.run().
|
||||
#
|
||||
# `read.live_update=False`: use this when the agent should not repair stale memory during
|
||||
# the run. That can save a few seconds, but stale memory debt can accumulate until a later
|
||||
# consolidation, which may or may not catch the staleness. It also prevents the agent from
|
||||
# updating memory immediately during the run, including when the user explicitly asks it to
|
||||
# remember something new or revise existing memory.
|
||||
#
|
||||
# If you need additional memory-generation guidance, `generate.extra_prompt` is appended to the
|
||||
# built-in memory prompt. Keep it short, ideally a few focused bullets and well under ~5k
|
||||
# tokens, so the model still pays attention to the conversation evidence.
|
||||
#
|
||||
# Memory(
|
||||
# generate=MemoryGenerateConfig(
|
||||
# extra_prompt="Pay extra attention to documenting what bug was fixed and why it happened."
|
||||
# )
|
||||
# )
|
||||
)
|
||||
|
||||
|
||||
def _artifact_paths(
|
||||
*, memories_dir: str = "memories", sessions_dir: str = "sessions"
|
||||
) -> tuple[Path, ...]:
|
||||
return (
|
||||
Path(sessions_dir),
|
||||
Path(memories_dir) / "MEMORY.md",
|
||||
Path(memories_dir) / "memory_summary.md",
|
||||
Path(memories_dir) / "raw_memories.md",
|
||||
Path(memories_dir) / "raw_memories",
|
||||
Path(memories_dir) / "rollout_summaries",
|
||||
)
|
||||
|
||||
|
||||
def _print_memory_tree(workspace_root: Path) -> None:
|
||||
print("\nGenerated memory artifacts:")
|
||||
for relative_path in _artifact_paths():
|
||||
full_path = workspace_root / relative_path
|
||||
if not full_path.exists():
|
||||
print(f"- {relative_path} (missing)")
|
||||
continue
|
||||
|
||||
if full_path.is_dir():
|
||||
print(f"- {relative_path}/")
|
||||
for child in sorted(full_path.iterdir()):
|
||||
print(f" - {relative_path / child.name}")
|
||||
if relative_path == Path("sessions"):
|
||||
contents = child.read_text().rstrip()
|
||||
if not contents:
|
||||
print(" (empty)")
|
||||
else:
|
||||
for line in contents.splitlines():
|
||||
print(f" {line}")
|
||||
continue
|
||||
|
||||
print(f"- {relative_path}")
|
||||
print(full_path.read_text().rstrip() or "(empty)")
|
||||
|
||||
|
||||
def _run_config(*, sandbox: BaseSandboxSession, workflow_name: str) -> RunConfig:
|
||||
return RunConfig(
|
||||
sandbox=SandboxRunConfig(session=sandbox),
|
||||
workflow_name=workflow_name,
|
||||
tracing_disabled=True,
|
||||
)
|
||||
|
||||
|
||||
async def main(*, model: str) -> None:
|
||||
manifest = _build_manifest()
|
||||
agent = _build_agent(model=model, manifest=manifest)
|
||||
client = UnixLocalSandboxClient()
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="sandbox-memory-example-") as snapshot_dir:
|
||||
# Use a local snapshot so the second run resumes the same workspace in a new sandbox
|
||||
# session. That makes the second prompt rely on memory instead of in-process agent state.
|
||||
sandbox = await client.create(
|
||||
manifest=manifest,
|
||||
snapshot=LocalSnapshotSpec(base_path=Path(snapshot_dir)),
|
||||
)
|
||||
workspace_root = Path(sandbox.state.manifest.root)
|
||||
|
||||
try:
|
||||
async with sandbox:
|
||||
# Run 1 fixes the bug and generates memory artifacts when the session closes.
|
||||
first = await Runner.run(
|
||||
agent,
|
||||
FIRST_PROMPT,
|
||||
run_config=_run_config(
|
||||
sandbox=sandbox,
|
||||
workflow_name="Sandbox memory example: initial fix",
|
||||
),
|
||||
max_turns=20,
|
||||
)
|
||||
print("\n[first run]")
|
||||
print(first.final_output)
|
||||
|
||||
resumed_sandbox = await client.resume(sandbox.state)
|
||||
async with resumed_sandbox:
|
||||
# Run 2 starts from the resumed snapshot and reads the memory generated by run 1
|
||||
# before answering the follow-up prompt.
|
||||
second = await Runner.run(
|
||||
agent,
|
||||
SECOND_PROMPT,
|
||||
run_config=_run_config(
|
||||
sandbox=resumed_sandbox,
|
||||
workflow_name="Sandbox memory example: follow-up",
|
||||
),
|
||||
max_turns=20,
|
||||
)
|
||||
print("\n[second run]")
|
||||
print(second.final_output)
|
||||
|
||||
_print_memory_tree(workspace_root)
|
||||
finally:
|
||||
await client.delete(sandbox)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run one sandbox agent twice across a snapshot resume with shared memory."
|
||||
)
|
||||
parser.add_argument("--model", default=DEFAULT_MODEL, help="Model name to use.")
|
||||
args = parser.parse_args()
|
||||
asyncio.run(main(model=args.model))
|
||||
Reference in New Issue
Block a user