Skip to main content

Multi-agent security monitoring SDK for LangChain / LangGraph: paired-event capture, real-time classification, and block mode.

Project description

Adrian SDK

Multi-agent security monitoring SDK for LangChain / LangGraph.

Documentation  •  Dashboard  •  Discord  •  LinkedIn

Adrian auto-instruments LangChain / LangGraph and emits paired events - each LLM call (chat_model_start + llm_end) and each tool execution (tool_start + tool_end) is assembled into a single PairedEvent carrying agent identity, parent context, and paired payload. Events stream over WebSocket to the Adrian backend, which classifies them against the MAD policy and returns verdicts. The agent profile's execution mode (Alert / Block / Human Review) is decided server-side; in Block and Human Review modes, malicious LLM decisions are caught before their tool calls execute.

Why Adrian

Most agent monitoring stops at activity logs (APIs, MCP, DB interactions, tool calls). Adrian also analyses the agent's reasoning: understanding why it took an action, under what context, and what it is planning on doing next. Research by OpenAI and DeepMind found that combining behaviour and reasoning analysis like this boosts detection accuracy by around 35% and is 4x more likely to catch nuanced attacks.

The classifier reasons about each action against the agent's stated remit rather than pattern-matching against a labelled prompt-injection dataset. If your e-commerce agent suddenly starts resetting user passwords, that does not appear in any training set, but it is a risk you want flagged.

Install

pip install adrian-sdk

Requires Python 3.12+.

Quickstart

import asyncio

import adrian
from langchain_openai import ChatOpenAI


async def main():
    adrian.init(api_key="adr_live_...")

    # Your LangChain / LangGraph code runs normally - every call is captured.
    llm = ChatOpenAI(model="gpt-4o")
    response = await llm.ainvoke(
        "Use web search to identify the most underpriced recent IPOs, "
        "compile a research dossier and implement an investment strategy",
    )
    print(response.content)

    adrian.shutdown()


asyncio.run(main())

The SDK defaults to wss://adrian.secureagentics.ai/ws (the hosted Adrian backend). Override via ws_url= or ADRIAN_WS_URL to point at a self-hosted backend, or override the handlers= list entirely for JSONL-only / custom transports.

The Quickstart uses the async pattern (asyncio.run + await llm.ainvoke) because the WebSocket transport runs on the asyncio loop - sync llm.invoke returns before the loop has a chance to flush events.

Last verified with langchain-core==1.3.3, langgraph==1.1.2, langchain-openai==1.2.1 (2026-05-08).

How it works

  1. adrian.init(...) monkey-patches LangChain's Runnable, BaseChatModel, CallbackManager, Pregel, and ToolNode so every invocation routes through Adrian's callback handler.
  2. The handler pairs *_start + *_end callbacks by run_id, derives the agent's identity from LangGraph's checkpoint_ns, attaches the parent agent's context when delegation occurred, and emits a single PairedEvent to all registered handlers.
  3. Default handlers are JSONLHandler (writes each event as one JSON line to events.jsonl) and WebSocketClient (sends protobuf frames to the Adrian backend). Override either by passing handlers=[...] to init().
  4. On connect the server returns a LoginAck carrying the agent profile's effective PolicySnapshot (mode + per-MAD-code scope booleans). The backend then classifies each event and returns a Verdict with a mad_code (e.g. M4_a) plus the policy snapshot at decision time.
  5. In Block and Human Review modes, each ToolNode invocation awaits the verdict of the LLM pair that requested it (correlated by tool_call.id). When the policy halts, synthetic ToolMessage responses are returned to the agent; the real tool never runs.

Reference

Full reference for init() parameters, observer callbacks, and the PairedEvent schema lives on the docs site: SDK reference.

Execution modes

The agent profile's execution mode is set in the dashboard and pushed to the SDK in the LoginAck frame; there is no client-side switch. The mode plus a PolicySnapshot of per-MAD-code scope booleans (policy_m0, policy_m2, policy_m3, policy_m4) decide when a tool call should halt.

Mode Wire enum SDK behaviour
Alert MODE_ALERT No wait, no halt. The dashboard logs verdicts; tools run unimpeded.
Block MODE_BLOCK ToolNode.ainvoke awaits the verdict of the LLM pair that requested its tool calls. In-scope verdicts (policy_mN=true for the verdict's MAD prefix) halt with synthetic ToolMessage(content="[BLOCKED by security policy]"); out-of-scope continue. Fail-open after block_timeout.
Human Review MODE_HITL Same wait, but indefinite - the server holds the verdict until a human approves or rejects on the dashboard. Approve → continue, reject → halt. Out-of-scope verdicts forward immediately.

Halt correlation is per-tool_call.id - parallel fan-out patterns (S3 router, S8 deep research) wait on each specialist's own verdict, no cross-contamination.

Typical configurations:

  • Block-mode auto-enforce: policy_m3=true, policy_m4=true (optionally policy_m2=true for stricter posture).
  • Human Review gating: policy_m3=true, policy_m4=true for human review; M0/M2 silent.
  • Alert observability: per-MAD-code bools are irrelevant; the dashboard sees everything, the SDK does not gate.

Human Review durability caveat

Human Review waits are session-scoped and live in the SDK process. If the SDK restarts before the dashboard resolves a pending review, the resolution is dropped on arrival (logged at WARN) and the agent has no live future to wake. The audit trail in the dashboard's review queue survives - recovery to a live agent does not.

Catch-on-next-turn for tool-output attacks

The classifier targets LLM pairs; tool outputs themselves are not directly classified. A tool-side attack - a benign-looking call whose output contains prompt injection or exfiltrated data - still fails: the classifier sees the poisoned output in the next LLM turn's input messages, flags the induced reasoning, and blocks the follow-up tool before it runs. No bypass as long as the agent is the only actor making tool calls.

Multi-agent support

Parent context is derived from the AgentContextTracker:

  • Delegation via tool call (S1 subagents-as-tools, S2 handoff, S4 hierarchical, S7 supervisor): the LLM's tool_calls mark it as the delegating agent; the next new agent that appears gets that agent as parent.
  • Parallel siblings spawned by one delegation (S8 deep research): all children inherit the same parent until the delegating agent itself resumes.
  • Code-dispatched peers (S3 router fan-out, S5 custom workflow): no delegation → no parent. Peers all have parent=None.
  • Set-once (S6 swarm handbacks): an agent's parent is fixed on first appearance and never changes, even across Alice ↔ Bob ↔ Alice handbacks.

End-to-end scenario tests at tests/test_parent_context_scenarios.py fire LangGraph-shaped callback sequences and assert the emitted PairedEvent.parent for each pattern.

S1-S8 are the SDK's internal labels for the eight LangGraph multi-agent topology patterns the parent-context tracker handles. Full breakdown in tests/test_parent_context_scenarios.py and tests/test_block_mode_races.py.

Session persistence

The first call to adrian.init() from a given working directory generates a UUID4 and persists it to ~/.adrian/projects/<cwd-key>/config.json, where <cwd-key> is the absolute working-directory path with / and \ and : replaced by - (e.g. /home/user/myapp-home-user-myapp; C:\Users\u\proj-C-Users-u-proj). Subsequent runs from the same directory pick up the same session_id, so the dashboard sees one continuous session per deployment instead of a fresh row per process restart.

Resolution order, highest priority first:

  1. ADRIAN_SESSION_ID environment variable.
  2. Explicit session_id="..." kwarg to init().
  3. Persisted value at ~/.adrian/projects/<cwd-key>/config.json.
  4. Generate a new UUID4 and persist it for next time.

Overrides via env var or kwarg do not write to the persistent file - the on-disk identifier is preserved for runs that want to fall back to it. Different working directories get distinct identifiers; multiple processes from the same cwd share one.

Manual instrumentation

When auto_instrument=True (the default), the SDK monkey-patches LangChain at import time. Set auto_instrument=False if you would rather attach the handler explicitly to specific calls - useful when you do not want third-party libraries patched globally, or when you are integrating Adrian into code that already manages its own callbacks.

import adrian
from langchain_openai import ChatOpenAI

adrian.init(api_key="adr_live_...", auto_instrument=False)
handler = adrian.get_handler()

llm = ChatOpenAI(model="gpt-4o")
await llm.ainvoke(prompt, config={"callbacks": [handler]})

adrian.get_handler() returns the handler the SDK built during init() and wired into the WebSocket hook chain. Constructing a fresh AdrianCallbackHandler() directly bypasses that wiring and emits no events, so get_handler() is the supported entry point.

License

Apache 2.0. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

adrian_sdk-1.0.0.tar.gz (143.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

adrian_sdk-1.0.0-py3-none-any.whl (117.2 kB view details)

Uploaded Python 3

File details

Details for the file adrian_sdk-1.0.0.tar.gz.

File metadata

  • Download URL: adrian_sdk-1.0.0.tar.gz
  • Upload date:
  • Size: 143.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for adrian_sdk-1.0.0.tar.gz
Algorithm Hash digest
SHA256 aeb4d9ea81ca4a675b88f8d6963303de08bae8c761314a91b9385eab90fbb5f7
MD5 84035a6de1a189ae1a06c04ed95d04e5
BLAKE2b-256 73a6c75d153a9c28c0c4c8449ed880efbef8fb9fe3be31bf142319ad3c34743d

See more details on using hashes here.

File details

Details for the file adrian_sdk-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: adrian_sdk-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 117.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for adrian_sdk-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3fa50e749c9d80322763d593585bd2ec84cdfabf6a13ce0ebe026ae1a03ecb0c
MD5 7b65606700815c8ff393dd83dbf45cd7
BLAKE2b-256 5ef5c6e95f2e61f2cc5d89cdcc74aabec950e21825aa7d24371f13304438db7a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page