Metadata-Version: 2.4
Name: saign
Version: 0.1.0
Summary: Streaming Artificially Intelligent Graph Navigator: multi-agent LLM coordination on top of SGN.
Author-email: Chad Hanna <crh184@psu.edu>
License: MPL-2.0
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: sgn
Requires-Dist: litellm
Requires-Dist: pyyaml
Provides-Extra: live
Requires-Dist: rich; extra == "live"
Provides-Extra: test
Requires-Dist: saign[live]; extra == "test"
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: pytest-markdown-docs; extra == "test"
Provides-Extra: docs
Requires-Dist: mkdocs; extra == "docs"
Requires-Dist: mkdocstrings; extra == "docs"
Requires-Dist: mkdocstrings-python; extra == "docs"
Requires-Dist: mkdocs-material; extra == "docs"
Requires-Dist: mkdocs-gen-files; extra == "docs"
Requires-Dist: mkdocs-literate-nav; extra == "docs"
Requires-Dist: mkdocs-section-index; extra == "docs"
Requires-Dist: pymdown-extensions; extra == "docs"
Provides-Extra: lint
Requires-Dist: black; extra == "lint"
Requires-Dist: flake8; extra == "lint"
Requires-Dist: flake8-bandit; extra == "lint"
Requires-Dist: flake8-black; extra == "lint"
Requires-Dist: flake8-bugbear; extra == "lint"
Requires-Dist: flake8-future-annotations; extra == "lint"
Requires-Dist: flake8-isort; extra == "lint"
Requires-Dist: flake8-logging; extra == "lint"
Requires-Dist: flake8-pyproject; extra == "lint"
Requires-Dist: isort; extra == "lint"
Requires-Dist: mypy; extra == "lint"
Requires-Dist: mypy-extensions; extra == "lint"
Requires-Dist: typing_extensions; extra == "lint"
Provides-Extra: dev
Requires-Dist: saign[docs]; extra == "dev"
Requires-Dist: saign[lint]; extra == "dev"
Requires-Dist: saign[test]; extra == "dev"
Dynamic: license-file

<h1 align="center">saign</h1>

<p align="center"><i>Streaming Artificially Intelligent Graph Navigator</i></p>

`saign` (pronounced "sane") is a multi-agent LLM coordination framework built
on top of [SGN](https://git.ligo.org/greg/sgn). It uses SGN's pad-DAG
iteration model as a control-loop substrate: each iteration is a "tick,"
elements maintain explicit state across ticks, and a coordinator gates state
transitions across the fleet.

> **Status:** past the initial skeleton. The architectural spine —
> `StatefulElement`, `Coordinator`, and the `LogSource`/`LogSink` blackboard
> cycle-breaker — is in place and exercised end-to-end. LLM integration (via
> [litellm](https://github.com/BerriAI/litellm), with a synchronous tool-use
> loop) and a durable `SQLiteBlackboard` backend are implemented. Budget/cost
> controls are not yet implemented.

## Installation

```bash
pip install -e ".[dev]"
```

## Example

A minimal saign pipeline has agents (`StatefulElement` subclasses) and one
coordinator (`Coordinator` subclass), wired by `with_blackboard`:

```{.python notest}
from sgn import Pipeline
from saign import Coordinator, Directive, StatefulElement, with_blackboard


class MyAgent(StatefulElement):
    default_states = {"nominal", "diagnosing", "diagnosed", "fixing"}

    def decide(self, state, inputs):
        # ...inspect inputs, current state, self.latest_directive...
        return state

    def act(self, state, inputs):
        # ...take action, return freeform context for outgoing report...
        return {"observation": "..."}


class MyCoordinator(Coordinator):
    default_states = {"watching", "deciding"}

    def decide(self, state, inputs):
        # ...read agent reports from inputs, decide next state...
        return state

    def act(self, state, inputs):
        self._directives = {"agent1": Directive(action="fix", rationale="...")}
        return None


a1 = MyAgent(name="agent1", sink_pad_names=["context"],
             source_pad_names=["report"], initial_state="nominal")
coord = MyCoordinator(name="coord",
                      sink_pad_names=["context", "from_agent1"],
                      source_pad_names=["report", "directives"],
                      initial_state="watching")

p = Pipeline()
bb = with_blackboard(p, agents=[a1], coordinator=coord, period=1.0)
p.run(auto_parallelize=False)
```

## Topology

```
log_src.context ──┬→ each agent.context
                  └→ coord.context

agent.report ──────────┬→ coord.from_<agent>
                       └→ log_snk.from_<agent>      (broadcast)

coord.report     ──────→ log_snk.from_<coord>
coord.directives ──────→ log_snk.directives_from_<coord>
```

Reports flow direct (zero-tick latency). Directives flow through the
blackboard with one-tick delay — this asymmetry is what keeps the topology
a DAG; agents read their latest directive next tick via
`StatefulElement.latest_directive`.

## Related

- [`sgn`](https://git.ligo.org/greg/sgn) — the streaming pipeline framework
  saign is built on.

## License

MPL-2.0 (same as SGN).
