Skip to content

fix(harness): eliminate agent forking — use single shared agent across modes#18008

Merged
abhiaiyer91 merged 7 commits into
mainfrom
devin/1781610039-fix-signal-provider-fork-memory
Jun 16, 2026
Merged

fix(harness): eliminate agent forking — use single shared agent across modes#18008
abhiaiyer91 merged 7 commits into
mainfrom
devin/1781610039-fix-signal-provider-fork-memory

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Description

Eliminates __fork() from the shared-agent path in Harness. Instead of creating per-mode agent clones, the harness reuses config.agent directly and never mutates its instructions or tools.

Root cause: PR #17892 introduced shared harness modes that fork the base agent per-mode. Signal providers connected to the base agent during construction skip re-connection on forks (isConnected guard), so they remain pointed at the original memoryless base agent. When GitHubSignals (or any signal provider) tries to deliver notifications, it crashes:

[Processor:task-state] computeStateSignal requires Mastra memory
  with an active resourceId and threadId

Fix — no more forking, no agent mutation:

 # getAgentForMode() — shared agent path
-  const forkedAgent = this.config.agent.__fork();
-  forkedAgent.__updateInstructions(instructions);
-  forkedAgent.__setTools(modeTools);
-  this.#legacyAgentMode[mode.id] = forkedAgent;
+  return this.config.agent;  // single instance, no fork, no mutation
  • getAgentForMode(): returns config.agent directly — no fork, no instructions/tools mutation
  • buildAgentMessageStreamOptions(): resolves mode instructions at call time via resolveCurrentModeInstructions() and passes them as the instructions stream option — the agent's own instructions are never touched
  • buildToolsets(): mode-specific tools (mode.tools / mode.additionalTools) are delivered through toolsets — the agent's own tools (including signal-provider tools) are never replaced
  • init(): propagates runtime services (memory, storage, etc.) once to the shared agent — signal providers connected during construction retain full access
  • setBrowser(): sets browser on the single agent instead of iterating forks
  • Deprecated mode.agent and no-backing-agent construction paths are unchanged

Related issue(s)

Reported in Slack #mastracode by Tyler Barnes. Regression from #17892 (shared harness modes via __fork()).

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • Code refactoring

Checklist

  • I have linked the related issue(s) in the description above
  • 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
  • I have addressed all Coderabbit comments on this PR

Link to Devin session: https://app.devin.ai/sessions/a7472d903dcd4982bbed590b0bde4c1c

ELI5

Harness can run “agent work” in different modes (like different capabilities). A bug made Harness create separate agent copies per mode, but some notification components (signal providers) kept using the original agent that never got the required memory/services—so notifications crashed. This PR stops the per-mode agent cloning and instead reuses one shared agent, wiring mode-specific instructions/tools at runtime so the shared agent has memory and signals work again.

Summary

Fixes a regression where Harness signal providers (e.g. GitHubSignals) could crash because they were still connected to a base Agent that lacked Mastra runtime services (especially memory) after PR #17892 introduced per-mode agent forking/cloning.

Key changes

  • Removed agent forking/cloning for the shared-agent (config.agent) path
    • getAgentForMode() returns config.agent directly (no per-mode cloning).
    • The deprecated mode.agent path remains supported via its existing per-mode caching behavior (agents can still be independent there).
  • Mode instructions are applied dynamically at call time (no mutation of the shared agent)
    • resolveCurrentModeInstructions() computes harness-level + current mode instructions on demand.
    • buildAgentMessageStreamOptions() passes these as the instructions stream option, instead of updating the agent’s own instructions (avoids cross-mode instruction bleed).
  • Mode tools are delivered via toolsets (no replacement of the shared agent’s tools)
    • buildToolsets() supplies mode tool overrides at the toolset level (via result.modeTools) for the shared-agent path, rather than baking/replacing tools on the shared agent.
  • Runtime services and browser propagation happen once for the shared agent
    • init() propagates runtime services (memory/storage/workspace/browser/pubsub, etc.) once to the shared config.agent.
    • setBrowser() sets the browser on the single shared agent.
  • Deprecated/no-backing-agent behavior remains unchanged
    • When there is no shared backing agent, the harness continues constructing/caching per-mode agents with combined harness+mode instructions and merged mode tools, as before.

Tests

Extended packages/core/src/harness/fork-clone-metadata.test.ts to cover:

  • The same config.agent instance is reused across mode switches (no forking).
  • The shared agent’s own instruction text is not mutated across mode switches.
  • Mode-specific instructions are resolved dynamically at call time.
  • Mode tools appear/disappear correctly in generated toolsets while using the shared-agent path.
  • SignalProvider-based regression coverage: a connected base/shared agent retains access to runtime services (memory) after harness.init(), staying properly connected across mode switches.
  • A regression test confirming the deprecated mode.agent path still keeps agents independent per mode.

Changeset

  • Added a patch changeset for @mastra/core documenting that signal providers no longer lose memory access when Harness previously forked agents per mode.

…iders retain memory

When Harness forks the config.agent into per-mode agents, it propagates memory
and storage only to the forks. Signal providers connected to the base agent
during construction remain pointed at the memoryless original instance, causing
notification delivery to fail (e.g. GitHubSignals crash with
'computeStateSignal requires Mastra memory').

Now Harness.init() also propagates runtime services to config.agent so signal
providers connected to it have full access to memory and storage.

Co-Authored-By: Abhi Aiyer <abhi@mastra.ai>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@changeset-bot

changeset-bot Bot commented Jun 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4bc2d9d

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

This PR includes changesets to release 23 packages
Name Type
@mastra/core Patch
mastracode Patch
@mastra/mcp-docs-server Patch
@internal/playground Patch
@mastra/client-js Patch
@mastra/opencode Patch
@mastra/longmemeval Patch
mastra Patch
@mastra/deployer-cloud Patch
@mastra/deployer-vercel Patch
@mastra/playground-ui Patch
@mastra/react Patch
@mastra/server Patch
@mastra/deployer Patch
create-mastra Patch
@mastra/express Patch
@mastra/fastify Patch
@mastra/hono Patch
@mastra/koa Patch
@mastra/nestjs Patch
@mastra/deployer-cloudflare Patch
@mastra/deployer-netlify Patch
@mastra/temporal 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

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mastra-playground-ui Ready Ready Preview, Comment Jun 16, 2026 1:34pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
mastra-docs-1.x Skipped Skipped Jun 16, 2026 1:34pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Harness refactors mode agent handling to reuse a single config.agent across all modes instead of forking per-mode agents. Runtime services and browser propagate once to the shared backing agent when available, falling back to per-mode propagation otherwise. Mode-specific instructions and tools are now applied at call time via dynamic injection and modeTools routing. Tests verify agent reuse across mode switches and signal provider memory access after initialization.

Changes

Harness mode agent sharing and runtime propagation

Layer / File(s) Summary
Agent mode resolution and shared backing agent reuse
packages/core/src/harness/harness.ts
Refactors getAgentForMode() to reuse a single config.agent across all modes instead of forking per-mode. Deprecated mode.agent instances are cached per-mode; when config.agent exists it is returned for all modes; when no backing agent exists, constructs and caches per-mode agents with combined instructions and merged tools.
Runtime service and browser propagation routing
packages/core/src/harness/harness.ts
init() and setBrowser() now propagate runtime services and browser to config.agent once when a shared backing agent is present, falling back to per-mode propagation when no shared agent is configured.
Call-time instruction and toolset handling
packages/core/src/harness/harness.ts
buildAgentMessageStreamOptions() injects mode-aware instructions into per-call stream options when a shared backing agent is configured. buildToolsets() returns ToolsetsInput with harnessBuiltIn always set, conditionally includes resolved harness tools, and returns current-mode tool overrides via modeTools for shared agent configurations.
Shared agent reuse and mode-switch tests
packages/core/src/harness/fork-clone-metadata.test.ts
Imports SignalProvider and adds test coverage verifying a single config.agent instance is reused across modes without forking, that agent instructions are not mutated by mode switches, that mode instructions resolve at call time, and that mode-specific tools appear/disappear in toolsets appropriately. Includes regression test for deprecated per-mode mode.agent path.
SignalProvider memory and base agent connection tests
packages/core/src/harness/fork-clone-metadata.test.ts
Adds SignalProvider-focused tests verifying that signal providers stay connected to the same base agent across mode switches with memory access, and that base agents gain memory access after harness.init() propagates runtime services.
Release notes
.changeset/clear-buses-judge.md
Documents patch release for @mastra/core noting signal providers now retain memory access when harness propagates runtime services to shared backing agent during initialization.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • mastra-ai/mastra#17892: Modifies harness mode typing and agent reuse architecture to switch modes to reuse a shared backing config.agent instead of per-mode forking, directly overlapping with this PR's agent-sharing refactor.
  • mastra-ai/mastra#17990: Both PRs modify buildAgentMessageStreamOptions() in packages/core/src/harness/harness.ts to handle shared agent configurations; this PR injects mode-specific instructions at call time while the related PR centralizes step-budget settings.
  • mastra-ai/mastra#16665: Both PRs update packages/core/src/harness/harness.ts to route PubSub and runtime service propagation through a shared backing agent rather than per-mode behavior.

Suggested labels

complexity: medium

Suggested reviewers

  • wardpeet
  • dane-ai-mastra
  • TylerBarnes
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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: eliminating agent forking to use a single shared agent across modes, which directly addresses the core change in the pull request.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1781610039-fix-signal-provider-fork-memory

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

@dane-ai-mastra dane-ai-mastra Bot added the complexity: low Low-complexity PR label Jun 16, 2026
@superagent-security

Copy link
Copy Markdown

Superagent didn't find any vulnerabilities or security issues in this PR.

@dane-ai-mastra

dane-ai-mastra Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

PR triage

Linked issue check skipped for excluded profile @devin-ai-integration[bot].


PR complexity score

Factor Value Score impact
Files changed 3 +6
Lines changed 483 +27
Author merged PRs 153 -20
Test files changed Yes -10
Final score 3

Applied label: complexity: low


Changed test gate

Changed tests failed against the base branch as expected.

Label: tests: green ✅

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/harness/fork-clone-metadata.test.ts`:
- Around line 215-219: The test file
packages/core/src/harness/fork-clone-metadata.test.ts uses concrete model IDs
like 'gpt-4o' and 'openai/gpt-4o' instead of registered placeholder tokens.
Replace all literal model IDs in the baseAgent creation (where model provider is
set to 'openai' with name 'gpt-4o') and at the other affected location (line
238-239) with the appropriate placeholder tokens defined in
docs/src/plugins/remark-model-tokens/models.ts according to the repo's coding
guidelines for model names in tests and examples.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5b7c534c-0be2-4cb3-8071-d691ac084bb7

📥 Commits

Reviewing files that changed from the base of the PR and between 641d50a and ffac9f8.

📒 Files selected for processing (3)
  • .changeset/clear-buses-judge.md
  • packages/core/src/harness/fork-clone-metadata.test.ts
  • packages/core/src/harness/harness.ts

Comment on lines +215 to +219
const baseAgent = new Agent({
name: 'base',
instructions: 'base agent',
model: { provider: 'openai', name: 'gpt-4o', toolChoice: 'auto' },
signals: [signalProvider],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

potential_issue: Replace newly added literal model IDs with registered placeholder tokens.

This test adds concrete model IDs (gpt-4o, openai/gpt-4o) instead of the tokenized placeholders required by repo policy.

As per coding guidelines, “When adding model names or IDs to examples, changesets, tests, or comments, use placeholder tokens from docs/src/plugins/remark-model-tokens/models.ts”.

Also applies to: 238-239

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/harness/fork-clone-metadata.test.ts` around lines 215 -
219, The test file packages/core/src/harness/fork-clone-metadata.test.ts uses
concrete model IDs like 'gpt-4o' and 'openai/gpt-4o' instead of registered
placeholder tokens. Replace all literal model IDs in the baseAgent creation
(where model provider is set to 'openai' with name 'gpt-4o') and at the other
affected location (line 238-239) with the appropriate placeholder tokens defined
in docs/src/plugins/remark-model-tokens/models.ts according to the repo's coding
guidelines for model names in tests and examples.

Source: Coding guidelines

@dane-ai-mastra dane-ai-mastra Bot added the tests: green ✅ Changed tests failed against base as expected label Jun 16, 2026
…s modes

Instead of __fork()-ing config.agent per mode (which left signal providers
connected to a memoryless base instance), the harness now reuses config.agent
directly:

- getAgentForMode() returns config.agent and applies mode instructions in place
- Mode tools are delivered via buildToolsets() instead of __setTools() on forks
- init() propagates runtime services (memory, storage, etc.) once to the shared
  agent — signal providers connected during construction retain full access
- setBrowser() sets browser on the single agent instead of iterating forks

The deprecated mode.agent path and the no-backing-agent construction path are
unchanged.

Co-Authored-By: Abhi Aiyer <abhi@mastra.ai>
@vercel vercel Bot temporarily deployed to Preview – mastra-docs-1.x June 16, 2026 11:58 Inactive
@devin-ai-integration devin-ai-integration Bot changed the title fix(harness): propagate runtime services to base agent so signal providers retain memory fix(harness): eliminate agent forking — use single shared agent across modes Jun 16, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/harness/harness.ts`:
- Around line 570-576: The code currently propagates browser and runtime updates
only to `config.agent` when it exists, but fails to update explicit deprecated
`mode.agent` instances that may coexist in mixed configurations. In the
setBrowser method around lines 570-576 (the if-else block handling
config.agent), after updating the shared config.agent, also iterate through
this.config.modes and call setBrowser on any explicit mode.agent instances.
Apply the same logic to the init method at lines 639-650, ensuring that both
setBrowser and runtime initialization propagate to explicit mode.agent instances
to keep signal providers properly connected to backing agents in mixed mode
configurations.
- Around line 4283-4288: The code in the conditional block at lines 4283-4288
does not distinguish between replacement semantics (when mode.tools is used) and
augmentation semantics (when additionalTools is used), yet validateModes()
enforces this distinction. When currentMode.tools is defined instead of
currentMode.additionalTools, it indicates the mode intends to replace (restrict)
the agent's tools, but the current code just adds them to modeTools while
preserving the shared agent's own tools. Fix this by explicitly checking whether
currentMode.tools (replacement intent) or currentMode.additionalTools (augment
intent) is being used, and either implement a per-run tool mask to enforce the
replacement semantics, or reject/throw an error when mode.tools is used with a
shared agent to prevent unintended tool exposure until replacement enforcement
can be properly implemented.
- Around line 930-936: The applyModeInstructions method has a logic issue where
switching to a mode without instructions skips the __updateInstructions() call
due to the `if (instructions)` check, but still marks the mode as applied. This
leaves the shared agent with stale instructions from the previous mode. Remove
the conditional check around the __updateInstructions() call so that it is
invoked whenever a different mode is being applied (after the early return
check), ensuring that empty instructions properly clear any previous mode's
instructions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e2d36820-6b0f-4d34-953d-278ca75ad47c

📥 Commits

Reviewing files that changed from the base of the PR and between ffac9f8 and 404571a.

📒 Files selected for processing (2)
  • packages/core/src/harness/fork-clone-metadata.test.ts
  • packages/core/src/harness/harness.ts

Comment thread packages/core/src/harness/harness.ts Outdated
Comment thread packages/core/src/harness/harness.ts Outdated
Comment on lines +4283 to +4288
if (this.config.agent) {
const currentMode = this.getCurrentMode();
const modeTools = currentMode.tools ?? currentMode.additionalTools;
if (modeTools) {
result.modeTools = modeTools;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Preserve mode.tools replacement semantics with the shared agent.

validateModes() still treats tools as “replace” and additionalTools as “augment”, but this path maps both to modeTools while intentionally keeping the shared agent’s own tools. A mode using tools to restrict capabilities can still expose omitted base-agent tools; either honor replacement with a per-run tool mask or reject mode.tools for shared agents until replacement can be enforced.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/harness/harness.ts` around lines 4283 - 4288, The code in
the conditional block at lines 4283-4288 does not distinguish between
replacement semantics (when mode.tools is used) and augmentation semantics (when
additionalTools is used), yet validateModes() enforces this distinction. When
currentMode.tools is defined instead of currentMode.additionalTools, it
indicates the mode intends to replace (restrict) the agent's tools, but the
current code just adds them to modeTools while preserving the shared agent's own
tools. Fix this by explicitly checking whether currentMode.tools (replacement
intent) or currentMode.additionalTools (augment intent) is being used, and
either implement a per-run tool mask to enforce the replacement semantics, or
reject/throw an error when mode.tools is used with a shared agent to prevent
unintended tool exposure until replacement enforcement can be properly
implemented.

…gent on mode switch

Replace __updateInstructions() mutation with a dynamic function that
resolves the current mode's instructions at execution time. The function
is installed once on config.agent and reads harness.currentModeId via
closure, so no shared state is mutated on mode switch.

Co-Authored-By: Abhi Aiyer <abhi@mastra.ai>
… agent

Remove installDynamicModeInstructions and __updateInstructions usage.
Mode instructions are now resolved in resolveCurrentModeInstructions()
and passed via the instructions option in buildAgentMessageStreamOptions.
The agent's own instructions are never touched by the harness.

Co-Authored-By: Abhi Aiyer <abhi@mastra.ai>
…ehavior

- agent instructions never mutated by mode switches
- resolveCurrentModeInstructions returns correct per-mode instructions
- mode tools included in toolsets for shared agent path
- signal provider stays connected across mode switches
- deprecated mode.agent path still works independently

Co-Authored-By: Abhi Aiyer <abhi@mastra.ai>
@vercel vercel Bot temporarily deployed to Preview – mastra-docs-1.x June 16, 2026 13:03 Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
packages/core/src/harness/harness.ts (2)

565-574: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate runtime changes to explicit mode.agent instances too.

Lines 567 and 638 still make a deprecated mode.agent the actual backing agent for that mode (via getAgentForMode at lines 882-886) even when config.agent exists, but these branches only update config.agent. Mixed configs can leave those mode agents without browser/runtime services after setBrowser() or init(), so signal providers attached to them can remain memoryless until a later lazy getCurrentAgent() call — the exact regression this PR fixes, but in a different scenario.

Also applies to: 634-646

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/harness/harness.ts` around lines 565 - 574, The
setBrowser() method only updates the shared config.agent when it exists, but
deprecated mode.agent instances can still be backing agents for their modes (via
getAgentForMode) and remain unsynchronized. When config.agent exists, also
iterate through this.config.modes and call setBrowser() on the agent returned by
getAgentForMode for each mode to ensure deprecated mode.agent instances receive
the browser update. Apply the same fix to the other method affecting lines
634-646 (likely the init() method) to propagate runtime services consistently to
both config.agent and any mode-specific agents in mixed configurations.

4282-4288: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Preserve mode.tools replacement semantics with the shared agent.

validateModes() at lines 86-91 still treats tools as "replace" and additionalTools as "augment", but line 4284 maps both to modeTools while intentionally keeping the shared agent's own tools. A mode using tools to restrict capabilities can still expose omitted base-agent tools; either honor replacement with a per-run tool mask or reject mode.tools for shared agents until replacement can be enforced.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/harness/harness.ts` around lines 4282 - 4288, The code at
lines 4282-4288 in the if block checking this.config.agent needs to enforce the
replacement semantics defined in validateModes(). Currently, when a mode
specifies tools (intended to replace capabilities), the code still preserves the
shared agent's own tools by mapping both currentMode.tools and
currentMode.additionalTools equally to modeTools. To fix this, either implement
a per-run tool mask that filters out tools not in the replacement set when
currentMode.tools is explicitly defined (indicating replacement rather than
augmentation), or add validation logic to reject configurations where mode.tools
is used with a shared agent, preventing the capability restriction from being
bypassed by the base agent's unrestricted tools.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@packages/core/src/harness/harness.ts`:
- Around line 565-574: The setBrowser() method only updates the shared
config.agent when it exists, but deprecated mode.agent instances can still be
backing agents for their modes (via getAgentForMode) and remain unsynchronized.
When config.agent exists, also iterate through this.config.modes and call
setBrowser() on the agent returned by getAgentForMode for each mode to ensure
deprecated mode.agent instances receive the browser update. Apply the same fix
to the other method affecting lines 634-646 (likely the init() method) to
propagate runtime services consistently to both config.agent and any
mode-specific agents in mixed configurations.
- Around line 4282-4288: The code at lines 4282-4288 in the if block checking
this.config.agent needs to enforce the replacement semantics defined in
validateModes(). Currently, when a mode specifies tools (intended to replace
capabilities), the code still preserves the shared agent's own tools by mapping
both currentMode.tools and currentMode.additionalTools equally to modeTools. To
fix this, either implement a per-run tool mask that filters out tools not in the
replacement set when currentMode.tools is explicitly defined (indicating
replacement rather than augmentation), or add validation logic to reject
configurations where mode.tools is used with a shared agent, preventing the
capability restriction from being bypassed by the base agent's unrestricted
tools.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1122b605-8900-4315-bb9d-9734804788cb

📥 Commits

Reviewing files that changed from the base of the PR and between ed281fd and f7726bf.

📒 Files selected for processing (1)
  • packages/core/src/harness/harness.ts

@dane-ai-mastra dane-ai-mastra Bot added tests: failing ❌ Changed tests passed against base and removed tests: green ✅ Changed tests failed against base as expected labels Jun 16, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Test Results — PR #18008

Session: Devin
Method: Shell-based vitest + typecheck (core library change, no UI surface)

Targeted fix verification (7 tests)
Test Result
Signal provider memory access (original crash) ✅ passed
Agent identity preserved across mode switches ✅ passed
Agent instructions never mutated by mode switches ✅ passed
Mode instructions resolved at call time per mode ✅ passed
Mode tools in toolsets for shared agent path ✅ passed
Signal provider stability across mode switches ✅ passed
Deprecated mode.agent path still works ✅ passed
Regression suites
  • Harness: 348/348 tests passed (34 files)
  • Signal providers: 56/56 tests passed (3 files)
  • Typecheck: tsc --noEmit exit 0
Escalations
  • No TUI/E2E testing — OPENROUTER_API_KEY not available. Unit tests prove internal behavior but a live TUI test would provide stronger end-to-end confidence.
  • CodeRabbit Comment 9 (mixed config.agent + mode.agent): edge case of deprecated path, low risk.
  • CodeRabbit Comment 11 (mode.tools replacement semantics): pre-existing design limitation, not introduced by this PR.

Co-Authored-By: Abhi Aiyer <abhi@mastra.ai>
@vercel vercel Bot temporarily deployed to Preview – mastra-docs-1.x June 16, 2026 13:11 Inactive
@dane-ai-mastra dane-ai-mastra Bot added tests: green ✅ Changed tests failed against base as expected and removed tests: failing ❌ Changed tests passed against base labels Jun 16, 2026
- setBrowser() and init() now propagate to all unique agents (shared
  config.agent + any deprecated mode.agent instances) via a Set, so
  mixed configs don't leave mode.agent instances without browser or
  runtime services.

- buildToolsets() documents that mode.tools and mode.additionalTools
  are both treated as augmentation for the shared agent path; true
  'replace' semantics would require per-run tool filtering in Agent
  which isn't supported yet.

Co-Authored-By: Abhi Aiyer <abhi@mastra.ai>
@vercel vercel Bot temporarily deployed to Preview – mastra-docs-1.x June 16, 2026 13:33 Inactive
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Addressed CodeRabbit review comments in 4bc2d9d:

Comment 9 (mixed config.agent + mode.agent propagation): Fixed. setBrowser() and init() now collect all unique agents via a Set — shared config.agent plus any deprecated mode.agent instances — so mixed configs propagate browser and runtime services to everything.

Comment 10 (stale instructions on instructionless modes): Already resolved. The applyModeInstructions() / __updateInstructions() path no longer exists — replaced by resolveCurrentModeInstructions() which resolves fresh per-call and passes via stream options. No mutation, no stale state.

Comment 11 (mode.tools replacement semantics): Documented the limitation. Both mode.tools and mode.additionalTools are treated as augmentation for the shared agent path. True "replace" semantics (masking the agent's base tools) would require per-run tool filtering in Agent, which isn't supported yet. The original forking code's __setTools() also lost signal provider tools, so this was already broken.

Comment 7 (model ID tokens in tests): Not applicable — the placeholder tokens in models.ts are remark build-time replacements for docs. All 11 model references in this test file (including 3 pre-existing) use literal IDs, which is the established convention for unit tests.

@abhiaiyer91 abhiaiyer91 merged commit c853d53 into main Jun 16, 2026
96 checks passed
@abhiaiyer91 abhiaiyer91 deleted the devin/1781610039-fix-signal-provider-fork-memory branch June 16, 2026 13:57
abhiaiyer91 pushed a commit that referenced this pull request Jun 18, 2026
…ns (#18127)

## Description

When using a shared backing agent (introduced in #18008),
`buildAgentMessageStreamOptions` passed mode instructions via
`options.instructions`, which the agent treats as a **full override** of
its own instructions:

```ts
// agent.ts:6267
const instructions = options.instructions || (await this.getInstructions({ requestContext }));
```

This meant the agent's own dynamic instructions (AGENTS.md, project
context, mode-specific prompts from `getDynamicInstructions`) were
completely lost — only the harness/mode static instructions survived.

**Fix:** When mode instructions exist, resolve the agent's own
`getInstructions()` first and combine both before passing at call time.
When no mode instructions exist, don't pass `instructions` at all — the
agent uses its own `getInstructions()` naturally.

```diff
 // buildAgentMessageStreamOptions
-const modeInstructions = this.config.agent ? this.resolveCurrentModeInstructions() : undefined;
-...(modeInstructions && { instructions: modeInstructions }),
+if (this.config.agent) {
+  const modeInstructions = this.resolveCurrentModeInstructions();
+  if (modeInstructions) {
+    const agentInstructions = await agent.getInstructions({ requestContext });
+    callTimeInstructions = [agentStr, modeInstructions].filter(Boolean).join('\n');
+  }
+}
+...(callTimeInstructions && { instructions: callTimeInstructions }),
```

## Related issue(s)

Regression from #18008 (eliminate agent forking). Reported in Slack
#mastracode by Tyler Barnes.

## Type of change

- [x] Bug fix (non-breaking change that fixes an issue)

## Checklist

- [x] I have linked the related issue(s) in the description above
- [x] I have made corresponding changes to the documentation (if
applicable)
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have addressed all Coderabbit comments on this PR

Link to Devin session:
https://app.devin.ai/sessions/1eaa195dad2743feb71d55314e8a336f

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## ELI5 (Explain Like I'm 5)

Imagine you have a robot that knows how to do many things on its own
(its natural instructions), but also needs to follow special rules for
different jobs (mode instructions). Previously, when giving it the
special job rules, someone forgot to tell it about its natural
instructions, so it only followed the job rules. This PR fixes that by
making sure the robot remembers both its natural instructions AND the
special job rules at the same time.

## What's Fixed

This PR resolves a regression from `#18008` where shared backing agents
were losing their dynamic instructions when using mode-specific
instructions. The root cause was in `buildAgentMessageStreamOptions` in
`harness.ts`, which was passing only mode instructions via
`options.instructions`. Since the agent's instruction resolution logic
uses an OR operator (`||`) rather than merging, the agent's own dynamic
instructions—including AGENTS.md content, project context, and
mode-specific prompts from `getDynamicInstructions`—were completely
overridden.

## Changes Made

**packages/core/src/harness/harness.ts:**
- Added `AgentInstructions` to imports
- Implemented a new private helper method `instructionsToString()` that
normalizes `AgentInstructions` (which can be a string, string array, or
system message object with `content` field) into a combined plain-string
form
- Modified `buildAgentMessageStreamOptions()` to:
- When a backing agent exists and mode instructions are present: fetch
the agent's current runtime instructions via `agent.getInstructions({
requestContext })`, convert them to a string, and combine them with the
resolved mode instructions
- When mode instructions do not exist: no longer pass the `instructions`
option, allowing the agent to use its own `getInstructions()` naturally
- Added `resolveCurrentModeInstructions()` private method to handle
combining harness and mode instructions

**.changeset/brave-mails-listen.md:**
- New changeset documenting a patch version bump for `@mastra/core`
- Documents the fix to `harness.buildAgentMessageStreamOptions`
combining mode instructions with agent dynamic instructions rather than
replacing them

## Impact

The fix ensures that when harness uses mode-specific instructions, an
agent's own dynamic instructions are preserved and merged together,
rather than being overridden. This restores the intended behavior for
shared backing agents that rely on their own instruction sources
alongside mode-specific customizations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Abhi Aiyer <abhi@mastra.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

complexity: low Low-complexity PR tests: green ✅ Changed tests failed against base as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants