gRPC API — Agent Gateway
Active Development
The gRPC API is under active development. Proto definitions and RPC signatures may change between releases. Pin your proto file to a specific commit when building against this API.
The Agent Gateway is a gRPC server that allows external agents to connect to Aether, receive task assignments, and access Hub services (LLM, tools, memory, messaging).
Connection Details
Default address: localhost:9090
Proto package: aether.agent.v1
Service: AgentGatewayServiceThe gRPC server must be explicitly enabled:
AGENT_GATEWAY_ENABLED=true
AGENT_GATEWAY_PORT=9090 # optional, default: 9090Proto Definition
The proto file lives in the repository at:
goway/proto/agent/v1/agent.protoGenerate a client for any language:
# Go
protoc --go_out=. --go-grpc_out=. proto/agent/v1/agent.proto
# Python
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. proto/agent/v1/agent.proto
# TypeScript
npx protoc --ts_out=. proto/agent/v1/agent.protoOfficial SDKs
SDK Status
SDK definitions and examples are available in the repository (goway/pkg/sdk/ and sdks/). The SDKs are not yet published to public package registries (PyPI, pkg.go.dev, npm). To use them, clone the repository and reference the code directly.
Agent Lifecycle
RegisterAgent
Register an external agent with the Hub. Returns a session_id used for all subsequent calls.
rpc RegisterAgent(RegisterAgentRequest) returns (RegisterAgentResponse)Request:
| Field | Type | Required | Description |
|---|---|---|---|
agent_id | string | Yes | Unique agent identifier (must match an agent in the database) |
agent_secret | string | Yes | Authentication secret configured on the agent |
agent_name | string | Yes | Human-readable name |
version | string | No | Agent version string |
capabilities | map<string, string> | No | Declared capabilities (e.g., code_review: "true") |
metadata | map<string, string> | No | Additional metadata |
Response:
| Field | Type | Description |
|---|---|---|
success | bool | Whether registration succeeded |
session_id | string | Session token — include in all subsequent RPCs |
message | string | Status message |
registered_at | Timestamp | Server registration time |
Heartbeat
Keep the session alive. Must be sent periodically within the configured AGENT_HEARTBEAT_INTERVAL (default: 30s). Sessions that miss heartbeats are considered disconnected and cleaned up.
rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse)Request:
| Field | Type | Description |
|---|---|---|
session_id | string | Active session ID |
agent_id | string | Agent identifier |
timestamp | Timestamp | Client-side timestamp |
status | AgentStatus | IDLE, BUSY, or SHUTTING_DOWN |
metrics | map<string, string> | Optional runtime metrics |
Response:
| Field | Type | Description |
|---|---|---|
alive | bool | Whether the session is still valid |
server_time | Timestamp | Hub server time |
commands | AgentCommand[] | Commands from Hub (SHUTDOWN, RECONFIGURE) |
UnregisterAgent
Cleanly disconnect an agent. Releases the session and removes the agent from active routing.
rpc UnregisterAgent(UnregisterAgentRequest) returns (UnregisterAgentResponse)Task Management
AcquireTask
Open a server-streaming connection to receive task assignments. The server pushes tasks to the agent as they arrive.
rpc AcquireTask(AcquireTaskRequest) returns (stream TaskAssignment)Request:
| Field | Type | Description |
|---|---|---|
session_id | string | Active session ID |
agent_id | string | Agent identifier |
filter | TaskFilter | Optional filter (by trigger, project, etc.) |
Stream response (TaskAssignment):
| Field | Type | Description |
|---|---|---|
task_id | string | Task execution ID |
task_type | string | Task type |
payload | bytes | Task payload (JSON) |
context | TaskContext | Webhook context, project, agent info |
deadline | Timestamp | Must complete before this time |
StreamTasks
Bidirectional streaming for task processing. The agent receives tasks and sends status updates in the same stream.
rpc StreamTasks(stream TaskStreamMessage) returns (stream TaskStreamMessage)CompleteTask
Report successful task completion.
rpc CompleteTask(CompleteTaskRequest) returns (CompleteTaskResponse)Request:
| Field | Type | Description |
|---|---|---|
session_id | string | Active session ID |
task_id | string | Task execution ID |
result | string | Task output/result |
tokens_used | int64 | Total tokens consumed |
FailTask
Report task failure.
rpc FailTask(FailTaskRequest) returns (FailTaskResponse)Request:
| Field | Type | Description |
|---|---|---|
session_id | string | Active session ID |
task_id | string | Task execution ID |
error | string | Error message |
retriable | bool | Whether the task can be retried |
LLM Hub
Provides access to the configured LLM through the Aether Hub, subject to rate limiting.
LLMChat
Send a chat completion request through the Hub.
rpc LLMChat(LLMChatRequest) returns (LLMChatResponse)Request:
| Field | Type | Description |
|---|---|---|
session_id | string | Active session ID |
messages | Message[] | Conversation history |
system_prompt | string | Optional system prompt override |
tools | ToolDefinition[] | Optional tool definitions for function calling |
Response:
| Field | Type | Description |
|---|---|---|
content | string | LLM response text |
tool_calls | ToolCall[] | Tool calls requested by the LLM |
tokens_prompt | int64 | Prompt token count |
tokens_completion | int64 | Completion token count |
Tool Hub
Discover and execute tools registered in Aether.
ToolsList
Get available tools (built-in + MCP tools).
rpc ToolsList(ToolsListRequest) returns (ToolsListResponse)ToolsExecute
Execute a tool by name with parameters.
rpc ToolsExecute(ToolsExecuteRequest) returns (ToolsExecuteResponse)Request:
| Field | Type | Description |
|---|---|---|
session_id | string | Active session ID |
tool_name | string | Tool name (e.g., github_add_comment) |
parameters | map<string, string> | Tool parameters |
Memory Hub
Access the agent memory system for storing and retrieving information across sessions.
MemoryStore
Store a memory.
rpc MemoryStore(MemoryStoreRequest) returns (MemoryStoreResponse)Request fields:
| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Active session ID |
key | string | Yes | Memory key (unique identifier) |
content | string | Yes | Memory content |
scope | MemoryScope | Yes | TASK, AGENT, PROJECT, ORGANIZATIONAL |
ttl_seconds | int64 | No | TTL for short-term memories (0 = permanent) |
agent_id | string | No | Associate to an agent (mirrors REST) |
project_id | int64 | No | Associate to a project (mirrors REST) |
Response fields:
| Field | Type | Description |
|---|---|---|
success | bool | Whether the memory was stored |
message | string | Status or error detail |
MemoryRetrieve
Retrieve a specific memory by key.
rpc MemoryRetrieve(MemoryRetrieveRequest) returns (MemoryRetrieveResponse)Request fields: session_id, key
Response: memory object or NOT_FOUND.
MemoryList
List memories matching filters.
rpc MemoryList(MemoryListRequest) returns (MemoryListResponse)Filters: scope, agent_id, project_id, limit, offset.
MemoryDelete
Delete a memory.
rpc MemoryDelete(MemoryDeleteRequest) returns (MemoryDeleteResponse)Error semantics
UNAUTHENTICATEDfor missing/expired sessionPERMISSION_DENIEDif agent lacks access to scope/projectNOT_FOUNDfor missing keys on retrieve/deleteFAILED_PRECONDITIONfor invalid scope transitions
Notes
- Scopes mirror REST (
TASK,AGENT,PROJECT,ORGANIZATIONAL). - TTL is honored for short-lived memories; otherwise data persists in Postgres/pgvector.
- Semantic search is available via REST today; gRPC search endpoint is planned.
Messaging Hub
Inter-agent communication via NATS.
MessagingPublish
Publish a message to a NATS subject.
rpc MessagingPublish(MessagingPublishRequest) returns (MessagingPublishResponse)MessagingRequest
Send a request and wait for a reply (NATS request-reply pattern).
rpc MessagingRequest(MessagingRequestRequest) returns (MessagingRequestResponse)Authentication & Sessions
External agents authenticate using an agent_id + agent_secret pair. The secret is stored (hashed with bcrypt) on the agent record in the database.
Authentication flow:
- Set
auth_enabled = trueon the agent - Set
auth_secreton the agent (via API or seeding) - Call
RegisterAgentwith the agent_id and plaintext secret - Receive a
session_id— include it in all subsequent RPCs - Sessions expire after
AGENT_SESSION_TIMEOUTseconds without a heartbeat
Rate Limiting
The Hub enforces rate limits on LLM calls based on the agent's trust level:
| Trust Level | Default LLM Calls/Hour |
|---|---|
default | 20 (configurable via RATE_LIMIT_DEFAULT_LLM_CALLS_PER_HOUR) |
trusted | 100 (configurable via RATE_LIMIT_TRUSTED_LLM_CALLS_PER_HOUR) |
privileged | 500 (configurable via RATE_LIMIT_PRIVILEGED_LLM_CALLS_PER_HOUR) |
Rate limiting requires RATE_LIMIT_ENABLED=true and Redis configured.
Error Handling
gRPC status codes used:
| Code | Meaning |
|---|---|
OK | Success |
UNAUTHENTICATED | Invalid or expired session |
PERMISSION_DENIED | Agent lacks required trust level |
NOT_FOUND | Resource not found |
RESOURCE_EXHAUSTED | Rate limit exceeded |
INTERNAL | Server error |
UNAVAILABLE | Hub service temporarily unavailable |
Example Agent (Go)
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
pb "your-module/proto/agent/v1"
)
func main() {
conn, err := grpc.Dial("localhost:9090", grpc.WithInsecure())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
client := pb.NewAgentGatewayServiceClient(conn)
ctx := context.Background()
// Register
regResp, err := client.RegisterAgent(ctx, &pb.RegisterAgentRequest{
AgentId: "ta_leo",
AgentSecret: "my-secret",
AgentName: "Leo (TA)",
Version: "1.0.0",
})
if err != nil || !regResp.Success {
log.Fatal("registration failed")
}
sessionID := regResp.SessionId
// Heartbeat goroutine
go func() {
for {
time.Sleep(25 * time.Second)
client.Heartbeat(ctx, &pb.HeartbeatRequest{
SessionId: sessionID,
AgentId: "ta_leo",
Status: pb.AgentStatus_IDLE,
})
}
}()
// Acquire and process tasks
stream, err := client.AcquireTask(ctx, &pb.AcquireTaskRequest{
SessionId: sessionID,
AgentId: "ta_leo",
})
if err != nil {
log.Fatal(err)
}
for {
task, err := stream.Recv()
if err != nil {
break
}
// Process task...
client.CompleteTask(ctx, &pb.CompleteTaskRequest{
SessionId: sessionID,
TaskId: task.TaskId,
Result: "Technical analysis complete.",
})
}
}Example Agent (Python)
import grpc
import time
import threading
from proto.agent.v1 import agent_pb2, agent_pb2_grpc
channel = grpc.insecure_channel('localhost:9090')
stub = agent_pb2_grpc.AgentGatewayServiceStub(channel)
# Register
response = stub.RegisterAgent(agent_pb2.RegisterAgentRequest(
agent_id="ta_leo",
agent_secret="my-secret",
agent_name="Leo (TA)",
version="1.0.0"
))
session_id = response.session_id
# Heartbeat thread
def heartbeat():
while True:
time.sleep(25)
stub.Heartbeat(agent_pb2.HeartbeatRequest(
session_id=session_id,
agent_id="ta_leo",
status=agent_pb2.AgentStatus.IDLE
))
threading.Thread(target=heartbeat, daemon=True).start()
# Process tasks
for task in stub.AcquireTask(agent_pb2.AcquireTaskRequest(
session_id=session_id,
agent_id="ta_leo"
)):
# Call LLM via Hub
llm_response = stub.LLMChat(agent_pb2.LLMChatRequest(
session_id=session_id,
messages=[agent_pb2.Message(role="user", content=task.payload)]
))
# Complete the task
stub.CompleteTask(agent_pb2.CompleteTaskRequest(
session_id=session_id,
task_id=task.task_id,
result=llm_response.content
))