diff --git a/.github/workflows/update-ref-docs.yaml b/.github/workflows/update-ref-docs.yaml index 95b3a18e..d40bef75 100644 --- a/.github/workflows/update-ref-docs.yaml +++ b/.github/workflows/update-ref-docs.yaml @@ -303,9 +303,6 @@ jobs: # read none of the 1.x-only conrefs. WITH_RUNTIME_IMAGES=false WITH_KMCP=false - # 0.x predates the environment variable registry: docs/env.md - # does not exist at v0.10.1, and no 0.x page documents env vars. - WITH_ENV_DOCS=false ;; 1.x) SECTION=reference @@ -326,7 +323,6 @@ jobs: KAGENT_CLI_LINK_PREFIX=reference/cli WITH_RUNTIME_IMAGES=true WITH_KMCP=true - WITH_ENV_DOCS=true ;; *) echo "Error: unknown line '$LINE'." @@ -353,8 +349,6 @@ jobs: echo "CLI_WEIGHT=$CLI_WEIGHT" echo "WITH_RUNTIME_IMAGES=$WITH_RUNTIME_IMAGES" echo "WITH_KMCP=$WITH_KMCP" - echo "WITH_ENV_DOCS=$WITH_ENV_DOCS" - echo "KAGENT_ENV_PAGE=docs-site/content/kagent/$LINE/$SECTION/env-vars.md" } >> $GITHUB_ENV echo "Line $LINE -> content/kagent/$LINE/$SECTION/, $API_DIR" @@ -761,12 +755,6 @@ jobs: } >> $GITHUB_ENV fi - if [ "$WITH_ENV_DOCS" = "true" ]; then - echo "ENV_DOCS_NOTE=The environment variable reference is regenerated from \`docs/env.md\` in the kagent checkout, which upstream generates from the registry in \`go/core/pkg/env\` and holds to it in CI. The \`testing\` section is dropped as repository-facing; pass \`--include-section testing\` to publish it." >> $GITHUB_ENV - else - echo "ENV_DOCS_NOTE=No environment variable reference is generated on a $LINE run: \`docs/env.md\` does not exist at that line's releases." >> $GITHUB_ENV - fi - - name: Flag whether the conrefs moved run: | set -euo pipefail @@ -1200,31 +1188,6 @@ jobs: --out-dir "$KMCP_CLI_DIR" \ --url-prefix "$KMCP_CLI_URL_PREFIX" - - name: Generate environment variable reference - if: env.WITH_ENV_DOCS == 'true' - # kagent generates docs/env.md from the registry in go/core/pkg/env, - # and its own CI fails the build when the two drift (make - # env-docs-check). So the checked-out file is authoritative and this - # step only reshapes it into a page; it does not re-derive anything - # from Go source, and needs no build. - run: | - set -euo pipefail - cd "$GITHUB_WORKSPACE/website" - SOURCE="$GITHUB_WORKSPACE/kagent/docs/env.md" - # Fail loudly rather than skipping. A missing file here means the - # release being documented dropped or moved the registry output, - # which is a change this job must not paper over by silently - # leaving the published page on its previous contents. - if [ ! -f "$SOURCE" ]; then - echo "Error: $SOURCE not found in the kagent checkout at $KAGENT_VERSION." - echo "kagent generates it with 'make env-docs'. If upstream moved or" - echo "removed it, update this step and scripts/generate-env-docs.py." - exit 1 - fi - python3 scripts/generate-env-docs.py \ - --source "$SOURCE" \ - --out "$KAGENT_ENV_PAGE" - - name: Audit generated CLI docs run: | cd "$GITHUB_WORKSPACE/website" @@ -1244,7 +1207,7 @@ jobs: signoff: true title: "Update kagent ${{ env.LINE }} reference docs at ${{ env.KAGENT_TAG }}${{ env.CONREFS_CHANGED && ' (version conrefs changed)' || '' }}" body: | - Regenerates the kagent **${{ env.LINE }}** API and Helm references, its CLI and environment variable pages, and the version conrefs at a released tag. Nothing here was read from either repository's `main`: + Regenerates the kagent **${{ env.LINE }}** API and Helm references, its CLI pages, and the version conrefs at a released tag. Nothing here was read from either repository's `main`: - **kagent**: [`${{ env.KAGENT_TAG }}`](https://github.com/${{ github.repository_owner }}/kagent/releases/tag/${{ env.KAGENT_TAG }}) (`${{ env.KAGENT_COMMIT }}`) ${{ env.KMCP_SOURCE_LINE }} @@ -1252,8 +1215,6 @@ jobs: ${{ env.LINE_SCOPE_NOTE }} - ${{ env.ENV_DOCS_NOTE }} - ${{ env.CONREF_NOTE }} ${{ env.RUNTIME_IMAGE_NOTE }} diff --git a/docs-site/assets/kagent-docs/snippets/install-cli.md b/docs-site/assets/kagent-docs/snippets/install-cli.md new file mode 100644 index 00000000..5a1ec64b --- /dev/null +++ b/docs-site/assets/kagent-docs/snippets/install-cli.md @@ -0,0 +1,5 @@ +Download the kagent CLI. The `--version` flag matches the CLI to the release that these docs cover. + +```bash +curl https://raw.githubusercontent.com/kagent-dev/kagent/refs/heads/main/scripts/get-kagent | bash -s -- --version v{{< reuse "kagent-docs/versions/kagent.md" >}} +``` diff --git a/docs-site/content/kagent/1.x/about/architecture/kagent.md b/docs-site/content/kagent/1.x/about/architecture/kagent.md index 90d9aff4..3d98a013 100644 --- a/docs-site/content/kagent/1.x/about/architecture/kagent.md +++ b/docs-site/content/kagent/1.x/about/architecture/kagent.md @@ -70,7 +70,7 @@ Follow the **Kubernetes plane** first. An operator applies a Harness, an AgentTe The **kagent control plane** starts once that ActorTemplate exists. A caller, who might be the same person as the operator, calls `CreateSession` through kagent's gRPC API. This call is governed by kagent's own authentication and authorization, not by Kubernetes RBAC. kagent creates the Session from the Agent's latest successful revision. That Session runs on an {{< gloss "Actor" >}}Actor{{< /gloss >}}. -From there, the caller holds a conversation over the {{< gloss "A2A" >}}A2A{{< /gloss >}} (Agent-to-Agent) protocol. A caller addresses the Agent, and names the conversation with the Session's ID as the A2A `contextId`. The A2A gateway routes each request to the Actor running behind that Session. This means that the caller only ever needs to know the Agent's name and the Session's ID, never which Actor or {{< gloss "Worker" >}}Worker{{< /gloss >}} is behind it. +From there, the caller holds a conversation over the {{< gloss "A2A" >}}A2A{{< /gloss >}} (Agent2Agent) protocol. A caller addresses the Agent, and names the conversation with the Session's ID as the A2A `contextId`. The A2A gateway routes each request to the Actor running behind that Session. This means that the caller only ever needs to know the Agent's name and the Session's ID, never which Actor or {{< gloss "Worker" >}}Worker{{< /gloss >}} is behind it. ## Why two planes diff --git a/docs-site/content/kagent/1.x/about/core-concepts.md b/docs-site/content/kagent/1.x/about/core-concepts.md index 2d724e1f..9f07bd63 100644 --- a/docs-site/content/kagent/1.x/about/core-concepts.md +++ b/docs-site/content/kagent/1.x/about/core-concepts.md @@ -130,7 +130,7 @@ You can create a Session with the following command: kagent agent session create --agent assistant -n kagent ``` -After it is created, a Session talks to callers over the {{< gloss "A2A" >}}A2A{{< /gloss >}} (Agent-to-Agent) protocol, through kagent's A2A gateway. Callers address the Agent rather than the Session. The HTTP endpoint is `/agents/{namespace}/{name}` and gRPC carries the same `namespace/name` in the standard A2A `tenant` field. The Session's ID is the A2A `contextId`, so a message that does not include a context identifier starts a new conversation, and a message that repeats one continues that conversation. +After it is created, a Session talks to callers over the {{< gloss "A2A" >}}A2A{{< /gloss >}} (Agent2Agent) protocol, through kagent's A2A gateway. Callers address the Agent rather than the Session. The HTTP endpoint is `/agents/{namespace}/{name}` and gRPC carries the same `namespace/name` in the standard A2A `tenant` field. The Session's ID is the A2A `contextId`, so a message that does not include a context identifier starts a new conversation, and a message that repeats one continues that conversation. An expiration worker deletes a Session that does not record task activity for seven days. The `config.sessions.idleTtl` Helm value sets that window. A value of `0s` turns the expiration worker off. For more information about what the deletion retains, see [Expire idle Sessions]({{< link path="operations/operational-considerations#expire-idle-sessions" >}}). diff --git a/docs-site/content/kagent/1.x/about/substrate-runtime/identity.md b/docs-site/content/kagent/1.x/about/substrate-runtime/identity.md index d1aed138..ae0bd0f8 100644 --- a/docs-site/content/kagent/1.x/about/substrate-runtime/identity.md +++ b/docs-site/content/kagent/1.x/about/substrate-runtime/identity.md @@ -1,6 +1,6 @@ --- title: Identity -description: Understand how kagent resolves a caller's identity, scopes a Session to its creator, and how Agent Substrate identifies its own components. +description: Understand how kagent identifies each caller, scopes a Session to its creator, and identifies an agent to the services that it calls. weight: 30 author: kagent.dev aliases: @@ -11,7 +11,9 @@ A {{< reuse "kagent-docs/snippets/name-product.md" >}} installation identifies e - An operator applying a {{< gloss "Harness" >}}Harness{{< /gloss >}} is authenticated by [Kubernetes](#the-kubernetes-plane). - A caller that reaches [kagent's gRPC API](#the-kagent-control-plane) is assigned a principal by kagent, both for a {{< gloss "Session" >}}Session{{< /gloss >}} and for the resources that the API writes. +- A workload with no person signed in can call the API as its [Kubernetes ServiceAccount](#service-account-callers). - An agent's runtime calls the same API with a [token that Agent Substrate issues](#agent-runtime-calls). +- A service that an agent calls can receive an [Actor token](#actor-tokens-for-other-services) that identifies the agent. - The components inside [Agent Substrate](#the-agent-substrate-plane) authenticate each other. ## The Kubernetes plane @@ -20,7 +22,7 @@ Kubernetes role-based access control (RBAC) governs who can create, read, edit, Because neither a Harness nor an AgentTemplate names the other, the Agent resource carries the pairing, and RBAC on Agents governs it. Whoever can write Agents in a namespace controls which template runs on which Harness, even without edit access to the Harnesses and AgentTemplates that those Agents name. For more information about the pairing, see the [Agent core concept]({{< link path="about/core-concepts#agent" >}}). -kagent's gRPC API reaches these same resources by a second path that Kubernetes RBAC does not evaluate. For more information, see [Resources that the API writes](#resources-that-the-api-writes). +kagent's gRPC API reaches these same resources by a second path that Kubernetes RBAC does not evaluate. For more information, see [Resources that the API writes](#resources-that-the-api-writes). For what a namespace separates between teams, see [Namespace isolation]({{< link path="operations/multiple-namespaces#namespace-isolation" >}}). ## The kagent control plane @@ -34,22 +36,60 @@ The `config.server.auth.mode` Helm value selects which authenticator the control | Mode | How a principal is resolved | | ---- | --------------------------- | -| `insecure` | Every request is admitted. The caller's identity is read from the `X-User-Id` header, or from a `user_id` query parameter that takes precedence over it. A caller that supplies neither is named `admin@kagent.dev`. | -| `trusted-proxy` | The request must carry a bearer token in the `Authorization` header. The identity comes from the claim that `config.server.auth.userIdClaim` names, defaulting to `sub`. A missing custom claim falls back to `sub`, and a request with no identity is rejected. | +| `insecure` | Every request is admitted. The caller's identity is read from the `X-User-Id` header, or from a `user_id` query parameter that takes precedence over it. A caller that supplies neither is named `admin@kagent.dev`. **The caller selects its own principal.** The authenticator verifies neither the header nor the query parameter, and the same authenticator guards the controller's `/mcp` endpoint. Treat the principal on a call as a label that the caller chose, and restrict network access to both endpoints. | +| `trusted-proxy` | The request must carry a bearer token in the `Authorization` header. The identity comes from the claim that `config.server.auth.userIdClaim` names, defaulting to `sub`. A missing custom claim falls back to `sub`, and a request with no identity is rejected. **The authenticator decodes the token without verifying it.** It checks neither the signature nor the expiry, because it assumes that an upstream boundary already validated the credential. A caller that reaches the controller directly can present a token that it wrote itself. Use this mode only where ingress validates every token before forwarding it, and network policy stops anything else from reaching port `8083`. | -Any other value fails controller startup rather than falling back to a default. +Any other value stops the controller from starting. -> [!WARNING] -> **`trusted-proxy` decodes the token without verifying it.** The authenticator reads the bearer token's payload and checks neither its signature nor its expiry, because it assumes that an upstream boundary already validated the credential. A caller that reaches the controller directly can therefore present a token that it wrote itself. This mode is safe only where ingress validates every token before forwarding it, and network policy stops anything else from reaching port `8083`. +Authorization is separate from both modes. For user calls, kagent installs an authorizer that permits every check regardless of the authentication mode, so neither mode constrains what an authenticated user can do. For agent runtime calls, kagent applies a fixed policy that limits each Actor to its own resources. For more information, see [Agent runtime calls](#agent-runtime-calls). -> [!WARNING] -> **`insecure` lets a caller select its own principal.** It verifies neither the header nor the query parameter, and the same authenticator guards the controller's `/mcp` endpoint. Treat the principal on a call as a label that the caller chose, and restrict network access to both endpoints rather than relying on it. +### Service account callers -Authorization is separate from both modes. For user calls, kagent installs an authorizer that permits every check regardless of the authentication mode, so neither mode constrains what an authenticated user can do. For agent runtime calls, kagent applies a fixed policy that limits each Actor to its own resources. For more information, see [Agent runtime calls](#agent-runtime-calls). +Automation that runs with no person signed in, such as a CI pipeline, a scheduled job, or another service in the cluster, needs its own identity to create and use Sessions. Neither user authentication mode gives it a verified one. In `insecure` mode, the workload names itself. In `trusted-proxy` mode, the controller trusts a token that an upstream proxy validated, and does not verify the token itself. Service account authentication lets the workload call the controller as its Kubernetes ServiceAccount, and the controller verifies that identity with Kubernetes. + +Service account authentication is off by default and works alongside either user authentication mode. To turn it on, set `config.server.auth.serviceAccount.authority` in your Helm values. The authority is a name that you choose for the cluster that issues the tokens. The chart then grants the controller permission to create TokenReviews. + +```yaml +config: + server: + auth: + serviceAccount: + authority: prod-cluster +``` + +The workload sends a ServiceAccount token whose audience is `kagent-invocation` as a bearer token in the `Authorization` header. A workload usually mounts the token as a projected volume. To test from a terminal, create a token with `kubectl`, port-forward the controller's port `8083`, and call the API. The `grpcurl` command relies on gRPC reflection, so your installation must set `config.server.grpcReflection=true`. + +```bash +kubectl port-forward -n kagent deployment/kagent-controller 8083:8083 & +TOKEN=$(kubectl create token scheduler -n ci-bots --audience kagent-invocation) +grpcurl -plaintext -H "Authorization: Bearer $TOKEN" -d '{}' \ + localhost:8083 kagent.api.v1.SystemService/GetCurrentUser +``` + +Example output: + +```console +{ + "claims": { + "groups": [ + "system:serviceaccounts", + "system:serviceaccounts:ci-bots", + "system:authenticated" + ], + "username": "system:serviceaccount:ci-bots:scheduler" + } +} +``` + +The controller verifies the token through the Kubernetes TokenReview API of its own cluster. A token that has a `kubernetes.io` claim, or a subject that starts with `system:serviceaccount:`, always goes to this verifier. When verification fails, the controller rejects the call as unauthenticated and does not fall back to the user authentication mode. A ServiceAccount token whose audience is not `kagent-invocation` therefore fails, including a token for the default API server audience. Each controller caches a successful review for up to 30 seconds, so a deleted ServiceAccount or a revoked token keeps working for that interval. + +kagent builds the caller's principal from the authority and the ServiceAccount's UID, and records that principal as an opaque creator ID, such as `a3ViZXJuZXRlczpwcm9kLWNsdXN0ZXI.MTZkYjk1N2Qt...`. The [creator ownership](#creator-ownership) rules then scope the workload's Sessions in the same way as a user's, and the workload gets the same authorization as a user. Rotating the ServiceAccount's token keeps the principal so that the workload keeps its Sessions. Deleting and recreating the ServiceAccount gives it a new UID and a new principal, and the workload loses access to the Sessions that it created. + +When an agent forwards the caller's token to a model or MCP server that sets `tokenPassthrough`, that destination receives the ServiceAccount token. The token keeps its kagent authority until it expires. Set `tokenPassthrough` only on destinations that you trust with that authority. ### Resources that the API writes -kagent's gRPC API reaches the same resources by a second path. The AgentTemplate service creates, updates, and deletes AgentTemplates, and the Harness service creates and deletes Harnesses, both through the kagent controller. The `kagent apply -f` command calls the AgentTemplate service, and any client that reaches the gRPC endpoint can call either service. The controller writes these resources with its own service account rather than the caller's, so Kubernetes RBAC never evaluates the caller. The authenticator above resolves the caller instead. +kagent's gRPC API also writes Harness and AgentTemplate resources. The AgentTemplate service creates, updates, and deletes AgentTemplates, and the Harness service creates and deletes Harnesses, both through the kagent controller. The `kagent apply -f` command calls the AgentTemplate service, and any client that reaches the gRPC endpoint can call either service. The controller writes these resources with its own service account rather than the caller's, so Kubernetes RBAC never evaluates the caller. The controller's authenticator resolves the caller instead, as described in [Controller authentication modes](#controller-authentication-modes). > [!WARNING] > By default, kagent neither authenticates nor authorizes this path. The `insecure` authenticator admits every request, and the authorizer it installs permits every check. Any caller that reaches the gRPC endpoint can author an agent's Harness and AgentTemplate, regardless of their Kubernetes permissions. Do not expose port `8083` outside the cluster. For the identity that this mode gives an anonymous caller, and for the mode that changes it, see [Controller authentication modes](#controller-authentication-modes). @@ -69,9 +109,9 @@ A share lets a Session's owner give another account access to that one conversat Every share is read-only. The holder can read the conversation, but cannot send a message, answer an approval, cancel a task, or retry a turn. kagent enforces the restriction in its services and transports. A shared conversation in the UI does not show controls for those actions. -A client that creates a share can set `CreateSessionShareRequest.ttl` to a positive lifetime, which starts when the share is created. Once that lifetime ends, the token does not grant access, and a token lookup returns not found. The owner still sees the expired share in `ListSessionShares` and can revoke it. +A client that creates a share can set `CreateSessionShareRequest.ttl` to a positive lifetime. The lifetime starts when the share is created. Once that lifetime ends, the token does not grant access, and a token lookup returns not found. The owner still sees the expired share in `ListSessionShares` and can revoke it. -The returned `SessionShare.expiresAt` field records when the token stops granting access. Omit `ttl` and `expiresAt` stays unset. The share then stays valid until it is revoked or the Session is deleted. An operator caps share lifetimes with `config.sessions.shareMaxTtl`. For more information, see [Expire idle Sessions]({{< link path="operations/operational-considerations#expire-idle-sessions" >}}). +The returned `SessionShare.expiresAt` field records when the token stops granting access. When `ttl` is omitted, `expiresAt` stays unset, and the share stays valid until it is revoked or the Session is deleted. An operator caps share lifetimes with `config.sessions.shareMaxTtl`. For more information, see [Expire idle Sessions]({{< link path="operations/operational-considerations#expire-idle-sessions" >}}). A share widens what the holder can reach to what the owner can see, and the underlying record is read as the owner rather than as the visitor. Revoking the share withdraws that access. @@ -87,13 +127,92 @@ The runtime, the Agent Substrate egress gateway, and the controller each take on An authenticated Actor can read its own Agent and use its own task state and memories. The Actor can also read, update, and send messages to its own Session. The Actor cannot create or delete a Session, use a share, reach any other Session, or act with its owner's permissions. That restriction includes other Sessions of the same agent and the same owner. -The controller trusts the issuer in the `config.substrate.actorJwtIssuer` Helm value, which must match the `ateApi.actorJWTIssuer` value of the Agent Substrate chart. Both values default to `https://idp.ate-system.svc`, so change them only together. The controller fetches the issuer's signing keys at startup, and a controller that cannot reach the issuer does not start. +The controller trusts the issuer in the `config.substrate.actorJwtIssuer` Helm value. This value must match the `ateApi.actorJWTIssuer` value of the Agent Substrate chart. Both values default to `https://idp.ate-system.svc`, so change them only together. The controller fetches the issuer's signing keys at startup, and a controller that cannot reach the issuer does not start. Upgrade the controller and the runtime images together. An Actor snapshot from an earlier runtime image still sends an identity header that the controller no longer accepts, so you must recreate that snapshot after an upgrade. +### Actor tokens for other services + +An internal service that an agent calls, such as a large language model (LLM) gateway, an MCP server, or an API, might need to know which agent is calling so that it can authorize or audit the call. The other credentials do not identify the agent. A Secret-backed API key is the same for every agent that uses it, and `tokenPassthrough` forwards the identity of the user who started the Session. An unattended run has no such user. + +An Actor token identifies the calling {{< gloss "Actor" >}}Actor{{< /gloss >}} instead. The token is a short-lived JWT from the same Agent Substrate issuer that signs the tokens in [Agent runtime calls](#agent-runtime-calls), minted for an audience that you name. The egress gateway mints, injects, and refreshes the token, and the token never enters the runtime, so no Secret needs to be stored or rotated. + +Turn the token on for one destination in one of the following places: + +- A ModelConfig, in its `actorJwt` field. For more information, see [Authenticate as the Actor]({{< link path="setup/model-providers/about-model-providers#authenticate-as-the-actor" >}}). +- A RemoteMCPServer, in its `actorJwt` field. For more information, see [Actor token authentication]({{< link path="skills-and-mcp/about-tools#actor-token-authentication" >}}). +- An EgressPolicy destination, in its `authentication.actorJwt` field. For more information, see [Inject the Actor's token]({{< link path="agents/egress-policies#inject-the-actors-token" >}}). + +All three fields accept the same two settings, and only `audiences` is required. + +| Field | Description | +| ----- | ----------- | +| `audiences` | Required. The services that can accept the token, between 1 and 16 unique values. The API server rejects `kagent-runtime`, because that audience is reserved for calls to the controller. | +| `expirationSeconds` | Optional. The token lifetime, between 300 and 3600 seconds. Defaults to 900. | + +The receiving service gets a token that is signed with ES256 and contains the following claims: + +```json +{ + "iss": "https://idp.ate-system.svc", + "sub": "actor/kagent/session-01a18e6c-4454-7597-ba2b-e962d0f53967", + "aud": ["ticket-api"], + "actor.ate.dev": { + "atespace": "kagent", + "actorName": "session-01a18e6c-4454-7597-ba2b-e962d0f53967", + "actorUID": "3d0d0f07-40fb-4493-a7cd-3cb7f2d79c3b" + } +} +``` + +The `actor.ate.dev` claim names the Actor's {{< gloss "Atespace" >}}atespace{{< /gloss >}}, name, and UID. The atespace is the namespace of the Agent or SandboxTemplate. The token also contains the standard `exp`, `nbf`, `iat`, and `jti` claims. Each Session runs as its own Actor, so the Actor's name and UID change with every Session. To authorize a team or an environment from the token alone, base the decision on the atespace. To authorize by Agent, use the [Agent lookup](#agent-lookup). + +The receiving service must check the issuer, the signature, its own audience, and the time claims. The issuer is the `ateApi.actorJWTIssuer` value of the Agent Substrate chart. The issuer publishes its OpenID Connect discovery document at `/.well-known/openid-configuration` and its signing keys at `/openid/v1/jwks`. When `ateApi.actorJWTIssuer` is set to its default, the service reaches both over TLS inside the cluster, so it must also trust the certificate that the issuer serves. + +#### Agent lookup + +The token names the Actor and its atespace, but not the Agent. A receiving service that authorizes or audits by Agent can send the token to the controller's `kagent.api.v1.AgentIdentityService/ResolveAgentIdentity` method on port `8083`. The controller accepts only a token whose audiences include `kagent-agent-info`. Add that audience next to the service's own audience in the `actorJwt` field of the ModelConfig, RemoteMCPServer, or EgressPolicy that injects the token. + +```yaml +actorJwt: + audiences: [ticket-api, kagent-agent-info] +``` + +The service sends the token that it received as a bearer token, with an empty request. In the following command, `$ACTOR_TOKEN` holds that token. The `grpcurl` command relies on gRPC reflection, so your installation must set `config.server.grpcReflection=true`. The `-rpc-header` flag keeps the token off the reflection requests. + +```bash +grpcurl -plaintext -rpc-header "Authorization: Bearer $ACTOR_TOKEN" -d '{}' \ + kagent-controller.kagent.svc.cluster.local:8083 \ + kagent.api.v1.AgentIdentityService/ResolveAgentIdentity +``` + +Example output: + +```console +{ + "agent": { + "namespace": "kagent", + "name": "ticket-assistant", + "uid": "606e209e-e4a5-4520-929e-5a7f6f3d1926", + "labels": { + "team": "support" + }, + "annotations": { + "description": "Ticket triage" + } + }, + "sessionId": "01a18efc-83f3-7b12-a30a-73906fe617b0", + "runtimeRevision": "577c2a575fac1bf237a1fcac7674bc843feb1158b438f1c26d71bf73b735dc54" +} +``` + +The response contains the Agent's namespace, name, UID, labels, and annotations, and the ID of the Session and the revision that the Actor runs. The labels and annotations are the Agent's current values, returned unchanged. They include `kubectl.kubernetes.io/last-applied-configuration` when you applied the Agent with `kubectl apply`, so the service can read the Agent's full manifest. Do not put values in an Agent's labels or annotations that the receiving service must not see. The Session ID does not give the service access to the Session. + +A token without the `kagent-agent-info` audience fails with `Invalid Agent identity credential`. A token from a deleted Session, or from an Agent that was deleted and recreated, also does not resolve. + ## The Agent Substrate plane -{{< gloss "Agent Substrate" >}}Agent Substrate{{< /gloss >}} authenticates its own components rather than authenticating end users. Its API server accepts Kubernetes ServiceAccount tokens issued for its audience, and the components that carry traffic to an Actor authenticate each other with mutual Transport Layer Security (mTLS). The [kagent installation guide]({{< link path="setup/installation" >}}) covers creating the certificate authority pools and the JSON Web Token (JWT) authority pool that these identities are issued from, which is a required step that no Helm chart performs. +{{< gloss "Agent Substrate" >}}Agent Substrate{{< /gloss >}} authenticates its own components rather than authenticating end users. Its API server accepts Kubernetes ServiceAccount tokens issued for its audience, and the components that carry traffic to an Actor authenticate each other with mutual Transport Layer Security (mTLS). `kagent install` creates the certificate authority pools and the JWT authority pool that these identities are issued from. No Helm chart creates these pools. For an installation that you manage with Helm, see [Export the installation to Helm]({{< link path="operations/export-to-helm" >}}). Each Actor also carries an identity of its own, addressed as its {{< gloss "Atespace" >}}atespace{{< /gloss >}} and name together. [Sandboxing]({{< link path="about/substrate-runtime/sandboxing" >}}) covers how the router uses that identity to reach the right Worker over mTLS. diff --git a/docs-site/content/kagent/1.x/about/substrate-runtime/sandboxing.md b/docs-site/content/kagent/1.x/about/substrate-runtime/sandboxing.md index 9a92c614..dd17c964 100644 --- a/docs-site/content/kagent/1.x/about/substrate-runtime/sandboxing.md +++ b/docs-site/content/kagent/1.x/about/substrate-runtime/sandboxing.md @@ -33,74 +33,83 @@ flowchart LR A **sandbox class** is the sandbox runtime family that a Worker uses. {{< gloss "Agent Substrate" >}}Agent Substrate{{< /gloss >}} supports two. -- **`gvisor`** (default): Runs the workload against a [gVisor](https://gvisor.dev) user-space kernel, which keeps the workload's system calls from reaching the host kernel. -- **`microvm`**: Runs the workload inside a lightweight virtual machine, which places a hypervisor boundary between the workload and the host. +- **`gvisor`** (default): Runs the workload against a [gVisor](https://gvisor.dev) user-space kernel. The user-space kernel keeps the workload's system calls from reaching the host kernel. +- **`microvm`**: Runs the workload inside a lightweight virtual machine. A hypervisor boundary separates the workload from the host. -A {{< gloss "WorkerPool" >}}WorkerPool{{< /gloss >}}'s `sandboxClass` field sets the sandbox class for its Workers, and defaults to `gvisor`. That value does more than name a runtime. Agent Substrate builds the pool's Worker pods to match it, mounting the virtualization devices that a micro-VM needs and placing those pods on nodes that provide them. +A {{< gloss "WorkerPool" >}}WorkerPool{{< /gloss >}}'s `sandboxClasses` list sets the sandbox class for its Workers. Each entry names a class and, optionally, the SandboxConfig that serves it. The kagent Helm chart creates the default pool with one `gvisor` entry. Each class does more than name a runtime. Agent Substrate builds the pool's Worker pods to match it, mounting the virtualization devices that a micro-VM needs and placing those pods on nodes that provide them. > [!NOTE] -> kagent copies a pool's sandbox class onto every {{< gloss "ActorTemplate" >}}ActorTemplate{{< /gloss >}} that it generates for this pool. Each template then names the SandboxConfig that serves its class. A `gvisor` template names `gvisor-default`, and a `microvm` template names `microvm`. +> kagent copies the class of a pool's first `sandboxClasses` entry onto every {{< gloss "ActorTemplate" >}}ActorTemplate{{< /gloss >}} that it generates for this pool. The pool, not the template, resolves the SandboxConfig for that class. ## Sandbox configuration -A **SandboxConfig** is a cluster-scoped resource that describes the runtime files a sandbox class requires to start. The resource stores none of those files. Instead, it records the location that the node agent fetches each file from, keyed by processor architecture. It also names the pause image, which holds the sandbox's namespaces as its root container. +A **SandboxConfig** is a cluster-scoped resource that describes the runtime files a sandbox class requires to start. The resource stores none of those files. Instead, it records the location that the node agent fetches each file from, keyed by processor architecture. A SandboxConfig stores up to 16 named versions of these files. -Every ActorTemplate must name a SandboxConfig, and Agent Substrate does not supply a name of its own. A template cannot be prepared until the configuration that it names exists in the cluster. Many ActorTemplates can share one SandboxConfig. The runtime version is then pinned in a single place, rather than repeated on every template. +A WorkerPool resolves one SandboxConfig for each entry in its `sandboxClasses` list. An entry that sets `configRef` uses the named configuration. An entry without `configRef` uses the newest SandboxConfig of its class that has the `sandboxconfig.ate.dev/is-class-default: "true"` annotation. The pool reports the result in the `Resolved` condition under `status.sandboxClasses`. Agent Substrate blocks deletion of a SandboxConfig that a pool uses. -A default installation creates one SandboxConfig, named `gvisor-default`. kagent writes this name onto every ActorTemplate that it generates, so each template names it explicitly. The configuration looks like the following: +A default installation creates one SandboxConfig, named `gvisor-default`, and marks it as the `gvisor` class default. The kagent Helm chart also names it in the default pool's `configRef`. The configuration looks like the following: ```yaml -apiVersion: ate.dev/v1alpha1 +apiVersion: ate.dev/v1 kind: SandboxConfig metadata: name: gvisor-default + annotations: + sandboxconfig.ate.dev/is-class-default: "true" spec: sandboxClass: gvisor - pauseImage: registry.k8s.io/pause:3.10.2@sha256: - assets: - amd64: - gvisor: - url: gs://gvisor/releases/release/20260803/x86_64/gvisor.tar.bz2 - sha256: - arm64: - gvisor: - url: gs://gvisor/releases/release/20260803/aarch64/gvisor.tar.bz2 - sha256: + defaultVersion: v6 + versions: + - name: v6 + state: Enabled + assets: + amd64: + gvisor: + url: gs://gvisor/releases/release/20261026.0/x86_64/gvisor.tar.zstd + sha256: + arm64: + gvisor: + url: gs://gvisor/releases/release/20261026.0/aarch64/gvisor.tar.zstd + sha256: ``` +To read the configuration that your own cluster installed, including the pinned digests, run `kubectl get sandboxconfig gvisor-default -o yaml`. + | Field | Description | | ----- | ----------- | -| `sandboxClass` | The sandbox runtime family that this configuration applies to, `gvisor` or `microvm`. An ActorTemplate only uses configurations whose class matches its own, and preparation fails if the two disagree. | -| `pauseImage` | The image for the root sandbox container, which holds the sandbox's namespaces and does not run workload code. It must be pinned to a digest, because the snapshot manifest records it, and changing the image invalidates the snapshots that were taken with it. | -| `assets` | The files that the node agent fetches, keyed first by processor architecture and then by asset name. A `gvisor` class expects one `gvisor` asset, the release archive that the node agent extracts. A `microvm` class expects several, such as `cloud-hypervisor`, `kata-kernel`, and `kata-image`. | -| `assets...sha256` | The lowercase hex digest of the file. The node agent verifies each download against it, and caches the result under a path that includes the digest, so changing the digest fetches the new asset instead of reusing the cached one. To read the configuration that your own cluster installed, including the pinned digests, run `kubectl get sandboxconfig gvisor-default -o yaml`.| +| `sandboxClass` | The sandbox runtime family that this configuration applies to, `gvisor` or `microvm`. Defaults to `gvisor`. A WorkerPool uses only configurations whose class matches its entry. | +| `defaultVersion` | The version that new sandboxes boot with. It must name a version that exists and is not `Disabled`. | +| `versions[].name` | The version's name, a DNS label. | +| `versions[].state` | `Enabled` or `Disabled`. Defaults to `Enabled`. Worker pods keep every enabled version available, so a snapshot that was taken on an earlier version still restores. | +| `versions[].assets` | The files that the node agent fetches, keyed first by processor architecture and then by asset name. A `gvisor` class expects one `gvisor` asset, the release archive that the node agent extracts. A `microvm` class expects `cloud-hypervisor`, `virtiofsd`, `kata-kernel`, and `kata-image`. | +| `versions[].assets...sha256` | The lowercase hex digest of the file. The node agent verifies each download against it, so changing the digest fetches the new asset instead of reusing the cached one. | -## What the sandbox separates +## Sandbox boundaries {#what-the-sandbox-separates} -The sandbox draws a boundary in three places. +The sandbox separates the Actor from the Worker node in the following places. - **Process and kernel**: The Actor's processes run against the sandbox runtime rather than the Worker node's kernel. A system call that the workload makes is handled by {{< gloss "gVisor" >}}gVisor{{< /gloss >}}'s user-space kernel, or by the guest kernel inside a micro-VM, instead of reaching the host directly. - **Filesystem**: The Actor sees the filesystem assembled from its container image, plus whatever durable volume its ActorTemplate declares. Writes to the root filesystem are a layer on top of the image, captured in a `Full` {{< gloss "Snapshot" >}}snapshot{{< /gloss >}} and discarded by a `Data` one. For what each scope keeps, see [Suspend and resume]({{< link path="about/substrate-runtime/suspend-and-resume" >}}). - **Network**: The Actor does not share the Worker pod's network position. The node agent gives the active Actor a private, point-to-point virtual network inside the Worker pod, so reaching the Actor means going through Agent Substrate's own network path rather than connecting to the Worker directly. -## How traffic reaches a sandboxed Actor +## Traffic path to a sandboxed Actor {#how-traffic-reaches-a-sandboxed-actor} -Every Actor is addressed by its {{< gloss "Atespace" >}}atespace{{< /gloss >}} and name, at `..actors.resources.substrate.ate.dev`. Reaching it involves several hops, and each one keeps a sandboxed Actor addressable without exposing the Worker that it happens to be running on. +Every Actor is addressed by its {{< gloss "Atespace" >}}atespace{{< /gloss >}} and name, at `..actors.resources.substrate.ate.dev`. Reaching it involves several hops, and each one keeps a sandboxed Actor addressable without exposing the Worker that runs it. 1. Agent Substrate runs its own Domain Name System (DNS) service that answers queries for this address pattern with the address of the router, rather than any individual Worker. -2. The router reads the Actor name and atespace from the request, asks the Agent Substrate API to resume this Actor and report which Worker it is now assigned to, then selects this Worker as the destination. +2. The router reads the Actor name and atespace from the request, asks the Agent Substrate API to resume this Actor and report which Worker it is now assigned to, and then selects this Worker as the destination. 3. The router connects to a listener on the Worker over mutual Transport Layer Security (mTLS). The listener validates that the caller is the router, and forwards traffic only to the Actor currently assigned to this Worker. Because the router resolves the Worker assignment on every request, an Actor keeps a stable address across suspends, resumes, and moves between Workers. -A kagent user never uses this address. A message sent to a {{< gloss "Session" >}}Session{{< /gloss >}} through the kagent API or the web console reaches the kagent controller, which resolves the Session to its Actor and passes the request to Agent Substrate. Every hop after that stays inside the runtime. +A kagent user never uses this address. A message sent to a {{< gloss "Session" >}}Session{{< /gloss >}} through the kagent API or the web console reaches the kagent controller. The controller resolves the Session to its Actor and passes the request to Agent Substrate. Every hop after that stays inside the runtime. Traffic in the other direction leaves through a separate egress gateway rather than going straight out from the Worker. Routing Actor egress through one gateway provides a single place to apply outbound controls. ## Default network posture -Agent Substrate creates a Kubernetes NetworkPolicy for each WorkerPool, selecting this pool's Worker pods. The policy restricts **ingress** to the Agent Substrate router alone. No other pod in the cluster can open a connection to a Worker, so an Actor is not reachable by anything that bypasses the routing path. +Agent Substrate creates a Kubernetes NetworkPolicy for each WorkerPool that selects the pool's Worker pods. The policy restricts **ingress** to the Agent Substrate router alone. No other pod in the cluster can open a connection to a Worker, so an Actor is not reachable by anything that bypasses the routing path. -That policy governs inbound traffic only. Outbound traffic is governed separately by an egress policy that the gateway enforces on every connection that an Actor opens. That policy is default-deny. An Actor reaches a destination only when a rule allows it, and the gateway denies the connection when no rule does. +The NetworkPolicy governs inbound traffic only. Outbound traffic is governed separately by an egress policy that the gateway enforces on every connection that an Actor opens. The egress policy is default-deny. An Actor reaches a destination only when a rule allows it, and the gateway denies the connection when no rule does. -kagent derives the egress policy from the {{< gloss "AgentTemplate" >}}AgentTemplate{{< /gloss >}}, so an agent does not need egress configuration of its own. The destinations it allows come from what the AgentTemplate already names, such as its model provider and its MCP servers. Agent Substrate owns the ingress policy, and kagent regenerates the egress policy whenever the AgentTemplate changes. To allow a destination that the AgentTemplate does not name, select an EgressPolicy resource on the Agent, as described in [Grant access to a destination]({{< link path="agents/egress-policies#grant-access-to-a-destination" >}}). For what the policy holds, how a rule is matched, and how to diagnose a denied request, see [Egress control]({{< link path="about/substrate-runtime/networking-and-egress" >}}). +kagent derives the egress policy from the {{< gloss "AgentTemplate" >}}AgentTemplate{{< /gloss >}}, so an agent does not need egress configuration of its own. The destinations that the egress policy allows come from what the AgentTemplate already names, such as its model provider and its MCP servers. Agent Substrate owns the ingress policy, and kagent regenerates the egress policy whenever the AgentTemplate changes. To allow a destination that the AgentTemplate does not name, select an EgressPolicy resource on the Agent or SandboxTemplate, as described in [Grant access to a destination]({{< link path="agents/egress-policies#grant-access-to-a-destination" >}}). For what the policy holds, how a rule is matched, and how to diagnose a denied request, see [Egress control]({{< link path="about/substrate-runtime/networking-and-egress" >}}). diff --git a/docs-site/content/kagent/1.x/about/substrate-runtime/standalone-sandboxes.md b/docs-site/content/kagent/1.x/about/substrate-runtime/standalone-sandboxes.md index 91dd69d7..efd54e96 100644 --- a/docs-site/content/kagent/1.x/about/substrate-runtime/standalone-sandboxes.md +++ b/docs-site/content/kagent/1.x/about/substrate-runtime/standalone-sandboxes.md @@ -1,13 +1,13 @@ --- title: Standalone sandboxes -description: Run commands and transfer files in a scratch environment that no agent conversation owns. +description: Run commands and transfer files in a scratch environment that is independent of any agent Session. weight: 25 author: kagent.dev aliases: - /kagent/1.x/substrate-runtime/standalone-sandboxes/ --- -A **Sandbox** is a scratch environment for running commands and working with files. It is scoped to the caller who created it, it expires on a timer, and no agent conversation owns it. You create one from a **SandboxTemplate**, run processes and move files in it, then delete it or let it expire. +A **Sandbox** is a scratch environment for running commands and working with files. A Sandbox is scoped to the caller who created it, and it expires on a timer. You create one from a **SandboxTemplate**, run processes and move files in it, then delete it or let it expire. A Sandbox and a {{< gloss "Session" >}}Session{{< /gloss >}} are separate runtimes with separate configuration and separate lifetimes. Neither resource references the other. @@ -16,11 +16,11 @@ A Sandbox and a {{< gloss "Session" >}}Session{{< /gloss >}} are separate runtim | An {{< gloss "Agent" >}}Agent{{< /gloss >}} that pairs an AgentTemplate with a Harness | A Session | A2A interactions and tasks | | A SandboxTemplate that defines a tools environment | A Sandbox | Guest process and file operations | -An agent can create a Sandbox of its own through the kagent {{< gloss "MCP" >}}MCP{{< /gloss >}} server, using the same service that a person calls. The agent's conversation and the Sandbox's lifetime stay independent of each other. +An agent can create a Sandbox of its own through the kagent Model Context Protocol ({{< gloss "MCP" >}}MCP{{< /gloss >}}) server, by using the same service that a person calls. The Sandbox's lifetime stays independent of the agent's Session. ## Before you begin -1. [Install kagent]({{< link path="setup/installation" >}}) and set the following `config.sandboxes` values on the installation. Sandbox preparation needs a guest image, and the chart does not ship a default, so a SandboxTemplate never becomes ready until you set one. The remaining values allocate the compute that each Sandbox runs on and bound how long it lives. +1. [Install kagent]({{< link path="setup/installation" >}}) and set the following `config.sandboxes` values on the installation. Sandbox preparation needs a guest image. The chart does not ship a default guest image, so a SandboxTemplate does not become ready until you set one. The remaining values allocate the compute that each Sandbox runs on and bound how long it lives. ```yaml config: sandboxes: @@ -31,22 +31,19 @@ An agent can create a Sandbox of its own through the kagent {{< gloss "MCP" >}}M maxTtl: 86400s ``` - A SandboxTemplate cannot override these values, so they are the installation's policy rather than a per-template choice. + A SandboxTemplate cannot override these values, so they apply to every Sandbox in the installation. | Value | Default | Description | | ----- | ------- | ----------- | - | `config.sandboxes.guestImage` | `""` | The digest-pinned guest image, as a complete reference. Required: preparation fails without it. The controller passes the reference to Agent Substrate unchanged and does not resolve tags, so pin a digest rather than a tag. Setting `global.imageRegistry` rewrites the registry host of this reference. | + | `config.sandboxes.guestImage` | `""` | The digest-pinned guest image, as a complete reference. This value is required. The controller passes the reference to Agent Substrate unchanged and does not resolve tags, so pin a digest. Setting `global.imageRegistry` rewrites the registry host of this reference. | | `config.sandboxes.cpu` | `1` | CPU allocated to each Sandbox. | | `config.sandboxes.memory` | `1Gi` | Memory allocated to each Sandbox. | | `config.sandboxes.defaultTtl` | `3600s` | Lifetime applied when `kagent sandbox create` omits `--ttl`. Durations in the `config` block take protobuf seconds notation, so `3600s` rather than `1h`. | - | `config.sandboxes.maxTtl` | `86400s` | Upper bound on any requested lifetime, which the controller caps at `86400s`. Note that activity does not extend a Sandbox's lifetime. A Sandbox expires the configured interval after it is created, however recently a command ran in it. | + | `config.sandboxes.maxTtl` | `86400s` | Upper bound on any requested lifetime, which the controller caps at `86400s`. A Sandbox expires the configured interval after it is created, regardless of how recently a command ran in it. | -2. Download the kagent CLI. The `--version` flag matches the CLI to the release that these docs cover. - ```bash - curl https://raw.githubusercontent.com/kagent-dev/kagent/refs/heads/main/scripts/get-kagent | bash -s -- --version v{{< reuse "kagent-docs/versions/kagent.md" >}} - ``` +2. {{< reuse "kagent-docs/snippets/install-cli.md" >}} -3. Install [`jq`](https://jqlang.org/download/) to read the Sandbox ID out of the CLI's JSON output. +3. To read the Sandbox ID from the CLI's JSON output, install [`jq`](https://jqlang.org/download/). ## Create a SandboxTemplate @@ -80,12 +77,12 @@ A SandboxTemplate is a namespaced `api.kagent.dev/v1alpha3` resource that prepar | Field | Required | Description | | ----- | -------- | ----------- | | `workload.image` | Yes | The tools image, pinned by `sha256` digest. A tag alone is rejected, because a prepared revision must be reproducible. | - | `env` | No | Environment defaults for the guest, up to 100. Each entry sets a literal `value`, which is required and may be an empty string. The schema does not define a secret-backed source, so the API server rejects a `credentialRef` entry as an unknown field. | + | `env` | No | Environment defaults for the guest, up to 100. Each entry sets a literal `value`, which is required and can be an empty string. The schema does not define a secret-backed source, so the API server rejects a `credentialRef` entry as an unknown field. | | `substrate.workerPoolRef.name` | Yes | The WorkerPool that this template's Actors are scheduled onto. | | `substrate.snapshotPolicy.location` | Yes | The object storage location for Actor {{< gloss "Snapshot" >}}snapshots{{< /gloss >}}. | - | `egressPolicyRefs` | No | Up to 16 EgressPolicy resources in the template's namespace that grant the Sandbox outbound access. When this field is omitted, the Sandbox has no outbound access. For more information, see [Egress policies]({{< link path="agents/egress-policies" >}}). | + | `egressPolicyRefs` | No | Up to 16 EgressPolicy resources in the template's namespace that grant the Sandbox outbound access. When this field is omitted, the Sandbox does not have outbound access. For more information, see [Egress policies]({{< link path="agents/egress-policies" >}}). | - A SandboxTemplate does not take a startup command or a guest toggle. kagent supplies the guest entrypoint from the image that `config.sandboxes.guestImage` names. That image replaces the tools image's own entrypoint. Your tools image contributes the installed programs and nothing else. + A SandboxTemplate does not take a startup command or a guest toggle. kagent supplies the guest entrypoint from the image that `config.sandboxes.guestImage` references. That image replaces the tools image's own entrypoint. Your tools image contributes the installed programs and nothing else. 2. Confirm that the template prepared a revision before you create a Sandbox from it. ```bash @@ -93,15 +90,15 @@ A SandboxTemplate is a namespaced `api.kagent.dev/v1alpha3` resource that prepar -o jsonpath='{range .status.conditions[?(@.type=="Ready")]}{.status} {.reason} {.message}{end}' ``` -Editing a template prepares a new revision. A Sandbox that already exists keeps the revision it was created from, and deleting the template retires preparation without removing the Sandboxes that pinned it. +Editing a template prepares a new revision. A Sandbox that already exists keeps the revision that it was created from. Deleting the template retires preparation but does not remove the Sandboxes that pinned it. ## Run a command +Each `kagent sandbox` command makes one lifecycle attempt. The `create` command takes a stable `--request-id` value. To retry the same creation, reuse that value. + > [!WARNING] > **By default, the gateway denies every outbound connection from a Sandbox.** When the template does not select an EgressPolicy resource, kagent compiles an empty [egress policy]({{< link path="about/substrate-runtime/networking-and-egress#policy-generation" >}}) for the Sandbox. A command that fetches a package, clones a repository, or calls an API then fails. To allow a destination, select an EgressPolicy in the SandboxTemplate's `spec.egressPolicyRefs` field, as described in [Grant access to a destination]({{< link path="agents/egress-policies#grant-access-to-a-destination" >}}). Otherwise, move what a command needs into the Sandbox with `kagent sandbox upload`. -Each `kagent sandbox` command makes one lifecycle attempt rather than retrying for you, so `create` takes a stable `--request-id` that you reuse to retry the same creation. - 1. List the templates that your installation prepared. ```bash kagent sandbox templates @@ -119,7 +116,7 @@ Each `kagent sandbox` command makes one lifecycle attempt rather than retrying f echo $SANDBOX_ID ``` - Run the command without `-o json` to see the table instead. `STATE` and `OPERATION` both matter, because lifecycle work can still be pending when a call returns. + To see the table, run the command without `-o json`. `STATE` and `OPERATION` both matter, because lifecycle work can still be pending when a call returns. ```console ID TEMPLATE STATE OPERATION EXPIRES FAILURE 0198c3f1-2a44-7c90-b5e1-9d8f3a7b2c04 kagent/scratch RUNTIME_STATE_READY RUNTIME_OPERATION_NONE 2026-10-01T16:30:00Z @@ -130,7 +127,9 @@ Each `kagent sandbox` command makes one lifecycle attempt rather than retrying f kagent sandbox exec $SANDBOX_ID -- python --version ``` - A timeout stops the command from waiting rather than stopping the remote process. To reconnect to a process that outlived its `exec`, pass its process ID to `kagent sandbox wait`. + A timeout stops the command from waiting, but the remote process keeps running. To reconnect to a process that outlived its `exec`, pass its process ID to `kagent sandbox wait`. + + `kagent sandbox kill` sends `SIGKILL` to a process and returns when the guest delivers the signal. The command does not wait for the process to exit, so its output reports the process state and omits the exit code. To read the exit code, pass the process ID to `kagent sandbox wait`. 4. Move a file in, run against it, and read the result back out. ```bash @@ -139,17 +138,17 @@ Each `kagent sandbox` command makes one lifecycle attempt rather than retrying f kagent sandbox download $SANDBOX_ID /data/workspace/out.txt ./out.txt ``` -5. Delete the Sandbox when you are finished. Deleting is not required, because the Sandbox expires on its own. Deleting it releases the compute immediately. +5. When you are finished, delete the Sandbox to release its compute immediately. Deletion is optional, because the Sandbox expires on its own. ```bash kagent sandbox delete $SANDBOX_ID ``` > [!IMPORTANT] -> **Suspending a Sandbox interrupts whatever it is doing.** `kagent sandbox suspend` waits for no process, output stream, or file transfer, so a command can be cut off mid-run and a file can be left partly written. Resuming restores the durable files under `/data`, but a process handle does not survive, because the guest holds it in memory. Suspend a Sandbox only when you can repeat whatever it was running. +> **Suspending a Sandbox interrupts whatever it is doing.** `kagent sandbox suspend` does not wait for any process, output stream, or file transfer, so a command can be cut off mid-run and a file can be left partly written. Resuming restores the durable files under `/data`, but a process handle does not survive, because the guest holds it in memory. Suspend a Sandbox only when you can repeat whatever it was running. -## Reach a sandbox from an agent +## Reach a Sandbox from an agent -kagent's Helm chart installs a `RemoteMCPServer` named `kagent-api` in the controller's namespace, pointing at the controller's own `/mcp` endpoint. This server exposes the Session and checkpoint tools alongside the sandbox tools, so an AgentTemplate reaches sandboxes through an ordinary tool binding. +kagent's Helm chart installs a `RemoteMCPServer` named `kagent-api` in the controller's namespace, pointing at the controller's own `/mcp` endpoint. This server exposes the Session and checkpoint tools alongside the sandbox tools, so an AgentTemplate reaches Sandboxes through an ordinary tool binding. ```yaml tools: @@ -166,15 +165,17 @@ tools: - read_sandbox_outputs ``` -An agent that creates a Sandbox owns it under whatever identity the MCP connection authenticated, not under the identity of the person it is talking to. A Session share token does not grant access to a Sandbox. To have an agent act for the person who invoked it, configure credential propagation. Set `config.runtimeDefaults.tokenPropagation.enabled` to `true`, and the kagent runtime passes the caller's credentials and identity to the MCP servers that you trust. +An agent that creates a Sandbox owns it under the identity that the MCP connection authenticated, not under the identity of the person that the agent is talking to. A Session share token does not grant access to a Sandbox. To have an agent act for the person who invoked it, configure credential propagation by setting `config.runtimeDefaults.tokenPropagation.enabled` to `true`. The kagent runtime then passes the caller's credentials and identity to the MCP servers that you trust. The MCP transfer limits are tighter than the command line's. A gRPC file transfer is bounded at 64 MiB. `write_sandbox_file` and `read_sandbox_outputs` are bounded at 1 MiB and use base64. Output reads return continuation byte offsets. -When an agent must inspect a result file, use `read_sandbox_file`. The tool returns content that the model reads directly rather than `data_base64`, in one of three shapes. +An agent that starts a process with `start_sandbox_process` polls `get_sandbox_process` to learn when the process finishes. The `state` field reads `PROCESS_STATE_RUNNING` until the process exits, then `PROCESS_STATE_EXITED`. The `exit_code` field appears only in the exited state, and reads `0` for a successful exit. `kill_sandbox_process` sends `SIGKILL` in the same way as `kagent sandbox kill`, so the agent must poll `get_sandbox_process` for the exit code. + +When an agent must inspect a result file, use `read_sandbox_file`. The tool returns content that the model reads directly, not `data_base64`. The shape of the content depends on the file type. -Text returns as numbered lines. `offset` takes the first line to read, counting from 1, and `limit` takes the number of lines, which defaults to 2000. A page also stops at 32 KiB, whichever bound it reaches first. A line longer than 2000 bytes is truncated to 2000 bytes and marked `[line cut]`. A page that stops before the end of the file reports the offset to continue from. +Text returns as numbered lines. `offset` sets the first line to read, counting from 1. `limit` sets the number of lines, and defaults to 2000. A page also stops at 32 KiB, whichever bound it reaches first. A line longer than 2000 bytes is truncated to 2000 bytes and marked `[line cut]`. A page that stops before the end of the file reports the offset to continue from. -A PNG, JPEG, or GIF file of at most 10 MiB returns as an image. The Claude and Codex harnesses resize those images for their models. A larger image returns a message asking the agent to write a smaller copy in the Sandbox. Every other file type returns a description of the file rather than its bytes. +A PNG, JPEG, or GIF file of at most 10 MiB returns as an image. The `claude` and `codex` harnesses resize those images for their models. A larger image returns a message asking the agent to write a smaller copy in the Sandbox. Every other file type returns a description of the file, not its bytes. ## Next steps diff --git a/docs-site/content/kagent/1.x/about/what-is-kagent.md b/docs-site/content/kagent/1.x/about/what-is-kagent.md index be92a62e..e1342092 100644 --- a/docs-site/content/kagent/1.x/about/what-is-kagent.md +++ b/docs-site/content/kagent/1.x/about/what-is-kagent.md @@ -38,7 +38,7 @@ kagent 1.0 separates an agent's capabilities from its runtime, then runs the two {{< feature-card title="Agent tools" desc="Bind another AgentTemplate as a tool, so an agent can hand work to a specialist. A Shared binding nests that agent inside its parent's Actor, one level deep." >}} {{< feature-card title="Long-term memory" desc="Persistent, vector-backed memory across sessions. Agents remember context, not just the last prompt." >}} {{< feature-card title="Human-in-the-loop" desc="Tool approval gates and agent-initiated questions keep a person in control of consequential actions." >}} -{{< feature-card title="Agent-to-Agent (A2A)" desc="Agents talk to callers, and to each other, over the A2A protocol." >}} +{{< feature-card title="Agent2Agent (A2A)" desc="Agents talk to callers, and to each other, over the A2A protocol." >}} {{< feature-card title="Skills and plugins" desc="Load skills and plugins from an Open Container Initiative (OCI) registry, Git, or S3 at startup." >}} {{< feature-card title="Prompt templates" desc="Reusable prompt fragments stored as ConfigMaps. Keep system prompts consistent across agents." >}} {{< feature-card title="Observability" desc="OpenTelemetry tracing, structured logs, and an optional Prometheus metrics endpoint, with control plane traces carrying the Actor that they belong to." >}} diff --git a/docs-site/content/kagent/1.x/agents/agent-harness.md b/docs-site/content/kagent/1.x/agents/agent-harness.md index 7faa0db9..8226af91 100644 --- a/docs-site/content/kagent/1.x/agents/agent-harness.md +++ b/docs-site/content/kagent/1.x/agents/agent-harness.md @@ -395,7 +395,7 @@ When an agent requires an MCP server, the first tool listing or tool call opens ## Check that an agent can run -To check the status of a Harness, AgentTemplate, or Agent, check the status of only the Agent. It reports for all three resources, because neither a Harness nor an AgentTemplate reports a status of its own. For example, the `READY` column in `kubectl get harness` output is always empty. +To check the status of a Harness, AgentTemplate, or Agent, check the status of only the Agent. The Agent's status reports for all three resources, because neither a Harness nor an AgentTemplate reports a status of its own. An Agent that is not `Ready` states the reason in its conditions. diff --git a/docs-site/content/kagent/1.x/agents/agent-memory.md b/docs-site/content/kagent/1.x/agents/agent-memory.md index fd966945..3d30cdc8 100644 --- a/docs-site/content/kagent/1.x/agents/agent-memory.md +++ b/docs-site/content/kagent/1.x/agents/agent-memory.md @@ -137,7 +137,7 @@ Add memory to a Harness that already exists. The examples in these steps use `my {{< reuse "kagent-docs/snippets/review-table.md" >}} To review other available Harness fields, see [Agent harness]({{< link path="agents/agent-harness" >}}). | Field | Description | | ----- | ----------- | - | `memory.modelConfigRef.name` | The ModelConfig supplying the embedding model, in the Harness's namespace. Required when `memory` is set. | + | `memory.modelConfigRef.name` | The ModelConfig supplying the embedding model. Required when `memory` is set. The namespace defaults to the Harness's namespace. To use a ModelConfig in another namespace, set `memory.modelConfigRef.namespace`. That ModelConfig must admit the Harness's namespace, as described in [Share a ModelConfig across namespaces]({{< link path="setup/model-providers/about-model-providers#share-a-modelconfig-across-namespaces" >}}). | | `memory.ttlDays` | How many days a stored memory stays valid. Minimum 1. When omitted, the server applies a default of 15 days. | 3. Create a new Session against an {{< gloss "Agent" >}}Agent{{< /gloss >}} that uses this Harness. Editing the Harness builds a new {{< gloss "Revision" >}}revision{{< /gloss >}}, and an existing Session keeps running the revision it was created from. An agent that was already running does not gain memory until you start a new Session. diff --git a/docs-site/content/kagent/1.x/agents/bring-your-own-agent.md b/docs-site/content/kagent/1.x/agents/bring-your-own-agent.md index 03cab8d0..5ed63b81 100644 --- a/docs-site/content/kagent/1.x/agents/bring-your-own-agent.md +++ b/docs-site/content/kagent/1.x/agents/bring-your-own-agent.md @@ -9,7 +9,7 @@ The `byo` harness runs a container image that you build, and treats what is insi ## Responsibilities on each side of the A2A boundary -{{< gloss "A2A" >}}A2A{{< /gloss >}} (Agent-to-Agent) is the contract between kagent and a BYO agent. +{{< gloss "A2A" >}}A2A{{< /gloss >}} (Agent2Agent) is the contract between kagent and a BYO agent. Your image owns one side of the A2A contract by: diff --git a/docs-site/content/kagent/1.x/agents/context-management.md b/docs-site/content/kagent/1.x/agents/context-management.md index bd6af69b..a1ef7c0e 100644 --- a/docs-site/content/kagent/1.x/agents/context-management.md +++ b/docs-site/content/kagent/1.x/agents/context-management.md @@ -37,8 +37,8 @@ spec: # but keep the 10 most recent events uncompacted. tokenThreshold: 24000 eventRetentionSize: 10 - # Optional: name a ModelConfig (in this Harness's namespace) to write the - # summaries. When you omit it, the agent's own model summarizes. + # Optional: name a ModelConfig to write the summaries. Add namespace to use + # a shared ModelConfig. When you omit it, the agent's own model summarizes. summarizer: modelConfigRef: name: summarizer-model-config @@ -67,7 +67,7 @@ The table below lists the compaction fields and their requirements: | `overlapSize` | Optional | The number of already-compacted invocations pulled back into the next sliding window so consecutive summaries overlap. Requires `compactionInterval`. Minimum 0. | | `tokenThreshold` | Optional | The prompt token count at which tail-retention compaction summarizes the history before the next model call. Requires `eventRetentionSize`. Minimum 1. | | `eventRetentionSize` | Optional | The number of most recent events that tail retention keeps uncompacted. Requires `tokenThreshold`. Minimum 1. | -| `summarizer.modelConfigRef` | Optional | The {{< gloss "ModelConfig" >}}ModelConfig{{< /gloss >}} in the Harness namespace that writes the summaries. When you omit it, the agent's own model summarizes. | +| `summarizer.modelConfigRef` | Optional | The {{< gloss "ModelConfig" >}}ModelConfig{{< /gloss >}} that writes the summaries, by `name` and optional `namespace`. The namespace defaults to the Harness namespace. A ModelConfig in another namespace must admit the Harness namespace, as described in [Share a ModelConfig across namespaces]({{< link path="setup/model-providers/about-model-providers#share-a-modelconfig-across-namespaces" >}}). When you omit the field, the agent's own model summarizes. | | `summarizer.promptTemplate` | Optional | Replaces the runtime's default summarization prompt. Must contain `{conversation_history}`, which the runtime replaces with the rendered events. | ## Admission rules @@ -87,7 +87,7 @@ Naming an AgentTemplate's own model in `summarizer.modelConfigRef` is semantical ## Verify the configuration -If the summarizer names a ModelConfig that does not exist in the Harness namespace, the Agent is not ready. The failure surfaces on the Agent as `ResolvedRefs=False`, with a reason such as `resolve summarizer ModelConfig "x": model config "x" not found`. Confirm that the Agent is ready before relying on compaction: +If the summarizer names a ModelConfig that does not exist, the Agent is not ready. The failure surfaces on the Agent as `ResolvedRefs=False`, with a reason such as `resolve summarizer ModelConfig "x": model config "x" not found`. Confirm that the Agent is ready before relying on compaction: ```bash kagent agent get diff --git a/docs-site/content/kagent/1.x/agents/declarative-agents.md b/docs-site/content/kagent/1.x/agents/declarative-agents.md index 61e2c1d0..43d1e5c7 100644 --- a/docs-site/content/kagent/1.x/agents/declarative-agents.md +++ b/docs-site/content/kagent/1.x/agents/declarative-agents.md @@ -43,7 +43,7 @@ Compaction offers two strategies. Tail retention summarizes the older history on | Field | Description | | ----- | ----------- | -| `memory.modelConfigRef.name` | The {{< gloss "ModelConfig" >}}ModelConfig{{< /gloss >}} that supplies the embedding model, in the Harness's namespace. Required when `memory` is set. Setting `memory` gives every agent on this Harness memory that persists across conversations. For how agents store and retrieve it, see [Agent memory]({{< link path="agents/agent-memory" >}}). | +| `memory.modelConfigRef.name` | The {{< gloss "ModelConfig" >}}ModelConfig{{< /gloss >}} that supplies the embedding model. Required when `memory` is set. To use a ModelConfig outside the Harness's namespace, also set `memory.modelConfigRef.namespace`. That ModelConfig must admit the Harness's namespace, as described in [Share a ModelConfig across namespaces]({{< link path="setup/model-providers/about-model-providers#share-a-modelconfig-across-namespaces" >}}). Setting `memory` gives every agent on this Harness memory that persists across conversations. For how agents store and retrieve it, see [Agent memory]({{< link path="agents/agent-memory" >}}). | | `memory.ttlDays` | How many days a stored memory entry stays valid. Minimum 1. When omitted, the server applies a default of 15 days. | | `compaction.tokenThreshold` | Tail retention. The prompt token count that triggers a summary. Once the prompt passes this count, kagent summarizes the older history before the next model call. Requires `compaction.eventRetentionSize`. | | `compaction.eventRetentionSize` | Tail retention. The number of most recent events that kagent leaves out of that summary, so the latest exchanges reach the model in full. Requires `compaction.tokenThreshold`. | diff --git a/docs-site/content/kagent/1.x/agents/egress-policies.md b/docs-site/content/kagent/1.x/agents/egress-policies.md index e7516c09..57d8dc3f 100644 --- a/docs-site/content/kagent/1.x/agents/egress-policies.md +++ b/docs-site/content/kagent/1.x/agents/egress-policies.md @@ -7,9 +7,9 @@ author: kagent.dev An {{< gloss "Agent" >}}Agent{{< /gloss >}} reaches only the destinations that its AgentTemplate needs, such as its model provider, its Model Context Protocol (MCP) servers, and its skill sources. {{< reuse "kagent-docs/snippets/name-product.md" >}} infers these destinations and compiles them into the agent's [egress policy]({{< link path="about/substrate-runtime/networking-and-egress#policy-generation" >}}). The egress gateway denies every other destination. -To grant an Agent or SandboxTemplate access to destinations beyond the inferred set, you can create an EgressPolicy resource. The Agent or SandboxTemplate selects one or more policies by name in its `spec.egressPolicyRefs` field. An EgressPolicy only adds access, and cannot remove a destination that kagent inferred from the model or the tools. +To grant an Agent or SandboxTemplate access to destinations beyond the inferred set, create an EgressPolicy resource. The Agent or SandboxTemplate selects one or more policies by name in its `spec.egressPolicyRefs` field. An EgressPolicy only adds access, and cannot remove a destination that kagent inferred from the model or the tools. -A policy can also reference a credential in a Kubernetes Secret. The gateway adds the credential to each request, so the value never enters the agent's sandbox. Typical uses include a package registry that a skill installs from, or an internal API that a tool script calls. +A policy can also have the gateway add a credential to each request, either a value from a Kubernetes Secret or a token that identifies the calling agent. The credential never enters the agent's sandbox. Typical uses include a package registry that a skill installs from, or an internal API that a tool script calls. ## Before you begin @@ -93,7 +93,7 @@ A wildcard can replace the leftmost label of the hostname. For example, `https:/ A redirect to another origin needs its own grant. For example, a package registry often serves downloads from a separate host. An install fails at that host until the policy grants it as well. -The API server rejects a policy or an `egressPolicyRefs` list that exceeds the first two of the following limits. The compiled policy limit also counts the destinations that kagent inferred, so an Agent with many tools reaches it sooner. +The API server rejects an `egressPolicyRefs` list that selects more than 16 policies and a policy that lists more than 64 destinations. The compiled policy limit also counts the destinations that kagent inferred, so an Agent with many tools reaches it sooner. | Limit | Value | | ----- | ----- | @@ -104,9 +104,9 @@ The API server rejects a policy or an `egressPolicyRefs` list that exceeds the f ## Inject a credential -The gateway can add a credential from a Kubernetes Secret to each request for an origin. The agent sends the header with a placeholder value, and the gateway replaces this value as the request passes through. The Secret value is not stored in the agent's revision and never reaches the sandbox. +The gateway can add a credential to each request for an origin. The agent sends the header with a placeholder value, and the gateway replaces this value as the request passes through. The credential is not stored in the agent's revision and never reaches the sandbox. The following steps inject a value from a Kubernetes Secret. To inject a token that identifies the calling agent, see [Inject the Actor's token](#inject-the-actors-token). -1. Create the Secret in the namespace of the EgressPolicy. +1. Create the Secret in the namespace of the EgressPolicy. To rotate the credential later, update the Secret. The gateway reads the value from the Secret, so the agent's revision does not change. ```bash kubectl create secret generic ticket-auth -n kagent \ --from-literal=token= @@ -139,7 +139,7 @@ The gateway can add a credential from a Kubernetes Secret to each request for an | `origin` | The destination. An origin with a credential must be exact, so the API server rejects a wildcard origin. On a cleartext `http` origin, the credential is readable on the path between the gateway and the destination. Use an `http` origin with a credential only for an in-cluster service whose traffic a service mesh encrypts. | | `authentication.header` | The request header that carries the credential, such as `Authorization` or `X-API-Key`. The API server rejects the headers that control routing and framing, such as `Host` and `Content-Length`, and the `Proxy-Authorization`, `Proxy-Authenticate`, and `Cookie` headers. | | `authentication.prefix` | Optional. Text that the gateway puts before the Secret value. The prefix is literal. Include the trailing space, as in `"Bearer "`. When the prefix is omitted or empty, the header contains only the Secret value. | - | `authentication.secretKeyRef` | The Secret and key that store the credential. Both `name` and `key` are required, and the reference cannot be optional. | + | `authentication.secretKeyRef` | The Secret and key that store the credential. Both `name` and `key` are required, and the reference cannot be optional. A destination sets exactly one of `secretKeyRef` and `actorJwt`. For the token alternative, see [Inject the Actor's token](#inject-the-actors-token). | 3. Select the policy. A merge patch replaces the whole `egressPolicyRefs` list, so the command also keeps `node-dependencies`. Include every policy that the resource needs. {{< tabs >}} @@ -157,13 +157,66 @@ The gateway can add a credential from a Kubernetes Secret to each request for an {{% /tab %}} {{< /tabs >}} -4. In the script or software development kit (SDK) that sends the request, send the header with a placeholder value. The gateway replaces a header that the request already sends, and it does not add a header that is missing. Selecting a policy does not configure a tool or set an environment variable. The client must also trust the egress gateway's certificate authority, which kagent provides to every Actor. For the variables that carry the trust bundle, see [Environment variables on an Actor]({{< link path="agents/agent-harness#environment-variables-on-an-actor" >}}). +4. In the script or SDK that sends the request, send the header with a placeholder value. The gateway replaces a header that the request already sends, and it does not add a header that is missing. Selecting a policy does not configure a tool or set an environment variable. The client must also trust the egress gateway's certificate authority. kagent provides this certificate authority to every Actor. For the variables that carry the trust bundle, see [Environment variables on an Actor]({{< link path="agents/agent-harness#environment-variables-on-an-actor" >}}). ```bash curl -H 'Authorization: Bearer kagent-credential-injected' \ https://tickets.example.com/tickets ``` -To rotate the credential, update the Secret. The gateway reads the value from the Secret, so the agent's revision does not change. +### Inject the Actor's token + +A Secret-backed credential is the same for every agent that selects the policy, so the destination cannot tell the agents apart. When an internal API must authorize or audit each agent, inject a token that identifies the calling {{< gloss "Actor" >}}Actor{{< /gloss >}} instead. This method covers the requests that an agent's own code sends, such as a `curl` command or a script that a skill runs. For model and MCP server calls, set `actorJwt` on the ModelConfig or the RemoteMCPServer, as described in [Actor tokens for other services]({{< link path="about/substrate-runtime/identity#actor-tokens-for-other-services" >}}). + +The gateway mints, injects, and refreshes the token, so the policy needs no Secret, and you have no credential to rotate. + +1. Create an EgressPolicy whose `authentication` block sets `actorJwt`. The gateway replaces the header with the prefix followed by a newly issued Actor token. + ```yaml + kubectl apply -f - <}} For the token's claims and how the destination validates it, see [Actor tokens for other services]({{< link path="about/substrate-runtime/identity#actor-tokens-for-other-services" >}}). + + | Field | Description | + | ----- | ----------- | + | `authentication.actorJwt.audiences` | The services that can accept the token, between 1 and 16 values. The destination checks that its own audience is in the list. `kagent-runtime` is reserved and rejected. | + | `authentication.actorJwt.expirationSeconds` | Optional. The token lifetime, between 300 and 3600 seconds. Defaults to 900. | + +2. Select the policy. A merge patch replaces the whole `egressPolicyRefs` list, so include every policy that the resource needs. + {{< tabs >}} + {{% tab name="Agent" %}} + ```bash + kubectl patch agent my-first-agent -n kagent --type merge \ + -p '{"spec":{"egressPolicyRefs":[{"name":"node-dependencies"},{"name":"ticket-api"}]}}' + ``` + {{% /tab %}} + {{% tab name="Standalone sandbox" %}} + ```bash + kubectl patch sandboxtemplate scratch -n kagent --type merge \ + -p '{"spec":{"egressPolicyRefs":[{"name":"node-dependencies"},{"name":"ticket-api"}]}}' + ``` + {{% /tab %}} + {{< /tabs >}} + +3. In the client that sends the request, send the header with a placeholder value. + ```bash + curl -H 'Authorization: Bearer kagent-credential-injected' \ + https://tickets.example.com/tickets + ``` ## Troubleshoot a grant @@ -191,7 +244,7 @@ In a standalone Sandbox, a destination that no grant allows does not resolve. A ## Access control -An EgressPolicy is namespace-scoped, and kagent does not check who may select it. A user who can create or edit an Agent or a SandboxTemplate in a namespace can select every EgressPolicy in this namespace and use its credentials. Treat the namespace as the boundary for sharing policies and the Secrets that they reference. For more information about how Kubernetes role-based access control (RBAC) governs who can write these resources, see [The Kubernetes plane]({{< link path="about/substrate-runtime/identity#the-kubernetes-plane" >}}). +An EgressPolicy is namespace-scoped, and kagent does not check who may select it. A user who can create or edit an Agent or a SandboxTemplate in a namespace can select every EgressPolicy in that namespace and use its credentials. Treat the namespace as the boundary for sharing policies and the Secrets that they reference. To give each team a namespace of its own, see [Run agents in multiple namespaces]({{< link path="operations/multiple-namespaces" >}}). For more information about how Kubernetes role-based access control (RBAC) governs who can write these resources, see [The Kubernetes plane]({{< link path="about/substrate-runtime/identity#the-kubernetes-plane" >}}). To control who can grant access, give the permission to create and edit EgressPolicy resources only to administrators. The kagent controller needs only read access to EgressPolicy resources. diff --git a/docs-site/content/kagent/1.x/agents/human-in-the-loop.md b/docs-site/content/kagent/1.x/agents/human-in-the-loop.md index 6c463c72..04f95ba8 100644 --- a/docs-site/content/kagent/1.x/agents/human-in-the-loop.md +++ b/docs-site/content/kagent/1.x/agents/human-in-the-loop.md @@ -7,11 +7,11 @@ author: kagent.dev Some agents only answer questions, and those can run unattended. Others call tools that change a cluster, spend money, or read sensitive information. An agent that takes actions like those should get a person's approval first. The human in the loop (HITL) mechanism lets an agent stop mid-turn, return a question or a pending tool call to a person, and continue once that person answers. -A working HITL setup needs two halves. The agent must pause, which you configure by setting `requireApproval` on a tool binding in an {{< gloss "AgentTemplate" >}}AgentTemplate{{< /gloss >}}. The client must support the A2A human-in-the-loop extension, which presents the request to a person and returns the decision to the agent. A client without the extension still sees the agent stop, and cannot answer. Confirm that the client you plan to use supports the extension before you configure an agent to pause. For which clients qualify, see [Client support](#client-support). +A working HITL setup needs support from both the agent and the client. The agent must pause, which you configure by setting `requireApproval` on a tool binding in an {{< gloss "AgentTemplate" >}}AgentTemplate{{< /gloss >}}. The client must support the Agent2Agent (A2A) human-in-the-loop extension, so that the client can present the request to a person and return the decision to the agent. A client without the extension still sees the agent stop, but cannot answer. Confirm that the client you plan to use supports the extension before you configure an agent to pause. For the clients that support the extension, see [Client support](#client-support). -## How a pause works +## Pause lifecycle -A client opens every turn by telling the agent which optional A2A extensions it can handle. A client that requests the HITL extension on that message can answer a pause later in the turn. kagent activates the extension only for the calls that request it, and attaches the pause payload only to those calls. A client that does not request the extension still receives a reply when the turn finishes. When the agent stops for a person, that client has no payload to answer. +A client opens every turn by telling the agent which optional A2A extensions it can handle. A client that requests the HITL extension on that message can answer a pause later in the turn. A client that does not request the extension still receives a reply when the turn finishes. When the agent stops for a person, that client does not receive a payload to answer. The following diagram traces one turn in which the agent stops. @@ -38,14 +38,14 @@ The agent works until it either finishes the turn or needs a person. When it nee ## Pause kinds -An agent pauses either to get permission before it acts or to ask a question. A tool approval and a question raise different requests, but both pause and resume the same way. A client that handles one handles the other with a different payload. +An agent pauses either to get permission before it acts or to ask a question. A tool approval and a question raise different requests, but both pause and resume in the same way. A client that handles one handles the other with a different payload. | Request | Raised when | The client answers with | | ------- | ----------- | ----------------------- | | `tool_approval_request` | The agent wants to call a tool from a binding that sets `requireApproval`. | `tool_approval_response` | -| `ask_user_request` | The agent calls the built-in `ask_user` tool because it needs information only a person has. | `ask_user_response` | +| `ask_user_request` | The agent calls the built-in `ask_user` tool because it needs information that only a person has. | `ask_user_response` | -Every pause also adds a plain text part to its status message that states why the agent stopped. A client that never activated the extension receives that text and nothing else. It can show a person why the agent stopped, but it has no `id` to answer with, so the task stays at `INPUT_REQUIRED`. Treat the text as a diagnostic rather than as a fallback, and send the turn again from a client that requests the extension. The `hint` field of an activated payload holds the same string. +Every pause also adds a plain text part to its status message that states why the agent stopped. A client that never activated the extension receives that text and nothing else. The client can show a person why the agent stopped. The client does not have an `id` to answer with, so the task stays at `INPUT_REQUIRED`. Treat the text as a diagnostic, and send the turn again from a client that requests the extension. The `hint` field of an activated payload contains the same string. The `kagent` harness composes this text from the pending calls. The wording depends on what the agent stopped for. @@ -54,10 +54,23 @@ The `kagent` harness composes this text from the pending calls. The wording depe | `ask_user` | The questions the agent asked, joined with `; `. | | A tool pause whose tools supplied hints | The hints, followed by the tool names in parentheses, such as `Deleting this file requires approval (delete_file, restart_pod)`. | | A tool pause with no hints | `Approval is required for tool(s): delete_file` | -| A pause carrying no tool call | `Human input is required before the agent can continue.` | +| A pause that does not include a tool call | `Human input is required before the agent can continue.` | The `codex` and `claude` harnesses compose the text differently. Both pass through the hint that Codex or Claude Code supplied with the request, so the wording comes from the coding agent rather than from kagent. +### Disable questions + +When an agent runs unattended, nobody answers its question, so the task waits at `INPUT_REQUIRED`. To stop agents on the `kagent` harness from asking questions, set the `config.runtimeDefaults.builtinTools.disableAskUser` Helm value to `true`. The value removes the `ask_user` tool from every agent and subagent in the installation, and an AgentTemplate cannot override it. The value defaults to `false`. + +```yaml +config: + runtimeDefaults: + builtinTools: + disableAskUser: true +``` + +The value removes only the question tool. A binding that sets `requireApproval` still pauses the agent for approval, and a remote A2A agent that the agent calls can still pause the turn with its own question. The `codex` and `claude` harnesses do not read the value, because the coding agent supplies its own tools. A `byo` image decides whether to read it. + ## Require approval for a tool Approval is a property of a tool binding. For example, if you set `requireApproval` on an `mcp` tool binding in the AgentTemplate, the agent stops before each call to a tool that the binding exposes. @@ -101,15 +114,15 @@ spec: | Field | Description | | ----- | ----------- | -| `mcp.tools` | The names of the tools to bind. Omit the list, or leave it empty, to bind every tool that the server offers. | -| `mcp.requireApproval` | Pauses before each invocation of a tool that this binding exposes. The pause covers the tools in `mcp.tools`, or every tool on the server when `mcp.tools` is omitted or empty. Omit to run the bound tools without approval. | +| `mcp.tools` | The names of the tools to bind. To bind every tool that the server offers, omit the list or leave it empty. | +| `mcp.requireApproval` | Pauses before each invocation of a tool that this binding exposes. The pause covers the tools in `mcp.tools`, or every tool on the server when `mcp.tools` is omitted or empty. To run the bound tools without approval, omit the field. | Approval belongs to the binding rather than to the tool name, so one server can supply tools that require approval alongside tools that do not. Bind the tools that require approval in a binding that sets `requireApproval`, and bind the tools that do not in a second binding that omits it. The `codex` and `claude` harnesses reject a second binding to the same server. An agent on those harnesses applies one approval setting to every tool that the server offers. > [!NOTE] > kagent 0.x named the tools that needed approval in a `requireApproval` list on the `Agent` resource, which matched tool names across every server. In 1.x, approval is a property of one binding, so the same tool name can pause for one server and run freely for another. -A tool that no binding covers with `requireApproval` runs without a pause. That includes every built-in tool, such as file access, shell, and web search, and every MCP tool on a binding that omits the field. The sandbox contains those calls. For more information, see [Sandboxing]({{< link path="about/substrate-runtime/sandboxing" >}}). +A tool that no binding covers with `requireApproval` runs without a pause. These tools include every built-in tool, for example file access, shell, and web search, and every Model Context Protocol (MCP) tool on a binding that omits the field. The sandbox contains those calls. For more information, see [Sandboxing]({{< link path="about/substrate-runtime/sandboxing" >}}). ## Implement a client that supports human approval @@ -123,7 +136,7 @@ A client requests the extension by setting the extension URI as the `A2A-Extensi A2A-Extensions: https://kagent.dev/extensions/hitl/v1 ``` -kagent activates the extension only for calls that request it. It then echoes the activated URI back, and attaches a structured pause payload only to an activated call. An agent's card declares the extension, so a client can discover HITL support before it sends anything. +kagent activates the extension only for calls that request it. kagent then echoes the activated URI back and attaches a structured pause payload only to an activated call. An agent's card declares the extension, so a client can discover HITL support before it sends anything. A gRPC call names the {{< gloss "Agent" >}}Agent{{< /gloss >}} in the standard A2A `tenant` field, as `/`, and requests the extension through a header. Port-forward the controller's gRPC port first, as in [Install kagent]({{< link path="setup/installation" >}}). @@ -143,11 +156,11 @@ grpcurl -plaintext \ A message that omits both a `contextId` and a task ID starts a new {{< gloss "Session" >}}Session{{< /gloss >}}. To continue an existing one, set the message's `contextId` to the Session ID. Over HTTP, the URL path `/agents/{namespace}/{name}` selects the Agent instead, and no tenant is needed. For both transports, see [Call an agent over A2A]({{< link path="examples/a2a-agents" >}}). > [!WARNING] -> A send that omits the header produces a pause that no one can answer. The turn still stops, and its status message states the question as prose, with no `metadata` and no correlation `id`. The client therefore has nothing to render and no `id` to answer with. Re-reading that task with the header does not recover the payload, because kagent never attached one. To prevent this, send the header on every call. Sending it on a read is harmless, and missing it on a send cannot be undone. +> A send that omits the header produces a pause that no one can answer. The turn still stops, and its status message states the question as prose, without `metadata` or a correlation `id`. The client therefore does not have anything to render or an `id` to answer with. Re-reading that task with the header does not recover the payload, because kagent never attached one. To prevent an unanswerable pause, send the header on every call. Sending it on a read is harmless, and missing it on a send cannot be undone. ### Read the pause -When the agent pauses, the task moves to `INPUT_REQUIRED`. The payload arrives in the status message's `metadata`, keyed by the extension URI. The message's `extensions` array lists the same URI. Each payload carries a `type` field that names its shape. kagent stores an attached payload with the task. A later read returns it whether or not that read requests the extension. +When the agent pauses, the task moves to `INPUT_REQUIRED`. The payload arrives in the status message's `metadata`, keyed by the extension URI. The message's `extensions` array lists the same URI. Each payload includes a `type` field that identifies its shape. kagent stores an attached payload with the task. A later read returns it whether or not that read requests the extension. Each type travels in one direction. An agent raises a request, and a client answers with the matching response. @@ -160,7 +173,7 @@ Each type travels in one direction. An agent raises a request, and a client answ ### Approve or reject a tool -A `tool_approval_request` lists the pending calls, each with an `id`, the tool `name`, and the `args` the agent chose. Present those to a person, then send one `tool_approval_response` that decides every call in the request. +A `tool_approval_request` lists the pending calls, each with an `id`, the tool `name`, and the `args` that the agent chose. Present the pending calls to a person, then send one `tool_approval_response` that decides every call in the request. ```json { @@ -172,11 +185,11 @@ A `tool_approval_request` lists the pending calls, each with an `id`, the tool ` } ``` -A rejection reason is optional and worth sending because the agent receives it and can adapt rather than fail. +A rejection reason is optional. The agent receives the reason and can adjust its next action. ### Answer a question -An `ask_user_request` carries an `id` and a list of `questions`. The response echoes the same `id` and answers the questions in order. +An `ask_user_request` contains an `id` and a list of `questions`. The response echoes the same `id` and answers the questions in order. ```json { @@ -193,28 +206,28 @@ An `ask_user_request` carries an `id` and a list of `questions`. The response ec ## Resume a paused task -A paused task waits for the client to send a message that carries the response payload, on the same task and context. kagent rejects a resume attempt on a task that is not waiting. +A paused task waits for the client to send a message that contains the response payload, on the same task and context. kagent rejects a resume attempt on a task that is not waiting. While the task waits, kagent suspends the {{< gloss "Actor" >}}Actor{{< /gloss >}} to a durable {{< gloss "Snapshot" >}}snapshot{{< /gloss >}} in the same way as at the end of a turn. The snapshot keeps the durable volume and discards the running process. Each harness records the pending request in that volume and continues the turn when the answer arrives. -The `claude` harness resumes its deferred tool call. The `codex` harness runs the approved MCP call itself and continues with the result. The {{< gloss "Worker" >}}Worker{{< /gloss >}} is released in the meantime, so a conversation that sits at `INPUT_REQUIRED` does not consume pool capacity. The wait also survives the loss of the node. For more information, see [Suspend and resume]({{< link path="about/substrate-runtime/suspend-and-resume" >}}). +The `claude` harness resumes its deferred tool call. The `codex` harness runs the approved MCP call itself and continues with the result. The {{< gloss "Worker" >}}Worker{{< /gloss >}} is released in the meantime, so a task that waits at `INPUT_REQUIRED` does not consume pool capacity. The wait also survives the loss of the node. For more information, see [Suspend and resume]({{< link path="about/substrate-runtime/suspend-and-resume" >}}). -On the `codex` and `claude` harnesses, only one pending request survives the suspension. The `codex` harness cancels any other request along with the interrupted turn. The `claude` harness denies a protected call that arrives in a parallel batch, but a single remaining call can still pause for approval. If an approved call fails after it starts, the harness cannot resume the task. To continue the conversation, start a new Session. +On the `codex` and `claude` harnesses, only one pending request survives the suspension. The `codex` harness cancels any other request along with the interrupted turn. The `claude` harness denies a protected call that arrives in a parallel batch, but a single remaining call can still pause for approval. If an approved call fails after it starts, the harness cannot resume the task. To continue the work, start a new Session. -Because the {{< gloss "Transcript" >}}transcript{{< /gloss >}} only grows, the question and the answer both stay in the task history, so a later reader can see what was asked and what a person decided. +The {{< gloss "Transcript" >}}transcript{{< /gloss >}} only grows, so the question and the answer both stay in the task history. A later reader can see what was asked and what a person decided. ## Task states -An A2A task moves through several states over its life. The following states mean that the task has stopped and is waiting, rather than working. +An A2A task moves through several states over its life. The following states mean that the task has stopped and is waiting. | State | Meaning | | ----- | ------- | -| `INPUT_REQUIRED` | The agent is waiting for a person. This is the state that a tool approval or a question produces. | +| `INPUT_REQUIRED` | The agent is waiting for a person. A tool approval or a question produces this state. | | `AUTH_REQUIRED` | The agent is waiting for credentials. kagent's own harnesses never set this state. The gateway accepts a resume from it, so a `byo` image that produces it works. | ## Agents bound as tools -An agent that a parent binds as a tool can raise a pause of its own. The request then includes a `nested` block naming the subagent, along with its task and context. A client can therefore tell the person which agent is actually asking rather than attributing the question to the parent. +An agent that a parent binds as a tool can raise a pause of its own. The request then includes a `nested` block that identifies the subagent, along with its task and context. A client can therefore tell the person which agent is asking rather than attributing the question to the parent. ## Client support @@ -224,7 +237,7 @@ The AgentTemplate decides that a turn pauses, and the client that raised the tur | ------ | ---- | | Your own A2A client | Full. Request the extension URI and handle every payload type. | | An {{< gloss "MCP" >}}MCP{{< /gloss >}} client that supports tasks | Supported. The `invoke_session` tool returns a task, and input requests surface as MCP elicitations. | -| The kagent CLI | Not supported, and a turn that pauses is stranded. `kagent agent invoke` does not request the extension, so an agent that needs a person parks the task at `INPUT_REQUIRED` with nothing to answer it by. The CLI reports `Input required to continue this Session.` and stops there. Send the turn again from a client that requests the extension. | +| The kagent CLI | Not supported. `kagent agent invoke` does not request the extension, so a turn that pauses stays at `INPUT_REQUIRED` and the CLI cannot answer it. The CLI reports `Input required to continue this Session.` and stops there. Send the turn again from a client that requests the extension. | ## Next steps diff --git a/docs-site/content/kagent/1.x/agents/structured-output.md b/docs-site/content/kagent/1.x/agents/structured-output.md index 4223f161..cc19745c 100644 --- a/docs-site/content/kagent/1.x/agents/structured-output.md +++ b/docs-site/content/kagent/1.x/agents/structured-output.md @@ -12,8 +12,9 @@ The schema goes in one of two AgentTemplate fields, `spec.outputSchema` or `spec | Field | Description | | ----- | ----------- | | `outputSchema` | The schema, written inline in the AgentTemplate. | -| `outputSchemaFrom.name` | The ConfigMap holding the schema as JSON, in the AgentTemplate's namespace. | +| `outputSchemaFrom.name` | The ConfigMap that stores the schema as JSON, in the AgentTemplate's namespace. | | `outputSchemaFrom.key` | The key within that ConfigMap. | +| `outputSchemaStrict` | Whether the model provider constrains generation to the schema. Defaults to `false`. For the providers that honor it, see [Strict mode](#strict-mode). | ## Before you begin @@ -26,10 +27,9 @@ The schema goes in one of two AgentTemplate fields, `spec.outputSchema` or `spec kagent version ``` - If `kagent_version` in the output is not {{< reuse "kagent-docs/versions/kagent.md" >}}, install that version. - ```bash - curl https://raw.githubusercontent.com/kagent-dev/kagent/refs/heads/main/scripts/get-kagent | bash -s -- --version v{{< reuse "kagent-docs/versions/kagent.md" >}} - ``` + When `kagent_version` in the output differs from {{< reuse "kagent-docs/versions/kagent.md" >}}, replace the CLI. + + {{< reuse "kagent-docs/snippets/install-cli.md" >}} ## Set the schema inline @@ -113,7 +113,7 @@ Set `spec.outputSchema` to keep the schema in the AgentTemplate, so that the sch Use `outputSchemaFrom` to keep the schema outside the AgentTemplate, so that several AgentTemplates can share one schema. The value in the ConfigMap must be JSON, even though the ConfigMap itself is written in YAML. -1. Create a ConfigMap holding the schema. +1. Create a ConfigMap that stores the schema. ```yaml kubectl apply -f - <`, which cannot be recursive - `$schema` and `$id` +- `pattern`, in RE2 syntax, on a schema whose `type` includes `string` +- `format`, on a schema whose `type` includes `string`, with one of the values `date`, `date-time`, `duration`, `email`, `hostname`, `ipv4`, `ipv6`, `time`, or `uuid` + +Anything else is rejected, including `oneOf` and `allOf`, conditional schemas such as `if` and `then`, tuple arrays, references outside `$defs`, and validation keywords such as `minimum` and `minLength`. Claude structured outputs do not support word boundaries, so kagent also rejects a `pattern` that uses `\b` or `\B`. -Anything else is rejected, including `oneOf` and `allOf`, conditional schemas such as `if` and `then`, tuple arrays, references outside `$defs`, and validation keywords such as `pattern`, `format`, `minimum`, and `minLength`. To allow `null` alongside another type, use `anyOf`, because `type` takes one value. +JSON Schema treats `format` as an annotation. kagent forwards `format` to the model provider, but the runtime does not check it on the answer. + +A Gemini model answers by calling the `set_model_response` tool of the Go Agent Development Kit (ADK), and that tool checks each property against one type. For a model whose name starts with `gemini-`, kagent therefore rejects a property that uses `anyOf`, `type: null`, or a type list with more than one non-null type. kagent also rejects an array property that does not set `items`. To allow `null` in such a property, use a type list such as `["string", "null"]`. The check skips models on the `GeminiVertexAI` provider. kagent also enforces the following limits when it builds the revision. A schema that exceeds a limit is rejected, before any agent runs with it. @@ -194,9 +200,51 @@ kagent also enforces the following limits when it builds the revision. A schema kagent counts depth and nodes through `properties`, `items`, and `anyOf`, and counts a `$defs` entry again each time that it is referenced. The size limit applies before kagent normalizes the schema, so whitespace in a ConfigMap value counts toward it. +## Strict mode + +OpenAI models follow `format` and `pattern` during generation only in strict mode. To constrain generation to the schema, set `outputSchemaStrict` to `true` in the AgentTemplate. + +Strict mode accepts fewer schemas. kagent checks the following rules for every provider, so a schema stays valid when you change the provider of the ModelConfig. + +- The root does not use `anyOf`. +- Every node sets `type`, `anyOf`, `enum`, or `$ref`. +- A node that sets `$ref` does not set any other keyword. To add `null`, wrap the reference in `anyOf`, such as `anyOf: [{$ref: "#/$defs/item"}, {type: "null"}]`. +- Every object sets `additionalProperties` to `false` and lists all of its properties in `required`. A map with free-form keys cannot meet this rule. + +To make a property optional in strict mode, list it in `required` and add `null` to its type list. + +```yaml +spec: + outputSchemaStrict: true + outputSchema: + type: object + properties: + ticket: + type: string + pattern: "^OPS-[0-9]+$" + due: + type: [string, "null"] + format: date + required: + - ticket + - due + additionalProperties: false +``` + +The following fields turn on strict mode, make `due` optional, and constrain both values. + +| Field | Description | +| ----- | ----------- | +| `outputSchemaStrict` | Sends the schema in strict mode to the OpenAI, Azure OpenAI, and Azure AI Foundry providers. Azure AI Foundry receives it only with the `OpenAI` API format. Other providers ignore the field. | +| `outputSchema.properties.ticket.pattern` | Requires a ticket ID such as `OPS-123`. | +| `outputSchema.properties.due.type` | Allows `null`, which makes `due` optional. | +| `outputSchema.properties.due.format` | Asks for a date such as `2026-11-03`. | + +An AgentTemplate that sets `outputSchemaStrict` to `true` without a schema is rejected when you apply it, with the message `outputSchemaStrict requires outputSchema or outputSchemaFrom`. The runtime validates every answer against the schema in both modes. + ## Read the answer -A successful answer is published as one {{< gloss "A2A" >}}A2A{{< /gloss >}} (Agent-to-Agent) `DataPart` with the media type `application/json`. The part's metadata carries the SHA-256 digest of the schema that the answer was validated against, under the key `kagent.dev/a2a/output-schema-sha256`. kagent computes the digest after it normalizes the schema, so two schemas that differ only in formatting or key order have the same digest. The Agent's card lists `application/json` as its default output mode, instead of `text`. +A successful answer is published as one {{< gloss "A2A" >}}A2A{{< /gloss >}} (Agent2Agent) `DataPart` with the media type `application/json`. The part's metadata includes the SHA-256 digest of the schema that the answer was validated against, under the key `kagent.dev/a2a/output-schema-sha256`. kagent computes the digest after it normalizes the schema, so two schemas that differ only in formatting or key order have the same digest. The Agent's card lists `application/json` as its default output mode, instead of `text`. To see the part and its metadata, print the task as JSON. @@ -221,31 +269,33 @@ Example output: ] ``` -The runtime holds back the agent's partial output while the agent writes its answer, and publishes only the complete answer. Progress updates, tool calls, approval requests, and input-required messages keep their usual form, and the agent can call tools before it answers. A streaming client that needs only the answer reads the last artifact with content before the task reaches `TASK_STATE_COMPLETED`. That artifact's part carries the `kagent.dev/a2a/output-schema-sha256` metadata key. +The runtime holds back the agent's partial output while the agent writes its answer, and publishes only the complete answer. Progress updates, tool calls, approval requests, and input-required messages keep their usual form, and the agent can call tools before it answers. A streaming client that needs only the answer reads the last artifact with content before the task reaches `TASK_STATE_COMPLETED`. That artifact's part includes the `kagent.dev/a2a/output-schema-sha256` metadata key. ## Agents as tools The schema applies only to the root agent, which is the agent of the AgentTemplate that the Agent names. An AgentTemplate [bound to it as a subagent]({{< link path="skills-and-mcp/about-tools#subagents-as-tools" >}}) does not inherit the schema. If the bound AgentTemplate has a schema of its own, that schema applies only to Agents that name it directly. -With a `Shared` binding, the root agent can hand the conversation to the bound agent, which then answers in its place, in that turn and in the turns that follow. Those tasks fail with `output_validation_failed: root agent produced no result artifact`, because the answers do not come from the root agent. The bound agent's answers still reach the caller as text. Give a schema only to an AgentTemplate whose own agent writes the final answer. +If the subagent uses a `Shared` binding, the root agent can hand the conversation to the bound agent, which then answers in its place, in that turn and in the turns that follow. Those tasks fail with `output_validation_failed: root agent produced no result artifact`, because the answers do not come from the root agent. The bound agent's answers still reach the caller as text. Give a schema only to an AgentTemplate whose own agent writes the final answer. ## Answers that fail validation -Structured output has no fallback to text. If the model refuses, stops early, or returns an answer that is not JSON or does not match the schema, the task fails. kagent does not publish the invalid answer or include it in the failure message, because it can contain sensitive data. With a Gemini model on the `Gemini` provider, however, the agent is told to give its answer as the arguments of a `set_model_response` tool call. kagent publishes that call like any other tool call, before it validates the answer, so an answer given that way reaches the caller even when it fails validation. +Structured output does not have a fallback to text. If the model refuses, stops early, or returns an answer that is not JSON or does not match the schema, the task fails. kagent does not publish the invalid answer or include it in the failure message, because it can contain sensitive data. When the agent uses a Gemini model on the `Gemini` provider, however, the agent is told to give its answer as the arguments of a `set_model_response` tool call. kagent publishes that call like any other tool call, before it validates the answer, so an answer given that way reaches the caller even when it fails validation. -For example, an answer that does not match the schema fails the `kagent invoke` command with output like the following. The `output_validation_failed:` message names the cause. +For example, an answer that does not match the schema fails the `kagent agent invoke` command with output like the following. The `output_validation_failed:` message states the cause. ```console processor failed: output_validation_failed: root agent output does not conform to its schema Error: Session task 01a0ec27-157f-7034-bb89-09855f485b2a ended in TASK_STATE_FAILED ``` +In the CLI output, each of the following messages follows the `processor failed:` prefix. + | Message | Cause | | ------- | ----- | | `output_validation_failed: root agent output does not conform to its schema` | The answer is JSON, but it does not match the schema. | | `output_validation_failed: root agent output is not valid JSON` | The answer is not JSON. | | `output_validation_failed: root agent did not complete structured output` | The model stopped for a reason other than finishing normally, such as reaching its token limit. | -| `output_validation_failed: root agent produced no structured value` | The model returned no answer text. | +| `output_validation_failed: root agent produced no structured value` | The model did not return any answer text. | | `output_validation_failed: root agent produced no result artifact` | The task ended without a structured answer from the root agent, for example because the root agent handed the conversation to an [agent bound as a tool](#agents-as-tools). | ## Troubleshooting @@ -280,17 +330,22 @@ For example, if the `shared-schemas` ConfigMap no longer has the `arithmetic-ans ] ``` +The `message` field of the failing condition identifies the cause. + | Message | Condition and reason | Cause | | ------- | -------------------- | ----- | | `resolve output schema ConfigMap "x": not found` | `ResolvedRefs`, `ReferenceResolutionFailed` | The ConfigMap does not exist in the AgentTemplate's namespace. | -| `resolve output schema ConfigMap "x": key "y" not found` | `ResolvedRefs`, `ReferenceResolutionFailed` | The ConfigMap has no such key under `data`. | -| `invalid output schema: schema is empty` | `Compatible`, `UnsupportedConfiguration` | The key holds an empty value. | +| `resolve output schema ConfigMap "x": key "y" not found` | `ResolvedRefs`, `ReferenceResolutionFailed` | The ConfigMap does not have that key under `data`. | +| `invalid output schema: schema is empty` | `Compatible`, `UnsupportedConfiguration` | The key's value is empty. | | `invalid output schema: schema exceeds 65536 bytes` | `Compatible`, `UnsupportedConfiguration` | The schema is larger than 64 KiB. | | `invalid output schema: decode JSON: ...` | `Compatible`, `UnsupportedConfiguration` | The value is not valid JSON. | | `invalid output schema: decode trailing JSON: ...` | `Compatible`, `UnsupportedConfiguration` | Text that is not JSON follows the JSON value. | -| `invalid output schema: schema must contain one JSON value` | `Compatible`, `UnsupportedConfiguration` | The value holds more than one JSON value, such as two objects in a row. | +| `invalid output schema: schema must contain one JSON value` | `Compatible`, `UnsupportedConfiguration` | The value contains more than one JSON value, such as two objects in a row. | | `invalid output schema: schema does not match the portable output profile: ...` | `Compatible`, `UnsupportedConfiguration` | The schema uses a keyword or form outside the [portable output profile](#supported-schemas), or its root is not an object. | | `invalid output schema: resolve schema: ...` | `Compatible`, `UnsupportedConfiguration` | kagent cannot resolve the schema, for example because a `$ref` names a `$defs` entry that does not exist. | +| `invalid output schema: pattern ...` | `Compatible`, `UnsupportedConfiguration` | A `pattern` is not valid RE2, uses a word boundary, or the schema's patterns compile to more than 100,000 regexp instructions. | +| `output schema for Gemini: ...` | `Compatible`, `UnsupportedConfiguration` | A property breaks the [Gemini rule](#supported-schemas) for a single type, or an array property does not set `items`. | +| `outputSchemaStrict: ...` | `Compatible`, `UnsupportedConfiguration` | The schema breaks a [strict mode](#strict-mode) rule. | | `output schema is incompatible with Go ADK: ...` | `Compatible`, `UnsupportedConfiguration` | The schema has a recursive reference, or exceeds the depth or node limit. | | `Harness runtime "x" does not support structured output` | `Compatible`, `UnsupportedConfiguration` | The Harness does not use the `kagent` harness. | diff --git a/docs-site/content/kagent/1.x/examples/a2a-agents.md b/docs-site/content/kagent/1.x/examples/a2a-agents.md index 2401463d..e856d44c 100644 --- a/docs-site/content/kagent/1.x/examples/a2a-agents.md +++ b/docs-site/content/kagent/1.x/examples/a2a-agents.md @@ -5,9 +5,9 @@ weight: 30 author: kagent.dev --- -Every {{< gloss "Agent" >}}Agent{{< /gloss >}} is reachable over the {{< gloss "A2A" >}}A2A{{< /gloss >}} (Agent-to-Agent) protocol through the kagent controller. This example calls that service directly to read an Agent's card, send it a message, stream a reply, and answer the agent when it pauses for a person. +Every {{< gloss "Agent" >}}Agent{{< /gloss >}} is reachable over the {{< gloss "A2A" >}}A2A{{< /gloss >}} (Agent2Agent) protocol through the kagent controller. This example calls that service directly to read an Agent's card, send it a message, stream a reply, and answer the agent when it pauses for a person. -In {{< reuse "kagent-docs/snippets/name-product.md" >}}, agent traffic runs over A2A. The CLI and the [MCP server]({{< link path="examples/agents-via-mcp" >}}) reach an Agent through the same service. +In {{< reuse "kagent-docs/snippets/name-product.md" >}}, agent traffic runs over A2A. The CLI and the [Model Context Protocol (MCP) server]({{< link path="examples/agents-via-mcp" >}}) reach an Agent through the same service. This example uses [grpcurl](https://github.com/fullstorydev/grpcurl) to show the requests and replies directly. Real callers use an A2A client library rather than assembling requests by hand. @@ -15,18 +15,18 @@ This example uses [grpcurl](https://github.com/fullstorydev/grpcurl) to show the The controller serves `lf.a2a.v1.A2AService` on port `8083`, alongside its REST API and its MCP endpoint. The kagent CLI reaches the same port and the same service. -A gRPC caller names the Agent in the standard A2A `tenant` field, as `/`, and the controller routes the call to that Agent. The same controller serves A2A over HTTP JSON-RPC, where the URL path selects the Agent instead: +A gRPC caller identifies the Agent in the standard A2A `tenant` field, as `/`, and the controller routes the call to that Agent. The same controller serves A2A over HTTP JSON-RPC, where the URL path selects the Agent instead: | Transport | How a caller selects the Agent | | --------- | ----------------------------- | | gRPC | The `tenant` field on each request, as `/` | | HTTP JSON-RPC | `POST /agents/{namespace}/{name}`, with the card at `GET /agents/{namespace}/{name}/.well-known/agent-card.json` | -An HTTP request that also sets the `tenant` field must set a value that matches its URL. There is no deployment-wide agent card, because the controller serves many Agents. +An HTTP request that also sets the `tenant` field must set a value that matches its URL. The controller does not serve a deployment-wide agent card, because it serves many Agents. Both forms replace the `/api/a2a///` URL paths that kagent 0.x served over HTTP. -A caller addresses the Agent rather than one conversation. The message's `contextId` selects the conversation. Omitting both a `contextId` and a task ID starts a new {{< gloss "Session" >}}Session{{< /gloss >}}, and repeating a `contextId` continues that Session. +A caller addresses the Agent rather than one Session. The message's `contextId` selects the Session. Omitting both a `contextId` and a task ID starts a new {{< gloss "Session" >}}Session{{< /gloss >}}, and repeating a `contextId` continues that Session. If the Agent, the Session, or the task does not exist, an A2A call fails with `TaskNotFound` (JSON-RPC code `-32001`, or gRPC `NOT_FOUND`). Over HTTP, a request for the card of an Agent that does not exist returns `404`. To learn when kagent deletes a Session, see [Expire idle Sessions]({{< link path="operations/operational-considerations#expire-idle-sessions" >}}). @@ -102,19 +102,19 @@ An A2A client typically starts by reading the agent card, which tells it what th ``` 2. Read the card for what a caller acts on. - * Both `name` and `description` come from the {{< gloss "AgentTemplate" >}}AgentTemplate{{< /gloss >}}. `name` replaces hyphens with underscores, and `description` is `spec.description` verbatim, so a caller sees the description you wrote. + * Both `name` and `description` come from the {{< gloss "AgentTemplate" >}}AgentTemplate{{< /gloss >}}. In `name`, kagent replaces hyphens with underscores. The `description` value is `spec.description` verbatim, so a caller sees the description that you wrote. * Each `supportedInterfaces` URL is the controller's in-cluster address rather than the Actor's, because a caller reaches the agent through the controller. The card advertises JSON-RPC first, then gRPC. The JSON-RPC URL identifies the Agent in its path. The gRPC entry sets the `tenant` value instead. * The `capabilities.pushNotifications` value advertises task callbacks. Each callback belongs to one task, not to the whole Session. - * The `capabilities.extensions` list names the optional extensions that the agent supports. A client opts into the human-in-the-loop extension per call to answer pauses. With the usage extension, the agent reports the tokens that a task uses. For the payload, see [Read token usage](#read-token-usage). + * The `capabilities.extensions` list identifies the optional extensions that the agent supports. A client opts into the human-in-the-loop extension per call to answer pauses. Through the usage extension, the agent reports the tokens that a task uses. For the payload, see [Read token usage](#read-token-usage). > [!NOTE] -> The card carries no `skills`. kagent 0.x let you declare agent card skills in an `a2aConfig` block. However, v1alpha3 has no such field, so kagent generates the card from the AgentTemplate's name and description alone. An AgentTemplate's `spec.skills` field is a different feature: those are [Agent Skills]({{< link path="skills-and-mcp/skills" >}}) that the agent can use, not advertisements to a caller. +> The card does not include `skills`. kagent 0.x let you declare agent card skills in an `a2aConfig` block. However, v1alpha3 does not have that field, so kagent generates the card only from the AgentTemplate's name and description. An AgentTemplate's `spec.skills` field is a different feature: those are [Agent Skills]({{< link path="skills-and-mcp/skills" >}}) that the agent can use, not advertisements to a caller. ## Send a message `SendMessage` blocks until the agent finishes the turn, or pauses to ask a person, and then returns the whole task. A message needs its own ID, a role, and at least one part. -A message part can also carry a file. Put the file's bytes in the part's `raw` field, which JSON encodes as base64, and set `filename` and `mediaType` on the same part. To send a link to a file, set the part's `url` field in place of `raw`. When the ModelConfig sets `apiFormat: responses`, or the provider is Gemini, the model fetches the file from the link. Every other model receives the link as text. +A message part can also contain a file. Put the file's bytes in the part's `raw` field, which JSON encodes as base64, and set `filename` and `mediaType` on the same part. To send a link to a file, set the part's `url` field in place of `raw`. When the ModelConfig sets `apiFormat: responses`, or the provider is Gemini, the model fetches the file from the link. Every other model receives the link as text. > [!NOTE] > Unlike the UI, which checks each file as you stage it, this path accepts any media type. When the ModelConfig sets `apiFormat: responses`, a PDF, `.docx`, `.xlsx`, or `.pptx` file reaches the model as a file. Any other file whose media type is not text-like reaches the model as the note `[Uploaded file "report.pdf" (application/pdf) could not be read as text.]` instead of contents. The controller's gRPC server rejects any message over 16 MiB, framing included. For how files reach the model, and the file types and sizes that the UI accepts, see [Attach files to a message]({{< link path="observability/launch-ui#attach-files-to-a-message" >}}). @@ -148,11 +148,11 @@ A message part can also carry a file. Put the file's bytes in the part's `raw` f ``` The task returns two identifiers, `id` and `contextId`, and a caller uses them differently. * The `id` identifies one turn, and every message returns a new one. - * The `contextId` identifies the conversation, and is the Session's own ID. A second message that repeats this `contextId` therefore continues the conversation rather than starting a new one. + * The `contextId` is the Session ID. A second message that repeats this `contextId` continues the same Session. - Task metadata uses the `kagent.dev/a2a/` namespace. An artifact carries `kagent.dev/a2a/timeline-position`, an RFC 3339 timestamp that orders task history, and the task itself carries `kagent.dev/a2a/task-created-at`. A runtime that reports per-event model usage publishes it as `kagent.dev/a2a/usage` on a task event rather than on the artifact. For task-lifetime usage, see [Read token usage](#read-token-usage). + Task metadata uses the `kagent.dev/a2a/` namespace. An artifact includes `kagent.dev/a2a/timeline-position`, an RFC 3339 timestamp that orders task history. The task itself includes `kagent.dev/a2a/task-created-at`. A runtime that reports per-event model usage publishes it as `kagent.dev/a2a/usage` on a task event rather than on the artifact. For task-lifetime usage, see [Read token usage](#read-token-usage). -2. Save the task's `id` and the conversation's `contextId`, so that later steps can read the task and continue the same Session. +2. Save the task's `id` and the Session's `contextId`, so that later steps can read the task and continue the same Session. ```bash export TASK_ID= export SESSION_ID= @@ -169,7 +169,7 @@ A message part can also carry a file. Put the file's bytes in the part's `raw` f localhost:8083 lf.a2a.v1.A2AService/SendStreamingMessage ``` -2. Read the event sequence. The stream opens with the task at `TASK_STATE_SUBMITTED`, moves to `TASK_STATE_WORKING`, and then emits an artifact update for each chunk of the reply. Every chunk shares one `artifactId`, so a client appends them into a single artifact rather than treating each as a separate answer. Example output, abbreviated to the text of each event: +2. Read the event sequence. The stream opens with the task at `TASK_STATE_SUBMITTED`, moves to `TASK_STATE_WORKING`, and then emits an artifact update for each chunk of the reply. Every chunk shares one `artifactId`, so a client appends the chunks into a single artifact. Example output, abbreviated to the text of each event: ```console "state": "TASK_STATE_SUBMITTED" "state": "TASK_STATE_WORKING" @@ -181,7 +181,7 @@ A message part can also carry a file. Put the file's bytes in the part's `raw` f ## Read a task later -A task outlives the call that created it, so a caller that lost its connection can read the result rather than asking the agent again. +A task outlives the call that created it, so a caller that lost its connection can still read the result. 1. Read the task by ID. ```bash @@ -209,7 +209,7 @@ A task outlives the call that created it, so a caller that lost its connection c } ``` -A task that is still running reports `TASK_STATE_WORKING` and has no artifacts yet. To stop a task that is still running, call `CancelTask` with the same `id`. +A task that is still running reports `TASK_STATE_WORKING` and does not have artifacts yet. To stop a task that is still running, call `CancelTask` with the same `id`. ## Read token usage {#read-token-usage} @@ -261,7 +261,7 @@ A task callback lets a webhook learn when a task needs input or reaches a final kagent keeps one pending delivery for each callback, and a newer state replaces a delivery that has not yet succeeded. When a task resumes, kagent discards the pending delivery for the state that the task left, so a receiver does not see every state. -The kagent CLI wraps the three push notification configuration methods, so the steps that follow use the CLI rather than building each request by hand. The CLI defaults to port 8083, which the prerequisites already forward. +The kagent CLI wraps the push notification configuration methods, so this section uses the CLI. The CLI defaults to port 8083, which the prerequisites already forward. 1. To register a callback before a new task is dispatched, add `--push-url` to the kagent CLI invocation. The command returns immediately with the task ID and callback ID. ```bash @@ -300,7 +300,7 @@ The kagent CLI wraps the three push notification configuration methods, so the s A callback URL is a public endpoint that receives task state from kagent. Before you register a callback in production, decide how the receiver establishes that a delivery came from kagent. > [!IMPORTANT] -> kagent signs deliveries only when the installation names a signing key, and `controller.push.signing.secretName` is empty by default. Until you set it, kagent posts each callback without an `Authorization` header, and the controller serves `404` at `/.well-known/jwks.json`. A receiver cannot distinguish an unsigned kagent delivery from any other caller, so treat the default configuration as suitable for a trusted network only. +> kagent signs deliveries only when the installation sets a signing key, and `controller.push.signing.secretName` is empty by default. Until you set it, kagent posts each callback without an `Authorization` header, and the controller returns `404` at `/.well-known/jwks.json`. A receiver cannot distinguish an unsigned kagent delivery from any other caller, so use the default configuration only on a trusted network. kagent does not generate its own signing key, so you are responsible for creating the key, keeping it, and rotating it. The following steps turn signing on. @@ -312,13 +312,13 @@ kagent does not generate its own signing key, so you are responsible for creatin > [!NOTE] > The `openssl` binary that ships with macOS is LibreSSL, which rejects this command with `Algorithm ed25519 not found`. Install OpenSSL 3, or generate the key on another machine. -2. Store the key in a Secret. kagent reads the `private-key.pem` entry, so the entry name is not optional. +2. Store the key in a Secret. kagent reads only the `private-key.pem` entry, so the entry must have that name. ```bash kubectl create secret generic kagent-push-signing -n kagent \ --from-file=private-key.pem=private-key.pem ``` -3. Name the Secret in `controller.push.signing.secretName`, and set `config.push.issuer` to a URL that reaches the controller. Use the Helm release name and version that you installed kagent with. +3. Set `controller.push.signing.secretName` to the Secret name, and set `config.push.issuer` to a URL that reaches the controller. Use the Helm release name and version that you installed kagent with. ```bash helm upgrade kagent oci://ghcr.io/kagent-dev/kagent/helm/kagent \ --namespace kagent --reuse-values \ @@ -326,29 +326,29 @@ kagent does not generate its own signing key, so you are responsible for creatin --set config.push.issuer=https://kagent.example ``` - The issuer that a receiver trusts must match the host that it fetches the JWKS from. When you omit `config.push.issuer`, Helm derives it from `ui.externalUrl` and then from the in-cluster A2A gateway URL. Either fallback can name a host that does not serve the JWKS. + The issuer that a receiver trusts must match the host that it fetches the JSON Web Key Set (JWKS) from. When you omit `config.push.issuer`, Helm derives it from `ui.externalUrl` and then from the in-cluster A2A gateway URL. Either fallback can point to a host that does not serve the JWKS. 4. Confirm that the controller publishes a verification key. Before you set the Secret, this path returns `404`. ```bash curl -s http://localhost:8083/.well-known/jwks.json ``` - Example output, which carries the public key alone: + Example output, which includes only the public key: ```json {"keys":[{"kty":"OKP","crv":"Ed25519","alg":"EdDSA","use":"sig","kid":"B1BP8zPNOTEGYyBUVgUAeTyA5hPlnrNc0OB416NY-CM","x":"xIc8dv_I-NrYSvWKg4aKm_X1CjHmiUBSMf9cDXEKesM"}]} ``` -Receivers verify a signed delivery against the JSON Web Key Set (JWKS). The controller serves the path on the same port as its gRPC, A2A, and MCP endpoints, so a receiver outside of the cluster needs a stable HTTPS URL that routes to the controller. The UI does not proxy the path, so publishing only the UI leaves a receiver with no key to verify against. +Receivers verify a signed delivery against the JWKS. The controller serves the path on the same port as its gRPC, A2A, and MCP endpoints, so a receiver outside of the cluster needs a stable HTTPS URL that routes to the controller. The UI does not proxy the path, so publishing only the UI leaves a receiver without a key to verify against. -Receivers must not send a token or credential when they register a callback. kagent signs each delivery attempt with a fresh five-minute JWT whose audience is the exact callback URL, including the query string. Before you acknowledge the request with a 2xx response, verify the JWT signature, issuer, audience, expiry, and `taskId` claim. The `taskId` claim repeats the `statusUpdate.taskId` value in the body, so a receiver that compares them rejects a token replayed against a different task. +Receivers must not send a token or credential when they register a callback. kagent signs each delivery attempt with a fresh five-minute JSON Web Token (JWT) whose audience is the exact callback URL, including the query string. Before you acknowledge the request with a 2xx response, verify the JWT signature, issuer, audience, expiry, and `taskId` claim. The `taskId` claim repeats the `statusUpdate.taskId` value in the body, so a receiver that compares them rejects a token replayed against a different task. By default, callback URLs must use HTTPS. Callback URLs must not resolve to private, loopback, link-local, unspecified, or multicast addresses. To reach a test receiver inside the cluster, set `config.push.allowHttp=true` or `config.push.allowPrivateNetworks=true`. kagent does not follow redirects, and failed attempts are retried from the database-backed queue. ## When an agent needs a person -An agent can stop mid-task to ask a question or to request approval for a tool call. The task then reports `TASK_STATE_INPUT_REQUIRED` and waits until a caller answers it. Every kagent agent can raise the question kind, because the runtime gives each one a built-in `ask_user` tool, so a system prompt that tells an agent to ask before it answers is enough to see a pause. +An agent can stop mid-task to ask a question or to request approval for a tool call. The task then reports `TASK_STATE_INPUT_REQUIRED` and waits until a caller answers it. By default, every agent on the `kagent` harness can raise the question kind, because the runtime gives each one a built-in `ask_user` tool. A system prompt that tells an agent to ask before it answers is therefore enough to see a pause. -Answering a pause needs the human-in-the-loop extension, which a caller requests per call. The extension is a versioned URI, and a request names it in two places: the `A2A-Extensions` header, and the message's own `extensions` list. +Answering a pause needs the human-in-the-loop extension, which a caller requests per call. The extension is a versioned URI. A request must include the URI in both the `A2A-Extensions` header and the message's own `extensions` list. 1. Send a message that requests the extension. ```bash @@ -358,7 +358,7 @@ Answering a pause needs the human-in-the-loop extension, which a caller requests localhost:8083 lf.a2a.v1.A2AService/SendMessage ``` -2. Read the pause. The task stops at `TASK_STATE_INPUT_REQUIRED`, and the status message's `metadata` holds the request, keyed by the extension URI. Example output: +2. Read the pause. The task stops at `TASK_STATE_INPUT_REQUIRED`, and the status message's `metadata` contains the request, keyed by the extension URI. Example output: ```json { "task": { @@ -387,14 +387,14 @@ Answering a pause needs the human-in-the-loop extension, which a caller requests } ``` -3. Save the task ID, the conversation's `contextId`, and the request `id` to environment variables. The request `id` is within the extension metadata and starts with `adk-`. +3. Save the task ID, the Session's `contextId`, and the request `id` to environment variables. The request `id` is within the extension metadata and starts with `adk-`. ```bash export PAUSED_TASK_ID= export PAUSED_SESSION_ID= export REQUEST_ID= ``` -4. Answer on the same task. The response goes in the same metadata key, names the request `id` it answers, and sets `taskId` so that it answers the paused task rather than starting a new turn. +4. Answer on the same task. The response goes in the same metadata key, references the request `id` that it answers, and sets `taskId` so that it answers the paused task rather than starting a new turn. ```bash grpcurl -plaintext \ -H "A2A-Extensions: https://kagent.dev/extensions/hitl/v1" \ @@ -409,15 +409,15 @@ Answering a pause needs the human-in-the-loop extension, which a caller requests ``` > [!IMPORTANT] -> A caller that does not request the extension is still interrupted. The task stops at `TASK_STATE_INPUT_REQUIRED` exactly as before, and the question arrives as ordinary text on the status message, but the metadata does not include a request `id`. Without that `id` there is nothing to answer, so the task waits until something cancels it. Request the extension on any call to an agent that can pause. +> A caller that does not request the extension is still interrupted. The task stops at `TASK_STATE_INPUT_REQUIRED`, and the question arrives as ordinary text on the status message. However, the metadata does not include a request `id`. Without that `id`, the caller cannot answer the pause, so the task waits until the task is canceled. Request the extension on any call to an agent that can pause. -A tool approval works the same way with a different payload, `tool_approval_request` answered by `tool_approval_response`. For the pause kinds, the approval model, and what a nested agent's pause looks like, see [Human in the loop]({{< link path="agents/human-in-the-loop" >}}). +A tool approval works in the same way, but the agent sends `tool_approval_request`, and the caller answers with `tool_approval_response`. For the pause kinds, the approval model, and what a nested agent's pause looks like, see [Human in the loop]({{< link path="agents/human-in-the-loop" >}}). ## Clean up * This example does not create Kubernetes resources, so you have nothing to delete. * You can stop the 8083 port-forward for the kagent-controller service with `Ctrl+C`. -* The tasks that your messages created stay on the Session as part of its conversation, and deleting the Session removes them. +* The tasks that your messages created stay on the Session, and deleting the Session removes them. ## Next steps diff --git a/docs-site/content/kagent/1.x/examples/a2a-byo.md b/docs-site/content/kagent/1.x/examples/a2a-byo.md index eafa9e17..8c07cf39 100644 --- a/docs-site/content/kagent/1.x/examples/a2a-byo.md +++ b/docs-site/content/kagent/1.x/examples/a2a-byo.md @@ -5,7 +5,7 @@ weight: 70 author: kagent.dev --- -The `byo` runtime runs a container image that you build, so long as the image implements kagent's {{< gloss "A2A" >}}A2A{{< /gloss >}} (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 {{< gloss "Harness" >}}Harness{{< /gloss >}}, and invoke it. +The `byo` runtime runs a container image that you build, so long as the image implements kagent's {{< gloss "A2A" >}}A2A{{< /gloss >}} (Agent2Agent) contract. This example takes the shortest path through that contract: build the minimal BYO agent that kagent tests itself against, run it on a {{< gloss "Harness" >}}Harness{{< /gloss >}}, and invoke it. The agent that you build here does not call a model or bind 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]({{< link path="agents/bring-your-own-agent" >}}). diff --git a/docs-site/content/kagent/1.x/examples/skills.md b/docs-site/content/kagent/1.x/examples/skills.md index e5267624..d8960f6f 100644 --- a/docs-site/content/kagent/1.x/examples/skills.md +++ b/docs-site/content/kagent/1.x/examples/skills.md @@ -5,17 +5,17 @@ weight: 40 author: kagent.dev --- -A {{< gloss "Skill" >}}skill{{< /gloss >}} packages know-how that an agent picks up at run time: a `SKILL.md` file of instructions, together with the scripts and reference files those instructions depend on. This example builds a skill that turns raw commit subjects into release notes, publishes it as an OCI image, and attaches it to an {{< gloss "AgentTemplate" >}}AgentTemplate{{< /gloss >}}. +A {{< gloss "Skill" >}}skill{{< /gloss >}} packages know-how that an agent picks up at run time. A skill consists of a `SKILL.md` file of instructions, together with the scripts and reference files that those instructions depend on. This example builds a skill that turns raw commit subjects into release notes, publishes it as an Open Container Initiative (OCI) image, and attaches it to an {{< gloss "AgentTemplate" >}}AgentTemplate{{< /gloss >}}. For the fields that attach a skill and the rules that govern their names, see [Skills]({{< link path="skills-and-mcp/skills" >}}). For the format of a plugin, see [Plugins]({{< link path="skills-and-mcp/plugins" >}}). ## About skills at run time -kagent does not fetch a skill when you apply an AgentTemplate. The compiled revision records the source of each skill, and the skill arrives when the agent starts. For an `oci` source, Agent Substrate pulls the image from the Worker and mounts it as read-only into the {{< gloss "Actor" >}}Actor{{< /gloss >}} before the runtime starts. For a `git` or `bucket` source, the Actor fetches the files itself. The runtime then copies each enabled skill into the directory that its harness reads, which is `/skills/` on a `kagent` Harness. +kagent does not fetch a skill when you apply an AgentTemplate. The compiled revision records the source of each skill, and the skill arrives when the agent starts. For an `oci` source, Agent Substrate pulls the image from the Worker and mounts it as read-only into the {{< gloss "Actor" >}}Actor{{< /gloss >}} before the runtime starts. For a `git` or `bucket` source, the Actor fetches the files itself. The runtime then copies each enabled skill into the directory that its Harness reads, which is `/skills/` on a `kagent` Harness. Because kagent fetches a skill this late, review the following considerations. -* **A wrong source still compiles.** kagent validates skill names before it accepts an AgentTemplate, but it never checks that the artifact exists or that it holds a `SKILL.md` file. A bad digest produces a revision that reports `Ready`, and the agent then fails to start. +* **A wrong source still compiles.** kagent validates skill names before it accepts an AgentTemplate, but it never checks that the artifact exists or that it contains a `SKILL.md` file. A bad digest produces a revision that reports `Ready`, and the agent then fails to start. * **A private registry needs Worker credentials.** Agent Substrate pulls an `oci` source with the registry credentials that the Worker uses for workload images. Kubernetes `imagePullSecrets` on a pod do not configure that pull. * **Each Actor mounts up to 29 distinct skill images.** Images shared by several skills or plugins count once. A revision that needs more fails when kagent builds its ActorTemplate. * **Scripts run in the runtime image.** A skill's scripts get whatever the Harness image provides. The kagent runtime image is Alpine Linux with `bash`, `git`, and the standard Alpine utilities, and it does **not** include Python. @@ -48,13 +48,14 @@ config: disableBash: true ``` -| Setting | Description | -| ------- | ----------- | +| Value | Description | +| ----- | ----------- | | `disableFileSearch` | Set to `true` to remove `list_files` and `grep_file` from the root agent and from every subagent. Defaults to `false`. This value replaces `skillTools.fileSearchTools` from earlier releases, with the opposite meaning. | | `disableBash` | Set to `true` to remove the `bash` tool from the root agent and from every subagent. The agent keeps the file tools, and still loads skills. Defaults to `false`. | -| `bashVenvPath` | The path to a Python virtual environment in the Harness image. The `bash` tool adds the environment's `bin` directory to the start of `PATH`, and sets `VIRTUAL_ENV`. The kagent runtime image does not include Python. To use this setting, build a Harness image that includes the environment. Defaults to empty. | +| `disableAskUser` | Set to `true` to remove the `ask_user` tool. Defaults to `false`. For more information, see [Disable questions]({{< link path="agents/human-in-the-loop#disable-questions" >}}). | +| `bashVenvPath` | The path to a Python virtual environment in the Harness image. The `bash` tool adds the environment's `bin` directory to the start of `PATH`, and sets `VIRTUAL_ENV`. The kagent runtime image does not include Python. To use this value, build a Harness image that includes the environment. Defaults to empty. | -Attaching a skill also changes what the agent is told. The runtime appends the name and description of every attached skill to the model request, along with an instruction to call `load_skill` before acting on one, so a skill reaches the model even before any tool is called. +When you attach a skill, the runtime appends the name and description of each attached skill to the model request, together with an instruction to call `load_skill` before the agent acts on a skill. The model therefore sees each skill before the agent calls any tool. > [!IMPORTANT] > The `bash` tool gives the agent shell access inside its own Actor sandbox, and the sandbox is the boundary that contains the agent. The file tools are not limited to the workspace. They read and write anywhere in the sandbox, including `/skills`, so one skill's instructions can change another skill's files. Review a skill before you attach it, and treat the [egress]({{< link path="about/substrate-runtime/sandboxing" >}}) that you grant the Actor as the reach that the skill has. @@ -85,7 +86,7 @@ A skill is a directory whose root holds a `SKILL.md` file. Everything else in th cd release-notes ``` -2. Write `SKILL.md`. The YAML front matter must carry a `name` and a `description`, and the body holds the instructions the agent follows. +2. Write `SKILL.md`. The YAML front matter must include a `name` and a `description`. The body contains the instructions that the agent follows. ```bash cat > SKILL.md <<'EOF' --- @@ -110,7 +111,7 @@ A skill is a directory whose root holds a `SKILL.md` file. Everything else in th EOF ``` - The model reads the `description` and decides whether to load the skill. The model sees the name and description of every attached skill, and chooses among them in the same way that it chooses any other tool. State plainly when the skill applies. The instructions in the body are only read after the agent calls `load_skill`. + The model reads the `description` to decide whether to load the skill, in the same way as the model decides whether to call a tool. In the `description`, state plainly when the skill applies. The agent reads the instructions in the body only after it calls `load_skill`. 3. Add the script that the instructions call. The script runs in the agent's runtime image, so it uses `bash` rather than Python. ```bash @@ -140,7 +141,7 @@ A skill is a directory whose root holds a `SKILL.md` file. Everything else in th chmod +x scripts/group.sh ``` -4. Confirm that the script works before you publish it. The `bash` tool returns a failed command's error to the model rather than to you, so a broken script produces an unreliable answer rather than a failed resource. +4. Before you publish the skill, confirm that the script works. The `bash` tool returns a failed command's error to the model, not to you. A broken script therefore produces an unreliable answer, not a failed resource. ```bash printf 'feat: add checkpoint API\nfix: correct revision digest\ndocs: update install guide\n' > /tmp/commits.txt bash scripts/group.sh /tmp/commits.txt @@ -160,7 +161,7 @@ A skill is a directory whose root holds a `SKILL.md` file. Everything else in th ## Publish the skill as an OCI image -Agent Substrate mounts an `oci` source as a container image. The image must hold the skill directory and nothing else. Build it from `scratch`, which produces an image whose root **is** the skill root. +Agent Substrate mounts an `oci` source as a container image. The image must contain the skill directory and nothing else. Build it from `scratch`, which produces an image whose root **is** the skill root. 1. Create the Dockerfile. ```bash @@ -188,6 +189,8 @@ Agent Substrate mounts an `oci` source as a container image. The image must hold ## Attach the skill to an AgentTemplate +To give an agent the skill, reference the skill image from the AgentTemplate that the agent runs, and start a Session on the revision that kagent compiles. + 1. Add a `skills` entry to an AgentTemplate, and pair it with your Harness through an Agent. Keep the model configuration that your existing template uses, and change only the name and the skill. ```bash kubectl apply -f - <@sha256:`. | -2. Confirm that the Agent compiled. The revision is ready when `desiredRevision` and `latestSuccessfulRevision` hold the same value. +2. Confirm that the Agent compiled. The revision is ready when `desiredRevision` and `latestSuccessfulRevision` report the same value. ```bash kubectl get agent release-writer -n kagent \ -o jsonpath='{.status.desiredRevision}{"\t"}{.status.latestSuccessfulRevision}{"\n"}' ``` > [!NOTE] - > A ready revision means that kagent accepted the reference, not that the image exists. kagent fetches the skill when the agent starts, so a wrong digest surfaces in the next step rather than this one. + > A ready revision means that kagent accepted the reference, not that the image exists. A wrong digest surfaces only when the agent starts. -3. Create a {{< gloss "Session" >}}Session{{< /gloss >}}, and save its ID to an environment variable. A Session pins the revision that it was created on, so a session that already exists does not pick up the skill. +3. Create a {{< gloss "Session" >}}Session{{< /gloss >}}, and save its ID to an environment variable. A Session pins the revision that it was created on, so a Session that already exists does not pick up the skill. ```bash export SESSION_ID=$(kagent agent session create --agent release-writer -o json | jq -r '.session.id') echo $SESSION_ID @@ -244,6 +247,8 @@ Agent Substrate mounts an `oci` source as a container image. The image must hold ## Ask the agent to use the skill +Send requests to the Session that you created, and check that the agent runs the skill's script. + 1. Send the agent a request that matches the skill's description. ```bash kagent agent invoke --session $SESSION_ID \ @@ -264,14 +269,14 @@ Agent Substrate mounts an `oci` source as a container image. The image must hold - update install guide ``` -3. Ask the agent what skills it holds, to confirm the attachment from the agent's own side. +3. To confirm the attachment from the agent's own side, ask the agent which skills it has. ```bash kagent agent invoke --session $SESSION_ID --task "What skills do you have?" ``` ## Publish a new version of the skill -A source is immutable, so changing a skill is a two-step change: publish new content, then point the AgentTemplate at it. +A source is immutable. To change a skill, publish new content under a new digest. 1. Edit the skill. @@ -281,13 +286,13 @@ A source is immutable, so changing a skill is a two-step change: publish new con export SKILL_OCI="$SKILL_REPO@$(docker buildx imagetools inspect --format '{{.Manifest.Digest}}' "$SKILL_REPO:1.1.0")" ``` -3. Update `skills[].source.oci` on the AgentTemplate with the new digest, which compiles a new revision. +3. Update `skills[].source.oci` on the AgentTemplate with the new digest. kagent compiles a new revision. -4. Create a new Session. Conversations that are already running keep the skill content they started with, because their revision is pinned. +4. Create a new Session. Sessions that already exist keep the skill content of their pinned revision. ## Bundle the skill in a plugin -While a standalone source contains one skill, a {{< gloss "Plugin" >}}plugin{{< /gloss >}} contains multiple skills. An AgentTemplate attaches the plugin once and lists the skills that it wants. Use a plugin when you ship a set of skills together, or when you want the same artifact to contribute [MCP servers]({{< link path="skills-and-mcp/plugins#mcp-servers" >}}) as well. +While a standalone source contains one skill, a {{< gloss "Plugin" >}}plugin{{< /gloss >}} contains multiple skills. An AgentTemplate attaches the plugin once and lists the skills that it wants. To ship a set of skills together, or to have the same artifact contribute [Model Context Protocol (MCP) servers]({{< link path="skills-and-mcp/plugins#mcp-servers" >}}) as well, use a plugin. 1. Restructure the directory so that each skill sits under `skills/`, and add the manifest that makes it a plugin. ```bash @@ -336,21 +341,21 @@ While a standalone source contains one skill, a {{< gloss "Plugin" >}}plugin{{< ``` > [!IMPORTANT] - > A plugin enables only the skills that you list. If you omit `plugins[].skills`, or leave it empty, the agent gets none of them, and kagent accepts an empty list rather than reporting an error. For why an explicit list is the safer default, see [Plugins]({{< link path="skills-and-mcp/plugins#skill-allowlist" >}}). + > A plugin enables only the skills that you list. If you omit `plugins[].skills`, or leave it empty, the agent does not get any of the skills, and kagent accepts the empty list without an error. For why an explicit list is the safer default, see [Plugins]({{< link path="skills-and-mcp/plugins#skill-allowlist" >}}). -4. Create a Session on the new revision, and confirm that the agent still has the skill. The session from the previous section is pinned to the revision that carried the standalone skill. +4. Create a Session on the new revision, and confirm that the agent still has the skill. The `$SESSION_ID` Session is pinned to the revision that included the standalone skill. ```bash export PLUGIN_SESSION_ID=$(kagent agent session create --agent release-writer -o json | jq -r '.session.id') kagent agent invoke --session $PLUGIN_SESSION_ID --task "What skills do you have?" ``` - The agent reports `release-notes` exactly as before. A skill behaves the same whether it arrives on its own or inside a plugin, because kagent copies both into `/skills` before the agent starts. + The agent reports `release-notes` exactly as before. A skill behaves in the same way whether it arrives on its own or inside a plugin, because kagent copies both into `/skills` before the agent starts. ## Troubleshoot a skill that does not load A skill that kagent cannot fetch stops the agent from starting at all, rather than producing an agent without that skill. The agent logs the failure and exits, and the Agent never becomes ready. -1. Check the Agent's `Ready` condition. A skill that cannot be fetched leaves the Agent waiting, because the Actor that builds the golden snapshot is the Actor that fetches the skill. +1. Check the Agent's `Ready` condition. A skill that cannot be fetched leaves the Agent waiting, because the Actor that builds the golden snapshot also fetches the skill. ```bash kubectl get agent release-writer -n kagent \ -o jsonpath='{range .status.conditions[?(@.type=="Ready")]}{.status} {.reason} {.message}{end}' @@ -364,7 +369,7 @@ A skill that kagent cannot fetch stops the agent from starting at all, rather th > [!NOTE] > This condition does not name the skill, and reports the same reason for any Actor that has not yet produced a snapshot. An agent that is merely still starting looks identical to one whose skill cannot be fetched. -2. Find the WorkerPool that the Harness runs on, and read its Workers' logs. The Actor writes the failure there rather than to the Agent. +2. Find the WorkerPool that the Harness runs on, and read its Workers' logs. The Actor writes the failure to the Worker logs, not to the Agent. ```bash export WORKER_POOL=$(kubectl get harness my-first-harness -n kagent \ @@ -378,17 +383,17 @@ A skill that kagent cannot fetch stops the agent from starting at all, rather th {"error":"materialize agent plugins: materialize skill \"release-notes\": pull ghcr.io/example-org/release-notes@sha256:3091b91...: Get \"https://ghcr.io/v2/\": EOF","labels":{"ate.atespace":"ate-golden","ate.template.name":"release-writer-my-first-harness-23c20dcb296d"},"level":"ERROR","msg":"failed to materialize Agent Plugins"} ``` - A pool runs the Workers for every agent on it, so filter by the `ate.template.name` label to find one agent. Its value is the AgentTemplate name, the Harness name, and the revision's short form, joined by hyphens. + A pool runs the Workers for every agent on it. To find the entries for one agent, filter by the `ate.template.name` label. Its value is the AgentTemplate name, the Harness name, and the revision's short form, joined by hyphens. 3. Match the message to its cause. | Message | Cause | | ------- | ----- | | `pull : ... 401 Unauthorized` | The registry needs credentials that the cluster does not have. | - | `pull : ... x509` or a TLS error | The registry does not serve HTTPS with a certificate the runtime trusts. | - | `SKILL.md is required` | The artifact was fetched, but no `SKILL.md` file sits at the root that `source.path` selects. | + | `pull : ... x509` or a Transport Layer Security (TLS) error | The registry does not serve HTTPS with a certificate that the runtime trusts. | + | `SKILL.md is required` | The artifact was fetched, but the root that `source.path` selects does not contain a `SKILL.md` file. | | `symlink "..." escapes artifact root` | A symlink in the artifact points outside it. | - | `artifact contains more than 10000 filesystem entries`, or `artifact exceeds 104857600 bytes` | The artifact is over one of the plugin limits. | + | `artifact contains more than 10000 filesystem entries`, or `artifact exceeds 104857600 bytes` | The artifact exceeds one of the plugin limits. | > [!TIP] > Build the skill image with `--platform` set to the architecture of your worker nodes. kagent asks the registry for the `linux/amd64` or `linux/arm64` manifest that matches the node it runs on, so an image published for one architecture alone fails on the other. @@ -413,7 +418,7 @@ A skill that kagent cannot fetch stops the agent from starting at all, rather th rm -rf release-notes release-tools ``` -4. Delete the images that you pushed, `$SKILL_REPO` and `$PLUGIN_REPO`, using your registry's own tooling. +4. Delete the images that you pushed, `$SKILL_REPO` and `$PLUGIN_REPO`, by using your registry's own tooling. ## Next steps diff --git a/docs-site/content/kagent/1.x/get-started/your-first-agent.md b/docs-site/content/kagent/1.x/get-started/your-first-agent.md index fd2c4b90..f0b19a7c 100644 --- a/docs-site/content/kagent/1.x/get-started/your-first-agent.md +++ b/docs-site/content/kagent/1.x/get-started/your-first-agent.md @@ -10,10 +10,7 @@ This guide walks you through creating an agent, from applying a Harness and an A ## Before you begin 1. [Install kagent with a WorkerPool provisioned]({{< link path="setup/installation" >}}). -2. Download the kagent CLI. The `--version` flag matches the CLI to the release that these docs cover. - ```bash - curl https://raw.githubusercontent.com/kagent-dev/kagent/refs/heads/main/scripts/get-kagent | bash -s -- --version v{{< reuse "kagent-docs/versions/kagent.md" >}} - ``` +2. {{< reuse "kagent-docs/snippets/install-cli.md" >}} 3. Install [`jq`](https://jqlang.org/download/), to read the Session ID out of the CLI's JSON output. @@ -121,7 +118,7 @@ A Session is one running conversation with an Agent. Creating it starts an Actor ## Talk to your agent -1. Send a message to the Session. The CLI holds the conversation over the {{< gloss "A2A" >}}A2A{{< /gloss >}} (Agent-to-Agent) protocol. +1. Send a message to the Session. The CLI holds the conversation over the {{< gloss "A2A" >}}A2A{{< /gloss >}} (Agent2Agent) protocol. ```bash kagent agent invoke --session $SESSION_ID --task "What is 2+2?" ``` diff --git a/docs-site/content/kagent/1.x/get-started/your-first-mcp-tool.md b/docs-site/content/kagent/1.x/get-started/your-first-mcp-tool.md index 90f6c864..34a6b7cc 100644 --- a/docs-site/content/kagent/1.x/get-started/your-first-mcp-tool.md +++ b/docs-site/content/kagent/1.x/get-started/your-first-mcp-tool.md @@ -26,8 +26,8 @@ kagent records what it discovered on the server's status, so the tool names come Example output: The `ACCEPTED` column reports whether kagent reached the server and read its catalog. No tool can be bound from a server that is not `True`. ```console - NAME PROTOCOL URL ACCEPTED AGE - kagent-tool-server STREAMABLE_HTTP http://kagent-tools.kagent:8084/mcp True 14m + NAME PROTOCOL URL ACCEPTED + kagent-tool-server STREAMABLE_HTTP http://kagent-tools.kagent.svc.cluster.local:8084/mcp True ``` To see the tools that the server offers, read the discovered set from its status. @@ -39,14 +39,6 @@ kagent records what it discovered on the server's status, so the tool names come > [!NOTE] > The built-in server is installed only when the `kagent-tools.enabled` Helm value is `true`, which is the default. If the command does not return any resources, either re-install with that value enabled, or use your own server as described in [Bind your own MCP server](#bind-your-own-mcp-server). - > [!WARNING] - > At v1.0.0-alpha17, an agent's sandbox cannot resolve the short Service name in the built-in server's URL. Every tool call then fails with a `no such host` error. For more information, see [kagent#3315](https://github.com/kagent-dev/kagent/issues/3315). Until a fix ships, run the following command to point the server at the fully qualified Service name. The kagent Helm chart owns the `kagent-tool-server` resource, and a `helm upgrade` reverts the patch, so run the command again after each upgrade. - - ```bash - kubectl patch remotemcpserver kagent-tool-server -n kagent --type=merge \ - -p '{"spec":{"url":"http://kagent-tools.kagent.svc.cluster.local:8084/mcp"}}' - ``` - 2. Re-apply the `my-first-template` AgentTemplate with a `spec.tools` list and a system prompt that tells the model what the tools are for. ```yaml kubectl apply -f - <}}). | | `mcp.tools` | Optional. The names of the tools to bind, up to 50. An omitted or empty list exposes every tool on the server. An AgentTemplate takes at most 50 bindings in total. | 3. Confirm that kagent compiled a new {{< gloss "Revision" >}}revision{{< /gloss >}} for the Agent that uses the edited template. Every edit produces a new desired revision, and the Agent is current when the latest successful revision matches it. diff --git a/docs-site/content/kagent/1.x/observability/launch-ui.md b/docs-site/content/kagent/1.x/observability/launch-ui.md index b81d0666..81180ec5 100644 --- a/docs-site/content/kagent/1.x/observability/launch-ui.md +++ b/docs-site/content/kagent/1.x/observability/launch-ui.md @@ -5,7 +5,7 @@ weight: 10 author: kagent.dev --- -The {{< reuse "kagent-docs/snippets/name-product.md" >}} UI is a read-and-write console for everything the controller knows about: the agents you defined, the conversations the agents hold, and the Agent Substrate capacity that those conversations run on. The kagent chart installs the UI alongside the controller, so a cluster that follows [Install kagent]({{< link path="setup/installation" >}}) already runs a UI instance. +The {{< reuse "kagent-docs/snippets/name-product.md" >}} UI is a read-and-write console for everything that the controller knows about, including the agents that you defined, the conversations that the agents hold, and the Agent Substrate capacity that those conversations run on. The kagent chart installs the UI alongside the controller, so a cluster that follows [Install kagent]({{< link path="setup/installation" >}}) already runs a UI instance. ## Before you begin @@ -57,27 +57,27 @@ The UI service is a `ClusterIP` by default, so it is reachable only from inside 2. Open [http://localhost:8082](http://localhost:8082) in your browser. -3. When you are done, press `Ctrl+C` in the terminal that holds the port-forward. +3. When you are done, press `Ctrl+C` in the terminal that runs the port-forward. {{% /tab %}} {{< /tabs >}} -The dashboard opens on a summary of what the controller has loaded. A fresh installation counts zero agents and zero conversations, and already counts the model configuration and the {{< gloss "Model Context Protocol" >}}Model Context Protocol{{< /gloss >}} (MCP) servers that the chart installed. +The dashboard opens on a summary of what the controller has loaded. On a fresh installation, the dashboard counts zero agents and zero conversations. The dashboard also counts the model configuration and the {{< gloss "Model Context Protocol" >}}Model Context Protocol{{< /gloss >}} (MCP) servers that the chart installed. {{< reuse-image-light src="img/kagent-ui-dashboard.png" alt="The kagent dashboard after a fresh installation" caption="Figure: The kagent dashboard after a fresh installation" >}} {{< reuse-image-dark srcDark="img/kagent-ui-dashboard-dark.png" alt="The kagent dashboard after a fresh installation" caption="Figure: The kagent dashboard after a fresh installation" >}} ## Explore the UI -The left menu groups the console by the resource each page reads, so the page you want follows from the resource you are asking about. +The left menu groups the console pages by the resource that each page reads. | Page | Shows | | ---- | ----- | | **Dashboard** | Counts of agents, model configurations, MCP servers, and discovered tools, plus recent conversations. | | **Agents** | Every {{< gloss "Agent" >}}Agent{{< /gloss >}}, {{< gloss "AgentTemplate" >}}AgentTemplate{{< /gloss >}}, and {{< gloss "Harness" >}}Harness{{< /gloss >}}, and the conversations held with each Agent. | -| **Schedules** | Agents that run automatically, each execution starting a new conversation. | -| **Models** | The {{< gloss "ModelConfig" >}}ModelConfig{{< /gloss >}} resources that agents name, and the providers behind them. | +| **Schedules** | Agents that run automatically. Each execution starts a new conversation. | +| **Models** | The {{< gloss "ModelConfig" >}}ModelConfig{{< /gloss >}} resources that agents reference, and the providers behind them. | | **MCP Servers** | Connected {{< gloss "RemoteMCPServer" >}}RemoteMCPServer{{< /gloss >}} resources and the tools discovered from each. | -| **Prompts** | Prompt libraries, which hold reusable fragments that an AgentTemplate includes in its instructions. | +| **Prompts** | Prompt libraries, which store reusable fragments that an AgentTemplate includes in its instructions. | | **Substrate** | WorkerPools, Actors, and Workers, read from both Kubernetes and the Agent Substrate API. | Start at the **Agents** page to review your agents and the resources behind them. The page lists each resource kind on its own tab: **Agents**, **Templates**, and **Harnesses**. @@ -85,11 +85,11 @@ Start at the **Agents** page to review your agents and the resources behind them {{< reuse-image-light src="img/kagent-ui-agents.png" alt="The Agents page, listing agents, templates, and harnesses" caption="Figure: The Agents page" >}} {{< reuse-image-dark srcDark="img/kagent-ui-agents-dark.png" alt="The Agents page, listing agents, templates, and harnesses" caption="Figure: The Agents page" >}} -The **Agents** tab authors the Agent custom resource directly. Create one to pair a template with a Harness, edit one to change either side, and delete one to retire the pairing. Deleting an Agent leaves its Sessions in place, along with the AgentTemplate and Harness that it named. For CLI steps, see [Create your first agent]({{< link path="get-started/your-first-agent" >}}). +The **Agents** tab authors the Agent custom resource directly. Create one to pair a template with a Harness, edit one to change either side, and delete one to retire the pairing. Deleting an Agent leaves its Sessions in place, along with the AgentTemplate and Harness that it referenced. For CLI steps, see [Create your first agent]({{< link path="get-started/your-first-agent" >}}). ## Read a conversation -Opening an agent and sending a message creates a {{< gloss "Session" >}}Session{{< /gloss >}}, which is one conversation scheduled onto an Agent Substrate {{< gloss "Actor" >}}Actor{{< /gloss >}}. The conversation view shows the transcript, the tool calls the agent made along the way, and the checkpoint controls for the conversation. +Opening an agent and sending a message creates a {{< gloss "Session" >}}Session{{< /gloss >}}, which is one conversation scheduled onto an Agent Substrate {{< gloss "Actor" >}}Actor{{< /gloss >}}. The conversation view shows the transcript, the tool calls that the agent made along the way, and the checkpoint controls for the conversation. {{< reuse-image-light src="img/kagent-ui-chat.png" alt="A conversation with an agent, showing a named snapshot and the controls on its mark" caption="Figure: A conversation with an agent" >}} {{< reuse-image-dark srcDark="img/kagent-ui-chat-dark.png" alt="A conversation with an agent, showing a named snapshot and the controls on its mark" caption="Figure: A conversation with an agent" >}} @@ -98,11 +98,13 @@ The UI labels a {{< gloss "Checkpoint" >}}checkpoint{{< /gloss >}} **Snapshot**. To take a checkpoint, select the save icon beside the message box. kagent pins the snapshot that the Session most recently suspended to, and records how far the transcript advanced. To understand how pinning works, and why a turn must be complete first, see [Checkpoints]({{< link path="about/substrate-runtime/suspend-and-resume#checkpoints" >}}). -A checkpoint appears in the transcript as a mark carrying the snapshot name and three controls. Select the mark to open the record behind it. The record holds the checkpoint's ID, turn, state, and age. +A checkpoint appears in the transcript as a mark that shows the snapshot name and the following controls. -- **Fork** starts a second Session from the checkpoint. The new Session replays the conversation through the pinned turn and then continues on its own. For what a fork keeps from its source, see [Checkpoints]({{< link path="about/substrate-runtime/suspend-and-resume#checkpoints" >}}). -- **Rename** replaces the snapshot name that kagent generates on creation from the Session ID and the turn the checkpoint pins. Clearing the name restores that generated default. -- **Delete** removes the checkpoint and releases the snapshot it pinned. Agent Substrate is then free to collect that snapshot. +- **Fork** starts a new Session from the checkpoint. The new Session replays the conversation through the pinned turn and then continues on its own. For what a fork keeps from its source, see [Checkpoints]({{< link path="about/substrate-runtime/suspend-and-resume#checkpoints" >}}). +- **Rename** replaces the default snapshot name, which kagent generates from the Session ID and the pinned turn when it creates the checkpoint. Clearing the name restores that generated default. +- **Delete** removes the checkpoint and releases the snapshot that it pinned. Agent Substrate is then free to collect that snapshot. + +Select the mark to open the record behind it. The record shows the checkpoint's ID, turn, state, and age. > [!NOTE] > A checkpoint is always taken at the latest turn boundary, so the UI offers checkpoint controls on your own most recent message and not on earlier ones. @@ -117,7 +119,7 @@ Stage files in any of the following ways. - Select the paperclip beside the message box, then choose the files. - Drop files anywhere on the chat page. -- Paste a file or an image from your clipboard. A clipboard that also holds plain text pastes the text instead, so that a copy from a word processor keeps its text rather than its image rendering. +- Paste a file or an image from your clipboard. A clipboard that also contains plain text pastes the text instead, so that a copy from a word processor keeps its text rather than its image rendering. Sent files stay in the transcript as download chips, and reloading the page redraws them. How a file reaches the model depends on its type and on the agent's model provider. @@ -127,25 +129,25 @@ Sent files stay in the transcript as download chips, and reloading the page redr When the message text includes a link to a file of an accepted type other than HTML, such as `https://example.com/q3-report.pdf`, the UI also sends the link as a file link. When the ModelConfig sets `apiFormat: responses`, or the provider is Gemini, the model fetches the file itself. The provider must therefore be able to reach the link. The UI does not send a link to an IP address or to a local hostname, such as one that ends in `.svc` or `.local`, because the provider cannot reach such a host. Every other model receives the link as text, and an agent with a fetch tool can read the file from it. Only the message that includes a link sends it as a file. In later turns, the model receives the bare URL as text. -An empty file, or one that holds only whitespace, reaches the model as the note `[Uploaded file "notes.txt" (text/plain) contained no extractable text.]` instead of contents, so the agent reports the gap rather than answering from nothing. +An empty file, or one that contains only whitespace, reaches the model as the note `[Uploaded file "notes.txt" (text/plain) contained no extractable text.]`, so the agent reports the gap. -The following limits apply to attachments: +Different components between the browser and the model enforce the attachment limits: | Limit | Value | Enforced by | | ----- | ----- | ----------- | -| Accepted file types | Plain text, Markdown, CSV, JSON, XML, YAML, HTML, PDF, Word (`.docx`), Excel (`.xlsx`), and PowerPoint (`.pptx`) files, and PNG, JPEG, GIF, and WebP images | The UI, as you stage each file. A rejected file is reported by name. | +| Accepted file types | Plain text, Markdown, CSV, JSON, XML, YAML, HTML, PDF, Word (`.docx`), Excel (`.xlsx`), and PowerPoint (`.pptx`) files, and PNG, JPEG, GIF, and WebP images | The UI, as you stage each file. The UI reports a rejected file by name. | | Total size of the files in one message | 10 MB | The UI, as you stage each file. | | Text read from one file | 200,000 characters | The runtime, which appends `[truncated]` to a longer file. | -| gRPC message that the kagent API accepts | 16 MiB | The controller's gRPC server, and every kagent gRPC client. The limit is a compiled-in constant that no Helm value changes. | +| gRPC message that the kagent API accepts | 16 MiB | The controller's gRPC server, and every kagent gRPC client. The limit is a compiled-in constant that a Helm value cannot change. | | Request body that the UI's nginx accepts | `ui.nginx.clientMaxBodySize`, default `20m` | The UI pod's nginx. The default sits just above the 16 MiB gRPC message limit. | The UI falls back to the file extension when the browser does not report a media type for a file, or reports the wrong one. For example, browsers do not report a media type for `.md` files. -If you host a reverse proxy in front of the UI, the proxy applies its own body-size limit, and a default nginx allows only `1m`. Raise the limit as shown in [Serve the UI under a sub-path]({{< link path="setup/reverse-proxy#proxy-shapes" >}}). +If you host a reverse proxy in front of the UI, the proxy applies its own body-size limit. A default nginx allows only `1m`. To raise the limit, see [Serve the UI under a sub-path]({{< link path="setup/reverse-proxy#proxy-shapes" >}}). ## Check Agent Substrate capacity -The **Substrate** page shows whether there is capacity for an agent to run. It reads WorkerPools and ActorTemplates from Kubernetes, and live Actors and Worker assignments from the Agent Substrate API. +The **Substrate** page shows whether Agent Substrate has capacity to run an agent. It reads WorkerPools and ActorTemplates from Kubernetes, and live Actors and Worker assignments from the Agent Substrate API. {{< reuse-image-light src="img/kagent-ui-substrate.png" alt="The Substrate page, showing one worker pool and no running actors" caption="Figure: The Substrate page" >}} {{< reuse-image-dark srcDark="img/kagent-ui-substrate-dark.png" alt="The Substrate page, showing one worker pool and no running actors" caption="Figure: The Substrate page" >}} @@ -154,16 +156,18 @@ Read the page from the top tiles down. | Tile | Means | | ---- | ----- | -| **Worker pools** | {{< gloss "WorkerPool" >}}WorkerPools{{< /gloss >}} that an operator has provisioned. No Harness can run an agent until at least one exists. | +| **Worker pools** | {{< gloss "WorkerPool" >}}WorkerPools{{< /gloss >}} that an operator has provisioned. A Harness cannot run an agent until at least one WorkerPool exists. | | **Templates ready** | {{< gloss "ActorTemplate" >}}ActorTemplates{{< /gloss >}} that have compiled and are ready to be instantiated. | -| **Actors running** | Live Actors, each one a Session holding a conversation. | +| **Actors running** | Live Actors. Each Actor runs one Session. | | **Workers busy** | {{< gloss "Worker" >}}Workers{{< /gloss >}} currently assigned to an Actor, against the total provisioned. | -| **Scope** | The filters this page asked for, rather than what the API returned. Reads `all` until you set a filter, then `K8s: ; ATE: `. | +| **Requested filters** | The filters this page asked for, not what the API returned. Reads `none` until you set a filter, then `K8s: ; ATE: `, with `any` in place of a filter that you leave empty. | + +On a fresh installation, the page reports one WorkerPool, zero ActorTemplates, and zero Actors, because an agent does not exist yet. The **Workers busy** tile reads `0/1`, and the Workers table lists the pool's single worker with the pod that backs it. That unassigned worker confirms that capacity is provisioned and waiting for the first agent. The Actors table reads `No actors on this page.` -A fresh installation reports one worker pool, no actor templates, and no actors, because nothing has created an agent yet. The **Workers busy** tile reads `0/1`, and the Workers table lists the pool's single worker with the pod that backs it. That unassigned worker confirms that capacity is provisioned and waiting for the first agent. The Actors table reads `No actors on this page.` +When you [restrict kagent to specific namespaces]({{< link path="operations/multiple-namespaces#restrict-kagent-to-specific-namespaces" >}}), several kagent installations can share one Agent Substrate. The Substrate page then reads only the watched namespaces and the atespaces whose names kagent derives from those namespaces. Empty filters cover every watched namespace. A filter that specifies any other namespace or atespace fails with a `PermissionDenied` error. An installation that watches every namespace reads the entire Agent Substrate. > [!NOTE] -> When the Substrate page cannot reach the Agent Substrate API, it reports the failure rather than an empty result. A warning above the tables carries the error, and the Actors table reads `This read could not reach ate-api, so there may be actors it did not see.` On a cluster that does have agents, `No actors on this page.` means that the read succeeded and matched nothing, so check the **Kubernetes namespace** and **ATE atespace** filters. +> When the Substrate page cannot reach the Agent Substrate API, it reports the failure rather than an empty result. A warning above the tables shows the error, and the Actors table reads `This read could not reach ate-api, so there may be actors it did not see.` On a cluster that does have agents, `No actors on this page.` means that the read succeeded and matched nothing, so check the **Kubernetes namespace** and **ATE atespace** filters. ## Expose the UI outside the cluster @@ -187,9 +191,9 @@ kubectl get svc -n kagent {{< reuse "kagent-docs/snippets/name-ui.md" >}} ### OpenShift route -On OpenShift clusters kagent creates an edge-terminated `Route` for the UI whenever the `route.openshift.io/v1` API is present. The route is enabled by default through `ui.route.enabled`. +On OpenShift clusters, kagent creates an edge-terminated `Route` for the UI whenever the `route.openshift.io/v1` API is present. The route is enabled by default through `ui.route.enabled`. -The default HAProxy timeout is raised to 120 minutes, because agent conversations hold long-lived streaming connections that a shorter timeout terminates mid-reply. To change it, set the annotation. +The chart raises the default HAProxy timeout to 120 minutes, because agent conversations keep long-lived streaming connections open, and a shorter timeout terminates them mid-reply. To change the timeout, set the annotation. ```yaml ui: @@ -198,7 +202,7 @@ ui: haproxy.router.openshift.io/timeout: 60m ``` -To front the UI with your own ingress instead, set `ui.route.enabled: false` to stop kagent creating the route. +To front the UI with your own ingress instead, set `ui.route.enabled: false` so that kagent does not create the route. ### Gateway API HTTPRoute diff --git a/docs-site/content/kagent/1.x/observability/tracing.md b/docs-site/content/kagent/1.x/observability/tracing.md index f2788844..4a04dfdf 100644 --- a/docs-site/content/kagent/1.x/observability/tracing.md +++ b/docs-site/content/kagent/1.x/observability/tracing.md @@ -37,7 +37,7 @@ flowchart LR class grpc,gateway,router,runtime inner ``` -A caller reaches the gRPC API on the kagent controller, which starts the trace. The controller hands the request to its A2A gateway, which opens an {{< gloss "A2A" >}}A2A{{< /gloss >}} (Agent-to-Agent) connection to the Session's Actor and injects a `traceparent` header into that call. The Agent Substrate router forwards the call to the Worker that runs the Actor, and adds its own spans to the trace. The agent runtime inside the Actor reads the header and continues the same trace, so the model and tool spans it produces hang off the controller's spans rather than starting a trace of their own. +A caller reaches the gRPC API on the kagent controller, which starts the trace. The controller hands the request to its A2A gateway, which opens an {{< gloss "A2A" >}}A2A{{< /gloss >}} (Agent2Agent) connection to the Session's Actor and injects a `traceparent` header into that call. The Agent Substrate router forwards the call to the Worker that runs the Actor, and adds its own spans to the trace. The agent runtime inside the Actor reads the header and continues the same trace, so the model and tool spans it produces hang off the controller's spans rather than starting a trace of their own. Each hop sends its spans under a separate OpenTelemetry (OTel) service name. A tracing backend groups the spans by those names. @@ -241,7 +241,7 @@ Agent Substrate records traces for its own work, such as scheduling an Actor ont | ------- | ------- | | `ateapi` | Actor lifecycle operations, such as create, resume, and suspend, and the scheduling of Actors onto Workers. | | `atelet` | Work on a Worker's node, such as restoring an Actor from a snapshot. | -| `ateom-gvisor` | Work inside the sandbox that runs the Actor. | +| `ateworker-gvisor` | Work inside the sandbox that runs the Actor. | | `atecontroller` | Reconciliation of Agent Substrate resources, such as WorkerPools. | Agent Substrate exports traces only when its Helm release sets `otel.endpoint`, and it keeps 1% of its traces by default. To keep more, raise `otel.traces.samplingRatio`. diff --git a/docs-site/content/kagent/1.x/operations/debug.md b/docs-site/content/kagent/1.x/operations/debug.md index ebd3259d..1b7f871e 100644 --- a/docs-site/content/kagent/1.x/operations/debug.md +++ b/docs-site/content/kagent/1.x/operations/debug.md @@ -5,24 +5,11 @@ weight: 30 author: kagent.dev --- -Most {{< reuse "kagent-docs/snippets/name-product.md" >}} failures surface in one place: the readiness conditions on the {{< gloss "Agent" >}}Agent{{< /gloss >}}. This page explains where to read those conditions, how to tell the real failure from the ones that follow it, and how to work back from the symptoms that report nothing useful on their own. +Most {{< reuse "kagent-docs/snippets/name-product.md" >}} failures surface in the readiness conditions on the {{< gloss "Agent" >}}Agent{{< /gloss >}}. This page explains where to read those conditions, how to tell the real failure from the ones that follow it, and how to work back from the symptoms that report nothing useful on their own. ## Where kagent reports status -Two habits from Kubernetes lead nowhere here, so it is worth knowing what reports nothing before you start. - -A {{< gloss "Harness" >}}Harness{{< /gloss >}} has a `READY` column, but because nothing writes the Harness status, the column is always empty. - -```bash -kubectl get harness -n kagent -``` -Example output: -```console -NAME READY AGE -kagent 30h -``` - -An {{< gloss "AgentTemplate" >}}AgentTemplate{{< /gloss >}} has **no status at all**, and its list view has no status column, so a healthy template and a broken one look identical. +A {{< gloss "Harness" >}}Harness{{< /gloss >}} and an {{< gloss "AgentTemplate" >}}AgentTemplate{{< /gloss >}} have **no status at all**, and their list views have no status column, so a healthy resource and a broken one look identical. ```bash kubectl get agenttemplate -n kagent @@ -85,9 +72,15 @@ Compatible=False Blocked: blocked by ResolvedRefs Ready=False Blocked: blocked by ResolvedRefs ``` -The Agent's status reports two revision fields that answer a different question. `desiredRevision` is the {{< gloss "Revision" >}}revision{{< /gloss >}} that the current spec should produce, and `latestSuccessfulRevision` is the newest revision that finished preparing. A missing `latestSuccessfulRevision` means that the Agent was never ready, and two differing values mean that the newest edit is not yet prepared. +A failed golden snapshot is not final. When Agent Substrate fails to build the golden snapshot, kagent deletes the failed ActorTemplate and rebuilds it after 15 seconds, 1 minute, and 4 minutes, and then every 10 minutes indefinitely. While a rebuild waits, `Ready` is `False` with the reason `ActorTemplateRetrying` and a message such as `golden build failed (GoldenActorCrashed), retry 1 at