Skip to content

Conversation

@zshannon
Copy link
Contributor

@zshannon zshannon commented Dec 3, 2025

Critical bug:

When prepareStep returns modified messages, the agent's system prompt (instructions) is silently dropped. This causes agents to ignore their instructions entirely.

Match AI SDK v5 behavior where system is handled separately from messages. When prepareStep returns messages without a system override, the original system messages are preserved instead of being lost.

Description

When prepareStep returns modified messages, Mastra was creating a new MessageList from those messages but losing the original system messages (instructions). This differed from AI SDK v5's behavior where the system prompt is handled separately and preserved by default.

The fix:

  • Preserves original system messages when prepareStep returns messages without a system override
  • Uses the system from prepareStep if explicitly provided (existing behavior)
  • Skips any system messages in the returned messages array since system is handled separately

Related Issue(s)

None

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Code refactoring
  • Performance improvement
  • Test update

Checklist

  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works

Summary by CodeRabbit

  • Bug Fixes

    • System messages are now preserved during step preparation when no system override is returned; an explicit system value will replace the original. Behavior now aligns with AI SDK v5.
  • Tests

    • Added tests verifying preservation and replacement of system messages during step preparation.

✏️ Tip: You can customize this high-level summary in your review settings.

Match AI SDK v5 behavior where system is handled separately from messages.
When prepareStep returns messages without a system override, the original
system messages are preserved instead of being lost.
@changeset-bot
Copy link

changeset-bot bot commented Dec 3, 2025

🦋 Changeset detected

Latest commit: c931e62

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
@mastra/core Patch
@mastra/mcp-docs-server Patch
@mastra/client-js Patch
@mastra/react Patch
@mastra/dane Patch
@mastra/longmemeval Patch
@mastra/playground-ui Patch
@mastra/server Patch
@mastra/deployer Patch
@mastra/deployer-cloud Patch
@mastra/express Patch
@mastra/hono Patch
mastra Patch
@mastra/deployer-cloudflare Patch
@mastra/deployer-netlify Patch
@mastra/deployer-vercel Patch
create-mastra Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link

vercel bot commented Dec 3, 2025

@zshannon is attempting to deploy a commit to the Mastra Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 3, 2025

Walkthrough

This PR patches @mastra/core to preserve existing system messages when prepareStep returns messages without a system override; adds tests and a changeset documenting the fix.

Changes

Cohort / File(s) Summary
Changeset documentation
/.changeset/fix-prepare-step-system.md
Adds a patch changeset for @mastra/core describing the fix: preserve system messages when prepareStep returns messages without a system override.
Tests
packages/core/src/loop/test-utils/options.ts
Adds two tests: one asserting original system messages persist when prepareStep omits a system override, and one asserting provided prepareStep.system replaces the system message.
Core implementation
packages/core/src/loop/workflows/agentic-execution/llm-execution-step.ts
Updates prepareStep handling to build a new MessageList that preserves existing system messages when prepareStep.system is undefined and replaces them when provided; skips system messages in per-message processing to avoid duplication.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

  • Focus review on llm-execution-step.ts conditional handling to ensure system preservation vs replacement is correct and ordering/duplication are handled.
  • Verify tests in options.ts cover both branches and that the changeset metadata matches release expectations.

Possibly related PRs

Suggested labels

cherry

Suggested reviewers

  • abhiaiyer91
  • DanielSLew

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix: preserving system messages when prepareStep returns messages, which aligns directly with the primary change in the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/core/src/loop/test-utils/options.ts (1)

1888-2037: New prepareStep/system tests accurately exercise the updated behavior

Both tests correctly validate the intended semantics:

  • When prepareStep only returns messages, the original system message from MessageList is preserved and the user message is transformed before reaching the model.
  • When prepareStep returns a system value, that system string cleanly overrides the original system prompt, while the rest of the conversation remains intact.

The way doStreamCalls is captured and asserted matches existing patterns in this file. One subtle behavior to be aware of (and which seems intentional with the current implementation) is that explicitly returning something falsy for system (e.g. null or '') will clear all system messages for that step; if that’s desired, no further changes are needed.

packages/core/src/loop/workflows/agentic-execution/llm-execution-step.ts (1)

559-585: System/message reconstruction logic matches the documented behavior

The new prepareStepResult branch correctly:

  • Rebuilds a fresh MessageList when either messages or system is provided.
  • Preserves existing system messages via getAllSystemMessages() when system is undefined.
  • Uses prepareStepResult.system as an explicit override (and allows clearing system instructions when it’s falsy but defined).
  • Filters out role === 'system' from newMessages so that system is consistently sourced from the dedicated system path, while preserving user/assistant/tool messages.

This aligns with the changeset description and the new tests, with no obvious correctness or type‑safety issues.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 491bd01 and b833031.

📒 Files selected for processing (3)
  • .changeset/fix-prepare-step-system.md (1 hunks)
  • packages/core/src/loop/test-utils/options.ts (2 hunks)
  • packages/core/src/loop/workflows/agentic-execution/llm-execution-step.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Run pnpm typecheck to validate TypeScript types across all packages

Files:

  • packages/core/src/loop/test-utils/options.ts
  • packages/core/src/loop/workflows/agentic-execution/llm-execution-step.ts
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Run pnpm prettier:format to format code and pnpm format to run linting with auto-fix across all packages

Files:

  • packages/core/src/loop/test-utils/options.ts
  • packages/core/src/loop/workflows/agentic-execution/llm-execution-step.ts
packages/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

packages/**/*.{ts,tsx}: All packages must use TypeScript with strict type checking enabled
Use telemetry decorators for observability across components

Files:

  • packages/core/src/loop/test-utils/options.ts
  • packages/core/src/loop/workflows/agentic-execution/llm-execution-step.ts
.changeset/*.md

⚙️ CodeRabbit configuration file

.changeset/*.md: Changeset files are really important for keeping track of changes in the project. They'll be used to generate release notes and inform users about updates.

Review the changeset file according to these guidelines:

  • The target audience are developers
  • Write short, direct sentences that anyone can understand. Avoid commit messages, technical jargon, and acronyms. Use action-oriented verbs (Added, Fixed, Improved, Deprecated, Removed)
  • Avoid generic phrases like "Update code", "Miscellaneous improvements", or "Bug fixes"
  • Highlight outcomes! What does change for the end user? Do not focus on internal implementation details
  • Add context like links to issues or PRs when relevant
  • If the change is a breaking change or is adding a new feature, ensure that a code example is provided. This code example should show the public API usage (the before and after). Do not show code examples of internal implementation details.
  • Keep the formatting easy-to-read and scannable. If necessary, use bullet points or multiple paragraphs (Use bold text as the heading for these sections, do not use markdown headings).
  • For larger, more substantial changes, also answer the "Why" behind the changes
  • Each changeset file contains a YAML frontmatter at the top. It will be one or more package names followed by a colon and the type of change (patch, minor, major). Do not modify this frontmatter. Check that the description inside the changeset file only applies to the packages listed in the frontmatter. Do not allow descriptions that mention changes to packages not listed in the frontmatter. In these cases, the user must create a separate changeset file for those packages.

Files:

  • .changeset/fix-prepare-step-system.md
🧠 Learnings (4)
📚 Learning: 2025-11-24T16:42:04.244Z
Learnt from: CR
Repo: mastra-ai/mastra PR: 0
File: packages/codemod/AGENTS.md:0-0
Timestamp: 2025-11-24T16:42:04.244Z
Learning: Applies to packages/codemod/src/test/__fixtures__/**/*.ts : Create test fixtures by copying examples DIRECTLY from migration guides in `docs/src/content/en/guides/migrations/upgrade-to-v1/` without hallucinating or inventing changes

Applied to files:

  • packages/core/src/loop/test-utils/options.ts
📚 Learning: 2025-11-24T16:42:04.244Z
Learnt from: CR
Repo: mastra-ai/mastra PR: 0
File: packages/codemod/AGENTS.md:0-0
Timestamp: 2025-11-24T16:42:04.244Z
Learning: Applies to packages/codemod/src/test/**/*.test.ts : Include test cases for multiple occurrences of the transformation pattern, aliased imports, type imports, and mixed imports to verify the codemod works consistently

Applied to files:

  • packages/core/src/loop/test-utils/options.ts
📚 Learning: 2025-11-24T16:42:04.244Z
Learnt from: CR
Repo: mastra-ai/mastra PR: 0
File: packages/codemod/AGENTS.md:0-0
Timestamp: 2025-11-24T16:42:04.244Z
Learning: Applies to packages/codemod/src/test/__fixtures__/**/*.ts : In output fixtures, ensure all NEGATIVE test cases remain EXACTLY IDENTICAL to their input fixture counterparts to verify the codemod only transforms intended patterns

Applied to files:

  • packages/core/src/loop/test-utils/options.ts
📚 Learning: 2025-11-24T16:42:04.244Z
Learnt from: CR
Repo: mastra-ai/mastra PR: 0
File: packages/codemod/AGENTS.md:0-0
Timestamp: 2025-11-24T16:42:04.244Z
Learning: Applies to packages/codemod/src/test/__fixtures__/**/*.ts : In input fixtures, include both POSITIVE test cases (patterns that should transform) and NEGATIVE test cases (unrelated code with similar names/patterns that should NOT transform)

Applied to files:

  • packages/core/src/loop/test-utils/options.ts
🧬 Code graph analysis (1)
packages/core/src/loop/workflows/agentic-execution/llm-execution-step.ts (1)
packages/core/src/agent/message-list/index.ts (1)
  • MessageList (107-3357)
🔇 Additional comments (2)
.changeset/fix-prepare-step-system.md (1)

5-9: Changeset description is clear and scoped correctly

The summary is concise, outcome‑focused, and correctly scoped to @mastra/core, describing the observable behavior change (system messages preserved and aligned with AI SDK v5). No changes needed.

packages/core/src/loop/test-utils/options.ts (1)

14-14: MessageList import usage looks correct

MessageList is used in the new tests to construct custom message lists, matching how the production class is intended to be exercised. No issues here.

@zshannon
Copy link
Contributor Author

zshannon commented Dec 4, 2025

@abhiaiyer91 this one is also a very tiny fix 🙇‍♂️ ty for merging my other one

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/core/src/loop/workflows/agentic-execution/llm-execution-step.ts (1)

559-585: System‑message preservation / override logic looks correct; consider clarifying the override check

The new block correctly:

  • Uses prepareStepResult.system as an explicit override (including allowing null/falsy to effectively clear system messages).
  • Preserves existing system messages when messages are provided without a system override.
  • Ignores any system‑role entries in prepareStepResult.messages, matching the “system handled separately” semantics.

This aligns with the PR’s stated behavior and AI SDK v5 expectations, and the use of messageList.getAllSystemMessages() ensures both untagged and tagged system messages are preserved.

If you want slightly sharper semantics around “explicit override vs. absence”, you could optionally distinguish property presence from value using an in check, which also avoids repeating the !== undefined test:

-                  if (prepareStepResult.messages || prepareStepResult.system !== undefined) {
-                    const newMessages = prepareStepResult.messages ?? messageList.get.all.aiV5.model();
-                    const newMessageList = new MessageList();
-
-                    if (prepareStepResult.system !== undefined) {
-                      if (prepareStepResult.system) {
-                        newMessageList.addSystem(prepareStepResult.system);
-                      }
-                    } else {
-                      for (const sysMsg of messageList.getAllSystemMessages()) {
-                        newMessageList.addSystem(sysMsg);
-                      }
-                    }
+                  const hasSystemOverride = 'system' in prepareStepResult;
+                  if (prepareStepResult.messages || hasSystemOverride) {
+                    const newMessages = prepareStepResult.messages ?? messageList.get.all.aiV5.model();
+                    const newMessageList = new MessageList();
+
+                    if (hasSystemOverride) {
+                      if (prepareStepResult.system) {
+                        newMessageList.addSystem(prepareStepResult.system);
+                      }
+                    } else {
+                      for (const sysMsg of messageList.getAllSystemMessages()) {
+                        newMessageList.addSystem(sysMsg);
+                      }
+                    }
                   }

Not strictly necessary, but it makes the “explicit override” intent a bit clearer for future readers. Overall, the fix itself looks solid.

Also applies to: 573-577

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b833031 and c931e62.

📒 Files selected for processing (2)
  • packages/core/src/loop/test-utils/options.ts (2 hunks)
  • packages/core/src/loop/workflows/agentic-execution/llm-execution-step.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/loop/test-utils/options.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Run pnpm typecheck to validate TypeScript types across all packages

Files:

  • packages/core/src/loop/workflows/agentic-execution/llm-execution-step.ts
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Run pnpm prettier:format to format code and pnpm format to run linting with auto-fix across all packages

Files:

  • packages/core/src/loop/workflows/agentic-execution/llm-execution-step.ts
packages/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

packages/**/*.{ts,tsx}: All packages must use TypeScript with strict type checking enabled
Use telemetry decorators for observability across components

Files:

  • packages/core/src/loop/workflows/agentic-execution/llm-execution-step.ts

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.

1 participant