runLive passes partial events to appendEvent and never aggregates text chunks, unlike ADK Python
Les mainteneurs répondent en général sous 1 jour
Personne n'a encore pris cette issue.
Évaluation
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Accessibilité débutants
- 45/100
Piste de recherche
Start with GeminiLlmConnection.java and the live event handling in BaseLlmFlow.java, then compare the partial event handling in Runner.runAsync and Runner.runLive. Check RunnerTest and FirestoreSessionServiceTest for the described reproductions. Done means live text chunks stream as partial events while each turn's accumulated text is stored once as a non-partial event across session services.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Description
🔴 Required Information
Describe the Bug:
In live mode, the model text that a turn leaves in the session differs from ADK Python and depends on the session service. Two things combine:
runLivepasses partial events to the session service.Runner.runAsyncskips partial events beforeappendEvent(Runner.java:865-872, added in #1307), butRunner.runLivepasses every event toappendEvent(Runner.java:1462-1464).- The live path never aggregates text.
GeminiLlmConnectionturns theserverContentof each server message into oneLlmResponse(GeminiLlmConnection.java:129-191), andBaseLlmFlowmakes each response with content its own event (BaseLlmFlow.java:668-681). A response is partial only if its message carriesturnComplete: false(GeminiLlmConnection.java:185). Nothing produces a full-text event for the turn.
The session services handle partial events differently:
InMemorySessionServicedrops them. It calls the defaultappendEvent(InMemorySessionService.java:351), which returns early for partial events (BaseSessionService.java:232-235).FirestoreSessionServicewrites them like any other event. ItsappendEventhas no partial check (FirestoreSessionService.java:634-739), and the file never reads or writespartial, so a chunk comes back as an ordinary event on reload.VertexAiSessionServicecalls the defaultappendEventand then sends the event to the API anyway (VertexAiSessionService.java:320-331).partialis written to the event metadata and restored on reload (SessionJsonConverter.java:131,:527), butContentsnever reads it. I only read this code; I did not run it against Vertex AI.
So what a live turn leaves in the session depends on whether the Live API's interim messages carry turnComplete: false, which I could not confirm:
| Interim server messages | InMemorySessionService |
FirestoreSessionService, VertexAiSessionService |
|---|---|---|
(a) no turnComplete field |
one model event per chunk | one model event per chunk |
(b) turnComplete: false |
no model text | one model event per chunk |
In both cases, the message that completes the turn is also stored as an event of its own, even without content (BaseLlmFlow.java:746-762); ADK Python stores that event too. Case (b) assumes that this message carries no text; if it does, InMemorySessionService keeps only that last piece.
ADK Python stores one non-partial event with the full text either way. Its live connection marks every text chunk partial (gemini_llm_connection.py:404) and yields a separate non-partial response with the accumulated text when the turn completes or is interrupted, when a tool call arrives, or when a part that is neither text nor inline data arrives (:250-284, :407-410, :527-556, :600-627). Transcription chunks are likewise partial, followed by one non-partial transcription (:431-526). Its runner then skips partial events in live mode (runners.py:1556-1563), as the Java runAsync path does.
Stored chunks reach later requests. runLive sends the session history when it connects (BaseLlmFlow.java:587-592), and a later runAsync builds its request from the same events. Contents.java neither checks partial nor merges consecutive model events (it merges only function responses), so each stored chunk becomes its own model turn. I have not checked what the model does with that.
Steps to Reproduce:
- Check out
mainat189d463a, where I ran the tests. The code cited above is the same in 1.11.0; the links point tomain. - Add the first test below to
RunnerTest, next torunAsync_partialEvent_streamedButNotPassedToSessionService(RunnerTest.java:3469-3500), and run it. It reusesRecordingSessionService, the session service that test uses. The test model returns two text chunks and a turn-complete response, built the wayGeminiLlmConnectionbuilds them in case (a) and in case (b). - Add the second test to
FirestoreSessionServiceTestand run it. It appends one partial event using the class's mocks.
I did not run this against the Gemini Live API, Firestore or Vertex AI. TestLlm's live connection returns the responses it is given, so the first test shows what the runner and InMemorySessionService do with responses of that shape, not what the Live API sends.
Expected Behavior:
Partial chunks are streamed to the caller but not saved, and each live turn stores its full text in one non-partial event in every session service, as ADK Python does.
Observed Behavior:
The first test prints:
chunks partial = false
passed to appendEvent: ["Hel", "lo", turnComplete ""]
kept by InMemory: ["Hel", "lo", turnComplete ""]
chunks partial = true
passed to appendEvent: [partial "Hel", partial "lo", turnComplete ""]
kept by InMemory: [turnComplete ""]
The second prints the fields of the stored event document; partial is not among them:
stored fields: [appName, author, content, keywords, timestamp, userId]
Environment Details:
- ADK Library Version (see maven dependency):
mainat189d463a, where I ran the tests below (plusgoogle-adk-firestore-session-servicefor the Firestore part); the cited code is the same ingoogle-adk1.11.0 - OS: Windows 11 (not OS-specific)
- TS Version (tsc --version): N/A (Java: Microsoft OpenJDK 17.0.19)
Model Information:
- Which model is being used: N/A (no model was called; the affected path is
GeminiLlmConnection, which Gemini Live API models use)
🟡 Optional Information
Regression:
No. The live path has never aggregated text. In 1.6.0, #1307 added the partial check to runAsync only, and #1333 made runLive subscribe to the Single that appendEvent returns. Before #1333, the work of FirestoreSessionService (inside Single.fromCallable) and the API call of VertexAiSessionService never ran from runLive, so those services stored nothing from live turns. Since 1.6.0 they store the chunks.
Additional Context:
-
A direction that matches ADK Python takes two steps, and each needs the other:
- Aggregate in the live path: keep streaming text and transcription chunks as partial events, and emit one non-partial event with the accumulated text where ADK Python does (turn complete, interruption, tool call). This could go in
GeminiLlmConnection, as in Python, or in the live branch ofBaseLlmFlow. - Skip partial events in
runLiveasrunAsyncdoes. A live counterpart ofrunAsync_partialEvent_streamedButNotPassedToSessionServicewould cover it.
Step 2 alone would change nothing in case (a), where the chunks are not partial, and in case (b) it would make every session service lose the live model text, as
InMemorySessionServicedoes today. Step 1 alone would leave Firestore and Vertex AI storing the chunks next to the full text. - Aggregate in the live path: keep streaming text and transcription chunks as partial events, and emit one non-partial event with the accumulated text where ADK Python does (turn complete, interruption, tool call). This could go in
-
A smaller, independent alignment:
FirestoreSessionService.appendEventcould return early for partial events, like the default implementation and ADK Python's Firestore service (firestore_session_service.py:497-500). #1643 (for #1640) changes how events are stored, not which events are stored, so it does not change this: with #1643 a stored chunk would come back withpartial: true, butContentsdoes not read the flag. -
Live audio is a separate difference: ADK Python does not save live model audio events with inline data (
runners.py:1469-1486). Java has no such check, so those events follow the same rules as the text chunks, andFirestoreSessionServicewrites them without the audio. -
To tell (a) from (b),
GeminiLlmConnectionlogs every server message at debug level (GeminiLlmConnection.java:118). TheserverContentcases inGeminiLlmConnectionTestalways setturnComplete, and I did not find a recorded server message in the repository. -
Does aggregating in the live path and then skipping partial events in
runLivematch how you want live sessions to be stored? If so, I'm happy to send a PR and to follow your choice of where the aggregation should go.
Minimal Reproduction Code:
The first test goes into RunnerTest and uses its RecordingSessionService, createContent and imports:
@Test
public void runLive_textChunks_passedToSessionServiceUnaggregated() throws Exception {
// GeminiLlmConnection makes one LlmResponse per serverContent and never aggregates text.
// A chunk is partial only if its message carries turnComplete: false.
for (boolean chunksArePartial : new boolean[] {false, true}) {
TestLlm testLlm =
new TestLlm(
() ->
Flowable.just(
liveTextChunk("Hel", chunksArePartial),
liveTextChunk("lo", chunksArePartial),
LlmResponse.builder().partial(false).turnComplete(true).build()));
RecordingSessionService sessionService = new RecordingSessionService();
Runner runner =
Runner.builder()
.app(App.builder().name("test").rootAgent(createTestAgent(testLlm)).build())
.sessionService(sessionService)
.build();
Session session = sessionService.createSession("test", "user").blockingGet();
LiveRequestQueue liveRequestQueue = new LiveRequestQueue();
TestSubscriber<Event> testSubscriber =
runner.runLive(session, liveRequestQueue, RunConfig.builder().build()).test();
liveRequestQueue.content(createContent("hi"));
liveRequestQueue.close();
testSubscriber.await();
Session stored =
sessionService.getSession("test", "user", session.id(), Optional.empty()).blockingGet();
System.out.println("chunks partial = " + chunksArePartial);
System.out.println(" passed to appendEvent: " + describe(sessionService.appendedEvents));
System.out.println(" kept by InMemory: " + describe(stored.events()));
}
}
private static LlmResponse liveTextChunk(String text, boolean partial) {
return LlmResponse.builder()
.content(Content.builder().role("model").parts(Part.fromText(text)).build())
.partial(partial)
.turnComplete(false)
.build();
}
private static List<String> describe(List<Event> events) {
return events.stream()
.map(
e ->
(e.partial().orElse(false) ? "partial " : "")
+ (e.turnComplete().orElse(false) ? "turnComplete " : "")
+ "\""
+ e.content().map(Content::text).orElse("")
+ "\"")
.toList();
}
The second goes into FirestoreSessionServiceTest and uses its mocks, constants and imports:
@Test
@SuppressWarnings("unchecked")
void appendEvent_writesPartialEvent() {
Session session =
Session.builder(SESSION_ID)
.appName(APP_NAME)
.userId(USER_ID)
.state(new ConcurrentHashMap<>())
.build();
when(mockSessionsCollection.document(SESSION_ID)).thenReturn(mockSessionDocRef);
when(mockEventsCollection.document()).thenReturn(mockEventDocRef);
when(mockEventDocRef.getId()).thenReturn(EVENT_ID);
when(mockEventsCollection.document(EVENT_ID)).thenReturn(mockEventDocRef);
Event chunk =
Event.builder()
.author("model")
.partial(true)
.content(Content.fromParts(Part.fromText("Hel")))
.build();
sessionService.appendEvent(session, chunk).blockingGet();
// The partial event is written as an event document (verify passes).
ArgumentCaptor<Map<String, Object>> saved = ArgumentCaptor.forClass(Map.class);
verify(mockEventDocRef).set(saved.capture());
System.out.println("stored fields: " + saved.getValue().keySet().stream().sorted().toList());
}
How often has this issue occurred?:
- Always (100%) in live mode. In case (a), every turn whose text arrives in more than one server message is affected; in case (b), every turn with model text.
- Langage dominant
- Java
- Étoiles
- 1.7k
- Forks
- 433
- Merge moyen
- 2 j 21 h
- PR mergées (30 j)
- 40
Préparer son environnement
Lance le conteneur de développement du projet dans votre navigateur, avec votre propre compte GitHub.
- Aucun Dockerfile ni fichier Docker Compose
- Propose un modèle de pull request
- Lire le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de google/adk-java
-
GeminiUtil placeholder user turn ("Continue output. DO NOT look at this line ...") is flagged by prompt injection filtersPeut-être pris @hemasekhar-p l’a pris il y a 4 jours. Ouverteneeds review
Difficulté 2/5 1-3 heures Accessibilité débutants 76/100
google/adk-java#1628 · 1 commentaire · 1 personne assignée ·
Les mainteneurs répondent en général sous 1 jour
-
[spring-ai] ToolConverter silently drops enum and items from tool parameter schemasPeut-être pris @hirematha l’a pris il y a 7 jours. Ouverteneeds review
Difficulté 2/5 1-3 heures Accessibilité débutants 76/100
google/adk-java#1609 · 2 commentaires · 1 personne assignée ·
Les mainteneurs répondent en général sous 1 jour
-
[spring-ai] Streaming responses ending with CJK punctuation (。!?) are misclassified as partial and never persisted to the sessionPeut-être pris @hirematha l’a pris il y a 7 jours. Ouverteneeds review
Difficulté 2/5 1-3 heures Accessibilité débutants 84/100
google/adk-java#1608 · 3 commentaires · 1 personne assignée ·
Les mainteneurs répondent en général sous 1 jour
-
FirestoreSessionService compares event timestamps as text, reordering or skipping same-second eventsOuverte
Difficulté 3/5 1-2 jours Accessibilité débutants 50/100
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 4/5 3-5 jours Accessibilité débutants 45/100
Les mainteneurs répondent en général sous 1 jour
Toutes les issues de google/adk-java
Issues similaires
-
enhancement
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
ShaftHQ/SHAFT_ENGINE#6909 ·
Les mainteneurs répondent en général sous 1 jour
-
Mention parakeet-ffm in READMEOuverte
Difficulté 1/5 Moins d'une heure Accessibilité débutants 72/100
mudler/parakeet.cpp#99 ·
Les mainteneurs répondent en général sous 1 jour
-
[BUG] Re-subscribing the Mono returned by listTools/listResources/listResourceTemplates/listPrompts/listRoots duplicates the resultsPeut-être pris Une pull request liée à cette issue est ouverte ou déjà fusionnée. Ouverte
Difficulté 2/5 1-3 heures Accessibilité débutants 75/100
modelcontextprotocol/java-sdk#1174 · 1 réaction ·
Les mainteneurs répondent en général sous 2 jours
-
建议在文档中补充逻辑删除与唯一索引冲突的处理方案Ouverte
Difficulté 2/5 1-3 heures Accessibilité débutants 68/100
baomidou/mybatis-plus#7185 ·
-
bug
Difficulté 2/5 1-3 heures Accessibilité débutants 65/100
refinedmods/refinedstorage2#1419 ·