Commit 79c9fe5
temporal-spring-ai: per-model ActivityOptions registry (#2855)
* temporal-spring-ai: plan — activity summaries for debuggability
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* temporal-spring-ai: amend plan — limit summaries to chat + MCP
Narrows the activity-summaries scope to cases where the plugin itself owns
activity-stub creation. Activity-backed tool calls, Nexus tool calls, and
@SideEffectTool calls are now explicitly out of scope; the first two would
require per-call option overrides on user-owned stubs (no clean API), and
the third writes MarkerRecorded events which have no Summary field.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* temporal-spring-ai: add ActivitySummaryTest (chat path)
Runs a workflow that drives a single chat call through ActivityChatModel,
fetches the resulting history, and asserts the ActivityTaskScheduled event
for callChatModel carries a userMetadata Summary that starts with
"chat: default" and includes the user prompt. Intentionally fails against
unmodified chat code — the implementation follows in a subsequent commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* temporal-spring-ai: attach activity summaries for chat and MCP calls
ActivityChatModel.forModel() now stores the ActivityOptions it built and,
on each chat call, rebuilds the stub with a per-call Summary of the form
"chat: <model> · <first 60 chars of user prompt>". When a caller passes a
pre-built stub directly via the public constructors, behavior is unchanged
(no options known → no summary overlay).
ActivityMcpClient.create() does the same and adds a callTool(clientName,
request, summary) overload. McpToolCallback passes "mcp: <client>.<tool>".
Also fixes the activity-type-name casing in ActivitySummaryTest — Temporal
capitalizes the first character of method-name-derived activity types, so
the event carries "CallChatModel", not "callChatModel".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* temporal-spring-ai: drop PLAN.md
Planning scratchpad — not part of the shipped artifact. Removed before merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* temporal-spring-ai: drop user prompt from chat activity Summary
The Summary now carries only the model label ("chat: <model>") instead
of "chat: <model> · <first 60 chars of user prompt>". Including even a
truncated prompt leaks whatever the prompt contains — PII, secrets,
internal identifiers — into workflow history, server logs, and the
Temporal UI, which is a surprising default for an observability label.
An opt-in API for callers who explicitly want the prompt in the
Summary can be added later if there's demand.
ActivitySummaryTest.chatActivity_carriesModelOnlySummary_neverLeaksUserPrompt
asserts the Summary equals "chat: default" exactly and defensively
checks that no part of the prompt leaked in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* temporal-spring-ai: wrap nullable fields/params in Optional<>
Addresses review feedback (thread on #2852) preferring typesafe
Optional<T> over nullable fields and raw null delegation. Three
sites changed:
- ActivityChatModel: modelName and baseOptions fields + private ctor
params are now Optional<String> / Optional<ActivityOptions>.
getModelName() returns Optional<String>. Public ctors and factories
still accept nullable String modelName at the API boundary (matches
prior javadoc: "null for default"); they normalize via
Optional.ofNullable before storing. Internal readers use .map /
.orElse instead of null checks.
- ActivityMcpClient: the 3-arg callTool's summary parameter is now
Optional<String>. The 2-arg convenience overload passes
Optional.empty(). The rebuild-with-summary branch uses
.isPresent() + .get() instead of null checks.
- McpToolCallback.call(...) wraps its generated summary string in
Optional.of(...) before passing to callTool.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* temporal-spring-ai: use @nullable annotations instead of Optional<>
Reverses the previous Optional<> commit in favor of @nullable on the
nullable fields/params. Matches the dominant convention in
temporal-sdk (431+ @nullable usages on public API surface) and avoids
a thicker runtime story for what is fundamentally a documentation /
IDE-hint concern (no NullAway in the build either way).
- ActivityChatModel: modelName and baseOptions fields + private ctor
params are now @nullable String / @nullable ActivityOptions. Public
ctors/factories accept nullable String modelName at the API
boundary directly. getModelName() returns @nullable String.
- ActivityMcpClient: baseOptions field and callTool(..., summary)
param use @nullable instead of Optional<>. 2-arg callTool passes
null; McpToolCallback passes a plain String.
- ChatModelTypes.ChatModelActivityInput record: @nullable on
modelName and modelOptions fields so deserialized readers see the
nullability in the signature (per the reviewer's concern about the
deserialization-side typesafety).
Consistent with org.springframework.lang.Nullable already used
elsewhere in this module (TemporalChatClient, SpringAiPlugin).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* temporal-spring-ai: use javax.annotation.Nullable for consistency
Switch the five files in this module that imported
org.springframework.lang.Nullable over to javax.annotation.Nullable
to match the dominant convention in sdk-java (197 usages vs. 7).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* temporal-spring-ai: plan — ActivityOptions overloads and non-retryable error classification
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* temporal-spring-ai: amend plan — ActivityOptions overloads fix summaries UX wart
The activity-summaries branch only overlays Summaries when the factories
(forModel/create) built the stub. Users who need custom timeouts today fall
back to the public constructor, which silently drops UI Summaries. The
ActivityOptions overloads planned here are the proper fix: they let users
customize the stub and keep Summary labels. Plan now also covers deprecating
the public constructors with javadoc pointing at the factories.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* temporal-spring-ai: accept ActivityOptions and classify non-retryable AI errors
- ActivityChatModel.forDefault(ActivityOptions) and forModel(String,
ActivityOptions) overloads added. New public defaultActivityOptions()
returns the plugin's default bundle so callers can tweak one field
without losing the other sensible defaults.
- ActivityMcpClient.create(ActivityOptions) + defaultActivityOptions()
added, mirroring the chat side.
- Default RetryOptions for chat calls now mark
org.springframework.ai.retry.NonTransientAiException and
java.lang.IllegalArgumentException non-retryable. Default options for
MCP calls mark IllegalArgumentException non-retryable. User-supplied
ActivityOptions pass through verbatim — the plugin does not augment
them.
- new ActivityChatModel(...) and new ActivityMcpClient(activity)
constructors are @deprecated with javadoc pointing at the factories —
they still work at runtime but skip the UI Summary labels the
plugin-owned stub path attaches, which is now called out explicitly.
- README: new "Activity options and retry behavior" section documents
the defaults, how to customize, and the Summary/factory connection.
- Tests: two new suites — ActivityOptionsAndRetryTest covers the
non-retryable classification (1 attempt for NonTransientAiException,
3 attempts for transient RuntimeException, custom task queue landing
on the scheduled activity); ActivitySummaryTest gains a regression
test asserting forDefault(customOptions) still emits UI Summaries.
- build.gradle: spring-ai-retry added as a testImplementation so tests
can reference NonTransientAiException directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* temporal-spring-ai: drop PLAN.md
Planning scratchpad — not part of the shipped artifact. Removed before merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* temporal-spring-ai: remove (Duration, int) factory overloads
Now that forDefault(ActivityOptions) / forModel(String, ActivityOptions)
/ create(ActivityOptions) exist, the (Duration, int) convenience
overloads are asymmetric dead weight — they expose two of N ActivityOptions
fields as positional parameters, and callers wanting anything else
(heartbeats, task queue, custom retry backoff, ...) have to drop to the
ActivityOptions path anyway. Removed pre-release so the API surface is
consistent: no-arg → plugin defaults; ActivityOptions arg → caller options.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* temporal-spring-ai: plan — per-model ActivityOptions registry
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* temporal-spring-ai: per-model ActivityOptions registry
SpringAiPlugin now accepts an optional Map<String, ActivityOptions>
keyed by chat-model bean name. On construction the plugin publishes
the map to a new package-public static registry SpringAiPluginOptions,
which ActivityChatModel.forModel(name) and forDefault() consult when
building the activity stub. Entries resolve by bean name; the reserved
key SpringAiPlugin.DEFAULT_MODEL_NAME ("default") covers forDefault().
Callers who pass explicit ActivityOptions via forModel(name, options)
or forDefault(options) bypass the registry entirely — explicit options
always win. The registry has no effect on the (timeout, maxAttempts)
convenience factory either; that still builds options from its args.
Auto-configuration picks up a user bean named
"chatModelActivityOptions" (constant
SpringAiTemporalAutoConfiguration.CHAT_MODEL_ACTIVITY_OPTIONS_BEAN) of
type Map<String, ActivityOptions>. The explicit bean-name qualifier
avoids Spring's collection-of-beans auto-wiring for Map<String, T>.
Tests: PerModelActivityOptionsTest covers the three cases called out
in the plan — registry hit, registry miss (falls back to default
2-minute timeout), and explicit options bypass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* temporal-spring-ai: drop PLAN.md
Planning scratchpad — not part of the shipped artifact. Removed before merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* temporal-spring-ai: make per-model options compositional via 'default' catch-all
Address reviewer feedback on #2855: the three-way implicit fallback
(specific entry → library defaults) was ambiguous to readers of
forDefault() / forModel(name) — nothing locally tells the caller which
rung they land on. But a strict either-or (global OR exhaustive
per-model) breaks compositionality when a third-party starter
contributes a ChatModel bean your config didn't enumerate.
Resolution: keep per-model overrides, and reserve
ChatModelTypes.DEFAULT_MODEL_NAME as a user-declared catch-all key in
perModelOptions. Lookup is now map[name] ?? map["default"] ??
library defaults. SpringAiPlugin validates that every perModelOptions
key either matches a registered ChatModel bean or equals the catch-all
name; typos fail at plugin construction, not silently at call time.
Updates javadoc on SpringAiPlugin, ActivityChatModel.forModel(String),
and ChatModelActivityOptions. Collapses the duplicate "Activity
options" README section into one with an example of the catch-all
pattern. Adds tests for the catch-all lookup, specific-wins-over-
catch-all, and typo-key rejection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent c402f55 commit 79c9fe5
8 files changed
Lines changed: 556 additions & 30 deletions
File tree
- temporal-spring-ai
- src
- main/java/io/temporal/springai
- autoconfigure
- model
- plugin
- test/java/io/temporal/springai
- plugin
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
67 | 83 | | |
68 | 84 | | |
69 | 85 | | |
| |||
187 | 203 | | |
188 | 204 | | |
189 | 205 | | |
190 | | - | |
191 | | - | |
192 | | - | |
193 | | - | |
194 | | - | |
195 | | - | |
196 | | - | |
197 | | - | |
198 | | - | |
199 | | - | |
200 | | - | |
201 | | - | |
202 | | - | |
203 | | - | |
204 | | - | |
205 | | - | |
206 | | - | |
207 | | - | |
208 | 206 | | |
209 | 207 | | |
210 | 208 | | |
| |||
Lines changed: 52 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
Lines changed: 26 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
3 | 4 | | |
4 | 5 | | |
5 | 6 | | |
| |||
28 | 29 | | |
29 | 30 | | |
30 | 31 | | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
31 | 51 | | |
32 | 52 | | |
33 | | - | |
34 | | - | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
35 | 59 | | |
36 | 60 | | |
Lines changed: 39 additions & 7 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
| 7 | + | |
6 | 8 | | |
7 | 9 | | |
8 | 10 | | |
| |||
102 | 104 | | |
103 | 105 | | |
104 | 106 | | |
105 | | - | |
106 | | - | |
107 | | - | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
108 | 120 | | |
109 | 121 | | |
110 | 122 | | |
111 | 123 | | |
112 | 124 | | |
113 | 125 | | |
114 | | - | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
115 | 130 | | |
116 | 131 | | |
117 | 132 | | |
| |||
130 | 145 | | |
131 | 146 | | |
132 | 147 | | |
133 | | - | |
134 | | - | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
135 | 163 | | |
136 | 164 | | |
137 | 165 | | |
| |||
140 | 168 | | |
141 | 169 | | |
142 | 170 | | |
143 | | - | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
144 | 176 | | |
145 | 177 | | |
146 | 178 | | |
| |||
Lines changed: 49 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
3 | 4 | | |
4 | 5 | | |
5 | 6 | | |
| |||
53 | 54 | | |
54 | 55 | | |
55 | 56 | | |
56 | | - | |
57 | | - | |
58 | | - | |
| 57 | + | |
59 | 58 | | |
60 | 59 | | |
61 | 60 | | |
| |||
65 | 64 | | |
66 | 65 | | |
67 | 66 | | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
68 | 92 | | |
69 | 93 | | |
70 | 94 | | |
| |||
85 | 109 | | |
86 | 110 | | |
87 | 111 | | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
88 | 128 | | |
89 | 129 | | |
90 | 130 | | |
91 | 131 | | |
92 | 132 | | |
93 | 133 | | |
94 | 134 | | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
95 | 141 | | |
96 | 142 | | |
97 | 143 | | |
| |||
Lines changed: 54 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
0 commit comments