Skip to content

Commit d943985

Browse files
lex00claude
andcommitted
fix: add is_error handling to AnthropicProvider, add provider error test
- Set is_error=Boolean.TRUE on Anthropic tool result maps when a handler throws, matching the Anthropic API spec; OpenAI has no equivalent field - Add testAnthropicProvider_HandlerError_SetsIsError using an in-process HTTP server to verify is_error propagation without a real API key - Update README to clarify positioning vs Python/TypeScript framework plugins Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8445d86 commit d943985

3 files changed

Lines changed: 78 additions & 0 deletions

File tree

temporal-tool-registry/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ A Temporal Activity is a function that Temporal monitors and retries automatical
1313

1414
New to Temporal? → https://docs.temporal.io/develop
1515

16+
**Python or TypeScript user?** Those SDKs also ship framework-level integrations (`openai_agents`, `google_adk_agents`, `langgraph`, `@temporalio/ai-sdk`) for teams already using a specific agent framework. ToolRegistry is the equivalent story for direct Anthropic/OpenAI calls, and shares the same API surface across all six Temporal SDKs.
17+
1618
## Install
1719

1820
Add to your `build.gradle`:

temporal-tool-registry/src/main/java/io/temporal/toolregistry/AnthropicProvider.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,20 @@ public TurnResult runTurn(List<Map<String, Object>> messages, List<ToolDefinitio
119119
@SuppressWarnings("unchecked")
120120
Map<String, Object> input = (Map<String, Object>) call.get("input");
121121
String result;
122+
boolean isError = false;
122123
try {
123124
result = registry.dispatch(name, input);
124125
} catch (Exception e) {
125126
result = "error: " + e.getMessage();
127+
isError = true;
126128
}
127129
Map<String, Object> toolResult = new LinkedHashMap<>();
128130
toolResult.put("type", "tool_result");
129131
toolResult.put("tool_use_id", id);
130132
toolResult.put("content", result);
133+
if (isError) {
134+
toolResult.put("is_error", Boolean.TRUE);
135+
}
131136
toolResults.add(toolResult);
132137
}
133138
Map<String, Object> toolResultMsg = new LinkedHashMap<>();

temporal-tool-registry/src/test/java/io/temporal/toolregistry/ToolRegistryTest.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,77 @@ public void testFromMcpTools_populatesRegistry() throws Exception {
228228
assertEquals("", reg.dispatch("read_file", Map.of("path", "/etc/hosts")));
229229
}
230230

231+
// ── AnthropicProvider is_error / handler error tests ─────────────────────────
232+
233+
/**
234+
* Verifies that when a tool handler throws, the Anthropic tool result carries is_error=true and
235+
* the turn does not propagate the exception.
236+
*/
237+
@Test
238+
public void testAnthropicProvider_HandlerError_SetsIsError() throws Exception {
239+
// Start a minimal HTTP server to mock the Anthropic API.
240+
com.sun.net.httpserver.HttpServer server =
241+
com.sun.net.httpserver.HttpServer.create(new java.net.InetSocketAddress(0), 0);
242+
243+
server.createContext(
244+
"/",
245+
exchange -> {
246+
String body =
247+
"{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\","
248+
+ "\"content\":[{\"type\":\"tool_use\",\"id\":\"c1\","
249+
+ "\"name\":\"boom\",\"input\":{}}],"
250+
+ "\"model\":\"claude-sonnet-4-6\",\"stop_reason\":\"tool_use\","
251+
+ "\"usage\":{\"input_tokens\":10,\"output_tokens\":5}}";
252+
byte[] bytes = body.getBytes(java.nio.charset.StandardCharsets.UTF_8);
253+
exchange.getResponseHeaders().set("Content-Type", "application/json");
254+
exchange.sendResponseHeaders(200, bytes.length);
255+
try (java.io.OutputStream os = exchange.getResponseBody()) {
256+
os.write(bytes);
257+
}
258+
});
259+
server.start();
260+
261+
int port = server.getAddress().getPort();
262+
String baseUrl = "http://localhost:" + port;
263+
264+
ToolRegistry registry = new ToolRegistry();
265+
registry.register(
266+
ToolDefinition.builder()
267+
.name("boom")
268+
.description("d")
269+
.inputSchema(Collections.singletonMap("type", "object"))
270+
.build(),
271+
input -> {
272+
throw new RuntimeException("intentional failure");
273+
});
274+
275+
Provider provider =
276+
new AnthropicProvider(
277+
AnthropicConfig.builder().apiKey("test-key").baseUrl(baseUrl).build(),
278+
registry,
279+
"sys");
280+
281+
List<Map<String, Object>> messages = new ArrayList<>();
282+
messages.add(new java.util.LinkedHashMap<>(Map.of("role", "user", "content", "go")));
283+
284+
TurnResult result = provider.runTurn(messages, registry.definitions());
285+
server.stop(0);
286+
287+
assertFalse(result.isDone());
288+
assertEquals(2, result.getNewMessages().size());
289+
290+
Map<String, Object> toolResultMsg = result.getNewMessages().get(1);
291+
assertEquals("user", toolResultMsg.get("role"));
292+
@SuppressWarnings("unchecked")
293+
List<Map<String, Object>> toolResults =
294+
(List<Map<String, Object>>) toolResultMsg.get("content");
295+
assertEquals(1, toolResults.size());
296+
assertEquals("tool_result", toolResults.get(0).get("type"));
297+
assertEquals(Boolean.TRUE, toolResults.get(0).get("is_error"));
298+
String content = (String) toolResults.get(0).get("content");
299+
assertTrue("error message should contain failure text", content.contains("intentional failure"));
300+
}
301+
231302
// ── Integration tests (skipped unless RUN_INTEGRATION_TESTS is set) ───────────
232303

233304
private static ToolRegistry makeRecordRegistry(List<String> collected) throws Exception {

0 commit comments

Comments
 (0)