For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.
Run your own agent image
Build a minimal BYO agent image, run it on a byo Harness, and invoke it the same way as any other kagent agent.
The byo runtime runs a container image that you build, so long as the image implements kagent’s A2AA2AThe Agent-to-Agent protocol, which callers and other agents use to talk to an Agent. The conversation's context identifier is the Session ID, so a second message on the same ID continues the same conversation.Learn more (Agent-to-Agent) contract. This example takes the shortest path through that contract: build the minimal BYO agent that kagent tests itself against, run it on a HarnessHarnessA Kubernetes custom resource defining how an agent is allowed to run: its runtime, workload image, and WorkerPool and snapshot storage. An Agent pairs it with the AgentTemplate it runs.Learn more, and invoke it.
The agent that you build here calls no model and binds no tools. It answers every message with a fixed string, which makes it a poor agent and a clear demonstration: everything that happens between kagent agent invoke and that reply is kagent’s half of the contract. To review the contract and the configured agents that read their AgentTemplate instead of ignoring it, see Bring your own agent.
Before you begin
Install kagent, including the port-forward to the controller’s gRPC API.
Create your first agent so that you have a Harness, an AgentTemplate, and an Agent to model these on, and a snapshot location to reuse.
Install the following tools.
Export the container registry that your cluster can pull from. For example, a local kind cluster created with
make create-kind-clusterruns one onlocalhost:5001.export DOCKER_REGISTRY=localhost:5001Know which WorkerPoolWorkerPoolA Kubernetes custom resource declaring how many Workers to keep running and which sandbox class they use. An operator must provision one before any Agent can compile.Learn more and snapshot location your installation uses. The Harness that you created carries both.
kubectl get harness my-first-harness -n kagent \ -o custom-columns=WORKERPOOL:.spec.substrate.workerPoolRef.name,SNAPSHOT:.spec.substrate.snapshotPolicy.locationExample output:
WORKERPOOL SNAPSHOT kagent-default s3://ate-snapshots/kagent/
Build the agent image
kagent’s own end-to-end suite runs an opaque BYO agent from go/core/test/byoa2a/main.go, and the repository has a make target that builds it. Building that image rather than writing one from scratch means starting from a version that is proven against the current contract.
Clone the kagent repository and navigate to it.
git clone https://github.com/kagent-dev/kagent.git cd kagentBuild the image and push it to your registry. The target builds
go/core/test/byoa2a/main.gowith the repository’s Go Dockerfile, which produces a single binary at/app.make build-byo-a2a DOCKER_REGISTRY=$DOCKER_REGISTRY VERSION=byo-exampleResolve the digest, and save the pinned reference. A Harness rejects an image that names only a tag, because a revisionRevisionThe compiled, immutable output of one Agent, identified by a content digest. A Session runs the revision it was created from for its whole life, so editing the Agent affects only sessions created afterward. must be reproducible.
export BYO_IMAGE=$DOCKER_REGISTRY/kagent-dev/kagent/byo-a2a@$(docker buildx imagetools inspect \ $DOCKER_REGISTRY/kagent-dev/kagent/byo-a2a:byo-example \ | awk '$1 == "Digest:" { print $2; exit }') echo $BYO_IMAGEExample output:
localhost:5001/kagent-dev/kagent/byo-a2a@sha256:ea596db3dac8da570980143210efeb2b47bcfb0a3afc5aa0f325a6063c5cf009
Create the Harness and the AgentTemplate
A byo Harness carries two fields that the other runtimes do not need: an empty byo block to select the runtime, and workload.command to override the image entrypoint. The AgentTemplate stays almost empty, because this agent ignores everything that an AgentTemplate would configure.
Create the Harness. The repository’s Go Dockerfile puts the binary at
/app, socommandnames that path.kubectl apply -f - <<EOF apiVersion: api.kagent.dev/v1alpha3 kind: Harness metadata: name: byo-example namespace: kagent spec: byo: {} workload: image: $BYO_IMAGE command: ["/app"] substrate: workerPoolRef: name: kagent-default snapshotPolicy: location: s3://ate-snapshots/kagent/ EOFNote
This Harness sets no
KAGENT_PORTvariable, becausebyoa2a/main.gopinsPort: "80"in the image. An image that leaves the port to kagent listens on the wrong one and still reportsREADY. For that trap and its workaround, see Bring your own agent.Create the AgentTemplate and an Agent that pairs it with the Harness.
descriptionis the only field that this template needs.kubectl apply -f - <<EOF apiVersion: api.kagent.dev/v1alpha3 kind: AgentTemplate metadata: name: byo-example-template namespace: kagent spec: description: A BYO agent that replies with a fixed string. --- apiVersion: api.kagent.dev/v1alpha3 kind: Agent metadata: name: byo-example-agent namespace: kagent spec: templateRef: name: byo-example-template harnessRef: name: byo-example EOFmodelConfigis absent on purpose. kagent requires one for thekagentruntime and treats it as optional forbyo, because a BYO image chooses its own model, or no model at all.Confirm that the Agent compiled. An AgentTemplate carries no status, so the Agent reports the state of the pair.
kubectl get agent byo-example-agent -n kagent \ -o jsonpath='{.status.conditions[*].type}{"\n"}{.status.conditions[*].status}{"\n"}'Example output:
Accepted ResolvedRefs Compatible Ready True True True True
Invoke the agent
At this point, the BYO agent behaves in the same way as any other. The SessionSessionA running conversation with one Agent. Unlike the resources it is built from, a Session is not a Kubernetes resource: kagent's gRPC API creates it and its PostgreSQL database tracks it.Learn more is the conversation, and the CLI reaches it through the controller’s A2A service. Nothing in these commands names the runtime.
Create a Session against the Agent.
kagent agent session create --agent byo-example-agentThe command returns only after the Session reaches
READY. Example output:+--------------------------------------+-------------------+-------+----------------------+ | ID | AGENT | STATE | CREATED | +--------------------------------------+-------------------+-------+----------------------+ | 01a08301-8cd0-72c8-818f-26c7490ce37d | byo-example-agent | READY | 2026-09-14T14:22:07Z | +--------------------------------------+-------------------+-------+----------------------+Save the Session ID.
export SESSION_ID=$(kagent agent session create --agent byo-example-agent -o json | jq -r '.session.id')Send it a message.
kagent agent invoke --session $SESSION_ID --task "hello"Example output:
BYO agent responseThat string is hardcoded, so the reply itself proves nothing. Its path proves the contract: kagent compiled a revision, Agent Substrate started a sandboxed ActorActorThe sandboxed unit of compute, provided by Agent Substrate, that runs a Session's conversation loop. Every Session is backed by one.Learn more from your image, the controller’s A2A gateway routed the message to it, and your executor answered.
Send another message to the same Session. The reply does not change, but the message reaches the same Actor. Agent Substrate suspended that Actor after the first turn and resumed it for this message. For more information about the Actor lifecycle, see Suspend and resume.
kagent agent invoke --session $SESSION_ID --task "hello again"
Change what the agent does
The whole agent is one type with two methods. Execute receives a request and yields A2A events until the turn ends, and Cancel handles a caller stopping a task that is still running.
type executor struct{}
func (executor) Execute(_ context.Context, request *a2asrv.ExecutorContext) iter.Seq2[a2atype.Event, error] {
return func(yield func(a2atype.Event, error) bool) {
if !yield(a2atype.NewSubmittedTask(request, request.Message), nil) {
return
}
message := a2atype.NewMessage(a2atype.MessageRoleAgent, a2atype.NewTextPart("BYO agent response"))
message.ContextID, message.TaskID = request.ContextID, request.TaskID
yield(a2atype.NewStatusUpdateEvent(request, a2atype.TaskStateCompleted, message), nil)
}
}
func (executor) Cancel(context.Context, *a2asrv.ExecutorContext) iter.Seq2[a2atype.Event, error] {
return func(func(a2atype.Event, error) bool) {}
}Two events make a complete turn. NewSubmittedTask acknowledges the message and opens the task, and a TaskStateCompleted status update carrying an agent message ends it. Between them, a real agent yields whatever its work produces.
app.New serves the A2A gRPC service and the readiness endpoint on your behalf, and the Port: "80" line keeps the listener where kagent expects it. Both stay as they are in an agent of your own.
To read the AgentTemplate rather than ignore it, parse the KAGENT_CONFIG_JSON variable that kagent sets on the container. For what that variable holds, see Bring your own agent.
Build and run your own agent
An agent of your own takes the same path as the example image, with two differences: the build names your package, and the Harness moves to the image that it produces.
Replace the body of
Executewith the work that your agent does, and leave the rest of the file as-is.Build and push the image. The
build-byo-a2atarget names its package inline, so a package of your own means calling Docker directly. The Dockerfile takes the package as a build argument, relative to thegodirectory.docker build --build-arg BUILD_PACKAGE=core/test/myagent/main.go \ -t $DOCKER_REGISTRY/kagent-dev/kagent/my-agent:v1 -f go/Dockerfile ./go docker push $DOCKER_REGISTRY/kagent-dev/kagent/my-agent:v1That Dockerfile copies
api,core,adk,harness, andpkgfrom the kagent module, so it suits an agent written inside a checkout. An agent in a module of your own needs a Dockerfile of your own. kagent places no requirement on how the image is built, only on what it serves.Resolve the digest of the new image, as in Build the agent image. A Harness rejects an image that names only a tag.
export BYO_IMAGE=$DOCKER_REGISTRY/kagent-dev/kagent/my-agent@$(docker buildx imagetools inspect \ $DOCKER_REGISTRY/kagent-dev/kagent/my-agent:v1 \ | awk '$1 == "Digest:" { print $2; exit }')Point the Harness at the new image. kagent compiles a revision for the updated pair.
kubectl patch harness byo-example -n kagent --type=merge \ -p "{\"spec\":{\"workload\":{\"image\":\"$BYO_IMAGE\"}}}"Create a Session on the updated Harness, and invoke it as in Invoke the agent. A Session pins the revision that it was created from, so the one from earlier keeps running the example image.
kagent agent session create --agent byo-example-agent
Clean up
Delete the Session. Repeat for any Session that you created from an image of your own.
kagent agent session delete $SESSION_IDDelete the AgentTemplate and the Harness.
kubectl delete agent byo-example-agent -n kagent kubectl delete agenttemplate byo-example-template -n kagent kubectl delete harness byo-example -n kagent