VirtueAI Agent Suite SDK — Gateway for AI agents
Project description
AgentSuite SDK
Python SDK for connecting AI agent frameworks to the AgentSuite Gateway. Provides drop-in adapters for OpenAI Agents SDK, Google ADK, Claude Agent SDK, and LangChain with automatic session tracking and user query injection.
Installation
pip install agentsuite-sdk
With framework-specific extras:
pip install agentsuite-sdk[openai] # OpenAI Agents SDK
pip install agentsuite-sdk[adk] # Google ADK
pip install agentsuite-sdk[claude] # Claude Agent SDK
pip install agentsuite-sdk[langchain] # LangChain (create_agent API)
pip install agentsuite-sdk[all] # All frameworks
OpenAI Agents SDK
from agents import Agent, Runner, RunConfig
from agentsuite import GatewayClient
client = GatewayClient(url="...", api_key="sk-vai-...")
adapter = client.openai()
mcp_server = adapter.mcp_server()
await mcp_server.connect()
try:
agent = Agent(
name="my-agent",
model="gpt-4o",
mcp_servers=[mcp_server],
)
run_config = RunConfig(
model="gpt-4o",
call_model_input_filter=adapter.input_filter,
)
result = await Runner.run(agent, input="What are my open tickets?", run_config=run_config)
finally:
await mcp_server.cleanup()
Google ADK
from google.adk.agents import Agent
from agentsuite import GatewayClient
client = GatewayClient(url="...", api_key="sk-vai-...")
adapter = client.adk()
agent = Agent(
name="my_agent",
model="gemini-2.0-flash",
tools=[adapter.toolset()],
before_model_callback=adapter.before_model_callback,
before_tool_callback=adapter.before_tool_callback,
after_tool_callback=adapter.after_tool_callback,
)
LangChain
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
from agentsuite import GatewayClient
client = GatewayClient(url="...", api_key="sk-vai-...")
adapter = client.langchain()
async with adapter:
tools = await adapter.get_tools()
graph = create_agent(
model="openai:gpt-4o",
tools=tools,
system_prompt="You are a helpful assistant.",
)
result = await graph.ainvoke(
{"messages": [HumanMessage(content=user_message)]},
config={"callbacks": [adapter.create_callback()]},
)
Claude Agent SDK
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
from agentsuite import GatewayClient
client = GatewayClient(url="...", api_key="sk-vai-...")
adapter = client.claude()
options = ClaudeAgentOptions(
**adapter.options(),
model="claude-sonnet-4-5",
)
async with ClaudeSDKClient(options=options) as sdk:
user_message = "What are my open tickets?"
# Must call record_query() before sdk.query() — Claude has no before_model_callback
adapter.record_query(user_message)
await sdk.query(user_message)
async for event in sdk.receive_response():
...
Direct Connection (no adapter)
Connect directly to the MCP Gateway without the adapter. The LLM fills in session_id and user_queries from the tool schema — simpler setup but less reliable query capture.
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, HookMatcher
options = ClaudeAgentOptions(
mcp_servers={
"gateway": {
"type": "http",
"url": "https://...",
"headers": {"X-API-Key": "sk-vai-..."},
}
},
hooks={
"PreToolUse": [HookMatcher(matcher="^mcp__", hooks=[pre_tool_use_hook])],
"PostToolUse": [HookMatcher(matcher="^mcp__", hooks=[post_tool_use_hook])],
},
permission_mode="bypassPermissions",
setting_sources=[],
model="claude-sonnet-4-5",
)
How It Works
Each adapter integrates the gateway by hooking into its framework's native extension points. All three share the same two-phase session pattern:
- Before tool call — inject
session_id(or"sess_new"on first call) +user_queriesinto args - After tool call — parse
"Session ID: sess_xxxx"from the response, store it, and flush the query buffer
The difference is just where each framework lets you hook into that flow.
Session State & Deduplication
GatewaySession tracks which user messages have been sent to the gateway using a position watermark:
_consumed_count— number of user messages already sent_pending— messages buffered since the last tool call
On every LLM call, the adapter extracts the full list of user messages from conversation history and calls process_history(). Only messages at index ≥ _consumed_count + len(_pending) are added to _pending. After a successful tool call, flush() advances _consumed_count and clears _pending.
This approach:
- Is immune to Python object reconstruction (which broke
id()-based dedup in ADK) - Allows repeated identical messages (which text-based dedup would silently drop)
- Works reliably as long as conversation history is append-only (guaranteed by all frameworks)
OpenAI Agents SDK
The SDK exposes Agent(mcp_servers=[...]) and RunConfig(call_model_input_filter=...) as the main extension points. Unlike ADK, it has no before_model_callback or pre-tool-call callback — so there is no hook to intercept tool calls before they execute. To work around this, the adapter wraps the SDK's native MCPServerStreamableHttp in a proxy class (GatewayMCPServer) that intercepts call_tool directly.
adapter.mcp_server() returns the proxy — it injects session context before every tool call and parses the gateway response to capture the assigned session_id.
adapter.input_filter is registered as call_model_input_filter. It fires before each LLM call and passes the full user-message history to process_history(), which applies the position watermark to capture only genuinely new messages.
Google ADK
ADK has first-class before_model_callback, before_tool_callback, and after_tool_callback hooks on Agent — exactly what's needed, with no wrapping required.
adapter.toolset() returns a native MCPToolset. The three callbacks slot directly into Agent(...):
before_model_callbackextracts user messages fromllm_request.contentsand passes them toprocess_history()before_tool_callbackinjectssession_idanduser_queriesinto MCP tool argsafter_tool_callbackparses the session ID from the response and callsflush()
Tool callbacks are scoped to McpTool instances only — regular function tools are ignored.
LangChain
The adapter uses the new create_agent API and maps onto the gateway's needs via a callback and a tool wrapper:
adapter.create_callback()— returns a callback whoseon_chain_startfires once perainvoke()at run start. Used to capture user messages frominputs["messages"]and buffer them intouser_queries(same watermark logic as other adapters). Pass it inconfig={"callbacks": [adapter.create_callback()]}so you don't need to callrecord_user_message().- Tool wrapper (applied to every tool from
get_tools()) — runs before and after each gateway tool call. Before: injectssession_idanduser_queriesinto the tool arguments. After: extracts the gateway-assignedsession_idfrom the response and flushes the buffer.
adapter.record_user_message(message) is the alternative when you are not using the callback (e.g. custom input shape or you prefer explicit control).
Claude Agent SDK
The Claude SDK wraps the Claude CLI subprocess and exposes PreToolUse/PostToolUse hooks. It has no before_model_callback, so user messages cannot be captured automatically.
adapter.options() returns a dict for ClaudeAgentOptions(...) that wires up the MCP server, hooks, permission_mode="bypassPermissions", and setting_sources=[] (to isolate from ~/.claude settings).
adapter.record_query(message) must be called manually before each sdk.query() call. This is an intentional trade-off — the alternative is the Direct Connection path (no adapter), where the LLM fills in session_id and user_queries itself from the tool schema. That requires no SDK at all, but is less reliable since it depends on the LLM correctly tracking session state.
Action Guard SDK
The Guard SDK provides a standalone client for evaluating tool calls against security policies before execution. It follows OpenAI SDK patterns with shared HTTP clients, context managers, and typed exceptions.
Basic Usage
from agentsuite import ActionGuardClient, GuardError
# Sync client with context manager (recommended)
with ActionGuardClient(api_key="sk-vai-...", policy_id="agp_...") as guard:
result = guard.actions.guard_query(
query="Tool: delete_file, Args: {'path': '/etc/passwd'}",
)
if not result.allowed:
print(f"Blocked: {result.explanation}")
print(f"Violations: {result.violations}")
Async Client
from agentsuite import AsyncActionGuardClient
# Async client with context manager
async with AsyncActionGuardClient() as guard:
result = await guard.actions.guard_query(query="...")
if not result.allowed:
raise PermissionError(result.explanation)
Error Handling
In a guardrail hook, the only real decision is fail closed (deny on error, safer) or fail open (allow on error). Catching GuardError is all you need:
from agentsuite import AsyncActionGuardClient, GuardError
async def action_guard_hook(...):
try:
result = await guard.actions.guard_query(query=f"Tool: {tool_name}, Args: {tool_args}")
if not result.allowed:
return reject(result.explanation)
return allow()
except GuardError:
# Guard is unreachable — fail closed (deny) to stay safe
return reject("Action Guard unavailable")
Startup validation — ActionGuardClient() raises GuardConfigError if required env vars are missing. Catch this at boot before your agent starts:
from agentsuite.guard import GuardConfigError
try:
guard = ActionGuardClient() # raises GuardConfigError if VIRTUE_API_KEY or ACTION_GUARD_POLICY_ID not set
except GuardConfigError as e:
raise SystemExit(f"Guard misconfigured: {e}")
Observability — use status_code to log different failure modes in production. Only GuardAPIStatusError carries a status code; catch it before the base GuardError:
from agentsuite.guard import GuardAPIStatusError, GuardError
except GuardAPIStatusError as e:
if e.status_code == 401:
logger.critical("Guard auth failed — check VIRTUE_API_KEY")
elif e.status_code == 503:
logger.warning("Guard service unavailable")
else:
logger.error("Guard API error: %s (status=%s)", e.message, e.status_code)
return reject("Guard unavailable")
except GuardError as e:
logger.error("Guard connection error: %s", e.message)
return reject("Guard unavailable")
Stateful vs Stateless
Stateless (guard_query): Evaluate a single query with no context.
result = guard.actions.guard_query(
query="Tool: delete_user, Args: {'username': 'admin'}",
fast_mode=True, # Lower latency
)
Stateful (guard): Pass full session history for context-aware decisions.
result = guard.actions.guard(
session_id="user-123",
session_history={
"session_info": {"session_id": "user-123"},
"trajectory": [...],
},
)
Resource Management
The SDK automatically manages HTTP connections. Use context managers for automatic cleanup:
# Automatic cleanup (recommended)
with ActionGuardClient() as guard:
result = guard.actions.guard_query(query="...")
# Manual cleanup
guard = ActionGuardClient()
try:
result = guard.actions.guard_query(query="...")
finally:
guard.close() # Or await guard.aclose() for async
Custom HTTP Client
For advanced use cases, provide your own httpx.Client:
import httpx
# Sync
with httpx.Client(timeout=60.0, limits=httpx.Limits(max_connections=100)) as http:
guard = ActionGuardClient(http_client=http)
result = guard.actions.guard_query(query="...")
# Async
async with httpx.AsyncClient(timeout=60.0) as http:
guard = AsyncActionGuardClient(http_client=http)
result = await guard.actions.guard_query(query="...")
Configuration
| Variable | Description |
|---|---|
VIRTUE_GATEWAY_URL |
Gateway MCP endpoint URL |
VIRTUE_API_KEY |
VirtueAI API key (sk-vai-...) — shared across Gateway and Guard |
ACTION_GUARD_POLICY_ID |
Action Guard policy set ID (agp_...) |
DEBUG |
Set to 1 to enable debug logging |
Enable debug logging programmatically:
import logging
logging.getLogger("agentsuite").setLevel(logging.DEBUG)
License
MIT
Project details
Release history Release notifications | RSS feed
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 agentsuite_sdk-0.1.0.tar.gz.
File metadata
- Download URL: agentsuite_sdk-0.1.0.tar.gz
- Upload date:
- Size: 35.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0276881dc72c7cd918589ca0a9befbbceb4671b58bbe7a2ed02b55f4737bba3a
|
|
| MD5 |
3455dc432e4f4d3e5cfc97ccd45e19e5
|
|
| BLAKE2b-256 |
c101ab84db4c6956eb0112f828d47ab299589f4f47765972c7d39f4609430acf
|
File details
Details for the file agentsuite_sdk-0.1.0-py3-none-any.whl.
File metadata
- Download URL: agentsuite_sdk-0.1.0-py3-none-any.whl
- Upload date:
- Size: 37.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
15fb3884bf4431d7e5656493f0f414213170787175b0c62e38c895fd30bec6f4
|
|
| MD5 |
aea4c5ef8688bdc99d132faecf5dab8d
|
|
| BLAKE2b-256 |
c1f97b3e1e9159355de257e2c7dde261c7722f5313b0cc4ceedc063c0251203b
|