Skip to content

feat(adk): Go runtime conversation-history compaction - #2025

Closed
QuentinBisson wants to merge 2 commits into
kagent-dev:mainfrom
QuentinBisson:feat/go-adk-compaction
Closed

QuentinBisson wants to merge 2 commits into
kagent-dev:mainfrom
QuentinBisson:feat/go-adk-compaction

Conversation

@QuentinBisson

@QuentinBisson QuentinBisson commented Jun 15, 2026 •

Copy link
Copy Markdown
Contributor

What

Implement conversation-history compaction for the Go ADK runtime, bringing it to functional parity with the Python runtime's spec.declarative.context.compaction feature.

The Go runtime previously silently dropped the contextConfig.compaction field and raised UnsupportedFeatures on runtime: go declarative agents. This PR fixes that.

Approach

The compaction algorithm is ported from adk-python and runs entirely within the kagent adk/pkg/compaction package — no fork of google.golang.org/adk is required.

Because google.golang.org/adk v1.4.0 has no compaction event types, compaction markers are encoded in existing Event fields:

  • Author = "compaction"
  • InvocationID = "compaction_<startNano>_<endNano>"
  • Content holds the LLM-generated summary

A compactingService wrapper (session_wrapper.go) intercepts Get to replace the raw marker events with synthetic summary events before the runner sees them. The underlying storage keeps the markers intact so MaybeCompact can parse the watermark on subsequent invocations.

Algorithm

Mirrors adk-python's invocation-based sliding window:

  1. Group events by InvocationID; parse the watermark from the most recent compaction marker's InvocationID
  2. Count invocations whose latest timestamp is after the watermark; fire when ≥ compactionInterval
  3. Window start = max(0, firstNewIdx - overlapSize) (overlap reaches back into already-compacted invocations)
  4. Trim to the longest self-contained prefix — never compacts through an open function call or unresolved HITL confirmation

Supports both sliding-window and token-threshold modes. All model adapters (Anthropic, OpenAI, Bedrock, Ollama) are reused for summarization via the existing CreateLLM path.

Files changed

File Change
go/adk/pkg/compaction/compaction.go Rewritten: InvocationID-based marker encoding, BuildCompactedEventList, watermark parsing, filterCompactionMarkers, MaybeCompact
go/adk/pkg/compaction/session_wrapper.go New: compactingService + compactedSession wrapper; applies compacted view on Get, unwraps on AppendEvent
go/adk/pkg/compaction/compaction_test.go Updated: removed fork-type helpers, added makeCompactionMarkerEvent using the new encoding
go/adk/pkg/runner/adapter.go Wraps session service with NewCompactingService when compaction is configured
go/adk/pkg/session/local_session.go Remove ReplaceEvents (no callers)
go/core/internal/controller/reconciler/reconciler.go Remove UnsupportedFeatures guard for context.compaction on runtime: go
go/go.mod / go/go.sum Upgrade google.golang.org/adk to v1.4.0; no replace directive

Closes

#1173

@github-actions github-actions Bot added the enhancement New feature or request label Jun 15, 2026
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]>
…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]>
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jun 15, 2026
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

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.

@github-actions github-actions Bot added the stale This issue or PR has become stale label Jul 1, 2026
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

This pull request has been closed after 15 days of inactivity and an additional 5 days after being marked as stale.

@github-actions github-actions Bot closed this Jul 6, 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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request stale This issue or PR has become stale

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant