Composable Python runtime for building production AI agents
Project description
ModuAgent
ModuAgent is a composable Python runtime for building AI agents around your own model endpoints and Python functions.
Start with a normal model or Tool-calling loop. Add bounded conversation memory, validated Pydantic output, strict Plan-and-Execute, checkpoint recovery, Skills, and observability only when your application needs them.
Current version: 0.5.1a1 (Alpha) · Python 3.10+ · MIT License
New to ModuAgent? Follow the five short steps below. They use the 0.5 Quick API; the explicit component API remains available for advanced composition. For runnable files that add only one concept at a time, start with the beginner examples. When you are ready for multi-Tool workflows, continue with the intermediate examples.
What ModuAgent provides
An Agent is a composition of small, explicit parts:
User input
│
▼
Agent ──► Execution profile ──► Model
│ │ │
│ └──────────────► Tools
│
├── Conversation store + memory policy
├── Output codec
├── Checkpoint store
├── Skills and authorization
└── Events, diagnostics, and metrics
Agent.create()resolves the common configuration for you.AgentConfigexposes instructions, retry behavior, and run limits when you need explicit composition.- A model client connects to vLLM, Ollama, or another supported endpoint.
- Tools are typed Python functions the model may call.
- An execution profile controls how work proceeds.
- An output codec returns text or a validated Pydantic object.
- A conversation store saves history; a memory policy selects the model view.
- A checkpoint store saves interrupted runs for safe recovery.
Most applications should start with a model and a small set of Tools. Add the other components as requirements appear.
Choose an execution mode
| Standard execution | Strict Plan-and-Execute | |
|---|---|---|
| Selection | Default | Explicit opt-in |
| Best for | Chat, direct Tool use, short loops | Dependent, auditable multi-step work |
| Flow | Model → optional Tools → answer | Plan → act → validate/commit → answer |
| Cost | Lower latency and fewer calls | More calls for stronger control |
| Intermediate state | Lightweight | Versioned, validated step state |
Use Standard execution unless intermediate steps must be independently validated or safely resumed.
Installation
ModuAgent requires Python 3.10 or later. You also need a reachable model server; ModuAgent does not host a model itself.
Install the package:
python -m pip install "moduagent==0.5.1a1"
If your package index does not contain 0.5.1a1 yet and you already have a 0.5
source checkout, install it from the repository root:
cd /path/to/moduagent
python -m pip install -e .
Contributors can include the development tools:
python -m pip install -e '.[dev]'
Optional integrations are installed separately:
python -m pip install redis # Redis conversation/checkpoint stores
python -m pip install matplotlib # report automation example
python -m pip install "psycopg[binary]>=3.2,<4" # PostgreSQL report example
Step 1: run your first Agent
The example below connects to a vLLM OpenAI-compatible endpoint and runs a model-only Agent.
import asyncio
from moduagent import Agent, VLLMClient
async def main() -> None:
async with VLLMClient.from_env(
default_options={"temperature": 0, "max_tokens": 256},
) as model:
agent = Agent.create(
model=model,
instructions="Answer accurately and concisely.",
)
answer = await agent.ask(
"Explain what an AI agent is in one paragraph.",
session_id="getting-started",
)
print(answer)
if __name__ == "__main__":
asyncio.run(main())
Set the endpoint before running the file:
export VLLM_BASE_URL="http://localhost:8000/v1"
export VLLM_MODEL="your-model-name"
# export VLLM_API_KEY="your-token-if-required"
export VLLM_TIMEOUT="60"
python getting_started.py
VLLMClient.from_env() reads only these documented variables:
| Variable | Required | Meaning |
|---|---|---|
VLLM_MODEL |
Yes | Model name served by vLLM |
VLLM_BASE_URL |
No | OpenAI-compatible base URL; defaults to http://localhost:8000/v1 |
VLLM_API_KEY |
No | Bearer token |
VLLM_TIMEOUT |
No | Positive request timeout in seconds; defaults to 60 |
Use the regular VLLMClient(...) constructor when configuration comes from
another source. An explicit timeout= passed to from_env() takes precedence
over VLLM_TIMEOUT.
The endpoint and selected model must support the capabilities your Agent uses, such as Tool Calling or JSON Schema output. For Tool examples, configure vLLM's chat template and Tool parser for the selected model.
ask() is the shortest path: it returns the decoded output and raises the
secret-safe AgentRunError when the run does not complete. Use run() in
operational code that needs the full result; Step 5 shows both forms.
Ollama uses the same Agent API:
from moduagent import OllamaClient
model = OllamaClient(
base_url="http://localhost:11434",
model="qwen3:14b",
)
Step 2: add a Tool
Use @tool to expose a typed Python function.
import asyncio
from moduagent import Agent, VLLMClient, tool
@tool(timeout_seconds=5, max_result_bytes=4096)
def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
async def main() -> None:
async with VLLMClient.from_env(
default_options={"temperature": 0, "max_tokens": 256},
) as model:
calculator = Agent.create(
model=model,
instructions=(
"Use the add Tool whenever addition is required. "
"Do not invent a calculated result."
),
tools=[add],
)
answer = await calculator.ask(
"What is 12 plus 30?",
session_id="calculator-demo",
)
print(answer)
asyncio.run(main())
The function's type hints become its input schema, and its docstring becomes
the description shown to the model. Only Tools passed through tools=[...]
can be called.
@tool is the short name for the existing @function_tool adapter. Neither
form guesses whether a Tool is safe to retry or repair. For example,
idempotent=True declares that repeating the same validated call is safe; it
does not create a transaction or exactly-once guarantee. Write Tools still
need an application idempotency key and duplicate protection.
Blocking functions such as pandas.read_sql() run outside the event loop.
For production, share a bounded scheduler across synchronous Tools so timed-out
calls cannot create an unlimited number of background threads:
from moduagent import SyncToolScheduler, tool
blocking_tools = SyncToolScheduler(max_workers=8, max_queue=32)
@tool(sync_scheduler=blocking_tools, timeout_seconds=10)
def query_db(sql: str) -> list[dict]:
return run_read_only_query(sql)
Raw assistant Tool calls and raw Tool results are internal protocol messages.
They are available to the model during the run but are not added to
ConversationStore or AgentResult.messages. The default public Tool trace is
a bounded, secret-safe summary.
Step 3: return validated structured output
Pass a Pydantic model class through output=. The Quick API creates the
existing PydanticOutputCodec internally, and ask() returns the validated
model object.
import asyncio
from pydantic import BaseModel, Field
from moduagent import Agent, VLLMClient, tool
@tool
def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
class Answer(BaseModel):
answer: str
confidence: float = Field(ge=0, le=1)
async def main() -> None:
async with VLLMClient.from_env(
default_options={"temperature": 0, "max_tokens": 256},
) as model:
structured_agent = Agent.create(
model=model,
instructions=(
"Use the add Tool for every arithmetic operation, then return "
"the answer in the requested format."
),
tools=[add],
output=Answer,
)
answer: Answer = await structured_agent.ask(
"What is 20 plus 22?",
session_id="structured-demo",
)
print(answer.answer, answer.confidence)
if __name__ == "__main__":
asyncio.run(main())
Tools and structured output can be used together. ModuAgent separates the requests:
ACT: model receives Tool schemas, without the final output schema
FINALIZE: model receives the Pydantic schema, without Tools
This avoids the common vLLM conflict caused by putting Tool Calling and
structured output in the same request. VLLMClient declares that combination
unsupported by default, so the runtime uses this separated mode.
Step 4: use strict Plan-and-Execute
Use Plan-and-Execute when a task has dependent steps and each intermediate result must be validated before it can affect the final answer.
import asyncio
from pydantic import BaseModel, Field
from moduagent import Agent, RunLimits, VLLMClient, tool
@tool
def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
class Answer(BaseModel):
answer: str
confidence: float = Field(ge=0, le=1)
async def main() -> None:
async with VLLMClient.from_env(
default_options={"temperature": 0, "max_tokens": 512},
) as model:
planning_agent = Agent.create(
model=model,
instructions=(
"Use the add Tool for every arithmetic operation. "
"Complete multi-step requests using only validated and committed "
"step results."
),
tools=[add],
output=Answer,
execution="plan",
limits=RunLimits(
max_steps=4,
max_step_attempts=2,
max_replans=1,
max_tool_calls=8,
timeout_seconds=120,
),
)
answer: Answer = await planning_agent.ask(
"Calculate 10 + 20, then add 5 to that verified result.",
session_id="plan-demo",
)
print(answer)
if __name__ == "__main__":
asyncio.run(main())
The strict flow is:
PLAN → ACT_TOOL → STEP_RESULT → VALIDATE/COMMIT → VERIFY → FINALIZE
max_stepslimits generated Plan steps, not model calls.max_step_attemptslimits validation retries for one step.max_replanslimits revisions of unfinished work.max_tool_callslimits business Tool calls for the whole run.timeout_secondsis one deadline shared by planning, model calls, Tools, finalization, and persistence.
For execution="plan", the Quick API creates an LLMPlanGenerator with the
same model and synchronizes its max_steps with limits.max_steps. Pass an
explicit PlanExecutionProfile to execution= when you need a custom planner,
validator, or recovery policy.
Standard execution remains the better default for chat, direct Tool use, and short workflows.
Step 5: inspect results and bound model calls
ask() is equivalent to run() followed by unwrap(). Use it when the only
successful value you need is the decoded output. The snippets in this step
assume an already configured planning_agent; keep its model client's async
context open for the lifetime of these calls:
import asyncio
async def main() -> None:
answer = await planning_agent.ask("Complete the task.")
print(answer)
asyncio.run(main())
Use run() when you also need usage, traces, finish reasons, or recovery
metadata:
import asyncio
async def main() -> None:
result = await planning_agent.run(
"Complete the task.",
session_id="operations-demo",
)
print(result.explain()) # concise, sanitized terminal summary
answer = result.unwrap() # raises AgentRunError unless completed
print(answer)
asyncio.run(main())
Alternatively, replace the unwrap() line with the equivalent explicit form:
result.raise_for_error()
answer = result.output
The main AgentResult fields are:
| Field | Meaning |
|---|---|
output |
Final text or validated object |
error |
Safe public error message, or None |
finish_reason |
Stable terminal reason |
usage |
Accumulated model token usage |
run_id |
Failure-correlation and checkpoint identifier |
messages |
Public conversation messages |
metadata |
Bounded Tool trace, Plan summary, and safe error category |
run_usage |
Immutable model-turn, Tool-call, and elapsed-time summary |
tool_trace |
Immutable bounded projection of executed Tools |
error_summary |
Immutable safe terminal failure classification |
Terminal finish reasons are:
| Finish reason | Meaning |
|---|---|
completed |
Output completed successfully |
max_steps |
Plan or execution step budget was exhausted |
max_tool_calls |
Tool-call budget was exhausted |
max_model_turns |
Whole-run model-attempt budget was exhausted |
no_progress |
Repeated semantic state and response tripped the circuit breaker |
timeout |
The overall run deadline expired |
cancelled |
The caller cancelled the run |
error |
Another terminal failure occurred |
ask() and unwrap() raise a secret-safe AgentRunError for every reason
except completed:
import asyncio
from moduagent import AgentRunError
async def main() -> None:
try:
answer = await planning_agent.ask("Complete the task.")
print(answer)
except AgentRunError as exc:
print(exc.run_id, exc.finish_reason, exc.code)
print(exc.retryable, exc.resumable, exc.failure_id)
asyncio.run(main())
The exception does not retain prompts, output, Tool arguments, raw provider bodies, or arbitrary result metadata.
Strict model retry contract
RetryConfig.max_attempts includes the first call and defaults to 1, so
retries are opt-in:
from moduagent import RetryConfig
retrying_agent = Agent.create(
model=model,
instructions="Answer accurately.",
retry=RetryConfig(max_attempts=2),
)
Model calls are retried only for this allowlist:
- timeout failures;
- connection or network failures;
- HTTP
408; - HTTP
5xx.
They are not retried for:
- HTTP
429or any other HTTP4xx; - malformed JSON, invalid Tool arguments, or any provider protocol/parsing failure;
- provider output ending with
timeout,length, ormax_tokens; - structured-output validation failures;
- invalid requests, capability mismatches,
TypeError, or programming errors.
A streaming model call is not retried after a public delta has been emitted. Tool retry and repair are separate contracts: they additionally require the Tool's declared safety profile and never become safe merely because model retry is enabled.
An incomplete provider response fails with code model_output_incomplete.
Inspect result.error_summary["provider_finish_reason"] or
AgentRunError.provider_finish_reason to distinguish timeout, length, and
max_tokens; partial output and provider metadata are not retained.
Whole-run model guards
Every run has two independent model guards:
limits = RunLimits(
max_model_turns=32,
no_progress_model_turn_threshold=3,
)
max_model_turns=32bounds framework-managed model attempts across planning, acting, memory summarization, Skill selection, repairing, and finalization. Transport retries consume this budget too.no_progress_model_turn_threshold=3stops on the third consecutive identical semantic-state/normalized-response observation. A successful Tool outcome resets the streak only when its run-salted fingerprint is new; repeating the same successful Tool outcome does not bypass the guard. Each successfully consumed memory-summary batch and each committed Plan step also counts as progress. None of these resets the total turn count.
The resulting finish reasons are max_model_turns and no_progress; neither
is automatically retried or safely resumable. error_summary and
AgentRunError therefore report retryable=False and resumable=False with
only bounded counters and safe classification fields.
Built-in components route auxiliary calls through the run's ModelGateway.
Custom memory policies, selectors, planners, and model clients must preserve
that boundary: a custom component that calls a provider directly, or a client
that performs hidden internal retries, cannot be counted separately by the
framework.
With a checkpoint store, every framework-managed model attempt, including a provider retry, is durably reserved immediately before provider I/O. A hard crash cannot make that consumed turn available again on resume. The model guard checkpoint stores only numeric counters, a per-run random salt, and an HMAC-SHA-256 observation digest. Successful Tool progress is likewise represented by a run-salted fingerprint. Raw prompts, model output, Tool arguments, Tool results, provider metadata, and provider-generated call IDs are not stored by the guard.
Framework boundary
ModuAgent 0.5 does not include domain Recipes, a Workflow DSL, database abstractions, SQL generation, or report-specific behavior. The framework composes and runs the components; the application remains responsible for:
- instructions and business rules;
- Tool implementations and input/result schemas;
- authoritative database or service schemas;
- Tool idempotency, repair, and timeout-safety declarations;
- database roles, transactions, query limits, and other real security boundaries.
The Quick API only removes repetitive framework wiring. It does not infer domain semantics or Tool safety.
Advanced composition: conversation memory
Use the same session_id to continue a conversation. When you need to select
stores, sinks, authorization, checkpoints, Skills, custom engines, or other
advanced components, use the explicit Agent(...) constructor.
from moduagent import (
Agent,
AgentConfig,
InMemoryConversationStore,
RecentTurnsConversationMemoryPolicy,
)
conversations = InMemoryConversationStore(ttl_seconds=3600)
memory_agent = Agent(
config=AgentConfig(
name="memory-assistant",
instructions="Use relevant conversation context when answering.",
),
model=model,
conversation_store=conversations,
conversation_memory_policy=RecentTurnsConversationMemoryPolicy(max_turns=6),
)
async def demonstrate_memory() -> None:
first = await memory_agent.run(
"Remember that my deployment region is Seoul.",
session_id="user-42",
)
first.raise_for_error()
result = await memory_agent.run(
"Which deployment region did I choose?",
session_id="user-42",
)
print(result.unwrap())
The store and the memory policy have different jobs:
| Component | Responsibility |
|---|---|
ConversationStore |
Saves the complete public conversation |
ConversationMemoryPolicy |
Selects the view sent to the model |
RecentTurnsConversationMemoryPolicy does not delete stored messages. It sends
only the latest complete turns to the model. In-memory stores are intended for
single-process development and tests.
For strict token limits and automatic summarization, see the
Conversation Memory guide.
If exact vLLM tokenization is used repeatedly, wrap VLLMTokenCounter with
CachingTokenCounter; it stores only a bounded keyed digest and successful
token count.
Application example: report automation
The repository includes a complete Plan-and-Execute Agent with only two Tools:
query_db: runs a bounded, read-only query against SQLite (default) or PostgreSQL.plot_graph: reads the run-scoped query artifact and creates a PNG chart.
See examples/report_automation_agent.py.
python -m pip install matplotlib
export VLLM_BASE_URL="http://localhost:8000/v1"
export VLLM_MODEL="your-tool-capable-model"
python examples/report_automation_agent.py
To run the same example against PostgreSQL, keep the model variables above and add:
python -m pip install "psycopg[binary]>=3.2,<4"
export REPORT_DB_BACKEND="postgresql"
export REPORT_DATABASE_URL="postgresql://report_reader@localhost:5432/reporting"
python examples/report_automation_agent.py
Use a dedicated database role with only CONNECT, schema USAGE, and SELECT
privileges. The example also starts a read-only transaction and applies
statement and lock timeouts.
This is application code demonstrating how a user can define prompts, schemas, Tools, and safety controls. It is not a built-in Recipe or database abstraction.
After the Quick API
At this point you can build most Agents. The following features are optional; add them when you need streaming, recovery, reusable domain procedures, or deployment controls.
Streaming results
Use stream() for user-facing output:
from moduagent import EventType
async def stream_result() -> None:
result = None
async for event in planning_agent.stream(
"Run the task and stream the final answer.",
session_id="stream-demo",
):
if event.type in (EventType.MODEL_DELTA, EventType.FINAL_DELTA):
print(event.data["delta"], end="", flush=True)
elif event.type in (EventType.RUN_COMPLETED, EventType.RUN_FAILED):
result = event.data["result"]
if result is not None:
result.raise_for_error()
Handle both public delta types: direct Standard responses use MODEL_DELTA;
staged finalization, including Plan-and-Execute, uses FINAL_DELTA.
stream_all() also exposes diagnostic internal events and intermediate model
deltas. Use it only in an access-controlled diagnostic path, not as a direct
user-facing stream.
Inspecting steps and failures
Use an EventSink or stream_all() for the execution timeline. Add a
DiagnosticSink when developers also need the sanitized cause of an exception:
import asyncio
import logging
from moduagent import Agent, InMemoryDiagnosticSink, LoggingEventSink
logging.basicConfig(level=logging.INFO)
diagnostics = InMemoryDiagnosticSink(max_records=1_000)
observable_agent = Agent.create(
name="observable-agent",
instructions="Complete the request using the available Tools.",
model=model,
tools=[add],
event_sink=LoggingEventSink(),
diagnostic_sink=diagnostics,
)
async def main() -> None:
result = await observable_agent.run("Use add for 20 + 22.")
print(dict(result.run_usage))
for trace in result.tool_trace:
print(dict(trace))
if result.error_summary:
print(dict(result.error_summary))
if result.failure_id is not None:
failure = diagnostics.get(result.failure_id)
if failure is not None:
print(failure.to_dict())
for failure in diagnostics.for_run(result.run_id):
print(failure.failure_id, failure.component, failure.operation)
asyncio.run(main())
result.tool_trace shows executed Tools and their correlation IDs.
result.failure_id identifies the root failure of a terminal run. Its Tool
record can have terminal=False because that flag means “recoverable when
captured”; the Plan policy may decide to stop afterward. Recovered Tool
failures may appear only in the Tool trace and diagnostics.for_run().
Diagnostics are off by default; omitting diagnostic_sink or using
NoopDiagnosticSink preserves the default behavior. Delivery is best effort and
bounded by diagnostic_timeout_seconds and
diagnostic_max_pending_deliveries. Standard-library logging uses a bounded
daemon worker pool; a synchronous handler already in flight cannot be
force-cancelled. Custom async sinks must honor cancellation.
Diagnostic fields are bounded and omit raw exception messages, SQL, prompts,
Tool arguments or results, provider bodies, source lines, and local variables.
Real OSError.errno and eager allowlisted attributes can be retained;
Pydantic dynamic keys are hidden and truncated tracebacks keep their innermost
frames. Built-in event logs omit payloads and free-form reasons and hash
step/Tool correlation IDs.
Strict Plan validation exposes framework-owned validation_code,
validation_location, and optional validation_cause_code; inspect them
instead of parsing a reason string. Custom Engine authors must treat
EngineOutcome.error as public, trusted text.
AgentResult.metadata["error_summary"] is runtime-owned and cannot be
overridden through Engine metadata. See the
diagnostics guide
for logging, custom durable sinks, and security guidance.
Performance metrics
MetricsEventSink records model.calls and phase-aware model duration, plus
memory preparation, Tool, checkpoint, run, and same-session queue timings.
Noop observability skips its queue and copy path completely. Event handoff
queues are bounded, so a slow sink applies backpressure instead of retaining
unlimited payloads.
Run the source-tree microbenchmark after changing execution or persistence code:
python benchmarks/performance_v042.py --pretty
Checkpoints and safe resume
Add a checkpoint store when interrupted work must continue:
from moduagent import InMemoryCheckpointStore
checkpoints = InMemoryCheckpointStore()
resumable_agent = Agent(
config=AgentConfig(
name="resumable-agent",
instructions="Complete the request safely.",
),
model=model,
tools=[add],
conversation_store=InMemoryConversationStore(),
checkpoint_store=checkpoints,
)
async def resume_if_safe() -> None:
failed = await resumable_agent.run(
"Run the task.",
session_id="resume-demo",
)
error_summary = failed.metadata.get("error_summary", {})
if failed.error and error_summary.get("resumable") is True:
resumed = await resumable_agent.resume(
failed.run_id,
session_id="resume-demo",
)
print(resumed.output)
Resume with the original run_id, the same session_id, and a compatible
Agent configuration. Resume only when error_summary["resumable"] is true.
InMemoryCheckpointStore demonstrates same-process recovery only; it loses
all checkpoints when the process exits.
retryablemeans a new run may be attempted.resumablemeans the saved run can continue without replaying an unsafe side effect.
A checkpoint can exist while resumable is false. In particular, a Tool may
have started without a durably committed outcome. ModuAgent fails closed and
requires manual review instead of automatically replaying that Tool.
max_model_turns and no_progress are terminal guard decisions and are also
always resumable=false; resume cannot enlarge a consumed turn budget or
reopen a tripped circuit.
Checkpointed Agents require a ConversationStore with atomic
append_once(). Built-in in-memory and supported Redis stores implement this
contract. Use Redis or a custom durable adapter in production.
Add domain knowledge with Skills
Skills provide reusable instructions and bounded text resources. They do not grant Tool permission.
skills/
└── invoice-review/
├── SKILL.md
├── references/
│ └── policy.md
└── assets/
└── report-template.md
from moduagent import SkillRegistry, tool
@tool(idempotent=True)
def lookup_invoice(invoice_id: str) -> dict[str, object]:
"""Look up an invoice by ID."""
return {
"invoice_id": invoice_id,
"amount": 125_000,
"evidence_attached": True,
"approved": False,
}
skills = SkillRegistry.from_paths("./examples/skills")
agent = Agent(
config=AgentConfig(
name="invoice-agent",
instructions="Use verified evidence only.",
),
model=model,
tools=[lookup_invoice],
skill_registry=skills,
)
async def review_invoice() -> None:
result = await agent.run(
"Review invoice INV-100.",
session_id="invoice-42",
skills=["invoice-review"],
)
print(result.unwrap())
The sample Tool returns fixed data for demonstration. Replace its body with your own authorized data-access code.
The effective Tool scope is the intersection of registered Tools, the Skill's
allowed-tools, and the configured ToolAuthorizer. Skill scripts/ are
never executed automatically.
See the Agent Skills guide for authoring, lockfiles, resource limits, and automatic selection.
Inspect the resolved Agent
Agent.inspect() returns an immutable, credential-redacted AgentSpec without
making an external request:
spec = planning_agent.inspect()
print(spec.execution_profile.kind) # plan
print(spec.agent_fingerprint)
print(spec.to_dict(include_instructions=False))
The specification includes resolved model capabilities, Tool schema
fingerprints and safety profiles, output behavior, persistence policy, and
compatibility metadata. API keys and tokens are redacted. The original
instructions remain available on the object; do not put secrets in
instructions, and use include_instructions=False before logging or exporting
the specification.
Before production
- Replace in-memory conversation, checkpoint, and summary stores with durable stores.
- Set model, Tool, database, and overall run timeouts independently.
- A timeout around a synchronous Python Tool cannot forcibly stop the underlying thread. Configure a driver or server-side statement timeout too.
- Limit database rows, Tool result bytes, context tokens, output tokens, Plan steps, and Tool calls.
- Declare retry or changed-argument repair safety only after reviewing Tool side effects.
- Give write Tools application-level idempotency keys and duplicate handling.
- Use
ToolAuthorizeror RBAC; Skillallowed-toolsonly narrows scope. - Keep the default summary Tool trace unless argument logging has a clear, reviewed purpose.
- Never place raw exceptions, SQL, credentials, customer data, or internal paths in model-visible error messages.
- Configure encryption, tenant isolation, access control, retention, and TTLs for conversations, checkpoints, events, and generated artifacts.
- Record
agent.inspect()with the deployment and test resume behavior before upgrading a live Agent. - Send public streams to users and internal events to protected
EventSinkimplementations. Store failure diagnostics in a separately access-controlledDiagnosticSink.
ModuAgent does not provide a distributed lock, worker queue, scheduler, durable outbox, or end-to-end exactly-once Tool execution. Add these through your application infrastructure when required.
Common questions
Why did a Tool not run when I used Pydantic output?
In ModuAgent, Tool selection and final structured output are separate model phases. If no Tool was called, check the model's Tool Calling support, its chat template/parser configuration, the Tool description, and the Agent instructions.
Why did Plan-and-Execute finish with max_steps?
For strict Plan-and-Execute, max_steps is the maximum number of generated
Plan steps. It is not the total number of model requests. Increase it only
when the task genuinely requires more independently verifiable steps.
Where can I see which Tool was actually called?
Use result.metadata["tool_trace"]. A Plan step's allowed_tools lists what
was permitted, not what was executed.
Is InMemoryConversationStore production storage?
No. It is process-local and intended for examples, tests, and development. Use Redis or a durable custom store for multi-process or restart-safe systems.
Public API map
| Need | Main API |
|---|---|
| Quick build and output | Agent.create(), Agent.ask(), AgentRunError |
| Operate a run | Agent.run(), AgentResult, RunLimits, RetryConfig |
| Compose explicitly | Agent, AgentConfig |
| Connect models | VLLMClient, OllamaClient |
| Add Tools | tool, function_tool, ToolSafetyProfile, ToolAuthorizer |
| Choose execution | StandardExecutionProfile, PlanExecutionProfile |
| Validate output | PydanticOutputCodec, TextOutputCodec |
| Keep conversations | ConversationStore, RecentTurnsConversationMemoryPolicy |
| Resume work | CheckpointStore, Agent.resume() |
| Add domain procedures | SkillRegistry, SkillSelector |
| Observe runs | Agent.stream_all(), EventSink, DiagnosticSink, failure_id |
| Inspect configuration | Agent.inspect(), AgentSpec |
Examples
- Report automation Agent:
strict Plan-and-Execute using only
query_dbandplot_graph, with SQLite and PostgreSQL query backends. - Invoice review Skill: Skill instructions, references, and assets.
Documentation
The detailed guides are currently written in Korean.
- Core API: Agent construction and common APIs.
- Advanced API: custom Engines, Tool failure contracts, and extension points.
- Plan-and-Execute: strict state machine and recovery details.
- Conversation Memory: recent turns, token budgets, and summarization.
- Agent Skills: reusable procedures and resource access.
- Operations: security, timeouts, stores, events, and deployment.
- Diagnostics: step timelines, failure correlation, sanitized details, and custom sinks.
- 0.4 migration: source compatibility and checkpoint migration.
- Changelog
Development
Run the offline test suite:
python -m pytest -q tests --ignore=tests/integration
Live vLLM, Ollama, and Redis tests are under tests/integration and skip when
their environment variables are not configured.
ruff check .
ruff format --check .
License
ModuAgent is available under the MIT License.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file moduagent-0.5.1a1.tar.gz.
File metadata
- Download URL: moduagent-0.5.1a1.tar.gz
- Upload date:
- Size: 505.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
729f79766b34009ee5cb800004f85db88c0ac86f5043b0b748940a24f77de620
|
|
| MD5 |
4fe65befce9c2570911448172faea247
|
|
| BLAKE2b-256 |
8df20ffcb878eda0daeb16fb9e9e49db0ac34604923e48702c8aa6a528301f98
|
File details
Details for the file moduagent-0.5.1a1-py3-none-any.whl.
File metadata
- Download URL: moduagent-0.5.1a1-py3-none-any.whl
- Upload date:
- Size: 283.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5f0e6f71d76f03ef5978341df94bbe966a6a1bef4ea94d418c11c3327f9d9339
|
|
| MD5 |
060a4e95d85194466f577e6413a5163d
|
|
| BLAKE2b-256 |
cd58b8e5615b7c7027f2ac32aa9a72b35999305fa32d826039e12fa38050159a
|