Metadata-Version: 2.4
Name: unshadow
Version: 0.4.0
Summary: Canonical Unshadow memory client, v1 compatibility and shared Hermes transport.
Author-email: Unshadow <support@unshadow.dev>
License-Expression: MIT
Project-URL: Homepage, https://unshadow.dev
Project-URL: Documentation, https://unshadow.dev/docs/api/sdk
Project-URL: Repository, https://github.com/unshadow-ai/unshadow
Project-URL: Issues, https://github.com/unshadow-ai/unshadow/issues
Keywords: unshadow,memory,langchain,llamaindex,rag
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
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: Topic :: Software Development :: Libraries
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.3; extra == "langchain"
Provides-Extra: llamaindex
Requires-Dist: llama-index-core>=0.11; extra == "llamaindex"
Dynamic: license-file

<p align="center">
  <a href="https://unshadow.dev">
    <img src="https://unshadow.dev/unshadow-mark-on-white.png" alt="Unshadow" width="96" height="96">
  </a>
</p>

<p align="center">
  <a href="https://pypi.org/project/unshadow/"><img src="https://img.shields.io/pypi/v/unshadow" alt="PyPI"></a>
  <a href="https://pypi.org/project/unshadow/"><img src="https://img.shields.io/pypi/pyversions/unshadow" alt="Python"></a>
  <a href="https://unshadow.dev/docs/api/sdk"><img src="https://img.shields.io/badge/docs-unshadow.dev-111111" alt="Docs"></a>
  <a href="https://github.com/unshadow-ai/unshadow/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-111111" alt="MIT"></a>
</p>

# Unshadow

Python client for the canonical [Unshadow](https://unshadow.dev) memory API, with existing Engine and Bank clients retained as v1 compatibility wrappers. Version 0.4.0 source adds durable capture, source-backed recall, session history and native Hermes defaults on the shared transport. Registry publication is a separate release step.

## Canonical client (0.3.0)

```python
from unshadow import UnshadowClient, UnshadowHttpError, UnshadowValidationError

memory = UnshadowClient(api_key="unshadow_…")
saved = memory.retain(
    messages=[{"role": "user", "text": "The project uses PostgreSQL."}],
    conversation_key="thread-42",
    checkpoint={"expected_hash": None, "chunk_id": "chunk-1"},
    idempotency_key="thread-42-chunk-1",
)
# Persist only the acknowledged checkpoint; pass its hash for the next chunk.
checkpoint = saved["checkpoint"]
recalled = memory.recall("Database choice?", trust_floor="advisory")
packed = memory.context("Database choice?", token_budget=1500)
project = memory.project.get()
memory.project.put(
    instructions={"mission": "Build the project", "directives": ""},
    expected_version=project["version"],
)
```

Methods: `retain`, `recall`, `context`, `reflect`, `feedback`, `correct`, `forget`, `explain`, `project.get/put`, `key_info` and `health`. They return the raw canonical responses, including operation IDs, checkpoint receipts, provenance and trust. You can pass a request mapping instead of keyword fields. A unified project-bound key is required for v2; old keys and projects are not silently relabeled. `Unshadow` and `UnshadowBank` keep their v1 methods and responses.

The default base URL is `https://api.unshadow.dev/v2`, with a 30-second total request timeout. Set `base_url` and `timeout` explicitly when needed; a custom base includes the API version path. HTTPS is required. `allow_insecure_localhost=True` permits HTTP only on localhost, 127.0.0.1 or ::1. Redirects are rejected without forwarding credentials. Health sends no credential.

Each canonical method accepts `idempotency_key` and `cancel_event` (a `threading.Event`). Eligible GET failures retry once, honoring `Retry-After` within the total deadline. Billable legacy search retries only 429 admission failures. POST/PUT and ambiguous network failures never replay automatically, even with an idempotency key. Checkpoint/version conflicts preserve their details and require caller review; the client never changes an expected hash to force a write through.

`UnshadowHttpError` exposes `status`, `code`, `request_id`, `details`, `retry_after`, `retry_after_seconds` and the compatible `body`. Invalid requests raise `UnshadowValidationError` before network admission. Retain accepts at most 32,000 Unicode code points across input messages, 256 messages and 256 KiB serialized UTF-8. Bank/Hermes retain and Engine extraction also reject oversized text rather than clipping it. Error output and Hermes transport logging omit raw credentials/provider payloads.

The deadline covers networking, retry waiting and JSON response parsing. Since Python threads cannot forcibly interrupt every OS resolver call, work may briefly outlive cancellation; checks prevent subsequent retries and invocation after the resolver returns. A process-wide cap of 32 transport workers bounds stalled work, with a typed local 503 on saturation. Cancellation does not prove the server performed no work. Use stable operation/checkpoint identities and reconcile unknown outcomes before replaying writes.

LangChain and LlamaIndex retrievers accept either client generation with the same framework interfaces. Canonical recall maps evidence to documents while preserving trust metadata. `prefer_context` maps the retriever limit to the v2 context contract. Hermes uses the canonical v2 client; `UnshadowHermesClient` remains a v1 compatibility wrapper for existing scripts.

## Hermes

The native directory wrapper is prepared separately from the private monorepo and declares `unshadow==0.4.0`. After publication use `hermes plugins install <public-wrapper-URL> --ref <full-reviewed-SHA>`, then `hermes memory setup` and select `unshadow`. The release manifest records the real URL/commit; do not assume the local preview is published. On PM-managed Hermes, dependency preparation belongs to the plugin manager. An SDK installed in `/tmp/venv` alone is not discovery in Hermes' environment. Owner-managed builds may retain the existing valid Python class entry points.

Fresh write connections default automatic recall and capture on; verified readonly keys recall only. Explicit `auto_capture=false` / `auto_recall=false` remain off. Primary completed turns own capture; compression/session end/delegation do not copy full transcripts or tool payloads. Capture is sanitized and persisted to a protected 200-entry/10 MiB outbox before transport. Failed acknowledgements survive restart; full/corrupt/busy storage fails visibly. The legacy checkpoint path remains available only through explicit `capture_protocol=legacy`.

Automatic recall uses canonical source evidence and the cited project brief, with untrusted-data labels and a 1,200-token default. The default is project-wide developer memory so explicit saved facts and other approved hosts remain visible. Optional `subject_id` config is an opaque host identity hashed like n8n's subject selector; it filters within the granted project and is not customer authorization. Captured roles distinguish user decisions and assistant suggestions.

Native recall fetches fresh context each turn to respect external correction and deletion; legacy recall retains its bounded 60-second cache. Queued scopes survive session rotation. `diagnostics()` includes Python/provider, verified project/grants, pending entries, current-process last acknowledgement and safe errors. `hermes unshadow status` reads local setup/pending state without secrets. Host discovery/status does not prove remote readiness. Hermes logs lifecycle failures nonfatally, so inspect undrained-work diagnostics after outages.

Local native-loader/MemoryManager checks cover released Hermes v2026.9.24 and separately pinned current main with an installed wheel and synthetic HTTP backend; this is not a live LLM/production certification. `lattice` class entry point and `lattice-hermes` imports remain compatible. Existing `lattice.json` is read when `unshadow.json` is absent.

```bash
pip install unshadow
```

| Extra | Install | What you get |
|-------|---------|----------------|
| LangChain | `pip install "unshadow[langchain]"` | `langchain_unshadow.UnshadowRetriever` |
| LlamaIndex | `pip install "unshadow[llamaindex]"` | `llama_index_unshadow.UnshadowRetriever` |

TypeScript: [`npm i @unshadow/sdk`](https://www.npmjs.com/package/@unshadow/sdk). n8n: [`@unshadow/n8n-nodes-unshadow`](https://www.npmjs.com/package/@unshadow/n8n-nodes-unshadow). Docs: [unshadow.dev/docs/api/sdk](https://unshadow.dev/docs/api/sdk). API keys: [unshadow.dev/install/api](https://unshadow.dev/install/api).

## Engine

`Unshadow` matches the TypeScript SDK: `context`, `search`, `extract`, `forget`, `profile`.

```python
from unshadow import Unshadow

unshadow = Unshadow(api_key="unshadow_…")
packed = unshadow.inject_context("What am I working on?", token_budget=1500)
hits = unshadow.search("pnpm", limit=8, category="preference")
unshadow.extract(
    "User prefers pnpm",
    source_type="ai_conversation",
    idempotency_key="turn-1",
)
unshadow.forget(["11111111-1111-1111-1111-111111111111"])
```

`extract(..., idempotency_key=)` sends `Idempotency-Key` without automatically retrying the write. Eligible GET reads retry once on 429/5xx; billable search retries only 429. Search rows include `supersedes` and `superseded_by`. Filter with `unshadow.search("editor", category="preference")`.

### LangChain

```python
from langchain_unshadow import UnshadowRetriever
from unshadow import Unshadow

unshadow = Unshadow(api_key="unshadow_…")
retriever = UnshadowRetriever(client=unshadow, k=8, prefer_context=True)
docs = retriever.invoke("What did we decide about pnpm?")
```

`prefer_context` uses `POST /context` and splits `[mem:uuid]` lines. This is not `ConversationBufferMemory`.

### LlamaIndex

```python
from llama_index_unshadow import UnshadowRetriever
from unshadow import Unshadow

unshadow = Unshadow(api_key="unshadow_…")
retriever = UnshadowRetriever(unshadow, k=8, prefer_context=True)
nodes = retriever.retrieve("What did we decide about pnpm?")
```

## Agent bank

`UnshadowBank` is retain / recall / authorize. Those calls use `/bank/*`.

```python
from unshadow import UnshadowBank

bank = UnshadowBank(api_key="unshadow_…")
bank.remember("Ship on Fridays is forbidden.", conversation_key="agent-main")
packed = bank.context("release policy")
receipt = bank.authorize(["memory-id"], query="deploy production")
bank.explain(packed["operation_id"])
```

| Name | HTTP |
|------|------|
| `remember` | `POST /bank/retain` (delta, exact 32k limit, conflicts returned) |
| `explain` | `POST /bank/explain` |
| `authorize` | `POST /bank/recall` with `purpose: tool_arg` |

`retain` is an alias of `remember`.

The bank client does not ingest files. Capture stores pages and repo notes. Link that project into the agent bank. `remember(..., source_url=...)` cites a URL.
