Skip to content

Commit d3351a3

Browse files
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>
1 parent a33c9e0 commit d3351a3

2 files changed

Lines changed: 21 additions & 26 deletions

File tree

temporal-spring-ai/src/main/java/io/temporal/springai/model/ActivityChatModel.java

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -236,29 +236,19 @@ private ChatModelActivity stubForCall(Prompt prompt) {
236236
return chatModelActivity;
237237
}
238238
ActivityOptions withSummary =
239-
ActivityOptions.newBuilder(baseOptions).setSummary(buildSummary(prompt)).build();
239+
ActivityOptions.newBuilder(baseOptions).setSummary(buildSummary()).build();
240240
return Workflow.newActivityStub(ChatModelActivity.class, withSummary);
241241
}
242242

243-
private String buildSummary(Prompt prompt) {
243+
/**
244+
* Builds the activity Summary. Intentionally omits the user prompt — including even a truncated
245+
* slice would leak whatever the prompt contains (PII, secrets, internal identifiers) into
246+
* workflow history, server logs, and the Temporal UI, which is a surprising default for a plain
247+
* observability label.
248+
*/
249+
private String buildSummary() {
244250
String label = modelName != null ? modelName : "default";
245-
String userText = lastUserText(prompt);
246-
if (userText == null || userText.isEmpty()) {
247-
return "chat: " + label;
248-
}
249-
String truncated = userText.length() > 60 ? userText.substring(0, 60) + "…" : userText;
250-
return "chat: " + label + " · " + truncated.replace('\n', ' ');
251-
}
252-
253-
private String lastUserText(Prompt prompt) {
254-
List<Message> instructions = prompt.getInstructions();
255-
for (int i = instructions.size() - 1; i >= 0; i--) {
256-
Message m = instructions.get(i);
257-
if (m.getMessageType() == MessageType.USER) {
258-
return m.getText();
259-
}
260-
}
261-
return null;
251+
return "chat: " + label;
262252
}
263253

264254
private ChatModelTypes.ChatModelActivityInput createActivityInput(Prompt prompt) {

temporal-spring-ai/src/test/java/io/temporal/springai/ActivitySummaryTest.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.temporal.springai;
22

3+
import static org.junit.jupiter.api.Assertions.assertEquals;
34
import static org.junit.jupiter.api.Assertions.assertNotNull;
45
import static org.junit.jupiter.api.Assertions.assertTrue;
56

@@ -49,7 +50,7 @@ void tearDown() {
4950
}
5051

5152
@Test
52-
void chatActivity_carriesSummaryWithModelAndUserPrompt() {
53+
void chatActivity_carriesModelOnlySummary_neverLeaksUserPrompt() {
5354
Worker worker = testEnv.newWorker(TASK_QUEUE);
5455
worker.registerWorkflowImplementationTypes(ChatWorkflowImpl.class);
5556
worker.registerActivitiesImplementations(
@@ -60,19 +61,23 @@ void chatActivity_carriesSummaryWithModelAndUserPrompt() {
6061
client.newWorkflowStub(
6162
SummaryTestWorkflow.class,
6263
WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).build());
63-
workflow.chat("What is the capital of France?");
64+
String prompt = "What is the capital of France?";
65+
workflow.chat(prompt);
6466

6567
String workflowId = WorkflowStub.fromTyped(workflow).getExecution().getWorkflowId();
6668
List<HistoryEvent> events = client.fetchHistory(workflowId).getHistory().getEventsList();
6769

6870
String summary = findChatActivitySummary(events);
6971
assertNotNull(summary, "ActivityTaskScheduled event for callChatModel should have a Summary");
72+
assertEquals(
73+
"chat: default",
74+
summary,
75+
"Summary must be just the model label — user prompts can contain PII and must not"
76+
+ " appear in history/logs/UI by default.");
77+
// Defense in depth: explicitly confirm no part of the prompt leaked into the summary.
7078
assertTrue(
71-
summary.startsWith("chat: default"),
72-
"Summary should start with 'chat: default' but was: " + summary);
73-
assertTrue(
74-
summary.contains("What is the capital of France?"),
75-
"Summary should contain the user prompt but was: " + summary);
79+
!summary.contains(prompt) && !summary.contains("France"),
80+
"Summary must not include user prompt content, got: " + summary);
7681
}
7782

7883
private static String findChatActivitySummary(List<HistoryEvent> events) {

0 commit comments

Comments
 (0)