Skip to content

Commit ed07df3

Browse files
TylerBarnesMastra Code (opencode/kimi-k2.6)
andauthored
fix(core): remove saveStepMessages and disable savePerStep when observational memory is enabled (#15652)
Co-authored-by: Mastra Code (opencode/kimi-k2.6) <noreply@mastra.ai>
1 parent 8322484 commit ed07df3

9 files changed

Lines changed: 14 additions & 62 deletions

File tree

.changeset/puny-actors-vanish.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@mastra/core': patch
3+
---
4+
5+
Fixed interaction between savePerStep and observational memory that caused message duplication. The saveStepMessages method redundantly re-added response messages to the message list on every step, duplicating them. Additionally, savePerStep is now force-disabled when observational memory is enabled, since OM handles its own per-step persistence and the two features conflict.

docs/src/content/en/reference/agents/generate.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,8 @@ const response = await agent.generate('Help me organize my day', {
766766
name: 'savePerStep',
767767
type: 'boolean',
768768
isOptional: true,
769-
description: 'Save messages incrementally after each generation step completes (default: false).',
769+
description:
770+
'Save messages incrementally after each generation step completes (default: false). Disabled internally when observational memory is enabled.',
770771
},
771772
{
772773
name: 'providerOptions',

docs/src/content/en/reference/agents/generateLegacy.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,8 @@ await agent.generateLegacy('message for agent')
336336
name: 'savePerStep',
337337
type: 'boolean',
338338
isOptional: true,
339-
description: 'Save messages incrementally after each stream step completes (default: false).',
339+
description:
340+
'Save messages incrementally after each generation step completes (default: false). Disabled internally when observational memory is enabled.',
340341
},
341342
{
342343
name: 'providerOptions',

packages/core/src/agent/agent-legacy.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,6 @@ export interface AgentLegacyCapabilities {
165165
instructions?: DynamicArgument<string>;
166166
minMessages?: number;
167167
};
168-
/** Save step messages */
169-
saveStepMessages(args: { result: any; messageList: MessageList; runId: string }): Promise<void>;
170168
/** Convert instructions to string */
171169
convertInstructionsToString(instructions: AgentInstructions): string;
172170
/** Options for tracing policy */
@@ -834,12 +832,6 @@ export class AgentLegacyHandler {
834832
});
835833
threadCreatedByStep = true;
836834
}
837-
838-
await this.capabilities.saveStepMessages({
839-
result: props,
840-
messageList,
841-
runId,
842-
});
843835
}
844836

845837
return onStepFinish?.({ ...props, runId });

packages/core/src/agent/agent.ts

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,7 +1412,6 @@ export class Agent<
14121412
getMostRecentUserMessage: this.getMostRecentUserMessage.bind(this),
14131413
genTitle: this.genTitle.bind(this),
14141414
resolveTitleGenerationConfig: this.resolveTitleGenerationConfig.bind(this),
1415-
saveStepMessages: this.saveStepMessages.bind(this),
14161415
convertInstructionsToString: this.#convertInstructionsToString.bind(this),
14171416
tracingPolicy: this.#options?.tracingPolicy,
14181417
resolvedVersionId: this.toRawConfig()?.resolvedVersionId as string | undefined,
@@ -4530,47 +4529,6 @@ export class Agent<
45304529
return tools;
45314530
}
45324531

4533-
/**
4534-
* Adds response messages from a step to the MessageList and schedules persistence.
4535-
* This is used for incremental saving: after each agent step, messages are added to a save queue
4536-
* and a debounced save operation is triggered to avoid redundant writes.
4537-
*
4538-
* @param result - The step result containing response messages.
4539-
* @param messageList - The MessageList instance for the current thread.
4540-
* @param threadId - The thread ID.
4541-
* @param memoryConfig - The memory configuration for saving.
4542-
* @param runId - (Optional) The run ID for logging.
4543-
* @internal
4544-
*/
4545-
private async saveStepMessages({
4546-
result,
4547-
messageList,
4548-
runId,
4549-
}: {
4550-
result: any;
4551-
messageList: MessageList;
4552-
runId?: string;
4553-
}) {
4554-
try {
4555-
// Prefer dbMessages (MastraDBMessage[] with original IDs) over response.messages
4556-
// (ModelMessage[] without IDs) to avoid generating new IDs during format conversion
4557-
const stepResponseMessages = result.response.dbMessages?.length
4558-
? result.response.dbMessages
4559-
: result.response.messages;
4560-
if (stepResponseMessages?.length) {
4561-
messageList.add(stepResponseMessages, 'response');
4562-
}
4563-
// Message saving is now handled by MessageHistory output processor
4564-
} catch (e) {
4565-
this.logger.error('Error adding messages on step finish', {
4566-
agent: this.name,
4567-
error: e,
4568-
runId,
4569-
});
4570-
throw e;
4571-
}
4572-
}
4573-
45744532
async #runScorers({
45754533
messageList,
45764534
runId,
@@ -5013,7 +4971,6 @@ export class Agent<
50134971
'_agentNetworkAppend' in this
50144972
? Boolean((this as unknown as { _agentNetworkAppend: unknown })._agentNetworkAppend)
50154973
: undefined,
5016-
saveStepMessages: this.saveStepMessages.bind(this),
50174974
convertTools: this.convertTools.bind(this),
50184975
getMemoryMessages: this.getMemoryMessages.bind(this),
50194976
runInputProcessors: this.__runInputProcessors.bind(this),

packages/core/src/agent/agent.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ export type AgentExecutionOptionsBase<OUTPUT> = {
450450
/** Unique identifier for this execution run */
451451
runId?: string;
452452

453-
/** Save messages incrementally after each stream step completes (default: false). */
453+
/** Save messages incrementally after each stream step completes (default: false). Is disabled internally when observational memory is enabled, as OM handles its own message saving */
454454
savePerStep?: boolean;
455455

456456
/** Request Context containing dynamic configuration and state */

packages/core/src/agent/workflows/prepare-stream/map-results-step.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ export function createMapResultsStep<OUTPUT = undefined>({
7676
requestContext,
7777
messageList: memoryData.messageList,
7878
onStepFinish: async (props: any) => {
79-
if (options.savePerStep && !memoryConfig?.readOnly) {
79+
// When OM is enabled saving per step corrupts things because OM handles its own saving
80+
const shouldSavePerStep = options.savePerStep && !memoryConfig?.observationalMemory;
81+
if (shouldSavePerStep && !memoryConfig?.readOnly) {
8082
if (!memoryData.threadExists && !threadCreatedByStep && memory && memoryData.thread) {
8183
await memory.createThread({
8284
threadId: memoryData.thread?.id,
@@ -89,12 +91,6 @@ export function createMapResultsStep<OUTPUT = undefined>({
8991
threadCreatedByStep = true;
9092
}
9193

92-
await capabilities.saveStepMessages({
93-
result: props,
94-
messageList: memoryData.messageList!,
95-
runId,
96-
});
97-
9894
if (saveQueueManager && memoryData.thread?.id) {
9995
await saveQueueManager.flushMessages(memoryData.messageList!, memoryData.thread.id, memoryConfig);
10096
}

packages/core/src/agent/workflows/prepare-stream/schema.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export type AgentCapabilities = {
2222
generateMessageId: Mastra['generateId'];
2323
mastra?: Mastra;
2424
_agentNetworkAppend?: boolean;
25-
saveStepMessages: Agent['saveStepMessages'];
2625
convertTools: Agent['convertTools'];
2726
runInputProcessors: Agent['__runInputProcessors'];
2827
executeOnFinish: (args: AgentExecuteOnFinishOptions) => Promise<void>;

packages/core/src/harness/harness.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,7 @@ export class Harness<TState = {}> {
13281328
maxSteps: 1000,
13291329
// Harness supports suspending + resuming streams (tool approvals, tool suspensions, workflows).
13301330
// Persisting per-step snapshots ensures `resumeStream()` can load state reliably (especially in CI).
1331+
// Doesn't do anything when OM is enabled though, OM does its own saving per step
13311332
savePerStep: true,
13321333
requireToolApproval: !isYolo,
13331334
modelSettings: { temperature: 1 },

0 commit comments

Comments
 (0)