Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions docs/sage_integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# SAGE persistent memory integration

[SAGE](https://github.com/l33tdawg/sage) (Sovereign Agent Governed
Experience) is a BFT-consensus memory layer for AI agents. This
integration wires ii-agent into a SAGE node via the official async Python
SDK so each agent turn recalls prior committed memories before the model
runs and stores an observation after the turn completes.

## Installation

The SAGE SDK is an optional extra — the framework runs fine without it:

```bash
pip install "ii-agent[sage]"
```

## Environment variables

All configuration is driven by environment variables. No secrets live in
the source tree.

| Variable | Default | Description |
| ------------------------ | -------------------- | --------------------------------------------------------------------------------------------------------- |
| `SAGE_ENABLED` | `false` | Master switch. When false, `register_sage_hooks` is a no-op and the integration never contacts any node. |
| `SAGE_NODE_URL` | `http://localhost:8090` | Base URL of the SAGE REST API. |
| `SAGE_AGENT_ID` | unset | Optional display name for the agent on the SAGE network. |
| `SAGE_AGENT_KEY` | unset | Filesystem path to a 32-byte Ed25519 seed. Falls back to `AgentIdentity.default()`. |
| `SAGE_DEFAULT_DOMAIN` | `ii-agent` | Domain tag used for recall and propose when no per-call override is supplied. |
| `SAGE_RECALL_TOP_K` | `5` | Number of memories fetched per pre-hook recall. |
| `SAGE_PRE_HOOK_TIMEOUT_S`| `2.0` | Strict timeout (seconds) for the pre-hook recall. On timeout the agent turn proceeds with no context. |

## Registering the integration

```python
from ii_agent.agents.agent import IIAgent
from ii_agent.integrations.sage import register_sage_hooks

agent = IIAgent(
user_id="user-123",
session_id="session-abc",
model=...,
)

# Appends a pre-hook and a post-hook to the agent. No-op when SAGE_ENABLED is false.
register_sage_hooks(agent)
```

`register_sage_hooks` extends (not replaces) the agent's existing
`pre_hooks` / `post_hooks` lists, so it is safe to combine SAGE with
other observability or policy hooks.

## Turn lifecycle

1. **Pre-hook (blocking, bounded)** — before the model runs, the pre-hook
embeds the user's input and calls `AsyncSageClient.query()` for
semantically similar committed memories under `SAGE_DEFAULT_DOMAIN`.
Results are formatted into a context block and prepended to the
user's input so the model sees both.
2. **Post-hook (background)** — after the model responds, the post-hook
is scheduled as a FastAPI background task via
`@hook(run_in_background=True)`. It submits a concise observation of
the turn via `AsyncSageClient.propose()`. The agent response is
returned to the caller without waiting for BFT consensus.

## Fallback behaviour

The integration is defensive by design. **None** of the following
conditions block or fail an agent turn:

- `SAGE_ENABLED` unset or `false` — `register_sage_hooks` is a no-op.
- `sage-agent-sdk` extra not installed — the integration logs a debug
message and falls back to a no-op.
- SAGE node unreachable — `is_available()` returns `False`; recall
returns an empty list; propose is skipped.
- Pre-hook recall times out (default 2 s) — the turn proceeds with no
injected context.
- Any SDK exception on recall or propose — caught, logged at debug level,
swallowed.

If you need end-to-end correctness guarantees (e.g. an audit trail that
cannot skip turns), run the SAGE node in-process or behind a reverse
proxy with its own retry semantics — this integration deliberately
prioritises agent-turn latency over delivery guarantees.

## Testing

Unit tests live under `src/tests/unit/integrations/` and cover:

- Hook registration is a no-op when `SAGE_ENABLED` is false.
- Hook registration appends two callables to the agent when enabled.
- Turn flow — pre-hook fetches recall, post-hook submits observation —
with the SDK mocked out end to end.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ gaia = [
"termcolor>=3.0.1",
"uvicorn[standard]>=0.29.0",
]
sage = [
"sage-agent-sdk>=6.6.1",
]

[build-system]
requires = ["hatchling"]
Expand Down
9 changes: 9 additions & 0 deletions src/ii_agent/agents/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,15 @@ def __post_init__(self) -> None:
for sub_agent in self.sub_agents:
self._initialize_sub_agent(sub_agent)

# Opt-in SAGE persistent memory integration. Activated only when
# SAGE_ENABLED=true in the environment; otherwise a no-op.
try:
from ii_agent.integrations.sage import register_sage_hooks

register_sage_hooks(self)
except Exception as exc: # noqa: BLE001 — integration must never block agent init
logger.debug(f"SAGE integration skipped: {exc}")

def _initialize_sub_agent(self, sub_agent: "IIAgent") -> None:
"""Initialize a sub-agent with shared context from parent."""
# Share session store if not set
Expand Down
43 changes: 43 additions & 0 deletions src/ii_agent/core/config/sage_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Configuration for the SAGE persistent memory integration.

All settings are driven by environment variables so no secrets live in the
source tree. The integration is strictly opt-in via ``SAGE_ENABLED``.
"""

from __future__ import annotations

from pydantic_settings import BaseSettings


class SageConfig(BaseSettings):
"""Settings for the SAGE memory integration.

Attributes:
enabled: Master switch. When false (default) the integration is a
no-op and never contacts a SAGE node.
node_url: Base URL of the SAGE REST API (e.g. ``http://localhost:8090``).
agent_id: Optional agent identifier (display name) used on
``register_agent``. If unset, registration is skipped.
agent_key: Optional filesystem path to a 32-byte Ed25519 seed.
Falls back to :meth:`AgentIdentity.default()` which respects
``SAGE_IDENTITY_PATH``.
default_domain: Domain tag used for recall and propose when no
per-call override is supplied.
recall_top_k: Number of memories to fetch per pre-hook recall.
pre_hook_timeout_s: Strict timeout for the pre-hook recall. On
timeout the hook yields control immediately with no injected
context — the agent turn is never blocked.
"""

enabled: bool = False
node_url: str = "http://localhost:8090"
agent_id: str | None = None
agent_key: str | None = None
default_domain: str = "ii-agent"
recall_top_k: int = 5
pre_hook_timeout_s: float = 2.0

class Config:
env_prefix = "SAGE_"
env_file = ".env"
extra = "ignore"
31 changes: 31 additions & 0 deletions src/ii_agent/integrations/sage/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""SAGE persistent-memory integration for ii-agent.

SAGE (Sovereign Agent Governed Experience) is a BFT-consensus memory layer
for AI agents. This integration wires an :class:`IIAgent` instance into a
SAGE node via the async SDK so each turn recalls prior memories before the
model runs and stores an observation after the turn completes.

The integration is strictly opt-in: when ``SAGE_ENABLED`` is unset or
``false``, :func:`register_sage_hooks` is a no-op and the agent behaves as
if the integration did not exist. If the optional ``sage-agent-sdk``
dependency is not installed the integration also falls back to a no-op
with a debug log — the framework continues to work end-to-end.

Install extras::

pip install "ii-agent[sage]"

Typical usage::

from ii_agent.agents.agent import IIAgent
from ii_agent.integrations.sage import register_sage_hooks

agent = IIAgent(...)
register_sage_hooks(agent)
"""

from __future__ import annotations

from ii_agent.integrations.sage.registrar import register_sage_hooks

__all__ = ["register_sage_hooks"]
Loading