Overview

Agent is an autonomous ops helpdesk: you give it a free-text question about your systems, and it investigates using your own service registry, tickets, runbooks, and long-term memory, then either resolves the situation itself (tier-1) or opens an approval and waits for a human (tier-2). It ships as two independent pieces:

  • agent-rs — the Rust core engine. Use it as a standalone HTTP service (agent-server), or embed it directly in another Rust program via the agent-sdk crate.
  • agent-go — a Go client library plus agentctl, a CLI for scripting against a running instance.

Quick start

Requires a Rust toolchain (1.75+) and, optionally, Go (1.21+) if you want the CLI.

cp .env.example .env
# edit .env — set GROQ_API_KEY or OPENAI_API_KEY

cd agent-rs
cargo run -p agent-server
# listening on 0.0.0.0:8000

From another terminal:

curl http://localhost:8000/services

curl -X POST http://localhost:8000/query \
  -H 'Content-Type: application/json' \
  -d '{"query": "is payments healthy?"}'

Configuration

Every setting is an environment variable — no config file to manage.

VariableDefaultMeaning
GROQ_API_KEY—Checked first. Enables the LLM reasoner.
OPENAI_API_KEY—Used if GROQ_API_KEY isn't set.
LLM_MODELopenai/gpt-oss-20bModel name sent to the chat-completions endpoint.
LLM_BASE_URLGroq's endpointAny OpenAI-compatible base URL.
DATABASE_PATHdevops_agent.dbSQLite file. Use :memory: for ephemeral runs.
KNOWLEDGE_DIRSknowledge_sources:runbooksColon-separated dirs scanned for JSON/CSV/Markdown at startup.
BIND_ADDR0.0.0.0:8000agent-server only.
ALLOWED_ORIGINS(none)agent-server only. Comma-separated extra CORS origins allowed to call the API directly from a browser, e.g. http://localhost:3000,https://my-frontend.example.com.
ALLOW_ANY_ORIGINfalseagent-server only. Set true to accept requests from any origin — convenient for a public demo, not recommended once real credentials are involved.

API reference

All request/response bodies are JSON.

PathMethodNotes
/healthzGETLiveness check.
/servicesGETList every registered service.
/ticketsGETList every ticket.
/tickets/{id}GETFetch one ticket.
/queryPOSTBody: {"query": "..."}. Returns the tiered decision.
/query/{id}GETFetch a previously submitted query's status.
/approvalsGETList pending (undecided) approvals.
/approvals/{id}GETFetch one approval's full detail.
/approvals/{id}/decidePOSTBody: {"decision": "approve"|"reject", "approver": "name"}.
/audit-logsGETMost recent audit trail entries.
/memoryGET / POSTList, or add, a long-term memory fact.
/memory/{id}GETFetch one memory fact.

Embedding directly (Rust)

Skip HTTP entirely and call the engine as a library:

use agent_sdk::{Agent, AgentConfig};

let agent = Agent::bootstrap(AgentConfig::from_env()).await?;
let response = agent.submit_query("is payments healthy?").await?;

if let Some(approval_id) = response.approval_id {
    agent.decide_approval(approval_id, "approve", Some("alice"))?;
}

The AgentConfig builder lets you override the database path and knowledge directories without touching environment variables:

let config = AgentConfig::default()
    .with_database_path(":memory:")
    .with_knowledge_dirs(vec!["./my-runbooks".into()]);

Go CLI reference (agentctl)

agentctl health
agentctl services
agentctl tickets
agentctl query "<free-text query>"
agentctl query-status <id>
agentctl approvals list
agentctl approvals get <id>
agentctl approvals decide <id> --decision approve|reject [--approver <name>]
agentctl memory add --source <src> --fact "<fact>" [--context "<ctx>"]
agentctl memory list
agentctl audit-logs
agentctl wait [--timeout 30s]

Every command accepts --url, or reads $AGENT_URL. Output is pretty-printed JSON on stdout; failures exit non-zero with the backend's error message on stderr — safe to use in shell scripts and CI gates.

Deployment

Docker (backend + frontend together):

docker compose up --build

Bare binary + systemd: build with cargo build --release -p agent-server, copy the binary to the target host, and use the unit file in deploy/systemd/agent-server.service as a starting point — it includes an agentctl wait pre-start check.

Behind a reverse proxy: the server has no built-in TLS termination; put it behind nginx/Caddy/your load balancer, same as you would any other backend service.

Frontend: the frontend/ directory is now a small Express app (frontend/server.js), not a static-only site. Run npm install && npm start inside it, or let docker compose build it. It serves the site and proxies live-demo API calls to whichever backend URL is configured — see frontend/.env.example. Because the proxy hop happens server-to-server, it works against any deployed agent-server URL without needing that server's CORS list to know about this frontend's origin ahead of time.

Cross-origin access from other frontends: if you build a different UI that calls agent-server directly from the browser (rather than through the proxy above), add its origin to ALLOWED_ORIGINS (comma-separated) when starting agent-server, or set ALLOW_ANY_ORIGIN=true for a public demo deployment where every origin should be allowed.

FAQ

Does this replace my SQLite database from the old Python service?
No migration needed — same schema, same tables. Point DATABASE_PATH at your existing file, or start fresh.

What happens if the LLM key is missing?
agent-server refuses to start. agent-sdk, embedded, returns a clear error from submit_query instead — your calling code decides what to do.

Can I use a local model?
Yes — set LLM_BASE_URL to any OpenAI-compatible endpoint (vLLM, Ollama's OpenAI shim, etc).

How do I improve retrieval quality?
The default embedder is a dependency-free lexical fallback. Implement the Embedder trait in agent-core::retrieval to plug in a real embedding model — one function, no other code changes.