Agentic search
Agentic search runs a configured reasoning loop over one or more namespaces and
returns the same row shape as every other search endpoint. A model reads the query,
fans out diverse phrasings in parallel via layer’s
scatter/gather, ranks the candidates for relevance,
and returns a ranking fused from both signals. It is a better-ranked result set, not
a generated answer — the response is the
federated query shape, so any client that
reads /v2/query reads this with no changes.
The endpoint names a configured Agent in the path.
The model, the turn budget, the indices, and the output shaping are bound on that
resource, so the request body is just a query, an optional query embedding, and a
result count.
POST /v2/agents/{name}/query
Request
response = await client.query_agent("support-search", {
"query": "auth errors after the june upgrade",
"top_k": 20,
})response, err := client.QueryAgent(ctx, "support-search", &hevlayer.AgentQueryRequest{
Query: "auth errors after the june upgrade",
TopK: 20,
})const response = await client.queryAgent("support-search", {
query: "auth errors after the june upgrade",
top_k: 20,
});curl -X POST "$LAYER_GATEWAY_URL/v2/agents/support-search/query" \
-H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "auth errors after the june upgrade",
"top_k": 20
}' | Field | Purpose |
|---|---|
query | The natural-language query. The agent reformulates it; you do not pre-shape it into a route expression. |
vector | Optional. The embedding of query, used for the agent’s semantic recall leg. See Bring your own embedding. |
top_k | Rows to return after fusion. |
The model, fan-out, fusion weighting, and output are bound on the
Agent, which keeps the request trivial and makes the
agent the single source of truth for what a call costs and what it can read. query
and vector are the only request inputs, and they are data, not config — there are
no per-request overrides of the agent’s configured behavior.
Bring your own embedding
The agent fans out for recall over both routes: a lexical leg on your query text and
a semantic leg on a query vector. Layer never embeds query text — you supply the
vector, the same bring-your-own-embedding contract as
/v2/query. Pass it as vector:
curl -X POST "$LAYER_GATEWAY_URL/v2/agents/support-search/query" \
-H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "auth errors after the june upgrade",
"vector": [0.0123, -0.0456, 0.0789],
"top_k": 20
}'
vector is the embedding of query. The agent uses this one vector for every
planned semantic leg — it does not embed the reformulated phrasings, so the vector
carries the query’s semantic intent while the text reformulations broaden lexical
recall. Embed query with the same model your indices were embedded with, and match
their dimensionality.
Omit vector and the agent’s semantic legs fall back to the lexical route: recall
runs on the reformulations alone, which is the right behavior when an index has no
vector column or you have no embedder on the client.
Response
The response is the federated query shape:
rows each carrying $namespace, $rank, and a native $score/$dist, plus a
merge block and a namespaces block. The merge names the dual-score fusion.
{
"rows": [
{ "id": "T-4821", "$namespace": "tickets", "$rank": 1, "$score": 9.7, "subject": "SSO login fails after upgrade" }
],
"merge": { "method": "weighted-rrf", "route": "dual-score" },
"namespaces": [
{ "namespace": "tickets", "stable_as_of": 1747300000123, "matched": 20 }
]
}
By default the response is byte-shape-identical to a
federated query: a client cannot tell whether a
reasoning loop produced it. Set output.provenance
on the agent to surface the scores.
Provenance and trace
With provenance on, the response gains an agent echo and each row carries a
$agent field with both scores:
{
"rows": [
{ "id": "T-4821", "$namespace": "tickets", "$rank": 1, "$score": 9.7,
"$agent": { "retrievalScore": 3, "relevanceScore": 0.92, "query": "authentication failure post-upgrade", "queryIndex": 0 } }
],
"merge": { "method": "weighted-rrf", "route": "dual-score" },
"namespaces": [
{ "namespace": "tickets", "stable_as_of": 1747300000123, "matched": 20 }
],
"agent": {
"turns": 2,
"deadlineHit": false,
"recallDepth": 50,
"relevanceWeight": 0.6,
"queries": [
{ "namespaces": ["tickets"], "rankBy": "hybridText",
"query": "authentication failure post-upgrade",
"filters": { "created_at": { "$gte": "2026-06-01" } } }
]
}
}
| Field | Meaning |
|---|---|
$agent.retrievalScore | The row’s rank within the leg that first surfaced it — the per-query position (1-based; lower is better), not the fused-pool rank. With fanout > 1 a row can appear in several legs; this records the first leg’s rank. The fused recall signal is computed separately (RRF over every leg the row appeared in). |
$agent.relevanceScore | The model’s graded relevance for the row (the precision signal). |
$agent.query | The planned variant that first surfaced the row. |
$agent.queryIndex | Zero-based index of that planned variant in agent.queries. |
agent.turns | Model turns the call spent. |
agent.deadlineHit | True when the deadline ended the request early and returned the best ranking so far. |
agent.queries | The planned variants: route, reformulated text, and inferred filters. |
With output.trace the agent echo also
carries the full reasoning trace. The trace is written to the
search-history record whether or not it is echoed, so
agentic and plain searches share one surface for evaluation.
Auth
Auth follows the same model as the other API endpoints; multi-namespace queries
follow federated query auth behavior. A
minted key additionally needs an agent.<name> entitlement on its
ApiKey to invoke the agent.
Validation
| Condition | Status |
|---|---|
{name} is not a known, Ready agent | 404 |
The minted key lacks the agent.<name> entitlement | 403 |
| A bound index is outside the key’s namespace grant | 403 |
query is empty | 422 |
vector is present and its dimensionality does not match the bound indices’ vector schema | 422 |
Deadline hit with onDeadline: bestEffort | 200, best ranking so far, agent.deadlineHit: true |
Deadline hit with onDeadline: error | 504 |
| The provider is unreachable on both primary and fallback | 502 |
Configuration
An Agent is a saved agentic-search configuration as a resource. It binds an
inference model, a turn budget, and a set of indices
to a name, so a caller searches with
POST /v2/agents/{name}/query and sends only a query string.
Everything that decides what a call costs and what it can read lives on the
resource, not in the request.
An agent adds no retrieval primitive. Its only tool is the
federated query; it sits one level above the
Auto router, using a model to reformulate the
query, fan out for recall, and score the candidates for relevance. What it returns
is the same row shape as every other search endpoint — a better-ranked result set,
not a generated answer.
An Agent has two authoring surfaces that round-trip
through one schema: kubectl get agent -o yaml and
client.agent("support-search").apply() are two spellings of the same object.
apiVersion: hevlayer.com/v1alpha1
kind: Agent
metadata:
name: support-search
namespace: layer
spec:
model:
provider: openrouter
name: anthropic/claude-haiku-4-5
fallback: anthropic/claude-sonnet-4-6 # optional; used on primary timeout/error
apiKeySecretRef:
name: openrouter
key: credential
budget:
deadlineMs: 60000
onDeadline: bestEffort
indices: [docs, tickets]
retrieval:
fanout: 8
recallDepth: 50
rankBy: auto
relevanceWeight: 0.6
output:
provenance: false
trace: false
status:
phase: Ready
conditions:
- type: SecretResolved
status: "True"
- type: ModelReachable
status: "True"
- type: IndicesResolved
status: "True"
Everything the request omits is bound on the Agent
resource: the model and its credential, the deadline, the indices, the fan-out and
fusion weighting, and the output shaping. Save the Agent manifest as agent.yaml
and apply it to the gateway’s Kubernetes namespace:
kubectl apply -n layer -f agent.yaml
kubectl get agent support-search -n layer -o yaml
The SDK query methods invoke an existing Agent; configure it through Kubernetes.
Model
| Field | Purpose |
|---|---|
provider | Inference backend. openrouter. |
name | Model id passed to the provider, e.g. anthropic/claude-haiku-4-5. |
fallback | Optional model used when the primary times out or errors. |
apiKeySecretRef | Secret holding the provider credential (name, key). The token is never inline on the resource — the same rule the ApiKey and VectorStore CRDs follow. |
Budget
| Field | Purpose |
|---|---|
deadlineMs | Wall-clock deadline for the whole request. |
onDeadline | bestEffort (default) returns the best ranking the agent has when the deadline hits; error fails the request instead. |
Indices
indices is the set of namespaces the agent searches, passed to the
federated query as its namespaces. The operator
checks that the list is present and every entry is a non-empty string, surfaced
as the IndicesResolved condition; it does not currently confirm each entry
names a namespace that exists. The agent reads each listed namespace under the
caller’s credential, so a caller only reaches the namespaces its own key grants.
Retrieval
All retrieval behavior is expressed against the federated query — nothing new reaches the upstream.
| Field | Default | Purpose |
|---|---|---|
fanout | 8 | Query reformulations the agent issues, run in parallel via layer’s scatter/gather and merged for recall. Higher fan-out trades latency for recall. |
recallDepth | 50 | Candidates gathered for ranking before top_k, at least top_k. Bounds how much the model reads. |
rankBy | auto | Default route per leg: auto, hybridText, or semantic. A semantic leg needs a query vector, which the caller supplies on the request (vector) — layer never embeds query text; see Agentic search. Without a supplied vector, semantic legs fall back to the lexical route. |
relevanceWeight | 0.6 | Weight of the relevance score against the recall score in the final ranking. |
Output
| Field | Purpose |
|---|---|
provenance | When true, each row carries a $agent field with its retrievalScore and relevanceScore, and the response gains a top-level agent echo. |
trace | When true, the agent echo also carries the full reasoning trace. |
Default off, the response is byte-for-byte the federated query shape: a client cannot tell a reasoning loop produced it.
Status
status carries health and validation only; latency, turn counts, and spend go to
metrics and history, never into etcd.
| Condition | Meaning |
|---|---|
SecretResolved | model.apiKeySecretRef exists and is readable. |
ModelReachable | the provider answered with the bound credential. |
IndicesResolved | spec.indices is non-empty and every entry is a non-empty string. (It does not yet verify each entry is a known namespace.) |
| Phase | Meaning |
|---|---|
Ready | Resolvable and callable. |
Degraded | Reachable but a condition is failing — calls may fall back or error. |
Invalid | A required field or reference does not resolve; calls are refused. |
kubectl get agent print columns: MODEL, INDICES, PHASE.
Edits are picked up shortly after they apply: the gateway resolves agents — spec,
provider credential, and per-index schema — into memory and refreshes on a periodic
tick, so a kubectl apply lands within the refresh interval rather than instantly.
Naming
Agent names are cluster-unique at the gateway because callers address agents by name only. If two namespaces define the same Agent name, the gateway sorts by agent name and namespace, keeps the lexicographically later namespace, and logs a warning naming both namespaces. Keep Agent names unique across the cluster.
Observability
Deadline-hit rate, provider latency, and token usage per agent export as
hevlayer_* metrics. Token counts come back on the inference response, so they
cost no extra call; dollar cost is derived from them downstream rather than fetched
in the request path. The reasoning trace is written to the
search-history record alongside the query it belongs
to, so agentic and plain searches share one history surface for evaluation.