Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions .github/workflows/linter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ jobs:

steps:
- name: Checkout Code
uses: actions/checkout@v5
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
fetch-depth: 0

- name: GitHub Super Linter
uses: super-linter/super-linter/slim@v8
uses: super-linter/super-linter/slim@9e863354e3ff62e0727d37183162c4a88873df41 # v8
env:
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -29,6 +29,8 @@ jobs:
SHELLCHECK_OPTS: -e SC1091 -e 2086
VALIDATE_ALL_CODEBASE: false
FILTER_REGEX_EXCLUDE: "^(\\.github/|\\.vscode/).*|CODE_OF_CONDUCT.md|(extensions/agp/).*|.*pyproto/.*|.*pb/.*|itk/agents/go/.*"
VALIDATE_GO: false
VALIDATE_GO_MODULES: false
VALIDATE_PYTHON_BLACK: false
VALIDATE_PYTHON_FLAKE8: false
VALIDATE_PYTHON_ISORT: false
Expand All @@ -52,3 +54,28 @@ jobs:
VALIDATE_BIOME_FORMAT: false
VALIDATE_BIOME_LINT: false
VALIDATE_GITHUB_ACTIONS_ZIZMOR: false

go-lint:
name: Lint Go
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout Code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5

- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: stable

- name: Lint all Go modules
run: |
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
status=0
while IFS= read -r dir; do
echo "::group::Linting $dir"
(cd "$dir" && golangci-lint run ./...) || status=$?
echo "::endgroup::"
done < <(find . -name go.mod -exec dirname {} \;)
exit $status
6 changes: 6 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: "2"
linters:
settings:
errcheck:
exclude-functions:
- "(*database/sql.Tx).Rollback"
2 changes: 2 additions & 0 deletions samples/go/agents/deepresearch/.example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
GOOGLE_API_KEY=
REPORT_URL=http://127.0.0.1:8080
118 changes: 118 additions & 0 deletions samples/go/agents/deepresearch/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# AGENTS.md — Deep Research Agent Style Guide

## Project Overview

This is a **multi-agent deep research system** built on the A2A (Agent-to-Agent) protocol. A single Go binary serves four agent roles — orchestrator, researcher, analyzer, synthesizer — selected at runtime via the `NODE_TYPE` environment variable. The system uses NATS JetStream for event sourcing and work distribution, MySQL for task persistence, and Gemini (via Google ADK) for LLM reasoning.

### Key dependencies

| Dependency | Purpose |
| --- | --- |
| `github.com/a2aproject/a2a-go/v2` | A2A protocol SDK (server, client, types, push, queues, stores) |
| `google.golang.org/adk` | Google Agent Development Kit (LLM agents, runners, sessions, tools) |
| `google.golang.org/genai` | Google GenAI SDK (Gemini model, content types) |
| `github.com/nats-io/nats.go` | NATS client / JetStream |
| `github.com/go-sql-driver/mysql` | MySQL driver (blank-imported for side effects) |

---

## Architecture

```text
Client → Orchestrator (state machine)
├── Researcher (Google Search grounding)
├── Analyzer (referenced-task injection)
└── Synthesizer (referenced-task injection)

Infrastructure: MySQL (task index + outbox) · NATS JetStream (events, work, state) · nginx (host-based LB)
```

- **Single binary, multi-role**: `main.go` reads `NODE_TYPE` and wires the corresponding `a2asrv.AgentExecutor`.
- **Event sourcing**: Tasks are materialized by replaying events from NATS streams.
- **Transactional outbox**: MySQL insert + NATS publish are guaranteed atomic via an outbox table relayed by a leader-elected poller.
- **Scatter/gather**: The orchestrator fans out subtasks via async A2A sends and gathers results through NATS push notifications.

---

## Project Layout

```text
deepresearch/
├── main.go # Entry point, config, server wiring
├── internal/
│ ├── agents/ # Agent executors (orchestrator, researcher, analyzer, synthesizer)
│ ├── clusterclient/ # Async A2A client wrapper for inter-agent communication
│ ├── domain/ # Shared domain types (AgentType enum, Info)
│ ├── lease/ # NATS KV-based leader election
│ ├── msgstream/ # NATS-backed event queues, work queues, push sender
│ ├── report/ # HTTP handler for serving synthesized reports
│ ├── server/ # Server wiring (infra setup, handler creation)
│ ├── statemachine/ # Generic event-sourced state machine
│ ├── store/ # MySQL-backed task store, indexing, transactional outbox
│ ├── testutil/ # Shared test helpers
│ └── utils/ # Small generic helpers (Must, SchemaFor)
├── infra/ # Docker Compose, nginx, MySQL schema, NATS bootstrap
├── Dockerfile
└── go.mod
```

**Rules**:
- All domain logic lives under `internal/` — one concern per package.
- Each package should have a single clear responsibility (e.g., `lease` only does leader election).
- `main.go` is the only file in package `main`; it handles configuration, dependency wiring, and graceful shutdown.

---

## Coding Rules

Comment thread
yarolegovich marked this conversation as resolved.
### Testing

- Test observable behavior, not the internal state.
- Use table-driven tests where applicable.
- Name test functions `TestFunctionName_scenario`.

### Comments

- **Prefer self-explanatory code**.
- **Doc comments**: `// SymbolName does X.` directly above the symbol. Start with the symbol name per Go convention. Add for all exported symbols, but be brief.
- **Inline comments**: Use sparingly, be brief, explain *why* not *what*.
- **References**: Use Go doc-link syntax `[a2a.Client]` when referencing other symbols.

### Logging

Use `github.com/a2aproject/a2a-go/v2/log` exclusively. Do not use `log/slog` or `fmt.Println` for application logging.

---

## Things to Know

### Event-sourced state machine (`internal/statemachine/`)

The generic `statemachine.Spec[E, S]` (driven by `statemachine.Run`) drives the orchestrator:
- **Decode**: Parse raw NATS messages into typed events.
- **Evolve**: Apply events to state (pure state transitions, no side effects).
- **Act**: Inspect state and decide on side effects (dispatch subtasks, call LLM, complete).

### Transactional outbox (`internal/store/outbox.go`)

Guarantees atomicity between MySQL writes and NATS publishes:
1. Insert task + outbox row (tagged with the agent type) in the same SQL transaction.
2. A leader-elected poller reads outbox rows for its own agent type, publishes to NATS, then deletes.

### Decorator pattern (`internal/agents/common.go`)

`referencedTaskLoader` wraps an `AgentExecutor` via embedding and intercepts `Execute` to inject referenced task content before delegating to the inner executor.

### Leader election (`internal/lease/`)

Uses NATS KV `Create` (atomic put-if-absent) for distributed locking. Watches for key deletion to retry acquisition.

### Infrastructure

All services are defined in `infra/docker-compose.yaml`.

### Misc

- This agent is a **self-contained Go module** (`go.mod` at the deepresearch root). It does not share code with other Go samples in the repository.
- The A2A SDK (`a2a-go/v2`) provides the server framework, client, types, and infrastructure interfaces (queues, stores, push). Domain logic implements these interfaces.
- The orchestrator's workflow proceeds through stages: **research -> analyze -> follow-up research -> synthesize -> complete**.
13 changes: 13 additions & 0 deletions samples/go/agents/deepresearch/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM golang:1.25-alpine AS builder

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /deepresearch .

FROM gcr.io/distroless/static:nonroot
COPY --from=builder /deepresearch /deepresearch
ENTRYPOINT ["/deepresearch"]
76 changes: 76 additions & 0 deletions samples/go/agents/deepresearch/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
COMPOSE := docker compose --env-file .env -f infra/docker-compose.yaml
NATS_URL := nats://localhost:4222
MYSQL_DSN := root:root@tcp(localhost:3306)/planner?parseTime=true

# Local ports (go run mode)
ORCH_PORT := 8080
RESEARCH_PORT := 8081
ANALYZE_PORT := 8082
SYNTH_PORT := 8083

# ── Docker Compose ──────────────────────────────────────────────

.PHONY: up force-up infra-up down clean

## up: start all services (cached images)
up:
$(COMPOSE) up -d

## force-up: rebuild images and start all services
force-up:
$(COMPOSE) up --build -d

## infra-up: start only NATS, MySQL, and run the NATS init script
infra-up:
$(COMPOSE) up nats mysql -d
$(COMPOSE) run --rm nats-init

## down: stop all services (preserves volumes)
down:
$(COMPOSE) down

## clean: stop all services and remove volumes
clean:
$(COMPOSE) down -v

# ── Local Mode (go run) ────────────────────────────────────────
# Runs all four agents as local processes against containerised
# NATS + MySQL. No nginx needed — the orchestrator connects to
# researcher/analyzer/synthesizer directly on localhost ports.
#
# Prerequisites: make infra-up
# Usage: make local
# Stop: Ctrl-C (kills all four processes)

.PHONY: local

local:
@if [ -z "$$GOOGLE_API_KEY" ] && [ -f infra/.env ]; then \
export $$(grep -v '^#' infra/.env | xargs); \
fi; \
trap 'kill 0' EXIT; \
GOOGLE_API_KEY=$${GOOGLE_API_KEY} NODE_TYPE=researcher LISTEN_ADDR=:$(RESEARCH_PORT) go run . & \
GOOGLE_API_KEY=$${GOOGLE_API_KEY} NODE_TYPE=analyzer LISTEN_ADDR=:$(ANALYZE_PORT) go run . & \
GOOGLE_API_KEY=$${GOOGLE_API_KEY} NODE_TYPE=synthesizer LISTEN_ADDR=:$(SYNTH_PORT) go run . & \
sleep 1; \
GOOGLE_API_KEY=$${GOOGLE_API_KEY} \
REPORT_URL=http://127.0.0.1:8080 \
NODE_TYPE=orchestrator \
LISTEN_ADDR=:$(ORCH_PORT) \
RESEARCHER_URL=http://localhost:$(RESEARCH_PORT) \
ANALYZER_URL=http://localhost:$(ANALYZE_PORT) \
SYNTHESIZER_URL=http://localhost:$(SYNTH_PORT) \
go run . ; \
wait

# ── Testing ─────────────────────────────────────────────────────

.PHONY: test send

## test: run integration tests (starts infra containers automatically)
test:
go test -v -timeout 60s ./itest/

## send: send a test message to the orchestrator via the a2a CLI
send:
a2a send http://localhost:$(ORCH_PORT) "Research the impact of AI on healthcare" --transport rest --stream --timeout 5m
42 changes: 42 additions & 0 deletions samples/go/agents/deepresearch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Deep Research

A multi-agent system that performs deep research on a given topic. The project showcases a way of implementing the standard SDK interfaces for integrating with various popular infra components like MySQL and NATS.

Built using [a2a-go](https://github.com/a2aproject/a2a-go) and [adk](https://github.com/google/adk-go).

## Overview

* Horizontally scalable cluster of different agent types: orchestrator, researcher, analyzer, and synthesizer.
* MySQL for task indexing and Jetstream for event persistence.
* Push notification sender for signaling subtask completion to the orchestrator.
* NATS for work distribution, event and push notification delivery.
* Retryable execution with state checkpointing.

<img src="./assets/deepresearch.png" width="740" alt="Deep research agent architecture diagram"/>

## Running

1. Rename `.example.env` to `.env` and update your `GOOGLE_API_KEY` ([learn more](https://docs.cloud.google.com/docs/authentication/api-keys)).

2. Start the full stack using docker-compose by running `make up`.

3. Call orchestrator using [a2a-cli](https://github.com/a2aproject/a2a-go#-cli) (`make send`), [a2a-inspector](https://github.com/a2aproject/a2a-inspector) or another client.


## Details

Orchestrator agents handle client requests:
1. Uses LLM planner to decompose a question into subtasks.
2. Dispatches them to a cluster of researcher agents with `returnImmediately: true`.
3. Waits for results using NATS-based push notifications.
4. Invokes an analyzer to find contradictory topics for a follow-up research.
5. Initiates the follow-up research.
6. Invokes a synthesizer to generate a final report.

If an orchestrator crashes, the state machine replays its event stream from the NATS STATES stream to recover which stages were dispatched and which completed, then resumes from where it left off.

Orchestrator never loads large task contents into memory and instead uses task references when communicating with synthesizer and analyzer. The final report is returned to a user as a reference.

Push notifications allow orchestrator to limit the number of open long-lived connections and avoid subtask status polling.

<img src="./assets/sample_output.png" width="740" alt="Sample output of the deep research agent"/>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 51 additions & 0 deletions samples/go/agents/deepresearch/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
module github.com/a2aproject/a2a-samples/samples/go/agents/deepresearch

go 1.25.0

require (
github.com/a2aproject/a2a-go/v2 v2.3.2-0.20260606182037-3134e71be608
github.com/go-sql-driver/mysql v1.10.0
github.com/google/uuid v1.6.0
github.com/nats-io/nats.go v1.52.0
golang.org/x/sync v0.20.0
google.golang.org/adk v1.3.0
google.golang.org/genai v1.58.0
)

require (
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.20.0 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/jsonschema-go v0.4.3 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/safehtml v0.1.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/log v0.16.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.37.0 // indirect
google.golang.org/api v0.279.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect
google.golang.org/grpc v1.81.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
rsc.io/omap v1.2.0 // indirect
rsc.io/ordered v1.1.1 // indirect
)
Loading
Loading