Google ADK
On an agent built with Google's Agent Development Kit, register the recorder once. Every model call and tool call is recorded, under the sub-agent that made it, and nothing at your call sites changes.
Install
pip install techbridge-agentpulse
go get github.com/techbridgeinnovation/agentpulse/recorder
go get github.com/techbridgeinnovation/agentpulse/recorder/adkv2hooks
The recorder has no dependencies of its own. It uses the ADK you already have.
Set four settings
The recorder reads them from the environment. See the quickstart for what each one is.
AP_GATEWAY=gateway.agentspulse.ai
AP_API_KEY=organisations/<organisation>/apiKeys/<key>
AP_API_SECRET=<secret>
AP_AGENT=organisations/<organisation>/agents/<agent>
Register it
import agentpulse
from google.adk.agents import Agent
from google.adk.apps import App
reporter = agentpulse.connect(service="research-agent").governed()
root_agent = Agent(
name="research",
model="gemini-2.5-pro",
instruction="Answer research questions using the tools.",
tools=tools,
)
app = App(
name="research",
root_agent=root_agent,
plugins=[reporter.adk_plugin(denied_message="You have used this month's research credits.")],
)
import (
"github.com/techbridgeinnovation/agentpulse/recorder"
"github.com/techbridgeinnovation/agentpulse/recorder/adkv2hooks"
"google.golang.org/adk/v2/agent"
"google.golang.org/adk/v2/agent/llmagent"
)
key := os.Getenv("AP_API_KEY")
conn, err := recorder.DialWithKey(os.Getenv("AP_GATEWAY"), key, os.Getenv("AP_API_SECRET"))
if err != nil {
log.Fatal(err)
}
rec := recorder.New(recorder.Config{
Sinks: []recorder.Sink{recorder.NewGRPCSink(conn, recorder.OrganisationOfKey(key))},
})
defer rec.Close(ctx)
recording := adkv2hooks.Options{
Agent: os.Getenv("AP_AGENT"),
Service: "research-agent",
Model: "gemini-2.5-pro",
// Lets budgets stop calls. Leave it out and budgets only count.
Decider: recorder.NewGRPCDecider(conn),
}
rootAgent, err := llmagent.New(llmagent.Config{
Name: "research",
Model: model,
Tools: tools,
BeforeModelCallbacks: []llmagent.BeforeModelCallback{adkv2hooks.BeforeModel(rec, recording)},
AfterModelCallbacks: []llmagent.AfterModelCallback{adkv2hooks.AfterModel(rec, recording)},
OnModelErrorCallbacks: []llmagent.OnModelErrorCallback{adkv2hooks.OnModelError(rec, recording)},
BeforeToolCallbacks: []llmagent.BeforeToolCallback{adkv2hooks.BeforeTool(rec, recording)},
AfterToolCallbacks: []llmagent.AfterToolCallback{adkv2hooks.AfterTool(rec, recording)},
OnToolErrorCallbacks: []llmagent.OnToolErrorCallback{adkv2hooks.OnToolError(rec, recording)},
AfterAgentCallbacks: []agent.AfterAgentCallback{adkv2hooks.AfterAgent(rec, recording)},
})
In Go, adkv2hooks is for ADK 2 and adkhooks for ADK 1. They take the same callbacks; only adkv2hooks takes Model. adkhooks comes with the recorder, so ADK 1 needs only the first go get. Put the recorder's callbacks ahead of your own: an error callback after one that handles the error never runs, and the failed call would not be recorded.
What is recorded
- The user, session, agent and turn come from ADK's own context. A request or user you set with
scopeorrecorder.WithUsertakes priority. - A streamed call is recorded once, when it finishes.
- A call cancelled part way is recorded as
TRUNCATED, with the tokens it had used. - A tool's result size is recorded, never its content.
In Python, an agent run as a tool with include_plugins=False runs without the plugin, so its calls are not recorded.
Budgets on ADK
With a governed reporter in Python, or a Decider in Go, each model call checks your budgets first. A stopped call is never sent: in Python the user reads your denied_message as the reply. A warn-only budget past its limit lets the call through and alerts you.