Skip to content

feat(sdk): add Anthropic SDK integration#61

Open
Uchebuzz wants to merge 5 commits into
secureagentics:mainfrom
Uchebuzz:feat/anthropic-sdk-integration
Open

feat(sdk): add Anthropic SDK integration#61
Uchebuzz wants to merge 5 commits into
secureagentics:mainfrom
Uchebuzz:feat/anthropic-sdk-integration

Conversation

@Uchebuzz

Copy link
Copy Markdown

Summary

Adds first-class Anthropic SDK support to the Adrian Python SDK. Every
messages.create call on both anthropic.Anthropic and
anthropic.AsyncAnthropic is now captured automatically as a
PairedEvent and emitted through the hook registry — no changes needed
to user agent code beyond adrian.init().

Changes

  • sdk/adrian/anthropic_handler.py (new): message format conversion,
    tool call and usage extraction, PairedEvent assembly, idempotent
    monkey-patch of Messages.create / AsyncMessages.create, and
    anthropic_invocation() / anthropic_invocation_sync() context managers
    for grouping multi-turn calls under a single invocation ID.
  • sdk/tests/test_anthropic_handler.py (new): 61 tests covering all
    helper functions, emission, getter lifecycle, the no-package no-op path,
    and both context managers. All pass.
  • sdk/adrian/__init__.py: patch_anthropic(), anthropic_invocation,
    and anthropic_invocation_sync exported; _auto_instrument_anthropic()
    wired into init() alongside LangChain auto-instrumentation.
  • sdk/pyproject.toml: anthropic optional dependency group added;
    anthropic>=0.40.0 added to [dev] for the test suite.
  • examples/anthropic_quickstart.py (new): minimal demo showing single-call
    and multi-turn anthropic_invocation() usage.

Test plan

  • uv pip install -e ./sdk[dev,anthropic] then
    pytest sdk/tests/test_anthropic_handler.py -v — 61 tests pass
  • pytest sdk/tests/ — no regressions in existing suite
  • Run examples/anthropic_quickstart.py against a live backend and
    confirm events appear in the dashboard with correct model, messages,
    and usage fields
  • Confirm anthropic_invocation() groups multi-turn calls under a
    shared invocation_id in the dashboard

Checklist

  • CLA signed (see CLA.md)
  • Tests pass locally
  • Docs updated where needed
  • British English; no em-dashes; no marketing fluff

@max-c-sa

Copy link
Copy Markdown
Contributor

Thanks for the PR Uche! I'll leave to the engineers to work with you but huge thanks for taking on Anthropic integration and i'd like to invite you to the private contributors discord community channel. https://discord.gg/zcwYwfdAFz I am 'maxicorbs' DM on there and i'll add you

@yanny-sec

Copy link
Copy Markdown
Contributor

Thanks for the PR! Currently it's audit-only. Could you add logic to actually hold the tool calls for HITL and Block modes? You can use the TS OpenAI or Python LangChain integrations for reference.

@Uchebuzz

Copy link
Copy Markdown
Author

Thanks for the PR! Currently it's audit-only. Could you add logic to actually hold the tool calls for HITL and Block modes? You can use the TS OpenAI or Python LangChain integrations for reference.

Okay, I will look into this and fix thank you

@yanny-sec

Copy link
Copy Markdown
Contributor

I've merged #78 which modularises the existing LangChain integration from __init__.py to its own langchain_handler.py file. You should rebase from main later.

Uchebuzz added 3 commits July 21, 2026 15:54
Patches anthropic.Anthropic and anthropic.AsyncAnthropic so every
messages.create call is captured as an Adrian PairedEvent and emitted
through the hook registry, with the same lifecycle guarantees as the
existing LangChain/LangGraph integration.

- sdk/python/adrian/anthropic_handler.py: core patch module
  - _flatten_content / _flatten_anthropic_messages: normalise Anthropic
    message params (strings, TypedDict blocks, SDK objects) to ChatMessage
  - _extract_anthropic_tool_calls: pull ToolUseBlock records from response
  - _extract_anthropic_usage: map input/output tokens to TokenUsage
  - build_anthropic_llm_pair: assemble PairedEvent from request + response
  - _emit_pair / _schedule_emit: async and sync emission paths
  - patch_anthropic: idempotent monkey-patch; reads hooks/config at call
    time so shutdown + re-init cycles are handled correctly
  - anthropic_invocation / anthropic_invocation_sync: context managers
    to group multi-turn calls under a single invocation_id

- sdk/python/adrian/__init__.py: wire auto-instrumentation
  - patch_anthropic() public function (manual opt-in path)
  - _auto_instrument_anthropic() called by init() when auto_instrument=True

- sdk/python/pyproject.toml: add anthropic optional-dep group and to dev

- sdk/python/tests/test_anthropic_handler.py: 61 unit tests covering all
  helper functions, PairedEvent assembly, emission, patching, and context
  managers; all passing locally

- examples/python/anthropic_quickstart.py: minimal two-turn async example

All new source files carry the LICENSE_HEADER.txt SPDX header per
CONTRIBUTING.md.
@Uchebuzz

Copy link
Copy Markdown
Author

I've merged #78 which modularises the existing LangChain integration from __init__.py to its own langchain_handler.py file. You should rebase from main later.

thanks for the heads up, rebased onto main 👍

still investigating this....

Quick question before I do the gate: the Anthropic SDK has no tool-execution step to intercept like LangChain's BaseTool.invoke, the only hook point is the return of messages.create(), where the model hands back tool_use blocks. So when a verdict says block, how do you want it to surface?

  • Rewrite the response swap the blocked tool_use block for a [BLOCKED] text block, so the caller just sees no tool call (matches the LangChain behavior),
    or
  • Raise an error — hard-stop the whole call.

@yanny-sec

Copy link
Copy Markdown
Contributor

I've merged #78 which modularises the existing LangChain integration from __init__.py to its own langchain_handler.py file. You should rebase from main later.

thanks for the heads up, rebased onto main 👍

still investigating this....

Quick question before I do the gate: the Anthropic SDK has no tool-execution step to intercept like LangChain's BaseTool.invoke, the only hook point is the return of messages.create(), where the model hands back tool_use blocks. So when a verdict says block, how do you want it to surface?

  • Rewrite the response swap the blocked tool_use block for a [BLOCKED] text block, so the caller just sees no tool call (matches the LangChain behavior),
    or
  • Raise an error — hard-stop the whole call.

To keep it consistent with the other integrations let's go for the [BLOCKED] text block, that way the model will know something stopped its action too.

The Anthropic handler was audit-only: it emitted a PairedEvent after
messages.create() but never acted on the verdict. This adds the verdict
gate requested in review.

The Anthropic Messages API has no discrete tool-execution step to intercept
(unlike LangChain's BaseTool.invoke or the TS OpenAI captureTool), so the
gate runs at the only choke point the SDK owns: the return of
messages.create(). After emitting (which registers the verdict future), the
async path awaits each tool_use block's verdict and, when it halts, rewrites
that block into a "[BLOCKED by security policy]" text block -- consistent
with the LangChain gate, so the model sees its action was stopped.

Fail-closed rules mirror langchain_handler: missing LoginAck within 5s
blocks; MODE_BLOCK verdict timeout blocks; MODE_HITL waits indefinitely.

Scope: async (AsyncMessages) only; sync Messages.create stays audit-only
pending maintainer confirmation on parity. _should_halt is a self-contained
copy so the Anthropic integration does not depend on the LangChain module.

Adds 18 tests: ALERT passthrough, BLOCK halt/allow, verdict-timeout
fail-closed, partial block, dict/object block shapes, HITL
reject/approve/holds-until-human, and an end-to-end emit->gate seam test.
@Uchebuzz
Uchebuzz force-pushed the feat/anthropic-sdk-integration branch from 0a52bb1 to 5212849 Compare July 22, 2026 09:32
@Uchebuzz

Uchebuzz commented Jul 22, 2026

Copy link
Copy Markdown
Author

Thanks for the review — pushed the fix.

The handler now actually holds tool calls instead of just auditing them. On the async path, each tool_use block waits for its classifier verdict, and under BLOCK/HITL a halted call is rewritten into a [BLOCKED by security policy] text block so the model can see its action was stopped (kept consistent with the LangChain gate). Fail-closed behaviour matches langchain_handler, and I've rebased onto main including #78.

A couple of things I'd like your call on:

  • Scoped to async AsyncMessages for now; sync Messages.create is still audit-only. Happy to add sync parity if you want it in this PR.
  • _should_halt is duplicated locally so the Anthropic module doesn't import the LangChain one — glad to share it via a common home instead if you'd prefer.

Added 18 tests, including an end-to-end emit→gate check.

@yanny-sec

Copy link
Copy Markdown
Contributor

Thanks for the review — pushed the fix.

The handler now actually holds tool calls instead of just auditing them. On the async path, each tool_use block waits for its classifier verdict, and under BLOCK/HITL a halted call is rewritten into a [BLOCKED by security policy] text block so the model can see its action was stopped (kept consistent with the LangChain gate). Fail-closed behaviour matches langchain_handler, and I've rebased onto main including #78.

A couple of things I'd like your call on:

  • Scoped to async AsyncMessages for now; sync Messages.create is still audit-only. Happy to add sync parity if you want it in this PR.
  • _should_halt is duplicated locally so the Anthropic module doesn't import the LangChain one — glad to share it via a common home instead if you'd prefer.

Added 18 tests, including an end-to-end emit→gate check.

That's a good shout, _should_halt can and should be modularised. One more thing that's missing, LangChain integration currently exposes on_verdict, on_block, on_audit as notification callbacks for the developer. To maintain feature parity with the LangChain integration, sync Messages.create should also be gated.

@Uchebuzz

Copy link
Copy Markdown
Author

Okay — will do all three: modularise should_halt, fire the on_verdict/on_block/on_audit callbacks for Anthropic events, and gate sync Messages.create too. Pushing shortly.

Addresses review feedback on the Anthropic integration to reach full
parity with the LangChain handler.

1. Modularise should_halt: promoted from langchain_handler into ws.py as a
   public helper next to policy_active / block_timeout (mirrors the TS core
   shouldHalt). Both handlers now import it; the Anthropic integration no
   longer needs a local copy or a dependency on the LangChain module.

2. Fire on_verdict / on_block / on_audit for Anthropic. These callbacks only
   fire for events registered in the callback handler's event map, and the
   Anthropic handler previously emitted straight to the hook registry,
   bypassing it -- so the callbacks never reached the developer. Emission now
   delegates to the callback handler when available (registering the event and
   firing on_event), with the direct-hooks path kept as a fallback.

3. Gate the sync Messages.create path. Verdict futures live on the WS event
   loop, so sync emission + gating are bridged onto it via
   run_coroutine_threadsafe and the caller blocks for the verdict, matching
   the LangChain _sync_gate. Degrades to audit-only when gating isn't possible
   (no WS client, ALERT/pre-login, or a call from an event-loop thread).

Tests: should_halt cases moved to test_ws.py; added notification-callback
parity tests (block/audit tiers) and sync-gate tests (bridge fail-closed
rewrite, event-loop-thread passthrough, _should_gate_sync matrix).
@Uchebuzz

Copy link
Copy Markdown
Author

Pushed all three:

  1. should_halt modularised — moved into ws.py as a public helper (next to policy_active/block_timeout, mirroring the TS core shouldHalt); both handlers import it now, no LangChain dependency.
  2. on_verdict/on_block/on_audit fire for Anthropic — the handler was emitting straight to the hook registry and bypassing the callback handler's event map (which those callbacks key on). Emission now delegates to the callback handler, so the notification callbacks reach the developer with parity.
  3. Sync Messages.create gated — emit + gate are bridged onto the WS event loop via run_coroutine_threadsafe (matching _sync_gate); the caller blocks for the verdict, and it degrades to audit-only when gating isn't possible.

Tests: should_halt cases relocated to test_ws.py; added callback-parity tests (block/audit tiers) and sync-gate tests. Full suite green; ruff + basedpyright clean.

@yanny-sec

Copy link
Copy Markdown
Contributor

Thanks, all three items are addressed: should_halt is shared via ws.py, the on_verdict / on_block / on_audit callbacks now fire for Anthropic events, and sync Messages.create gates. I confirmed the callback parity and sync gating locally.

One change before merge: the sync bridge fails open where it should fail closed.

In sdk/python/adrian/anthropic_handler.py, _emit_and_gate_sync L656-662, a bridge/gate exception is logged and then control falls through to return response, so under BLOCK a halted tool call is let through:

            except Exception:
                logger.exception("Anthropic sync gate bridge failed; emitting audit-only")

    _schedule_emit(response, kwargs)
    return response

langchain_handler._sync_gate fails closed at the same point (L639-640: except Exception: return True) - please match, rewriting to [BLOCKED] on error under BLOCK. (Same function: the loop-less path at L661-662 also degrades to audit-only, where LangChain uses asyncio.run(_async_gate(...)) at L644.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants