paybondpaybond
Sign in

Google ADK adapter

Google ADK agent spend controls — wrap FunctionTool execute with Harbor verify and auto-evidence via @paybond/google-adk and paybond_kit.google_adk. TypeScript and Python parity.

Paybond integrates with Google ADK at the FunctionTool execution boundary — Harbor verify runs before side-effecting tools execute, then auto-evidence fires after success. Model inference stays on Gemini and your ADK runner; Paybond guards paid and side-effecting tool calls only.

Install

Install

npm install @paybond/google-adk @google/adk

Import from @paybond/google-adk

import { createPaybondGoogleAdkConfig } from "@paybond/google-adk";
  • Equivalent subpath on the core package: `@paybond/kit/google-adk` — use `@paybond/kit` when you need multiple adapters in one app.
  • Python: `paybond agent demo google-adk smoke` requires the optional `google-adk` extra. Use `pip install "paybond-kit[google-adk]"`, `pipx install 'paybond-kit[google-adk]'`, or `pipx inject paybond-kit google-adk` (when base paybond-kit is already installed).
  • Smoke: `paybond agent demo google-adk smoke --operation paid-tool --requested-spend-cents 100 --evidence-preset cost_and_completion --format json`.

One-liner (sandbox): paybond.instrument({ policy, framework: "google-adk", tools }) or paybond.agent({ policy, framework: "google-adk", tools }) returns guarded ADK tools. Production: omit sandbox and call instrumented.bind() per session.

TypeScript

import { FunctionTool, LlmAgent } from "@google/adk";
import { Paybond } from "@paybond/kit";

const paybond = await Paybond.open({ apiKey: process.env.PAYBOND_API_KEY! });

const bookHotelTool = new FunctionTool({
  name: "travel.book_hotel",
  description: "Book a hotel room",
  parameters: {
    type: "object",
    properties: {
      city: { type: "string" },
      estimatedPriceCents: { type: "integer", minimum: 0 },
    },
    required: ["city", "estimatedPriceCents"],
  },
  execute: async (args) => bookHotel(args),
});

const { agentTools: tools } = await paybond.instrument({
  policy: "./paybond.policy.yaml",
  framework: "google-adk",
  tools: [bookHotelTool, searchWebTool],
});

const agent = new LlmAgent({ name: "Travel", tools });

Python

from google.adk.tools import FunctionTool
from google.adk.agents import LlmAgent
from paybond_kit import Paybond

paybond = await Paybond.open(api_key=os.environ["PAYBOND_API_KEY"])

def book_hotel(city: str, estimated_price_cents: int) -> dict:
    """Book a hotel room."""
    ...

book_hotel_tool = FunctionTool(book_hotel)

result = await paybond.instrument(
    policy="./paybond.policy.yaml",
    framework="google-adk",
    tools=[book_hotel_tool, search_web_tool],
)

agent = LlmAgent(name="Travel", tools=result.tools)

Already bound a run? Use createPaybondGoogleAdkConfig(run, tools) / create_paybond_google_adk_config(run, tools) — returns { tools, wrapTool } / { tools, wrap_tool } for incremental wiring. Prefer LlmAgent(tools=config.tools) with pre-wrapped tools.

See Agent middleware for run binding, registry rules, and tenant isolation.

Advanced / manual wiring

When you need step-by-step control over registry and bind:

  1. Bind a PaybondAgentRun with a tool registry (sandbox bootstrap or production attach).
  2. Register side-effecting tools on the registry so spend resolvers read tool args (for example amount_cents from tool kwargs).
  3. Wrap tools with createPaybondGoogleAdkConfig / create_paybond_google_adk_config before registering them on LlmAgent / sub-agents.

TypeScript

import { FunctionTool } from "@google/adk";
import { createPaybondGoogleAdkConfig } from "@paybond/google-adk";

const submitPoTool = new FunctionTool({
  name: "procurement.submit_po",
  description: "Submit a purchase order",
  parameters: { /* ... */ },
  execute: async (args) => submitPo(args),
});

const config = createPaybondGoogleAdkConfig(run, [submitPoTool]);
const guardedTools = config.tools;
// Or wrap incrementally: config.wrapTool(newTool)

Python

from google.adk.tools import FunctionTool
from paybond_kit.google_adk import create_paybond_google_adk_config

submit_po_tool = FunctionTool(submit_po)

config = create_paybond_google_adk_config(run, [submit_po_tool])
guarded_tools = config.tools
# Or wrap incrementally: config.wrap_tool(new_tool)

Non-side-effecting tools (per registry) pass through unchanged.

Scaffold and smoke

# TypeScript
paybond init agent-middleware --framework google-adk --out paybond-google-adk.ts
# Python
paybond init agent-middleware --framework google-adk --out paybond_google_adk.py
paybond policy init --preset saas --out paybond.policy.yaml

Validate authorization and evidence without a live ADK agent or LLM:

paybond login
paybond agent demo google-adk smoke \
  --operation paid-tool \
  --requested-spend-cents 100 \
  --evidence-preset cost_and_completion \
  --format json

TypeScript: npm install @paybond/google-adk @google/adk. Python optional extra: pip install "paybond-kit[google-adk]". With pipx: pipx install 'paybond-kit[google-adk]' or pipx inject paybond-kit google-adk.

Approval holds

When Harbor returns an approval hold, surface it to operators, approve in the tenant console, then retry with the same operation, amount, metadata, and approvalToken. Hard denials must not execute the tool.

  • Python: guarded tools raise RuntimeError with a descriptive Paybond message (Paybond capability approval required: … or Paybond capability denied: …) so the ADK agent loop surfaces the hold without performing the side effect. After quorum, store the console token keyed by ADK function_call_id, then retry:
run.store_approval_token(tool_call_id, approval_token_from_console)

Pass the same tool_call_id on retry (ADK reuses function_call_id on the tool context). Wrapped tools read it via run.get_approval_token(tool_call_id).

  • TypeScript: guarded tools rethrow clear Error messages (Paybond capability approval required: … / Paybond capability denied: …) with the original Harbor error as cause. Use the ADK functionCallId from toolContext when storing a retry token:
run.storeApprovalToken(toolCallId, approvalTokenFromConsole);

Pass the same toolCallId on retry (ADK reuses functionCallId on the tool context). Hard denials must not execute the tool.

A2A complementarity

Paybond publishes A2A discovery (GET /.well-known/agent-card.json, GET /protocol/v2/a2a/task-contracts) beside ADK's RemoteA2aAgent — use Paybond for card/contracts discovery and Harbor-backed spend on local tools. Each agent that executes paid tools must wrap FunctionTool locally with Paybond; there is no A2A spend proxy in v1. Remote hops through AgentTool / RemoteA2aAgent do not inherit Paybond guards from the caller.

Known limitations

Paybond only governs tools that go through the wrap helper:

  • GOOGLE_SEARCH and ADK built-ins — provider-executed or ADK-managed tools are not intercepted unless you replace them with local FunctionTool handlers.
  • AgentTool / RemoteA2aAgent remote hops — spend controls apply only on the process that wraps and executes the tool; remote agents need their own Paybond bind.
  • Unwrapped tools — tools added to LlmAgent after wrap (or without passing config.tools) bypass Harbor.
  • LongRunning toolsisLongRunning tools are wrapped when registered, but Paybond does not model async human-in-the-loop completion beyond Harbor approval holds.
  • before_tool_callback is NOT the Paybond path — ADK callbacks can observe tool calls but do not substitute for Harbor verify, capability tokens, or auto-evidence on the wrapped execute boundary.

For raw Gemini SDK function calling without ADK, use Gemini with agent-agnostic.