This is an automated email from the ASF dual-hosted git repository.

wenjin272 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git


The following commit(s) were added to refs/heads/main by this push:
     new da996ceb [api][java] Keep failed Python tool calls as errors in 
ToolResponseEvent (#1180)
da996ceb is described below

commit da996cebf8f754ea8d94a150ea1a638e85a91b92
Author: Sangkyoon Nam <[email protected]>
AuthorDate: Wed Sep 30 12:28:11 2026 +0900

    [api][java] Keep failed Python tool calls as errors in ToolResponseEvent 
(#1180)
    
    Generated-by: Claude Code 2.1.283 (Claude Opus 5.5)
---
 .../flink/agents/api/event/ToolResponseEvent.java  | 12 +++++++++--
 .../agents/api/CrossLanguageEventSnapshotTest.java | 23 +++++++++++++++++++++-
 .../python/tool_response_event.json                | 13 ++++++++----
 .../tests/test_cross_language_event_snapshots.py   | 16 ++++++++++++++-
 4 files changed, 56 insertions(+), 8 deletions(-)

diff --git 
a/api/src/main/java/org/apache/flink/agents/api/event/ToolResponseEvent.java 
b/api/src/main/java/org/apache/flink/agents/api/event/ToolResponseEvent.java
index 9c288491..16d8f317 100644
--- a/api/src/main/java/org/apache/flink/agents/api/event/ToolResponseEvent.java
+++ b/api/src/main/java/org/apache/flink/agents/api/event/ToolResponseEvent.java
@@ -75,6 +75,8 @@ public class ToolResponseEvent extends Event {
         }
         Map<String, ?> rawResponses = (Map<String, ?>) 
attributes.get("responses");
         if (rawResponses != null) {
+            Object rawSuccess = attributes.get("success");
+            Map<?, ?> success = rawSuccess instanceof Map ? (Map<?, ?>) 
rawSuccess : Map.of();
             Map<String, ToolResponse> responses = new HashMap<>();
             for (Map.Entry<String, ?> entry : rawResponses.entrySet()) {
                 Object v = entry.getValue();
@@ -82,6 +84,11 @@ public class ToolResponseEvent extends Event {
                     responses.put(entry.getKey(), (ToolResponse) v);
                 } else if (v instanceof Map) {
                     responses.put(entry.getKey(), MAPPER.convertValue(v, 
ToolResponse.class));
+                } else if (Boolean.FALSE.equals(success.get(entry.getKey()))) {
+                    // The Python runtime records a failed call as the text it 
shows the model in
+                    // `responses`, with `success` false and the diagnostic in 
`error`. Keep that
+                    // text as the error so Java consumers show the model the 
same thing.
+                    responses.put(entry.getKey(), 
ToolResponse.error(String.valueOf(v)));
                 } else {
                     responses.put(entry.getKey(), ToolResponse.success(v));
                 }
@@ -146,9 +153,10 @@ public class ToolResponseEvent extends Event {
                 + getRequestId()
                 + ", response="
                 + getResponses()
-                + ", success=true"
+                + ", success="
+                + getAttr("success")
                 + ", timestamp="
-                + getTimestamp()
+                + getAttr("timestamp")
                 + '}';
     }
 }
diff --git 
a/api/src/test/java/org/apache/flink/agents/api/CrossLanguageEventSnapshotTest.java
 
b/api/src/test/java/org/apache/flink/agents/api/CrossLanguageEventSnapshotTest.java
index 703ee5a5..47566957 100644
--- 
a/api/src/test/java/org/apache/flink/agents/api/CrossLanguageEventSnapshotTest.java
+++ 
b/api/src/test/java/org/apache/flink/agents/api/CrossLanguageEventSnapshotTest.java
@@ -52,6 +52,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
@@ -71,6 +72,9 @@ class CrossLanguageEventSnapshotTest {
     private static final String FIXED_TOOL_CALL_ID = "call_aaaa";
     private static final String FIXED_TOOL_CALL_ID_NUMERIC = "call_bbbb";
     private static final String FIXED_TOOL_CALL_ID_BOOL = "call_cccc";
+    private static final String FIXED_TOOL_CALL_ID_FAILED = "call_dddd";
+    private static final String FIXED_TOOL_FAILURE_TEXT = "Tool `get_weather` 
execute failed.";
+    private static final String FIXED_TOOL_ERROR = "ValueError: boom";
     private static final String MEMORY_REF_ATTACHMENT_EVENT_TYPE = 
"_memory_ref_attachment_event";
     private static final long FIXED_TIMESTAMP = 1_700_000_000_000L;
 
@@ -406,10 +410,27 @@ class CrossLanguageEventSnapshotTest {
 
         Map<String, Object> attrs = typed.getAttributes();
         assertEquals(Boolean.TRUE, typed.getSuccess().get(FIXED_TOOL_CALL_ID));
-        assertTrue(typed.getError().isEmpty());
+        assertEquals(Map.of(FIXED_TOOL_CALL_ID_FAILED, FIXED_TOOL_ERROR), 
typed.getError());
         assertFalse(attrs.containsKey("timestamp"));
     }
 
+    @Test
+    void pythonToolResponseEventKeepsFailedCallsAsErrors() throws Exception {
+        Event base = readPythonSnapshot("tool_response_event.json");
+        ToolResponseEvent typed = ToolResponseEvent.fromEvent(base);
+
+        ToolResponse failed = 
typed.getResponses().get(FIXED_TOOL_CALL_ID_FAILED);
+        assertNotNull(failed);
+        assertTrue(failed.isError(), "A call Python marked as failed must not 
read as a success.");
+        // The response keeps the text Python shows the model; the diagnostic 
stays in getError().
+        assertEquals(FIXED_TOOL_FAILURE_TEXT, failed.getError());
+        assertEquals(FIXED_TOOL_ERROR, 
typed.getError().get(FIXED_TOOL_CALL_ID_FAILED));
+        assertEquals(Boolean.FALSE, 
typed.getSuccess().get(FIXED_TOOL_CALL_ID_FAILED));
+
+        String printed = assertDoesNotThrow(typed::toString);
+        assertTrue(printed.contains(FIXED_TOOL_CALL_ID_FAILED + "=false"), 
printed);
+    }
+
     // ── ContextRetrievalRequestEvent ───────────────────────────────────────
 
     private static ContextRetrievalRequestEvent 
buildContextRetrievalRequestEvent() {
diff --git 
a/e2e-test/cross-language-event-snapshots/python/tool_response_event.json 
b/e2e-test/cross-language-event-snapshots/python/tool_response_event.json
index 65b2cbd6..2dce2cc1 100644
--- a/e2e-test/cross-language-event-snapshots/python/tool_response_event.json
+++ b/e2e-test/cross-language-event-snapshots/python/tool_response_event.json
@@ -6,18 +6,23 @@
     "responses": {
       "call_aaaa": "pong",
       "call_bbbb": 42,
-      "call_cccc": true
+      "call_cccc": true,
+      "call_dddd": "Tool `get_weather` execute failed."
     },
     "success": {
       "call_aaaa": true,
       "call_bbbb": true,
-      "call_cccc": true
+      "call_cccc": true,
+      "call_dddd": false
+    },
+    "error": {
+      "call_dddd": "ValueError: boom"
     },
-    "error": {},
     "external_ids": {
       "call_aaaa": null,
       "call_bbbb": null,
-      "call_cccc": null
+      "call_cccc": null,
+      "call_dddd": null
     }
   },
   "attachments": {}
diff --git 
a/python/flink_agents/api/tests/test_cross_language_event_snapshots.py 
b/python/flink_agents/api/tests/test_cross_language_event_snapshots.py
index 13ba3fc9..c14bf0f9 100644
--- a/python/flink_agents/api/tests/test_cross_language_event_snapshots.py
+++ b/python/flink_agents/api/tests/test_cross_language_event_snapshots.py
@@ -47,6 +47,9 @@ _FIXED_REQUEST_ID = 
UUID("00000000-0000-0000-0000-000000000002")
 _FIXED_TOOL_CALL_ID = "call_aaaa"
 _FIXED_TOOL_CALL_ID_NUMERIC = "call_bbbb"
 _FIXED_TOOL_CALL_ID_BOOL = "call_cccc"
+_FIXED_TOOL_CALL_ID_FAILED = "call_dddd"
+_FIXED_TOOL_FAILURE_TEXT = "Tool `get_weather` execute failed."
+_FIXED_TOOL_ERROR = "ValueError: boom"
 
 
 def _regenerate_enabled() -> bool:
@@ -312,19 +315,30 @@ def 
test_python_can_deserialize_tool_request_event_from_java_snapshot() -> None:
 def _build_tool_response_event() -> ToolResponseEvent:
     # Mixed scalar value types pin the Python -> Java round-trip on the Java
     # ToolResponseEvent.fromEvent fall-through that wraps non-ToolResponse/Map
-    # values via ToolResponse.success(v).
+    # values via ToolResponse.success(v). The failed call is shaped the way
+    # ToolCallAction records a raised exception: the text shown to the model in
+    # `responses`, `success` False, and the diagnostic in `error`.
     event = ToolResponseEvent(
         request_id=_FIXED_REQUEST_ID,
         responses={
             _FIXED_TOOL_CALL_ID: "pong",
             _FIXED_TOOL_CALL_ID_NUMERIC: 42,
             _FIXED_TOOL_CALL_ID_BOOL: True,
+            _FIXED_TOOL_CALL_ID_FAILED: _FIXED_TOOL_FAILURE_TEXT,
         },
         external_ids={
             _FIXED_TOOL_CALL_ID: None,
             _FIXED_TOOL_CALL_ID_NUMERIC: None,
             _FIXED_TOOL_CALL_ID_BOOL: None,
+            _FIXED_TOOL_CALL_ID_FAILED: None,
         },
+        success={
+            _FIXED_TOOL_CALL_ID: True,
+            _FIXED_TOOL_CALL_ID_NUMERIC: True,
+            _FIXED_TOOL_CALL_ID_BOOL: True,
+            _FIXED_TOOL_CALL_ID_FAILED: False,
+        },
+        error={_FIXED_TOOL_CALL_ID_FAILED: _FIXED_TOOL_ERROR},
     )
     return _force_id(event, _FIXED_EVENT_ID)
 

Reply via email to