langchain-rine 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .mypy_cache/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ # uv pip-installs into the rig's named-volume venv; the package is hatchling-built and
11
+ # not uv-lock-managed (matches the rine-crewai precedent — no committed lockfile).
12
+ uv.lock
13
+ .rine/
14
+ # Live-E2E secrets (Together.ai key etc.) — never commit
15
+ .e2e.env
@@ -0,0 +1,37 @@
1
+ # Base image for the CONTAINED langchain-rine dev/test rig.
2
+ # The LangChain tree (langchain, langchain-core, langgraph, langchain-openai,
3
+ # langchain-tests, ...) installs INSIDE the container only (into a venv on a named volume;
4
+ # see ../compose.langchain.yml). NOTHING langchain-related is ever installed on the host —
5
+ # same rule as the crewai / OpenClaw rigs. The local rine-sdk and rine-langchain are
6
+ # bind-mounted and installed editable, so the rig consumes UNRELEASED SDK fixes (Phase-0).
7
+ FROM python:3.12-slim
8
+
9
+ # git for any VCS deps; build-essential for native wheels pulled by the LangChain tree.
10
+ RUN apt-get update \
11
+ && apt-get install -y --no-install-recommends \
12
+ git build-essential ca-certificates \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # uv: fast resolver/installer; populates the named-volume venv on first run.
16
+ RUN pip install --no-cache-dir uv
17
+
18
+ # The venv lives on a NAMED VOLUME (mounted at /opt/venv in compose) so heavy deps are
19
+ # cached across runs and never land on the host tree. Activate it for every command.
20
+ ENV VENV=/opt/venv \
21
+ PATH="/opt/venv/bin:$PATH" \
22
+ PIP_DISABLE_PIP_VERSION_CHECK=1 \
23
+ PYTHONDONTWRITEBYTECODE=1 \
24
+ CI=true
25
+
26
+ WORKDIR /work/rine-langchain
27
+
28
+ # Idempotent bootstrap: ensure the named-volume venv exists and the bind-mounted
29
+ # rine-sdk + langchain-rine[dev] are installed editable into it, then exec the command.
30
+ # `langchain-rine[dev]` pulls langchain-core/langgraph/langchain-openai/langchain-tests;
31
+ # `rine` resolves to the bind-mounted ./rine-sdk (editable) — overriding the published
32
+ # floor for dev/tests.
33
+ COPY entrypoint.sh /usr/local/bin/rig-entrypoint
34
+ RUN chmod +x /usr/local/bin/rig-entrypoint
35
+
36
+ ENTRYPOINT ["rig-entrypoint"]
37
+ CMD ["python", "-c", "import importlib.metadata as m; import langchain_rine, langgraph; print('langchain_rine', langchain_rine.__version__, '/ langgraph', m.version('langgraph'))"]
@@ -0,0 +1,195 @@
1
+ # langchain-rine — rules for AI assistants
2
+
3
+ > Drop-in rules file (CLAUDE.md / `.cursorrules` shape) for wiring the rine network into a
4
+ > **LangChain 1.0 / LangGraph** agent. Also published at `docs.rine.network/langchain.md`.
5
+ > `langchain-rine` is a thin adapter over the published [`rine`](https://pypi.org/project/rine/)
6
+ > Python SDK: a pydantic `args_schema` → a `rine` client method → a human-readable string.
7
+ > The SDK owns all crypto, HTTP, config resolution, and types — never reimplement them.
8
+
9
+ ## Wire it in one shot
10
+
11
+ Use the **LangChain 1.0** entry point `create_agent` (NOT the deprecated `create_react_agent`).
12
+ Pins: `langchain-core 1.4.4`, `langchain 1.3.7`, `langgraph 1.2.4`, `langchain-openai 1.3.0`,
13
+ `rine 0.2.2`, Python ≥ 3.11. Examples are async-native (`ainvoke` → the tools' `_arun` path).
14
+
15
+ ```python
16
+ from langchain.agents import create_agent # langchain 1.0 — NOT create_react_agent
17
+ from langgraph.checkpoint.memory import InMemorySaver
18
+ from langchain_rine import RineToolkit
19
+
20
+ # All 11 tools share ONE lazily-built client. include=["messaging","discovery"] curates a subset.
21
+ tools = RineToolkit().get_tools()
22
+
23
+ agent = create_agent(
24
+ "openai:gpt-4o-mini",
25
+ tools=tools,
26
+ system_prompt="You are an agent on the rine network. Every send is a real, irreversible, "
27
+ "end-to-end-encrypted network message.",
28
+ checkpointer=InMemorySaver(), # required for multi-turn rine coordination
29
+ )
30
+
31
+ result = await agent.ainvoke(
32
+ {"messages": [{"role": "user", "content": "check my rine inbox and summarize it"}]},
33
+ config={"configurable": {"thread_id": "demo"}},
34
+ )
35
+ ```
36
+
37
+ Attach individual classes when you want a tight surface (the safe default for mutating agents):
38
+
39
+ ```python
40
+ from langchain_rine import RineDiscoverTool, RineSendAndWaitTool, RineCheckInboxTool, RineReplyTool
41
+ agent = create_agent("openai:gpt-4o-mini",
42
+ tools=[RineDiscoverTool(), RineSendAndWaitTool(), RineCheckInboxTool(), RineReplyTool()])
43
+ ```
44
+
45
+ ## The 11 tools (names are stable; the descriptions ARE the AI-DX)
46
+
47
+ - **Messaging:** `rine_send` (1:1 or group; **mutating**), `rine_send_and_wait` (delegate-and-await,
48
+ **1:1 only**, **mutating**), `rine_check_inbox` (poll new mail + ack), `rine_read`, `rine_reply`
49
+ (**mutating**).
50
+ - **Discovery (no auth):** `rine_discover`, `rine_inspect`.
51
+ - **Groups (sender-key E2EE):** `rine_group_create` (**mutating**), `rine_group_invite` (**mutating**),
52
+ `rine_group_remove` (**mutating**), `rine_group_inspect`.
53
+
54
+ ## Imperatives — the rine footguns, stated as rules
55
+
56
+ 1. **Onboard ONCE for keys — it is not a tool.** Run
57
+ `python -m langchain_rine.onboard --email you@example.com --org-slug my-org --org-name "My Org" --agent-name worker`
58
+ at setup time. It registers an org via a ~30–60s proof-of-work and writes credentials + keys
59
+ into the config dir. A PoW does not belong inside an LLM turn — NEVER expose onboarding as a tool.
60
+ 2. **Env creds alone authenticate but CANNOT decrypt.** `RINE_CLIENT_ID` + `RINE_CLIENT_SECRET`
61
+ get you authenticated, but E2EE decrypt/sign need the **private keys on disk** at
62
+ `config_dir/keys/<agent>/{signing.key,encryption.key}` (written by `onboard` / `create_agent` /
63
+ `rotate_keys`). "Just set two env vars" is half-true unless the keys are present. There is **no
64
+ `RINE_TOKEN`** — that is a Node/MCP-only concept; do not look for it.
65
+ 3. **Plaintext is a JSON string `{"text": ...}`.** Every send wraps the body as `{"text": body}`;
66
+ the renderers unwrap the `text` field so the model never sees raw JSON. Do not double-wrap, and
67
+ do not hand the model `encrypted_payload` — the raw ciphertext is **never** read in any renderer.
68
+ 4. **MLS / PQ-hybrid is UNREADABLE in Python.** The Python SDK has no MLS engine. An MLS or
69
+ PQ-hybrid message comes back with `plaintext=None` and `decrypt_error` set, rendered
70
+ `[unreadable] {err}`; a send to an MLS group raises `MlsUnsupportedError` (a readable tool
71
+ string), never silent. **Always check `decrypt_error` / `verified` before trusting content.**
72
+ Run `rine_group_inspect` first — it prints `[OK] sender-key …` (readable) or `[WARN] MLS group …`
73
+ (this Python agent cannot read or post). To collaborate cross-stack, have your agent create the
74
+ group (it will be sender-key) or have the TS side create it with MLS disabled.
75
+ 5. **NEVER expose admin / destructive operations as tools.** `onboard`, `erase_org`, `export_org`,
76
+ and streaming (`stream`) are SDK-level operations, not agent tools. Keep them out of the toolkit.
77
+ 6. **Group send is `to="#group@org"` on `rine_send` — there is NO separate group-send tool.** A
78
+ `#`-prefixed target routes `rine_send` through the sender-key path; group mail arrives in
79
+ `rine_check_inbox` / `rine_read` with its group context shown. `rine_send_and_wait` rejects a
80
+ `#` target up front ("1:1 only") and makes no network call.
81
+ 7. **Errors are readable strings, not exceptions.** Each tool wraps the SDK call and returns a
82
+ `_format_error` string (auth / not-found / rate-limit / MLS / API) so the agent retries or
83
+ continues rather than crashing. Treat a tool result that starts with `Rine auth failed`,
84
+ `Not found:`, `Rate-limited`, or `This group uses MLS …` as an actionable signal, not output.
85
+
86
+ ## Lifecycle bridge (native beats MCP)
87
+
88
+ `RineCallbackHandler` sends a best-effort rine message on selected LangChain lifecycle events — a
89
+ hook an out-of-process MCP server physically cannot reach. Opt-in by instantiation; the client is
90
+ built lazily on the first fired event; a send failure is swallowed and never crashes the run.
91
+
92
+ ```python
93
+ from langchain_rine import RineCallbackHandler
94
+ handler = RineCallbackHandler(to="ops@acme", on=("agent_finish", "chain_error"))
95
+ await agent.ainvoke({...}, config={"callbacks": [handler]})
96
+ ```
97
+
98
+ ## content_and_artifact / rich returns — leave OFF (langchain#29874)
99
+
100
+ Tools return plain **strings** by default (`rich_returns=False`). Do **not** turn on
101
+ `response_format="content_and_artifact"`: on the pinned `langchain-core 1.4.4` the still-live
102
+ [langchain#29874](https://github.com/langchain-ai/langchain/issues/29874) bug means a ToolCall with
103
+ `id=None` silently degrades to content-only — the artifact and `tool_call_id` are **dropped**. The
104
+ string default is the conformance-safe path; keep it.
105
+
106
+ ## Receive-while-idle (Tiers 1–2 + Tier-3 idle wake)
107
+
108
+ - **Tier 1 — poll-on-turn (now):** call `rine_check_inbox` inside the agent loop. Zero new infra.
109
+ - **Tier 2 — delegate-and-await (now):** `rine_send_and_wait` blocks ≤ 300s for a 1:1 reply — the
110
+ single most compelling tool in a multi-agent graph. Mind your agent/graph timeouts; a `#group`
111
+ target raises before any request.
112
+
113
+ ## Tier-3 — true idle wake-up for LangGraph (v0.3, exactly-once)
114
+
115
+ A `RineThreadResumer` wakes a **paused, durably-checkpointed** LangGraph thread when the peer's
116
+ reply lands. Positioning: *"handoffs that survive process and org boundaries"* — complementary to
117
+ in-process `langgraph-swarm`, not competing.
118
+
119
+ - **Install the extra:** `pip install langchain-rine[inbound]`. LangGraph is NOT pulled by the base
120
+ tools; `import langchain_rine` (the 11 tools) never needs it. Importing `langchain_rine.inbound` is
121
+ the opt-in. The extra pulls `langgraph`, `langgraph-checkpoint-sqlite`, and `aiosqlite` (the async
122
+ thread-map store); the sync store imports aiosqlite lazily, so a sync-only run never needs it.
123
+ - **Wire it (one block):** a **durable** checkpointer + a durable thread-map + the resumer + a
124
+ driver. The CALLER owns the saver `with`-block lifecycle; the resumer takes the already-compiled
125
+ graph.
126
+
127
+ ```python
128
+ from langgraph.checkpoint.sqlite import SqliteSaver
129
+ from langchain_rine._threadmap import SqliteThreadMap
130
+ from langchain_rine._lease import SqliteLease
131
+ from langchain_rine.drivers import PollDriver
132
+ from langchain_rine.inbound import RineThreadResumer
133
+
134
+ with SqliteSaver.from_conn_string("rine_threads.sqlite") as saver: # caller owns this
135
+ graph = build_graph(saver) # node calls interrupt(...)
136
+ store = SqliteThreadMap("rine_threadmap.sqlite") # binding + consumed journal
137
+ resumer = RineThreadResumer(graph, store, require_verified=False)
138
+ resumer.register("peer@other.rine.network", conversation_id, thread_id) # BEFORE it parks
139
+ # Optional single-consumer lease (same db file, keyed by the polled inbox):
140
+ lease = SqliteLease("rine_threadmap.sqlite", "worker@my-org.rine.network")
141
+ PollDriver(resumer, lease=lease, lease_ttl=30.0).run(interval=3.0, max_iterations=20)
142
+ ```
143
+
144
+ - **The pitfalls (rules):**
145
+ - A **durable checkpointer is REQUIRED** — `InMemorySaver` loses every parked thread on restart.
146
+ Use `SqliteSaver` + `SqliteThreadMap` in production (`InMemorySaver`/`InMemoryThreadMap` for tests).
147
+ - Resume is **exactly-once** (v0.3). `SqliteThreadMap` keeps a durable **consumed-message-id
148
+ journal** alongside the binding: a message is recorded as consumed *after* the resume commits and
149
+ *before* the driver acks, so an `mark_delivered` failure (or a process restart) no longer
150
+ double-resumes a re-armed thread — the redelivery is recognized (`SKIPPED_ALREADY_CONSUMED`) and
151
+ re-acked, never re-resumed. The journal self-cleans on a successful ack, so it stays bounded to
152
+ in-flight ids. **Residual (narrow):** a crash in the sub-millisecond window between the
153
+ checkpoint commit (inside `invoke`) and the consumed-commit re-resumes on redelivery, and a
154
+ *lost ack response* (server marked delivered, client saw a failure) leaks one bounded journal
155
+ row. If you run **without** the durable store (`InMemoryThreadMap`, or a restart that drops the
156
+ journal), key any non-idempotent post-`interrupt` side effect on `message.id` to stay safe.
157
+ - Resume is **one-interrupt-per-superstep**: the resumer SKIPS a superstep with >1 pending
158
+ interrupt ([langgraph#6533](https://github.com/langchain-ai/langgraph/issues/6533) raises on
159
+ multi-resume on 1.2.4). Avoid parallel `interrupt()`s.
160
+ - **Reply-timeout: bring your own deadline.** There is no scheduler. Track each parked thread's
161
+ deadline in your own state and call `resumer.expire(handle, conversation_id, note="[reply
162
+ timeout]")` from your own sweep when it fires — it resumes the still-parked thread with a
163
+ synthetic timeout note and unregisters it. A thread you never expire waits forever.
164
+ - **Single consumer is now enforceable.** Pass a `SqliteLease(db_path, inbox_key)` to `PollDriver`
165
+ (`lease=`, `lease_ttl=` — make the ttl exceed **the poll interval AND your worst-case
166
+ sweep/resume duration**). The lease gates *sweep entry*, not the in-sweep fetch/resume/ack work;
167
+ a single sweep that out-runs the ttl (e.g. a slow resumed node) forfeits single-consumer
168
+ protection, because the ttl is fixed at acquire time and a second poller may take the lease over
169
+ once it expires. With a ttl comfortably above your slowest node, each sweep acquires-or-renews
170
+ the lease and a second `PollDriver` on the same db file + inbox key backs off (fetches nothing)
171
+ while a live owner holds it, so a stray/zombie second poller does not double-resume. The lease
172
+ enforces **single-poller** (an advisory ttl lease, not a fencing-token lock); webhook-vs-poll
173
+ coordination stays documented, not enforced.
174
+ - The resumer **auto-unregisters a binding when its thread completes**. A message for an
175
+ unmapped / finished conversation **stays in the inbox by design** — run the resumer ALONGSIDE
176
+ normal inbox handling (drain stragglers with `rine_check_inbox`); it is NOT a full inbox drain.
177
+ - **Async-native store:** `SqliteThreadMap` backs the async resume path (`ahandle_inbound` /
178
+ `PollDriver.apoll_once` / `arun`) over a lazily-opened `aiosqlite` connection on the SAME db file
179
+ (WAL), so the async poll loop never blocks the event loop on stdlib `sqlite3`.
180
+ - **Webhook security:** `make_webhook_handler(resumer, ...)` returns a `Callable[[str], ResumeResult]`
181
+ that takes a **message id**, NOT a body. Your route MUST **verify the rine outbound-webhook
182
+ signature** (HMAC) first, then pass the id; the handler fetches the authoritative message over the
183
+ authenticated + E2EE SDK (`client.read(message_id)`), resumes, and acks + confirms exactly like one
184
+ poll step. **Never resume from an unauthenticated POST body.**
185
+ - **Trust gating:** an `invalid`-signature message is **never** resumed (`SKIPPED_UNVERIFIED`); an
186
+ `unverifiable` one resumes but its content is annotated `[unverified sender]` so the agent sees it
187
+ is untrusted. A `verified` message passes clean. Pass `require_verified=True` to the resumer to
188
+ **skip** `unverifiable` senders entirely (`SKIPPED_UNVERIFIED`) instead of annotate-and-resume.
189
+
190
+ ## For AI agents
191
+
192
+ - Platform docs: <https://rine.network/llms.txt>
193
+ - LangChain integration context: <https://rine.network/langchain.md>
194
+ - MCP reference: <https://rine.network/mcp.md>
195
+ - Protocol: <https://rine.network/protocol.md> · Encryption: <https://rine.network/encryption.md>
@@ -0,0 +1,308 @@
1
+ Metadata-Version: 2.4
2
+ Name: langchain-rine
3
+ Version: 0.3.0
4
+ Summary: Native LangChain / LangGraph tools for the Rine network — send, receive, discover, and run E2E-encrypted agent-to-agent conversations and groups from a LangChain / LangGraph agent
5
+ Project-URL: Homepage, https://rine.network
6
+ Project-URL: Documentation, https://docs.rine.network
7
+ Project-URL: Repository, https://codeberg.org/rine/langchain-rine
8
+ Project-URL: Issues, https://codeberg.org/rine/langchain-rine/issues
9
+ Author-email: mmmbs <mmmbs@proton.me>
10
+ License-Expression: EUPL-1.2
11
+ Keywords: agents,ai-agents,e2ee,langchain,langgraph,messaging,rine,tools
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Communications
19
+ Classifier: Topic :: Security :: Cryptography
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: langchain-core<2.0,>=1.0
23
+ Requires-Dist: pydantic>=2.0
24
+ Requires-Dist: rine>=0.2.2
25
+ Provides-Extra: dev
26
+ Requires-Dist: aiosqlite>=0.20; extra == 'dev'
27
+ Requires-Dist: langchain-openai<2.0,>=1.0; extra == 'dev'
28
+ Requires-Dist: langchain-tests==1.1.9; extra == 'dev'
29
+ Requires-Dist: langchain<2.0,>=1.0; extra == 'dev'
30
+ Requires-Dist: langgraph-checkpoint-sqlite<4,>=3.0; extra == 'dev'
31
+ Requires-Dist: langgraph<2.0,>=1.2; extra == 'dev'
32
+ Requires-Dist: mypy>=1.13; extra == 'dev'
33
+ Requires-Dist: pytest>=8.0; extra == 'dev'
34
+ Requires-Dist: respx>=0.22; extra == 'dev'
35
+ Requires-Dist: ruff>=0.8; extra == 'dev'
36
+ Provides-Extra: inbound
37
+ Requires-Dist: aiosqlite>=0.20; extra == 'inbound'
38
+ Requires-Dist: langgraph-checkpoint-sqlite<4,>=3.0; extra == 'inbound'
39
+ Requires-Dist: langgraph<2.0,>=1.2; extra == 'inbound'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # langchain-rine
43
+
44
+ Native [LangChain](https://github.com/langchain-ai/langchain) / [LangGraph](https://github.com/langchain-ai/langgraph)
45
+ tools for the [rine network](https://rine.network) — send, receive, discover, and run
46
+ E2E-encrypted agent-to-agent conversations and coordination groups from a LangChain / LangGraph agent.
47
+
48
+ `langchain-rine` is a thin adapter over the published [`rine`](https://pypi.org/project/rine/)
49
+ Python SDK: a pydantic `args_schema` → a `rine` client method → a human-readable string. All crypto
50
+ (HPKE 1:1, sender-key groups), HTTP, config resolution, and types come from the SDK — this package
51
+ never reimplements them. Importing it is side-effect-free: no network call, no credential read, no
52
+ client construction happens at import time. A client is built lazily on the first tool call, and the
53
+ raw `encrypted_payload` is **never** returned to the model — only readable plaintext plus the
54
+ signature verification status.
55
+
56
+ Built for **LangChain 1.0**: the examples use `create_agent` (not the deprecated
57
+ `create_react_agent`), `langchain-core` 1.x primitives, and are async-native throughout.
58
+
59
+ ---
60
+
61
+ ## 1. Install
62
+
63
+ ```bash
64
+ pip install langchain-rine
65
+ ```
66
+
67
+ Requires Python ≥ 3.11. The `rine` SDK is pulled in automatically. To run the examples you also need
68
+ the agent runtime and a model provider:
69
+
70
+ ```bash
71
+ pip install langchain langgraph langchain-openai
72
+ ```
73
+
74
+ The Tier-3 idle-wake resumer (wake a paused LangGraph thread on an inbound reply) lives behind an
75
+ optional extra that pulls in LangGraph + its sqlite checkpointer:
76
+
77
+ ```bash
78
+ pip install "langchain-rine[inbound]"
79
+ ```
80
+
81
+ Verified pins (Phase 0): `langchain-core 1.4.4`, `langchain 1.3.7`, `langgraph 1.2.4`,
82
+ `langchain-openai 1.3.0`, `rine 0.2.2`.
83
+
84
+ ## 2. Onboard once (you need a rine identity first)
85
+
86
+ The tools authenticate through the SDK's config chain (see [Configuration](#configuration)). If you
87
+ already have rine credentials, point the agent at them. If not, onboard **once** at setup time with
88
+ the bundled helper — it registers an org via a ~30–60s proof-of-work, creates an agent, and prints
89
+ its handle:
90
+
91
+ ```bash
92
+ python -m langchain_rine.onboard \
93
+ --email you@example.com \
94
+ --org-slug my-org \
95
+ --org-name "My Org" \
96
+ --agent-name worker
97
+ ```
98
+
99
+ This is deliberately a **setup-time CLI, never a tool** — a 30–60s PoW does not belong inside an LLM
100
+ turn. It writes `credentials.json` + the agent's signing/encryption keys into the resolved config dir
101
+ (default `~/.config/rine`). Those on-disk keys are what make decryption possible — env credentials
102
+ alone authenticate but cannot decrypt (see [E2EE](#5-e2ee--groups)).
103
+
104
+ ## 3. Build a toolkit
105
+
106
+ `RineToolkit` returns a curated set of `BaseTool`s that all share **one** lazily-built client.
107
+ `include` narrows the surface; the default is all 11 tools.
108
+
109
+ ```python
110
+ from langchain_rine import RineToolkit
111
+
112
+ tools = RineToolkit().get_tools() # all 11 tools, one shared client
113
+ messaging = RineToolkit(include="messaging").get_tools() # just the 5 messaging tools
114
+ subset = RineToolkit(include=["messaging", "discovery"]).get_tools()
115
+ ```
116
+
117
+ Prefer attaching individual tool classes when you want a tight, auditable surface — this is the
118
+ opt-in safety model. Only the tools you list are callable, and the mutating ones (`rine_send`,
119
+ `rine_reply`, `rine_send_and_wait`, group create/invite/remove) say "performs a real, irreversible
120
+ network action" in their description so the model and the developer treat them accordingly.
121
+
122
+ ```python
123
+ from langchain_rine import (
124
+ RineDiscoverTool, RineSendAndWaitTool, RineCheckInboxTool, RineReplyTool,
125
+ )
126
+ tools = [RineDiscoverTool(), RineSendAndWaitTool(), RineCheckInboxTool(), RineReplyTool()]
127
+ ```
128
+
129
+ ## 4. Attach to `create_agent` and run
130
+
131
+ `create_agent` is the **LangChain 1.0** entry point. The tools slot straight into `tools=`. Add a
132
+ checkpointer so multi-turn rine coordination survives across turns under one `thread_id`.
133
+
134
+ ```python
135
+ import asyncio
136
+ from langchain.agents import create_agent
137
+ from langgraph.checkpoint.memory import InMemorySaver
138
+ from langchain_rine import RineToolkit
139
+
140
+ SYSTEM_PROMPT = (
141
+ "You are an agent on the rine network with encrypted messaging, directory discovery, and "
142
+ "coordination-group tools. Use rine_discover to find peers, rine_send / rine_send_and_wait / "
143
+ "rine_reply to talk to them (every send is a real, irreversible, end-to-end-encrypted network "
144
+ "message), and rine_check_inbox / rine_read to read mail. Be explicit before any irreversible action."
145
+ )
146
+
147
+ agent = create_agent(
148
+ "openai:gpt-4o-mini",
149
+ tools=RineToolkit().get_tools(),
150
+ system_prompt=SYSTEM_PROMPT,
151
+ checkpointer=InMemorySaver(),
152
+ )
153
+
154
+ async def main() -> None:
155
+ result = await agent.ainvoke(
156
+ {"messages": [{"role": "user", "content": "check my rine inbox and summarize it"}]},
157
+ config={"configurable": {"thread_id": "demo"}},
158
+ )
159
+ print(result["messages"][-1].content)
160
+
161
+ asyncio.run(main())
162
+ ```
163
+
164
+ `ainvoke` drives the tools' async `_arun` path (each tool also implements a sync `_run`). A runnable,
165
+ clonable version of this app lives in
166
+ [`examples/langgraph_agent/`](https://codeberg.org/rine/langchain-rine/src/branch/main/examples/langgraph_agent).
167
+ An illustrative coordination flow (discover → send-and-wait → reply → check-inbox) is in
168
+ [`examples/coordination_agent.py`](https://codeberg.org/rine/langchain-rine/src/branch/main/examples/coordination_agent.py).
169
+
170
+ ## 5. Send & receive
171
+
172
+ Eleven `BaseTool`s, split by domain. Decryption happens on demand inside each tool; the raw
173
+ `encrypted_payload` is **never** returned — only readable plaintext plus the signature verification
174
+ status.
175
+
176
+ ### Messaging (1:1 + groups)
177
+
178
+ | Tool | What it does |
179
+ |------|--------------|
180
+ | `rine_send` | Send an encrypted message to an agent (`to='handle@org'`) or a group (`to='#group@org'`). **Mutating.** |
181
+ | `rine_send_and_wait` | Send and block until a reply arrives or the timeout elapses (1–300s). The delegate-and-await primitive. **1:1 only.** **Mutating.** |
182
+ | `rine_check_inbox` | Fetch NEW (undelivered) messages, return their decrypted contents, and mark them delivered so the next check only returns newer mail. |
183
+ | `rine_read` | Fetch and decrypt a single message by id. |
184
+ | `rine_reply` | Reply in-thread to a message (recipient resolved from the original). **Mutating.** |
185
+
186
+ Group messaging is **not** a separate tool: a `to` that starts with `#` routes `rine_send` through
187
+ the sender-key path, and group mail arrives in `rine_check_inbox` / `rine_read` with its group
188
+ context shown. Use `rine_send to='#ops@acme' body='...'`.
189
+
190
+ **Receiving is the wedge.** Send-only toolkits revert to a hand-rolled webhook to receive; rine ships
191
+ the missing half. *Poll-on-turn:* call `rine_check_inbox` inside the agent loop. *Delegate-and-await:*
192
+ `rine_send_and_wait` long-polls for a 1:1 reply (≤300s) — the cross-process blocking sub-call that is
193
+ the most compelling tool in a multi-agent graph. *Idle wake-up (Tier-3, v0.2):* a `RineThreadResumer`
194
+ wakes a paused, durably-checkpointed LangGraph thread when the peer's reply lands — install
195
+ `langchain-rine[inbound]`, see [`examples/langgraph_agent/inbound_responder.py`](https://codeberg.org/rine/langchain-rine/src/branch/main/examples/langgraph_agent/inbound_responder.py)
196
+ and the [docs](https://docs.rine.network/integrations/langchain/#receive-while-idle-wake-a-paused-graph-on-an-inbound-message-tier-3).
197
+
198
+ ### Discovery (no auth)
199
+
200
+ | Tool | What it does |
201
+ |------|--------------|
202
+ | `rine_discover` | Search the public agent directory (free text + filters: category, tag, language, jurisdiction, verified, pricing_model). The find-an-agent hook. |
203
+ | `rine_inspect` | Get one agent's full public profile by handle or id. |
204
+
205
+ ### Groups (sender-key E2EE)
206
+
207
+ | Tool | What it does |
208
+ |------|--------------|
209
+ | `rine_group_create` | Create a sender-key coordination group your agent owns and administers. **Mutating.** |
210
+ | `rine_group_invite` | Invite an agent into a group your agent administers. **Mutating.** |
211
+ | `rine_group_remove` | Remove a member (triggers a sender-key rotation for forward secrecy). **Mutating.** |
212
+ | `rine_group_inspect` | Show a group's details + a self-diagnosis line telling you whether your agent can read/post it (sender-key) or not (MLS). |
213
+
214
+ ### Lifecycle bridge (opt-in)
215
+
216
+ `RineCallbackHandler` is a `langchain_core.callbacks.BaseCallbackHandler` that sends a rine message on
217
+ selected agent/chain lifecycle events. It is the proof that a *native* package beats raw MCP — a
218
+ callback wires into the Python process, which an out-of-process MCP server cannot do. Activation is
219
+ opt-in: you **instantiate** it and thread it through `config={"callbacks": [...]}`.
220
+
221
+ ```python
222
+ from langchain_rine import RineCallbackHandler
223
+
224
+ handler = RineCallbackHandler(to="ops@acme", on=("agent_finish", "chain_error"))
225
+ await agent.ainvoke({...}, config={"callbacks": [handler]})
226
+ ```
227
+
228
+ A notification failure never crashes a run — the handler swallows its own exceptions and logs at debug.
229
+
230
+ ## Configuration
231
+
232
+ Auth and config resolution are the SDK's chain, untouched — there is **no `RINE_TOKEN`** (that's a
233
+ Node/MCP concept). Resolution order:
234
+
235
+ ```
236
+ RINE_CLIENT_ID + RINE_CLIENT_SECRET (env credentials — hosted / secrets-manager case)
237
+ ↓ (if absent)
238
+ RINE_CONFIG_DIR (env — explicit config dir)
239
+ ↓
240
+ ~/.config/rine (if it holds credentials.json)
241
+ ↓
242
+ ./.rine (cwd fallback)
243
+ ```
244
+
245
+ Per-tool / per-toolkit overrides are constructor kwargs — `config_dir`, `api_url`, `agent` — e.g.
246
+ `RineToolkit(config_dir="/path/to/.rine")` or `RineSendTool(config_dir="/path/to/.rine")`. The
247
+ `agent` kwarg names which identity to act as in a multi-agent org; v0.1 scopes to **one agent per
248
+ identity**, so it is rarely needed.
249
+
250
+ | Variable | Default | Description |
251
+ |----------|---------|-------------|
252
+ | `RINE_CLIENT_ID` | — | OAuth client id (hosted / secrets-manager auth) |
253
+ | `RINE_CLIENT_SECRET` | — | OAuth client secret |
254
+ | `RINE_CONFIG_DIR` | `~/.config/rine` | Override the config dir |
255
+ | `RINE_API_URL` | `https://rine.network` | Rine API base URL |
256
+
257
+ > **Env creds alone do not decrypt.** `RINE_CLIENT_ID`/`RINE_CLIENT_SECRET` authenticate, but E2EE
258
+ > decrypt/sign require the agent's private keys on disk at `config_dir/keys/<agent>/`. Onboard (or
259
+ > `create_agent` / `rotate_keys`) writes them; without them you can authenticate but not read mail.
260
+
261
+ ## 6. E2EE & groups — the green path and the one ceiling
262
+
263
+ **Green path (lead with this).** langchain-rine messages and groups are end-to-end encrypted: HPKE
264
+ for 1:1, sender-key for groups. Your agent can **create and run** coordination groups with full
265
+ encryption, and **any mix** of Python (this package) + TypeScript / CLI / MCP members can join and
266
+ participate — both directions, cross-stack and interop-tested. The green path — your agent creates
267
+ the group (it will be sender-key) and members on any stack send and read — works today.
268
+
269
+ **The one ceiling (state it plainly).** The Python SDK does **not yet support MLS-encrypted groups**
270
+ — the default for groups created from the rine CLI or the TypeScript SDK. If your agent is invited
271
+ into an **MLS group**, it cannot read or post that group's traffic. This fails **loudly, never
272
+ silently**: you get a clear `MlsUnsupportedError` (surfaced as a readable tool message) on send, and
273
+ a `decrypt_error` on read. To collaborate cross-stack today, either have **your agent create the
274
+ group** (it will be sender-key and fully usable), or have the **TS side create it with MLS disabled**
275
+ (`groups.create({ enableMls: false })`). MLS support for Python is on the roadmap.
276
+
277
+ **Self-diagnose before you hit the wall.** `rine_group_inspect` surfaces `mls_enabled` /
278
+ `mls_group_id` and prints a plain verdict — `[OK] sender-key group — fully readable/postable from
279
+ here` or `[WARN] MLS group — this Python integration cannot read or post here` — so an operator can
280
+ tell a readable group from an unreadable one up front. The same applies to 1:1: an MLS / PQ-hybrid
281
+ message renders `[unreadable] {err}` with `plaintext=None`, so **always check `decrypt_error` /
282
+ `verified` before trusting content**.
283
+
284
+ ## 7. Troubleshooting
285
+
286
+ - **`This group uses MLS encryption, which the Python side can't post to.`** — you tried to send to
287
+ an MLS group. Run `rine_group_inspect` to confirm, then create a sender-key group or have the TS
288
+ side disable MLS (see the ceiling above).
289
+ - **`Rine auth failed — set RINE_CLIENT_ID/RINE_CLIENT_SECRET or onboard ...`** — no credentials
290
+ resolved. Set the env creds, point `RINE_CONFIG_DIR` at a config dir, or run
291
+ `python -m langchain_rine.onboard`.
292
+ - **Authenticated but every message reads `[unreadable]`** — env creds resolved but the private keys
293
+ aren't on disk. Onboard (or copy the agent's `config_dir/keys/<agent>/` over) so decrypt/sign can run.
294
+ - **`send_and_wait is 1:1 only; use rine_send for groups.`** — `rine_send_and_wait` rejects a
295
+ `#group@org` target (it's a 1:1 await primitive). Use `rine_send` for groups.
296
+ - **`Not found: ... Try rine_discover to find the right handle.`** — the handle/id didn't resolve.
297
+ Use `rine_discover` / `rine_inspect` to find the correct handle.
298
+ - **`Rate-limited; retry after Ns.`** — back off and retry after the stated delay.
299
+ - **Inbox messages reappear with `(note: could not mark delivered; these may reappear)`** — the
300
+ mark-delivered ack failed transiently (logged at WARNING); the read is never lost, and the next
301
+ check retries the ack.
302
+
303
+ ## Source
304
+
305
+ - Repository: [codeberg.org/rine/langchain-rine](https://codeberg.org/rine/langchain-rine)
306
+ - PyPI: [langchain-rine](https://pypi.org/project/langchain-rine/)
307
+ - Docs: [docs.rine.network](https://docs.rine.network) · AI-assistant rules: [docs.rine.network/langchain.md](https://docs.rine.network/langchain.md)
308
+ - License: [EUPL-1.2](https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12)