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)