Orphaned function_call permanently and silently disables context compaction (once triggered)
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 68/100
Línea de trabajo
Start in src/google/adk/apps/compaction.py, especially _longest_self_contained_prefix and the two event-selection paths that call it. Run the focused tests in tests/unittests/apps/test_compaction.py and the linked deterministic reproduction before changing behavior. Done means orphaned calls no longer permanently block later compaction, while pending or cross-window calls retain the existing conservative behavior and all listed tests pass.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
🔴 Required Information
Describe the Bug:
Once the first context compaction happens in a session, if there is an
orphaned function_call (a tool call whose function_response never
arrives — e.g. the process handling the tool died, such as a Kubernetes pod
restart while the agent was waiting on a tool), every subsequent
compaction is silently and permanently blocked, even as the prompt keeps
growing past the configured token_threshold. There is no log and no error.
Root cause: google.adk.apps.compaction._longest_self_contained_prefix()
walks the compaction window keeping a set of open obligations keyed by call
id (function_call opens an id, the matching function_response closes
it), and only records a cut point where that set is completely empty. Once
the first compaction has happened, an orphaned call becomes the first
candidate event of every subsequent round, so the set can never empty again
— safe_length freezes at 0 forever, and _events_to_compact_for_token_threshold
(and the sliding-window path, which shares the same helper) returns [].
This affects both compaction strategies since both call the same helper.
Steps to Reproduce:
- Install:
pip install google-adk==2.9.2 - Persist a session (e.g. via
DatabaseSessionService) that already had a
successful 1st compaction, followed by one orphanedfunction_call
(no matchingfunction_responseanywhere in the session), followed by
several normal, balanced turns. - Reload the session and call the internal
google.adk.apps.compaction._events_to_compact_for_token_threshold(...)
(or exercise the sliding-window path) on it. - Observe the returned list is empty — no compaction — even though there
are dozens of eligible events after the orphan.
A minimal, deterministic, runnable reproduction (no LLM call / API key
needed — the bug lives entirely in the pure event-selection logic) is here:
https://github.com/erickleal-azos/simulate-adk-fc-orphan
git clone https://github.com/erickleal-azos/simulate-adk-fc-orphan
cd simulate-adk-fc-orphan
poetry install
poetry run python repro_orphan_function_call.py
Expected Behavior:
An orphaned function_call from an already-finished invocation, with no
response anywhere in the session and not a long-running/pending
auth/tool-confirmation call, should eventually be treated as unanswerable
so that compaction can resume over the healthy events that follow it
(optionally with a warning log, since today the stall is silent).
Observed Behavior:
_events_to_compact_for_token_threshold (and the sliding-window variant)
returns [] forever after the orphan, so the session's prompt never
compacts again and grows without bound. Example from the linked repro:
Scenario A -- NO orphaned function_call (baseline)
Baseline: events_to_compact = 36 event(s)
-> 2nd compaction would proceed normally.
Scenario B -- orphaned function_call right after the 1st compaction
Buggy: events_to_compact = 0 event(s)
-> BLOCKED: no 2nd compaction, even with 42 eligible events in the session.
Scenario B (healed) -- removing only the orphaned call event
Healed: events_to_compact = 36 event(s)
-> 2nd compaction would proceed normally.
Environment Details:
- ADK Library Version (pip show google-adk): 2.9.2 (verified byte-identical on
main, so also present on latest) - Desktop OS: Linux (also observed in a GKE/Kubernetes)
- Python Version (python -V): 3.11
Model Information:
- Are you using LiteLLM: No
- Which model is being used: N/A — the bug is in pure event-selection logic (
google.adk.apps.compaction), no LLM call is involved in triggering or reproducing it.
🟡 Optional Information
Regression:
Not a regression from a specific prior version as far as I can tell — the
guard this interacts with was added in PR #5832 (fixing #5602, "Compaction
breaks LongRunningFunctionTool resume") and is deliberate/tested for the
first stall. What wasn't accounted for is that the stall is unbounded:
once the window's start point is pinned at last_compacted_end_timestamp,
an orphaned call there blocks that window from ever balancing again.
Logs:
None — this is precisely the problem: the block is completely silent, no
log or error is emitted, which made the original incident hard to diagnose.
Additional Context:
This was found in a real staging incident: a Kubernetes pod running the
tool-execution side was restarted while an agent was waiting on a tool call.
The session came back with a function_call that would never be answered,
and from that point on it never compacted again.
I have a proposed fix with a 7-scenario validation matrix (including the
two existing upstream tests that encode the conservative window,
test_sliding_window_excludes_pending_function_call_events and
test_sliding_window_plain_orphaned_function_call_dropped_from_contents,
replayed to confirm no regression) here:
https://github.com/erickleal-azos/simulate-adk-fc-orphan#the-proposed-solution
Summary of the approach: keep the strict rule exactly as-is (if it yields a
non-empty prefix, return it untouched — behavior for the normal/healthy
case does not change). Only when it yields nothing (the stall) do we ask a
second question — is each open obligation still fulfillable? An id keeps
blocking if: (1) a matching function_response exists anywhere in the
session (not just the window, to avoid splitting a pair across the window
boundary when the response lives past the window's end — this is what the
original guard protects), (2) it is in Event.long_running_tool_ids or a
pending auth/tool-confirmation request, or (3) it was opened by the newest
invocation in the session (may still be in flight). Everything else is a
normal tool call from an already-finished invocation with no response
anywhere — provably dead, since a normal tool always emits a response event
(including on error), so its absence means the process died holding the
obligation. Event.long_running_tool_ids is persisted in its own DB column
and survives the kind of restart that causes this, which the fix relies on.
I'm open to opening a PR against src/google/adk/apps/compaction.py with
this fix, if the maintainers agree the approach makes sense — just let me
know and I'll follow up with tests in tests/unittests/apps/test_compaction.py.
Minimal Reproduction Code:
See https://github.com/erickleal-azos/simulate-adk-fc-orphan/blob/main/repro_orphan_function_call.py
for the full runnable script. Core of the buggy helper it exercises
(google/adk/apps/compaction.py):
open_ids: set[str] = set()
safe_length = 0
for index, event in enumerate(events):
open_ids -= _event_function_response_ids(event)
open_ids |= _event_function_call_ids(event)
if event.actions:
open_ids |= set(event.actions.requested_tool_confirmations)
open_ids |= set(event.actions.requested_auth_configs)
if not open_ids:
safe_length = index + 1
return events[:safe_length]
How often has this issue occurred?:
- Rare (<1%) — requires the process handling a tool call to die or be
killed mid-call (pod restart, OOM kill, crash) after at least one
compaction has already happened in the session. In practice an
uncommon window, but when it does line up the block is permanent and
silent.
- Lenguaje dominante
- Python
- Estrellas
- 21.6k
- Forks
- 4k
- Merge medio
- 13 h 49 min
- PR fusionados (30 d)
- 10
Guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de google/adk-python
-
mcp
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
google/adk-python#7217 · 3 comentarios · 1 asignado ·
-
tools
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
google/adk-python#7206 · 1 comentario · 1 asignado ·
-
request clarification tools
Dificultad 2/5 1-3 horas Aptitud para principiantes 86/100
google/adk-python#7205 · 2 comentarios · 1 asignado ·
-
mcp
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
google/adk-python#7196 · 1 comentario · 1 asignado ·
-
eval request clarification
Dificultad 1/5 1-3 horas Aptitud para principiantes 86/100
google/adk-python#7146 · 2 comentarios · 1 asignado ·
Todos los issues de google/adk-python
Issues similares
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
stephrobert/dsoxlab#238 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
sublimehq/package_control#1780 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
nwg-piotr/nwg-displays#145 ·