Skip to content

kagent

You can connect a kagent Agent to a kslate record in two ways. With the sidecar, a small container runs next to your agent and does all the kslate work for it. With MCP, the model calls kslate tools directly. The two paths enforce different things, so pick deliberately.

Sidecar (kslate-reconciler)MCP (RemoteMCPServer)
Changes to your agentNo code change. Its prompt has to ask for the response shape below.No code change. Its prompt should say what to do on a rejection.
How kslate knows who is callingThe pod’s ServiceAccount token, checked with the Kubernetes APIAn API key stored in a Secret
How based_on is setThe sidecar declares them from the record it just readkslate infers them from a scope header, if kagent passes it through
What happens after a rejectionThe sidecar reads again and re-runs the agent. If it loses its lease it drops the result.The model decides. kslate cannot make it re-read.
What kslate enforcesVersion, based_on, lease, scope, identity, checkpointVersion, scope, identity, and based_on when the header is present

Both paths were tested against kagent 0.10.2 with kagent.dev/v1alpha2. Install kslate first (Install). The gateway Service is kslate-gateway.kslate.svc.

Path 1: the sidecar

A kagent Agent with type: BYO runs your own container image. You add the sidecar to the same pod with spec.byo.deployment.extraContainers. That field is part of the kagent CRD, so you do not patch the Deployment kagent manages.

apiVersion: kagent.dev/v1alpha2
kind: Agent
metadata:
  name: kslate-remediator
  namespace: kagent
spec:
  type: BYO
  description: Incident remediator; coordination is handled by the kslate-reconciler sidecar, not by this agent.
  byo:
    deployment:
      image: your-agent-image:tag            # serves A2A on 8080
      replicas: 1
      serviceAccountName: remediator         # → sa:kagent/remediator in the class
      volumes:
        - name: kslate-token
          projected:
            sources:
              - serviceAccountToken:
                  audience: kslate
                  expirationSeconds: 3600
                  path: token
      extraContainers:
        - name: kslate-reconciler
          image: kslate-reconciler:dev
          imagePullPolicy: Never
          args:
            - --server=http://kslate-gateway.kslate.svc
            - --token-file=/var/run/secrets/kslate/token
            - --record=incidents/inc-42
            - --claim-field=remediation
            - --watch-field=planned_action
            - --lease-seconds=20
            - --a2a-endpoint=http://localhost:8080
            - --interval=3s
          volumeMounts:
            - name: kslate-token
              mountPath: /var/run/secrets/kslate
              readOnly: true

This manifest is deploy/examples/kagent-remediator-agent.yaml. Build the sidecar image with docker build -f Dockerfile.reconciler -t kslate-reconciler:dev ., then load it into your cluster or push it to a registry.

What the sidecar does

Every few seconds (--interval), the sidecar runs one cycle:

  1. It reads the record. If the watch field has not changed since the last time it acted, it stops here. Reading is free. Claiming writes a history row, so the sidecar does not claim until there is work.
  2. It claims the task field. If another replica holds it, the claim returns HELD and the sidecar stops.
  3. It loads the role’s last checkpoint. If the checkpoint says this version was already handled, it releases the claim and stops. This is what keeps a replacement from repeating an action after a takeover.
  4. It reads the record again, because the plan may have changed between steps 1 and 2.
  5. It sends one JSON-RPC message/send request to your agent container. While your agent works, the sidecar keeps renewing the lease. If a renewal returns HELD, another replica has taken over and the sidecar cancels the request. The request times out after --a2a-timeout (default 5 minutes).
  6. It writes your agent’s answer to the record with based_on set to the watch field at the version it read. If the plan changed while your agent was working, kslate rejects the write with STALE_PREMISE and the next cycle starts over.
  7. It saves a checkpoint and releases the lease.

What your agent receives and returns

The sidecar does not decide what to write. Your agent does. The sidecar hands it the record and takes back a list of writes.

The request. One JSON-RPC call, method message/send. The params are the record name and every field the sidecar’s identity may read, in the same shape get returns:

{"jsonrpc": "2.0", "id": 1, "method": "message/send", "params": {
  "record": "incidents/inc-42",
  "fields": {
    "planned_action": {
      "value": "roll back api deployment",
      "version": 5,
      "author": "sa:kagent/diagnoser",
      "evidence": "api logs show config parse error at 14:02:11",
      "updated_at": "2026-10-02T18:42:20Z"
    },
    "actions_taken": {
      "version": 2,
      "entries": [ { "seq": 7, "value": "restarted payments-api pods", "author": "sa:kagent/remediator", "evidence": "kubectl rollout restart", "updated_at": "..." } ]
    },
    "status": { "value": "mitigating", "version": 2, "author": "sa:kagent/detector", "updated_at": "..." }
  }
}}

Your agent reads fields, does its work, and decides what to record.

The response. The writes it wants made. This is a small subset of the A2A protocol, not the whole thing.

{"jsonrpc": "2.0", "id": 1, "result": {
  "writes": [
    {"op": "append", "field": "actions_taken", "entry": "rolled back the deployment", "evidence": "executed the plan"}
  ]
}}

Each write has op (put or append), field, a value (for put) or entry (for append), and evidence. An empty writes list means “nothing to do”. The agent never mentions a version, a lease, or based_on.

What the sidecar adds before writing:

  • based_on: the watch field at the version it read before calling your agent. If the plan changed during the call, kslate rejects the write with STALE_PREMISE.
  • expected_version on a put: the version it read, or 0 if the field did not exist. Your agent can set expected_version itself, but normally should not.
  • A final lease renewal. If that returns HELD, another replica took the task during the call and the sidecar discards your agent’s answer unwritten.

On a rejection. If a write is rejected with STALE_PREMISE and none of this decision’s writes have landed yet, the sidecar reads the record again. It calls your agent again with the new fields, up to a retry limit.

If an earlier write in the same decision already landed, it stops and logs instead. kslate has no multi-field atomic write, so running the decision again could record an append twice. Return one write per decision when you can.

The checkpoint. After the writes land, the sidecar saves a checkpoint with last_acted_version (the watch field version it acted on) and the sections the class requires. A replacement reads it first, so it does not act on the same version twice.

Your agent’s code does not change. Its prompt does: wherever you prompt the agent, you tell it what fields contains and ask it to answer in the response shape above.

Flags

FlagMeaning
--serverThe gateway URL.
--token-fileFile holding the bearer token. Read again on every request, so a rotated projected token is picked up.
--apikeyBearer token, for a gateway running with --identity apikey.
--recordWatch one record, given as class/id.
--class, --filter field=value, --concurrencyWatch every record in a class that matches the filters. The claim decides which replica handles each one.
--claim-fieldThe task field to claim before acting.
--watch-fieldThe field whose version change means there is work to do.
--lease-secondsHow long each claim lasts. Default 300.
--a2a-endpoint, --a2a-timeoutWhere your agent container listens, and how long to wait for it. Defaults http://localhost:8080 and 5 minutes.
--checkpoint-roleWhich role to checkpoint as, if the identity holds more than one.
--intervalHow often to run a cycle. Default 5 seconds.

The flags for reaching an agent through kagent 1.0’s gateway (--transport kagent-gateway, --kagent-gateway, --agent, --kagent-user-id, --task-encoding, --instruction, --verify-contract, --stalled-backoff) are in the table under Path 1 on kagent 1.0 below. The CLI reference lists every flag.

Identity

Every pod of one Agent uses the same ServiceAccount, so kslate sees them as one identity. If you restart an Agent, the new pod re-claims as the same identity. That is not a takeover. To see a takeover, run two Agents with two ServiceAccounts, both named in the class’s role.

Path 1 on kagent 1.0: the reconciler as its own Deployment

On kagent 1.0 an agent runs as an Actor inside a shared Agent Substrate worker pod. There is no second container slot, and the Actor has no ServiceAccount token to present. So kslate-reconciler runs as its own Deployment and reaches the agent through kagent’s A2A gateway. The loop is the same code as the sidecar. The transport flags change.

args:
  - --server=http://kslate-gateway.kslate.svc
  - --token-file=/var/run/secrets/kslate/token
  - --transport=kagent-gateway
  - --kagent-gateway=http://kagent-controller.kagent.svc:8083
  - --agent=kagent/kslate-remediator-agent
  - --record=incidents/inc-42
Flag
--transport=kagent-gatewayCall the agent through kagent’s gateway instead of localhost:8080.
--kagent-gatewaykagent’s controller base URL.
--agentThe kagent Agent to invoke, as namespace/name.
--task-encodingHow the task reaches the agent: text (default), data or both. kagent’s own runtime drops data parts before the model sees them. A BYO Actor receives them intact.
--verify-contractauto (default), require or skip. At startup the reconciler fetches the gateway’s gRPC descriptors and checks the fields it depends on. kagent’s A2A service is a fork of the public one, and a mismatch would otherwise fail silently.
--kagent-user-idThe X-User-Id header sent to kagent. A label. kagent does not verify it.
--instructionText put in front of the task. Empty by default. Needs a text part, so it cannot be combined with --task-encoding data; the reconciler refuses to start.
--stalled-backoffHow long to leave a record alone after the agent asked for input. Only the kagent-gateway transport reports that state; on the sidecar transport this flag has no effect.

The reconciler’s own ServiceAccount is the writer. Give it a projected token with audience kslate, as for the sidecar, and list sa:<namespace>/<name> in the class role. One ServiceAccount per agent: two agents sharing one are one identity to kslate.

An agent that answers with INPUT_REQUIRED ends the cycle. The reconciler does not answer questions. It leaves the record alone for --stalled-backoff, then tries again. Write the agent’s prompt so that it decides instead of asking.

The full manifest, with the ServiceAccount and the token mount, is deploy/examples/kagent1-reconciler-deployment.yaml in the repository.

Path 2: MCP

The gateway serves MCP over streamable HTTP at /mcp on the API port. It exposes six tools: kslate_get, kslate_put, kslate_append, kslate_claim, kslate_checkpoint and kslate_query. They run the same checks and return the same errors as the HTTP API.

This section covers the kagent wiring. For any other MCP client, or to try the endpoint with curl, see Any MCP client.

apiVersion: v1
kind: Secret
metadata:
  name: kslate-mcp-credential
  namespace: kagent
type: Opaque
stringData:
  authorization: "Bearer <a key from the gateway's --apikeys file>"
---
apiVersion: kagent.dev/v1alpha2
kind: RemoteMCPServer
metadata:
  name: kslate
  namespace: kagent
spec:
  description: kslate shared context - the six operations of the kslate gateway.
  url: http://kslate-gateway.kslate.svc/mcp
  protocol: STREAMABLE_HTTP
  timeout: 30s
  headersFrom:
    - name: Authorization
      valueFrom:
        type: Secret
        name: kslate-mcp-credential
        key: authorization
---
apiVersion: kagent.dev/v1alpha2
kind: Agent
metadata:
  name: kslate-mcp-diagnoser
  namespace: kagent
spec:
  type: Declarative
  declarative:
    modelConfig: default-model-config
    systemMessage: |
      ...read a versioned field before writing it and pass the version as
      expected_version; on STALE, re-read and decide again; on
      STALE_PREMISE, re-read the moved field first; never invent evidence.
    tools:
      - type: McpServer
        mcpServer:
          name: kslate
          kind: RemoteMCPServer
          apiGroup: kagent.dev
          toolNames: [kslate_get, kslate_put, kslate_append, kslate_query]
          allowedHeaders:
            - X-Kslate-Scope

The full manifest, including the complete system message, is deploy/examples/kagent-mcp-agent.yaml.

Two things about the kagent CRD shape this path:

  • The credential is an API key. RemoteMCPServer.spec.headersFrom can only read a header value from a Secret or a ConfigMap. It cannot read the pod’s projected ServiceAccount token. So the gateway that serves MCP agents has to run with --identity apikey, and you are responsible for keeping the key safe. A long-lived key in a Secret is weaker than a token that rotates every hour. The Helm chart does not install such a gateway: it starts one with --identity k8s-sa. Take this path against a gateway you run yourself.
  • based_on inference needs allowedHeaders. Whoever invokes the Agent sets X-Kslate-Scope on the request. kagent passes it to every tool call. kslate then treats whatever that header value read as the based_on of whatever it writes. See based_on. The model never sees the header. If you leave out allowedHeaders, kslate still records every write in history but no longer checks based_on. Nothing in the agent or its transcript shows you the difference.

Use one scope header value per task or conversation, at most 256 bytes. Do not use the MCP session id. One session covers unrelated conversations.

Choosing a path

Use the sidecar when you need coordination. Its loop is code. It reads again after a rejection and stands down when it loses a lease.

Use MCP when you need a governed record. kslate still enforces identity, evidence, scope and history no matter what the model does, and it checks based_on when the header is present. But kslate cannot make the model re-read after a rejection. The model may ignore a 409 or retry the same write. kslate only guarantees that the rejected write did not land and that the attempt is in history.

A runnable demo of the sidecar path is in a companion repository. It includes a stub agent, a standby Agent for the takeover, and three test arms.

Which identity each path gets

The path you pick decides what kslate can verify about the writer. That sets the identity strength on every write, and the floors a class can usefully require.

pathwhere the adapter runswriter identitystrength
kagent 0.x sidecara second container in the agent’s own podthe pod’s ServiceAccount, via TokenReviewpod-bound, or sa-bound on a cluster that does not report the binding
kagent 1.0 gatewayits own Deployment, one per agentthe reconciler’s own ServiceAccount, via TokenReviewsame
Python adapter in your processwherever your process runswhatever that process presentspod-bound or sa-bound in a pod; issuer-declared with OIDC
CI or a laptopa standalone reconcileran OIDC token, or an API keyissuer-declared, or shared-secret for a key
MCPthe model calls kslate directlywhatever credential the MCP server was givenusually shared-secret

Two consequences follow.

A strong strength does not mean the agent decided it. On kagent 0.x the sidecar’s token belongs to the pod that also runs the agent. sa:kagent/remediator meant “this write came from the agent’s own pod”. On kagent 1.0 the reconciler runs in its own Deployment, so the same identity means “a Deployment whose flags name that agent”. The write is just as authenticated either way. The link between the decision and the writer is now configuration. The invocation object is what records which agent call produced the write.

The MCP path is the weakest of the five. The credential has to live where the MCP server can reach it. That is usually a key, not a bound token. If a class sets a floor above shared-secret, the MCP path cannot write those fields at all.