Files
2026-07-13 12:39:17 +08:00

235 lines
8.4 KiB
Python

from __future__ import annotations
import argparse
import asyncio
import sys
from pathlib import Path
from agents import Runner, SQLiteSession
from agents.run import RunConfig
from agents.sandbox import Manifest, MemoryLayoutConfig, SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Filesystem, Memory, Shell
from agents.sandbox.entries import Dir, File
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient
if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
DEFAULT_MODEL = "gpt-5.6-sol"
GTM_SESSION_ID = "gtm-q2-pipeline-review"
ENGINEERING_SESSION_ID = "eng-invoice-test-fix"
GTM_TURN_1 = (
"Analyze data/leads.csv. Find one promising GTM segment, explain why, and say what "
"follow-up data you need."
)
GTM_TURN_2 = (
"Using your previous GTM analysis, write a short outreach hypothesis and save it to "
"gtm_hypothesis.md."
)
ENGINEERING_TURN = (
"Fix the invoice total bug in src/acme_metrics/report.py, then run the test suite."
)
def _build_manifest() -> Manifest:
return Manifest(
entries={
"data": Dir(
children={
"leads.csv": File(
content=(
b"account,segment,seats,trial_events,monthly_spend\n"
b"Northstar Health,healthcare,240,98,18000\n"
b"Beacon Retail,retail,75,18,4200\n"
b"Apex Fintech,financial-services,180,76,13500\n"
b"Summit Labs,healthcare,52,22,3900\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": Dir(
children={
"acme_metrics": Dir(
children={
"__init__.py": File(
content=b"from .report import format_invoice_total\n"
),
"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": Dir(
children={
"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_gtm_agent(*, model: str, manifest: Manifest) -> SandboxAgent:
return SandboxAgent(
name="GTM analyst",
model=model,
instructions=(
"You are a GTM analyst. Inspect the workspace data before answering. Keep analysis "
"specific and cite file paths you used."
),
default_manifest=manifest,
capabilities=[
# Same layout + same SDK session across turns means one memory conversation.
Memory(
layout=MemoryLayoutConfig(
memories_dir="memories/gtm",
sessions_dir="sessions/gtm",
)
),
Filesystem(),
Shell(),
Filesystem(),
],
)
def _build_engineering_agent(*, model: str, manifest: Manifest) -> SandboxAgent:
return SandboxAgent(
name="Engineering fixer",
model=model,
instructions=(
"You are an engineer. Inspect files before editing, make minimal changes, and verify "
"with tests. 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."
),
default_manifest=manifest,
capabilities=[
# Different layout keeps engineering memory separate even in the same sandbox workspace.
Memory(
layout=MemoryLayoutConfig(
memories_dir="memories/engineering",
sessions_dir="sessions/engineering",
)
),
Shell(),
Filesystem(),
],
)
def _print_tree(
root: Path, label: str, relative_path: str, *, print_file_contents: bool = False
) -> None:
print(f"\n[{label}]")
base = root / relative_path
if not base.exists():
print(f"{relative_path} (missing)")
return
for path in sorted(base.rglob("*")):
if path.is_file():
print(path.relative_to(root))
if print_file_contents:
contents = path.read_text().rstrip()
if not contents:
print(" (empty)")
else:
for line in contents.splitlines():
print(f" {line}")
async def main(*, model: str) -> None:
manifest = _build_manifest()
gtm_agent = _build_gtm_agent(model=model, manifest=manifest)
engineering_agent = _build_engineering_agent(model=model, manifest=manifest)
client = UnixLocalSandboxClient()
sandbox = await client.create(manifest=manifest)
workspace_root = Path(sandbox.state.manifest.root)
try:
async with sandbox:
gtm_conversation_session = SQLiteSession(GTM_SESSION_ID)
gtm_config = RunConfig(
sandbox=SandboxRunConfig(session=sandbox),
workflow_name="GTM memory layout example",
)
gtm_first = await Runner.run(
gtm_agent,
GTM_TURN_1,
session=gtm_conversation_session,
run_config=gtm_config,
)
print("\n[gtm turn 1]")
print(gtm_first.final_output)
# Reuse the SDK session so the model sees prior turns and memory extracts them together.
gtm_second = await Runner.run(
gtm_agent,
GTM_TURN_2,
session=gtm_conversation_session,
run_config=gtm_config,
)
print("\n[gtm turn 2]")
print(gtm_second.final_output)
engineering_conversation_session = SQLiteSession(ENGINEERING_SESSION_ID)
engineering_config = RunConfig(
sandbox=SandboxRunConfig(session=sandbox),
workflow_name="Engineering memory layout example",
)
engineering = await Runner.run(
engineering_agent,
ENGINEERING_TURN,
session=engineering_conversation_session,
run_config=engineering_config,
max_turns=20,
)
print("\n[engineering]")
print(engineering.final_output)
_print_tree(workspace_root, "gtm memory", "memories/gtm")
_print_tree(workspace_root, "engineering memory", "memories/engineering")
_print_tree(workspace_root, "gtm sessions", "sessions/gtm", print_file_contents=True)
_print_tree(
workspace_root,
"engineering sessions",
"sessions/engineering",
print_file_contents=True,
)
finally:
await client.delete(sandbox)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Run two sandbox agents with separate memory layouts in one workspace."
)
parser.add_argument("--model", default=DEFAULT_MODEL, help="Model name to use.")
args = parser.parse_args()
asyncio.run(main(model=args.model))