@tryagent/adapters
v0.0.1
Published
Framework adapters that wire TryAgent human-in-the-loop guardrails into LangGraph, the Vercel AI SDK, and the OpenAI Agents SDK.
Maintainers
Readme
@tryagent/adapters
Framework adapters that wire TryAgent human-in-the-loop guardrails into LangGraph, the Vercel AI SDK, and the OpenAI Agents SDK. Each adapter is thin sugar over the SDK's guard() / escalation primitives — same policies, same rubrics, same audit trail, expressed in each framework's native shape.
npm install @tryagent/adapters @tryagent/sdkZero config: unknown policy keys and agent ids auto-register on the first escalation, then get tuned from the TryAgent dashboard (rubrics, HITL mode ladder, conditions).
Vercel AI SDK
guardTools wraps the tools you name in a ToolSet; everything else passes through untouched.
import { TryAgent } from "@tryagent/sdk";
import { guardTools } from "@tryagent/adapters/vercel-ai";
import { generateText, tool } from "ai";
const tryagent = new TryAgent({ apiKey: process.env.TRYAGENT_API_KEY });
const result = await generateText({
model,
tools: guardTools(tryagent, { sendEmail, searchDocs }, {
sendEmail: "email.send", // policy key, or:
// sendEmail: { policy: "email.send", severity: "sev2" },
}),
prompt,
});A guarded call escalates, waits for the human (or rubric) decision, and only then runs execute. Denials resolve to a GuardDenial object the model sees as the tool result — no thrown errors mid-generation. Evidence is the tool's input object, so rubric conditions like args.amount > 500 work out of the box; the AI SDK's ToolCallOptions (conversation, abort signal) never leave the process.
OpenAI Agents SDK
Guard the tool definition before tool() wraps it:
import { tool } from "@openai/agents";
import { guardAgentTool } from "@tryagent/adapters/openai-agents";
const issueRefund = tool(
guardAgentTool(tryagent, "payments.refund", {
name: "issue_refund",
description: "Issue a Stripe refund",
parameters,
execute: async ({ chargeId, amount }) => stripe.refunds.create({ charge: chargeId, amount }),
}),
);Same semantics: escalate → wait → execute on approval, GuardDenial result on denial, RunContext forwarded but never escalated.
LangGraph
LangGraph agents pause with interrupt() instead of blocking inside a tool. The adapter keeps escalation creation outside the graph, so checkpoint replays never double-escalate.
Inside a node:
import { tryagentInterrupt } from "@tryagent/adapters/langgraph";
const decision = tryagentInterrupt({
policy: "treasury.wire",
agentId: "treasury-bot",
runId: state.runId,
subject: { type: "wire", id: wire.id, label: `Wire ${wire.id}` },
question: `Send $${wire.amount} to ${wire.recipient}?`,
evidence: [JSON.stringify({ args: { amount: wire.amount } })],
choices: [
{ id: "deny", label: "Deny" },
{ id: "approve", label: "Approve" },
],
});
// decision: { escalationId, status, choice, reason?, response? }Dev loop — escalate, block until the human decides, resume, repeat until the run finishes:
import { invokeWithTryAgent } from "@tryagent/adapters/langgraph";
const result = await invokeWithTryAgent(graph, input, config, tryagent);Production — create the escalation, persist the thread id, and resume from your decision webhook:
import {
escalateInterrupt,
readTryAgentInterrupts,
toResume,
} from "@tryagent/adapters/langgraph";
import { Command } from "@langchain/langgraph";
const state = await graph.invoke(input, config);
for (const pending of readTryAgentInterrupts(state)) {
const escalation = await escalateInterrupt(tryagent, pending);
// persist { escalation.id -> thread_id }, return
}
// later, in the webhook handler:
await graph.invoke(new Command({ resume: toResume(decidedEscalation) }), config);Interrupts raised by anything other than tryagentInterrupt are left pending for your own handling. @langchain/langgraph is an optional peer dependency — only the /langgraph subpath needs it.
Fail-closed by default
Guarded tools list Deny first, so an SLA breach falls back to denial. The LangGraph resume carries status: "breached" and the escalation's defaultChoice so your node can fail closed the same way.
