Skip to content

temporal-spring-ai: attach activity summaries for chat and MCP calls#2852

Open
donald-pinckney wants to merge 9 commits intomasterfrom
spring-ai/activity-summaries
Open

temporal-spring-ai: attach activity summaries for chat and MCP calls#2852
donald-pinckney wants to merge 9 commits intomasterfrom
spring-ai/activity-summaries

Conversation

@donald-pinckney
Copy link
Copy Markdown
Contributor

@donald-pinckney donald-pinckney commented Apr 21, 2026

What was changed

  • ActivityChatModel attaches an activity Summary of the form chat: <model> (model label only — user prompts may contain PII, so they are deliberately not included) to each chat-model activity call.
  • ActivityMcpClient gains a callTool(clientName, request, summary) overload; McpToolCallback uses it to attach mcp: <client>.<tool>.
  • Summaries are applied only when the plugin itself created the activity stub (via the existing forModel / create factories). When the user supplies their own pre-built stub to the public constructors, behavior is unchanged, as they are non-generic tools that are up to the user to annotate.
  • New ActivitySummaryTest runs a workflow, fetches history, and asserts the ActivityTaskScheduled event for CallChatModel carries the expected userMetadata.summary.

Activity-backed tool calls, Nexus tool calls, and @SideEffectTool calls are intentionally out of scope — either the stub is user-owned with no clean per-call option override (activity and Nexus tools), or there is no Summary slot on the resulting history event (@SideEffectTool writes a MarkerRecorded, not an ActivityTaskScheduled).

Why?

Without Summaries, every chat and MCP step in a Spring AI workflow shows up as an opaque callChatModel or callTool row in the Temporal UI, making multi-step agents hard to follow. Chat and MCP are the cases where the plugin owns stub creation end-to-end, so adding the Summary is a cheap, low-risk overlay; broader coverage would require reaching into user-owned stubs and was deliberately left out to keep this change small and review-friendly.

donald-pinckney and others added 4 commits April 21, 2026 15:52
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
donald-pinckney and others added 2 commits April 22, 2026 12:06
Planning scratchpad — not part of the shipped artifact. Removed before merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
@donald-pinckney donald-pinckney marked this pull request as ready for review April 22, 2026 19:49
@donald-pinckney donald-pinckney requested a review from a team as a code owner April 22, 2026 19:49
* @param activity the activity stub for MCP operations
*/
public ActivityMcpClient(McpClientActivity activity) {
this(activity, null);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I've never been a fan of throwing around raw nulls in Java. This is likely a hangover of mine from Google days where Java style is very heavily anti-null (e.g. Guava suggests avoiding null though after Java 8 Guava's Optional type isn't needed, can just use builtin java.util.Optional).

But I'd prefer not using nullable params or fields, and instead explicitly marking them as Optional<Foo>. Barring that, you should probably look into marking things with @Nullable if you plan to allow that. See this blog on Spring Null Safety

I'm not sure how standard this type of null usage has been across the Spring codebase though, so in the end, this can be a judgement call you make.

Same comment applies across all the files in this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code style wise, sdk-java uses a mix of Nullable annotations and Optional. And my preference would be Optional as well. PR has been updated, all nulls have been changed to Optionals, except for the ChatModelActivityInput which I'll defer a decision on for a later PR.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome, thanks. LGTM

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>
Comment thread temporal-spring-ai/src/main/java/io/temporal/springai/mcp/ActivityMcpClient.java Outdated
donald-pinckney and others added 2 commits April 22, 2026 20:39
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants