# agency declares MIT in its metadata and GPL-3.0 in its own pyproject.toml

> operand/agency is a Python library that puts every participant in an application, whether a language model, a plain class, or a person, behind one Agent class and lets them talk through a Space. The API is small enough to read in a sitting, and the packaging metadata around it carries two contradictions worth checking before you depend on it.

**operand/agency** — A fast and minimal framework for building agentic systems

- Repository: https://github.com/operand/agency
- Website: https://createwith.agency
- Stars: 488 · Forks: 28
- Language: Python
- License: MIT
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/operand-agency

## Two licences, one bare author name, and an exclude wider than the include

The packaging metadata disagrees with itself in three places, and all three are visible in pyproject.toml.

The licence line says GPL-3.0. The repository metadata reports MIT. A LICENSE file sits at the root, and neither the README nor any other visible file says which of the three is authoritative. For a library whose whole pitch is dropping into someone else's application, that is the first thing to resolve.

The authors field is a bare name, Daniel Rodriguez, with no email address in the usual name-and-email form. Combined with a single-author field on a library that describes itself as an Actor-model framework, it is worth knowing who you are taking a dependency from before you read further.

The packaging block is the third problem. The file sets include to the agency package tree and then sets exclude to a wildcard covering everything:

```toml
include = ["agency/**/*"]
exclude = ["*"]
```

Whichever way the build tool resolves that precedence, the intent written into the file is ambiguous: the only thing selected for shipping is also the thing the exclude pattern matches. Nothing in the repository documents what actually ends up in the published artifact, and there is no test in the tree that checks it.

The build backend is poetry-core, so the resolution of these rules is Poetry's rather than setuptools', and a lock file is committed alongside it.

## Version 1.6.3 in the manifest, last commit four months ago

The version story is the clearest signal about how this project is maintained.

pyproject.toml declares version 1.6.3, which matches the newest published release, v1.6.3, from 2024-04-24. Before that sit v1.6.1 and v1.6.0, both from the same day on 2023-09-26. So in the visible release history there is a nine-month gap between 1.6.0 and 1.6.1, a seven-month gap to 1.6.3, and no 1.6.2 in the list at all.

The repository was last pushed on 2026-06-10 and is not archived. That is four months of commits on main with the version string still reading 1.6.3. Nothing in the repository marks unreleased work, so anyone pinning agency to 1.6.3 from a lock file is pinning to a state of the source tree that no longer exists, and anyone installing from the index gets the April 2024 code.

The project does say where unfinished work is tracked. Planned Work points at the issues page and invites either an issue or a discussion, which is where you would look to judge whether the four months of commits are fixes, features, or churn.

Contributions are routed through a separate contributing guide at the root of the tree, and the README's contribution section is four lines long, which suggests the review process is documented elsewhere.

## pydantic with no ceiling, and five dependencies that each explain a feature

The dependency list is short enough to read as a design statement.

```toml
python = "^3.9"
pydantic = ">=1.8"
kombu = "^5.3.1"
docstring-parser = "^0.15"
colorlog = "^6.7.0"
pygments = "^2.16.1"
```

kombu is the transport behind the networked Space, and its presence is why the AMQP implementation can talk to RabbitMQ without the library implementing the protocol. docstring-parser is the interesting one: it explains how an action can be a bare decorated method with no schema, since the argument list and description come from the function itself rather than from a class. pygments explains the syntax-highlighted console in the demo, and colorlog the detailed logging listed under Observability and Control.

pydantic is the problem. It is declared as greater than or equal to 1.8 with no upper bound, and the 1.x to 2.x transition is a breaking change with a different API surface. A fresh resolve on any current index gives pydantic 2, which means the floor in the file is decorative unless the library has compatibility code, and nothing in the repository says whether it does.

The Python floor is 3.9, which is older than the development toolchain. The dev group pins pytest 7, pytest-randomly, pytest-watch, and pdoc, and the repository has a separate pytest.ini. pytest-randomly means test order changes between runs, which is a deliberate choice to catch tests that depend on each other and one more reason a single local green run tells you little.

## You add the class, and the second argument becomes the routing key

The communication model is four pieces long, which is the project's main argument for being minimal.

Everything is an Agent. The library is explicit that this covers AI-driven agents, software interfaces, and human users alike, so the same base class represents a language model agent and an ordinary class you wrote.

An agent exposes actions by decorating a method.

```python
class CalculatorAgent(Agent):
    @action
    def add(a, b):
        return a + b
```

Other agents discover and invoke those actions at run time by sending a message that names the recipient, the action, and its arguments as a nested dictionary, with `to` set to the string used at registration rather than to a class name.

```python
other_agent.send({
    'to': 'CalcAgent',
    'action': {
        'name': 'add',
        'args': {
            'a': 1,
            'b': 2,
        }
    },
})
```

The registration is what creates that address.

```python
space = LocalSpace()
space.add(CalculatorAgent, "CalcAgent")
space.add(MyAgent, "MyAgent")
```

Two things are happening in those three lines. You pass the class rather than an instance, so the Space owns instantiation and lifetime, which is what makes it possible to run the same agent in a separate process. And the second argument is the name that appears in the `to` field of every message, so an agent's identity in the system is assigned when it joins a Space, not when it is defined.

There are two Space implementations and the choice is the only architectural decision you make: LocalSpace keeps agents in the same application, and AMQPSpace connects them across a network through an AMQP server such as RabbitMQ.

## ACCESS_REQUESTED requires a review, and nothing says who performs it

Actions can carry an access policy, which is the library's only stated safety mechanism.

```python
@action(access_policy=ACCESS_PERMITTED) # This allows the action at any time
def add(a, b):
    ...

@action(access_policy=ACCESS_REQUESTED) # This requires review before the action
def add(a, b):
    ...
```

Two constants, and the comments say what they do: one permits the action at any time, the other requires review before the action runs.

What the documentation never says is who or what performs that review. Permission callbacks are listed as a feature under the heading Observability and Control, alongside action and lifecycle callbacks, which implies there is a hook to answer the request, but the API overview gives no signature for it, no statement about whether the message is queued or blocked, and no mention of a timeout. So an application that sets ACCESS_REQUESTED is relying on behaviour it cannot see from the documentation.

That matters more here than it would elsewhere, because of how agents are named. An action is reachable by any agent in the same Space that knows the recipient's registered name and the action name, and the example shows those as plain strings in a dictionary. There is no mention of per-caller identity in the message format, which means an access policy is a property of the action rather than a property of the caller, and one agent cannot, on the evidence shown, be distinguished from another.

The stated purpose of that control is controlling access for safety. On the documentation available, it is a global gate per action with an unspecified arbiter.

## after_action receives the error as a string, not as an exception

The lifecycle callbacks are the observability surface, and their signatures say a lot.

```python
def before_action(self, message: dict):
    """Called before an action is attempted"""

def after_action(self, message: dict, return_value: str, error: str):
    """Called after an action is attempted"""
```

Both the return value and the error arrive as strings, in the same callback, and the callback fires after an action is attempted rather than after it succeeds. So failure is a normal return path in this framework: an action that raises is reported to after_action as a populated error string with an empty return value, and the caller receives a message rather than a traceback.

That is a defensible choice for a networked agent system, where an exception has to be serialised into something an AMQP broker can carry anyway. It also means you cannot distinguish, from the callback signature alone, an action that returned the literal string in its error field from one that raised, and you have to inspect both fields on every call.

The other two callbacks are lifecycle rather than action scope. after_add fires once the agent is in a Space and may begin communicating, and before_remove fires before it is removed. Together with before_action and after_action they give you the four points at which an application's behaviour changes state: joining, attempting, completing, and leaving.

None of the four docstrings says whether the callbacks may block, whether raising inside one cancels the action, or what happens to a half-finished action when before_remove runs during an in-flight call. Those are the questions a message-driven system has to answer, and the README does not attempt them.

## The demo has a slash syntax it never defines, and an example the README skips

The demo application is described as an experimental development environment and a demonstration of library features, and it is the most substantial thing in the repository.

It ships multiple agent examples that may communicate with each other: two OpenAI agent examples, a HuggingFace transformers agent example, and operating system access. It includes a Gradio interface, and Docker configuration for reference and development. Running it means following the directions in its own directory rather than anything in the README.

Two gaps are worth knowing before you plan around it.

The first is the slash syntax. The demo supports a slash syntax for invoking actions as an agent yourself, which is the demo's most interesting interaction model, and the README does not give one character of it. There is no example, no grammar, and no pointer to where it is described. If you want the human-in-the-loop pattern the library's actor framing implies, you have to read the demo source.

The second is the example inventory. The top-level tree contains two example directories, examples/demo and examples/mqtt_demo, and the README points only at the first. So the networked Space, which is listed as a Performance and Scalability feature on the strength of AMQP support, has an example directory that the documentation never mentions. If you are evaluating whether the two-process, two-machine story actually works, that directory is where the evidence is.

One more layout note: a site/ directory is committed at the repository root. The development dependencies include pdoc, so that directory is generated API documentation checked into version control rather than built at release time.

## Conclusion

agency is worth a look if you want agents and ordinary software classes sharing one address space with a message protocol between them, since the whole model is one base class, one decorator, and a dictionary-shaped message. It is a poor fit if you need typed contracts or a steady release cadence: the newest published release is v1.6.3 from 2024-04-24 while the branch was last pushed on 2026-06-10, so main has moved a long way without a version bump. Settle two things before you depend on it. The licence is reported as MIT in the repository metadata and declared as GPL-3.0 in pyproject.toml, and pydantic is required at >=1.8 with no ceiling, so a fresh install resolves across the 1.x to 2.x boundary that the dependency set never acknowledges.

## FAQ

### How do I install agency?

Two ways are documented. Either run pip install agency, or add it to a Poetry project with poetry add agency. The package is published under the name agency and the source lives in the agency directory of the repository.

### How do agents find each other in agency?

Through a Space, because an agent cannot communicate until it has been added to one. You pass the class and the name it will be addressed by, as in space.add(CalculatorAgent, "CalcAgent"), and other agents then use that string in the to field of a message. LocalSpace keeps agents in one application and AMQPSpace connects them across a network through an AMQP server such as RabbitMQ.

### What does the ACCESS_REQUESTED policy do in agency?

It requires review before the action runs, where ACCESS_PERMITTED allows the action at any time. Permission callbacks are listed as a feature, but the documentation does not say who or what performs the review, or whether the message waits.

### How does agency report a failed action?

Through the after_action callback, which is called after an action is attempted and takes both a return_value and an error, each typed as a string. Failure is a normal callback path rather than an exception, so the same signature handles success and failure.

### What licence is agency released under?

The repository metadata reports MIT and pyproject.toml declares GPL-3.0, with a LICENSE file at the root of the tree. The README does not state a licence, so the two declarations have to be resolved with the project rather than inferred.

## Sources

- [License: MIT](https://github.com/operand/agency/blob/main/LICENSE)
- [operand/agency on GitHub](https://github.com/operand/agency)
- [Project website](https://createwith.agency)
- [README](https://github.com/operand/agency/blob/main/README.md)
- [Releases](https://github.com/operand/agency/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/operand-agency
