cetana
Auditable BDI agents — a cognitive architecture for the LLM era.
Most LLM agents are a prompt loop: state lives in a transcript, "goals" are whatever the last completion implied, and nobody can say why the agent did what it did. The classical answer to exactly this problem is 30 years old: the Belief–Desire–Intention architecture (Bratman 1987; Rao & Georgeff 1995) — explicit mental state, deliberate commitment, coherent behaviour over time.
cetana is BDI rebuilt for LLMs, on one principle:
The architecture decides when to think. The LLM decides what to think.
- Beliefs are a revisable, sourced, timestamped store — not lines in a prompt
- Intentions are commitments: the agent does not re-deliberate every cycle, so behaviour is stable, cheap, and predictable
- The LLM is consulted at exactly two points — option generation and
planning — through a provider-agnostic
(prompt) -> strcallable - Plans may only use registered actions; a hallucinated capability is a caught error, not silent improvisation
- Every cycle is recorded:
agent.explain()answers "why did you do that?" with the actual cognitive history
Zero dependencies. Any LLM client. Fully deterministic under test (script the LLM, assert the behaviour).
cetanā (චේතනා) is the Pali term for volition — in Buddhist psychology, the mental factor that directs the mind toward action.
Installation
pip install cetana
Quick start
from cetana import ActionRegistry, BDIAgent, ok, fail
actions = ActionRegistry()
@actions.register("check_fridge", "See what ingredients are available")
def check_fridge():
return ok({"rice": True, "eggs": 2})
@actions.register("cook", "Cook a named dish")
def cook(dish):
return ok(f"{dish} ready")
# Any callable (prompt: str) -> str works: Anthropic, OpenAI, local, or a stub.
import anthropic
client = anthropic.Anthropic()
def llm(prompt):
msg = client.messages.create(
model="claude-opus-4-8", max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return msg.content[0].text
agent = BDIAgent("Feed the household dinner", llm, actions)
agent.believe("time", "evening")
agent.run()
print(agent.explain())
# cycle 1:
# options: cook_dinner: Cook rice for dinner; wait: ...
# committed to: Cook rice for dinner — it's evening and rice is available
# plan: check_fridge -> cook
# executed: check_fridge({}) [ok] obs={'rice': True, 'eggs': 2}
# cycle 2:
# executed: cook({'dish': 'rice'}) [ok] obs='rice ready'
# intention: succeeded
The cycle
percepts ──> revise beliefs ──> committed? ──yes──> execute next step ──> observe
│ no │
└─> deliberate (LLM) ─> commit intention ┘
Deterministic architecture code does perception, belief revision, commitment bookkeeping, execution, and failure accounting. The LLM fills two cognitive gaps — what is worth pursuing? and how? — and on step failure, a bounded replan (still worth it? new plan or give up?).
Why commitment matters
A prompt-loop agent reconsiders everything every turn — one odd completion
and it wanders. A BDI agent deliberates, commits, and then executes until
the intention succeeds, fails, or is deliberately dropped. Fewer LLM calls,
stabler behaviour, and a meaningful answer to "what is the agent doing right
now?": read agent.intention.
Testing your agent
The LLM port makes agents deterministic under test:
class ScriptedLLM:
def __init__(self, *responses):
self.responses = list(responses)
def __call__(self, prompt):
return self.responses.pop(0)
Script deliberation, assert on agent.trace — no API key, no flakiness.
This library's own test suite works exactly this way.
Status & roadmap
Early release (0.1.0) — the core loop, belief store, commitment machinery, trace, and failure handling are complete and tested. Planned: belief decay and confidence-weighted revision, multiple concurrent intentions with priority scheduling, a structured-output deliberation mode, and provider extras. The design is discussed in my Artificial Cognitive Systems series on Medium.
License
MIT — see LICENSE.
Author
Ravindu Pabasara Karunarathna — also the author of tokscope, slnic, and sinhaladate.
Release files for cetana 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cetana-0.1.0.tar.gz | 17.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cetana-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 32.8 kB
Release files / cetana-0.1.0.tar.gz
| Download URL | cetana-0.1.0.tar.gz |
|---|---|
| Size | 17.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
144973fb13764e0e9bdb49041d225008e267afe548a4e0a34ece912f117f80b7
|
|
BLAKE2b-256 checksum How to use checksums |
4b73772500c3b5dc226c8f887c0ea73eb53614dcf521ecb7b683560c64d84bc4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.2
|
Release files / cetana-0.1.0-py3-none-any.whl
| Download URL | cetana-0.1.0-py3-none-any.whl |
|---|---|
| Size | 15.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1954d06f481207112b87c8e1fa040c38f890e62798e04a31f57070973776dcbe
|
|
BLAKE2b-256 checksum How to use checksums |
5a6ec1666ebf13a53bca586bdea5e29646f2526e693d1a894242d08298e8f091
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.2
|