# Agent Gateway

`agent-gateway` connects message sources to long-lived AI agent sessions. It accepts normalized Lark messages and programmatic Application messages, persists Session and Task state in MySQL, executes tasks through a configured Agent Provider, and sends asynchronous task events back to the source system.

In the current Lark integration, responsibilities are split as follows:

- Ticket Manager owns Lark event ingress, message filtering, user identity resolution, card rendering, and replies.
- Agent Gateway owns message-to-session mapping, task deduplication and lifecycle, Agent Provider calls, and task event delivery.

Agent Gateway does not require Lark application credentials.

Applications can submit messages directly through the gRPC or HTTP `ProcessMessage` API. Application Sessions are grouped by `source instance + session_key`; repeated `message_id` values are deduplicated within that Session. The first request selects the optional `context.agent_id` or the configured default, and that Agent remains fixed for the Session.

## Request flow

```text
Lark
  -> Ticket Manager
  -> Agent Gateway ProcessLarkMessage (gRPC)
  -> MySQL Session / Task
  -> Codex CLI or Codex app-server
  -> Agent Gateway task event (HTTP)
  -> Ticket Manager
  -> Lark reaction / card
```

For Lark messages, a Session is grouped by `source instance + chat_id + anchor_message_id`. The root message ID is used as the anchor when present; otherwise the current message ID is used. Repeated submissions of the same `source_message_id` in a Session return the existing Task and are not executed again.

## Interfaces

The service starts both a plaintext gRPC server and a gRPC-Gateway HTTP server.

Main APIs:

- `ProcessMessage`: `POST /api/v1/agent-gateway/messages:process`
- `ProcessLarkMessage`: `POST /api/v1/agent-gateway/lark/messages:process`
- `ListSessions`: `GET /api/v1/agent-gateway/sessions`
- `GetSession`: `GET /api/v1/agent-gateway/sessions/{session_id}`
- `ListSessionTasks`: `GET /api/v1/agent-gateway/sessions/{session_id}/tasks`
- `DeleteSession`: `DELETE /api/v1/agent-gateway/sessions/{session_id}`
- `ListAgents`: `GET /api/v1/agent-gateway/agents`
- Project management: `/api/v1/agent-gateway/agents/{agent_id}/projects/...`
- Route configuration: `GET /api/v1/agent-gateway/agents/{agent_id}/routes`
- Secret configuration and cached status: `GET /api/v1/agent-gateway/agents/{agent_id}/secrets`

AgentService exposes these lists as `ListAPIProxyRoutes` and `ListSecretStoreSecrets`.
Route and Secret lists require `X-Operator` and a configured sidecar. They return all
configured resources in stable name/ID order without fetching or refreshing credentials.
Secret sources include static configuration and logical references, never resolved values.
Secret status includes `lastError` (Fetch error details, cleared on success) and
`lastAttemptAt` (the start of the latest Fetch attempt).
Middleware and SecretSource configurations use typed Protobuf `oneof` branches; HTTP
responses expose the selected branch (for example, `credentialAuth` or `awsSecretsManager`).
The sidecar exposes these lists through `APIProxyService.ListRoutes` and
`SecretStoreService.ListSecrets` over gRPC. Upgrade the sidecar before the gateway.

The complete contract is in [`packages/contracts/agent-gateway`](../../packages/contracts/agent-gateway), including the generated [OpenAPI document](../../packages/contracts/agent-gateway/agent-gateway.swagger.yaml).

`ProcessMessage` and `ProcessLarkMessage` return immediately after the Task has been persisted and queued. Execution results are delivered through the configured `gateway.eventListeners` endpoint as `TaskEvent` JSON. Event JSON uses protobuf field names such as `task_id`, `session_id`, and `session_source`; the receiver must return HTTP 2xx.

Callbacks to matching listeners are started asynchronously. Task processing does not wait for delivery, and starting, commentary, and finished callbacks are not guaranteed to arrive in order.

Both message APIs carry `instance_id`, `working_directory`, `agent_id`, and `agent_model` in a shared `context` object. `ProcessMessage` also requires an `ApplicationMessage` containing `message_id`, `session_key`, `sender_email`, and `content`. An empty `agent_id` uses the default Agent when creating a Session. A later request may omit `agent_id` or repeat the Session Agent, but cannot switch an existing Session to another Agent. If a Session already has an active Turn, the message follows the same Steering behavior as Lark messages.

`context.agent_model` optionally overrides the model for each new turn. Both Codex app-server and CLI use the request model first, then the Agent configuration model; when both are empty, the underlying runtime decides its default behavior, including any retained thread state. The request model is persisted on the Task and restored for pending execution. It does not change Gateway Session defaults or deduplication keys. Duplicate messages retain the original Task and model.

When a Session has an active turn, steering ignores the requested model and continues with the active turn's model. The Task still records the requested `agent_model`; this field is not evidence of the actual runtime model. Provider steering errors and refusals retain their existing behavior. Invalid models for new turns are reported through Task failure events.

Example HTTP request body (replace `<model-id>` with an available model):

```json
{
  "context": {
    "instanceId": "ticket-manager",
    "workingDirectory": "/projects/workspace-1",
    "agentId": "codex",
    "agentModel": "<model-id>"
  },
  "message": {
    "messageId": "step-1",
    "sessionKey": "handler:session-1",
    "senderEmail": "user@example.com",
    "content": "Analyze this issue"
  }
}
```

The following commands can also be sent as message text:

- `/status`: show the current Session's Agent status. `default_model` is the configured model, not the actual model of the current turn; empty means no model is configured.
- `/agents`: list registered Agents and the current default.

## Configuration

Copy the checked-in example before local use:

```bash
cd services/agent-gateway
cp config.example.yaml config.local.yaml
```

Configuration is merged in this order, with later sources overriding earlier ones:

1. Defaults declared on the Go configuration structs.
2. The YAML file supplied through `--config`.
3. YAML stored in AWS Secrets Manager when `secret.rootConfigSecretName` is set.

The main configuration groups are:

| Group | Purpose |
|---|---|
| `database` | MySQL connection used for Session and Task persistence. |
| `rpcServer` | Plaintext gRPC listener. Default: `:9090`. |
| `rpcGatewayServer` | HTTP listener and the address it uses to reach the local gRPC server. Defaults: `:8080` and `:9090`. |
| `gateway.eventListeners` | HTTP Task Event receivers keyed by an arbitrary listener name. Each listener has one or more `sources`, whose types may be `lark` or `application`. |
| `agentRuntime` | Default Agent and named Agent Providers. Supported provider types are `codex_cli` and `codex_app_server`. |
| `taskService` | Maximum number of concurrently executing tasks. |
| `clients.core` | Core Server plaintext gRPC address used for Audit RPCs. |

For a Ticket Manager integration, these values must agree:

- Agent Gateway `gateway.eventListeners.<name>.sources[].instanceID`
- Ticket Manager `services.agent.instanceId`

Ticket Manager's `clients.agentGateway.address` must point to Agent Gateway's `rpcServer.address`.

### Agent Providers

`codex_cli` starts `codex exec` as a subprocess. The configured `home` and `codexHome` are passed as `HOME` and `CODEX_HOME`, so that directory must already contain valid Codex authentication. The caller supplies `working_directory` for every execution; Ticket Manager configures its value through `services.agent.workingDirectory`.

Verify the selected Codex home before starting the service:

```bash
CODEX_HOME="$HOME/.codex" codex login status
```

`codex_app_server` connects to an already-running Codex app-server through `unix://` or `ws://`. Agent Gateway does not start that process. A Unix endpoint may use `~`, for example `unix://~/.codex/app-server.sock`. If the WebSocket disconnects, Agent Gateway reconnects and repeats the app-server handshake; the interrupted turn fails and is not replayed.

Every configured Agent Provider is initialized during startup. Only configure a `codex_app_server` provider when its endpoint is reachable.

Every key under `agentRuntime.agents` registers an Agent. An Agent supports Project management when it has an optional `sidecar` block with an Agent Sidecar plaintext gRPC `endpoint` and request `timeout`. Sidecar connections are created without requiring the Sidecar to be online at Gateway startup. Project management requests are routed by `agent_id` and are never retried automatically.

Multiple logical Agents may target the same Codex app-server while pinning different models. Keep the general-purpose Agent as `agentRuntime.defaultAgent`, and let specialized application flows select their Agent with `ProcessMessage.context.agent_id`. Model-specific defaults such as reasoning effort remain owned by the Agent Codex model catalog.

`ListAgents` returns static configuration-derived metadata for every key under `agentRuntime.agents`, including provider, Project management support, and whether it is the default Agent. It does not probe Sidecar health and does not require `X-Operator` at the service layer.

`agentRuntime.defaultAgent` must match a registered Agent ID. The default Agent is fixed at startup; changing it requires a service restart. Deployments that load the root configuration from an external Secret must move `taskService.defaultAgent` to `agentRuntime.defaultAgent` before deploying this version. The old key is not supported.

All Project management RPCs require the trusted `X-Operator` metadata injected by Kong. Successful Project and file mutations are synchronously recorded through Core Server Audit; read operations and failed Sidecar mutations are not audited.

## Run locally

Prerequisites:

- Go toolchain used by this repository.
- A reachable MySQL database that already exists; tables are migrated automatically.
- Either an authenticated Codex CLI or a reachable Codex app-server.
- An HTTP receiver for Task Events. Ticket Manager exposes `POST /api/v1/ticketmanager/agent-gateway/events`.
- A reachable Core Server gRPC endpoint for mutation audit records.

Edit `config.local.yaml`, then validate configuration and storage initialization:

```bash
cd services/agent-gateway
go run ./cmd/agent-gateway --config config.local.yaml --check
```

A successful check prints:

```text
agent-gateway config and storage are ready
```

Start the service:

```bash
go run ./cmd/agent-gateway --config config.local.yaml
```

When Ticket Manager and Agent Gateway run on the same host, their default gRPC and HTTP ports may overlap. Override one service's listener ports and update Ticket Manager `clients.agentGateway.address` accordingly.

## HTTP example

The normal integration uses the generated gRPC client from Ticket Manager. The HTTP gateway can be used for a smoke test:

```bash
curl -X POST http://127.0.0.1:8080/api/v1/agent-gateway/lark/messages:process \
  -H 'Content-Type: application/json' \
  -d '{
    "instanceId": "ticket-manager",
    "message": {
      "messageId": "om_example",
      "chatId": "oc_example",
      "chatType": "p2p",
      "messageType": "text",
      "content": "{\"text\":\"Summarize the current repository\"}",
      "senderEmail": "user@example.com"
    }
  }'
```

The `instanceId` must match a configured event listener source. Reusing the same `messageId` for the same source Session returns the existing Task with a duplicated disposition.

## Development

From `services/agent-gateway`:

```bash
make build
go test ./...
```

After changing protobuf contracts or Go interfaces:

```bash
make generate
```

Contract generation reads from [`packages/contracts/agent-gateway`](../../packages/contracts/agent-gateway) and writes generated Go SDK code under [`packages/sdk-go/contracts`](../../packages/sdk-go/contracts).

## Runtime notes

- Startup runs database migrations before accepting requests.
- Tasks persisted in the received state are resumed after a restart.
- `taskService.maxConcurrency` limits concurrent Agent Provider execution, not HTTP or gRPC request handling.
- Multiple listeners may be registered for the same source; each receives the matching Task Events.
- Application listeners match `MESSAGE_SOURCE_TYPE_APPLICATION + instance_id`; callers without a matching listener are rejected before Task creation.
- Task Event delivery currently performs a direct HTTP POST. Delivery failures are logged but do not change the completed Task result.
- gRPC server shutdown uses a five-second timeout; background services are stopped afterward.
