Skip to main content

AgentM - an agent framework

Project description

AgentM

A pluggable agent framework in Python. The SDK is a mechanism; every policy is a port; every port has a default; every default is a replaceable extension.

See .claude/designs/pluggable-architecture.md for the boundary contract.

Two concepts

Everything in AgentM is built out of two things:

Extension (also called atom). One Python file that registers behavior on the substrate — a tool, a policy, an LLM provider, a compaction strategy, an observability subscriber, ... Each file exports a MANIFEST (name, version, deps) and an install(api, config) function. The §11 contract requires it to be one file — no atom-to-atom imports, no reach into the runtime substrate. This is the atomic unit of policy.

Scenario. A YAML file naming which extensions to install, in what order, with what config. A scenario is just a composition; it has no Python code of its own (apart from optional scenario-private local: atoms next to the manifest). Switching scenarios switches the entire policy stack without changing the substrate.

The substrate (in agentm.core) is the only unreplaceable part. Extensions reach stateful subsystems only through ExtensionAPI services (api.bus, api.get_operations(), api.skills, api.catalog, ...). A mechanical validator rejects any extension that imports core.runtime.* or core._internal.* directly.

flowchart LR
    SC["--scenario rca<br/>(or default: general_purpose)"] --> L["loader.load_scenario(name)"]
    EX["--extension &lt;dotted&gt;[:JSON]<br/>(repeatable, appended)"] --> R
    UA["&lt;cwd&gt;/.agentm/atoms/*.py<br/>(user atoms, layered on top)"] --> R
    L --> R["resolve &amp; topo-sort by MANIFEST.requires"]
    R --> F["inject floor atoms<br/>(by provides_role, if missing)"]
    F --> I["for each atom:<br/>install(api, config)"]
    I --> S["AgentSession ready"]

Extension: one file

# src/agentm/extensions/builtin/my_atom.py
from agentm.core.abi.extension import ExtensionAPI, ExtensionManifest

MANIFEST = ExtensionManifest(
    name="my_atom",                    # must equal the filename stem
    version="0.1.0",
    requires=("operations_local",),    # by atom NAME; topo-sorted
    provides_role=(),                  # optional capability advertisement
)

def install(api: ExtensionAPI, config: dict) -> None:
    api.bus.subscribe(SomeEvent, handler)
    # `config` came straight from the manifest's per-atom block

Extensions live in three places, all on the same auto-discovery path:

Location Who owns it Mounted by
src/agentm/extensions/builtin/<name>.py SDK scenario manifest, -e, or auto-discovery
contrib/extensions/<name>.py (flat) repo contributors scenario manifest, -e, or auto-discovery
contrib/extensions/<pkg>/... (nested) third-party packages scenario manifest or -e only (no auto-discovery)
<cwd>/.agentm/atoms/*.py the running agent itself auto-layered on top of any scenario

The extensions.validate checker enforces the §11 contract: allowlist is core.abi + core.lib + the public extensions surface. Stateful subsystems are reached only via ExtensionAPI services.

Scenario: a YAML composition of extensions

# contrib/scenarios/general_purpose/manifest.yaml
name: general_purpose
extensions:
  - module: agentm.extensions.builtin.operations_local
  - module: agentm.extensions.builtin.tool_read
  - module: agentm.extensions.builtin.tool_bash
    config:
      timeout_seconds: 60
  - module: agentm.extensions.builtin.observability

Each entry is module: (fully-qualified dotted path) or local: (scenario-private .py next to the manifest). The optional config: block is passed verbatim into that extension's install(api, config) call.

Variants live in the same directory as manifest.<variant>.yaml and must declare name: <scenario>:<variant> (e.g. name: rca:harness.sync). Two scenarios that share most of their stack typically ship one base manifest + several variant files differing only in a few extensions or configs.

Order matters. Extensions are installed in the extensions: declaration order, then re-ordered only as much as MANIFEST.requires forces (a dependency must install before its dependent — otherwise position is preserved). Two practical consequences:

  • For axes where last registration wins (notably the LLM provider, and any api.register_* hook called more than once), an override must appear after the default in the list.
  • For event-bus subscribers, earlier-installed extensions register their handlers first, so they see events ahead of later ones. This is how interceptors / filters slot in.

Two extensions cannot both claim the same provides_role — that is a hard load-time error, not a silent override. Use requires to express "install B after A"; use provides_role to claim a floor capability (command_parser, system_prompt_provider, ...) so the substrate skips its default injection.

Extension vs Scenario — recap:

Extension Scenario
What it is one Python file one YAML file
What it produces behavior at runtime (tool / policy / event subscriber / ...) a list of extensions to install
Composition atomic — does one thing combines many extensions
Mounted by scenario manifest, --extension, auto-discovery --scenario <name> (one at a time)
Substitutable yes, by another extension claiming the same role yes, by another scenario

Enabling: from CLI to a running session

The default scenario is general_purpose — no flag needed:

uv run agentm -p "list files in src/"

Pick another by name (resolves to contrib/scenarios/<name>/manifest.yaml):

uv run agentm --scenario rca "diagnose this trace"
uv run agentm --scenario rca:harness.sync "..."         # variant shorthand

Stack extra extensions on top with -e / --extension (repeatable). Takes a fully-qualified dotted module path — there is no short-name resolution.

uv run agentm --scenario rca \
              -e contrib.extensions.llmharness.adapters.agentm:'{"mode":"sync"}' \
              "..."

Four sources feed the install list, in this precedence order:

Source When Notes
AgentSessionConfig.extensions (programmatic) always wins if non-empty hard override — suppresses scenario, auto-discovery, and --extension
--scenario <name> (or default) when programmatic list is empty resolves a manifest; declaration order preserved, then topo-sorted
Auto-discovery only when no scenario AND no programmatic list scans builtin + flat-file contrib + user-atom dirs
--extension <dotted>[:JSON] always appended (except --no-extensions / programmatic override) repeatable; cannot remove or reorder, only stack

After merging, the list is topologically sorted by MANIFEST.requires (by atom name), and missing floor atoms are injected via provides_role. You cannot stack scenarios — only one loads at a time; compose by writing a new manifest.

--no-extensions bypasses everything for kernel-floor diagnosis (only the LLM provider — no tools, no skills, no observability):

uv run agentm --no-extensions "explain core/abi/loop.py"

Discovering what's available

agentm list-extensions          # every auto-discoverable atom, with its
                                # tier, registered hooks, and the exact
                                # `-e <dotted.path>` form to mount it

agentm --help                   # full flag list (--scenario, --extension,
                                # --resume, --continue, --tools, ...)

ls contrib/scenarios/           # shipped scenarios: general_purpose,
                                # agent_env, chatbot, format_fix,
                                # mcp_demo, rca, rca_hfsm, auditor,
                                # verifier

Beyond the one-shot prompt, agentm has two subcommands: agentm gateway (single-process gateway holding all chat sessions in memory) and agentm trace (query the OTLP/JSON session log). The chat-client peers ship as separate binaries for vendor-SDK isolation only — agentm-terminal and agentm-feishu. Run agentm <sub> --help or <binary> --help for flags.

Five pluggability axes

Each axis is a typing.Protocol in core.abi. The scenario manifest decides which extension fills each role; extensions register via api.register_* hooks.

# Axis Protocol / Port Default impl
1 LLM stream StreamFn extensions.builtin.llm_anthropic (also llm_openai)
2 Tool environment Tool + *Operations LocalFileOperations, LocalBashOperations (via api.get_operations())
3 Session state SessionManager InMemorySessionManager
4 Project context ResourceLoader DefaultResourceLoader
5 Policy / cross-cut EventBus + ExtensionAPI bus + per-extension install hook

Every signal — install, LLM request, tool call, mutation, turn summary — flows through the same EventBus. The observability builtin is a pure subscriber writing OTel-flavored JSONL to <cwd>/.agentm/observability/<trace_id>.jsonl.

Showcase

  • contrib/scenarios/rca/ — root-cause-analysis scenario over observability traces, with optional llmharness audit overlay. Manifest variants (manifest.harness.*.yaml) compose the same extension set with different audit topologies. See its README.
  • contrib/extensions/llmharness/ — cognitive-audit pipeline. Mounts via llmharness.adapters.agentm; subscribes TurnEndEvent to spawn extractor/auditor children and DecideTurnActionEvent to inject reminders into the main loop. Loose-coupled — rca scenarios opt in by manifest only. See its README.

Quick start

uv sync
uv run agentm -p "list files in src/"

Model provider settings can be supplied through environment variables or profiles in ~/.agentm/config.toml ($AGENTM_HOME/config.toml overrides the directory). A minimal config profile:

default_model = "my-model"

[models.my-model]
provider = "openai"
model = "gpt-4o"
base_url = "https://api.openai.com/v1"
api_key = "..."

Select a profile with uv run agentm --model my-model -p "..."; if default_model is set, uv run agentm -p "..." uses it.

Programmatic:

from agentm.core.abi.session_config import AgentSessionConfig
from agentm.core.runtime.session import AgentSession
from agentm.extensions.loader import load_scenario

session = await AgentSession.create(AgentSessionConfig(
    cwd=".",
    extensions=load_scenario("general_purpose"),
    provider=("agentm.extensions.builtin.llm_anthropic",
              {"model": "claude-sonnet-4-6"}),
))
final = await session.prompt("explain core/abi/loop.py")
await session.shutdown()

Setting extensions=[...] is a hard override — scenario, auto-discovery, and --extension are all suppressed. Use it for tests; use scenarios in production.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agentm-0.2.1.tar.gz (432.8 kB view details)

Uploaded Source

Built Distribution

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

agentm-0.2.1-py3-none-any.whl (530.9 kB view details)

Uploaded Python 3

File details

Details for the file agentm-0.2.1.tar.gz.

File metadata

  • Download URL: agentm-0.2.1.tar.gz
  • Upload date:
  • Size: 432.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for agentm-0.2.1.tar.gz
Algorithm Hash digest
SHA256 735db9a374ffc1c1dd35502a6a6654c7483d27197d962d89d11f6b5b6bff2fdf
MD5 4580205452b3b2a9e53d7ab61ebf2efc
BLAKE2b-256 aab394c61e80093be0c7d4e9f65932a1c63ba51b4e7f61f7b7aaff6470e1a222

See more details on using hashes here.

File details

Details for the file agentm-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: agentm-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 530.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for agentm-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e75e02828da2d854c427ffdaf2c71df7d1794b6ce8e76a4721338ddab81b5102
MD5 ee6e299d7697fd1008a28fa4698df182
BLAKE2b-256 17bd6c3572034747d1f0deea2b351c3ce73634d6cf9d09a36b6d87030a37a57b

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 Pingdom Monitoring Sentry Error logging StatusPage Status page