What You Will Build
Add a Status filter to the Agents page. Select Ready to hide agents whose current configuration is still being prepared or has failed. Select several statuses to see their combined results. Clear the selection to see all agents.
You will install kagent with Substrate, build the UI filter, add a Go status calculation and API response field, then deploy the updated controller and UI. Follow the chapters in order. All commands below run in the kagent checkout, except commands explicitly identified as workshop-site commands.
First build the UI filter from the existing Agent status. Then move the revision-state calculation into the controller and expose it in the Agent response. Update the UI to use that value, rebuild the controller, and verify the response and filter together. No database or CRD change is needed.
The 1.x Pieces Behind the Page
- An AgentTemplate defines behavior: model, prompt, tools, and skills.
- A Harness defines execution: runtime, WorkerPool, and snapshot location.
- An Agent pairs the template with the Harness. The controller prepares a revision and reports its status.
- A Session is a conversation pinned to a successful revision. Substrate runs its Actor on a Worker.
Between turns, kagent uses Data snapshots to retain the Actor's durable data and release its Worker. On resume, containers restart with that data. A Session can stay READY while its Actor is suspended. The filter uses Agent revision status, not Actor or Session state.
The UI already computes these six states in agentRevisionState:
| Value | Label | Meaning |
|---|---|---|
ready |
Ready | The current revision has been prepared successfully. |
updating |
Updating | A newer revision is being prepared; an earlier one exists. |
updateFailed |
Update failed | A later edit failed; an earlier successful revision remains. |
preparing |
Preparing | The first revision is being prepared. |
failed |
Failed | Preparation failed before any successful revision. |
notReported |
No status | The controller has not reported a revision. |
An agent in Update failed can still start a conversation on its last successful revision. That does not make its current configuration Ready. Reuse the existing calculation rather than introducing a second definition of readiness.
Install kagent and Substrate
Prepare the cluster before the workshop. Use the cloned repository's Makefile and Kind scripts so the controller, charts, CLI, runtime images, and Substrate come from the same source configuration.
You need Docker with Buildx running, Kind, kubectl, Helm 3, Git, Make, jq,
envsubst, and the Go toolchain specified by go/go.mod. For the UI, the inspected
checkout requires Node 24.13+ and uses Yarn 4.9.0 through Corepack. Install
grpcurl for the later API check. Follow the pinned checkout's DEVELOPMENT.md
for the remaining build-tool versions.
1. Get the Source
These edits were checked against local main commit
8392184232e9c78196433a34b1fd3b33db5fcb97. Use the same commit for the code
examples and for the source-built installation below.
git clone https://github.com/kagent-dev/kagent.git
cd kagent
git switch --detach 8392184232e9c78196433a34b1fd3b33db5fcb97
git switch -c contribfest-status-filter
If the presenter supplies a newer tested commit, use that one and its matching instructions. Preserve existing local work when using a checkout you already own.
2. Create a Fresh Cluster
Run all Make commands in this chapter from the kagent repository root:
docker info
make create-kind-cluster AWK=awk
kubectl --context kind-kagent get nodes
kubectl --context kind-kagent api-resources --api-group=certificates.k8s.io
The target calls scripts/kind/setup-kind.sh and setup-metallb.sh. It creates
the kagent cluster, a local registry on localhost:5001, node pull routing to
that registry, and MetalLB. The default context is kind-kagent.
Use the cluster settings shipped with the pinned checkout. At this commit,
KIND_IMAGE_VERSION defaults to 1.35.0, and scripts/kind/kind-config.yaml
enables the certificate feature gates and certificates.k8s.io/v1beta1 APIs.
Confirm PodCertificateRequest and ClusterTrustBundle are listed. The newer
release-install docs describe Kubernetes 1.37 and v1 APIs; that is a different
installation baseline. Do not copy its node image into this recipe without
also checking the source scripts and matching Substrate version.
The script skips cluster creation if a cluster named kagent already exists.
Use a fresh development cluster or confirm that the existing one was created
with this checkout's settings. Do not reuse a 0.x installation. Do not delete
an existing cluster without checking whether you need its data.
3. Configure the Build and Provider
Use one development version for every build and install command. Set it in each terminal where you run Make:
export VERSION=0.0.0-contribfest
export MODEL_PROVIDER=openAI
export OPENAI_API_KEY="<key>"
Replace <key> with your OpenAI API key in the shell that will run the
installer. Follow the provider setup in the checkout's development guide
if using another provider. Keep credentials out of tracked files, command
history, screenshots, and the PR. The filter itself never invokes a model.
Check make help AWK=awk for available targets. Keep the default Docker registry,
repository, cluster name, and sandbox class for this lab unless the presenter
has tested overrides. Changing KIND_CLUSTER_NAME also changes the context
used by the later kubectl commands.
4. Build the Images
make build VERSION="$VERSION" AWK=awk BUILD_PROTO_GENERATE=
make build-img-versions VERSION="$VERSION" AWK=awk
make build builds and pushes the controller, UI, Go ADK, native Harness, and
sandbox images to the local registry for your machine's architecture. It also
writes runtime-image digest metadata under build/runtime-images/. The chart
preparation step needs those digests, so building only the UI or controller
is not sufficient for the first installation.
At this revision, ordinary build targets also run remote protobuf generation.
BUILD_PROTO_GENERATE= skips that step for the initial build, which uses the
generated code already committed in the checkout. AWK=awk supplies the parser
used to read the Go and Substrate versions, including when an empty environment
variable would prevent the Makefile's default from taking effect.
Only skip automatic generation when generated code is current. The backend
chapter explicitly regenerates it after changing agents.proto. Buf remote
generation has a separate rate limit; do not repeatedly retry a quota failure.
See the recovery instructions in that chapter if it happens.
5. Install from the Checkout
Before the first install, check the CRD chart for artifacts left from older checkouts. Prepare the local charts, then render the CRD names:
make helm-dev VERSION="$VERSION" AWK=awk
helm template kagent-crds ./helm/kagent-crds --namespace kagent | grep '^ name:'
At this revision, every CRD name in that chart should end in .api.kagent.dev.
Substrate CRDs belong to a separate substrate-crds release in ate-system.
If the output includes .ate.dev or mcpservers.kagent.dev, stop: inspect
helm/kagent-crds/charts/ for old dependency archives. An existing checkout
can retain ignored chart downloads across branch changes. Back up stale,
untracked archives before removing them; do not delete tracked chart sources.
Helm can render these leftover archives even though the current Chart template does not declare them. Installing that mixed bundle gives Substrate CRDs the wrong owner and makes the CLI's separate Substrate CRD install fail.
Once the rendered chart contains only kagent CRDs:
make kagent-cli-deploy VERSION="$VERSION" AWK=awk BUILD_PROTO_GENERATE=
The target:
- Runs
use-kind-cluster, which writes/tmp/kind-config, merges the Kind context into your kubeconfig, and sets its default namespace tokagent. - Builds
go/core/bin/kagent-localfrom this checkout. - Runs
helm-devto prepare local charts with the built runtime-image digests. - Creates local values/configuration files in
go/core/bin/, including the Kind registry routing for Substrate. - Uses the local CLI to install kagent, Substrate, certificate infrastructure, and the development PostgreSQL database.
The target selects the Substrate version from the checkout's Go dependency; do not download a separate release CLI for this path. It reads and changes the current user's kubeconfig as described above; inspect existing contexts before running it on a machine used for other clusters.
If the target fails because ~/.kube/config does not exist, create a private
file for this new Kind cluster before retrying. Kind exports and merges its
context without replacing unrelated contexts:
kind export kubeconfig --name kagent
chmod 600 "$HOME/.kube/config"
make kagent-cli-deploy VERSION="$VERSION" AWK=awk BUILD_PROTO_GENERATE=
Do not run broad Docker cleanup commands.
If a previous attempt already installed a mixed CRD release, cleaning local
files is not enough. Inspect helm list --all-namespaces --all and count the
custom resources before choosing a recovery. On a confirmed disposable,
partial installation with no custom resources, remove the partial
kagent-crds release, then rerun the corrected installer. Uninstalling this
release deletes its CRDs and any objects using them. Get approval first;
do not force Helm ownership annotations or use this cleanup on a shared cluster.
make kagent-cli-install VERSION="$VERSION" AWK=awk BUILD_PROTO_GENERATE=
runs the same deployment target,
then opens the dashboard and holds its forward. It does not replace the initial
make build. We use kagent-cli-deploy here so installation and browser access
remain separate steps.
6. Check the Installation
kubectl --context kind-kagent -n kagent rollout status deployment/kagent-controller --timeout=300s
kubectl --context kind-kagent -n kagent rollout status deployment/kagent-ui --timeout=300s
kubectl --context kind-kagent -n kagent get pods
kubectl --context kind-kagent -n kagent get workerpools
Check for ready WorkerPool capacity as well as running controller/UI pods. The target definitions and scripts were checked in both the pinned commit and the local main checkout. Cluster creation and full installation have not been run for this kit; the presenter must rehearse them before the event.
Run the UI and Get Test Data
1. Start the Controller Forward
Keep this running in a separate terminal:
kubectl --context kind-kagent -n kagent port-forward --address 127.0.0.1 svc/kagent-controller 8083:8083
2. Install UI Dependencies
From the kagent checkout:
cd ui
corepack yarn --version
corepack yarn install --immutable
The version command should print 4.9.0 for this checkout. Use corepack yarn
throughout the lab so a global Yarn 1 executable cannot take precedence.
The next UI chapters use kagent/ui as their working directory. The backend
chapters will say when to return to the source root or Go module. The filter
reuses FilterBar and useListView, so no new UI dependencies are needed.
3. Start with Fixtures
KAGENT_UI_ENABLE_MOCK=true corepack yarn dev --host 127.0.0.1
Open the printed URL, normally http://localhost:8001/agents. This mode uses
in-browser fixtures and lets you test rendering without waiting for real
agent revisions. Later, you will switch mock mode off to verify the API path.
Open src/pages/AgentsPage.tsx. Locate FILTER_IDS, the filtered calculation,
and the <FilterBar> element. The existing namespace filter shows how the new
status filter will connect to URL state and pagination.
Add the Filter Function
Open ui/src/api/domain/agents.ts in the source checkout. Add this function
after agentRevisionState:
export function matchesAgentStatus(
agent: Agent,
selected: readonly string[],
): boolean {
return selected.length === 0 || selected.includes(agentRevisionState(agent));
}
An empty selection includes every agent. Otherwise the agent must match one of the selected states. The function only reads its inputs.
No export edit is needed in src/api/index.ts: it already re-exports
./domain/agents. Pages will import the function through @/api.
Test the Function
Open ui/src/api/domain/agents.test.ts. Replace its import from ./agents
with this import; leave the other imports and existing tests in place:
import {
agentDescription,
agentRevisionState,
matchesAgentStatus,
newConversationBlockedReason,
type Agent,
type AgentStatus,
} from "./agents";
The file already defines an agent(status?) test helper. Append these tests
after the existing describe block:
describe("Agent status filter", () => {
it("includes every agent when nothing is selected", () => {
expect(matchesAgentStatus(agent(), [])).toBe(true);
});
it("includes a ready agent", () => {
const row = agent({ desiredRevision: "good", latestSuccessfulRevision: "good" });
expect(matchesAgentStatus(row, ["ready"])).toBe(true);
});
it("excludes an agent whose first revision is still preparing", () => {
const row = agent({ desiredRevision: "new" });
expect(matchesAgentStatus(row, ["ready"])).toBe(false);
});
it("matches any selected state", () => {
const row = agent({ desiredRevision: "new" });
expect(matchesAgentStatus(row, ["ready", "preparing"])).toBe(true);
});
it("keeps a failed update out of Ready even when an older revision can run", () => {
const row = agent({
desiredRevision: "new",
latestSuccessfulRevision: "good",
conditions: [{ type: "Compatible", status: "False" }],
});
expect(matchesAgentStatus(row, ["ready"])).toBe(false);
expect(matchesAgentStatus(row, ["updateFailed"])).toBe(true);
});
it("matches agents without reported status", () => {
expect(matchesAgentStatus(agent(), ["notReported"])).toBe(true);
});
});
From kagent/ui, run:
corepack yarn test src/api/domain/agents.test.ts
Expect the existing readiness tests and all six new filter tests to pass.
If a test fails, fix the function before editing the page. A compile error or
no tests to run is not a pass.
Connect the Filter to the Page
All three edits below are in ui/src/pages/AgentsPage.tsx.
1. Import and Register the Filter
In the existing import from @/api, add matchesAgentStatus beside
agentRevisionState:
agentRevisionState,
matchesAgentStatus,
Replace the FILTER_IDS declaration with:
const FILTER_IDS: readonly string[] = ["ns", "status"];
Registering the ID makes the existing list-view hook include this filter when it clears selections, remembers URL parameters, and resets pagination.
2. Apply the Selection
Replace the complete const filtered = useMemo(...) block with:
const filtered = useMemo(() => {
const matching = agents.filter((agent) =>
matchesAgentStatus(agent, view.selected("status")) &&
matchesQuery(view.query, [
agent.name,
agent.namespace,
templateRefName(agent),
harnessRefName(agent),
descriptions.get(agent.ref),
]),
);
if (!view.sort) matching.sort((left, right) => left.name.localeCompare(right.name));
return matching;
}, [agents, descriptions, view]);
The status and text filters must both match. Namespace filtering already controls which agents are fetched. Keep the existing sort and pagination code.
3. Add the Control
In <FilterBar>, find the filters array. Insert this object immediately
after the existing namespace filter object, inside the same array:
{
id: "status",
label: "Status",
allLabel: "All statuses",
options: [
{ value: "ready", label: "Ready" },
{ value: "updating", label: "Updating" },
{ value: "updateFailed", label: "Update failed" },
{ value: "preparing", label: "Preparing" },
{ value: "failed", label: "Failed" },
{ value: "notReported", label: "No status" },
],
},
Save the file. Vite reloads the page. You should see All statuses beside the namespace filter. Existing controls provide the selection pills and Clear filters.
corepack yarn test src/api/domain/agents.test.ts
corepack yarn typecheck
corepack yarn lint
Return Revision State from the Controller
The filter currently calculates revision state in the browser. Add that same calculation to the controller so API clients can use the controller's result. Keep selection and pagination behavior unchanged: this is a new response field, not server-side filtering.
You need the Go toolchain from go/go.mod (Go 1.27.3 at this revision), Make,
and access to Buf's remote code-generation plugins. Run commands from the
kagent repository root unless stated otherwise.
1. Add the Go Calculation
Create go/core/internal/service/agentstatus/status.go:
package agentstatus
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/kagent-dev/kagent/go/api/v1alpha3"
)
func RevisionState(agent *v1alpha3.Agent) string {
if agent == nil {
return "notReported"
}
status := agent.Status
failed := false
for _, condition := range status.Conditions {
if condition.Status == metav1.ConditionFalse &&
!(condition.Type == "Ready" && condition.Reason == "ActorTemplatePending") {
failed = true
}
}
behind := status.ObservedGeneration < agent.Generation ||
(status.DesiredRevision != "" && status.DesiredRevision != status.LatestSuccessfulRevision)
if status.LatestSuccessfulRevision != "" {
if failed {
return "updateFailed"
}
if behind {
return "updating"
}
return "ready"
}
if failed {
return "failed"
}
if status.DesiredRevision != "" || behind {
return "preparing"
}
return "notReported"
}
The handler will call this function. Keeping the calculation outside the transport handler makes it testable without a cluster or database. It reads Agent status only; it does not change the stored Agent or its readiness.
2. Add Go Tests
Create go/core/internal/service/agentstatus/status_test.go:
package agentstatus
import (
"reflect"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/kagent-dev/kagent/go/api/v1alpha3"
)
func TestRevisionState(t *testing.T) {
failure := []metav1.Condition{{Type: "Compatible", Status: metav1.ConditionFalse}}
pending := []metav1.Condition{{Type: "Ready", Status: metav1.ConditionFalse, Reason: "ActorTemplatePending"}}
cases := []struct {
name string
generation int64
status v1alpha3.AgentStatus
want string
}{
{name: "no report", want: "notReported"},
{name: "first revision", status: v1alpha3.AgentStatus{DesiredRevision: "new"}, want: "preparing"},
{name: "ready", status: v1alpha3.AgentStatus{DesiredRevision: "good", LatestSuccessfulRevision: "good"}, want: "ready"},
{name: "updating", status: v1alpha3.AgentStatus{DesiredRevision: "new", LatestSuccessfulRevision: "good", Conditions: pending}, want: "updating"},
{name: "failed update", status: v1alpha3.AgentStatus{DesiredRevision: "new", LatestSuccessfulRevision: "good", Conditions: failure}, want: "updateFailed"},
{name: "failed first revision", status: v1alpha3.AgentStatus{DesiredRevision: "new", Conditions: failure}, want: "failed"},
{name: "pending golden snapshot", status: v1alpha3.AgentStatus{DesiredRevision: "new", Conditions: pending}, want: "preparing"},
{name: "unobserved edit", generation: 2, status: v1alpha3.AgentStatus{ObservedGeneration: 1, DesiredRevision: "good", LatestSuccessfulRevision: "good"}, want: "updating"},
}
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
agent := &v1alpha3.Agent{ObjectMeta: metav1.ObjectMeta{Generation: test.generation}, Status: test.status}
before := agent.DeepCopy()
if got := RevisionState(agent); got != test.want {
t.Fatalf("RevisionState() = %q, want %q", got, test.want)
}
if !reflect.DeepEqual(agent, before) {
t.Fatal("RevisionState() mutated the Agent")
}
})
}
if got := RevisionState(nil); got != "notReported" {
t.Fatalf("RevisionState(nil) = %q", got)
}
}
Run the focused tests before wiring the response:
gofmt -w go/core/internal/service/agentstatus/status.go go/core/internal/service/agentstatus/status_test.go
cd go
go test ./core/internal/service/agentstatus -count=1 -v
cd ..
3. Add the Response Field and Generate Clients
Open proto/kagent/api/v1/agents.proto. Replace only message Agent with:
message Agent {
ResourceReference ref = 1;
// Complete Agent CR, including its inline or referenced configuration and status.
StructuredObject resource = 2;
// Current revision state: ready, updating, updateFailed, preparing, failed, or notReported.
// Empty on controllers that predate this field.
string revision_state = 3;
}
This adds a response field without changing existing field numbers or request types. Run the repository's generators; do not edit generated Go or TypeScript files by hand:
make proto-lint proto-generate AWK=awk
Generation may update clients for other languages too. Keep generated changes together even though this lab does not require editing their implementations. For upstream review, also run the repository's protobuf compatibility check against the agreed base revision.
If Buf returns resource_exhausted: too many requests, stop retrying. Remote
generation has a separate quota; unauthenticated requests share the source
IP's quota, which matters on conference Wi-Fi. Authenticate in your terminal:
go run github.com/bufbuild/buf/cmd/buf@v1.72.0 registry login buf.build
Enter credentials only through Buf's own login flow. Authentication raises the generation quota; it does not make it unlimited. If still limited, honor the server's retry/reset time. The presenter can provide already-generated files for the exact changed proto as a fallback.
After login or quota reset, run make proto-lint proto-generate AWK=awk once
and confirm it completes. Do not skip this step after changing a proto.
Subsequent builds use BUILD_PROTO_GENERATE= to avoid regenerating the same
files on every build or CLI installation. The Buf rate-limit docs
describe the current limits.
4. Wire the Existing Agent Mapper
Open go/core/internal/grpcserver/agent.go. Add this import:
"github.com/kagent-dev/kagent/go/core/internal/service/agentstatus"
In func (s *agentServer) agent(...), replace the returned apiv1.Agent
literal with:
return &apiv1.Agent{
Ref: &apiv1.ResourceReference{Namespace: agent.Namespace, Name: agent.Name},
Resource: resource,
RevisionState: agentstatus.RevisionState(agent),
}, nil
The shared mapper serves list, get, create, and update responses. Authorization and pagination still follow their existing paths. The original resource stays intact and no status is written back to Kubernetes.
5. Check the API Response with a Generated Client
Append this test to go/core/internal/grpcserver/agent_test.go. Its imports
already include the types and test helpers used here:
func TestAgentServiceReportsRevisionState(t *testing.T) {
ready := &v1alpha3.Agent{
ObjectMeta: metav1.ObjectMeta{Namespace: "team", Name: "ready"},
Status: v1alpha3.AgentStatus{DesiredRevision: "good", LatestSuccessfulRevision: "good"},
}
failed := &v1alpha3.Agent{
ObjectMeta: metav1.ObjectMeta{Namespace: "team", Name: "failed"},
Status: v1alpha3.AgentStatus{DesiredRevision: "new", Conditions: []metav1.Condition{
{Type: "Compatible", Status: metav1.ConditionFalse},
}},
}
client := apiv1.NewAgentServiceClient(newTemplateAndHarnessConnection(t, ready, failed))
ctx := metadata.AppendToOutgoingContext(t.Context(), "x-user-id", "alice")
listed, err := client.ListAgents(ctx, &apiv1.ListAgentsRequest{Namespace: "team"})
require.NoError(t, err)
states := map[string]string{}
for _, agent := range listed.Agents {
states[agent.Ref.Name] = agent.RevisionState
}
require.Equal(t, map[string]string{"ready": "ready", "failed": "failed"}, states)
got, err := client.GetAgent(ctx, &apiv1.GetAgentRequest{
Ref: &apiv1.ResourceReference{Namespace: "team", Name: "ready"},
})
require.NoError(t, err)
require.Equal(t, "ready", got.Agent.RevisionState)
}
gofmt -w go/core/internal/grpcserver/agent.go go/core/internal/grpcserver/agent_test.go
cd go
go test ./core/internal/service/agentstatus ./core/internal/grpcserver -run 'TestRevisionState$|TestAgentService' -count=1 -v
cd ..
After the focused tests pass, run the full affected packages before upstream
review. Record failures separately; the wider gRPC suite includes tests beyond
this feature.
If an envtest case reports a missing etcd executable, use the Go Makefile's
envtest-path target to obtain its assets and set KUBEBUILDER_ASSETS to the
printed path before rerunning. The focused tests above do not need envtest.
6. Read the Controller Value in the UI
In ui/src/api/domain/agents.ts, add this optional field to interface Agent:
revisionState?: AgentRevisionState;
Add this function to the same file:
export function revisionStateFromApi(value: string): AgentRevisionState | undefined {
switch (value) {
case "ready":
case "updating":
case "updateFailed":
case "preparing":
case "failed":
case "notReported":
return value;
default:
return undefined;
}
}
Change the beginning of agentRevisionState to the following, keeping the
existing calculation after it as a fallback for older controllers:
export function agentRevisionState({ revisionState, resource: { metadata, status } }: Agent): AgentRevisionState {
if (revisionState !== undefined) return revisionState;
Open ui/src/api/grpc/operations.ts. Replace its type-only import from
../domain/agents with:
import { revisionStateFromApi, type Agent, type AgentResource } from "../domain/agents";
Then replace toAgent with:
function toAgent(agent: PbAgent): Agent {
return {
ref: refToString(agent.ref),
namespace: agent.ref?.namespace ?? "",
name: agent.ref?.name ?? "",
resource: unwrap<AgentResource>(agent.resource, "AgentService", "Agent"),
revisionState: revisionStateFromApi(agent.revisionState),
};
}
The existing status display and your new filter both call agentRevisionState,
so both now use the controller value. Old controllers return an empty field;
the fallback keeps the UI working during the upgrade.
In ui/src/api/domain/agents.test.ts, add revisionStateFromApi to the
./agents import and append:
describe("Controller revision state", () => {
it("uses the controller value before the compatibility fallback", () => {
const row = agent();
row.revisionState = "ready";
expect(agentRevisionState(row)).toBe("ready");
expect(matchesAgentStatus(row, ["ready"])).toBe(true);
});
it("accepts known states and ignores empty or unknown values", () => {
expect(revisionStateFromApi("updateFailed")).toBe("updateFailed");
expect(revisionStateFromApi("")).toBeUndefined();
expect(revisionStateFromApi("futureState")).toBeUndefined();
});
});
From kagent/ui:
corepack yarn test src/api/domain/agents.test.ts
corepack yarn typecheck
Deploy the Updated Controller
Run these commands from the kagent repository root after generation and tests:
export VERSION=0.0.0-contribfest
make build-controller VERSION="$VERSION" AWK=awk BUILD_PROTO_GENERATE=
make kagent-cli-deploy VERSION="$VERSION" AWK=awk BUILD_PROTO_GENERATE=
The first target builds and pushes the updated controller. The second runs the same local-chart deployment used during setup. Keep the development database, Substrate, and runtime-image metadata from that setup; the new field needs no database migration or agent-runtime change.
Because the development image tag stays the same, explicitly restart the controller after the deployment command finishes:
kubectl --context kind-kagent -n kagent rollout restart deployment/kagent-controller
kubectl --context kind-kagent -n kagent rollout status deployment/kagent-controller --timeout=300s
The install target sets image pull policy to Always, so replacement pods pull the rebuilt image. A Helm upgrade alone can leave existing pods running when the pod template has not changed. Repeat these four commands for later controller edits; no image patch or container-name substitution is needed.
Restart the controller forward after the old pod is replaced:
kubectl --context kind-kagent -n kagent port-forward --address 127.0.0.1 svc/kagent-controller 8083:8083
Create an Agent to Inspect
The installer does not create Agent, AgentTemplate, or Harness resources. Create a small test Agent before checking the list response. It deliberately references missing resources, so it should become Failed after reconciliation; it will not start a conversation or call a model.
kubectl --context kind-kagent -n kagent apply -f - <<'YAML'
apiVersion: api.kagent.dev/v1alpha3
kind: Agent
metadata:
name: workshop-pending
namespace: kagent
spec:
template:
ref:
name: workshop-missing-template
harnessRef:
name: workshop-missing-harness
YAML
kubectl --context kind-kagent -n kagent get agent workshop-pending -o jsonpath='{.status.conditions}'
If admission rejects unresolved references at your revision, use the presenter's prepared non-ready Agent instead; do not disable validation.
Inspect the Response
The source chart disables gRPC reflection by default. From the kagent root, create a descriptor set from your updated proto files, then use it with grpcurl:
go run github.com/bufbuild/buf/cmd/buf@v1.72.0 build proto --as-file-descriptor-set -o /tmp/contribfest-api.binpb
grpcurl -plaintext -protoset /tmp/contribfest-api.binpb -d '{"namespace":"kagent"}' localhost:8083 kagent.api.v1.AgentService/ListAgents
Check for revisionState beside each returned Agent's ref and resource.
The test Agent should report failed once its unresolved references are
recorded; immediately after creation it may report notReported or preparing.
An empty namespace can return only {"page":{}}: protobuf JSON omits the
empty Agent array. That response confirms the RPC succeeded, but cannot verify
the new field. Check kubectl get agents.api.kagent.dev -n kagent if it is empty.
An old controller can still satisfy the UI fallback, so the filter working alone does not prove that the backend change is deployed. The descriptor-set command does not need reflection. Keep the controller bound to loopback.
Verify the UI Against kagent
1. Check with Fixtures
On the Agents page:
- Select Ready and check that every visible row has Ready status.
- Select Preparing too; both statuses should now be included.
- Use a namespace and text query at the same time. Results must match all three filters.
- Reload the page. The status selection should remain in the URL and control.
- Click Clear filters. All statuses should return.
- Try a combination with no matches. Expect the existing filtered empty state, not the create-agent empty state.
2. Switch to the Cluster
Stop Vite with Ctrl-C. Keep the controller forward on 8083 running. Start Vite again with mock mode explicitly disabled:
KAGENT_UI_ENABLE_MOCK=false corepack yarn dev --host 127.0.0.1
Check ui/.env for old backend URL overrides if reads fail. The default Vite
proxy targets http://127.0.0.1:8083. Kubernetes access and kagent API access
are separate; use the prepared cluster's identity settings if calls are denied.
3. Create Two Observable States
In the installed UI, prepare an agent with a valid Harness and template/model configuration using the 1.x first-agent guide. Wait for its successful revision. This gives you a Ready row without sending a chat message.
Use the workshop-pending Agent created in the controller-deployment chapter
as the non-ready row. Inspect its conditions:
kubectl --context kind-kagent -n kagent get agent workshop-pending -o jsonpath='{.status.conditions}'
The new row may initially show No status, then Failed after the controller reports unresolved references. It must not appear when only Ready is selected. Use the status actually shown by the page when testing the matching selection.
Compare the page with the Agent conditions and revisions. No model invocation is needed to exercise the filter. Substrate still needs healthy Workers and snapshot storage to prepare a valid agent revision.
Build and Deploy the Updated UI
1. Build the Production Bundle
From kagent/ui:
corepack yarn build
This runs the type checker and builds the static bundle. It does not replace the image in Kind.
2. Build and Push an Image
From the kagent repository root:
export VERSION=0.0.0-contribfest
make build-ui VERSION="$VERSION" AWK=awk BUILD_PROTO_GENERATE=
make kagent-cli-deploy VERSION="$VERSION" AWK=awk BUILD_PROTO_GENERATE=
The build target pushes the updated UI to the registry created during setup. The deployment target reapplies the local charts with the same development version and the controller image built in the previous chapter.
3. Restart the UI
As with the controller, restart the UI so new pods pull the rebuilt same-tag image under the install target's Always pull policy:
kubectl --context kind-kagent -n kagent rollout restart deployment/kagent-ui
kubectl --context kind-kagent -n kagent rollout status deployment/kagent-ui --timeout=300s
Repeat the build, deploy, and restart sequence for later UI edits.
4. Open the Deployed Version
Stop any old UI forward and start this one:
kubectl --context kind-kagent -n kagent port-forward --address 127.0.0.1 svc/kagent-ui 8082:8080
Open http://localhost:8082/agents and repeat the Ready/filter/clear checks.
This is the packaged UI, not Vite on port 8001. Confirm the image in use:
kubectl --context kind-kagent -n kagent get deployment kagent-ui -o jsonpath='{.spec.template.spec.containers[*].image}'
If rollout fails, inspect pod events and logs. If a browser still shows the old page, check its port, refresh, and confirm that the rollout replaced the UI pod. These are development instructions for the disposable Kind cluster.
Review and Contribute
From the source root:
git status --short
git diff --check
git diff --stat
Review the UI files, new agentstatus package, Agent mapper/API test,
agents.proto, and generated clients. Keep install configuration, credentials,
and test manifests out of the PR. Confirm:
- The Go status and AgentService tests pass; the UI's 16 readiness/filter tests pass.
- The API response contains
revisionStatefrom the updated controller; the UI uses it, with a fallback for older controllers. - Protobuf generation and compatibility checks pass; all affected generated clients are included.
- Type checking, lint, and the production build pass.
- Ready means the current revision is ready; a runnable older revision is not enough.
- Status selection works with search, namespaces, sorting, pagination, reload, and Clear filters.
- The filter works against the updated controller and from the deployed UI image.
Delete only the test Agent you created:
kubectl --context kind-kagent -n kagent delete agent workshop-pending
Stop Vite and port-forwards with Ctrl-C. Keep the cluster if you want to
continue later. Before opening an upstream PR, check for existing work and
coordinate with a maintainer. Use signed-off commits (git commit -s) and list
the exact checks you ran, including any cluster steps you could not complete.