Announcing Agent Signals

A context engineering primitive for interacting with running agents.

Tyler BarnesTyler Barnes·

Jun 3, 2026

·

9 min read

We just shipped Agent Signals. A new way of interacting with a running agent: steer them, wake them, connect multiple clients to one stream, and update state without invalidating the prompt cache.

Signals decouple stream ownership from context delivery. Multiple users can steer and observe the same agent loop. Signals can inject guidance based on agent behavior, and they can also connect agents to the outside world via notifications.

Why we shipped Signals

These days, due to model and harness advancements, agents can run autonomously for hours. The old request/response shape that worked for simple text completions and tool calls is no longer enough.

We want to see what the agent is doing from Slack, the web, a CLI, and a mobile app, all at once. We want multiple people to be able to jump into the conversation, see what's happening in real time and contribute as needed.

From a developer perspective, this is typically hard to achieve. Most frameworks, Mastra included, give you a way to prompt an agent, attach memory and tools, and consume the stream that comes back from the LLM.

That works well for narrowly scoped agents running on a small task. But what happens when two people interact with a long running agent at the same time?

const resourceId = "user_123"
const threadId = "conversation_1"
 
// caller A
const stream = agent.stream("How's that report going?", {
  resourceId,
  threadId,
})
 
// caller B
const stream = agent.stream("Remind me about the report we were working on last week.", {
  resourceId,
  threadId,
})

You get two independent agent loops, both competing for the same conversation, each agent breaking the flow of the other. Each user that sent their prompts would not know this was happening, they might only notice the agent behaving strangely for some unknown reason.

Before today, the framework would allow this. If you wanted to stream the same loop to multiple places, you had to write your own plumbing to repeat the stream out to every client, and any new message would have to be wired to detect the loop was active, abort the agent loop and start it again.

Stream ownership was tightly coupled to context delivery. The caller who sent the prompt was required to also own the stream.

Messages: a shared, addressable loop

Signals solve this by fully decoupling sending context into an agent and owning the stream.

You can subscribe to an existing thread from any number of clients:

const subscription = await agent.subscribeToThread({
  resourceId,
  threadId,
})
 
for await (const chunk of subscription.stream) {
  // handle the stream here
}

Now multiple clients can watch the same conversation. Those clients can also add messages at any point, and every subscriber sees those messages threaded into the same stream.

agent.sendMessage("How's that report going?", {
  resourceId,
  threadId,
})

A second user in a different UI immediately sees that a question was just asked. They can now still choose to intentionally steer the agent, or queue a message for when the agent is idle:

agent.queueMessage("Remind me about the report we were working on last week.", {
  resourceId,
  threadId,
})

This is the core primitive: the agent loop becomes addressable. You can send context to it without restarting it, and you can participate without having initiated the interaction.

Multiplayer

Being able to steer the agent is great, but multiplayer introduces an obvious question: how does the agent know who is talking?

Signals allow you to include custom attributes which are shown to the model:

agent.sendMessage({
  contents: "Can you prioritize the Postgres version?",
  attributes: {
    name: "Jane",
    sentFrom: "slack",
  },
}, {
  resourceId,
  threadId,
})

Internally, the model sees those attributes as xml attributes:

<user name="Jane" sentFrom="slack">
Can you prioritize the Postgres version?
</user>

You can include whatever context is useful for your application. Attributes are fully developer-defined. You can set anything that would help the agent interpret the signal. For example: who sent the message, where it came from, whether it came from Slack or mobile, or what role the sender has, or anything else.

We recommend updating your system prompt with instructions about what your attributes mean, but clear attribute names go a long way.

Reactive Signals

Messages are user-sent Signals. But Signals can also come from inside the agent loop.

A common need is influencing the loop programmatically while the agent is running. You may want to inject project instructions when the agent edits a file. You may want to remind the agent to produce a final answer when it is nearing maxSteps. You may want to enforce a policy right when the agent is about to do something risky.

Reactive Signals are sent from processors. A processor watches the agent's activity and sends context when a condition matches.

In this example, when the agent interacts with files that are in a directory that contains a nested AGENTS.md file, that file is auto loaded into context, via reactive signals:

const agentsMdReminderProcessor: Processor = {
  id: "agents-md-loader",
  async processInputStep({ messageList, sendSignal }) {
    const agentsMdPath = findAgentsMdPathFromToolCalls(messageList)
 
    if (!agentsMdPath) return messageList
 
    await sendSignal?.({
      type: "reactive",
      contents: await readAgentsMd(agentsMdPath),
      attributes: {
        type: "dynamic-agents-md",
        path: agentsMdPath,
      },
      metadata: {
        path: agentsMdPath,
      },
    })
 
    return messageList
  },
}

This turns processors into active context providers. They are no longer limited to observing or filtering the stream. They can guide the loop at the exact moment their context matters.

And there are many use cases for this: you can detect if an agent hasn't done something it should every x steps. You can remind the agent of how it should use a specific tool, or remind it of an action it should take when calling two related tools. You can let the agent know if it's about to hit the maximum number of steps, and ask it to output a final summary.

Reactive signals allow you to reactively guide your agent while it's working, without interrupting it. Reactive Signals solve moment-in-time guidance.

State Signals

Dynamic system prompts are useful, but they have a major cost: every update invalidates the prompt cache. That makes them a bad fit for state that changes often, like agent memory, browser state, user preferences, or current task status.

State Signals make dynamic system guidance append-only and prompt-cacheable.

Instead of rewriting the system prompt, a processor can own a named state lane and recompute it as the agent loop progresses. Each state signal has an id, a producer-owned cacheKey, model-facing contents, and a mode: snapshot for the authoritative current state, or delta for a change event. If the processor returns the same cacheKey and mode again while that state is still current, Mastra skips the duplicate.

The agent sees the new state in order, just like any other context. Because the update lives in thread history, it stays inside the cache boundary.

For example, working memory can be modeled as a state signal. When the state is first introduced, or when the old snapshot has fallen out of the active context window, the processor can inject a snapshot:

<working-memory-state format="markdown">
 ## Profile
 Name: Caleb
 
 ## Allergies
 - peanuts
</working-memory-state>

After that, it does not need to resend the whole document for every small edit, the agent already saw the full state snapshot, so it sends a delta:

<working-memory-delta format="markdown" notation="unified-diff">
 @@ -1,2 +1,2 @@
 -Name: Caleb
 +Name: Caleb Barnes
</working-memory-delta>

The snapshot gives the model the full current state. The delta tells it exactly what changed. When memory removes the old snapshot from the context window, the processor can collapse the accumulated deltas back into a fresh snapshot at the new cache boundary.

We've just shipped this as a new experimental working memory feature. Turn it on with the following settings:

import { Memory } from '@mastra/memory';
 
const memory = new Memory({
  options: {
    workingMemory: {
      enabled: true,
      useStateSignals: true
    }
  },
})

Notification Signals

The same primitive also gives agents a way to react to the outside world.

Most agents only start acting when you prompt them. But long-running agents should be able to respond to things that happen elsewhere: a GitHub review, a CI failure, an email, a Slack mention, an incident, a meeting transcript becoming available.

Notification Signals connect your agent to those external events.

await agent.sendNotificationSignal({
  source: "github",
  kind: "pull-request-review",
  priority: "high",
  summary: "PR #1820 was approved by @sam.",
  dedupeKey: "github:mastra-ai/mastra#1820:approved",
  coalesceKey: "github:mastra-ai/mastra#1820",
  attributes: {
    owner: "mastra-ai",
    repo: "mastra",
    number: 1820,
  },
}, {
  resourceId,
  threadId,
})

Notification Signals are stored as notification records first. External events are noisy, so this is where the interesting behavior lives. A delivery policy then decides, based on the given notification priority, what to do with each one: deliver it into the active loop, wake the agent, persist it for later, batch it, summarize it, or leave it in an inbox the agent checks when it has time.

We've shipped an example of this in Mastra Code.

Go to /settings and turn on Experimental GitHub Signals, then run /github subscribe to subscribe to the currently checked-out PR. From then on, the agent will receive notification signals for activity on that PR: reviews, approvals, CI status, and new comments.

It also uses reactive signals to make subscribing discoverable. When the agent does something PR-related, like running a gh pr shell command, a reactive signal lets it know it can subscribe to that PR's activity directly.

Get started

Signals are available in @mastra/core@1.39.0 or later, for agents using Mastra Memory. Memory gives Signals the resourceId and threadId needed to find the active loop, wake an idle agent, and preserve conversation order.

const resourceId = "user_123"
const threadId = "conversation_1"
 
const subscription = await agent.subscribeToThread({
  resourceId,
  threadId,
})
 
agent.sendMessage("Compare that with the previous option.", {
  resourceId,
  threadId,
})
 
for await (const chunk of subscription.stream) {
  console.log(chunk)
}

Signals change the shape of agents. They turn context delivery into something any authorized user, client, processor, or integration can do. Once an agent loop is addressable, agents become steerable, multiplayer, stateful, and proactive. Signals work on single-server deployments out of the box, and across multiple processes when Pub/Sub is configured. Upgrade to the latest Mastra package versions to start using them.

For full setup instructions, see the Signals docs.

Share:
Tyler Barnes
Tyler BarnesFounding Engineer

Tyler Barnes (BC, Canada) is a founding engineer at Mastra, focusing on all things related to agents (memory, tools, model routing, etc). Previously, he was a staff software engineer at Gatsby and Netlify, working on open-source developer tooling, GraphQL APIs, and large scale data ingestion.

All articles by Tyler Barnes