Skip to main content

SimAgentPlg

English | 简体中文

SimAgentPlg 0.2.4 is a lightweight framework for building stateful OpenAI-compatible agents with composable tool handlers, optional MCP tools, and local skill indexing.

Features

  • Stateful BaseAgent with conversation memory and explicit reset()
  • Immutable, required agent_id owned by each agent
  • OpenAI-compatible model configuration through .env or direct construction
  • Handler-driven tool execution with no separate tool-mode switch
  • Built-in BashHandler for bounded Bash execution
  • Built-in GitDiffHandler for Git working-tree inspection
  • Built-in FinishHandler for explicit task completion
  • MethodToolHandler for small custom Python tools
  • AgentManager with per-agent serialization and cross-agent concurrency
  • Optional MCP integration through McpToolHandler and McpServerManager
  • Optional local skill discovery, indexing, and on-demand loading through SkillManager

Python 3.12 or newer is required.

Installation

Install the local project and dependencies with uv:

uv sync

Configuration

Copy .env.example to .env, then fill in your model credentials:

MODEL_API_KEY=sk-xxxxxxxx
MODEL_URL=https://api.deepseek.com
CHAT_MODEL=deepseek-v4-flash
LLM_TIMEOUT=60
LLM_TEMPERATURE=0.7

ModelConfig.from_env() reads CHAT_MODEL, MODEL_API_KEY, MODEL_URL, LLM_TIMEOUT, and LLM_TEMPERATURE.

You can also construct a config directly:

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

Pass handlers explicitly to enable tool execution:

import json

from simagentplg import (
    BaseAgent,
    BashHandler,
    FinishHandler,
    GitDiffHandler,
    ModelConfig,
)

agent = BaseAgent(
    config=ModelConfig.from_env(),
    agent_id="developer",
    system_prompt="Complete coding tasks using the available tools.",
    handlers=[BashHandler(), GitDiffHandler(), FinishHandler()],
)

result = await agent.runtime(task="Create hello.py that prints 'hello'.")
report = json.loads(result)
print(report["summary"])

await agent.shutdown()

Agents expose only the handlers passed to BaseAgent:

BaseAgent
  -> BashHandler
       -> bash_run
  -> GitDiffHandler
       -> run_gitdiff
  -> FinishHandler
       -> run_finish
  -> MethodToolHandler subclasses
  -> McpToolHandler

In tool mode, ordinary text does not complete a task. The model must call a finishing tool, normally run_finish, or a custom tool must return StepOutcome(..., should_exit=True).

Tool mode stops with an error when:

  • no finishing tool is called within max_steps
  • the same tool and arguments are requested three consecutive times

Built-In Handlers

BashHandler exposes bash_run and executes a bounded Bash command. It has a working directory, timeout, and output limit.

GitDiffHandler exposes run_gitdiff. It inspects the current Git working tree and does not finish the task:

{
  "status": "success",
  "mode": "status",
  "command": "git status --short",
  "output": "?? hello.py\n"
}

Supported modes are status for git status --short, stat for git diff --stat, and diff for git diff.

FinishHandler exposes run_finish. It returns a JSON result and exits the current runtime():

{
  "summary": "Created hello.py"
}

Tool Middleware

ToolMiddleware can inspect tool calls before execution. The framework does not define global risk levels; applications can write their own middleware. BashApprovalMiddleware is an approval gate, not a shell sandbox or security boundary. By default, commands outside a small safe allowlist require y/n approval:

from simagentplg import (
    BaseAgent,
    BashApprovalMiddleware,
    BashHandler,
    FinishHandler,
    ModelConfig,
)

agent = BaseAgent(
    ModelConfig.from_env(),
    agent_id="coder",
    handlers=[BashHandler(), FinishHandler()],
    middlewares=[BashApprovalMiddleware()],
)

The default approval_policy="unless_safe" skips approval only for simple read-only commands such as pwd, ls, git status, git diff, git log, rg, sed -n, cat, and Python unittest invocations. Use approval_policy="always" to review every bash_run command. approval_policy="never" disables this approval gate explicitly.

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()],
)

Handler startup builds one routing table. Duplicate tool names are rejected instead of silently overriding another handler.

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.4.",
        "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.

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("examples/mcp_config.json")],
)

Example MCP configuration:

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest", "--headless"]
    }
  }
}

McpServerManager loads configured services, exposes tools with service-name prefixes, and lets one failed service avoid blocking the rest.

Skills

Skills are optional prompt extensions and remain separate from tool handlers:

from pathlib import Path

from simagentplg import BaseAgent, FinishHandler, ModelConfig

agent = BaseAgent(
    config=ModelConfig.from_env(),
    agent_id="skilled-agent",
    handlers=[FinishHandler()],
    skills_dir=Path("examples/skills"),
)

SkillManager scans each child directory containing SKILL.md, indexes the skill name and YAML front matter description, and injects compact skill metadata into the model context. The model can call the internal load_skill tool to load full SKILL.md, optional template.md, and optional examples/sample.md content on demand. Users can also force a skill with $skill_name or skill:skill_name.

examples/skills/
  release_notes/
    SKILL.md
    template.md
    examples/
      sample.md

Skill context itself does not require handler tools. Register FinishHandler only when the task should finish with run_finish.

Examples

Runnable examples are available in examples/:

uv run python examples/01_stateful_chat.py
uv run python examples/02_custom_tool.py
uv run python examples/03_multi_agent.py
uv run python examples/04_mcp_tools.py
uv run python examples/06_skill.py
uv run python examples/07_bash_approval.py

Testing

Run the test suite from the repository root:

uv run python -m unittest

The current tests cover agents, custom handlers, tool middleware, finish behavior, manager locking/concurrency, and importable examples.

Public API

BaseAgent(
    config: ModelConfig | None = None,
    *,
    agent_id: str,
    system_prompt: str = DEFAULT_SYSTEM_PROMPT,
    handlers: Iterable[BaseHandler] | None = None,
    middlewares: Iterable[Middleware] | None = None,
    skills_dir: str | Path | None = None,
    max_steps: int = 20,
    client: Any | None = None,
)

await agent.runtime(*, task: str) -> str | None
agent.reset(history=None)
await agent.startup()
await agent.shutdown()

Passing handlers puts BaseAgent in tool execution mode and injects the runtime's internal tool protocol system message. Without handlers, the agent is plain chat; skills_dir can still expose the internal load_skill context tool without requiring a finishing tool.

The top-level package exports BaseAgent, ModelConfig, StepOutcome, AgentManager, handler base classes, MethodToolHandler, BashHandler, GitDiffHandler, FinishHandler, McpToolHandler, handler errors, McpServerManager, SkillManager, and default resource paths.

Changes in 0.2.4

  • Added the sibling FinishHandler and built-in run_finish tool
  • Added the sibling GitDiffHandler and built-in run_gitdiff tool
  • Required explicit finishing-tool completion in tool mode
  • Added protection against three identical consecutive tool calls
  • Raised a clear error when tool mode exhausts max_steps
  • Kept BashHandler focused exclusively on bash_run

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

simagentplg-0.2.4.tar.gz (113.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

simagentplg-0.2.4-py3-none-any.whl (31.5 kB view details)

Uploaded Python 3

File details

Details for the file simagentplg-0.2.4.tar.gz.

File metadata

  • Download URL: simagentplg-0.2.4.tar.gz
  • Upload date:
  • Size: 113.0 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

Hashes for simagentplg-0.2.4.tar.gz
Algorithm Hash digest
SHA256 b8d9c64977821cb802a6359b0b4e8304e79335b16628231a016452bd7eb5aa92
MD5 40417e093cb67fc5328d6832fbfe515b
BLAKE2b-256 efd711a1404a611a19cb396f0a1171c31604b1c7cd3ebdb934e24cf61c2baea0

See more details on using hashes here.

File details

Details for the file simagentplg-0.2.4-py3-none-any.whl.

File metadata

  • Download URL: simagentplg-0.2.4-py3-none-any.whl
  • Upload date:
  • Size: 31.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

Hashes for simagentplg-0.2.4-py3-none-any.whl
Algorithm Hash digest
SHA256 f5e16859d8b360af424db54340b111f3a6878ff0ced8a3d6d4a70cdbe9ddcb83
MD5 1344b3c8f7102c765adcad6a54aa8bc1
BLAKE2b-256 41bd5d38af2e9bbab81b727535ebd2b1f248c6934df035a0b80c7a73aaf15145

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page