Metadata-Version: 2.4
Name: cetana
Version: 0.2.0
Summary: Auditable BDI (belief-desire-intention) agents with LLM deliberation — explicit beliefs, committed intentions, and a trace that answers 'why'
Author-email: Ravindu Pabasara Karunarathna <karurpabe@gmail.com>
Maintainer-email: Ravindu Pabasara Karunarathna <karurpabe@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/RavinduPabasara/cetana
Project-URL: Documentation, https://github.com/RavinduPabasara/cetana#readme
Project-URL: Repository, https://github.com/RavinduPabasara/cetana
Project-URL: Bug Tracker, https://github.com/RavinduPabasara/cetana/issues
Keywords: bdi,agents,cognitive architecture,llm,beliefs,intentions,agentic ai,neuro-symbolic
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: black>=24.0; extra == "dev"
Requires-Dist: flake8>=7.0; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# cetana

**Auditable BDI agents — a cognitive architecture for the LLM era.**

[![PyPI version](https://img.shields.io/pypi/v/cetana.svg)](https://pypi.org/project/cetana/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

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) -> str` callable
- Plans may only use **registered actions**, called with arguments that fit
  their signatures; a hallucinated capability or argument is rejected before
  anything runs, not silently improvised around
- 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

```bash
pip install cetana
```

## Quick start

```python
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-5-5", 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`.

Commitment is the default, not a cage. Pass `reconsider` to decide when new
percepts should break it:

```python
def reconsider(intention, percepts):
    return percepts.get("guests_cancelled", False)

agent = BDIAgent("Feed the household dinner", llm, actions, reconsider=reconsider)
```

It is plain Python, so *when* to reconsider stays an architectural decision
you can test, rather than something the LLM is asked every cycle.

## When things go wrong

| What happens | What the agent does |
|---|---|
| An action raises, or returns something other than `ok()` / `fail()` | The step fails with the error as its observation, and the agent replans |
| A plan names an unknown action, or arguments the action doesn't accept | `DeliberationError` (while replanning: the intention fails) |
| The LLM's reply is not a JSON object of the expected shape | `DeliberationError` |
| The replanner gives up, or returns an empty plan | The intention is dropped, never marked achieved |
| Replans run out (`max_replans`) | The intention fails |

Every one of these is recorded in the trace, so `agent.explain()` shows it.

## Testing your agent

The LLM port makes agents deterministic under test:

```python
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.2.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](https://medium.com/@karurpabe) on Medium.

## License

MIT — see [LICENSE](LICENSE).

## Author

**Ravindu Pabasara Karunarathna** — also the author of
[tokscope](https://github.com/RavinduPabasara/tokscope),
[slnic](https://pypi.org/project/slnic/), and
[sinhaladate](https://pypi.org/project/sinhaladate/).
