# Talking to a Running Agent

A `sac` agent is a long-lived process: once started, it stays alive and accepts new turns. Three transports reach into that process, ordered from external-friendliest to most internal:

Transport | When to use | Auth
--- | --- | ---
A2A sidecar — `POST /v1/sac/agents/<name>/turn` on the agent’s own port | External tools, browsers, curl, peer agents, A2A-spec consumers | none (loopback)
CLI — `sac agents send` / `tail` | Scripted flows on the same host (the one running the agent) | none
`sac listen` — `POST /v1/sac/agents/<name>/send` on the host port (7878) | Trusted orchestrators (e.g. orochi), cross-host via the existing SSH mesh | bearer token

All three end up dropping a `TurnEnvelope` on the runner’s shared inbox, so the SDK conversation is identical regardless of which door the prompt came through.

## 1. A2A sidecar — `POST /v1/sac/agents/<name>/turn`

Enable it by leaving `spec.a2a.port` at its default (`auto`) — sac claims a free port from `~/.scitex/agent-container/config.yaml`’s `a2a.port_range` (default `[19000, 19999]`) at start time and persists it in `state.db`. Operators see the assigned port via `sac agents list`. Most agents need no `a2a` block at all:

    spec:
      # a2a:
      #   port: auto         # the default; sac picks + persists
      #   port: 7901         # pin only when you need a stable external URL
      #   port: null         # disable the sidecar entirely

Per-agent ports are an internal IPC detail. External clients reach every agent through the one stable host port at `sac listen` (default `127.0.0.1:7878`). The AgentCard’s `url` field advertises that stable URL — so the URL survives restarts even when the per-agent port churns.

The per-agent sidecar then exposes the same URL shape as the host-level `sac listen`, so the same client URL works whether it routes through the host control plane or POSTs directly to the agent’s port. Per the sac/orochi contract, two symmetric namespaces serve identical handlers:

Method | Path | Purpose
--- | --- | ---
POST | `/v1/sac/agents/<name>/turn` | Canonical. Drop a prompt onto the SDK session
POST | `/v1/sac/agents/<name>/send` | Alias of `/turn` (matches `sac listen`’s verb)
GET | `/v1/sac/agents/<name>/card` | This agent’s AgentCard
POST | `/v1/a2a/agents/<name>/turn` | A2A-protocol-compat mirror of `/turn`
POST | `/v1/a2a/agents/<name>/send` | A2A-protocol-compat mirror of `/send`
GET | `/v1/a2a/agents/<name>/card` | A2A-protocol-compat mirror of `/card`
POST | `/v1/turn` | Bare shortcut (port already pins the agent)
GET | `/.well-known/agent-card.json` | A2A discovery card (built from this agent’s `spec.yaml`)
GET | `/.well-known/agent.json` | Alias of `agent-card.json` (some clients try this)
GET | `/health` | `{status: "ok"}` liveness probe

The `{name}` segment is informational — port routing has already pinned the request to one agent, so a name mismatch returns `404 {"error": "this port serves agent 'X', not 'Y'"}` as a sanity check.

### Send a turn

    curl -s --max-time 120 -X POST \
      http://127.0.0.1:7901/v1/sac/agents/ecosystem-auditor/turn \
      -H 'Content-Type: application/json' \
      -d '{"text": "Which scitex packages currently have audit-all violations?"}'

Or, since the port already identifies the agent, the bare shortcut:

    curl -s --max-time 120 -X POST http://127.0.0.1:7901/v1/turn \
      -H 'Content-Type: application/json' \
      -d '{"text": "Same question, via the shortcut."}'

Response shape (either path):

    { "reply": "scitex-stats has 2 PS-204 violations; rest are green.",
      "exit_after": false }

The runner stays attached after — subsequent POSTs reach the same SDK session (the conversation accumulates).

### Discover the agent

The AgentCard at `/.well-known/agent-card.json` is auto-generated from the agent’s `spec.yaml` (see `spec-reference.md` for the field-to-card mapping). Browsers and A2A-spec discovery clients read it directly:

    curl -s http://127.0.0.1:7901/.well-known/agent-card.json | python3 -m json.tool

The card’s `url` field advertises `<base>/v1/sac/agents/<name>` — exactly the path the sidecar serves — so a client following the advertised URL hits a working endpoint.

### One-shot vs follow-up

The HTTP body accepts an `"exit_after"` flag:

    { "text": "Reply DONE and exit.", "exit_after": true }

When `true`, the runner shuts down after this turn. Default is `false` (stay alive for more turns).

## 2. CLI — `sac agents send` / `sac agents tail`

The same-host, no-network path:

    # Send a turn — output is just the ack; the reply lands on session.jsonl
    sac agents send ecosystem-auditor "Which packages have uncommitted changes?"

    # Read the latest assistant turns
    sac agents tail ecosystem-auditor -n 5

    # Or stream as structured JSON (one envelope per line)
    sac agents tail ecosystem-auditor -n 5 --json

This is the right transport for shell scripts driving an agent locally — no port, no JSON, no token.

## 3. `sac listen` — host-level HTTP control plane

`sac listen` boots a host-wide bearer-auth HTTP server (default port `7878`, loopback only). Cross-host orchestrators reach it through the existing SSH mesh; same-host orchestrators speak to it directly.

    # Start the listen server (one per host; sac respects existing instance)
    sac listen &

    TOKEN=$(cat ~/.scitex/agent-container/tokens/listen-$(hostname).token)

    # Send a turn through the control plane
    curl -s -X POST http://127.0.0.1:7878/v1/sac/agents/ecosystem-auditor/send \
      -H "Authorization: Bearer $TOKEN" \
      -H 'Content-Type: application/json' \
      -d '{"type":"prompt","prompt":"Same question, via sac listen."}'

The full host-level surface (mirrored at `/v1/a2a/...` for A2A-spec consumers):

Method | Path | Purpose
--- | --- | ---
GET | `/v1/sac/health` | `{ok, version, host}`
GET | `/v1/sac/agents` | List all agents on this host
GET | `/v1/sac/agents/<name>/status` | Full agent state
GET | `/v1/sac/agents/<name>/tail?since=...&follow=...` | SSE stream of `session.jsonl`
POST | `/v1/sac/agents/<name>/send` | Send prompt or interrupt key
GET | `/v1/sac/agents/<name>/card` | AgentCard for this agent
POST | `/v1/sac/agents` | Create + start from an inline spec
DELETE | `/v1/sac/agents/<name>` | Stop the agent

`send` accepts both turn types:

    // prompt turn
    { "type": "prompt", "prompt": "Your question here", "options": { ... } }

    // key / interrupt
    { "type": "key", "key": "ESC" }

## Cross-host: `sac --on <peer>`

`sac --on <peer> agents send ...` dispatches the call across hosts via the peer registry’s SSH mesh. The remote `sac` does the local send; output streams back. Same prompt-text contract.

    sac --on gpu-box agents send researcher "Resume training and tail the logs."

## Forwarding to external A2A (`kind: AgentProxy`)

Sometimes the agent on the other end isn’t a sac agent — it’s a peer A2A endpoint hosted somewhere else (a hosted service, a peer fleet, a contracted vendor). Wrapping it in a `kind: AgentProxy` agent lets the rest of sac (`sac agents send`, `sac listen`, the AgentCard discovery surface) treat it the same as a local SDK agent.

The proxy agent has no Claude SDK; it just forwards `POST /v1/turn` to its configured `spec.proxy.upstream` and re-projects the upstream AgentCard at its own `/.well-known/agent-card.json` so peers see one consistent skill list.

    apiVersion: scitex-agent-container/v3
    kind: AgentProxy

    spec:
      runtime: apptainer
      apptainer: { image: ~/.scitex/agent-container/containers/sac-proxy.sif }
      proxy:
        upstream: https://peer.example.com
        trust: local-mesh
        redact: [ANTHROPIC_API_KEY, sk-]
        timeout_s: 30.0
      a2a: { port: 7905 }

See `spec-reference.md` § `kind: AgentProxy` and `examples/agents/proxy-agent/` for the full reference.

## Picking a transport

- A2A-spec client / browser / curl → POST to the URL the AgentCard advertises (`<base>/v1/sac/agents/<name>/turn`). Loopback only; no auth.

- Shell script on the same host → `sac agents send` + `tail`. No port, no JSON, no token.

- Another agent, on the same host → A2A sidecar; agents have `httpx` in the SIF. Use the canonical URL or the `/v1/turn` shortcut — both work.

- Orchestrator (orochi, custom) → `sac listen` (port 7878) with the bearer token; same `/v1/sac/agents/<name>/...` path shape, cross-host via the existing SSH mesh.

- External A2A peer (hosted service, vendor) → wrap as a `kind: AgentProxy` agent so the rest of sac treats it the same.

Pick the most external transport that meets your needs — every layer above the inbox is a thin wrapper, so there’s no functional difference once the turn lands. The URL shape is identical on both the per-agent sidecar and `sac listen`, so the same client code works against either.

## See also

- `spec-reference.md` — the YAML knobs (`spec.a2a.port`, `spec.listen.port`)

- `how-sac-works.md` — the architecture diagram showing where each transport hooks in

- `sac-and-orochi.md` — how orochi consumes `sac listen` across hosts