SimAgentPlg
SimAgentPlg 0.2.1 is a lightweight framework for stateful OpenAI-compatible agents, composable tool handlers, MCP integration, and role-based multi-agent workflows.
Features
- Stateful
BaseAgentwith explicitreset()support - Immutable, required
agent_idowned by each agent - Reusable local and external tool handlers
- Built-in
BashHandlerfor bounded command execution - Built-in
FinishHandlerfor explicit completion and Git change reporting AgentManagerwith per-agent serialization and cross-agent concurrency- Linear
AgentWorkflowfor planner, executor, reviewer, and other roles - Optional MCP tools and local skills
- OpenAI-compatible model configuration
Python 3.12 or newer is required.
Installation
pip install simagentplg
Or install the local project with uv:
uv sync
Configuration
Create a .env file:
CHAT_MODEL=deepseek-v4-flash
SKILL_MODEL=deepseek-v4-flash
MODEL_API_KEY=sk-xxxxxxxx
MODEL_URL=https://api.deepseek.com
LLM_TIMEOUT=60
LLM_TEMPERATURE=0.7
ModelConfig.from_env() reads these variables. A configuration can also be
constructed directly and shared by multiple agents:
from simagentplg import ModelConfig
config = ModelConfig(
model="deepseek-v4-flash",
api_key="sk-xxxxxxxx",
base_url="https://api.deepseek.com",
)
Quick Start
Plain Chat
Tool execution is disabled by default. A plain agent keeps conversation
history between runtime() calls:
from simagentplg import BaseAgent, ModelConfig
agent = BaseAgent(
config=ModelConfig.from_env(),
agent_id="tutor",
system_prompt="You are a concise Python tutor.",
)
first = await agent.runtime(task="Remember that I prefer Python.")
second = await agent.runtime(task="Which language do I prefer?")
agent.reset()
await agent.shutdown()
Tool Mode
Set enable_tools=True to expose the built-in tools:
import json
from simagentplg import BaseAgent, ModelConfig
agent = BaseAgent(
config=ModelConfig.from_env(),
agent_id="developer",
system_prompt="Complete coding tasks using the available tools.",
enable_tools=True,
)
result = await agent.runtime(
task="Create hello.py that prints 'hello'."
)
report = json.loads(result)
print(report["summary"])
print(report["changes"])
await agent.shutdown()
Tool-enabled agents automatically include two sibling handlers:
BaseAgent
-> BashHandler
-> bash_run
-> FinishHandler
-> run_finish
-> custom handlers
-> McpToolHandler
bash_run executes a bounded Bash command. When the task is complete, the
model must call run_finish with a non-empty summary. Returning ordinary text
does not finish a tool task.
run_finish returns a JSON result and exits the current runtime():
{
"summary": "Created hello.py",
"changes": {
"available": true,
"repository": "/repo/root",
"added": ["hello.py"],
"modified": [],
"deleted": []
}
}
The change report compares Git state at the beginning and end of the current
task. Existing dirty files are omitted unless the task changes them again.
run_finish does not commit, stage, or revert files. Outside a Git repository,
the task can still finish with changes.available set to false.
Tool mode stops with an error when:
run_finishis not called withinmax_steps- the same tool and arguments are requested three consecutive times
Custom Tool Handlers
MethodToolHandler maps a tool named add to an async method named
do_add:
from collections.abc import Mapping
from typing import Any
from simagentplg import (
BaseAgent,
MethodToolHandler,
ModelConfig,
StepOutcome,
)
ADD_TOOL = {
"type": "function",
"function": {
"name": "add",
"description": "Add two numbers.",
"parameters": {
"type": "object",
"properties": {
"left": {"type": "number"},
"right": {"type": "number"},
},
"required": ["left", "right"],
},
},
}
class MathHandler(MethodToolHandler):
def __init__(self) -> None:
super().__init__((ADD_TOOL,))
async def do_add(
self,
arguments: Mapping[str, Any],
) -> StepOutcome:
return StepOutcome(
{"value": arguments["left"] + arguments["right"]}
)
agent = BaseAgent(
config=ModelConfig.from_env(),
agent_id="calculator",
handlers=[MathHandler()],
enable_tools=True,
)
Handler startup builds one routing table. Duplicate tool names are rejected
instead of silently overriding another handler. A custom tool may also return
StepOutcome(data=..., should_exit=True) to terminate the task.
Agent Manager
Each agent owns its identity, so registration does not repeat the ID:
from simagentplg import AgentManager, BaseAgent, ModelConfig
config = ModelConfig.from_env()
manager = AgentManager()
manager.register(
BaseAgent(
config=config,
agent_id="writer",
system_prompt="You write concise release notes.",
)
)
manager.register(
BaseAgent(
config=config,
agent_id="reviewer",
system_prompt="You review software changes for risk.",
)
)
results = await manager.run_many(
{
"writer": "Write release notes for version 0.2.1.",
"reviewer": "Review the release for compatibility risks.",
}
)
await manager.shutdown()
Calls to the same agent are serialized because they share message history.
Calls to different agents can run concurrently. run_many() returns failures
as values so one failed agent does not cancel the others.
run_isolated(agent_id, task) resets and executes an agent while holding the
same per-agent lock. It is used by workflows to prevent implicit history from
leaking between roles or steps.
Role-Based Workflow
AgentWorkflow executes different agent roles as a validated linear pipeline:
from simagentplg import (
AgentManager,
AgentWorkflow,
BaseAgent,
ModelConfig,
WorkflowStep,
)
config = ModelConfig.from_env()
manager = AgentManager()
manager.register(
BaseAgent(
config=config,
agent_id="planner",
system_prompt="Create concise implementation plans.",
)
)
manager.register(
BaseAgent(
config=config,
agent_id="executor",
system_prompt="Execute the plan using tools.",
enable_tools=True,
)
)
manager.register(
BaseAgent(
config=config,
agent_id="reviewer",
system_prompt="Review completed work for correctness and risk.",
)
)
workflow = AgentWorkflow(
manager,
[
WorkflowStep(
name="plan",
agent_id="planner",
prompt="Plan this task:\n{input}",
),
WorkflowStep(
name="execute",
agent_id="executor",
prompt=(
"Original task:\n{original_task}\n\n"
"Execute this plan:\n{input}"
),
),
WorkflowStep(
name="review",
agent_id="reviewer",
prompt="Review the execution result:\n{execute}",
),
],
)
result = await workflow.run("Implement user login")
print(result.final_output)
await manager.shutdown()
Workflow templates support:
{input}: previous step output, or the original task for the first step{original_task}: the task passed toworkflow.run(){step_name}: output from an already completed named step
Unknown and forward references are rejected when the workflow is created.
Steps stop at the first failure and WorkflowExecutionError preserves the
failed step, original cause, and completed results. Version 0.2.1 supports
linear steps only; branching, loops, and automatic retries are not included.
MCP Tools
MCP is opt-in and follows the same handler contract:
from simagentplg import BaseAgent, McpToolHandler, ModelConfig
agent = BaseAgent(
config=ModelConfig.from_env(),
agent_id="browser",
handlers=[McpToolHandler("my_project/mcp_config.json")],
enable_tools=True,
)
Example MCP configuration:
{
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--headless"]
}
}
Only tools explicitly registered by McpToolHandler are routed to MCP.
Skills
Skills are optional prompt extensions and remain separate from tool handlers:
from pathlib import Path
from simagentplg import BaseAgent, ModelConfig
skills_dir = Path("example/skills")
agent = BaseAgent(
config=ModelConfig.from_env(),
agent_id="skilled-agent",
skills_dir=skills_dir,
enable_tools=True,
)
SkillManager scans each child directory containing SKILL.md. The routing
model selected by SKILL_MODEL chooses the matching skill from its YAML front
matter. The skill definition and optional template and sample are then
injected into the agent context:
example/skills/
release_notes/
SKILL.md
template.md
examples/
sample.md
Skills currently run through the tool-mode lifecycle, so set
enable_tools=True and finish with run_finish. See
example/06_skill.py for a complete local skill
example.
Examples
Runnable examples are available in example/:
uv run python example/01_stateful_chat.py
uv run python example/02_custom_tool.py
uv run python example/03_multi_agent.py
uv run python example/04_mcp_tools.py
uv run python example/05_role_workflow.py
uv run python example/06_skill.py
Public API
BaseAgent(
config: ModelConfig | None = None,
*,
agent_id: str,
system_prompt: str = REACT_LOOP_PROMPT,
handlers: Iterable[BaseHandler] | None = None,
enable_tools: bool = False,
skills_dir: str | Path | None = None,
max_steps: int = 20,
)
await agent.runtime(*, task: str) -> str | None
agent.reset(history=None)
await agent.startup()
await agent.shutdown()
The top-level package exports BaseAgent, ModelConfig, StepOutcome,
AgentManager, workflow types, handler base classes, BashHandler,
FinishHandler, McpToolHandler, and resource defaults.
Changes in 0.2.1
- Added the sibling
FinishHandlerand built-inrun_finishtool - Added per-task Git change reporting
- Required explicit
run_finishcompletion in tool mode - Added protection against three identical consecutive tool calls
- Raised a clear error when tool mode exhausts
max_steps - Kept
BashHandlerfocused exclusively onbash_run
Development
uv run python -m unittest discover -s tests -p "test*.py" -v
License
MIT
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 simagentplg-0.2.1.tar.gz.
File metadata
- Download URL: simagentplg-0.2.1.tar.gz
- Upload date:
- Size: 103.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1644e98b99ba123fe29e217091902cf2deaf7e77e2eaf37953fca3fb03190b89
|
|
| MD5 |
aad2c42c11ef147f514ab75cd7784e0a
|
|
| BLAKE2b-256 |
7dffb6956445c696d33fdf93a06fb2b9d3554c2a35d819bc2def3638df6689ea
|
File details
Details for the file simagentplg-0.2.1-py3-none-any.whl.
File metadata
- Download URL: simagentplg-0.2.1-py3-none-any.whl
- Upload date:
- Size: 28.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
684417a03f819d29602ece5957d8a12f0fac95a3e2bd20e1e2be8bc21a1d064e
|
|
| MD5 |
034aa0271bfee5e5b3465f209f4ebde3
|
|
| BLAKE2b-256 |
758fb360f39da3310091a97fa08a0dc0d6d031733c266265bdd0ffd9e18d7bf4
|