-
Notifications
You must be signed in to change notification settings - Fork 689
feat: deepresearch a2a-go agent #590
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
yarolegovich
wants to merge
8
commits into
main
Choose a base branch
from
yarolegovich/deepresearch
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+4,066
−67
Open
Changes from 2 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
dc1c2ac
deepresearch a2a-go agent
yarolegovich 1e3a89c
move non-a2a-go code to archived
yarolegovich d762de6
linter fixes
yarolegovich 86cac8b
review comments
yarolegovich 11b2b8b
more lint
yarolegovich c11d042
lint
yarolegovich 888cc80
more lint
yarolegovich 9ba422f
separate go lint
yarolegovich File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
|
||
| ``` | ||
| 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 | ||
|
|
||
| ``` | ||
| 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 | ||
|
|
||
| ## 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**. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 way of implementing the standard SDK interfaces for integrating with various popular infra components like MySQL and NATS. | ||
|
yarolegovich marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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"/> | ||
|
|
||
| ## 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 a reference. | ||
|
yarolegovich marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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"/> | ||
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| ) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.