9aa5559e45
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
109 lines
4.4 KiB
Markdown
109 lines
4.4 KiB
Markdown
# Assistant — the orchestrating agent
|
|
|
|
The single entry point for users. Understands a request, consults the
|
|
specialists that can settle it, and composes one answer.
|
|
|
|
**Framework:** LangGraph · **Mode:** conversational · **Client id:**
|
|
`classic-models-agent-assistant`
|
|
|
|
## It holds nothing, and that is enforced by the realm
|
|
|
|
This is the component that sees raw user text, so it is the prompt-injection
|
|
target. It has no MCP server, no domain scope, and no data of its own. That is
|
|
not a convention in the prompt — it is what the realm grants:
|
|
|
|
```
|
|
OBTAINED classic-models-agent-catalogue-audience aud=[classic-models-agent-catalogue]
|
|
refused mcp-catalog-audience → Invalid scopes
|
|
refused mcp-sales-audience → Invalid scopes
|
|
refused catalog-api-audience → Invalid scopes
|
|
```
|
|
|
|
An injection that succeeds against this agent finds no path to a domain,
|
|
because there is none to find. Its two optional scopes are one specialist and
|
|
the inference endpoint — inference is not a domain, and the agent has to reason.
|
|
|
|
Adding a specialist is an entry in `specialists.py` plus an audience scope
|
|
granted in the realm. Add the entry without the grant and every call to it fails
|
|
at the token exchange with `Invalid scopes`, loudly, before any data moves.
|
|
|
|
## The chain
|
|
|
|
Four hops, `sub` unchanged throughout — which is what makes the end user's own
|
|
roles apply at the far end:
|
|
|
|
```
|
|
1. user sub=pm-user azp=librechat aud=[…-agent-assistant]
|
|
2. assistant→cat sub=pm-user azp=classic-models-agent-assistant aud=[…-agent-catalogue]
|
|
3. cat→mcp sub=pm-user azp=classic-models-agent-catalogue aud=[mcp-catalog, token-exchange-broker]
|
|
4. MCP call → 200
|
|
```
|
|
|
|
`azp` names the last agent in the chain, not the first. Keycloak accepts
|
|
`actor_token` and emits no `act` claim, so an orchestrator's identity does not
|
|
survive a second exchange — the delegation is visible in the traces, not in the
|
|
token.
|
|
|
|
## What it does with an answer
|
|
|
|
A specialist returns an answer, never data. The orchestrator composes; it does
|
|
not aggregate records it was not given.
|
|
|
|
Two behaviours worth naming, both verified end to end:
|
|
|
|
- **Out of remit → it says so.** Asked for a company-wide revenue figure, it
|
|
reports that no specialist can settle it rather than answering from what the
|
|
model knows. An invented answer is indistinguishable from one the estate gave.
|
|
- **A held write is relayed verbatim.** Catalogue parks a proposed write in
|
|
`input-required`; this agent passes that message through unchanged and stops.
|
|
The user's `approve` comes back through the same conversation and reaches the
|
|
specialist, which owns the decision. The orchestrator never approves on
|
|
anyone's behalf.
|
|
|
|
## The graph
|
|
|
|
`plan → delegate → plan → …` until the model stops calling tools. One tool per
|
|
specialist, described by what it *decides*. Delegations in the same step run
|
|
concurrently — that is the point of fanning out at all — and are capped by
|
|
`ASSISTANT_MAX_DELEGATIONS` (8), counted in calls rather than model turns,
|
|
because one specialist consulted twice for the same question is the loop worth
|
|
catching.
|
|
|
|
The graph is rebuilt per turn: its tools carry the user's token, and a graph
|
|
held across turns would be a token held across turns.
|
|
|
|
## Running it
|
|
|
|
```
|
|
uv venv && uv pip install -e '.[dev]'
|
|
ASSISTANT_CLIENT_SECRET=… ASSISTANT_CA_BUNDLE=… \
|
|
ASSISTANT_URL_CATALOGUE=http://localhost:8099/ \
|
|
python -m assistant_agent
|
|
```
|
|
|
|
`ASSISTANT_URL_<NAME>` overrides a specialist's address, for running outside the
|
|
cluster against a port-forward. In the cluster nothing sets it.
|
|
|
|
macOS resolves every `.local` name through mDNS before falling back to
|
|
`/etc/hosts`, at a flat ~5s per host. `ASSISTANT_HTTP_TIMEOUT` defaults to 60s
|
|
for that reason; in the cluster the names resolve instantly.
|
|
|
|
## Deployment
|
|
|
|
```
|
|
gitea.sttlab.eu/stt/classic-models-agent-assistant:v0.2.1
|
|
```
|
|
|
|
`k8s/` is what runs, `dev/` is the Flux plumbing. Two manual applies close the
|
|
loop once — the `agents` namespace and the `sttlab-ca` ConfigMap already exist,
|
|
created by the catalogue agent's repo, and are deliberately not declared here so
|
|
two Kustomizations do not fight over pruning them.
|
|
|
|
```
|
|
kubectl apply -f dev/gitrepository.yaml
|
|
kubectl apply -f dev/bootstrap-kustomization.yaml
|
|
```
|
|
|
|
`replicas: 1` is a constraint: conversation history lives in the process, and a
|
|
specialist's approval round trip passes back through here.
|