From 859359a7acf5013863eb86a4d45663aaacfa8485 Mon Sep 17 00:00:00 2001
From: jujn <2087687391@qq.com>
Date: Sun, 9 Aug 2026 22:36:03 +0800
Subject: [PATCH 1/2] fix(agui): parse request bodies with Jackson 2 codec
---
.../agui/common/AguiRequestBodyParser.java | 43 ++++++++++
.../boot/agui/mvc/AguiRestController.java | 11 ++-
.../boot/agui/webflux/AguiWebFluxHandler.java | 7 +-
.../common/AguiRequestBodyParserTest.java | 86 +++++++++++++++++++
4 files changed, 141 insertions(+), 6 deletions(-)
create mode 100644 agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/common/AguiRequestBodyParser.java
create mode 100644 agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/test/java/io/agentscope/spring/boot/agui/common/AguiRequestBodyParserTest.java
diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/common/AguiRequestBodyParser.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/common/AguiRequestBodyParser.java
new file mode 100644
index 0000000000..b15ae03c08
--- /dev/null
+++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/common/AguiRequestBodyParser.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * 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 io.agentscope.spring.boot.agui.common;
+
+import io.agentscope.core.agui.model.RunAgentInput;
+import io.agentscope.core.util.JsonUtils;
+import java.util.Objects;
+
+/**
+ * Parses AG-UI request bodies with AgentScope's JSON codec.
+ *
+ *
The default AgentScope codec is Jackson 2. Keeping this conversion at the AG-UI boundary
+ * avoids selecting Spring Boot 4's Jackson 3 codec for AG-UI's Jackson 2-annotated models, while
+ * leaving the application's other HTTP converters unchanged.
+ */
+public final class AguiRequestBodyParser {
+
+ private AguiRequestBodyParser() {}
+
+ /**
+ * Parses a JSON request body into {@link RunAgentInput}.
+ *
+ * @param body the raw JSON request body
+ * @return the parsed AG-UI input
+ */
+ public static RunAgentInput parse(String body) {
+ Objects.requireNonNull(body, "body cannot be null");
+ return JsonUtils.getJsonCodec().fromJson(body, RunAgentInput.class);
+ }
+}
diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/mvc/AguiRestController.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/mvc/AguiRestController.java
index 2b4feef6bf..9c169dee3f 100644
--- a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/mvc/AguiRestController.java
+++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/mvc/AguiRestController.java
@@ -16,6 +16,7 @@
package io.agentscope.spring.boot.agui.mvc;
import io.agentscope.core.agui.model.RunAgentInput;
+import io.agentscope.spring.boot.agui.common.AguiRequestBodyParser;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
@@ -63,7 +64,7 @@ public AguiRestController(
*
"default"
*
*
- * @param input The run agent input
+ * @param body The run agent input
* @param agentIdHeader The agent ID from HTTP header (optional)
* @return An SseEmitter for streaming AG-UI events
*/
@@ -72,12 +73,13 @@ public AguiRestController(
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter run(
- @RequestBody RunAgentInput input,
+ @RequestBody String body,
@RequestHeader(
value = "${agentscope.agui.agent-id-header:X-Agent-Id}",
required = false)
String agentIdHeader,
HttpServletRequest request) {
+ RunAgentInput input = AguiRequestBodyParser.parse(body);
return aguiMvcController.handle(input, agentIdHeader, request);
}
@@ -87,7 +89,7 @@ public SseEmitter run(
* The path variable takes highest priority for agent resolution.
*
* @param agentId The agent ID from path variable
- * @param input The run agent input
+ * @param body The run agent input
* @param agentIdHeader The agent ID from HTTP header (optional)
* @return An SseEmitter for streaming AG-UI events
*/
@@ -97,12 +99,13 @@ public SseEmitter run(
produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter runWithAgentId(
@PathVariable String agentId,
- @RequestBody RunAgentInput input,
+ @RequestBody String body,
@RequestHeader(
value = "${agentscope.agui.agent-id-header:X-Agent-Id}",
required = false)
String agentIdHeader,
HttpServletRequest request) {
+ RunAgentInput input = AguiRequestBodyParser.parse(body);
return aguiMvcController.handleWithAgentId(input, agentIdHeader, agentId, request);
}
}
diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/webflux/AguiWebFluxHandler.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/webflux/AguiWebFluxHandler.java
index 54b0777885..6a69ae1ca4 100644
--- a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/webflux/AguiWebFluxHandler.java
+++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/webflux/AguiWebFluxHandler.java
@@ -24,6 +24,7 @@
import io.agentscope.core.agui.model.RunAgentInput;
import io.agentscope.core.agui.processor.AguiRequestProcessor;
import io.agentscope.core.agui.registry.AguiAgentRegistry;
+import io.agentscope.spring.boot.agui.common.AguiRequestBodyParser;
import io.agentscope.spring.boot.agui.common.AguiRuntimeContextRequest;
import io.agentscope.spring.boot.agui.common.AguiRuntimeContextResolver;
import io.agentscope.spring.boot.agui.common.DefaultAgentResolver;
@@ -112,7 +113,8 @@ private AguiWebFluxHandler(Builder builder) {
* @return A Mono containing the server response with SSE stream
*/
public Mono handle(ServerRequest request) {
- return request.bodyToMono(RunAgentInput.class)
+ return request.bodyToMono(String.class)
+ .map(AguiRequestBodyParser::parse)
.flatMap(input -> processInput(input, request, null))
.onErrorResume(this::handleParseError);
}
@@ -128,7 +130,8 @@ public Mono handle(ServerRequest request) {
*/
public Mono handleWithAgentId(ServerRequest request) {
String pathAgentId = request.pathVariable(AGENT_ID_PATH_VARIABLE);
- return request.bodyToMono(RunAgentInput.class)
+ return request.bodyToMono(String.class)
+ .map(AguiRequestBodyParser::parse)
.flatMap(input -> processInput(input, request, pathAgentId))
.onErrorResume(this::handleParseError);
}
diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/test/java/io/agentscope/spring/boot/agui/common/AguiRequestBodyParserTest.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/test/java/io/agentscope/spring/boot/agui/common/AguiRequestBodyParserTest.java
new file mode 100644
index 0000000000..295b88038b
--- /dev/null
+++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/test/java/io/agentscope/spring/boot/agui/common/AguiRequestBodyParserTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * 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 io.agentscope.spring.boot.agui.common;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.agentscope.core.agui.model.MessageContent;
+import io.agentscope.core.agui.model.RunAgentInput;
+import org.junit.jupiter.api.Test;
+
+class AguiRequestBodyParserTest {
+
+ @Test
+ void shouldParseTextContentWithAgentScopeCodec() {
+ String body =
+ """
+ {
+ "threadId": "thread-1",
+ "runId": "run-1",
+ "messages": [
+ {"id": "message-1", "role": "user", "content": "Hello!"}
+ ]
+ }
+ """;
+
+ RunAgentInput input = AguiRequestBodyParser.parse(body);
+
+ assertEquals("Hello!", input.getMessages().get(0).getTextContent());
+ }
+
+ @Test
+ void shouldParseMultimodalContentWithAgentScopeCodec() {
+ String body =
+ """
+ {
+ "threadId": "thread-1",
+ "runId": "run-1",
+ "messages": [
+ {
+ "id": "message-1",
+ "role": "user",
+ "content": [
+ {"type": "text", "text": "Describe this"},
+ {
+ "type": "image",
+ "source": {
+ "type": "url",
+ "value": "https://example.com/image.png"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ """;
+
+ RunAgentInput input = AguiRequestBodyParser.parse(body);
+ MessageContent.Blocks content =
+ assertInstanceOf(
+ MessageContent.Blocks.class, input.getMessages().get(0).getContent());
+
+ assertEquals(2, content.parts().size());
+ assertTrue(input.getMessages().get(0).hasBlocks());
+ }
+
+ @Test
+ void shouldRejectNullBody() {
+ assertThrows(NullPointerException.class, () -> AguiRequestBodyParser.parse(null));
+ }
+}
From 0578fe7310771d0271270462c806faf7d442c64b Mon Sep 17 00:00:00 2001
From: jujn <2087687391@qq.com>
Date: Mon, 10 Aug 2026 00:09:00 +0800
Subject: [PATCH 2/2] fix: improve
---
.../boot/agui/mvc/AguiRestController.java | 35 ++++++++++++++-
.../boot/agui/mvc/AguiRestControllerTest.java | 43 +++++++++++++++++++
2 files changed, 76 insertions(+), 2 deletions(-)
create mode 100644 agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/test/java/io/agentscope/spring/boot/agui/mvc/AguiRestControllerTest.java
diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/mvc/AguiRestController.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/mvc/AguiRestController.java
index 9c169dee3f..8cfac18022 100644
--- a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/mvc/AguiRestController.java
+++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/mvc/AguiRestController.java
@@ -15,10 +15,16 @@
*/
package io.agentscope.spring.boot.agui.mvc;
+import io.agentscope.core.agui.encoder.AguiEventEncoder;
+import io.agentscope.core.agui.event.AguiEvent;
import io.agentscope.core.agui.model.RunAgentInput;
+import io.agentscope.core.util.JsonException;
import io.agentscope.spring.boot.agui.common.AguiRequestBodyParser;
import jakarta.servlet.http.HttpServletRequest;
+import java.util.Map;
import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -36,6 +42,7 @@
public class AguiRestController {
private final AguiMvcController aguiMvcController;
+ private final AguiEventEncoder encoder = new AguiEventEncoder();
private final String pathPrefix;
private final boolean enablePathRouting;
@@ -64,7 +71,7 @@ public AguiRestController(
* "default"
*
*
- * @param body The run agent input
+ * @param body The raw run agent input JSON
* @param agentIdHeader The agent ID from HTTP header (optional)
* @return An SseEmitter for streaming AG-UI events
*/
@@ -89,7 +96,7 @@ public SseEmitter run(
* The path variable takes highest priority for agent resolution.
*
* @param agentId The agent ID from path variable
- * @param body The run agent input
+ * @param body The raw run agent input JSON
* @param agentIdHeader The agent ID from HTTP header (optional)
* @return An SseEmitter for streaming AG-UI events
*/
@@ -108,4 +115,28 @@ public SseEmitter runWithAgentId(
RunAgentInput input = AguiRequestBodyParser.parse(body);
return aguiMvcController.handleWithAgentId(input, agentIdHeader, agentId, request);
}
+
+ /**
+ * Return HTTP 400 for AG-UI request body parsing failures.
+ *
+ * @param error the JSON parse failure
+ * @return an SSE-compatible bad request response
+ */
+ @ExceptionHandler(JsonException.class)
+ public ResponseEntity handleParseError(JsonException error) {
+ String errorEvent =
+ encoder.encodeToJson(
+ new AguiEvent.Raw(
+ "unknown",
+ "unknown",
+ Map.of(
+ "error",
+ "Failed to parse request: " + error.getMessage())))
+ .trim();
+ String finishEvent =
+ encoder.encodeToJson(new AguiEvent.RunFinished("unknown", "unknown")).trim();
+ return ResponseEntity.badRequest()
+ .contentType(MediaType.TEXT_EVENT_STREAM)
+ .body("data: " + errorEvent + "\n\n" + "data: " + finishEvent + "\n\n");
+ }
}
diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/test/java/io/agentscope/spring/boot/agui/mvc/AguiRestControllerTest.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/test/java/io/agentscope/spring/boot/agui/mvc/AguiRestControllerTest.java
new file mode 100644
index 0000000000..fd6f2507dd
--- /dev/null
+++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/test/java/io/agentscope/spring/boot/agui/mvc/AguiRestControllerTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * 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 io.agentscope.spring.boot.agui.mvc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.agentscope.core.util.JsonException;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+
+class AguiRestControllerTest {
+
+ @Test
+ void shouldReturnBadRequestSseForParseErrors() {
+ AguiRestController controller = new AguiRestController(null, "/agui", true);
+
+ ResponseEntity response =
+ controller.handleParseError(new JsonException("bad json"));
+
+ assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
+ assertEquals(MediaType.TEXT_EVENT_STREAM, response.getHeaders().getContentType());
+ assertNotNull(response.getBody());
+ assertTrue(response.getBody().contains("Failed to parse request: bad json"));
+ assertTrue(response.getBody().contains("data: "));
+ }
+}