unitares-sdk
Build your own Unitares resident agent. A resident is a long-running (or scheduled) process that checks in to governance, carries an EISV state vector, and participates in the shared knowledge graph. Vigil, Sentinel, and Chronicler are reference implementations.
Install
pip install unitares-sdk
To install the SDK as it shipped with a specific server release, use the Git URL instead:
pip install "unitares-sdk @ git+https://github.com/cirwel/unitares@v2.17.0#subdirectory=agents/sdk"
Replace @v2.17.0 with another server release tag only after checking the
compatibility map.
Pin a commit SHA when reproducing a specific development build.
Or from a checkout of the unitares repo:
pip install -e agents/sdk
Releases are tag-driven. Pushing an
sdk-v*tag whose version matchesversioninpyproject.tomlruns.github/workflows/publish-sdk.yml, which builds and uploads via PyPI Trusted Publishing — OIDC, no API token. The SDK carries its own version series, independent of the server's. See the release process.
Links in this file are absolute on purpose: pyproject.toml uses it as the
package readme, so PyPI renders it outside the repo and relative paths 404.
The package is standalone — it talks to a UNITARES server over MCP/REST and
never needs the server codebase importable. Fully typed (py.typed).
The 30-line resident
from pathlib import Path
from unitares_sdk.agent import CycleResult, GovernanceAgent
from unitares_sdk.client import GovernanceClient
class MyResident(GovernanceAgent):
def __init__(self):
super().__init__(
name="MyResident",
mcp_url="http://127.0.0.1:8767/mcp/",
persistent=True, # protects from auto-archive
refuse_fresh_onboard=True, # explicit bootstrap required
cycle_timeout_seconds=60.0, # hard cap on one cycle
log_file=Path("/tmp/my_resident.log"),
max_log_lines=10_000,
)
async def run_cycle(self, client: GovernanceClient) -> CycleResult | None:
# Do your work here. Return a CycleResult to trigger a check-in,
# or None to skip (useful for "nothing to do this tick" paths).
count = await self.do_scan(client)
if count == 0:
return None
return CycleResult(
summary=f"scanned {count} items",
complexity=0.2,
confidence=0.9,
)
if __name__ == "__main__":
import asyncio
asyncio.run(MyResident().run_forever(interval=60))
First run: UNITARES_FIRST_RUN=1 python my_resident.py — this mints
the identity and stores its UUID anchor at
~/.unitares/anchors/myresident.json. Every subsequent run resumes
that anchor automatically. Never delete anchors: if you do, set
UNITARES_FIRST_RUN=1 again to re-bootstrap (you will get a new UUID).
Extension points
The base class handles MCP connect, identity resolve, check-in, heartbeat, log rotation, state persistence, and graceful shutdown. Override these to extend behavior:
| Hook | When | Signature | Return |
|---|---|---|---|
run_cycle(client) |
Each iteration. The only required override. | (client) -> CycleResult | None |
CycleResult or None |
on_after_checkin(client, checkin_result, cycle_result) |
After each check-in, even on pause/reject. Use for EISV logging, coherence tracking, state writes that need the server response. | All three args typed via unitares_sdk.models. |
None |
on_verdict_pause(client, checkin_result, cycle_result) |
When the server returns a pause verdict. Use for self-recovery. |
Same arg signature as on_after_checkin for consistency. |
True to retry the check-in once; False to let VerdictError propagate. |
Hook exceptions are logged and swallowed — a broken hook cannot take
down a cycle. asyncio.CancelledError always propagates.
Do not override _ensure_identity, _handle_cycle_result, or
_send_heartbeat — those are load-bearing and change across versions.
Constructor reference
| Parameter | Type | Default | Purpose |
|---|---|---|---|
name |
str |
required | Agent display name; drives anchor path (~/.unitares/anchors/<name_lower>.json). |
mcp_url |
str |
http://127.0.0.1:8767/mcp/ |
Governance MCP endpoint. |
persistent |
bool |
False |
Stamp the persistent + autonomous tags on fresh onboard. Set True for long-running residents. |
refuse_fresh_onboard |
bool |
False |
Require UNITARES_FIRST_RUN=1 to mint a new identity. Set True to prevent silent ghost-forks. |
cycle_timeout_seconds |
float | None |
None |
Hard cap on a single run_once / run_forever iteration. MCP's anyio task group can hang on session.initialize if the server flakes — use 60–120s. |
log_file |
Path | None |
None |
Log file to auto-trim after each cycle (in both run_once and run_forever, in a finally block so it fires on error and timeout too). Leave unset if launchd / logrotate owns rotation. |
max_log_lines |
int |
10_000 |
Trim threshold for log_file. |
state_dir |
Path | None |
<repo>/data/<name_lower> |
Default directory for state persistence. Used when state_file is not set. |
state_file |
Path | None |
None |
Explicit cross-cycle state path. Takes precedence over state_dir / "state.json" when set. |
parent_agent_id |
str | None |
None |
Forked-from UUID. Forwards to server on fresh onboard. |
spawn_reason |
str | None |
None |
Registered values: subagent, dialectic_reviewer, dispatch, compaction, explicit, new_session. Unknown values are accepted but receive no live-parent exemption. |
Lifecycle shapes
- Daemon:
asyncio.run(agent.run_forever(interval=60))— loops forever with heartbeats when idle. Reference:agents/sentinel/agent.py. - Scheduled:
asyncio.run(agent.run_once())under launchd / systemd cron. References:agents/chronicler/agent.py,agents/vigil/agent.py.
Both shapes respect cycle_timeout_seconds and auto-trim log_file.
State persistence
self.save_state(d: dict)andself.load_state() -> dictlet yourrun_cyclecarry data across iterations. Writes are atomic (os.replace).- Non-JSON-serializable values (e.g.
datetime,Path) are coerced to theirstr()representation on save rather than raisingTypeError. You get a lossy round-trip, not a silent state-write failure.
Identity rules
- The agent's first MCP call (
onboardoridentity) is the sole source of identity. Do not set identity out-of-band. - UUID is the ground truth.
client_session_idandcontinuity_tokenare cache keys for ephemeral clients; residents don't need them. - Anchors live at
~/.unitares/anchors/<name_lower>.json. One anchor per host per role. The file containsagent_uuidand is written atomically. - Never silent-swap an identity. If the anchor is missing and
refuse_fresh_onboard=True,_ensure_identityraisesIdentityBootstrapRefused— the operator must explicitly setUNITARES_FIRST_RUN=1once to mint a new one.
Not in the SDK (on purpose)
agents/common/findings.pyandagents/common/config.pyare internal to the reference residents in this repo. If you need findings-posting in your own resident, vendor the helper or POST to/api/findingsyourself — the REST contract is the public surface.- The violation taxonomy is server-owned vocabulary
(
src/violation_taxonomy.yaml); consume it via the/v1/taxonomyendpoint rather than importing server code. - Watcher (
agents/watcher/agent.py) uses a different execution model (sync, hook-driven, one-shot per tool-use event) and does not subclassGovernanceAgent.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file unitares_sdk-0.1.0.tar.gz.
File metadata
- Download URL: unitares_sdk-0.1.0.tar.gz
- Upload date:
- Size: 103.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b2a560173d81040899468e241869c4b6dd5ab1c6954cc8e5a3608837893600f
|
|
| MD5 |
32ed4959e9a562c95db74befa22093d1
|
|
| BLAKE2b-256 |
43ffc7538185d669358ba95ee57c480e1eaa76061ba87df0cd80074dcf06fc7f
|
Provenance
The following attestation bundles were made for unitares_sdk-0.1.0.tar.gz:
Publisher:
publish-sdk.yml on cirwel/unitares
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
unitares_sdk-0.1.0.tar.gz -
Subject digest:
1b2a560173d81040899468e241869c4b6dd5ab1c6954cc8e5a3608837893600f - Sigstore transparency entry: 2431037256
- Sigstore integration time:
-
Permalink:
cirwel/unitares@2e55f0007794be4237ab30ec32223e59ffce4ea8 -
Branch / Tag:
refs/tags/sdk-v0.1.0 - Owner: https://github.com/cirwel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-sdk.yml@2e55f0007794be4237ab30ec32223e59ffce4ea8 -
Trigger Event:
push
-
Statement type:
File details
Details for the file unitares_sdk-0.1.0-py3-none-any.whl.
File metadata
- Download URL: unitares_sdk-0.1.0-py3-none-any.whl
- Upload date:
- Size: 75.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dd5d729d762397c9b6669e2e7bea21293b490ff6c8a152dcdd6828c953142385
|
|
| MD5 |
4cdc35205d6f8c9ad612a9046902dd37
|
|
| BLAKE2b-256 |
b683d4c01c6c3c2ddd293b0d7ea74847b002632e338d62de6c6ac8b94bdb6e3b
|
Provenance
The following attestation bundles were made for unitares_sdk-0.1.0-py3-none-any.whl:
Publisher:
publish-sdk.yml on cirwel/unitares
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
unitares_sdk-0.1.0-py3-none-any.whl -
Subject digest:
dd5d729d762397c9b6669e2e7bea21293b490ff6c8a152dcdd6828c953142385 - Sigstore transparency entry: 2431037418
- Sigstore integration time:
-
Permalink:
cirwel/unitares@2e55f0007794be4237ab30ec32223e59ffce4ea8 -
Branch / Tag:
refs/tags/sdk-v0.1.0 - Owner: https://github.com/cirwel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-sdk.yml@2e55f0007794be4237ab30ec32223e59ffce4ea8 -
Trigger Event:
push
-
Statement type: