From fb50537fd2d83cc97cafad047a49b7d2f4179475 Mon Sep 17 00:00:00 2001 From: westerberg Date: Mon, 31 Aug 2026 10:32:35 +0000 Subject: [PATCH] test(llmagent): drive context compaction against a recorded real model Adds the end-to-end test and the worked example. Every other test in the stack uses a fake summarizer, which is right for pinning behaviour but says nothing about whether a real model, handed a real transcript through the real prompt-assembly path, produces something usable. This one runs the full chain against recorded traffic: turns accumulate, the threshold trips, a summary is written, and the next prompt carries the summary in place of the turns it covers while the raw tail stays intact. The assertions are load-bearing rather than structural. A compaction that deletes its covered events without substituting anything fails here, and so does one whose summary never reaches the prompt. The example arms the sliding window and carries the tail-retention pair commented out, with what to delete in order to swap. Arming both is what the documentation warns against: the sliding window consumes the events tail retention would summarize, so the ceiling never applies. --- agent/llmagent/llmagent_compaction_test.go | 616 ++++++++++++++++++ .../testdata/TestCompactionE2E.httprr | 447 +++++++++++++ examples/compaction/main.go | 126 ++++ 3 files changed, 1189 insertions(+) create mode 100644 agent/llmagent/llmagent_compaction_test.go create mode 100644 agent/llmagent/testdata/TestCompactionE2E.httprr create mode 100644 examples/compaction/main.go diff --git a/agent/llmagent/llmagent_compaction_test.go b/agent/llmagent/llmagent_compaction_test.go new file mode 100644 index 000000000..26472f96a --- /dev/null +++ b/agent/llmagent/llmagent_compaction_test.go @@ -0,0 +1,616 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package llmagent_test + +import ( + "context" + "os" + "path/filepath" + "strings" + "sync" + "testing" + + "google.golang.org/genai" + + "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/agent/llmagent" + "google.golang.org/adk/v2/internal/compactioninternal" + "google.golang.org/adk/v2/internal/httprr" + "google.golang.org/adk/v2/internal/testutil" + "google.golang.org/adk/v2/internal/utils" + "google.golang.org/adk/v2/model" + "google.golang.org/adk/v2/session" + "google.golang.org/adk/v2/session/compaction" + "google.golang.org/adk/v2/tool" + "google.golang.org/adk/v2/tool/functiontool" +) + +// minCassetteBytes is the floor below which a cassette cannot hold a recorded +// conversation. The header alone is a few dozen bytes and a real recording here +// is tens of kilobytes, so anything under this is a stub from a failed record. +const minCassetteBytes = 1024 + +// TestCompactionE2E drives a real model through enough turns to trigger a +// sliding-window compaction, then checks that the next prompt carries the +// summary in place of the turns it covers, and is still accepted by the model. +// +// Not that the prompt is smaller. On a three-turn recording it is not: at the +// compaction boundary this one goes from 183 characters to 870, because a +// summary of several short turns is longer than the turns. Compaction pays off +// against real history, and this fixture is not that, so there is no size +// assertion here. +// +// What a passing run establishes, stated narrowly because replay keys on exact +// request bytes and it is easy to read more into a green test than it holds: +// +// - A real model accepted a compacted prompt, at the time the cassette was +// recorded. Not since. +// - A summary and live tool traffic coexisted in one accepted prompt. +// - Offline, over the prompts the agent actually sent: the summary replaced +// the turns it covers, the covered turns are still in the session, and no +// prompt carries a function response without its call. +// +// What it does not establish is that anything still works against a live API. A +// structural defect introduced later changes the prompt bytes, so it arrives as +// a cassette miss rather than an API rejection. The offline assertions run +// before that miss is reported, so the failure says which property broke rather +// than only that the bytes moved. +// +// The fourth turn calls a tool after the compaction point on purpose, so the +// prompt it produces carries a summary and function traffic together. Without +// it the recording never exercises call pairing across a summary, which is the +// one thing this test is best placed to check. +// +// Deliberately not asserted: any particular wording for the summary. It is model +// output, and pinning it would fail on any model or prompt revision without +// indicating a real problem. What is asserted is that whatever the summarizer +// produced is what reaches the next prompt. +// +// Recording: this test needs a cassette. With credentials available, run +// +// GOOGLE_API_KEY=... go test ./agent/llmagent/ \ +// -run '^TestCompactionE2E$' -httprecord='TestCompactionE2E\.httprr$' -count=1 -v +// +// Note the two regexes differ on purpose. -run matches test names, so it is +// anchored. -httprecord matches the cassette FILE PATH, so anchoring it the same +// way would never match "testdata/TestCompactionE2E.httprr", so nothing would +// be recorded. +// +// Commit the resulting testdata/TestCompactionE2E.httprr. The cassette is +// committed, so a missing one is a lost or renamed file and fails the test +// rather than skipping it. +// +// The directive below claims exactly this one cassette, and nothing else in the +// package claims it. That partitioning is enforced by +// TestHTTPRecordDirectivesPartitionCassettes: every cassette must be re-recordable +// by exactly one directive, so neither a stray "go generate ./..." nor a broad +// pattern can quietly rewrite a neighbour's recording along with this one. +// +// This used to carry no directive at all, on the grounds that the package-level +// one matched every cassette here. It no longer does -- that directive is now +// scoped to the FunctionTool, LLMAgent and ToolCallback cassettes -- so this one +// was left claimed by nothing and could not be re-recorded by go generate. +// +// Note that a failed recording still leaves a cassette behind, and it can look +// plausibly sized because the failing exchange is recorded too. Delete it, or +// the next run replays the recorded failure. +// +// The cassette is sensitive to anything that changes prompt bytes, including the +// summarizer prompt template, the transcript line format, tool-argument +// rendering and truncation behaviour. Any of those changes requires re-recording. +//go:generate go test -httprecord=^testdata[/\\]TestCompactionE2E\.httprr$ + +func TestCompactionE2E(t *testing.T) { + // Matches llmagent_delegation_test.go, which is the most recently recorded + // suite in this package. Change it only alongside a re-record, since the + // model name is part of the request URL the cassette keys on. + + // The cassette is committed, so its absence is a lost or renamed file rather + // than an unrecorded checkout. Skipping would turn that into a silent pass, + // which is how a test quietly stops running for months. Every other cassette + // test in this package fails instead, and so does this one. Recording mode + // goes ahead regardless, since that is the run that creates the file. + trace := filepath.Join("testdata", t.Name()+".httprr") + if recording, _ := httprr.Recording(trace); !recording { + const reRecord = "Re-record with: GOOGLE_API_KEY=... go test ./agent/llmagent/ " + + "-run '^TestCompactionE2E$' -httprecord='TestCompactionE2E\\.httprr$' -count=1 -v" + info, err := os.Stat(trace) + if err != nil { + t.Fatalf("no cassette at %s: %v. It is committed, so this means it was lost or renamed. %s", trace, err, reRecord) + } + // A failed or interrupted re-record leaves the header and nothing else. + // Accepting it meant dying eighty lines later on a replay miss, with a + // message that never mentioned the cassette. + if info.Size() < minCassetteBytes { + t.Fatalf("the cassette at %s is %d bytes, too small to hold a conversation. "+ + "A re-record that failed partway leaves a header-only stub. %s", trace, info.Size(), reRecord) + } + } + + // Captured before each model call, so the assertions can look at the exact + // history the agent sent rather than inferring it from the session. + var ( + mu sync.Mutex + prompts [][]*genai.Content + capture = func(_ agent.Context, req *model.LLMRequest) (*model.LLMResponse, error) { + mu.Lock() + defer mu.Unlock() + prompts = append(prompts, req.Contents) + return nil, nil + } + ) + + // A tool gives the transcript function calls and responses to render, which + // is the part of the summarizer prompt most likely to break. + type cityArgs struct { + City string `json:"city" jsonschema:"the city to look up"` + } + type weatherResult struct { + Weather string `json:"weather"` + } + weather, err := functiontool.New[cityArgs, weatherResult]( + functiontool.Config{Name: "get_weather", Description: "Returns the weather in a city."}, + func(_ agent.Context, args cityArgs) (weatherResult, error) { + return weatherResult{Weather: "sunny in " + args.City}, nil + }, + ) + if err != nil { + t.Fatalf("functiontool.New() error = %v", err) + } + + a, err := llmagent.New(llmagent.Config{ + Name: "compaction_agent", + Description: "agent used to exercise context compaction", + Model: compactionModel(t), + Instruction: "You are a concise assistant. Answer in one short sentence.", + Tools: []tool.Tool{weather}, + BeforeModelCallbacks: []llmagent.BeforeModelCallback{capture}, + DisallowTransferToParent: true, + DisallowTransferToPeers: true, + }) + if err != nil { + t.Fatalf("llmagent.New() error = %v", err) + } + + // Interval 2 keeps the recording short: three turns produce exactly one + // compaction, which is all this test needs. + // + // No OverlapSize. It would be inert here and claiming otherwise was wrong: + // overlap only reaches back past an earlier compaction, and the first + // compaction of a session starts at the first invocation whatever the + // overlap is. Exercising the seam needs a second window, so it belongs in + // the offline tests where windows are cheap. + // An explicit summarizer with no timeout, because a deadline on the + // summarization call travels to the wire as an X-Server-Timeout header and + // so becomes part of what the recording has to match. The runner installs + // one with a timeout by default, which is right in production and would + // make every cassette holding a summarizer call depend on that number. + summarizer, err := compaction.NewLLMSummarizer(compaction.LLMSummarizerConfig{ + Model: compactionModel(t), + }) + if err != nil { + t.Fatalf("NewLLMSummarizer() error = %v", err) + } + r := testutil.NewTestAgentRunnerWithCompaction(t, a, &compaction.Config{ + CompactionInterval: 2, + Summarizer: summarizer, + }) + + const sessionID = "compaction_session" + turns := []string{ + "What is the weather in Zurich?", + "My favourite colour is teal, remember that.", + "What was my favourite colour again?", + // A tool call after the compaction point, so the prompt that follows it + // carries a function call and its response alongside a summary. That is + // the arrangement the call-pairing and call-recovery paths exist for, + // and without this turn the recording never produces one. + "Now check the weather in Oslo.", + } + answers := make([][]string, len(turns)) + var runErr error + failedTurn := -1 + for i, turn := range turns { + answer, err := testutil.CollectTextParts(r.Run(t, sessionID, turn)) + if err != nil { + runErr, failedTurn = err, i + break + } + answers[i] = answer + } + + // The offline checks run on whatever was captured, before any run failure + // is reported. A compaction defect changes the bytes of the prompt, so it + // reaches this test as a replay miss, and failing on that first put every + // assertion in this file behind an error that says only "cached HTTP + // response not found". The checks below are what say which defect it was. + // + // This one is weaker than it looks: the framework's own pairing guard + // rejects an orphaned response before a prompt is ever sent, so it catches + // a defect only in the window where the prompt is assembled but not yet + // validated. Running it is still the difference between a diagnosis and a + // byte mismatch. + mu.Lock() + captured := append([][]*genai.Content(nil), prompts...) + mu.Unlock() + for i, p := range captured { + assertNoOrphanFunctionResponses(t, i, p) + } + + if runErr != nil { + t.Fatalf("turn %d (%q) failed: %v", failedTurn+1, turns[failedTurn], runErr) + } + + // A compaction must have landed. Without this the rest proves nothing. + events := sessionEventsFor(t, r, sessionID) + summaries := make([]*session.Event, 0, 1) + for _, ev := range events { + if compactioninternal.HasUsableSummary(ev) { + summaries = append(summaries, ev) + } + } + if len(summaries) == 0 { + t.Fatalf("no compaction event after %d turns, so this test exercised nothing", len(turns)) + } + + // The first summary, not the last. With four turns the last compaction is + // written after the final model call, so it cannot appear in any recorded + // prompt; the first one covers the opening turns and is what the later + // prompts stand on. + summaryText := textOf(summaries[0].Actions.Compaction.CompactedContent) + if strings.TrimSpace(summaryText) == "" { + t.Error("the stored summary is empty") + } + + // The final prompt is the interesting one: it is the first assembled after a + // summary existed. It must carry the summary instead of the turns it covers, + // and the model must have accepted it, which the absence of an error above + // already establishes. + mu.Lock() + defer mu.Unlock() + if len(prompts) < len(turns) { + t.Fatalf("captured %d prompts, want at least %d", len(prompts), len(turns)) + } + final := promptTextOf(prompts[len(prompts)-1]) + + if !strings.Contains(final, strings.TrimSpace(summaryText)) { + t.Errorf("final prompt does not contain the stored summary.\nsummary:\n%s\n\nprompt:\n%s", summaryText, final) + } + if strings.Contains(final, turns[0]) { + t.Errorf("final prompt still contains the compacted first turn %q:\n%s", turns[0], final) + } + if !strings.Contains(final, turns[len(turns)-1]) { + t.Errorf("final prompt is missing the current turn %q:\n%s", turns[len(turns)-1], final) + } + // Turn 2 is inside the compacted range as well, so it must be gone for the + // same reason turn 1 is. Asserting only turn 1 left half the range unchecked. + if strings.Contains(final, turns[1]) { + t.Errorf("final prompt still contains the compacted second turn %q:\n%s", turns[1], final) + } + + // No assertion on prompt size here, deliberately. + // + // The doc comment used to promise one and there was none. Adding one showed + // why: at the real compaction boundary this recording goes from 183 + // characters to 870, and that is correct rather than a defect, because a + // summary of several short turns is longer than the turns. An assertion + // placed there fails on working code. Placed anywhere else it compares two + // prompts on the same side of the boundary and cannot fail at all, which is + // worse: the version that lived here did not fire against the recording + // that carried a 3,464-byte thought signature, because promptTextOf counts + // text and a signature is bytes. + // + // What the size of a compacted prompt should be is a property of real + // history, not of a three-turn fixture. The offline tests above assert what + // this recording can actually establish: the summary is in the prompt and + // the turns it covers are not. + + // Compacting rather than truncating, asserted against the store rather than + // inferred from the prompt. Nothing here distinguished "absent from the + // prompt" from "absent from the session" before: a compaction patched to + // drop the covered events outright took the session from 14 events to 2, + // lost every raw turn, and this test still passed. + // + // The session is the audit record. A summary standing in for turns inside a + // prompt is the feature, and those same turns disappearing from storage is + // data loss. + if len(events) < len(turns) { + t.Errorf("the session holds %d events for %d turns, so history was deleted rather than compacted", + len(events), len(turns)) + } + for _, want := range turns { + if !sessionHoldsText(events, want) { + t.Errorf("turn %q is no longer in the session: compaction must leave history intact", want) + } + } + + // The point of compacting rather than truncating: the fact survives into the + // summary and the model can still answer from it. This is turn 3 + // specifically, the one that asks, rather than whichever turn happens to be + // last. Without it the test proved the prompt shrank, not that it kept + // working. + recall := strings.ToLower(strings.Join(answers[2], " ")) + if !strings.Contains(recall, "teal") { + t.Errorf("the model could not recall the colour from the summary alone; answer was %q", recall) + } + + // Structural checks, asserted here rather than inferred from the fact that a + // recorded model once accepted the bytes. Every prompt is checked, not only + // the final one. + // The summary must actually stand for the range it claims. Every event the + // first compaction covers has to be absent from the final prompt: the two + // turns asserted above are the visible part of that, but the range is the + // contract, so it is checked directly. + covered := summaries[0].Actions.Compaction + + // Searched with the summary removed. The summary is in the prompt on + // purpose and it paraphrases the turns it covers, so any overlap of wording + // reads as a covered turn surviving. The recorded summary says "The current + // weather in Zurich is sunny." against a covered "The weather in Zurich is + // currently sunny.", which is one word's ordering away from failing this + // test for no reason on the next re-record. + outsideSummary := strings.ReplaceAll(final, strings.TrimSpace(summaryText), "") + + // hasCompaction, not HasUsableSummary: the latter answers "is there a + // usable summary here", which its own doc says is a different question from + // "is this bookkeeping". Filtering on it left the compacted tool traffic + // unexamined, which is exactly the pair the range is most likely to break. + checked := 0 + for _, ev := range events { + if ev.Actions.Compaction != nil || + ev.Timestamp.Before(covered.StartTimestamp) || ev.Timestamp.After(covered.EndTimestamp) { + continue + } + content := utils.Content(ev) + if content == nil { + // A covered event with no content used to take the package binary + // down with a nil dereference here. + continue + } + for _, part := range content.Parts { + if part == nil { + continue + } + if text := strings.TrimSpace(part.Text); text != "" { + checked++ + if strings.Contains(outsideSummary, text) { + t.Errorf("event %q is covered by the summary but its text is still in the final prompt: %q", ev.ID, part.Text) + } + } + // The tool traffic, matched by call ID rather than by text. Two of + // the six covered events are a call and its response, and neither + // carries text, so a text-only sweep never looked at them. + if fc := part.FunctionCall; fc != nil && fc.ID != "" { + checked++ + if promptMentionsCallID(prompts[len(prompts)-1], fc.ID) { + t.Errorf("event %q is covered but its function call %q is still in the final prompt", ev.ID, fc.ID) + } + } + if fr := part.FunctionResponse; fr != nil && fr.ID != "" { + checked++ + if promptMentionsCallID(prompts[len(prompts)-1], fr.ID) { + t.Errorf("event %q is covered but its function response %q is still in the final prompt", ev.ID, fr.ID) + } + } + } + } + // Without a floor this loop goes quiet rather than failing: blanking the + // covered events' parts left it making zero comparisons and the test still + // passed. Six events are covered in the recording and four of them carry + // something to compare, so anything below that means the sweep stopped + // looking rather than stopped finding. + if checked < 4 { + t.Errorf("the covered-range sweep made %d comparisons, want at least 4: it is not examining what it claims to", checked) + } + + withSummaryAndTools := 0 + for _, p := range prompts { + if promptHasFunctionTraffic(p) && strings.Contains(promptTextOf(p), strings.TrimSpace(summaryText)) { + withSummaryAndTools++ + } + } + // At least one prompt must carry a summary and function traffic together. + // That is the arrangement call pairing has to survive, and the only one this + // test is better placed to check than an offline test is. A re-record whose + // conversation stops calling tools after the compaction point would lose it + // silently, so it is asserted rather than assumed. + if withSummaryAndTools == 0 { + t.Error("no recorded prompt carries both a summary and function traffic, so pairing across a summary was never exercised") + } +} + +// promptHasFunctionTraffic reports whether contents carry a function call or +// response. +func promptHasFunctionTraffic(contents []*genai.Content) bool { + for _, c := range contents { + if c == nil { + continue + } + for _, part := range c.Parts { + if part != nil && (part.FunctionCall != nil || part.FunctionResponse != nil) { + return true + } + } + } + return false +} + +// assertNoOrphanFunctionResponses checks that every function response in a +// prompt is preceded by the call it answers. +// +// This is the property compaction is most likely to break: the summary replaces +// a span of history, and a response whose call fell inside that span while the +// response itself did not would reach the model unpaired, which real backends +// reject. +func assertNoOrphanFunctionResponses(t *testing.T, promptIdx int, contents []*genai.Content) { + t.Helper() + + seenCalls := make(map[string]bool) + responses := 0 + for _, c := range contents { + if c == nil { + continue + } + for _, part := range c.Parts { + if part == nil { + continue + } + if fc := part.FunctionCall; fc != nil { + seenCalls[callKey(fc.ID, fc.Name)] = true + } + if fr := part.FunctionResponse; fr != nil { + responses++ + if !seenCalls[callKey(fr.ID, fr.Name)] { + t.Errorf("prompt %d carries a function response %q with no preceding call", promptIdx, fr.Name) + } + } + } + } + if responses == 0 { + // Not a failure. It is the documented gap: this recording has no tool + // traffic after the compaction point, so for the final prompt there is + // nothing here to check. + t.Logf("prompt %d carries no function responses, so pairing was not exercised in it", promptIdx) + } +} + +// callKey identifies a call by ID when the model supplies one, and by name +// otherwise, which is what happens with models that omit call IDs. +func callKey(id, name string) string { + if id != "" { + return "id:" + id + } + return "name:" + name +} + +func sessionEventsFor(t *testing.T, r *testutil.TestAgentRunner, sessionID string) []*session.Event { + t.Helper() + resp, err := r.SessionService().Get(context.Background(), &session.GetRequest{ + AppName: "test_app", UserID: "test_user", SessionID: sessionID, + }) + if err != nil { + t.Fatalf("session Get() error = %v", err) + } + var events []*session.Event + for ev := range resp.Session.Events().All() { + events = append(events, ev) + } + return events +} + +func textOf(c *genai.Content) string { + if c == nil { + return "" + } + var b strings.Builder + for _, p := range c.Parts { + if p != nil && p.Text != "" { + b.WriteString(p.Text) + } + } + return b.String() +} + +func promptTextOf(contents []*genai.Content) string { + var b strings.Builder + for _, c := range contents { + if c == nil { + continue + } + for _, p := range c.Parts { + switch { + case p == nil: + case p.Text != "": + b.WriteString("[" + c.Role + "] " + p.Text + "\n") + case p.FunctionCall != nil: + b.WriteString("[" + c.Role + "] CALL " + p.FunctionCall.Name + "\n") + case p.FunctionResponse != nil: + b.WriteString("[" + c.Role + "] RESPONSE " + p.FunctionResponse.Name + "\n") + } + } + } + return b.String() +} + +// sessionHoldsText reports whether any stored event still carries want. +// +// Read against the session rather than the prompt, so it answers "is the +// history still there" rather than "was it shown to the model", which is the +// distinction between compacting and truncating. +func sessionHoldsText(events []*session.Event, want string) bool { + for _, ev := range events { + content := utils.Content(ev) + if content == nil { + continue + } + for _, part := range content.Parts { + if part != nil && strings.Contains(part.Text, want) { + return true + } + } + } + return false +} + +// promptMentionsCallID reports whether any part of the prompt carries a +// function call or response with the given ID. +func promptMentionsCallID(contents []*genai.Content, id string) bool { + for _, c := range contents { + if c == nil { + continue + } + for _, part := range c.Parts { + if part == nil { + continue + } + if fc := part.FunctionCall; fc != nil && fc.ID == id { + return true + } + if fr := part.FunctionResponse; fr != nil && fr.ID == id { + return true + } + } + } + return false +} + +// compactionModelName is the model this end-to-end test records against. +const compactionModelName = "gemini-3.5-flash" + +// compactionModel returns the one Gemini model this test uses, for both the +// agent and the summarizer. +// +// It must be one instance. newGeminiModel opens a recorder per call, and in +// record mode that is an os.Create on a path derived from the test name: +// truncating, with its own write offset. Two of them on one trace overwrite +// each other, and the result stops parsing after one record while still +// clearing the size floor this file checks, so the documented re-record command +// produced a cassette that looked fine and was not. Replay is unaffected, which +// is why every assertion passed. The delegation tests memoise for exactly this +// reason. +func compactionModel(t *testing.T) model.LLM { + t.Helper() + if m, ok := compactionModels.Load(t.Name()); ok { + return m.(model.LLM) + } + m := newGeminiModel(t, compactionModelName, nil) + compactionModels.Store(t.Name(), m) + t.Cleanup(func() { compactionModels.Delete(t.Name()) }) + return m +} + +// compactionModels caches one model instance per test name, cleared in +// t.Cleanup so a -count=N run does not reuse a stale one. +var compactionModels sync.Map diff --git a/agent/llmagent/testdata/TestCompactionE2E.httprr b/agent/llmagent/testdata/TestCompactionE2E.httprr new file mode 100644 index 000000000..986dcc99d --- /dev/null +++ b/agent/llmagent/testdata/TestCompactionE2E.httprr @@ -0,0 +1,447 @@ +httprr trace v1 +1000 2106 +POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent HTTP/1.1 +Host: generativelanguage.googleapis.com +User-Agent: Go-http-client/1.1 +Content-Length: 768 +Content-Type: application/json + +{"contents":[{"parts":[{"text":"What is the weather in Zurich?"}],"role":"user"}],"generationConfig":{},"systemInstruction":{"parts":[{"text":"You are a concise assistant. Answer in one short sentence.\n\nYou are an agent. Your internal name is \"compaction_agent\". The description about you is \"agent used to exercise context compaction\"."}],"role":"user"},"tools":[{"functionDeclarations":[{"description":"Returns the weather in a city.","name":"get_weather","parametersJsonSchema":{"additionalProperties":false,"properties":{"city":{"description":"the city to look up","type":"string"}},"required":["city"],"type":"object"},"responseJsonSchema":{"additionalProperties":false,"properties":{"weather":{"type":"string"}},"required":["weather"],"type":"object"}}]}]}HTTP/2.0 200 OK +Content-Type: application/json; charset=UTF-8 +Date: Mon, 24 Aug 2026 12:39:26 GMT +Server: scaffolding on HTTPServer2 +Server-Timing: gfet4t7; dur=1162 +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gemini-Service-Tier: standard +X-Xss-Protection: 0 + +{ + "candidates": [ + { + "content": { + "parts": [ + { + "functionCall": { + "name": "get_weather", + "args": { + "city": "Zurich" + }, + "id": "call_156800" + }, + "thoughtSignature": "Et4ECtsEARFNMg+mULUQ83jwmVgWmMDjjtDc0pk+4l6IA60xz+5EkWz5WTJQQZibk4C6F2LZB/GXuSjrXxX2GBMtrgTjsHqcTIk2ENcmHQ4Q5YCwUNQR/yX2eFPXJlKLZml7wmfa1jSyd2u6D7qQqyjZrvenktqN9HCLL2m7PTBR1Z7WJVbKnbSTry2tNUwvL7P36ddw7YjrAdO5PTtnaqidMB4c0vBOPPP6zX2s6WB3jJN/PN3lqqekizo/twpL5/a5mTNIHymCpjpMwJeNx1o640W5GOyyq6dpc57I9fYrsm0ofSvPJwoPXiAVd4bPmRwabrjfE5GJi59RMxhe+raMSOs2IIaJh9JC09eheNq3dPeXrdbRFCs9srTUMfM1uHId/lVlo0uVzDZUFBhtAO129Iv0lcUH11rxO+1/NUJk0JNn6v+qU/x3OLG60rnuEMUn1NAnhlMzRlaegVLQo/rS2yc/QNBWcBsCHPzKcyHkqNWWFRIwvy/+lucvH/5nn84fUH3tfZPj1xVV+CT//frl9lJ8Tz1RxikldUqkpsMmHERIOnWOVTS8w01F8kBDAKM9Kwiw6wmb9oH97l0kEngueYgmEOX4oJ9nYPDn5ArbZfdN+IhlInx701e3sjriQqONSfhPOmlJawaWV2hsSFn8F9eRLQ238hOnpN+zKzjudJtRtJiOTMZxgxBPLdqjPDiS7WCbV0KsnNcD+Zk+DbjHsk3qkVnxb/ZRxTK+JVOENoXi1OikQZ1KnilfNW3es3dnFehLqrJJZ3tmXZKkMsPRcPfiq7Vb49tmO4xCSk9F" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "finishMessage": "Model generated function call(s)." + } + ], + "usageMetadata": { + "promptTokenCount": 128, + "candidatesTokenCount": 17, + "totalTokenCount": 258, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 128 + } + ], + "thoughtsTokenCount": 113, + "serviceTier": "standard", + "rawPromptTokenCount": 167 + }, + "modelVersion": "gemini-3.5-flash", + "responseId": "fDuMarnNOv_Z7M8Pre6rsAk", + "turnToken": "v1_ChdmRHVNYXJuTk92X1o3TThQcmU2cnNBaxIXZkR1TWFybk5Pdl9aN004UHJlNnJzQWs" +} +2076 1445 +POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent HTTP/1.1 +Host: generativelanguage.googleapis.com +User-Agent: Go-http-client/1.1 +Content-Length: 1843 +Content-Type: application/json + +{"contents":[{"parts":[{"text":"What is the weather in Zurich?"}],"role":"user"},{"parts":[{"functionCall":{"args":{"city":"Zurich"},"id":"call_156800","name":"get_weather"},"thoughtSignature":"Et4ECtsEARFNMg+mULUQ83jwmVgWmMDjjtDc0pk+4l6IA60xz+5EkWz5WTJQQZibk4C6F2LZB/GXuSjrXxX2GBMtrgTjsHqcTIk2ENcmHQ4Q5YCwUNQR/yX2eFPXJlKLZml7wmfa1jSyd2u6D7qQqyjZrvenktqN9HCLL2m7PTBR1Z7WJVbKnbSTry2tNUwvL7P36ddw7YjrAdO5PTtnaqidMB4c0vBOPPP6zX2s6WB3jJN/PN3lqqekizo/twpL5/a5mTNIHymCpjpMwJeNx1o640W5GOyyq6dpc57I9fYrsm0ofSvPJwoPXiAVd4bPmRwabrjfE5GJi59RMxhe+raMSOs2IIaJh9JC09eheNq3dPeXrdbRFCs9srTUMfM1uHId/lVlo0uVzDZUFBhtAO129Iv0lcUH11rxO+1/NUJk0JNn6v+qU/x3OLG60rnuEMUn1NAnhlMzRlaegVLQo/rS2yc/QNBWcBsCHPzKcyHkqNWWFRIwvy/+lucvH/5nn84fUH3tfZPj1xVV+CT//frl9lJ8Tz1RxikldUqkpsMmHERIOnWOVTS8w01F8kBDAKM9Kwiw6wmb9oH97l0kEngueYgmEOX4oJ9nYPDn5ArbZfdN+IhlInx701e3sjriQqONSfhPOmlJawaWV2hsSFn8F9eRLQ238hOnpN+zKzjudJtRtJiOTMZxgxBPLdqjPDiS7WCbV0KsnNcD+Zk+DbjHsk3qkVnxb/ZRxTK+JVOENoXi1OikQZ1KnilfNW3es3dnFehLqrJJZ3tmXZKkMsPRcPfiq7Vb49tmO4xCSk9F"}],"role":"model"},{"parts":[{"functionResponse":{"id":"call_156800","name":"get_weather","response":{"weather":"sunny in Zurich"}}}],"role":"user"}],"generationConfig":{},"systemInstruction":{"parts":[{"text":"You are a concise assistant. Answer in one short sentence.\n\nYou are an agent. Your internal name is \"compaction_agent\". The description about you is \"agent used to exercise context compaction\"."}],"role":"user"},"tools":[{"functionDeclarations":[{"description":"Returns the weather in a city.","name":"get_weather","parametersJsonSchema":{"additionalProperties":false,"properties":{"city":{"description":"the city to look up","type":"string"}},"required":["city"],"type":"object"},"responseJsonSchema":{"additionalProperties":false,"properties":{"weather":{"type":"string"}},"required":["weather"],"type":"object"}}]}]}HTTP/2.0 200 OK +Content-Type: application/json; charset=UTF-8 +Date: Mon, 24 Aug 2026 12:39:26 GMT +Server: scaffolding on HTTPServer2 +Server-Timing: gfet4t7; dur=923 +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gemini-Service-Tier: standard +X-Xss-Protection: 0 + +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "It is currently sunny in Zurich.", + "thoughtSignature": "EoQCCoECARFNMg9M5SkpUzCBMw6zh0wpziEm7+0asa2MjpZZ9MBkBq2WXreN29gInxvaCfb/zD9EjK6hntcc7PSoGjJkwWbOed5DTtfOshzYNOIPC+fzkyd0AczSgOVol/iYrb/ckGQL9Ckfho4ZevYKMONVIg79L0SDQCn0d0nALlexs5ag1jo1nS2b+rTor3q7QH9srJ1XyAbmGwOcFf65UN+YSLr9QM/haVEa/KqSbICoD7stiyIT6XZXKFey7bVNhputrPUZ3U8Gmy/DSW/fzypdO52snzIKMEG38XIOsHn13zn/ofGpBbTx5fUKtv0I3N3ozeNu7GaMp2E+AzPMK4/jpXg=" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 274, + "candidatesTokenCount": 7, + "totalTokenCount": 313, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 274 + } + ], + "thoughtsTokenCount": 32, + "serviceTier": "standard", + "rawPromptTokenCount": 323 + }, + "modelVersion": "gemini-3.5-flash", + "responseId": "fjuMapTrBMuznsEP--DtUA", + "turnToken": "v1_ChZmanVNYXBUckJNdXpuc0VQLS1EdFVBEhZmanVNYXBUckJNdXpuc0VQLS1EdFVB" +} +2602 1499 +POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent HTTP/1.1 +Host: generativelanguage.googleapis.com +User-Agent: Go-http-client/1.1 +Content-Length: 2369 +Content-Type: application/json + +{"contents":[{"parts":[{"text":"What is the weather in Zurich?"}],"role":"user"},{"parts":[{"functionCall":{"args":{"city":"Zurich"},"id":"call_156800","name":"get_weather"},"thoughtSignature":"Et4ECtsEARFNMg+mULUQ83jwmVgWmMDjjtDc0pk+4l6IA60xz+5EkWz5WTJQQZibk4C6F2LZB/GXuSjrXxX2GBMtrgTjsHqcTIk2ENcmHQ4Q5YCwUNQR/yX2eFPXJlKLZml7wmfa1jSyd2u6D7qQqyjZrvenktqN9HCLL2m7PTBR1Z7WJVbKnbSTry2tNUwvL7P36ddw7YjrAdO5PTtnaqidMB4c0vBOPPP6zX2s6WB3jJN/PN3lqqekizo/twpL5/a5mTNIHymCpjpMwJeNx1o640W5GOyyq6dpc57I9fYrsm0ofSvPJwoPXiAVd4bPmRwabrjfE5GJi59RMxhe+raMSOs2IIaJh9JC09eheNq3dPeXrdbRFCs9srTUMfM1uHId/lVlo0uVzDZUFBhtAO129Iv0lcUH11rxO+1/NUJk0JNn6v+qU/x3OLG60rnuEMUn1NAnhlMzRlaegVLQo/rS2yc/QNBWcBsCHPzKcyHkqNWWFRIwvy/+lucvH/5nn84fUH3tfZPj1xVV+CT//frl9lJ8Tz1RxikldUqkpsMmHERIOnWOVTS8w01F8kBDAKM9Kwiw6wmb9oH97l0kEngueYgmEOX4oJ9nYPDn5ArbZfdN+IhlInx701e3sjriQqONSfhPOmlJawaWV2hsSFn8F9eRLQ238hOnpN+zKzjudJtRtJiOTMZxgxBPLdqjPDiS7WCbV0KsnNcD+Zk+DbjHsk3qkVnxb/ZRxTK+JVOENoXi1OikQZ1KnilfNW3es3dnFehLqrJJZ3tmXZKkMsPRcPfiq7Vb49tmO4xCSk9F"}],"role":"model"},{"parts":[{"functionResponse":{"id":"call_156800","name":"get_weather","response":{"weather":"sunny in Zurich"}}}],"role":"user"},{"parts":[{"text":"It is currently sunny in Zurich.","thoughtSignature":"EoQCCoECARFNMg9M5SkpUzCBMw6zh0wpziEm7+0asa2MjpZZ9MBkBq2WXreN29gInxvaCfb/zD9EjK6hntcc7PSoGjJkwWbOed5DTtfOshzYNOIPC+fzkyd0AczSgOVol/iYrb/ckGQL9Ckfho4ZevYKMONVIg79L0SDQCn0d0nALlexs5ag1jo1nS2b+rTor3q7QH9srJ1XyAbmGwOcFf65UN+YSLr9QM/haVEa/KqSbICoD7stiyIT6XZXKFey7bVNhputrPUZ3U8Gmy/DSW/fzypdO52snzIKMEG38XIOsHn13zn/ofGpBbTx5fUKtv0I3N3ozeNu7GaMp2E+AzPMK4/jpXg="}],"role":"model"},{"parts":[{"text":"My favourite colour is teal, remember that."}],"role":"user"}],"generationConfig":{},"systemInstruction":{"parts":[{"text":"You are a concise assistant. Answer in one short sentence.\n\nYou are an agent. Your internal name is \"compaction_agent\". The description about you is \"agent used to exercise context compaction\"."}],"role":"user"},"tools":[{"functionDeclarations":[{"description":"Returns the weather in a city.","name":"get_weather","parametersJsonSchema":{"additionalProperties":false,"properties":{"city":{"description":"the city to look up","type":"string"}},"required":["city"],"type":"object"},"responseJsonSchema":{"additionalProperties":false,"properties":{"weather":{"type":"string"}},"required":["weather"],"type":"object"}}]}]}HTTP/2.0 200 OK +Content-Type: application/json; charset=UTF-8 +Date: Mon, 24 Aug 2026 12:39:27 GMT +Server: scaffolding on HTTPServer2 +Server-Timing: gfet4t7; dur=930 +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gemini-Service-Tier: standard +X-Xss-Protection: 0 + +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "I will remember that your favorite color is teal.", + "thoughtSignature": "Ep4CCpsCARFNMg9NJLpJc1VEmEpt/7sMnGngE+Ap4rbp/999jJBbfSLpunruxzZ9WCdUJc00NhzMRbUkwRjcWxsUDv6jCrRDJZH7R/x4x+gyriRy8SltzCb5pdgQ7OTJrOSkAcBeMJOZpsUTi5Hnak1t24JEQv0wMNl2GhN5A3bZb0z/RMWvP6zxvwMaASwdNT+43ZGVg1l1mguKLxDenO16SyxzDnPMLdh20delpVBl/OMy3hRgyviA5H5LruKISOEf2NBQC2PFnfm3BWQ1oDHowJCRGvtYZ8H88g/77Kqwn4r6RDUlQMNcDZIVGtT2Oj9UVDbzHPqJQDcA5ToPl4C59QdzhtEKT333VcXRAme4jxLFFAEd3WGPGUNCIep0zw==" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 324, + "candidatesTokenCount": 10, + "totalTokenCount": 369, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 324 + } + ], + "thoughtsTokenCount": 35, + "serviceTier": "standard", + "rawPromptTokenCount": 385 + }, + "modelVersion": "gemini-3.5-flash", + "responseId": "fzuMapoL9a6ewQ-Krd_hBw", + "turnToken": "v1_ChZmenVNYXBvTDlhNmV3US1LcmRfaEJ3EhZmenVNYXBvTDlhNmV3US1LcmRfaEJ3" +} +1295 4974 +POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent HTTP/1.1 +Host: generativelanguage.googleapis.com +User-Agent: Go-http-client/1.1 +Content-Length: 1062 +Content-Type: application/json + +{"contents":[{"parts":[{"text":"The following is a conversation history between a user and an AI agent. It may or may not start from a compacted history. Please identify and reiterate the user request, summarize the context so far, focusing on key decisions made and information obtained, as well as any unresolved questions or tasks. CRITICAL INSTRUCTIONS: 1. Explicitly identify and state the primary language used by the user at the top of your summary (e.g., \"Conversation Language: English\"). 2. If the agent called any tools, accurately list the exact tool names used to maintain tool grounding. The rest of the summary should be concise and capture the essence of the interaction.\n\nuser: What is the weather in Zurich?\ncompaction_agent called tool: get_weather({city: Zurich})\nTool response from get_weather: {weather: sunny in Zurich}\ncompaction_agent: It is currently sunny in Zurich.\nuser: My favourite colour is teal, remember that.\ncompaction_agent: I will remember that your favorite color is teal."}],"role":"user"}],"generationConfig":{}}HTTP/2.0 200 OK +Content-Type: application/json; charset=UTF-8 +Date: Mon, 24 Aug 2026 12:39:31 GMT +Server: scaffolding on HTTPServer2 +Server-Timing: gfet4t7; dur=3175 +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gemini-Service-Tier: standard +X-Xss-Protection: 0 + +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "**Conversation Language:** English\n\n### User Request\nThe user's most recent request was for the agent to remember that their favorite color is teal. (Prior to this, the user asked for the current weather in Zurich).\n\n### Context Summary\n* **Information Obtained & Decisions Made:**\n * The user asked about the weather in Zurich. \n * The agent called the tool `get_weather` with the argument `city: Zurich` to retrieve the current conditions.\n * The tool reported that the weather is sunny, which the agent communicated to the user.\n * The user shared a personal preference (favorite color is teal) for future reference, which the agent acknowledged and committed to remembering.\n* **Tools Used:** \n * `get_weather`\n\n### Unresolved Questions or Tasks\n* There are currently no unresolved questions or pending tasks.", + "thoughtSignature": "Ev0RCvoRARFNMg8dYzB6LJgxEdMHoeE2jf9EpQOppI85iOoqVMSbTqdrQoUj56o1t/hcPBQ6O0A9MQcfAXq/c/X7f3vuXPdasBXXEaab0S9YJWyot4/U7tq7SjQOJ7kmX2dOov5nnK9jgHxqeJSvThGk5dk0qlRzrU0aMWotBnDxPi8VylGTAfb+Dr9OFvZtLiFKE9WgvHW5cImFLJKIqZz6oCUIH+n21CIsVLscrqgjyYjrIJudX+nSsr2ayPgOr9ryOG7QhXz0cjKlqFkPkF+qGOHckXochX5ivLYJhFm/qmjSdA3oB87WuHKya2dIKeRhqazZZdz85kfogzW1OKfMsBBFpA7BVr/Aq354F7gFLZwsN061+cKzFCdGnyzZGvnjx3berTN9p0zV03XpodR3+MhCZyt1JQRe3/mTsrbeebytEde/ANAzogMDehjQybqEVCQ06MYpxtDcME9nIvE2e3mp0CIk93Z6vEUFGd6/jPM8Q+PD1Hb/icyWj+9pZEK1XbXlvKm0oxdYK5sjxCGGWzyhnkpm1gaaTRlGBwpAyiatL0GLrvUN7cWI9FiB/QVOus3XdyaBQpvZXnXTUV5cMARPz3byP+jG/wxxxvHCjNJvX9XalWD2gT3DHo+UEUaghvGP+5MWuezouSYE9gFUakADnWTD9IqJYu+l9IF6qdJIU7ZaIAMUK+3aEZZz3iouQ5r/stszOvfjo1hOtXWmiDzGQ5qHP7wsZavPvzfWZgABJ9/3T6vXYDqm/RLdzk2UcWmTPGDdVGPuURHijCBwAhR/AbUYpy+YRlTuFOfK73SUuNzzx7r5Q5O9tlRx4kYmzKhMeUnPuCAP9LSKouPifhkBSDGhpi4LGJ1oU860qeBdtblWH8dDilb7MmHv6/nbRUSjrX7ICfHNp1XWAt0vyM92IFXBvRDo1/IIkyqqK/h9jwpQeLdCbcRNZrL4DcXy7iYRt4+kx1pulZa/q79v5wwQFt0nbsTaZSpLrS61C6s1GVCBJ3El1da1PDXMQ6J+NubTe+oZ2YQLAROPPaw4yWW20QR97rAUB1+WxX0XNxA6h3goZGrfO+EPbYkBya9lvH4YGym0cdPdBnbUvz4dzgr97ReaDovlNVaQ+x0uRiAYIyACwlorbdFpTaBdjVWIKIHV2rh6sH0xTR7/5ViQNIny2Vb6gtL28o7ia+X1pftI0+mGgE5BYYAGNk9+XmQOtqBiC/us5ezzg9LQ4gaN/T8BxykC+zS76YTEaCDTd214SQcvORft+ru6MM2uQf1r6n37PsCwxL0Hl2qZxhCR79SrjMCXzjRpP+haafrmvcIoorqeTd5h/ClYcwdZZ9i3peBVeUDOqmfVqAIQTUjAhVKvhfSRXHcv6y1/4OSIElAaCal1CLe2wTNAvrLpBW12mQxPzi3C70d2R6tCwPcRywWbKdrVYN6qAfWuRZ+libR8riD05xEMKEENsVR9MwaX5exsVbrTAXthPF9gVNQUR7sTARSvB4eePJk37A/h+4xuxAryehCJh2e6G/0Zqz32kOw/NalwQdvAzsCrW3u5BtEE5jQQahNosiMF0bp+iQrmvH/I1SvmRNToG+/Tl/81o6Kzz0kEQKx9Fg/4luPD1mB+vd3SdMeG33JrKC1AkFL2EJnBAFG9baeRKWZyr5ssstnd83212bN12Cfsw3QwwJTK+DtmRSHOj4on0PkLyAWutaZdofUlY6JH1bhjkQy+7dDIe1fb/BPjxrZ92X/LnZtNBDD6OeOw+FJ4T6HoxFSzOBkkyt2B4vMvT8D1gz9OSokeFc04uYOLx7SdDJ4iXuFAunQXkvvLfEMj/pi/kfrcFB4xco9Wtz+DLPDL79STP3oE4frEtB1IUjej8Xlz+c4m2uJqKNXg2iKvhYMOhzqiGvvC0cVpyCMSuZWHOqcPeignNJcWwYqactBv1rOuUhGOyNm8izKl4cPICQb1FHoAf5kh6WjX0j/BLAlA2nLEzDzZbWbQhjTR+4RM0yDnnn50dOPKp67SrFWZcnGHYjkjsFfpG3FOQzGQCZQI/48MRvxR9Sz2gIS5yfxrTW6KfBgBMNCk57Z9rwdLl9eL62nAz9mppQPWv9g7pFA7Q5to4W4BHzgX7kdwRaxc0Aw0K6XP5ltN+DOHNHmnPVI05IqPrbVMxPSVluNTlCA3Bjk3o3ZlJbYfN52JMSn0u9fOa1loU9DINTzM1zk6PTZhoirX7W2wZhFL3vUCeTbGPJnBBAIHC6al8ojsAJs7bLVkr3jZMCFYrwSotV4aZKmXDkEdzv6g4hSc90kuy41oKAxGecxiYMxiwUBVBiOvAdyTLz6DxbuuU73z3PG7h0Wiq1eZIz8tZL+G4G2oMJ+sPOpFHLxZrB7rbGNYHdVpK38Os7F7zf8GB5qJKlHjm895QWL1xzvYzc3zAMORGW0VzO/JYjDNRYD9tq1bJNJ7gTZJ9B5jWtJ2M7vAG7w/ulGrBm5PdMDsaJX/OGYNSVrszEeDekaCPMK9rmUANRdPpeHBzLKkNJgvul4gRA8GomeiT/NDDGgBSbvvSsULzznil/iEVn9QG2IABW2fraC533L6uQcfM9WJX1Jv6tbccBieVkjE+IMtjw0fC79LXyLPS9uXQeyVzERjijlvG2LzzrfMcJhfb8P3stKnkth0p4EtFsD9HPcC1PZ9TDt/3tlG4uFNZyfqJZys2bZRjnJ8ajY/QpN6ddqkJ7rmU4MbzcUBo+5wxo9miyp1JUoXP4zZDLPY3mSUbUGI1bLp2E74fFqELnfjxPPnGCslmRhTYeKIbHAUMjAmVUloQ5CVStGuvEnM2BpOsanZrwZ1pg+Vc+eV76I+Gfd+OwSkFsNiR5595mm4Brnl30Us3DskPTRTFfQKQA5v3GsDAJcDElRJ9nct1CIa8XQiaxqvlX9m9qNkrvgyjVFFyqJhGraT3qM4wfGXsdHS/b1Nj/WZ1K4UzloQRW1YqMmDhzqc4fjCGBNty3u3Jm7+odhXByZxzheo+P9YVSagmjpgJljZRv4gFDOW17/MQhD/3KJFT1eNvqK6e+fajv+MwqFmYbtzsnA5" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 215, + "candidatesTokenCount": 180, + "totalTokenCount": 922, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 215 + } + ], + "thoughtsTokenCount": 527, + "serviceTier": "standard", + "rawPromptTokenCount": 246 + }, + "modelVersion": "gemini-3.5-flash", + "responseId": "fzuMaqnDOeDwnsEPysWK-Qk", + "turnToken": "v1_ChdmenVNYXFuRE9lRHduc0VQeXNXSy1RaxIXZnp1TWFxbkRPZUR3bnNFUHlzV0stUWs" +} +1883 1959 +POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent HTTP/1.1 +Host: generativelanguage.googleapis.com +User-Agent: Go-http-client/1.1 +Content-Length: 1650 +Content-Type: application/json + +{"contents":[{"parts":[{"text":"**Conversation Language:** English\n\n### User Request\nThe user's most recent request was for the agent to remember that their favorite color is teal. (Prior to this, the user asked for the current weather in Zurich).\n\n### Context Summary\n* **Information Obtained \u0026 Decisions Made:**\n * The user asked about the weather in Zurich. \n * The agent called the tool `get_weather` with the argument `city: Zurich` to retrieve the current conditions.\n * The tool reported that the weather is sunny, which the agent communicated to the user.\n * The user shared a personal preference (favorite color is teal) for future reference, which the agent acknowledged and committed to remembering.\n* **Tools Used:** \n * `get_weather`\n\n### Unresolved Questions or Tasks\n* There are currently no unresolved questions or pending tasks."}],"role":"model"},{"parts":[{"text":"What was my favourite colour again?"}],"role":"user"}],"generationConfig":{},"systemInstruction":{"parts":[{"text":"You are a concise assistant. Answer in one short sentence.\n\nYou are an agent. Your internal name is \"compaction_agent\". The description about you is \"agent used to exercise context compaction\"."}],"role":"user"},"tools":[{"functionDeclarations":[{"description":"Returns the weather in a city.","name":"get_weather","parametersJsonSchema":{"additionalProperties":false,"properties":{"city":{"description":"the city to look up","type":"string"}},"required":["city"],"type":"object"},"responseJsonSchema":{"additionalProperties":false,"properties":{"weather":{"type":"string"}},"required":["weather"],"type":"object"}}]}]}HTTP/2.0 200 OK +Content-Type: application/json; charset=UTF-8 +Date: Mon, 24 Aug 2026 12:39:32 GMT +Server: scaffolding on HTTPServer2 +Server-Timing: gfet4t7; dur=1236 +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gemini-Service-Tier: standard +X-Xss-Protection: 0 + +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "Your favorite color is teal.", + "thoughtSignature": "EoQFCoEFARFNMg8waSifAc1tCO0PJI1q2mZdiqtdHOuG5ltqeQjy+jLf/LcRnL5SiyKMyr2dqMA2EW4ZQhCKR1IyrVwHb7GQ0NxFtFTTCeSdS7fsApTMTz3YAPpF0qYfqV2KAi7I2ZPZ7LYowe9wybVYyAdv2MciQ1UuugIkgNQO+Ya6035NDyZAJ2D4cIJxY7LWJ1Tvj/J4Fgrm/DgDqbFYycyp1RAhITDAUl93gzcLnYTPELA+D8NY+Uu9ZeA1MnUV8nrHjPxj3KhvkiVNzEV+lW4ZG/L2IMW7T+UxBNoOCbYt1myZi0JxCzr1dkkKO5B/hS4tZLIIAi0lwrxpkTINn7Rq+xBI/sqLUPZPiX1KqbSlmjhSxJ0bEZiCEx47HJ8SOFmnWex8l9Y+Y5FtayLn6pe+7kaCsUkpnMrOB1hhEZhc1H1x4ayDIKdgcBeig5BZNBWUHJM/cpH54afY57SHfQLtuF0FnL5WfoZpuoPlykaa6fdCtXxwKdp81Ev59dSjvHcIOmEivzwrZZu1aYwLO1LCaoKCoQLW+bxYAXkc63oSEs0MV1/v9ydcw1/FImG5OqVulAb/1JVzxXIdmE+lBWE0jXKiMDUbb6NUZ0Js5xecSPAqEuTXCgHi8yp1YcMk/APzdJVHQ8SEeXqiSnULSnNuGTNtbkYmQh84U5yPlu9FhqcouXlcFtneXO4/jvSwz/lUg0Gzht1t0VbtVwP2N5GLuCJ5AXunf8ycB7Y+lzxs2FFEp54LLkn/2Ek7sG593JsYHphPG5YwGSEx9IwDuOq6zCmMd9g2NoYzx8d5KaZbPFKrUVR+gdBg+Y+CpjIYgfdm89H6kZzlNnxj2MjGeNUhabc=" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 309, + "candidatesTokenCount": 6, + "totalTokenCount": 433, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 309 + } + ], + "thoughtsTokenCount": 118, + "serviceTier": "standard", + "rawPromptTokenCount": 352 + }, + "modelVersion": "gemini-3.5-flash", + "responseId": "gzuMaq2tB5iunsEPnOnYsQw", + "turnToken": "v1_ChdnenVNYXEydEI1aXVuc0VQbk9uWXNRdxIXZ3p1TWFxMnRCNWl1bnNFUG5PbllzUXc" +} +2904 1919 +POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent HTTP/1.1 +Host: generativelanguage.googleapis.com +User-Agent: Go-http-client/1.1 +Content-Length: 2671 +Content-Type: application/json + +{"contents":[{"parts":[{"text":"**Conversation Language:** English\n\n### User Request\nThe user's most recent request was for the agent to remember that their favorite color is teal. (Prior to this, the user asked for the current weather in Zurich).\n\n### Context Summary\n* **Information Obtained \u0026 Decisions Made:**\n * The user asked about the weather in Zurich. \n * The agent called the tool `get_weather` with the argument `city: Zurich` to retrieve the current conditions.\n * The tool reported that the weather is sunny, which the agent communicated to the user.\n * The user shared a personal preference (favorite color is teal) for future reference, which the agent acknowledged and committed to remembering.\n* **Tools Used:** \n * `get_weather`\n\n### Unresolved Questions or Tasks\n* There are currently no unresolved questions or pending tasks."}],"role":"model"},{"parts":[{"text":"What was my favourite colour again?"}],"role":"user"},{"parts":[{"text":"Your favorite color is teal.","thoughtSignature":"EoQFCoEFARFNMg8waSifAc1tCO0PJI1q2mZdiqtdHOuG5ltqeQjy+jLf/LcRnL5SiyKMyr2dqMA2EW4ZQhCKR1IyrVwHb7GQ0NxFtFTTCeSdS7fsApTMTz3YAPpF0qYfqV2KAi7I2ZPZ7LYowe9wybVYyAdv2MciQ1UuugIkgNQO+Ya6035NDyZAJ2D4cIJxY7LWJ1Tvj/J4Fgrm/DgDqbFYycyp1RAhITDAUl93gzcLnYTPELA+D8NY+Uu9ZeA1MnUV8nrHjPxj3KhvkiVNzEV+lW4ZG/L2IMW7T+UxBNoOCbYt1myZi0JxCzr1dkkKO5B/hS4tZLIIAi0lwrxpkTINn7Rq+xBI/sqLUPZPiX1KqbSlmjhSxJ0bEZiCEx47HJ8SOFmnWex8l9Y+Y5FtayLn6pe+7kaCsUkpnMrOB1hhEZhc1H1x4ayDIKdgcBeig5BZNBWUHJM/cpH54afY57SHfQLtuF0FnL5WfoZpuoPlykaa6fdCtXxwKdp81Ev59dSjvHcIOmEivzwrZZu1aYwLO1LCaoKCoQLW+bxYAXkc63oSEs0MV1/v9ydcw1/FImG5OqVulAb/1JVzxXIdmE+lBWE0jXKiMDUbb6NUZ0Js5xecSPAqEuTXCgHi8yp1YcMk/APzdJVHQ8SEeXqiSnULSnNuGTNtbkYmQh84U5yPlu9FhqcouXlcFtneXO4/jvSwz/lUg0Gzht1t0VbtVwP2N5GLuCJ5AXunf8ycB7Y+lzxs2FFEp54LLkn/2Ek7sG593JsYHphPG5YwGSEx9IwDuOq6zCmMd9g2NoYzx8d5KaZbPFKrUVR+gdBg+Y+CpjIYgfdm89H6kZzlNnxj2MjGeNUhabc="}],"role":"model"},{"parts":[{"text":"Now check the weather in Oslo."}],"role":"user"}],"generationConfig":{},"systemInstruction":{"parts":[{"text":"You are a concise assistant. Answer in one short sentence.\n\nYou are an agent. Your internal name is \"compaction_agent\". The description about you is \"agent used to exercise context compaction\"."}],"role":"user"},"tools":[{"functionDeclarations":[{"description":"Returns the weather in a city.","name":"get_weather","parametersJsonSchema":{"additionalProperties":false,"properties":{"city":{"description":"the city to look up","type":"string"}},"required":["city"],"type":"object"},"responseJsonSchema":{"additionalProperties":false,"properties":{"weather":{"type":"string"}},"required":["weather"],"type":"object"}}]}]}HTTP/2.0 200 OK +Content-Type: application/json; charset=UTF-8 +Date: Mon, 24 Aug 2026 12:39:33 GMT +Server: scaffolding on HTTPServer2 +Server-Timing: gfet4t7; dur=990 +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gemini-Service-Tier: standard +X-Xss-Protection: 0 + +{ + "candidates": [ + { + "content": { + "parts": [ + { + "functionCall": { + "name": "get_weather", + "args": { + "city": "Oslo" + }, + "id": "call_2010978" + }, + "thoughtSignature": "EtQDCtEDARFNMg/WY+2b2BY+zTqXTVopqLpYo9dFObaQZoXW/t9BwZWCbDaoboOV45qyUx7cOKo+Bp+vqlwuwUpij/sn+3ne3y0aXR0ngvo2xdQfu7yqKzwmvwcLw+Noxn9n0Bc/46mCqHN3Tc9fdDjuYLp4dszrHLI7H/J8xvqHdg8NaQFrRkDV7QmeNbZEoNSJen0Yc2qRqKNPKgSEVnTG1QOeZEBHvoBsUM0s6AkgVKSJy5ZOqMAzNUG3hSwNWGmrT3+X8xRWxY6iKrZSo+f+jSlBE+y2FJYy5s7Io/kvLmAiyBwtAcCPZc7XO5FT1h4/xu7k875Gs5vnIBCXHuKLOOlFAOe+kVeEywXfDjlSRJqGlivD0hcvH5ZeOQ6orWZp5zDj2CUsNmBGr8+4AfduvPZsktrvJ1BycgxPI1fhP7lWo9FkfqxuL30FkGS/yZiccQ363mRIeyl+cmUd3TPHGlY1nUNKxJ5Pe9XNNKhc1xUs8R9bq4KEXbVZ1/Z9geXM/4pRjfvcMj2GbYKgDkr5Ydcc4UPsvp4BdO9ajKbqaaovn3aaDhljZvrb5ho4MJuhJOAk41C2yEKH2fJ7b087ri+0VrE7KcgNusMcPrTlHCXwSb9/" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "finishMessage": "Model generated function call(s)." + } + ], + "usageMetadata": { + "promptTokenCount": 442, + "candidatesTokenCount": 17, + "totalTokenCount": 554, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 442 + } + ], + "thoughtsTokenCount": 95, + "serviceTier": "standard", + "rawPromptTokenCount": 497 + }, + "modelVersion": "gemini-3.5-flash", + "responseId": "hDuMaqi9FcXlnsEPmOny4QU", + "turnToken": "v1_ChdoRHVNYXFpOUZjWGxuc0VQbU9ueTRRVRIXaER1TWFxaTlGY1hsbnNFUG1Pbnk0UVU" +} +3793 1608 +POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent HTTP/1.1 +Host: generativelanguage.googleapis.com +User-Agent: Go-http-client/1.1 +Content-Length: 3560 +Content-Type: application/json + +{"contents":[{"parts":[{"text":"**Conversation Language:** English\n\n### User Request\nThe user's most recent request was for the agent to remember that their favorite color is teal. (Prior to this, the user asked for the current weather in Zurich).\n\n### Context Summary\n* **Information Obtained \u0026 Decisions Made:**\n * The user asked about the weather in Zurich. \n * The agent called the tool `get_weather` with the argument `city: Zurich` to retrieve the current conditions.\n * The tool reported that the weather is sunny, which the agent communicated to the user.\n * The user shared a personal preference (favorite color is teal) for future reference, which the agent acknowledged and committed to remembering.\n* **Tools Used:** \n * `get_weather`\n\n### Unresolved Questions or Tasks\n* There are currently no unresolved questions or pending tasks."}],"role":"model"},{"parts":[{"text":"What was my favourite colour again?"}],"role":"user"},{"parts":[{"text":"Your favorite color is teal.","thoughtSignature":"EoQFCoEFARFNMg8waSifAc1tCO0PJI1q2mZdiqtdHOuG5ltqeQjy+jLf/LcRnL5SiyKMyr2dqMA2EW4ZQhCKR1IyrVwHb7GQ0NxFtFTTCeSdS7fsApTMTz3YAPpF0qYfqV2KAi7I2ZPZ7LYowe9wybVYyAdv2MciQ1UuugIkgNQO+Ya6035NDyZAJ2D4cIJxY7LWJ1Tvj/J4Fgrm/DgDqbFYycyp1RAhITDAUl93gzcLnYTPELA+D8NY+Uu9ZeA1MnUV8nrHjPxj3KhvkiVNzEV+lW4ZG/L2IMW7T+UxBNoOCbYt1myZi0JxCzr1dkkKO5B/hS4tZLIIAi0lwrxpkTINn7Rq+xBI/sqLUPZPiX1KqbSlmjhSxJ0bEZiCEx47HJ8SOFmnWex8l9Y+Y5FtayLn6pe+7kaCsUkpnMrOB1hhEZhc1H1x4ayDIKdgcBeig5BZNBWUHJM/cpH54afY57SHfQLtuF0FnL5WfoZpuoPlykaa6fdCtXxwKdp81Ev59dSjvHcIOmEivzwrZZu1aYwLO1LCaoKCoQLW+bxYAXkc63oSEs0MV1/v9ydcw1/FImG5OqVulAb/1JVzxXIdmE+lBWE0jXKiMDUbb6NUZ0Js5xecSPAqEuTXCgHi8yp1YcMk/APzdJVHQ8SEeXqiSnULSnNuGTNtbkYmQh84U5yPlu9FhqcouXlcFtneXO4/jvSwz/lUg0Gzht1t0VbtVwP2N5GLuCJ5AXunf8ycB7Y+lzxs2FFEp54LLkn/2Ek7sG593JsYHphPG5YwGSEx9IwDuOq6zCmMd9g2NoYzx8d5KaZbPFKrUVR+gdBg+Y+CpjIYgfdm89H6kZzlNnxj2MjGeNUhabc="}],"role":"model"},{"parts":[{"text":"Now check the weather in Oslo."}],"role":"user"},{"parts":[{"functionCall":{"args":{"city":"Oslo"},"id":"call_2010978","name":"get_weather"},"thoughtSignature":"EtQDCtEDARFNMg/WY+2b2BY+zTqXTVopqLpYo9dFObaQZoXW/t9BwZWCbDaoboOV45qyUx7cOKo+Bp+vqlwuwUpij/sn+3ne3y0aXR0ngvo2xdQfu7yqKzwmvwcLw+Noxn9n0Bc/46mCqHN3Tc9fdDjuYLp4dszrHLI7H/J8xvqHdg8NaQFrRkDV7QmeNbZEoNSJen0Yc2qRqKNPKgSEVnTG1QOeZEBHvoBsUM0s6AkgVKSJy5ZOqMAzNUG3hSwNWGmrT3+X8xRWxY6iKrZSo+f+jSlBE+y2FJYy5s7Io/kvLmAiyBwtAcCPZc7XO5FT1h4/xu7k875Gs5vnIBCXHuKLOOlFAOe+kVeEywXfDjlSRJqGlivD0hcvH5ZeOQ6orWZp5zDj2CUsNmBGr8+4AfduvPZsktrvJ1BycgxPI1fhP7lWo9FkfqxuL30FkGS/yZiccQ363mRIeyl+cmUd3TPHGlY1nUNKxJ5Pe9XNNKhc1xUs8R9bq4KEXbVZ1/Z9geXM/4pRjfvcMj2GbYKgDkr5Ydcc4UPsvp4BdO9ajKbqaaovn3aaDhljZvrb5ho4MJuhJOAk41C2yEKH2fJ7b087ri+0VrE7KcgNusMcPrTlHCXwSb9/"}],"role":"model"},{"parts":[{"functionResponse":{"id":"call_2010978","name":"get_weather","response":{"weather":"sunny in Oslo"}}}],"role":"user"}],"generationConfig":{},"systemInstruction":{"parts":[{"text":"You are a concise assistant. Answer in one short sentence.\n\nYou are an agent. Your internal name is \"compaction_agent\". The description about you is \"agent used to exercise context compaction\"."}],"role":"user"},"tools":[{"functionDeclarations":[{"description":"Returns the weather in a city.","name":"get_weather","parametersJsonSchema":{"additionalProperties":false,"properties":{"city":{"description":"the city to look up","type":"string"}},"required":["city"],"type":"object"},"responseJsonSchema":{"additionalProperties":false,"properties":{"weather":{"type":"string"}},"required":["weather"],"type":"object"}}]}]}HTTP/2.0 200 OK +Content-Type: application/json; charset=UTF-8 +Date: Mon, 24 Aug 2026 12:39:34 GMT +Server: scaffolding on HTTPServer2 +Server-Timing: gfet4t7; dur=923 +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gemini-Service-Tier: standard +X-Xss-Protection: 0 + +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "The weather in Oslo is currently sunny.", + "thoughtSignature": "EvUCCvICARFNMg/S48fJzW40RK6Y3a3xUezedGISYZlcUBHucZm/xegoN+S4TGyomJaN9K2Ol7w+F8FmNMa+lbuvf9nWBgZhL+h3rvp7l91T5Bkrr4l8vSfZHAjxpDnEnW3Rp+wCuxKXgAaFy4i3OB52w7tXufKy6UhCemShNoxNbJ23mGrdijek3J4DMTSreRDi1r1ZBBKrCk4d9/8T/zEo3YLMgzdZCJlYF4TDQFN4FPtGT86aDW+r1zVNmu2WRZ+fGkXAj+X2UgaaJmjuEBwzt9bz7YIkAwQBelZG3AaGAcA4PR3LoKqn3nQF8StvSfZo0bUoitc9NRXC5aHh+h0m/kc8xVB82DSiX1GvhZIJo9pdRLaa2+498WlGKVDGX09P6B8hD5HjsOf4mdKZ7u9hWcnkbwKBEQRiZtmj3zwFdvg61AVsUUoY7RAUrESVBApbY4S0zO9LBfOgLkFUCyuSj5ZxBMOK2VafIyMXRJdUj0S5Agi/VQ==" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 570, + "candidatesTokenCount": 8, + "totalTokenCount": 645, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 570 + } + ], + "thoughtsTokenCount": 67, + "serviceTier": "standard", + "rawPromptTokenCount": 635 + }, + "modelVersion": "gemini-3.5-flash", + "responseId": "hTuMapKlFb3lnsEP8L_BmQY", + "turnToken": "v1_ChdoVHVNYXBLbEZiM2xuc0VQOExfQm1RWRIXaFR1TWFwS2xGYjNsbnNFUDhMX0JtUVk" +} +1269 4721 +POST https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent HTTP/1.1 +Host: generativelanguage.googleapis.com +User-Agent: Go-http-client/1.1 +Content-Length: 1036 +Content-Type: application/json + +{"contents":[{"parts":[{"text":"The following is a conversation history between a user and an AI agent. It may or may not start from a compacted history. Please identify and reiterate the user request, summarize the context so far, focusing on key decisions made and information obtained, as well as any unresolved questions or tasks. CRITICAL INSTRUCTIONS: 1. Explicitly identify and state the primary language used by the user at the top of your summary (e.g., \"Conversation Language: English\"). 2. If the agent called any tools, accurately list the exact tool names used to maintain tool grounding. The rest of the summary should be concise and capture the essence of the interaction.\n\nuser: What was my favourite colour again?\ncompaction_agent: Your favorite color is teal.\nuser: Now check the weather in Oslo.\ncompaction_agent called tool: get_weather({city: Oslo})\nTool response from get_weather: {weather: sunny in Oslo}\ncompaction_agent: The weather in Oslo is currently sunny."}],"role":"user"}],"generationConfig":{}}HTTP/2.0 200 OK +Content-Type: application/json; charset=UTF-8 +Date: Mon, 24 Aug 2026 12:39:37 GMT +Server: scaffolding on HTTPServer2 +Server-Timing: gfet4t7; dur=2982 +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Gemini-Service-Tier: standard +X-Xss-Protection: 0 + +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "**Conversation Language:** English\n\n### User Request\nThe user asked to recall their favorite color and then requested to check the current weather in Oslo.\n\n### Context Summary\n* **Information Obtained:** \n * The user's favorite color is teal.\n * The weather in Oslo is currently sunny.\n* **Tools Used:** \n * `get_weather` was called to retrieve the weather for Oslo.\n\n### Unresolved Questions or Tasks\nThere are currently no unresolved tasks or pending questions.", + "thoughtSignature": "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" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 210, + "candidatesTokenCount": 104, + "totalTokenCount": 883, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 210 + } + ], + "thoughtsTokenCount": 569, + "serviceTier": "standard", + "rawPromptTokenCount": 241 + }, + "modelVersion": "gemini-3.5-flash", + "responseId": "hjuMarfHEOqhnsEP2tjesQg", + "turnToken": "v1_ChdoanVNYXJmSEVPcWhuc0VQMnRqZXNRZxIXaGp1TWFyZkhFT3FobnNFUDJ0amVzUWc" +} diff --git a/examples/compaction/main.go b/examples/compaction/main.go new file mode 100644 index 000000000..d17da4790 --- /dev/null +++ b/examples/compaction/main.go @@ -0,0 +1,126 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package main provides an example ADK agent with context compaction enabled. +// +// Compaction keeps an agent's prompt small as its conversation grows: older +// turns are summarized into a single event, and later prompts carry that +// summary instead of the raw turns. Two triggers are available, and this +// example arms the sliding window: +// +// - Sliding window fires after every CompactionInterval completed turns. It +// replaces each group of turns with one summary, a constant-factor +// reduction. Summaries are never re-summarized, so the prompt still grows +// with the length of the conversation. +// - Tail retention fires mid-turn once a prompt reaches TokenThreshold, and +// keeps one rolling summary plus the most recent events. This is the +// trigger that puts a ceiling on prompt size. +// +// Both can be armed at once, but not at this interval. Tail retention only +// fires when more events accumulate between sliding-window compactions than +// EventRetentionSize holds back, and an interval of 2 keeps that number tiny. +// This example uses 2 so a compaction happens within a few turns of starting +// it, which is the wrong end of that trade for tail retention. Raise the +// interval and both run; the package documentation has the measurements. +// +// Setting Compaction on [launcher.Config] enables compaction on +// every surface that reads that config. The launcher used here, full.NewLauncher, +// serves the console, the web UI, A2A, the Pub/Sub and Eventarc triggers, and +// the REST API. Agent Engine reads the same field but is served by its own +// handler rather than by this launcher. +// +// Run it and hold a conversation of several turns: +// +// GOOGLE_API_KEY=... go run ./examples/compaction console +// GOOGLE_API_KEY=... go run ./examples/compaction web webui +// +// The web command needs at least one sublauncher named after it, as above. +// Running it bare exits with "no active sublaunchers found". +// +// After every two turns a compaction event is appended to the session, and the +// turns it covers stop being sent to the model. The summary is bookkeeping +// rather than conversation, so it is not streamed back with the agent's reply; +// look for it in the session's event list, for example in the web UI. +package main + +import ( + "context" + "log" + "os" + + "google.golang.org/genai" + + "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/agent/llmagent" + "google.golang.org/adk/v2/cmd/launcher" + "google.golang.org/adk/v2/cmd/launcher/full" + "google.golang.org/adk/v2/model/gemini" + "google.golang.org/adk/v2/session/compaction" +) + +func main() { + ctx := context.Background() + + model, err := gemini.NewModel(ctx, "gemini-flash-latest", &genai.ClientConfig{ + APIKey: os.Getenv("GOOGLE_API_KEY"), + }) + if err != nil { + log.Fatalf("Failed to create model: %v", err) + } + + a, err := llmagent.New(llmagent.Config{ + Name: "assistant", + Model: model, + Description: "A general purpose assistant with a long memory.", + Instruction: "You are a helpful assistant. Keep your answers to a sentence or two.", + }) + if err != nil { + log.Fatalf("Failed to create agent: %v", err) + } + + config := &launcher.Config{ + AgentLoader: agent.NewSingleLoader(a), + + // Summarizer is left nil, so the runner summarizes with the root + // agent's own model. + Compaction: &compaction.Config{ + // Sliding window: after every 2 completed turns, summarize the + // turns since the last compaction, carrying 1 earlier turn forward + // so consecutive summaries overlap and context is not lost at the + // seam. Runs once a turn has finished. + CompactionInterval: 2, + OverlapSize: 1, + + // Tail retention, commented out on purpose. Arming it alongside + // the interval of 2 above would do nothing: the sliding window + // summarizes every group of two turns, so the events this trigger + // looks at, the ones no compaction covers yet, never reach + // EventRetentionSize and it never fires. + // + // To try it, delete the two sliding-window settings above and + // uncomment these. It runs *during* a turn, so it also catches a + // single long tool-calling turn that inflates the prompt on its + // own, and it is the trigger that bounds prompt size rather than + // just reducing it. Running both is fine at a larger interval. + // + // TokenThreshold: 32_000, + // EventRetentionSize: 10, + }, + } + + l := full.NewLauncher() + if err = l.Execute(ctx, config, os.Args[1:]); err != nil { + log.Fatalf("Run failed: %v\n\n%s", err, l.CommandLineSyntax()) + } +}