|
| 1 | +# Plan: Per-model activity options (timeouts, retries) |
| 2 | + |
| 3 | +## Scope |
| 4 | + |
| 5 | +Today all `ActivityChatModel` instances share a single |
| 6 | +`DEFAULT_TIMEOUT = Duration.ofMinutes(2)` and `DEFAULT_MAX_ATTEMPTS = 3`. |
| 7 | +With multiple chat models registered via `SpringAiPlugin` (e.g. a fast 4o-mini |
| 8 | +and a slow reasoning model), one timeout does not fit both. The integration |
| 9 | +guide explicitly calls out: *"you may wish to specify a different |
| 10 | +`start_to_close_timeout` depending on the model, and for example, whether it |
| 11 | +is in thinking mode or not."* |
| 12 | + |
| 13 | +## Design |
| 14 | + |
| 15 | +Introduce a per-model options registry on `SpringAiPlugin` that |
| 16 | +`ActivityChatModel.forModel(name)` consults when building its stub. |
| 17 | + |
| 18 | +Key choice: the options are registered **on the plugin** (worker-side) but |
| 19 | +must be resolved **in the workflow** when `Workflow.newActivityStub(...)` is |
| 20 | +called. Because the plugin is a worker-side object, we either need to: |
| 21 | + |
| 22 | +- (A) Ship the options map across the serialization boundary — awkward, |
| 23 | + `ActivityOptions` isn't trivially serializable. |
| 24 | +- (B) Expose a registry lookup on the plugin via a static accessor that |
| 25 | + `ActivityChatModel.forModel` calls. Workers run both workflow and plugin in |
| 26 | + the same JVM, and `ActivityChatModel.forModel` is already called from |
| 27 | + workflow code — it can safely read a static registry populated at plugin |
| 28 | + construction time. |
| 29 | + |
| 30 | +Go with (B). Plugin construction happens before any workflow executes on the |
| 31 | +worker, so the registry is populated in time. |
| 32 | + |
| 33 | +## Files to change |
| 34 | + |
| 35 | +- `src/main/java/io/temporal/springai/plugin/SpringAiPlugin.java` |
| 36 | + - Accept an optional `Map<String, ActivityOptions> perModelOptions` on a |
| 37 | + new constructor / builder. |
| 38 | + - On construction, publish the map to a package-private static |
| 39 | + `SpringAiPluginOptions.register(map)` (or similar). Clearing on plugin |
| 40 | + shutdown is nice-to-have but not strictly required for a worker lifecycle. |
| 41 | + |
| 42 | +- New: `src/main/java/io/temporal/springai/plugin/SpringAiPluginOptions.java` |
| 43 | + - Thread-safe registry: `register(Map)`, `optionsFor(String modelName)`, |
| 44 | + returns `Optional<ActivityOptions>`. |
| 45 | + |
| 46 | +- `src/main/java/io/temporal/springai/model/ActivityChatModel.java` |
| 47 | + - `forModel(String modelName)` checks `SpringAiPluginOptions.optionsFor( |
| 48 | + modelName)`. If present, use those. Otherwise fall back to the existing |
| 49 | + default (2 min, 3 attempts). |
| 50 | + - Caller-supplied `ActivityOptions` (from the overload added in the |
| 51 | + `spring-ai/retry-and-options` branch) always wins over the registry. |
| 52 | + |
| 53 | +- `src/main/java/io/temporal/springai/autoconfigure/SpringAiTemporalAutoConfiguration.java` |
| 54 | + - Accept a `Map<String, ActivityOptions>` bean if present |
| 55 | + (`ObjectProvider`) and forward it to the plugin constructor. |
| 56 | + |
| 57 | +## Config example (documented in the PR body) |
| 58 | + |
| 59 | +```java |
| 60 | +@Bean |
| 61 | +Map<String, ActivityOptions> springAiActivityOptions() { |
| 62 | + return Map.of( |
| 63 | + "reasoning", ActivityOptions.newBuilder() |
| 64 | + .setStartToCloseTimeout(Duration.ofMinutes(15)) |
| 65 | + .setHeartbeatTimeout(Duration.ofMinutes(1)) |
| 66 | + .build(), |
| 67 | + "fast", ActivityOptions.newBuilder() |
| 68 | + .setStartToCloseTimeout(Duration.ofSeconds(20)) |
| 69 | + .build()); |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +## Coordination with other branches |
| 74 | + |
| 75 | +This branch layers on top of `spring-ai/retry-and-options`, which adds the |
| 76 | +`forModel(name, ActivityOptions)` overload. If that branch merges first, |
| 77 | +this one depends on the new overload. If this branch merges first, implement |
| 78 | +the overload here and the retry branch will rebase cleanly. |
| 79 | + |
| 80 | +## Test plan |
| 81 | + |
| 82 | +- Unit test: register a plugin with `perModelOptions = {"slow": 10min}`, |
| 83 | + build an `ActivityChatModel.forModel("slow")` in a workflow, and assert |
| 84 | + (via test env history inspection) the scheduled activity's |
| 85 | + `startToCloseTimeout` is 10 min, not 2 min. |
| 86 | +- Negative: `forModel("unknown-name")` falls back to default 2 min. |
| 87 | +- Explicit override: caller passes `ActivityOptions` — registry is ignored. |
| 88 | + |
| 89 | +## PR |
| 90 | + |
| 91 | +**Title:** `temporal-spring-ai: per-model ActivityOptions registry` |
| 92 | + |
| 93 | +**Body:** |
| 94 | + |
| 95 | +``` |
| 96 | +## What was changed |
| 97 | +- `SpringAiPlugin` accepts an optional `Map<String, ActivityOptions>` |
| 98 | + keyed by chat-model name. |
| 99 | +- `ActivityChatModel.forModel(name)` consults that registry; an |
| 100 | + explicit `ActivityOptions` argument still takes precedence. |
| 101 | +- Auto-configuration forwards a user-provided |
| 102 | + `Map<String, ActivityOptions>` bean into the plugin. |
| 103 | +
|
| 104 | +## Why? |
| 105 | +A single default (2 min start-to-close, 3 attempts) doesn't fit every |
| 106 | +model. Reasoning and thinking-mode models routinely need 10+ minutes; |
| 107 | +fast models want shorter timeouts so retries recover quickly. |
| 108 | +The Temporal AI integration guide specifically calls this out as a |
| 109 | +capability partners should expose. With this change, users register |
| 110 | +one bean and never have to hand-build activity stubs again. |
| 111 | +``` |
0 commit comments