Skip to content

Commit 1c40eae

Browse files
feat: add comprehensive logging to agent sandbox (#26)
* feat: add comprehensive logging to agent sandbox - Log each step of sandbox creation - Check environment (which claude, PATH, API key) - Add --output-format stream-json --verbose to claude command - Log each line received from stdout - Log when stream_reader starts/finishes * feat: add logfire logging from inside Modal sandbox
1 parent 64e43a8 commit 1c40eae

3 files changed

Lines changed: 91 additions & 0 deletions

File tree

src/policyengine_api/agent_sandbox.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ def run_claude_code_in_sandbox(
4242
4343
Returns the sandbox and process handle for streaming output.
4444
"""
45+
import logfire
46+
47+
from policyengine_api.config import settings
48+
49+
logfire.info(
50+
"run_claude_code_in_sandbox: starting",
51+
question=question[:100],
52+
api_base_url=api_base_url,
53+
)
54+
4555
# MCP config for Claude Code (type: sse for HTTP SSE transport)
4656
mcp_config = f"""{{
4757
"mcpServers": {{
@@ -53,31 +63,91 @@ def run_claude_code_in_sandbox(
5363
}}"""
5464

5565
# Get reference to deployed app (required when calling from outside Modal)
66+
logfire.info("run_claude_code_in_sandbox: looking up Modal app")
5667
sandbox_app = modal.App.lookup("policyengine-sandbox", create_if_missing=True)
68+
logfire.info("run_claude_code_in_sandbox: Modal app found")
5769

70+
logfire.info("run_claude_code_in_sandbox: creating sandbox")
5871
sb = modal.Sandbox.create(
5972
app=sandbox_app,
6073
image=sandbox_image,
6174
secrets=[anthropic_secret, logfire_secret],
6275
timeout=600,
6376
workdir="/tmp",
6477
)
78+
logfire.info("run_claude_code_in_sandbox: sandbox created")
79+
80+
# Log from inside the sandbox via Python
81+
logfire.info("run_claude_code_in_sandbox: logging from inside sandbox")
82+
escaped_question = question[:50].replace("'", "\\'").replace('"', '\\"')
83+
sandbox_log = sb.exec(
84+
"python",
85+
"-c",
86+
f"""
87+
import logfire
88+
logfire.configure(token='{settings.logfire_token}', service_name='modal-sandbox-inner')
89+
logfire.info('Inside Modal sandbox', question='{escaped_question}')
90+
print('Logfire configured inside sandbox')
91+
""",
92+
)
93+
sandbox_log.wait()
94+
logfire.info(
95+
"run_claude_code_in_sandbox: sandbox inner log result",
96+
stdout=sandbox_log.stdout.read()[:200],
97+
stderr=sandbox_log.stderr.read()[:200],
98+
returncode=sandbox_log.returncode,
99+
)
100+
101+
# Check environment inside sandbox
102+
logfire.info("run_claude_code_in_sandbox: checking sandbox environment")
103+
env_check = sb.exec(
104+
"sh",
105+
"-c",
106+
"which claude && echo PATH=$PATH && echo ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:0:10}...",
107+
)
108+
env_check.wait()
109+
env_stdout = env_check.stdout.read()
110+
env_stderr = env_check.stderr.read()
111+
logfire.info(
112+
"run_claude_code_in_sandbox: env check",
113+
stdout=env_stdout[:500] if env_stdout else None,
114+
stderr=env_stderr[:500] if env_stderr else None,
115+
returncode=env_check.returncode,
116+
)
65117

66118
# Write MCP config
119+
logfire.info("run_claude_code_in_sandbox: writing MCP config")
67120
sb.exec("mkdir", "-p", "/root/.claude")
68121
config_process = sb.exec(
69122
"sh", "-c", f"cat > /root/.claude/mcp_servers.json << 'EOF'\n{mcp_config}\nEOF"
70123
)
71124
config_process.wait()
125+
logfire.info(
126+
"run_claude_code_in_sandbox: MCP config written",
127+
returncode=config_process.returncode,
128+
)
129+
130+
# Verify config was written
131+
verify_process = sb.exec("cat", "/root/.claude/mcp_servers.json")
132+
verify_process.wait()
133+
logfire.info(
134+
"run_claude_code_in_sandbox: MCP config contents",
135+
config=verify_process.stdout.read()[:500],
136+
)
72137

73138
# Run Claude Code with the question
139+
logfire.info("run_claude_code_in_sandbox: starting claude CLI")
74140
process = sb.exec(
75141
"claude",
76142
"-p",
77143
question,
144+
"--output-format",
145+
"stream-json",
146+
"--verbose",
78147
"--allowedTools",
79148
"mcp__policyengine__*,Bash,Read,Grep,Glob,Write,Edit",
80149
)
150+
logfire.info("run_claude_code_in_sandbox: claude CLI process started, returning")
81151

82152
return sb, process
83153

src/policyengine_api/api/agent.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,19 +114,39 @@ async def _stream_modal_sandbox(question: str, api_base_url: str):
114114

115115
def stream_reader():
116116
try:
117+
logfire.info("stream_reader: starting to read stdout")
118+
line_count = 0
117119
for line in process.stdout:
120+
line_count += 1
121+
logfire.info(
122+
"stream_reader: got line",
123+
line_num=line_count,
124+
line_preview=line[:200] if line else None,
125+
)
118126
line_queue.put(("line", line))
127+
logfire.info("stream_reader: stdout exhausted, waiting for process")
119128
process.wait()
129+
logfire.info(
130+
"stream_reader: process finished", returncode=process.returncode
131+
)
120132
if process.returncode != 0:
121133
stderr = process.stderr.read()
134+
logfire.error(
135+
"stream_reader: process failed",
136+
returncode=process.returncode,
137+
stderr=stderr[:500] if stderr else None,
138+
)
122139
line_queue.put(("error", (process.returncode, stderr)))
123140
else:
124141
line_queue.put(("done", process.returncode))
125142
except Exception as e:
143+
logfire.exception("stream_reader: exception", error=str(e))
126144
line_queue.put(("exception", str(e)))
127145

146+
logfire.info("_stream_modal_sandbox: starting reader thread")
128147
reader_thread = threading.Thread(target=stream_reader, daemon=True)
129148
reader_thread.start()
149+
logfire.info("_stream_modal_sandbox: reader thread started, entering main loop")
130150

131151
while True:
132152
try:

src/policyengine_api/api/analysis.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def _safe_float(value: float | None) -> float | None:
4848
return None
4949
return value
5050

51+
5152
# Namespace for deterministic UUIDs
5253
SIMULATION_NAMESPACE = UUID("a1b2c3d4-e5f6-7890-abcd-ef1234567890")
5354
REPORT_NAMESPACE = UUID("b2c3d4e5-f6a7-8901-bcde-f12345678901")

0 commit comments

Comments
 (0)