Skip to content

Commit 9c6b9a9

Browse files
lex00claude
andcommitted
Add temporal-tool-registry module with AgenticSession support
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8716d22 commit 9c6b9a9

25 files changed

Lines changed: 2497 additions & 1 deletion

gradle/linting.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ subprojects {
99
target 'src/*/java/**/*.java'
1010
targetExclude '**/generated/*'
1111
targetExclude '**/.idea/**'
12-
googleJavaFormat('1.24.0')
12+
googleJavaFormat('1.25.2')
1313
}
1414

1515
kotlin {

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ include 'temporal-sdk'
55
include 'temporal-testing'
66
include 'temporal-test-server'
77
include 'temporal-opentracing'
8+
include 'temporal-tool-registry'
89
include 'temporal-kotlin'
910
include 'temporal-spring-boot-autoconfigure'
1011
include 'temporal-spring-boot-starter'

temporal-tool-registry/README.md

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# temporal-tool-registry
2+
3+
LLM tool-calling primitives for Temporal activities — define tools once, use with
4+
Anthropic or OpenAI.
5+
6+
## Before you start
7+
8+
A Temporal Activity is a function that Temporal monitors and retries automatically on failure. Temporal streams progress between retries via heartbeats — that's the mechanism `AgenticSession` uses to resume a crashed LLM conversation mid-turn.
9+
10+
`ToolRegistry.runToolLoop` works standalone in any function — no Temporal server needed. Add `AgenticSession` only when you need crash-safe resume inside a Temporal activity.
11+
12+
`AgenticSession` requires a running Temporal worker — it reads and writes heartbeat state from the active activity context. Use `ToolRegistry.runToolLoop` standalone for scripts, one-off jobs, or any code that runs outside a Temporal worker.
13+
14+
New to Temporal? → https://docs.temporal.io/develop
15+
16+
## Install
17+
18+
Add to your `build.gradle`:
19+
20+
```groovy
21+
dependencies {
22+
// Replace VERSION with the latest release from https://search.maven.org
23+
implementation 'io.temporal:temporal-tool-registry:VERSION'
24+
// Add only the LLM SDK(s) you use:
25+
implementation 'com.anthropic:anthropic-java:VERSION' // Anthropic
26+
implementation 'com.openai:openai-java:VERSION' // OpenAI
27+
}
28+
```
29+
30+
## Quickstart
31+
32+
Tool definitions use [JSON Schema](https://json-schema.org/understanding-json-schema/) for `inputSchema`. The quickstart uses a single string field; for richer schemas refer to the JSON Schema docs.
33+
34+
```java
35+
import io.temporal.toolregistry.*;
36+
37+
@ActivityMethod
38+
public List<String> analyze(String prompt) throws Exception {
39+
List<String> issues = new ArrayList<>();
40+
ToolRegistry registry = new ToolRegistry();
41+
registry.register(
42+
ToolDefinition.builder()
43+
.name("flag_issue")
44+
.description("Flag a problem found in the analysis")
45+
.inputSchema(Map.of(
46+
"type", "object",
47+
"properties", Map.of("description", Map.of("type", "string")),
48+
"required", List.of("description")))
49+
.build(),
50+
(Map<String, Object> input) -> {
51+
issues.add((String) input.get("description"));
52+
return "recorded"; // this string is sent back to the LLM as the tool result
53+
});
54+
55+
AnthropicConfig cfg = AnthropicConfig.builder()
56+
.apiKey(System.getenv("ANTHROPIC_API_KEY"))
57+
.build();
58+
Provider provider = new AnthropicProvider(cfg, registry,
59+
"You are a code reviewer. Call flag_issue for each problem you find.");
60+
61+
ToolRegistry.runToolLoop(provider, registry, "" /* system prompt: "" defers to provider default */, prompt);
62+
return issues;
63+
}
64+
```
65+
66+
### Selecting a model
67+
68+
The default model is `"claude-sonnet-4-6"` (Anthropic) or `"gpt-4o"` (OpenAI). Override with the `model()` builder method:
69+
70+
```java
71+
AnthropicConfig cfg = AnthropicConfig.builder()
72+
.apiKey(System.getenv("ANTHROPIC_API_KEY"))
73+
.model("claude-3-5-sonnet-20241022")
74+
.build();
75+
```
76+
77+
Model IDs are defined by the provider — see Anthropic or OpenAI docs for current names.
78+
79+
### OpenAI
80+
81+
```java
82+
OpenAIConfig cfg = OpenAIConfig.builder()
83+
.apiKey(System.getenv("OPENAI_API_KEY"))
84+
.build();
85+
Provider provider = new OpenAIProvider(cfg, registry, "your system prompt");
86+
ToolRegistry.runToolLoop(provider, registry, "" /* system prompt: "" defers to provider default */, prompt);
87+
```
88+
89+
## Crash-safe agentic sessions
90+
91+
For multi-turn LLM conversations that must survive activity retries, use
92+
`AgenticSession.runWithSession`. It saves conversation history via
93+
`Activity.getExecutionContext().heartbeat()` on every turn and restores it on retry.
94+
95+
```java
96+
@ActivityMethod
97+
public List<Object> longAnalysis(String prompt) throws Exception {
98+
List<Object> issues = new ArrayList<>();
99+
100+
AgenticSession.runWithSession(session -> {
101+
ToolRegistry registry = new ToolRegistry();
102+
registry.register(
103+
ToolDefinition.builder().name("flag").description("...").inputSchema(Map.of("type", "object")).build(),
104+
input -> { session.addIssue(input); return "ok"; /* sent back to LLM */ });
105+
106+
AnthropicConfig cfg = AnthropicConfig.builder()
107+
.apiKey(System.getenv("ANTHROPIC_API_KEY")).build();
108+
Provider provider = new AnthropicProvider(cfg, registry, "your system prompt");
109+
110+
session.runToolLoop(provider, registry, "your system prompt", prompt);
111+
issues.addAll(session.getIssues()); // capture after loop completes
112+
});
113+
114+
return issues;
115+
}
116+
```
117+
118+
## Testing without an API key
119+
120+
```java
121+
import io.temporal.toolregistry.testing.*;
122+
123+
@Test
124+
public void testAnalyze() throws Exception {
125+
ToolRegistry registry = new ToolRegistry();
126+
registry.register(
127+
ToolDefinition.builder().name("flag").description("d")
128+
.inputSchema(Map.of("type", "object")).build(),
129+
input -> "ok");
130+
131+
MockProvider provider = new MockProvider(
132+
MockResponse.toolCall("flag", Map.of("description", "stale API")),
133+
MockResponse.done("analysis complete"));
134+
135+
List<Map<String, Object>> msgs =
136+
ToolRegistry.runToolLoop(provider, registry, "sys", "analyze");
137+
assertTrue(msgs.size() > 2);
138+
}
139+
```
140+
141+
## Integration testing with real providers
142+
143+
To run the integration tests against live Anthropic and OpenAI APIs:
144+
145+
```bash
146+
RUN_INTEGRATION_TESTS=1 \
147+
ANTHROPIC_API_KEY=sk-ant-... \
148+
OPENAI_API_KEY=sk-proj-... \
149+
./gradlew test --tests "*.ToolRegistryTest.testIntegration*"
150+
```
151+
152+
Tests skip automatically when `RUN_INTEGRATION_TESTS` is unset. Real API calls
153+
incur billing — expect a few cents per full test run.
154+
155+
## Storing application results
156+
157+
`session.getIssues()` accumulates application-level
158+
results during the tool loop. Elements are serialized to JSON inside each heartbeat
159+
checkpoint — they must be plain maps/dicts with JSON-serializable values. A non-serializable
160+
value raises a non-retryable `ApplicationError` at heartbeat time rather than silently
161+
losing data on the next retry.
162+
163+
### Storing typed results
164+
165+
Convert your domain type to a plain dict at the tool-call site and back after the session:
166+
167+
```java
168+
record Issue(String type, String file) {}
169+
170+
// Inside tool handler:
171+
session.addIssue(Map.of("type", "smell", "file", "Foo.java"));
172+
173+
// After session (using Jackson for convenient mapping):
174+
// requires jackson-databind in your build.gradle:
175+
// implementation 'com.fasterxml.jackson.core:jackson-databind:VERSION'
176+
ObjectMapper mapper = new ObjectMapper();
177+
List<Issue> issues = session.getIssues().stream()
178+
.map(m -> mapper.convertValue(m, Issue.class))
179+
.toList();
180+
```
181+
182+
## Per-turn LLM timeout
183+
184+
Individual LLM calls inside the tool loop are unbounded by default. A hung HTTP
185+
connection holds the activity open until Temporal's `ScheduleToCloseTimeout`
186+
fires — potentially many minutes. Set a per-turn timeout on the provider client:
187+
188+
```java
189+
AnthropicConfig cfg = AnthropicConfig.builder()
190+
.apiKey(System.getenv("ANTHROPIC_API_KEY"))
191+
.timeout(Duration.ofSeconds(30))
192+
.build();
193+
Provider provider = new AnthropicProvider(cfg, registry, "your system prompt");
194+
// provider now enforces 30s per turn
195+
```
196+
197+
Recommended timeouts:
198+
199+
| Model type | Recommended |
200+
|---|---|
201+
| Standard (Claude 3.x, GPT-4o) | 30 s |
202+
| Reasoning (o1, o3, extended thinking) | 300 s |
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
description = '''Temporal Java SDK Tool Registry - LLM tool-calling primitives for Temporal activities'''
2+
3+
// Both Anthropic and OpenAI Java SDKs require Java 11+, so this module targets Java 11.
4+
// The core SDK supports Java 8+, but this contrib module is explicitly Java 11+.
5+
afterEvaluate {
6+
compileJava.options.compilerArgs.removeAll { it == '8' }
7+
compileJava.options.compilerArgs.removeAll { it == '--release' }
8+
compileJava.options.compilerArgs.addAll(['--release', '11'])
9+
10+
compileTestJava.options.compilerArgs.removeAll { it == '8' }
11+
compileTestJava.options.compilerArgs.removeAll { it == '--release' }
12+
compileTestJava.options.compilerArgs.addAll(['--release', '11'])
13+
}
14+
15+
ext {
16+
anthropicVersion = '2.24.0' // com.anthropic:anthropic-java
17+
openaiVersion = '4.31.0' // com.openai:openai-java
18+
}
19+
20+
dependencies {
21+
// Not bundled — consumers provide temporal-sdk themselves, just like temporal-opentracing.
22+
compileOnly project(':temporal-sdk')
23+
24+
// LLM providers are optional compile-time deps; users add only what they use.
25+
compileOnly "com.anthropic:anthropic-java:$anthropicVersion"
26+
compileOnly "com.openai:openai-java:$openaiVersion"
27+
28+
testImplementation project(':temporal-testing')
29+
testImplementation "junit:junit:${junitVersion}"
30+
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
31+
testImplementation "com.anthropic:anthropic-java:$anthropicVersion"
32+
testImplementation "com.openai:openai-java:$openaiVersion"
33+
testRuntimeOnly "ch.qos.logback:logback-classic:${logbackVersion}"
34+
}

0 commit comments

Comments
 (0)