Skip to content

Commit f8345bb

Browse files
lex00claude
andcommitted
feat(tool-registry): rename issues→results, remove system param, timeout docs
- Rename AgenticSession.issues → results, getIssues() → getResults(), addIssue() → addResult(); rename SessionCheckpoint JSON key 'issues' → 'results' - Remove unused system parameter from AgenticSession.runToolLoop and ToolRegistry.runToolLoop - Add ScheduleToCloseTimeout guidance to README - Update all test call sites Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d943985 commit f8345bb

5 files changed

Lines changed: 54 additions & 41 deletions

File tree

temporal-tool-registry/README.md

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public List<String> analyze(String prompt) throws Exception {
6060
Provider provider = new AnthropicProvider(cfg, registry,
6161
"You are a code reviewer. Call flag_issue for each problem you find.");
6262

63-
ToolRegistry.runToolLoop(provider, registry, "" /* system prompt: "" defers to provider default */, prompt);
63+
ToolRegistry.runToolLoop(provider, registry, prompt);
6464
return issues;
6565
}
6666
```
@@ -97,23 +97,23 @@ For multi-turn LLM conversations that must survive activity retries, use
9797
```java
9898
@ActivityMethod
9999
public List<Object> longAnalysis(String prompt) throws Exception {
100-
List<Object> issues = new ArrayList<>();
100+
List<Object> results = new ArrayList<>();
101101

102102
AgenticSession.runWithSession(session -> {
103103
ToolRegistry registry = new ToolRegistry();
104104
registry.register(
105105
ToolDefinition.builder().name("flag").description("...").inputSchema(Map.of("type", "object")).build(),
106-
input -> { session.addIssue(input); return "ok"; /* sent back to LLM */ });
106+
input -> { session.addResult(input); return "ok"; /* sent back to LLM */ });
107107

108108
AnthropicConfig cfg = AnthropicConfig.builder()
109109
.apiKey(System.getenv("ANTHROPIC_API_KEY")).build();
110110
Provider provider = new AnthropicProvider(cfg, registry, "your system prompt");
111111

112-
session.runToolLoop(provider, registry, "your system prompt", prompt);
113-
issues.addAll(session.getIssues()); // capture after loop completes
112+
session.runToolLoop(provider, registry, prompt);
113+
results.addAll(session.getResults()); // capture after loop completes
114114
});
115115

116-
return issues;
116+
return results;
117117
}
118118
```
119119

@@ -135,7 +135,7 @@ public void testAnalyze() throws Exception {
135135
MockResponse.done("analysis complete"));
136136

137137
List<Map<String, Object>> msgs =
138-
ToolRegistry.runToolLoop(provider, registry, "sys", "analyze");
138+
ToolRegistry.runToolLoop(provider, registry, "analyze");
139139
assertTrue(msgs.size() > 2);
140140
}
141141
```
@@ -156,7 +156,7 @@ incur billing — expect a few cents per full test run.
156156

157157
## Storing application results
158158

159-
`session.getIssues()` accumulates application-level
159+
`session.getResults()` accumulates application-level
160160
results during the tool loop. Elements are serialized to JSON inside each heartbeat
161161
checkpoint — they must be plain maps/dicts with JSON-serializable values. A non-serializable
162162
value raises a non-retryable `ApplicationError` at heartbeat time rather than silently
@@ -167,17 +167,17 @@ losing data on the next retry.
167167
Convert your domain type to a plain dict at the tool-call site and back after the session:
168168

169169
```java
170-
record Issue(String type, String file) {}
170+
record Result(String type, String file) {}
171171

172172
// Inside tool handler:
173-
session.addIssue(Map.of("type", "smell", "file", "Foo.java"));
173+
session.addResult(Map.of("type", "smell", "file", "Foo.java"));
174174

175175
// After session (using Jackson for convenient mapping):
176176
// requires jackson-databind in your build.gradle:
177177
// implementation 'com.fasterxml.jackson.core:jackson-databind:VERSION'
178178
ObjectMapper mapper = new ObjectMapper();
179-
List<Issue> issues = session.getIssues().stream()
180-
.map(m -> mapper.convertValue(m, Issue.class))
179+
List<Result> results = session.getResults().stream()
180+
.map(m -> mapper.convertValue(m, Result.class))
181181
.toList();
182182
```
183183

@@ -203,6 +203,21 @@ Recommended timeouts:
203203
| Standard (Claude 3.x, GPT-4o) | 30 s |
204204
| Reasoning (o1, o3, extended thinking) | 300 s |
205205

206+
### Activity-level timeout
207+
208+
Set `setScheduleToCloseTimeout` on the activity stub options to bound the entire conversation:
209+
210+
```java
211+
ActivityOptions opts = ActivityOptions.newBuilder()
212+
.setScheduleToCloseTimeout(Duration.ofMinutes(10))
213+
.build();
214+
MyActivities stub = Workflow.newActivityStub(MyActivities.class, opts);
215+
```
216+
217+
The per-turn client timeout and `ScheduleToCloseTimeout` are complementary:
218+
- Per-turn timeout fires if one LLM call hangs (protects against a single stuck turn)
219+
- `ScheduleToCloseTimeout` bounds the entire conversation including all retries (protects against runaway multi-turn loops)
220+
206221
## MCP integration
207222

208223
`ToolRegistry.fromMcpTools` converts a list of `McpTool` descriptors into a populated

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

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
import org.slf4j.LoggerFactory;
1414

1515
/**
16-
* Maintains conversation state (messages and issues) across multiple turns of a tool-calling loop,
17-
* with heartbeat checkpointing for crash recovery.
16+
* Maintains conversation state (messages and results) across multiple turns of a tool-calling
17+
* loop, with heartbeat checkpointing for crash recovery.
1818
*
1919
* <p>Use {@link #runWithSession(SessionFn)} inside a Temporal activity to get automatic checkpoint
2020
* restore-on-retry and heartbeat on each turn:
2121
*
2222
* <pre>{@code
2323
* AgenticSession.runWithSession(session -> {
24-
* session.runToolLoop(provider, registry, systemPrompt, userPrompt);
24+
* session.runToolLoop(provider, registry, userPrompt);
2525
* });
2626
* }</pre>
2727
*
@@ -32,7 +32,7 @@ public class AgenticSession {
3232
private static final Logger log = LoggerFactory.getLogger(AgenticSession.class);
3333

3434
private final List<Map<String, Object>> messages = new ArrayList<>();
35-
private final List<Map<String, Object>> issues = new ArrayList<>();
35+
private final List<Map<String, Object>> results = new ArrayList<>();
3636

3737
/** Creates an empty session. */
3838
public AgenticSession() {}
@@ -50,13 +50,12 @@ public AgenticSession() {}
5050
*
5151
* @param provider the LLM provider adapter
5252
* @param registry the tool registry
53-
* @param system the system prompt
5453
* @param prompt the initial user prompt (ignored if restoring from a checkpoint that already has
5554
* messages)
5655
* @throws ActivityCompletionException if the activity is cancelled
5756
* @throws Exception on API or dispatch errors
5857
*/
59-
public void runToolLoop(Provider provider, ToolRegistry registry, String system, String prompt)
58+
public void runToolLoop(Provider provider, ToolRegistry registry, String prompt)
6059
throws Exception {
6160
if (messages.isEmpty()) {
6261
Map<String, Object> userMsg = new java.util.LinkedHashMap<>();
@@ -83,24 +82,24 @@ public void runToolLoop(Provider provider, ToolRegistry registry, String system,
8382
*
8483
* <p>Throws {@link ActivityCompletionException} if the activity has been cancelled.
8584
*
86-
* @throws ApplicationFailure (non-retryable) if any issue is not JSON-serializable
85+
* @throws ApplicationFailure (non-retryable) if any result is not JSON-serializable
8786
*/
8887
public void checkpoint() throws ActivityCompletionException {
8988
ObjectMapper mapper = new ObjectMapper();
90-
for (int i = 0; i < issues.size(); i++) {
89+
for (int i = 0; i < results.size(); i++) {
9190
try {
92-
mapper.writeValueAsString(issues.get(i));
91+
mapper.writeValueAsString(results.get(i));
9392
} catch (Exception e) {
9493
throw ApplicationFailure.newNonRetryableFailure(
95-
"AgenticSession: issues["
94+
"AgenticSession: results["
9695
+ i
9796
+ "] is not JSON-serializable: "
9897
+ e.getMessage()
9998
+ ". Store only Map<String, Object> with JSON-serializable values.",
10099
"InvalidArgument");
101100
}
102101
}
103-
SessionCheckpoint cp = new SessionCheckpoint(messages, issues);
102+
SessionCheckpoint cp = new SessionCheckpoint(messages, results);
104103
Activity.getExecutionContext().heartbeat(cp);
105104
}
106105

@@ -114,7 +113,7 @@ public void checkpoint() throws ActivityCompletionException {
114113
*
115114
* <pre>{@code
116115
* AgenticSession.runWithSession(session -> {
117-
* session.runToolLoop(provider, registry, systemPrompt, userPrompt);
116+
* session.runToolLoop(provider, registry, userPrompt);
118117
* });
119118
* }</pre>
120119
*
@@ -152,21 +151,21 @@ public List<Map<String, Object>> getMessages() {
152151
return Collections.unmodifiableList(messages);
153152
}
154153

155-
/** Returns an unmodifiable view of the issues collected during the session. */
156-
public List<Map<String, Object>> getIssues() {
157-
return Collections.unmodifiableList(issues);
154+
/** Returns an unmodifiable view of the results collected during the session. */
155+
public List<Map<String, Object>> getResults() {
156+
return Collections.unmodifiableList(results);
158157
}
159158

160-
/** Appends an issue to the issue list. */
161-
public void addIssue(Map<String, Object> issue) {
162-
issues.add(issue);
159+
/** Appends a result to the results list. */
160+
public void addResult(Map<String, Object> result) {
161+
results.add(result);
163162
}
164163

165164
/** Restores session state from a checkpoint. Called by {@link #runWithSession} on retry. */
166165
void restore(SessionCheckpoint checkpoint) {
167166
messages.clear();
168167
messages.addAll(checkpoint.messages);
169-
issues.clear();
170-
issues.addAll(checkpoint.issues);
168+
results.clear();
169+
results.addAll(checkpoint.results);
171170
}
172171
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ class SessionCheckpoint {
1515
public int version = 1;
1616

1717
public List<Map<String, Object>> messages = new ArrayList<>();
18-
public List<Map<String, Object>> issues = new ArrayList<>();
18+
public List<Map<String, Object>> results = new ArrayList<>();
1919

2020
/** No-arg constructor required for Jackson deserialization. */
2121
SessionCheckpoint() {}
2222

23-
SessionCheckpoint(List<Map<String, Object>> messages, List<Map<String, Object>> issues) {
23+
SessionCheckpoint(List<Map<String, Object>> messages, List<Map<String, Object>> results) {
2424
this.messages = new ArrayList<>(messages);
25-
this.issues = new ArrayList<>(issues);
25+
this.results = new ArrayList<>(results);
2626
}
2727
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,11 @@ public List<Map<String, Object>> toOpenAI() {
139139
*
140140
* @param provider the LLM provider adapter
141141
* @param registry the tool registry (may be the same object, provided for clarity)
142-
* @param system the system prompt
143142
* @param prompt the initial user prompt
144143
* @return the full message history on completion
145144
*/
146145
public static List<Map<String, Object>> runToolLoop(
147-
Provider provider, ToolRegistry registry, String system, String prompt) throws Exception {
146+
Provider provider, ToolRegistry registry, String prompt) throws Exception {
148147
List<Map<String, Object>> messages = new ArrayList<>();
149148
Map<String, Object> userMsg = new LinkedHashMap<>();
150149
userMsg.put("role", "user");

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ public void testRunToolLoop_singleDone() throws Exception {
148148
new io.temporal.toolregistry.testing.MockProvider(
149149
io.temporal.toolregistry.testing.MockResponse.done("finished"));
150150

151-
List<Map<String, Object>> msgs = ToolRegistry.runToolLoop(provider, registry, "sys", "hello");
151+
List<Map<String, Object>> msgs = ToolRegistry.runToolLoop(provider, registry, "hello");
152152

153153
// user + assistant
154154
assertEquals(2, msgs.size());
@@ -195,7 +195,7 @@ public void testRunToolLoop_withToolCall() throws Exception {
195195
}
196196
});
197197

198-
List<Map<String, Object>> msgs = ToolRegistry.runToolLoop(provider, registry, "sys", "go");
198+
List<Map<String, Object>> msgs = ToolRegistry.runToolLoop(provider, registry, "go");
199199

200200
assertEquals(Arrays.asList("first", "second"), collected);
201201
// user + (assistant + tool_result_wrapper)*2 + final assistant
@@ -335,7 +335,7 @@ public void testIntegration_Anthropic() throws Exception {
335335
"You must call record() exactly once with value='hello'.");
336336

337337
ToolRegistry.runToolLoop(
338-
provider, registry, "", "Please call the record tool with value='hello'.");
338+
provider, registry, "Please call the record tool with value='hello'.");
339339

340340
assertTrue("expected 'hello' in collected", collected.contains("hello"));
341341
}
@@ -355,7 +355,7 @@ public void testIntegration_OpenAI() throws Exception {
355355
"You must call record() exactly once with value='hello'.");
356356

357357
ToolRegistry.runToolLoop(
358-
provider, registry, "", "Please call the record tool with value='hello'.");
358+
provider, registry, "Please call the record tool with value='hello'.");
359359

360360
assertTrue("expected 'hello' in collected", collected.contains("hello"));
361361
}

0 commit comments

Comments
 (0)