You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: AgentEmitter#requiresInput(..., isFinal=true) does not close the event stream #975
After #756, streams correctly stay open for interrupted states (INPUT_REQUIRED / AUTH_REQUIRED).
However, AgentEmitter#requiresInput(Message, boolean isFinal) and requiresAuth(..., boolean isFinal) accept an isFinal flag that appears to have no effect on stream completion. Passing isFinal=true still leaves the SSE / Flow.Publisher open (subscriber onComplete is never called).
Expected behavior
Either:
requiresInput(message, true) / requiresAuth(message, true) closes the stream after the status event is delivered, or
The isFinal parameter is documented as not controlling stream lifecycle (and ideally removed/deprecated if unused).
Actual behavior
In 1.1.0.Final, isFinal only sets the internal terminalStateReached flag in AgentEmitter#updateStatus. It is not written onto TaskStatusUpdateEvent.
EventConsumer terminates the stream when TaskStatusUpdateEvent#isFinal() is true, but that method only delegates to TaskState#isFinal(), which is always false for INPUT_REQUIRED / AUTH_REQUIRED.
So requiresInput(msg, true) and requiresInput(msg, false) behave the same for stream completion.
Related leftover: AgentEmitter#emitEvent Javadoc still shows TaskStatusUpdateEvent.builder()...isFinal(false), but the builder no longer has isFinal(...).
Steps to reproduce
In an AgentExecutor, after work finishes for this turn, call:
emitter.requiresInput(replyMessage, true);
Subscribe to onMessageSendStream / JSON-RPC streaming response.
Observe that events are delivered, but the publisher never completes / SSE connection stays open.
If keeping the stream open for interrupted states is intentional (per #756), what is the supported way for an agent to end the current stream after emitting INPUT_REQUIRED, when the client is expected to continue via a new request rather than the same open stream?
In updateStatus, isFinal only drives terminalStateReached.compareAndSet(false, true). The event itself is built as TaskStatusUpdateEvent.builder().taskId(...).contextId(...).status(new TaskStatus(state, message, null)).build(), with no finality carried onto it.
TaskStatusUpdateEvent.isFinal() returns status.state().isFinal(), i.e. it is derived purely from the state.
EventConsumer decides completion on tue.isFinal() (event instanceof TaskStatusUpdateEvent tue && tue.isFinal()), and for INPUT_REQUIRED / AUTH_REQUIREDTaskState.isFinal() is false (correctly, per fix: Keep stream open on interrupted state changes #756: interrupted states are not terminal).
So isFinal=true does two things that have quietly come apart: it seals the emitter against further updates (via terminalStateReached, so a later updateStatus throws), but it does not finalize the event or complete the stream, because the only thing EventConsumer reads is state-derived finality.
This is not purely cosmetic. Because the intended finalization never completes the publisher, connections an agent asked to release stay open, so a server handling many concurrent interrupted turns keeps SSE / Flow.Publisher resources it was told to free. That is a mild availability cost that only shows up under load, and it leans toward actually wiring the flag rather than only documenting it.
The reason the flag is stranded is that in the v1.0 spec TaskStatusUpdateEvent has no explicit final field (task_id, context_id, status, metadata only in specification/a2a.proto), so finality became a function of the state. The isFinal parameter is a leftover from the earlier model where the event carried its own final bit. That is also why the emitEvent Javadoc still shows TaskStatusUpdateEvent.builder()...isFinal(false), a builder call that no longer exists.
Which fix fits depends on the intended contract, and the two are not equivalent:
If an agent is meant to be able to end the current stream after INPUT_REQUIRED (your question, and the useful case), the stream-close signal has to be separate from state-derived finality, since fix: Keep stream open on interrupted state changes #756 deliberately keeps interrupted states open. Concretely: carry the caller's isFinal onto the enqueued event (or a distinct terminal marker) and have EventConsumer complete when that is set, independent of state().isFinal(). That preserves fix: Keep stream open on interrupted state changes #756's default (interrupted stays open) while letting the agent opt to close and have the client resume via a new request.
If the intended contract is that interrupted streams always stay open and the client always resumes on a fresh request, then isFinal on requiresInput / requiresAuth is misleading and should be deprecated or removed, with the lifecycle documented, so callers are not led to expect a completion that never comes.
Option 1 answers the "how does the agent free the connection" case; option 2 only closes the documentation gap. Either way terminalStateReached and stream completion are two separate concepts today, and the bug is that isFinal currently reaches only the first.
Done, opened #980. It takes option 1 (wire isFinal to actually close the stream, reusing the existing QueueClosedEvent path so interrupted states still stay open by default per #756). Verified on JDK 17, server-common suite green. Glad to switch it to the deprecate/document approach instead if you'd prefer that.
Cross-SDK note: a2a-python closes the stream on INPUT_REQUIRED
In a2a-python, emitting INPUT_REQUIRED effectively ends the current request stream. This differs from a2a-java after #756 (stream stays open for interrupted states), and there is currently no working way in Java to opt into the Python behavior (requiresInput(..., true) does not close the stream).
1. Legacy path — EventConsumer treats INPUT_REQUIRED as final
EventConsumer.consume_all includes TASK_STATE_INPUT_REQUIRED in the final-state set, then closes the queue and stops consumption:
Send a follow-up message with the same taskId (new stream)
Suggestion for a2a-java
Please consider either:
Align with a2a-python: close the current stream after emitting INPUT_REQUIRED (client continues via a new request), or
Keep fix: Keep stream open on interrupted state changes #756 behavior, but provide a supported API to close the stream after an interrupted state — and either honor requiresInput(..., isFinal=true) / requiresAuth(..., isFinal=true), or remove/deprecate that parameter and document the intended close API.
Happy to help validate either approach from a server-embedding perspective.
Thanks for the cross-SDK writeup, that is a useful comparison to have on record.
#980 already takes the second option you outlined: keep #756's default (interrupted states stay open), and add a supported way to close the stream when the agent wants to, by honoring requiresInput/requiresAuth isFinal=true instead of leaving it stranded. It reuses the existing QueueClosedEvent path, so a client that gets INPUT_REQUIRED with isFinal=true can resume via a new request, same as the Python behavior you described, while the default for callers not asking for isFinal is unchanged.
It is still open and awaiting review. Flagging here in case it helps move it forward, since it should close this issue once it lands.
I have opened #989 as an alternative PR for the reasons mentioned in #980 (comment)
Thanks @kabir — deprecating the isFinal overloads sounds good.
We still need a way to close the stream after INPUT_REQUIRED (to free SSE connections; clients can resume via a new request). Do you have a rough timeline or tracking issue for that dedicated close API?
@bianjp I don't know exactly what that would look like yet.
I guess the issue is that if the client doesn't react to either of these interrupted states, the stream stays open and never closes?
OTOH I don't think there is anything wrong with a client being subscribed to the stream until the end, and farming off sending input/auth via a synchronous sendMessage() call when it receives these events.
Also, we could have the case where a lot of clients are subscribed to the stream while a Task takes days to complete.
Maybe what we need is some kind of hook that is notified when the events happen, and provide an API for users to close the stream. That way users could handle it the way they see fit.
I guess the issue is that if the client doesn't react to either of these interrupted states, the stream stays open and never closes?
@kabir Yes — that’s the issue. With SSE, only the server can complete the stream. The client can’t cleanly close it; dropping the connection still leaves the server-side publisher/resources open until timeout.
Also, SSE is one-way, so after INPUT_REQUIRED the client has to send the follow-up via a new request (e.g. sendMessage) anyway — keeping the original stream open doesn’t help that path.
Would you prefer I open a new issue to track an explicit close/hook API for this?
Activity
chopmob-cloud commentedon Jul 13, 2026
Confirmed the trace, and it points at a clear root cause: the
isFinalparameter no longer has anywhere to land on the wire.Walking it through on
1.1.0.Final:requiresInput(message, isFinal)callsupdateStatus(TASK_STATE_INPUT_REQUIRED, message, isFinal).updateStatus,isFinalonly drivesterminalStateReached.compareAndSet(false, true). The event itself is built asTaskStatusUpdateEvent.builder().taskId(...).contextId(...).status(new TaskStatus(state, message, null)).build(), with no finality carried onto it.TaskStatusUpdateEvent.isFinal()returnsstatus.state().isFinal(), i.e. it is derived purely from the state.EventConsumerdecides completion ontue.isFinal()(event instanceof TaskStatusUpdateEvent tue && tue.isFinal()), and forINPUT_REQUIRED/AUTH_REQUIREDTaskState.isFinal()isfalse(correctly, per fix: Keep stream open on interrupted state changes #756: interrupted states are not terminal).So
isFinal=truedoes two things that have quietly come apart: it seals the emitter against further updates (viaterminalStateReached, so a laterupdateStatusthrows), but it does not finalize the event or complete the stream, because the only thingEventConsumerreads is state-derived finality.This is not purely cosmetic. Because the intended finalization never completes the publisher, connections an agent asked to release stay open, so a server handling many concurrent interrupted turns keeps SSE /
Flow.Publisherresources it was told to free. That is a mild availability cost that only shows up under load, and it leans toward actually wiring the flag rather than only documenting it.The reason the flag is stranded is that in the v1.0 spec
TaskStatusUpdateEventhas no explicit final field (task_id,context_id,status,metadataonly inspecification/a2a.proto), so finality became a function of the state. TheisFinalparameter is a leftover from the earlier model where the event carried its own final bit. That is also why theemitEventJavadoc still showsTaskStatusUpdateEvent.builder()...isFinal(false), a builder call that no longer exists.Which fix fits depends on the intended contract, and the two are not equivalent:
If an agent is meant to be able to end the current stream after
INPUT_REQUIRED(your question, and the useful case), the stream-close signal has to be separate from state-derived finality, since fix: Keep stream open on interrupted state changes #756 deliberately keeps interrupted states open. Concretely: carry the caller'sisFinalonto the enqueued event (or a distinct terminal marker) and haveEventConsumercomplete when that is set, independent ofstate().isFinal(). That preserves fix: Keep stream open on interrupted state changes #756's default (interrupted stays open) while letting the agent opt to close and have the client resume via a new request.If the intended contract is that interrupted streams always stay open and the client always resumes on a fresh request, then
isFinalonrequiresInput/requiresAuthis misleading and should be deprecated or removed, with the lifecycle documented, so callers are not led to expect a completion that never comes.Option 1 answers the "how does the agent free the connection" case; option 2 only closes the documentation gap. Either way
terminalStateReachedand stream completion are two separate concepts today, and the bug is thatisFinalcurrently reaches only the first.kabir commentedon Jul 13, 2026
Hi @chopmob-cloud - I'm on my own this week, and pulled off into other things where I cover for people on holiday.
Would you be able to use your analysis to open a PR? :-)
chopmob-cloud commentedon Jul 13, 2026
Done, opened #980. It takes option 1 (wire
isFinalto actually close the stream, reusing the existingQueueClosedEventpath so interrupted states still stay open by default per #756). Verified on JDK 17,server-commonsuite green. Glad to switch it to the deprecate/document approach instead if you'd prefer that.bianjp commentedon Jul 16, 2026
Cross-SDK note: a2a-python closes the stream on
INPUT_REQUIREDIn a2a-python, emitting
INPUT_REQUIREDeffectively ends the current request stream. This differs from a2a-java after #756 (stream stays open for interrupted states), and there is currently no working way in Java to opt into the Python behavior (requiresInput(..., true)does not close the stream).1. Legacy path —
EventConsumertreatsINPUT_REQUIREDas finalEventConsumer.consume_allincludesTASK_STATE_INPUT_REQUIREDin the final-state set, then closes the queue and stops consumption:2. Current default path — V2 ends the subscriber when
execute()returnsDefaultRequestHandleris aliased to V2:The executor contract says that for
INPUT_REQUIREDthe agent should publish the status update and return to yield control:When
execute()returns, the producer enqueues_RequestCompleted:And
ActiveTask.subscribereturns on matching_RequestCompleted, ending the stream for that request:So the Python client interaction is roughly:
INPUT_REQUIRED→ current stream endstaskId(new stream)Suggestion for a2a-java
Please consider either:
INPUT_REQUIRED(client continues via a new request), orrequiresInput(..., isFinal=true)/requiresAuth(..., isFinal=true), or remove/deprecate that parameter and document the intended close API.Happy to help validate either approach from a server-embedding perspective.
chopmob-cloud commentedon Jul 21, 2026
Thanks for the cross-SDK writeup, that is a useful comparison to have on record.
#980 already takes the second option you outlined: keep #756's default (interrupted states stay open), and add a supported way to close the stream when the agent wants to, by honoring requiresInput/requiresAuth isFinal=true instead of leaving it stranded. It reuses the existing QueueClosedEvent path, so a client that gets INPUT_REQUIRED with isFinal=true can resume via a new request, same as the Python behavior you described, while the default for callers not asking for isFinal is unchanged.
It is still open and awaiting review. Flagging here in case it helps move it forward, since it should close this issue once it lands.
kabir commentedon Jul 22, 2026
I have opened #989 as an alternative PR for the reasons mentioned in #980 (comment)
bianjp commentedon Jul 22, 2026
Thanks @kabir — deprecating the
isFinaloverloads sounds good.We still need a way to close the stream after
INPUT_REQUIRED(to free SSE connections; clients can resume via a new request). Do you have a rough timeline or tracking issue for that dedicated close API?kabir commentedon Jul 22, 2026
@bianjp I don't know exactly what that would look like yet.
I guess the issue is that if the client doesn't react to either of these interrupted states, the stream stays open and never closes?
OTOH I don't think there is anything wrong with a client being subscribed to the stream until the end, and farming off sending input/auth via a synchronous sendMessage() call when it receives these events.
Also, we could have the case where a lot of clients are subscribed to the stream while a Task takes days to complete.
Maybe what we need is some kind of hook that is notified when the events happen, and provide an API for users to close the stream. That way users could handle it the way they see fit.
Just some initial thoughts
bianjp commentedon Jul 22, 2026
@kabir Yes — that’s the issue. With SSE, only the server can complete the stream. The client can’t cleanly close it; dropping the connection still leaves the server-side publisher/resources open until timeout.
Also, SSE is one-way, so after
INPUT_REQUIREDthe client has to send the follow-up via a new request (e.g.sendMessage) anyway — keeping the original stream open doesn’t help that path.Would you prefer I open a new issue to track an explicit close/hook API for this?
kabir commentedon Jul 22, 2026
@bianjp Yes please :-) Please include a bit of the stuff we discussed.
BTW I'll be away for a few weeks from Friday, so I doubt much will happen before I'm back