Agent¶
The miminions.agent module is the core reasoning engine — a Minion is an async LLM agent built on pydantic-ai and, by default, OpenRouter. A Minion owns the full agent lifecycle: model selection, a tool registry (plain Python functions, GenericTool objects, and MCP-server tools), optional memory, optional workspace context injection, and the async run() / run_stream() loop.
Import from the subpackage
Always import from miminions.agent — the top-level import miminions does not re-export these symbols.
Features¶
- Async execution — owns its full
async run()reasoning loop, with a streamingrun_stream()variant - Resilience — per-request timeouts and automatic retries (with exponential backoff) on transient provider errors
- Observability hooks — optional
on_tool_call/on_turn_endcallbacks for tool-call and usage/latency reporting - Tool registration — register plain Python functions or
GenericToolobjects as LLM-callable tools - MCP integration — connect to Model Context Protocol servers and load their tools
- Memory — attach any memory backend and get 7 memory tools auto-registered
- Context injection — attach a workspace and the agent builds a dynamic system prompt via
ContextBuilderbefore every turn
Quick Start¶
import asyncio
from miminions.agent import create_minion
async def main():
agent = create_minion("MyAgent")
def add(a: int, b: int) -> int:
return a + b
agent.register_tool("add", "Add two numbers", add)
reply = await agent.run("What is 3 + 7?")
print(reply)
asyncio.run(main())
Set OPENROUTER_API_KEY
The default provider is OpenRouter, and the key is validated at construction time: create_minion(...) raises ValueError if OPENROUTER_API_KEY is not set. Other providers (openai, anthropic, gemini) construct fine without a key and fail at call time instead. For offline runs (tests, examples), use provider="test" (see below).
Model & Provider Selection¶
create_minion defaults to OpenRouter's free openai/gpt-oss-20b:free model. Switch providers with provider=, or pass a fully-built pydantic-ai model with model=.
from miminions.agent import create_minion
# Default: OpenRouter free model — needs OPENROUTER_API_KEY
agent = create_minion("MyAgent")
# Anthropic, OpenAI, or Gemini (each needs its own API key in the env)
agent = create_minion("MyAgent", provider="anthropic")
provider |
Default model | Required env var |
|---|---|---|
"openrouter" (default) |
openai/gpt-oss-20b:free |
OPENROUTER_API_KEY |
"openai" |
gpt-4o |
OpenAI key |
"anthropic" |
claude-3-5-sonnet-latest |
Anthropic key |
"gemini" |
gemini-1.5-flash |
Gemini key |
"test" |
TestModel (offline) |
— |
Switching models later
Call agent.set_model(model) to swap the underlying pydantic-ai model at runtime — it rebuilds the internal agent immediately.
Registering Tools¶
A Minion can run two flavours of tool. Use register_tool for plain functions, or add_tool for a GenericTool.
from miminions.agent import create_minion
from miminions.tools import tool
agent = create_minion("MyAgent")
# 1. Plain function — schema inferred from the signature
def multiply(a: int, b: int) -> int:
return a * b
agent.register_tool("multiply", "Multiply two numbers", multiply)
# 2. A GenericTool built with @tool
@tool(name="greet", description="Greet a user by name")
def greet(name: str) -> str:
return f"Hello, {name}!"
agent.add_tool(greet)
print(agent.list_tools()) # ['multiply', 'greet']
See Tools for @tool, create_tool, and the GenericTool API.
Attaching Memory¶
Pass memory= at construction, or call set_memory(...) later. The moment memory is attached, 7 tools auto-register so the LLM can read and write memory on its own:
memory_store, memory_recall, memory_update, memory_delete, memory_get, memory_list, and ingest_document (chunked PDF/text ingestion).
from miminions.agent import create_minion
from miminions.memory.sqlite import SQLiteMemory # requires the [sqlite] extra
memory = SQLiteMemory(db_path=":memory:")
agent = create_minion("MyAgent", memory=memory)
# Direct (non-LLM) helpers — both require memory to be attached
entry_id = agent.store_knowledge("MiMinions runs on pydantic-ai.")
hits = agent.recall_knowledge("what does MiMinions run on?", top_k=3)
print(hits[0]["text"]) # result dicts use the key "meta", not "metadata"
Note
store_knowledge / recall_knowledge raise ValueError("No memory attached") if no backend is set. SQLiteMemory lives in miminions.memory.sqlite (not the package top level) and needs pip install "miminions[sqlite]". See Memory for the full backend reference.
Attaching a Workspace¶
Attach a workspace with set_context(workspace, root_path). WorkspaceManager requires a config directory and you resolve a single workspace with resolve_workspace — there is no get_workspace method.
from pathlib import Path
from miminions.agent import create_minion
from miminions.core.workspace import WorkspaceManager, resolve_workspace
config_dir = Path.home() / ".miminions"
mgr = WorkspaceManager(config_dir)
# resolve by exact id, id prefix, or exact name
ws = resolve_workspace(mgr.load_workspaces(), "default")
agent = create_minion("MyAgent")
agent.set_context(ws, root_path=config_dir / "workspaces" / f"ws_{ws.id}")
Once context is set, a @agent.system_prompt callback runs ContextBuilder().build(workspace, root_path) on every run(), injecting identity, prompt files, memory, the workspace graph summary, and a skills index into the system prompt. Call set_context before the first run() for it to take effect.
Builder signature
ContextBuilder().build(workspace, root_path) takes the workspace and root path as arguments. See Context Builder for the emitted sections.
Loading MCP Tools¶
Connect to a Model Context Protocol server, then load its tools. The flow is two steps — connect, then load. (MCP needs the optional mcp package.)
from mcp import StdioServerParameters
from miminions.agent import create_minion
agent = create_minion("MyAgent")
await agent.connect_mcp_server(
"filesystem",
StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
),
)
# Register every tool the server exposes
await agent.load_tools_from_mcp_server("filesystem")
print(agent.list_tools())
# Close MCP connections when done
await agent.cleanup()
Tip
await agent.load_tools_from_all_servers() loads tools from every connected server in one call.
Executing Tools Directly vs. run()¶
There are three ways to invoke a tool. Pick by how you want errors and results surfaced.
| Method | Returns | On error |
|---|---|---|
await run(prompt) |
the LLM's reply (the model decides which tools to call) | retries transient errors, then re-raises |
execute(name, arguments=None, **kwargs) |
a ToolExecutionResult |
never raises — failure is captured on the result |
execute_tool(name, **kwargs) |
the tool's raw return value | raises ValueError / RuntimeError |
# Let the LLM reason and call tools as needed
reply = await agent.run("Multiply 6 by 7")
# Structured result — inspect .status / .error / .result
from miminions.tools.schemas import ExecutionStatus
res = agent.execute("multiply", {"a": 6, "b": 7})
print(res.result if res.status == ExecutionStatus.SUCCESS else res.error)
# Raw result — raises on missing tool or execution error
value = agent.execute_tool("multiply", a=6, b=7) # -> 42
Async tools
execute / execute_tool are synchronous and reject coroutine tools. For async tools use await execute_async(...) (structured) or await execute_tool_async(...) (raw).
Multi-turn Conversations¶
run() stores the full message list on agent._last_messages after each call. Pass it back as message_history to continue a conversation within a session.
reply1 = await agent.run("My name is Asher.")
reply2 = await agent.run("What is my name?", message_history=agent._last_messages)
Bounding long histories
For long sessions, miminions.session.store.trim_message_history(messages, max_messages=40) caps the history without breaking request/response pairing — it only cuts at a user-prompt turn boundary, so a trimmed history never starts mid-tool-exchange. The CLI chat loop applies this automatically.
Streaming Replies¶
run_stream() mirrors run() but yields the reply incrementally as text deltas. Tool calls still execute (and fire on_tool_call); on_turn_end fires when the stream completes, and _last_messages is updated only once the stream is fully consumed.
Consume the stream to completion
Breaking out of the loop early abandons the underlying HTTP stream and its deferred cleanup can raise RuntimeError at generator close. Also note run_stream() does not retry transient errors — deltas already yielded cannot be withdrawn, so errors propagate immediately.
Timeouts, Retries & Hooks¶
create_minion (and Minion) accept resilience and observability options:
| Parameter | Default | Meaning |
|---|---|---|
request_timeout |
60.0 |
Per-HTTP-request timeout in seconds for model calls (None disables it). Bounds each request, not the whole turn — a multi-tool turn may take several requests. |
max_retries |
2 |
How many times run() retries after a transient model error before re-raising. |
retry_base_delay |
1.0 |
First retry delay in seconds; doubles per attempt (exponential backoff). |
on_tool_call |
None |
Callback fired as (tool_name, args) whenever the model invokes a tool. |
on_turn_end |
None |
Callback fired as (usage, latency_seconds) after each successful turn — usage is a pydantic-ai RunUsage. |
Transient means HTTP 429/5xx, connection errors, and timeouts (ModelHTTPError / ModelAPIError); other errors re-raise immediately. Exceptions raised inside either callback are logged and swallowed, so a broken hook never breaks the turn.
def show_tool(name, args):
print(f"[tool] {name} {args}")
def show_usage(usage, latency):
print(f"[turn] {usage.input_tokens} in / {usage.output_tokens} out, {latency:.1f}s")
agent = create_minion(
"MyAgent",
request_timeout=30.0,
max_retries=3,
on_tool_call=show_tool,
on_turn_end=show_usage,
)
API Reference¶
create_minion¶
create_minion(
name: str,
description: str = "",
memory: BaseMemory | None = None,
model: Any | None = None,
provider: str = "openrouter",
request_timeout: float | None = 60.0,
max_retries: int = 2,
retry_base_delay: float = 1.0,
on_tool_call: Callable[[str, dict], None] | None = None,
on_turn_end: Callable[[RunUsage, float], None] | None = None,
) -> Minion
Factory that builds a configured Minion. When model is None, provider drives model selection (default OpenRouter free model). See Timeouts, Retries & Hooks for the resilience and callback parameters. This is the primary entry point.
Minion methods¶
| Method | Description |
|---|---|
async run(prompt, message_history=None) -> str |
Send a prompt; the model reasons and calls tools. Retries transient provider errors with backoff. Returns the reply. |
run_stream(prompt, message_history=None) -> AsyncIterator[str] |
Streaming variant of run(); yields text deltas. No automatic retry. |
register_tool(name, description, func, schema=None) -> ToolDefinition |
Register a Python function as a tool (schema inferred if omitted). |
add_tool(tool: GenericTool) -> ToolDefinition |
Register a GenericTool. |
unregister_tool(name) -> bool |
Remove a tool by name. |
list_tools() -> list[str] |
List registered tool names. |
search_tools(query) -> list[str] |
Case-insensitive search over tool names/descriptions. |
execute(name, arguments=None, **kwargs) -> ToolExecutionResult |
Run a tool synchronously; never raises. |
async execute_async(name, arguments=None, **kwargs) -> ToolExecutionResult |
Async variant of execute; awaits coroutine tools. |
async execute_many_async(requests, max_concurrency=16) -> list[ToolExecutionResult] |
Run structured tool requests with bounded concurrency; results preserve request order and isolate failures. |
execute_tool(name, **kwargs) -> Any |
Run a tool and return its raw result; raises on error. |
async execute_tool_async(name, **kwargs) -> Any |
Async raw-result variant; raises on error. |
set_memory(memory) -> None |
Attach a memory backend and auto-register the 7 memory tools. |
store_knowledge(text, metadata=None) -> str |
Store text in memory; raises ValueError if no memory attached. |
recall_knowledge(query, top_k=5) -> list[dict] |
Recall from memory; raises ValueError if no memory attached. |
set_context(workspace, root_path) -> None |
Attach workspace context for ContextBuilder injection on every run. |
set_model(model) -> None |
Swap the pydantic-ai model and rebuild the agent. |
async connect_mcp_server(name, server_params) -> None |
Connect to an MCP server (StdioServerParameters). |
async load_tools_from_mcp_server(name) -> list[ToolDefinition] |
Register all tools from a connected server. |
async load_tools_from_all_servers() -> list[ToolDefinition] |
Load tools from every connected server. |
async cleanup(rebuild=True) -> None |
Close MCP connections (and rebuild the internal agent). |
get_state() -> AgentState |
Snapshot: config, tool count, memory flag, connected servers. |
Properties¶
| Property | Returns |
|---|---|
name |
The agent name (str). |
description |
The agent description (str). |
memory |
The attached BaseMemory or None. |
See Also¶
- Memory — vector and markdown memory backends
- Context Builder — what gets injected into the system prompt
- Tools —
@tool,create_tool, andGenericTool - Workspaces — nodes, rules, and on-disk layout