Skip to content

Commit 5e1082f

Browse files
danielchalefclaude
andauthored
Add Google ADK Go integration (#527)
* Add Google ADK Go integration Zep memory for Google ADK (Go) via a before-model callback, a memory.Service, and a graph-search tool. - NewBeforeModelCallback(client, ...): persists the latest user message (Thread.AddMessages{ReturnContext:true}) and injects the returned Context Block into req.Config.SystemInstruction. Attach via llmagent.Config BeforeModelCallbacks. - zepMemoryService implements memory.Service (AddSessionToMemory / SearchMemory over Graph.Search); attaches at the Runner. Optional graph-search FunctionTool. - Module github.com/getzep/zep/integrations/adk/go; deps google.golang.org/adk v1.4.0 + github.com/getzep/zep-go/v3 v3.23.0. Live calls guarded when ZEP_API_KEY is unset. - README, SETUP, example main.go, table tests. CI already routes integrations/adk/go. Verified: go build ./... + go vet ./... + go test ./... (all exit 0). See integrations/SPIKE_FINDINGS.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(test-go): upgrade golangci-lint-action v6 -> v8 (golangci-lint v2) The pinned golangci-lint-action@v6 installs golangci-lint v1.64.8 (built with go1.24), which fails to lint a module whose go.mod targets go 1.25.0 with "the Go language version (go1.24) ... is lower than the targeted Go version (1.25.0)". v8 installs golangci-lint v2.x (built with go >=1.25), which lints go 1.25 modules. golangci-lint v2.12.2 reports 0 issues on the adk/go package. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix ADK Go memory: tool-loop dedup, assistant persistence, scope mapping Apply code-review fixes to the zepadk integration: - BeforeModelCallback no longer re-persists the user message or re-injects the Context Block on tool-loop continuations (e.g. the search_memory path). It detects a continuation by checking whether the latest req.Contents entry is a function response, so a turn is recorded in Zep exactly once. - Add NewAfterModelCallback (WithAssistantMessageName, WithAfterLogger) that persists the assistant's reply to the same thread via Thread.AddMessages, so the user graph captures both halves of the conversation. Wired into the example and documented. - Map every supported graph search scope into results in both the memory service and the search tool (edges -> facts, nodes -> entity summaries, episodes -> content, observations -> derived memories, auto -> Context Block). Unsupported scopes (e.g. thread_summaries) are rejected loudly instead of silently returning nothing. Shared mapping lives in search.go. - Truncate over-limit message content to 4000 chars (Zep's limit is 4096) before persisting, with a lengths-only warning (never logs content/PII). Shared helper in zep.go. - Introduce a minimal zepAPI seam over *zepclient.Client (AddMessages/Search) so persist/inject/dedup/scope-mapping are table-tested with an in-memory fake (seam_test.go); public constructor signatures are unchanged. - Correct the documented Go floor to 1.25 (adk v1.4.0 requires it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent dca59ae commit 5e1082f

18 files changed

Lines changed: 2588 additions & 1 deletion

File tree

.github/actions/test-go/action.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ runs:
4242
run: go test ./...
4343

4444
- name: Lint
45-
uses: golangci/golangci-lint-action@55c2c1448f86e01eaae002a5a3a9624417608d84 # v6
45+
# v8 installs golangci-lint v2.x, which supports go >=1.25 modules
46+
# (the v6/golangci-lint v1.64.8 default refuses go 1.25 go.mod directives).
47+
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8
4648
with:
4749
version: latest
4850
working-directory: ${{ steps.paths.outputs.dir }}

integrations/adk/go/CHANGELOG.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Changelog
2+
3+
## 0.1.0 (2026-06-16)
4+
5+
### Added
6+
7+
- `NewBeforeModelCallback` — an ADK `llmagent.BeforeModelCallback` that persists
8+
each new user turn to a Zep thread and injects the returned Context Block into
9+
the model request's system instruction (`Thread.AddMessages` with
10+
`ReturnContext=true`). It detects tool-loop continuations (a function response
11+
as the latest content in `req.Contents`) and skips re-persisting/re-injecting,
12+
so a turn that calls `search_memory` is recorded exactly once. Oversize
13+
messages are truncated to Zep's 4,096-character limit with a lengths-only
14+
warning (content is never dropped or logged). Configurable via
15+
`WithContextPrefix`, `WithUserMessageName`, and `WithLogger`.
16+
- `NewAfterModelCallback` — an ADK `llmagent.AfterModelCallback` that persists the
17+
assistant's text reply back to the same Zep thread, so the user graph captures
18+
both halves of the conversation. Skips model errors, partial streaming chunks,
19+
and function-call-only responses (tool-loop steps). Configurable via
20+
`WithAssistantMessageName` and `WithAfterLogger`.
21+
- `NewMemoryService` — an ADK `memory.Service` backed by Zep user-graph search,
22+
attachable at the runner via `runner.Config.MemoryService`. Maps every
23+
supported search scope into results (edges → facts, nodes → entity summaries,
24+
episodes → message content, observations → derived memories, auto → the Context
25+
Block) and rejects unsupported scopes loudly. Configurable via
26+
`WithSearchScope`, `WithSearchLimit`, and `WithMemoryLogger`.
27+
- `NewGraphSearchTool` — a `functiontool` (`search_memory`) the model can call to
28+
search the user's Zep knowledge graph on demand, with optional standalone-graph
29+
scoping via `WithGraphID`. Maps the same set of search scopes as the memory
30+
service.
31+
- `EnsureUser` / `EnsureThread` — idempotent helpers to provision the Zep user
32+
and thread keyed on the ADK user ID and session ID.
33+
- `NewClient` / `NewClientFromEnv` — Zep client constructors; `NewClientFromEnv`
34+
returns `nil` when `ZEP_API_KEY` is unset so the integration degrades to a
35+
no-op.
36+
- `InjectSystemInstruction`, `LastUserText`, `AssistantText`,
37+
`IsToolLoopContinuation` — exported helpers for building custom callbacks.
38+
- Graceful error handling throughout: a `nil` client and transient Zep errors
39+
never crash the host agent.
40+
- Table-based unit tests (no network, via an internal client seam) and a runnable
41+
example wiring an `llmagent` and runner.
42+
43+
Targets `google.golang.org/adk` v1.4.0 and `github.com/getzep/zep-go/v3` v3.23.0.

integrations/adk/go/Makefile

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Makefile for the Zep Google ADK (Go) integration
2+
3+
.PHONY: help tidy fmt fmt-check vet lint test test-race build example all ci clean
4+
5+
help:
6+
@echo "Available commands:"
7+
@echo " tidy - go mod tidy"
8+
@echo " fmt - Format code with gofmt"
9+
@echo " fmt-check - Verify code is gofmt-clean (fails if not)"
10+
@echo " vet - Run go vet"
11+
@echo " lint - Run golangci-lint (if installed)"
12+
@echo " test - Run tests"
13+
@echo " test-race - Run tests with the race detector"
14+
@echo " build - Build all packages"
15+
@echo " example - Run the example (needs ZEP_API_KEY + GOOGLE_API_KEY)"
16+
@echo " all - tidy + fmt-check + vet + lint + test"
17+
@echo " ci - vet + lint + test (no auto-formatting)"
18+
@echo " clean - go clean"
19+
20+
tidy:
21+
go mod tidy
22+
23+
fmt:
24+
gofmt -w .
25+
26+
fmt-check:
27+
@unformatted=$$(gofmt -l .); \
28+
if [ -n "$$unformatted" ]; then \
29+
echo "These files are not gofmt-clean:"; echo "$$unformatted"; exit 1; \
30+
fi
31+
32+
vet:
33+
go vet ./...
34+
35+
# Runs golangci-lint when available; skips with a notice otherwise.
36+
lint:
37+
@if command -v golangci-lint >/dev/null 2>&1; then \
38+
golangci-lint run ./...; \
39+
else \
40+
echo "golangci-lint not installed; skipping (install: https://golangci-lint.run)"; \
41+
fi
42+
43+
test:
44+
go test ./...
45+
46+
test-race:
47+
go test -race ./...
48+
49+
build:
50+
go build ./...
51+
52+
example:
53+
go run ./examples
54+
55+
all: tidy fmt-check vet lint test
56+
@echo "All checks passed!"
57+
58+
ci: vet lint test
59+
@echo "CI checks passed!"
60+
61+
clean:
62+
go clean ./...

integrations/adk/go/README.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# Zep Google ADK (Go) Integration
2+
3+
`zepadk` gives [Google Agent Development Kit (ADK) for Go](https://github.com/google/adk-go)
4+
agents persistent, cross-session memory backed by [Zep](https://www.getzep.com),
5+
Zep's temporal Context Graph platform for agent memory.
6+
7+
It plugs into ADK's native extension points — it does **not** wrap or replace the
8+
ADK runtime:
9+
10+
- **Context injection** via an `llmagent.BeforeModelCallback` that persists each
11+
new user turn to Zep and injects the user's Context Block into the model
12+
request. It skips tool-loop continuations so a turn is recorded exactly once.
13+
- **Assistant persistence** via an `llmagent.AfterModelCallback` that writes the
14+
assistant's reply back to the same Zep thread, so the user graph captures both
15+
sides of the conversation.
16+
- **Memory service** — an ADK `memory.Service` backed by Zep user-graph search,
17+
attached at the runner and reached by tools through `ToolContext.SearchMemory`.
18+
- **On-demand recall** — a `functiontool` the model can call to search the user's
19+
Zep knowledge graph.
20+
21+
## Installation
22+
23+
```bash
24+
go get github.com/getzep/zep/integrations/adk/go@latest
25+
```
26+
27+
```go
28+
import zepadk "github.com/getzep/zep/integrations/adk/go"
29+
```
30+
31+
Requirements: Go 1.25+ (`google.golang.org/adk` v1.4.0 requires Go 1.25),
32+
`google.golang.org/adk` v1.4.0, `github.com/getzep/zep-go/v3` v3.23.0.
33+
34+
## Quick start
35+
36+
```go
37+
zep := zepadk.NewClientFromEnv() // nil when ZEP_API_KEY is unset -> no-op
38+
39+
agent, _ := llmagent.New(llmagent.Config{
40+
Name: "assistant",
41+
Model: llm, // a model.LLM, e.g. gemini.NewModel(...)
42+
BeforeModelCallbacks: []llmagent.BeforeModelCallback{zepadk.NewBeforeModelCallback(zep)},
43+
AfterModelCallbacks: []llmagent.AfterModelCallback{zepadk.NewAfterModelCallback(zep)},
44+
Tools: []tool.Tool{searchTool}, // from zepadk.NewGraphSearchTool(zep)
45+
})
46+
47+
run, _ := runner.New(runner.Config{
48+
AppName: "my_app",
49+
Agent: agent,
50+
SessionService: sessions,
51+
MemoryService: zepadk.NewMemoryService(zep),
52+
})
53+
```
54+
55+
See [`examples/main.go`](examples/main.go) for a complete, runnable wiring of the
56+
agent, runner, session service, and Zep user/thread provisioning.
57+
58+
## How it works
59+
60+
The integration contract maps ADK identifiers to Zep identifiers:
61+
62+
| ADK | Zep |
63+
|-----|-----|
64+
| session ID | thread ID |
65+
| user ID | user ID (user graph) |
66+
67+
Provision the Zep user and thread out of band before the first turn with
68+
`EnsureUser` and `EnsureThread` (both idempotent). Then, on a genuinely new user
69+
turn, the callback returned by `NewBeforeModelCallback`:
70+
71+
1. Reads the user's latest message from the ADK callback context.
72+
2. Truncates it to Zep's 4,096-character per-message limit if needed (logging a
73+
lengths-only warning — message content is never dropped or logged).
74+
3. Persists it to the user's Zep thread, requesting the Context Block in the same
75+
round-trip (`Thread.AddMessages` with `ReturnContext=true`).
76+
4. Injects the returned Context Block into `req.Config.SystemInstruction`.
77+
78+
During a tool loop ADK re-invokes the before-model callback after each tool
79+
result. On those continuations the latest content in `req.Contents` is a function
80+
response rather than new user input, so the callback returns early without
81+
re-persisting the message or re-injecting the Context Block — a turn that calls
82+
`search_memory` is recorded in Zep exactly once.
83+
84+
`NewAfterModelCallback` complements it: after the model replies, it persists the
85+
assistant's text to the same thread (as an `assistant` message). It skips model
86+
errors, partial streaming chunks, and function-call-only responses (tool-loop
87+
steps), so only genuine replies are recorded.
88+
89+
The public surface lives in:
90+
91+
- [`zepadk.go`](zepadk.go)`NewBeforeModelCallback`, `NewAfterModelCallback`,
92+
`EnsureUser`, `EnsureThread`, and the helpers `InjectSystemInstruction`,
93+
`LastUserText`, `AssistantText`, `IsToolLoopContinuation`.
94+
- [`memory.go`](memory.go)`NewMemoryService` (the ADK `memory.Service` over
95+
Zep `Graph.Search`).
96+
- [`tool.go`](tool.go)`NewGraphSearchTool` (the on-demand `search_memory` tool).
97+
- [`search.go`](search.go) — scope-aware mapping of Zep search results.
98+
- [`client.go`](client.go)`NewClient` / `NewClientFromEnv`.
99+
100+
### Search scopes
101+
102+
The memory service and search tool map every supported Zep search scope into
103+
results — earlier versions read only `edges` and silently returned nothing for
104+
other scopes:
105+
106+
| Scope | Result |
107+
|-------|--------|
108+
| `edges` (default) | facts |
109+
| `nodes` | entity summaries (`name: summary`) |
110+
| `episodes` | message/data content |
111+
| `observations` | derived memories |
112+
| `auto` | the pre-materialized Context Block |
113+
114+
An unsupported scope (e.g. `thread_summaries`) is rejected loudly: the service or
115+
tool logs an error and returns no results rather than silently swallowing them.
116+
117+
## Configuration
118+
119+
Each constructor accepts functional options:
120+
121+
| Constructor | Options |
122+
|-------------|---------|
123+
| `NewBeforeModelCallback` | `WithContextPrefix`, `WithUserMessageName`, `WithLogger` |
124+
| `NewAfterModelCallback` | `WithAssistantMessageName`, `WithAfterLogger` |
125+
| `NewMemoryService` | `WithSearchScope`, `WithSearchLimit`, `WithMemoryLogger` |
126+
| `NewGraphSearchTool` | `WithToolName`, `WithToolDescription`, `WithGraphID`, `WithToolSearchScope`, `WithToolSearchLimit`, `WithToolLogger` |
127+
128+
`WithGraphID` scopes the search tool to a standalone graph instead of the calling
129+
user's graph (`UserID` and `GraphID` are mutually exclusive in Zep).
130+
131+
## Error handling
132+
133+
A Zep failure never crashes the host agent:
134+
135+
- A `nil` client (for example when `ZEP_API_KEY` is unset) makes the callback,
136+
memory service, and tool safe no-ops.
137+
- Transient Zep errors are logged via the configured `slog.Logger` and swallowed;
138+
the callback proceeds to the model without injected memory, and the memory
139+
service and tool return empty results.
140+
141+
## Notes
142+
143+
- **Ingestion is asynchronous.** A message added during a turn is not guaranteed
144+
to be retrievable within that same turn; the returned Context Block reflects
145+
prior turns. Design for eventual availability.
146+
- **Reuse one client** across the lifetime of the process.
147+
- Pass real user names (and ideally last name + email) to `EnsureUser` so Zep
148+
resolves the user's identity in the graph.
149+
150+
## Development
151+
152+
```bash
153+
make all # tidy + fmt-check + vet + lint + test
154+
make test # go test ./...
155+
```
156+
157+
`make lint` runs `golangci-lint` if it is installed.
158+
159+
## Support
160+
161+
- [Zep documentation](https://help.getzep.com)
162+
- [Google ADK for Go](https://github.com/google/adk-go)
163+
- [GitHub issues](https://github.com/getzep/zep/issues)
164+
165+
## License
166+
167+
Apache 2.0 — see the repository [LICENSE](../../../LICENSE).

integrations/adk/go/SETUP.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Setup Guide
2+
3+
This guide takes you from a fresh checkout to running the example agent with
4+
Zep memory.
5+
6+
## 1. Sign up for Zep and create an API key
7+
8+
1. Go to [https://www.getzep.com](https://www.getzep.com) and create an account.
9+
2. Open the [Zep dashboard](https://app.getzep.com) and select (or create) a project.
10+
3. In the project settings, go to **API Keys** and create a new key.
11+
4. Copy the key — you will set it as `ZEP_API_KEY` below.
12+
13+
Zep is a paid product; see [getzep.com](https://www.getzep.com) for plan details.
14+
15+
## 2. Get a Google API key (for the example)
16+
17+
The integration is model-agnostic, but the bundled example drives the agent with
18+
Google's Gemini models — ADK for Go's native model provider. Create a key in
19+
[Google AI Studio](https://aistudio.google.com/apikey) and copy it for
20+
`GOOGLE_API_KEY`.
21+
22+
## 3. Install
23+
24+
Add the module to your project:
25+
26+
```bash
27+
go get github.com/getzep/zep/integrations/adk/go@latest
28+
```
29+
30+
Import it as `zepadk`:
31+
32+
```go
33+
import zepadk "github.com/getzep/zep/integrations/adk/go"
34+
```
35+
36+
To work from the repository instead:
37+
38+
```bash
39+
git clone https://github.com/getzep/zep.git
40+
cd zep/integrations/adk/go
41+
go mod download
42+
```
43+
44+
Requirements: Go 1.25+ (`google.golang.org/adk` v1.4.0 requires Go 1.25),
45+
`google.golang.org/adk` v1.4.0, `github.com/getzep/zep-go/v3` v3.23.0.
46+
47+
## 4. Configure environment variables
48+
49+
```bash
50+
export ZEP_API_KEY="your-zep-api-key"
51+
export GOOGLE_API_KEY="your-google-api-key"
52+
```
53+
54+
If `ZEP_API_KEY` is unset, the integration disables itself (the Zep client is
55+
`nil` and every Zep call becomes a no-op), so the agent still runs — useful for
56+
confirming the wiring without a Zep account. If `GOOGLE_API_KEY` is unset, the
57+
example prints the configured wiring and exits before calling the model.
58+
59+
## 5. Run the example
60+
61+
```bash
62+
go run ./examples
63+
```
64+
65+
The example:
66+
67+
1. Creates (idempotently) a Zep user and a thread keyed on the ADK session ID.
68+
2. Builds an `llmagent` whose `BeforeModelCallback` persists each new user turn
69+
to Zep and injects the user's Context Block into the prompt, and whose
70+
`AfterModelCallback` persists the assistant's reply back to the same thread.
71+
3. Registers a `search_memory` tool the model can call on demand and attaches a
72+
Zep-backed `memory.Service` at the runner.
73+
4. Sends two turns and prints the agent's replies.
74+
75+
Because Zep ingestion is asynchronous, memory recall improves across turns and
76+
sessions rather than instantly within the first turn.
77+
78+
## 6. Run the tests
79+
80+
The tests are mock/table-based and make no network calls, so no API keys are
81+
required:
82+
83+
```bash
84+
make test # or: go test ./...
85+
```
86+
87+
## Troubleshooting
88+
89+
- **`go get` cannot find the module** — the module path is
90+
`github.com/getzep/zep/integrations/adk/go`; Go modules under this subpath are
91+
tagged `integrations/adk/go/vX.Y.Z`.
92+
- **Recall returns nothing** — Zep ingestion is asynchronous; a just-added fact
93+
is not instantly retrievable. Recall improves on subsequent turns and across
94+
sessions for the same user.
95+
- **Authentication errors** — confirm `ZEP_API_KEY` is set in the same shell and
96+
belongs to the intended project.
97+
- **Agent runs but has no memory** — verify `ZEP_API_KEY` is exported; an unset
98+
key makes the Zep client `nil` and all Zep calls no-ops by design.

0 commit comments

Comments
 (0)