Skip to content

fix(server): start event bus processor for manual wiring - #997

Open
014-code wants to merge 3 commits into
a2aproject:mainfrom
014-code:fix/995-main-event-bus-processor-startup
Open

fix(server): start event bus processor for manual wiring#997
014-code wants to merge 3 commits into
a2aproject:mainfrom
014-code:fix/995-main-event-bus-processor-startup

Conversation

@014-code

Copy link
Copy Markdown
Contributor

Summary

  • Ensure manually constructed MainEventBusProcessor instances are started through ensureStarted().
  • Call ensureStarted() from DefaultRequestHandler.create(...) so Spring/manual wiring paths do not leave events stuck on MainEventBus.
  • Add regression coverage for the manually wired request handler path returning a Message response.

Related Issue

This fixes #995.

Testing

  • Passed: mvn -pl server-common -am -Dtest=DefaultRequestHandlerTest#testCreateStartsManuallyConstructedMainEventBusProcessor -Dsurefire.failIfNoSpecifiedTests=false test

Ensure MainEventBusProcessor.ensureStarted() starts manually constructed processors, and call it from DefaultRequestHandler.create(...). This prevents manually wired integrations from leaving events stuck on the main event bus.

Add a regression test for the manual wiring path that returns a Message response.

This fixes a2aproject#995
@guofengzh

Copy link
Copy Markdown

Based on the summary in #997, I adjusted the way Spring beans are constructed in ServerConfig, then, the expected response was received.

The change are:

  1. Use the constructor of DefaultRequestHandler to construct an instance of DefaultRequestHandler.
  2. Construct a separate MainEventBusProcessor bean, and then inject it into the DefaultRequestHandler bean.

In other words, the following construct

	@Bean
	public RequestHandler requestHandler(AgentExecutor agentExecutor, TaskStore taskStore, QueueManager queueManager,
	                                     PushNotificationConfigStore pushConfigStore,
										 MainEventBus mainEventBus,
										 PushNotificationSender pushSender,
	                                     @Qualifier("a2aInternal") Executor executor) {
		ExecutorService eventConsumerExecutor = Executors.newFixedThreadPool(3);
		return new DefaultRequestHandler(agentExecutor, taskStore, queueManager, pushConfigStore,
				new MainEventBusProcessor(mainEventBus, taskStore, pushSender, queueManager),
				executor,
				eventConsumerExecutor);
	}

is transformed into

	@Bean
	public MainEventBusProcessor mainEventBusProcessor(MainEventBus mainEventBus,TaskStore taskStore,
													   PushNotificationSender pushSender,
													   QueueManager queueManager) {
		return new MainEventBusProcessor(mainEventBus, taskStore, pushSender, queueManager);
	}
	

	@Bean
	public RequestHandler requestHandler(AgentExecutor agentExecutor, TaskStore taskStore, QueueManager queueManager,
	                                     PushNotificationConfigStore pushConfigStore,
	                                     MainEventBusProcessor mainEventBusProcessor,
	                                     @Qualifier("a2aInternal") Executor executor) {
		return new DefaultRequestHandler(agentExecutor, taskStore, queueManager,
				pushConfigStore, mainEventBusProcessor,
				executor, executor);
	}

Spring Boot recognizes @PostConstruct (and @Inject), so DefaultRequestHandler.initConfig() and MainEventBusProcessor.start() will be executed in this case.

Then, test using the following request:

{
  "jsonrpc": "2.0",
  "id": "6e7bf51a-f364-42d7-a060-343da3cc4f43",
  "method": "SendMessage",
  "params": {
    "message": {
      "messageId": "messageId-1",
      "role": "ROLE_USER",
      "parts": [
        {
          "text": "Can you check systems?"
        }
      ]
    }
  }
}

Response 200 OK

Response Body:

{
  "error": null,
  "id": "6e7bf51a-f364-42d7-a060-343da3cc4f43",
  "jsonrpc": "2.0",
  "result": {
    "role": "ROLE_AGENT",
    "parts": [
      {
        "text": "Hello World",
        "metadata": null
      }
    ],
    "messageId": "d062faba-39e6-4ac6-b891-9e0ba1363a69",
    "contextId": "c2c16e07-dfad-40f1-958c-41d336d4363e",
    "taskId": "d38c2bc5-7491-4920-a290-93582d21a9e0",
    "referenceTaskIds": null,
    "metadata": null,
    "extensions": null
  }
}

That is, a success result was returned.

Hopefully, these new improvements will be helpful to your revisions.

Thanks for what you did.

014-code added 2 commits July 27, 2026 20:43
Start the main event bus processor from the public request handler constructor and verify the direct manual wiring path.\n\nThis fixes a2aproject#995
Ensure AgentEmitter fills missing task and context IDs when sending an existing Message, so streaming clients can reliably observe the server-generated task ID for message-only responses.

This fixes a2aproject#995
@ehsavoie
ehsavoie requested a review from kabir August 5, 2026 12:12
@ehsavoie

ehsavoie commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Can you please rebase ? Also an example on how you are using this would be great since it is in SpringBoot runtime

@kabir kabir left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @014-code! Overall it looks good, but I think exposing the start() method directly will be simpler.

@SuppressWarnings("NullAway.Init")
@PostConstruct
void start() {
synchronized void start() {

@kabir kabir Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can just make this public synchronized

*/
public void ensureStarted() {
// Method intentionally empty - just forces proxy resolution
start();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please undo this change

// I am unsure about the correct scope.
// Also reworked to make a Supplier since otherwise the builder gets polluted with wrong tasks
this.requestContextBuilder = () -> new SimpleRequestContextBuilder(taskStore, false);
this.mainEventBusProcessor.ensureStarted();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just call mainEventBusProcessor.start() if we have to.
But I wonder, since this comes in as a constructor parameter, if you could just not call start() on it before calling the constructor? So this line doesn't seem essential to me.

}
eventQueue.enqueueEvent(message);
Message messageToSend = message;
if (message.taskId() == null || message.contextId() == null) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude thinks:

The null-ID backfill in AgentEmitter (lines 522-528 of the diff) is unrelated to the event bus processor startup fix.
The PR's own test doesn't exercise this path — the test executor calls sendMessage(String), not sendMessage(Message).
This should be a separate PR with its own issue reference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Help]: Could not find a Task/Message for 84ced8e6-3705-41c6-9158-f2b880463a32

4 participants