AbstractAgent
AbstractAgent provides agent patterns (ReAct, CodeAct and experimental MemAct) built on AbstractRuntime for durable execution and AbstractCore for models and tools. Every run can pause, resume across restarts and be read back from its ledger, the record of every step it took.
pip install abstractagent
# depends on AbstractCore and AbstractRuntime 0.7+
Three loops, one contract
Each pattern is runtime-agnostic logic, a runtime workflow adapter and a thin agent class on BaseAgent. On completion, state.output holds at least answer, iterations and messages.
ReAct
A tool-first Reason, Act, Observe loop that runs until the model returns no tool calls. review_mode is on by default: a verifier checks each candidate final answer, and an incomplete one goes back into the tool loop.
CodeAct
Executes Python through the execute_python tool (and fenced code blocks on prompted-tools models). The code runs in a local subprocess, with an optional timeout. It is not a hardened sandbox.
MemAct experimental
A memory-enhanced agent that keeps Active Memory: blocks of working memory owned by the runtime, injected into the prompt and updated through structured steps. Import it from abstractagent.agents.memact; it is deliberately not exported at the top level.
Start, then run to completion
start(task) creates a run and returns its id. run_to_completion() steps until the run completes, fails or waits for you. Provider and model come from your AbstractCore defaults (abstractcore --config) when you leave them out.
- ✓ Default budget:
max_iterations=20; when it runs out, the agent concludes from what it has - ✓ Restrict tools per run:
agent.start(task, allowed_tools=[...]) - ✓ Other controls:
step(),resume(response),cancel(),get_ledger(),save_state()/load_state()
from abstractagent import create_react_agent
agent = create_react_agent(provider="mlx", model="mlx-community/Qwen3.5-4B-4bit")
agent.start("List the files in the current directory")
state = agent.run_to_completion()
print(state.output["answer"])react-agent command only points there).Durable runs you can steer
Pause and resume
Every iteration is a durable runtime step. With a persistent RunStore and LedgerStore, a run resumes by run_id after a restart. A question to the user (ask_user) is a durable wait.
Loop hooks
LoopHooks let you listen, steer and capture on all three loops. A handler that returns guidance adds it to the durable guidance inbox, and the model sees it at the next reasoning step.
Tool control
Explicit tool sets and per-run allowlists; a grant name that cannot be resolved is recorded, never dropped silently. delegate_agent starts a child agent that cannot widen its parent's tool grant.
The history window
When a host sets _runtime.history_window_tokens (the entity visit sets 50,000), ReAct sends the runtime's window of the newest whole turns on every call. The stored transcript stays whole.
Generation controls
Temperature, seed, media policy, prompt-cache identity, streaming, Core thinking and speculation (MTP) are normalized before each call and inherited by delegated children.
Plans and skills
The update_plan builtin keeps a checklist in the scratchpad, and read_skill opens attached skills on demand. Media that a tool returns (a camera capture, for example) is attached to the next model call.
Loop safety
Repeated identical tool batches end the loop instead of spinning, a context_warning event fires when token use nears the model window, and the terminal outcome is machine-readable.
Loops as gateway workflows
A native-loop registry builds and audits the manifests that let AbstractGateway serve these loops as workflow bundles (react-agent, codeact-agent, memact-agent).
Ledger
LLM calls, tool calls and waits go to the durable ledger; agent.get_ledger() reads them back.
from abstractagent import create_react_agent
from abstractruntime.storage.json_files import JsonFileRunStore, JsonlLedgerStore
run_store = JsonFileRunStore(".runs") # run state as JSON files
ledger_store = JsonlLedgerStore(".runs") # append-only ledger, one JSONL file per run
agent = create_react_agent(provider="mlx", model="mlx-community/Qwen3.5-4B-4bit",
run_store=run_store, ledger_store=ledger_store)
agent.start("List the files in the current directory, then say which one is a CSV file")
agent.save_state("agent_state.json") # the run id; the state itself is in .runs/
# ... the process stops here (crash, restart, deploy): nothing in memory survivesAfter a restart, in a new process:
from abstractagent import create_react_agent
from abstractruntime.storage.json_files import JsonFileRunStore, JsonlLedgerStore
run_store = JsonFileRunStore(".runs")
ledger_store = JsonlLedgerStore(".runs")
agent = create_react_agent(provider="mlx", model="mlx-community/Qwen3.5-4B-4bit",
run_store=run_store, ledger_store=ledger_store)
agent.load_state("agent_state.json") # re-attach to the same run
state = agent.run_to_completion()
print(state.output["answer"])from abstractagent import create_react_agent
agent = create_react_agent()
agent.start("Plan a team offsite; ask me anything you need to know")
state = agent.run_to_completion()
while agent.is_waiting(): # the agent called ask_user
question = agent.get_pending_question()
agent.resume(input(question["prompt"] + " "))
state = agent.run_to_completion()
print(state.output["answer"])Checked against AbstractAgent 0.3.17 (agents/base.py, lines 254-299); a run on a 4B local model asked and resumed 11 times without finishing in 300 s, so use a stronger model; not executed to the end for this page.
from abstractagent import create_react_agent
from abstractcore.tools import tool
@tool(name="my_tool", description="Example tool")
def my_tool(query: str) -> str:
return f"Echo: {query}"
agent = create_react_agent(provider="mlx", model="mlx-community/Qwen3.5-4B-4bit", tools=[my_tool])
agent.start("Call my_tool with 'hello' and report the result", allowed_tools=["my_tool"])
state = agent.run_to_completion()
print(state.output["answer"])from abstractagent import LoopHooks, HookEvent, ReactAgent
from abstractruntime.integrations.abstractcore import create_local_runtime
def my_handler(event: HookEvent):
# LISTEN / CAPTURE: a copied payload, safe to inspect
print(event.run_id, event.name, event.step, event.data)
# STEER: the injection reaches the model at the next reason boundary
if event.name == "tool_executed" and event.data.get("tool") == "web_search":
return {"inject": "Prefer primary sources over blogs."}
runtime = create_local_runtime(provider="mlx", model="mlx-community/Qwen3.5-4B-4bit")
agent = ReactAgent(runtime=runtime, hooks=LoopHooks().add(my_handler))
agent.start("In one sentence, what is a durable run?") # W2: run it so the hook prints events (this ReactAgent has no tools)
state = agent.run_to_completion()
print(state.output["answer"])On Ollama, raise the context window to the model's maximum (for example llm_kwargs={"num_ctx": ...}): the server default is small and silently drops the oldest content. These loops send the transcript and about 19 tool schemas on every cycle, so prefer a tool-capable model.