Repository navigation
feat(adk): Go runtime conversation-history compaction - #2025
Closed
QuentinBisson wants to merge 2 commits into
Closed
QuentinBisson wants to merge 2 commits into
QuentinBisson wants to merge 2 commits into
Conversation
Port adk-python's sliding-window + token-threshold compaction to the Go runtime, bringing it to full feature parity with spec.declarative.context.compaction. Changes: - New go/adk/pkg/compaction package: watermark-based invocation counting, longestSelfContainedPrefix safety, token-threshold mode, and LLM summarization wired through existing model adapters. - go/adk/pkg/runner/adapter.go: buildCompactionConfig translates AgentCompressionConfig into a Compactor, wiring SummarizerLLM from AgentCompressionConfig.SummarizerModel (or falling back to agent model). - go/adk/pkg/a2a/executor.go: MaybeCompact called post-invocation (step 11), best-effort, never fails the request. - go/adk/pkg/session/local_session.go: ReplaceEvents for forward compat. - go/core/internal/controller/reconciler/reconciler.go: remove UnsupportedFeatures guard for context compaction. - go/go.mod: replace directive pins google.golang.org/adk to github.com/QuentinBisson/adk-go feat/event-compaction (adds EventCompaction type, EventActions.Compaction field, and applyCompaction pass in buildContentsDefault). Summary events are persisted as compaction marker events in the session backend (idempotent / resumable), not as in-memory req.Content mutations. The Anthropic null-system 400 (adk-python#5318) is structurally avoided: the summarizer request carries no SystemInstruction. Signed-off-by: QuentinBisson <[email protected]>
QuentinBisson
force-pushed
the
feat/go-adk-compaction
branch
from
June 15, 2026 22:25
763cc6c to
16fe1c1
Compare
…nvocationID Replace the personal adk-go fork (`github.com/QuentinBisson/adk-go`) with the canonical `google.golang.org/adk` module at v1.4.0. The fork added `EventActions.Compaction` and `EventCompaction` types to persist compaction summaries. Those types do not exist in upstream v1.4.0, so the approach is changed: compaction marker events are encoded entirely within the existing `Event` fields — `Author = "compaction"` and `InvocationID = "compaction_<startNano>_<endNano>"` — with the summary in `Event.Content`. Key changes: - compaction.go: rewritten to use InvocationID encoding; adds `buildCompactionEvent`, `parseCompactionInvocationID`, `findWatermark`, `filterCompactionMarkers`, and exported `BuildCompactedEventList` - session_wrapper.go: new `compactingService` wrapper that applies `BuildCompactedEventList` on `Get` so the runner receives a compacted view while the raw marker events remain in storage for `MaybeCompact` - adapter.go: wraps the session service with `NewCompactingService` when compaction is configured; unwrapping is transparent to the executor - local_session.go: remove `ReplaceEvents` (no callers) - go.mod: drop `replace google.golang.org/adk => …` directive; upgrade to `google.golang.org/adk v1.4.0` Signed-off-by: QuentinBisson <[email protected]>
|
This pull request has been marked as stale because of no activity in the last 15 days. It will be closed in the next 5 days unless it is tagged "no stalebot" or other activity occurs. |
|
This pull request has been closed after 15 days of inactivity and an additional 5 days after being marked as stale. |
This was referenced Sep 10, 2026
pull Bot
pushed a commit
to TheTechOddBug/kagent
that referenced
this pull request
Sep 20, 2026
## Problem Since 0.10.0 the Agent CRD defaults `spec.declarative.runtime` to `go`, and the Go runtime never read `spec.declarative.context.compaction`. The controller compiles the field into the agent's `config.json` (`context_config.compaction`) and the Python runtime consumes it, but a Go agent grew its prompt with every turn regardless of what the operator configured; the only signal was the `UnsupportedFeatures` condition (kagent-dev#2728). Measured on an internal installation running 0.10.1: an agent session grew from 4k to 190k input tokens per model call over 119 calls, and every Go agent there ran without compaction because the runtime ignored the field. `google.golang.org/adk/v2` v2.3.0 (2026-08-31) ships native compaction (`session/compaction`: sliding window, tail retention, LLM summarizer) and the `runner.Config.Compaction` hook, so the runtime can consume it instead of carrying its own port of the algorithm, which is what kagent-dev#1971 and kagent-dev#2025 attempted before the primitives existed. ## Change - **Dependency:** `google.golang.org/adk/v2` v2.2.0 → v2.3.0. The OpenTelemetry exporters and contrib instrumentation move to the matching v1.45.0 / v0.21.0 / v0.70.0 set, which the new ADK version requires for `otel/log`. otel/log v0.21.0 hands log bodies as attribute.Value; the telemetry test on main follows that in the same commit. No other kagent code needed changes for the bump. - **Runtime:** `agent.CompactionConfig` translates `context_config.compaction` into a `compaction.Config` and `runner.CreateRunnerConfig` hands it to the ADK runner. `compaction_interval` / `overlap_size` drive the sliding window, `token_threshold` / `event_retention_size` tail retention; either strategy is off when its settings are unset, so an agent without `context_config` runs exactly as before. - **Half-configured settings** are ignored with a warning rather than rejected: `token_threshold` without `event_retention_size` (and the reverse), `overlap_size` without an interval. That mirrors the Python runtime, where adk-python runs tail retention only when both settings are present. What remains is validated at startup with the runner's own rules, so a configuration the runner would refuse (negative values, no strategy at all) fails fast with the reason instead of on the first turn. - **Summarizer:** `summarizer_model` builds a `compaction.LLMSummarizer` through the same model factory as the agent (`CreateLLM`), with the model's generation config and a 60 s bound per call (the same bound the runner applies to its default summarizer). A `prompt_template` gets the tool-name instruction the Python runtime appends (kagent-dev#1907), so summaries keep namespaced tool names; a template without a summarizer model runs on the agent's own model. With neither, the runner summarizes with the agent's model and ADK's default prompt, matching the Python runtime. - **Observability:** startup logs `context compaction enabled` with the resolved values and the summarizer model, or `context compaction not configured`, so an operator can see from the agent log what the runtime does with the field. - **API:** `main` had no field for compaction in the v1alpha3 API (the 0.10 line has `spec.declarative.context`), so nothing produced `context_config`. `spec.kagent.compaction` on the `Harness` adds it, next to `spec.kagent.memory`: `compactionInterval` / `overlapSize` for the sliding window, `tokenThreshold` / `eventRetentionSize` for tail retention, and an optional `summarizer` with a `modelConfigRef` (a `ModelConfig` in the Harness namespace) and a `promptTemplate`. It is runtime policy rather than portable agent behaviour (specific to the ADK runner; the Claude and Codex harnesses have nothing to apply it to), which is why it lives on the Harness and not on the `AgentTemplate`. The CRD rejects at admission what the runtime would refuse at startup: no strategy, `tokenThreshold` without `eventRetentionSize` (and the reverse), `overlapSize` without `compactionInterval`, a `promptTemplate` without `{conversation_history}`. - **Controller:** the kagent compiler translates the Harness's compaction policy into `context_config` in `config.json` of every root agent the Harness runs. A summarizer `ModelConfig` other than the agent's own is resolved from the Harness namespace like the memory model, so its credentials and egress join the revision and a change to it compiles a new revision; the agent's own model stays implicit because the runtime already summarizes with it by default. Compaction records travel on `EventActions.Compaction`; the local DurableDir session store persists `Actions` as a whole (ADK's `session/database`), so a summary survives an actor restart and the same range is not summarized again. The 0.10 line, where the field already exists on the Agent CRD and the reconciler flagged it as unsupported, is kagent-dev#2791. ## Testing - `go test ./adk/pkg/agent/`: `TestCompactionStrategies` (table: each strategy alone and combined, unset settings, the three half-configured cases, no strategy, negative values), `TestCompactionConfig` (not configured → nil; default summarizer; dedicated summarizer model; custom prompt on the agent model; prompt without `{conversation_history}` rejected; prompt without any model rejected; the `context_config` block as the controller compiles it), `TestCompactionSlidingWindowFiresAfterInterval` (a scripted model drives the ADK runner: no record after turn 1, one record after turn 2 whose content is the summarizer's reply, and turn 3's request shows the summary and none of the compacted turns), `TestCompactionTailRetentionFiresOverTokenThreshold`. - `go test ./core/internal/translator/...`: `TestApplyCompaction` (not configured → no `context_config`; all four settings and a prompt on the agent model → mapped with no `summarizer_model`; a dedicated summarizer `ModelConfig` → its model in `config.json`, its credential in the environment, its host in the egress, itself in the provenance models; a missing summarizer `ModelConfig` → error). - `go test ./api/v1alpha3/` (envtest): the five new CEL cases reject each half configuration with its message, and the fully configured Harness is accepted. - `make -C go test`: all packages pass. - `make -C go lint`: 0 issues. ## End-to-end `TestAgentInstanceContextCompaction` (in the `test-e2e` job) runs the whole chain with `mockllm` as the summarizer: a clone of the suite's `kagent` Harness with `compactionInterval: 2` and a dedicated summarizer `ModelConfig`, admitting a plain `AgentTemplate`; both models point at one mock LLM behind a recording proxy on the test host; a default header on each `ModelConfig` tells the two models apart. Three turns through the public A2A API, then the recorded requests show what the runtime sent: the summarizer model was called exactly once, after the second turn, with both turns in the transcript and the custom prompt; the second turn's request still carried the first turn raw; the third turn's request carries the summary and none of the four compacted messages. Run before pushing against a local kind cluster with Substrate 0.0.26 and the controller and Go ADK images built from this branch: passes in 4.9 s. The real-model run (kind, images built from the branch, Anthropic `claude-haiku-4-5`, a five-turn tool-using conversation: turn 5 needs 2,524 prompt tokens with compaction against 50,247 without) was done on the 0.10 backport kagent-dev#2791. The runtime code is the same on both branches apart from the logging package (`log/slog` here, logr on 0.10). Refs kagent-dev#2728 (closed by kagent-dev#2791 for the 0.10 line; on `main` the field returns with the v1alpha3 API). --------- Signed-off-by: Timo Derstappen <[email protected]>
alexdarbyshire
pushed a commit
to alexdarbyshire/kagent
that referenced
this pull request
Oct 5, 2026
## Problem Since 0.10.0 the Agent CRD defaults `spec.declarative.runtime` to `go`, and the Go runtime never read `spec.declarative.context.compaction`. The controller compiles the field into the agent's `config.json` (`context_config.compaction`) and the Python runtime consumes it, but a Go agent grew its prompt with every turn regardless of what the operator configured; the only signal was the `UnsupportedFeatures` condition (kagent-dev#2728). Measured on an internal installation running 0.10.1: an agent session grew from 4k to 190k input tokens per model call over 119 calls, and every Go agent there ran without compaction because the runtime ignored the field. `google.golang.org/adk/v2` v2.3.0 (2026-08-31) ships native compaction (`session/compaction`: sliding window, tail retention, LLM summarizer) and the `runner.Config.Compaction` hook, so the runtime can consume it instead of carrying its own port of the algorithm, which is what kagent-dev#1971 and kagent-dev#2025 attempted before the primitives existed. ## Change - **Dependency:** `google.golang.org/adk/v2` v2.2.0 → v2.3.0. The OpenTelemetry exporters and contrib instrumentation move to the matching v1.45.0 / v0.21.0 / v0.70.0 set, which the new ADK version requires for `otel/log`. otel/log v0.21.0 hands log bodies as attribute.Value; the telemetry test on main follows that in the same commit. No other kagent code needed changes for the bump. - **Runtime:** `agent.CompactionConfig` translates `context_config.compaction` into a `compaction.Config` and `runner.CreateRunnerConfig` hands it to the ADK runner. `compaction_interval` / `overlap_size` drive the sliding window, `token_threshold` / `event_retention_size` tail retention; either strategy is off when its settings are unset, so an agent without `context_config` runs exactly as before. - **Half-configured settings** are ignored with a warning rather than rejected: `token_threshold` without `event_retention_size` (and the reverse), `overlap_size` without an interval. That mirrors the Python runtime, where adk-python runs tail retention only when both settings are present. What remains is validated at startup with the runner's own rules, so a configuration the runner would refuse (negative values, no strategy at all) fails fast with the reason instead of on the first turn. - **Summarizer:** `summarizer_model` builds a `compaction.LLMSummarizer` through the same model factory as the agent (`CreateLLM`), with the model's generation config and a 60 s bound per call (the same bound the runner applies to its default summarizer). A `prompt_template` gets the tool-name instruction the Python runtime appends (kagent-dev#1907), so summaries keep namespaced tool names; a template without a summarizer model runs on the agent's own model. With neither, the runner summarizes with the agent's model and ADK's default prompt, matching the Python runtime. - **Observability:** startup logs `context compaction enabled` with the resolved values and the summarizer model, or `context compaction not configured`, so an operator can see from the agent log what the runtime does with the field. - **API:** `main` had no field for compaction in the v1alpha3 API (the 0.10 line has `spec.declarative.context`), so nothing produced `context_config`. `spec.kagent.compaction` on the `Harness` adds it, next to `spec.kagent.memory`: `compactionInterval` / `overlapSize` for the sliding window, `tokenThreshold` / `eventRetentionSize` for tail retention, and an optional `summarizer` with a `modelConfigRef` (a `ModelConfig` in the Harness namespace) and a `promptTemplate`. It is runtime policy rather than portable agent behaviour (specific to the ADK runner; the Claude and Codex harnesses have nothing to apply it to), which is why it lives on the Harness and not on the `AgentTemplate`. The CRD rejects at admission what the runtime would refuse at startup: no strategy, `tokenThreshold` without `eventRetentionSize` (and the reverse), `overlapSize` without `compactionInterval`, a `promptTemplate` without `{conversation_history}`. - **Controller:** the kagent compiler translates the Harness's compaction policy into `context_config` in `config.json` of every root agent the Harness runs. A summarizer `ModelConfig` other than the agent's own is resolved from the Harness namespace like the memory model, so its credentials and egress join the revision and a change to it compiles a new revision; the agent's own model stays implicit because the runtime already summarizes with it by default. Compaction records travel on `EventActions.Compaction`; the local DurableDir session store persists `Actions` as a whole (ADK's `session/database`), so a summary survives an actor restart and the same range is not summarized again. The 0.10 line, where the field already exists on the Agent CRD and the reconciler flagged it as unsupported, is kagent-dev#2791. ## Testing - `go test ./adk/pkg/agent/`: `TestCompactionStrategies` (table: each strategy alone and combined, unset settings, the three half-configured cases, no strategy, negative values), `TestCompactionConfig` (not configured → nil; default summarizer; dedicated summarizer model; custom prompt on the agent model; prompt without `{conversation_history}` rejected; prompt without any model rejected; the `context_config` block as the controller compiles it), `TestCompactionSlidingWindowFiresAfterInterval` (a scripted model drives the ADK runner: no record after turn 1, one record after turn 2 whose content is the summarizer's reply, and turn 3's request shows the summary and none of the compacted turns), `TestCompactionTailRetentionFiresOverTokenThreshold`. - `go test ./core/internal/translator/...`: `TestApplyCompaction` (not configured → no `context_config`; all four settings and a prompt on the agent model → mapped with no `summarizer_model`; a dedicated summarizer `ModelConfig` → its model in `config.json`, its credential in the environment, its host in the egress, itself in the provenance models; a missing summarizer `ModelConfig` → error). - `go test ./api/v1alpha3/` (envtest): the five new CEL cases reject each half configuration with its message, and the fully configured Harness is accepted. - `make -C go test`: all packages pass. - `make -C go lint`: 0 issues. ## End-to-end `TestAgentInstanceContextCompaction` (in the `test-e2e` job) runs the whole chain with `mockllm` as the summarizer: a clone of the suite's `kagent` Harness with `compactionInterval: 2` and a dedicated summarizer `ModelConfig`, admitting a plain `AgentTemplate`; both models point at one mock LLM behind a recording proxy on the test host; a default header on each `ModelConfig` tells the two models apart. Three turns through the public A2A API, then the recorded requests show what the runtime sent: the summarizer model was called exactly once, after the second turn, with both turns in the transcript and the custom prompt; the second turn's request still carried the first turn raw; the third turn's request carries the summary and none of the four compacted messages. Run before pushing against a local kind cluster with Substrate 0.0.26 and the controller and Go ADK images built from this branch: passes in 4.9 s. The real-model run (kind, images built from the branch, Anthropic `claude-haiku-4-5`, a five-turn tool-using conversation: turn 5 needs 2,524 prompt tokens with compaction against 50,247 without) was done on the 0.10 backport kagent-dev#2791. The runtime code is the same on both branches apart from the logging package (`log/slog` here, logr on 0.10). Refs kagent-dev#2728 (closed by kagent-dev#2791 for the 0.10 line; on `main` the field returns with the v1alpha3 API). --------- Signed-off-by: Timo Derstappen <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Implement conversation-history compaction for the Go ADK runtime, bringing it to functional parity with the Python runtime's
spec.declarative.context.compactionfeature.The Go runtime previously silently dropped the
contextConfig.compactionfield and raisedUnsupportedFeaturesonruntime: godeclarative agents. This PR fixes that.Approach
The compaction algorithm is ported from adk-python and runs entirely within the kagent
adk/pkg/compactionpackage — no fork ofgoogle.golang.org/adkis required.Because
google.golang.org/adkv1.4.0 has no compaction event types, compaction markers are encoded in existingEventfields:Author = "compaction"InvocationID = "compaction_<startNano>_<endNano>"Contentholds the LLM-generated summaryA
compactingServicewrapper (session_wrapper.go) interceptsGetto replace the raw marker events with synthetic summary events before the runner sees them. The underlying storage keeps the markers intact soMaybeCompactcan parse the watermark on subsequent invocations.Algorithm
Mirrors adk-python's invocation-based sliding window:
InvocationID; parse the watermark from the most recent compaction marker'sInvocationID≥ compactionIntervalmax(0, firstNewIdx - overlapSize)(overlap reaches back into already-compacted invocations)Supports both sliding-window and token-threshold modes. All model adapters (Anthropic, OpenAI, Bedrock, Ollama) are reused for summarization via the existing
CreateLLMpath.Files changed
go/adk/pkg/compaction/compaction.goBuildCompactedEventList, watermark parsing,filterCompactionMarkers,MaybeCompactgo/adk/pkg/compaction/session_wrapper.gocompactingService+compactedSessionwrapper; applies compacted view onGet, unwraps onAppendEventgo/adk/pkg/compaction/compaction_test.gomakeCompactionMarkerEventusing the new encodinggo/adk/pkg/runner/adapter.goNewCompactingServicewhen compaction is configuredgo/adk/pkg/session/local_session.goReplaceEvents(no callers)go/core/internal/controller/reconciler/reconciler.goUnsupportedFeaturesguard forcontext.compactiononruntime: gogo/go.mod/go/go.sumgoogle.golang.org/adkto v1.4.0; no replace directiveCloses
#1173