A minimal, readable ReAct-style agent that plans, executes tools in a sandbox, observes results, and recovers from failures. Built to be a clean reference implementation you can actually read in an afternoon.
User input ─► Plan (LLM, JSON steps)
└─► for each step:
Analyze ─► Execute tool ─► Observe ─► Update context
│
└─ on failure ─► Recovery steps
└─► Final synthesis
- Plan-then-act loop with an explicit OODA-style iteration per step
- Pluggable tools — ships with:
terminal_exec— shell commands,read_file,write_filepython_runner— executes Python with auto-install onModuleNotFoundErrorand LLM-driven self-heal on runtime errors
- E2B sandbox for safe, isolated execution
- Live workspace sync — files written by the agent appear under
output_workspaces/<plan_id>/in real time - Structured LLM output channels via
<analysis>/<final>tags - Multi-provider LLM client — Google Gemini (default), Anthropic, OpenAI, DeepSeek
- Streaming event callback for CLI or API integration (
thought,tool_start,tool_end,answer,error)
app/
agent/
session_manager.py # conversation → plan orchestration
session_context.py # per-session state
flow/
planning.py # plan → OODA loop → synthesis
tool_executor.py # tool dispatch, placeholder resolution
context_manager.py # LLM-summarized step memory
recovery.py # error-recovery step generation
helpers/workspace_sync.py
tool/
base.py # BaseTool, ToolResult
terminal_tool.py
python_runner_tool.py
sandbox/
e2b_handler.py # sandbox lifecycle
llm/ # provider clients
prompt/ # system + planner prompts
api/schema.py # AgentEvent protocol
schema.py # Plan, PlanStep, ContextObject, Message
config/settings.py
main.py # CLI entry point
server.py # API entry point (stub)
git clone <this-repo>
cd planact
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# edit .env and fill in E2B_API_KEY + your chosen LLM provider keyYou need:
- An E2B API key for the sandbox.
- At least one LLM API key (Gemini by default; set
LLM_PROVIDERto switch).
python main.pyThen type a task:
User > scrape the top 5 HN stories and save them to hn.json
You'll see color-coded events:
- Cyan
[ANALYSIS]— the agent's reasoning for the current step - Yellow
🛠️ TOOL_NAME...— tool invocation - Dim
└── Result: ...— tool output summary - Green
PlanAct > ...— final answer
Generated files land under output_workspaces/<plan_id>/.
from app.agent.session_manager import SessionManager
from app.tool.terminal_tool import TerminalTool
from app.tool.python_runner_tool import PythonRunnerTool
manager = SessionManager(
available_tools=[TerminalTool(), PythonRunnerTool()],
on_event=my_event_handler, # async callable(AgentEvent)
)
answer = await manager.process_message_from_api("write a fibonacci script and run it")- Plan. The LLM emits a JSON plan — an array of
{tool_name, tool_args, description}steps. Plans are generated once at the start of a task; each step is self-contained. - Analyze. Before executing a step, the LLM writes short
<analysis>reasoning about what the step should accomplish given the context so far. - Execute.
ToolExecutorresolves{{step_id.output}}placeholders against previous results, then calls the tool inside the E2B sandbox. - Observe. The workspace is synced to the local
output_workspaces/directory after every step, so you can inspect files as the agent works. - Update context.
ContextManageruses the LLM to keep a rolling summary of what has been learned, bounding context growth. - Recover. On tool failure,
recovery.generate_recovery_steps()asks the LLM for replacement steps.python_runneradditionally self-heals missing packages and simple runtime errors. - Synthesize. When the plan finishes, a final LLM call produces a polished answer in
<final>tags.
Subclass BaseTool in app/tool/base.py and register it with SessionManager:
from app.tool.base import BaseTool, ToolResult
class MyTool(BaseTool):
name = "my_tool"
description = "what it does"
async def execute(self, **kwargs) -> ToolResult:
...
return ToolResult(success=True, output_text="...", output_data={...})The planner prompt in app/prompt/master_planner_prompts.py needs a matching schema entry so the LLM knows how to call it.
See .env.example for all supported variables. Key ones:
| Variable | Purpose | Default |
|---|---|---|
E2B_API_KEY |
sandbox auth (required) | — |
E2B_TEMPLATE_ID |
sandbox base image | base |
LLM_PROVIDER |
google | anthropic | openai | deepseek |
google |
GOOGLE_API_KEY |
Gemini key | — |
GEMINI_MODEL_NAME |
model id | gemini-2.0-flash |
PLAN_WORKSPACES_DIR |
local mirror root | output_workspaces |
LOG_LEVEL |
logger verbosity | INFO |
This is a small, readable reference implementation — not a production framework. The pieces that are real and wired up:
- Planning loop, tool executor, context summarization, recovery
- Terminal + Python tools with sandboxed execution
- CLI front-end
The pieces that are intentionally thin:
server.pyis a placeholder; wire it to FastAPI yourself if you need HTTP/WS- The non-Gemini LLM clients are scaffolded but may need finishing for your model of choice
MIT.