Agentgram connects to remote agents through 3 protocols, allows the creation of internal agents (custom agents) with LLM + tools, and orchestrates multi-agent sessions with two execution modes.
┌─────────────┐ ┌──────────────────────────────────────┐ ┌─────────────────┐
│ │ │ API (Go) │ │ Agent (REST) │
│ Web │────>│ │────>│ SSE/JSON │
│ (Next.js) │<────│ ┌──────────┐ ┌──────────────────┐ │<────└─────────────────┘
│ │ │ │ Registry │ │ Proxy (router) │ │ ┌─────────────────┐
│ AG-UI SSE │ │ │ (DB+cache│ │ ├─ RESTProxy │ │────>│ Agent (A2A) │
│ │ │ │ 30s) │ │ ├─ A2AProxy │ │<────│ JSON-RPC │
└─────────────┘ │ └──────────┘ │ └─ ADKProxy │ │ └─────────────────┘
│ └──────────────────┘ │ ┌─────────────────┐
│ ┌──────────────────┐ │────>│ Agent (ADK) │
│ │ Custom Agent │ │<────│ Google Vertex │
│ │ Runtime (LLM + │ │ └─────────────────┘
│ │ tool-call loop) │ │ ┌─────────────────┐
│ └──────────────────┘ │────>│ MCP Servers │
└──────────────────────────────────────┘ └─────────────────┘
Agents are defined in PostgreSQL and cached in memory (agents/registry.go, auto-refresh every 30s). Each agent has a Protocol that determines the proxy.
- Request:
POSTto the agent endpoint with{"query": "...", "conversation_id": "..."} - Response: Auto-detects
text/event-stream(SSE) or JSON- SSE: parses native AG-UI (
TEXT_MESSAGE_CONTENT), legacy (content_delta,chunk), and tool calls - JSON: extracts content from OpenAI, Anthropic, or simple formats
- SSE: parses native AG-UI (
- Sessions: The agent returns
session_idorconversation_idin the SSE stream, and the API maps it
- Request: JSON-RPC 2.0
message/streamwith parts (text,data,file) - Response: SSE with JSON-RPC events
- States:
submitted→working→completed/failed/rejected/auth-required - Text from:
status-update(messages) andartifact-update(artifacts) - Tool calls: extracted from
dataparts withfunctionCall/functionResponse
- States:
- Sessions: Uses
contextIdfrom JSON-RPC responses - PipelineFinalAgent: Distinguishes "thinking" text from final output in internal pipelines
- Request: Creates a session (
POST /apps/{appName}/users/{userId}/sessions) + streaming (POST /run_sse) - Response: The most complex proxy (~715 lines):
- Context rotation: Detects
<context-limit-summary>, creates a new session, and retries - Deduplication: Filters
partial=truevspartial=falsetext - Keep-alive: Sends SSE comments every 10s
- Thoughts: Parts marked as
Thoughtare emitted as "thinking"
- Context rotation: Detects
- Sessions: Explicit management via the ADK agent's API
proxy/proxy.go:77-93 — a simple switch on agent.Protocol:
switch agent.Protocol {
case "custom": return p.rest.Handle(...)
case "a2a": return p.a2a.Handle(...)
case "adk": return p.adk.Handle(...)
}A different concept from external agents: they run an LLM + tool-calling loop inside the API itself.
| Field | Description |
|---|---|
SystemPrompt |
Prompt that defines the behavior |
LLMModelID |
Reference to an LLM model in the DB (Anthropic, OpenAI, Google) |
SubAgentIDs |
Regular agents invocable as tools |
MCPServerIDs |
MCP servers whose tools are available |
Visibility |
private / shared / public with full RBAC |
Uses the shared mcp.RunLoop loop with parallel tool execution:
1. Resolves the LLM model (fallback to default)
2. Builds tools (MCP servers + sub-agents) with permission verification
3. Delegates to mcp.RunLoop with Parallel=true:
├── Calls the LLM (max 10 rounds)
├── If there are tool calls → executes in parallel:
│ ├── MCP: callMCPTool → server.Client.CallTool
│ └── Agent: callSubAgent → REST/A2A/ADK client
├── Feeds results back to the LLM
└── If there are no tool calls → emits final text
- MCP tools: prefix
mcp__{serverID}__{toolName} - Sub-agent tools: prefix
agent__{agentID}with schema{"message": "..."} - Verifies the user's permissions on each MCP server and sub-agent
Sends the same message to multiple agents in parallel:
1. For each agent (goroutine):
├── PrepareMessagesForMultiAgent (computes delta)
├── Proxy.Handle → proxy by protocol
├── Captures SSE via pipeResponseWriter
└── Enriches events with agentId, sends to eventCh
2. Multiplexes eventCh → ResponseWriter (unified SSE)
3. Saves results per agent (AddMessage with atomic Lua)
Sequential execution through a defined sequence:
Sequence: ["agentA", "agentB", "user", "agentC"]
↑
Stops and waits for input
1. Loop over Sequence from SequenceIndex:
├── If step is "user" → emits step event, updates index, returns
└── If step is an agent:
├── PrepareMessagesForConversation (delta + round responses)
├── Proxy to the agent via pipeResponseWriter
├── Enriches events, streams inline
├── Saves message, accumulates roundResponses
└── Updates SequenceIndex (atomic Lua)
2. At the end: reset index to 0 (cyclic)
PrepareMessagesForMultiAgent: Messages from other agents since the target agent's last interactionPrepareMessagesForConversation: Delta + accumulated responses from the current round- Optionally summarizes context via the LLM (
summarizerpackage)
The LLM + tool-calling loop is used by MCP chat and custom agents:
mcp.RunLoop(ctx, w, mcp.LoopParams{
SessionID: sessionID,
SessionStore: sessionStore,
LLMClient: llmClient,
Tools: tools,
Messages: llmMessages,
Handler: toolHandler, // Resolve + Execute
Parallel: true/false, // Parallel (custom agents) or sequential (MCP)
})The ToolHandler abstracts the resolution (name + serverID) and execution of each tool call:
| Caller | Resolve | Execute | Parallel |
|---|---|---|---|
| MCP Chat (single) | tc.Name, "", false |
server.Client.CallToolWithHeaders |
No |
| MCP Chat (multi) | Parse serverID__toolName |
Route to correct server | No |
| Custom Agent | MCP: realName, serverID / Agent: tc.Name |
MCP tool or sub-agent | Yes |
| Redis Key | Purpose |
|---|---|
session:{uuid} |
Session data (JSON) |
user_sessions:{email}:{agentId} |
Set of IDs per user/agent |
user_multi_sessions:{email} |
Set of multi-agent sessions |
agent_session_map:{sessionID}:{agentID} |
UI session → agent session mapping |
| Type | Creation | Specifics |
|---|---|---|
| Single-agent | First interaction | Simple key session:{uuid} |
| Multi-agent | POST /api/sessions/multi |
Contains AgentIDs, MultiAgentMode, Sequence |
| Custom agent | First interaction | Same store, prefix custom:{agentID} |
AddMessage: Atomic append of messages (critical in broadcast with concurrent writes)UpdateSequenceIndex: Atomic update of the sequence index
Each external agent maintains its own session. The API maps the internal sessionID → the agent's sessionID:
- REST:
session_idorconversation_idin SSE events - A2A:
contextIdin JSON-RPC responses - ADK: Explicit creation via the API, with rotation on context-limit
| Layer | File | Rationale |
|---|---|---|
AgentWrapper (mutex) |
agents/agent.go |
Concurrent health checks vs reads |
Registry (cache + refresh) |
agents/registry.go |
Avoids DB queries on every request |
Proxy (router) |
proxy/proxy.go |
30 lines, separates 3 proxies of 100-700 lines |
SSEWriter |
proxy/sse.go |
Used by all 3 proxies, thread-safe |
pipeResponseWriter |
handlers/broadcast.go |
Multiplexes broadcast/conversation |
RunLoop |
mcp/loop.go |
Shared loop between MCP chat and custom agents |
ToolHandler |
mcp/loop.go |
Abstracts tool resolution and execution |