Skip to content

Commit 78777fc

Browse files
TylerBarnesMastra Code (openai/gpt-5.5)
andcommitted
feat(memory): simplify OM-managed working memory
Move the unreleased working memory extraction controls to the final agentManaged/manageWorkingMemory API. This keeps the working memory extractor branch from introducing the discarded injectTools name and documents the managed working memory defaults. Co-Authored-By: Mastra Code (openai/gpt-5.5) <noreply@mastra.ai>
1 parent 91e9c58 commit 78777fc

12 files changed

Lines changed: 438 additions & 76 deletions

File tree

.changeset/honest-llamas-dress.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
---
22
'@mastra/memory': minor
3-
'@mastra/core': patch
3+
'@mastra/core': minor
44
---
55

6-
add working memory extractor and injectTools config
6+
add OM-managed working memory
77

8-
Adds a built-in WorkingMemoryExtractor for the observtional
9-
memory extractor pipeline. When included in observation or
10-
reflection config, the observer/reflector can update working
11-
memory through the normal extractor pipeline instead of
12-
requiring the main agent to call the working memory tool.
8+
Adds `observationalMemory.observation.manageWorkingMemory` so the Observer can update working memory automatically instead of requiring the main agent to call the working memory tool.
139

14-
Adds injectTools: boolean (default true) to BaseWorkingMemory.
15-
Set to false to prevent the working memory update tool from
16-
being injected into the main agent.
10+
```ts
11+
new Memory({
12+
options: {
13+
workingMemory: { enabled: true },
14+
observationalMemory: {
15+
enabled: true,
16+
observation: { manageWorkingMemory: true },
17+
},
18+
},
19+
})
20+
```
21+
22+
This option adds `WorkingMemoryExtractor`, defaults `workingMemory.agentManaged` to `false`, and defaults `workingMemory.useStateSignals` to `true` when working memory is enabled. Set `workingMemory.agentManaged: true` to keep the main agent's working memory tool and instructions enabled.

docs/src/content/en/docs/memory/observational-memory.mdx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,32 @@ new Extractor({
248248
})
249249
```
250250

251+
### Working memory updates
252+
253+
Use `observationalMemory.observation.manageWorkingMemory` when working memory should be managed automatically by the Observer. The main agent no longer needs to remember to call the working memory tool while it is handling the user request.
254+
255+
This also keeps working memory prompt-cache friendly. Working memory normally lives in the system prompt, so updates can invalidate the prompt cache. OM-managed working memory defaults `workingMemory.useStateSignals` to `true`, which moves working memory into state signals instead.
256+
257+
```typescript title="src/mastra/agents/agent.ts"
258+
import { Memory } from '@mastra/memory'
259+
260+
const memory = new Memory({
261+
options: {
262+
workingMemory: {
263+
enabled: true,
264+
},
265+
observationalMemory: {
266+
enabled: true,
267+
observation: {
268+
manageWorkingMemory: true,
269+
},
270+
},
271+
},
272+
})
273+
```
274+
275+
This setting adds `WorkingMemoryExtractor`, defaults `workingMemory.agentManaged` to `false`, and defaults `workingMemory.useStateSignals` to `true`. Set `workingMemory.agentManaged: true` if the main agent should still receive working memory tool and instruction injection.
276+
251277
Use `onExtracted` to normalize or react to custom extracted values before they are persisted:
252278

253279
```typescript title="src/mastra/agents/agent.ts"

docs/src/content/en/docs/memory/working-memory.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Think of it as the agent's active thoughts or scratchpad – the key information
2020

2121
This is useful for maintaining ongoing state that's always relevant and should always be available to the agent.
2222

23+
If you use [Observational Memory](/docs/memory/observational-memory), `observationalMemory.observation.manageWorkingMemory` lets OM update working memory for the agent.
24+
2325
Working memory can persist at two different scopes:
2426

2527
- **Resource-scoped** (default): Memory persists across all conversation threads for the same user

docs/src/content/en/reference/memory/observational-memory.mdx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,10 +448,36 @@ const memory = new Memory({
448448
- Schema-backed extractors add a follow-up structured output request.
449449
- Schema-less extractors are inline string extractors emitted directly in the Observer or Reflector output.
450450
- Dynamic extractor functions receive runtime context, including `source`, `threadId`, `resourceId`, `mainAgent`, `memory`, and `requestContext` when available.
451+
- `WorkingMemoryExtractor` uses the normal extractor pipeline to update working memory through the active `Memory` instance. It uses structured extraction when working memory has a JSON schema and skips OM metadata persistence, so the working memory payload isn't duplicated under OM extracted metadata.
452+
- `observationalMemory.observation.manageWorkingMemory` adds `WorkingMemoryExtractor`, defaults `workingMemory.agentManaged` to `false`, and defaults `workingMemory.useStateSignals` to `true` when working memory is enabled.
451453
- Extraction failures are reported in OM marker data and do not discard other successful extracted values.
452454

453455
## Examples
454456

457+
### Working memory updates
458+
459+
Use `observationalMemory.observation.manageWorkingMemory` when OM should update working memory.
460+
461+
```typescript title="src/mastra/agents/agent.ts"
462+
import { Memory } from '@mastra/memory'
463+
464+
const memory = new Memory({
465+
options: {
466+
workingMemory: {
467+
enabled: true,
468+
},
469+
observationalMemory: {
470+
enabled: true,
471+
observation: {
472+
manageWorkingMemory: true,
473+
},
474+
},
475+
},
476+
})
477+
```
478+
479+
Set `workingMemory.agentManaged: true` if the main agent should still receive working memory tool and instruction injection.
480+
455481
### Resource scope with custom thresholds (experimental)
456482

457483
```typescript title="src/mastra/agents/agent.ts"

packages/core/src/memory/mock.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ export class MockMemory extends MastraMemory {
237237

238238
public listTools(_config?: MemoryConfigInternal): Record<string, ToolAction<any, any, any>> {
239239
const mergedConfig = this.getMergedThreadConfig(_config);
240-
if (!mergedConfig.workingMemory?.enabled) {
240+
if (!mergedConfig.workingMemory?.enabled || mergedConfig.workingMemory.agentManaged === false) {
241241
return {};
242242
}
243243

packages/core/src/memory/types.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,13 +197,13 @@ type BaseWorkingMemory = {
197197
*/
198198
useStateSignals?: boolean;
199199
/**
200-
* Whether to inject the working memory update tool into the main agent.
200+
* Whether the main agent manages working memory directly through tool/instruction injection.
201201
* Set to false when another path, such as Observational Memory extractors,
202202
* owns working memory updates.
203203
*
204204
* @default true
205205
*/
206-
injectTools?: boolean;
206+
agentManaged?: boolean;
207207
/** @deprecated The `use` option has been removed. Working memory always uses tool-call mode. */
208208
use?: never;
209209
};
@@ -444,6 +444,16 @@ export interface ObservationalMemoryObservationConfig {
444444
*/
445445
model?: AgentConfig['model'];
446446

447+
/**
448+
* Manage working memory through Observational Memory extraction.
449+
* When enabled alongside `workingMemory.enabled`, Memory supplies defaults that
450+
* disable main-agent working memory management and add the WorkingMemoryExtractor.
451+
* Set `workingMemory.agentManaged: true` to keep main-agent tools/instructions enabled.
452+
*
453+
* @default false
454+
*/
455+
manageWorkingMemory?: boolean;
456+
447457
/**
448458
* Token count of unobserved messages that triggers observation.
449459
* When unobserved message tokens exceed this, the Observer is called.
@@ -1329,6 +1339,9 @@ export type SerializedObservationalMemoryConfig = {
13291339
export type SerializedObservationalMemoryObservationConfig = {
13301340
/** Observer model ID */
13311341
model?: string;
1342+
/** Manage working memory through Observational Memory extraction. */
1343+
manageWorkingMemory?: boolean;
1344+
13321345
/** Token count threshold that triggers observation */
13331346
messageTokens?: number;
13341347
/** Model settings (temperature, maxOutputTokens, etc.) */

packages/memory/integration-tests/src/om-extracted-metadata.test.ts

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { MockLanguageModelV2, convertArrayToReadableStream } from '@internal/ai-
66
import type { MastraDBMessage } from '@mastra/core/agent';
77
import { getThreadOMMetadata, setThreadOMMetadata } from '@mastra/core/memory';
88
import { LibSQLStore } from '@mastra/libsql';
9-
import { Extractor, Memory } from '@mastra/memory';
9+
import { Extractor, Memory, WorkingMemoryExtractor } from '@mastra/memory';
1010
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
11+
import { z } from 'zod';
1112

1213
const createMessage = (
1314
threadId: string,
@@ -109,6 +110,163 @@ describe('Observational Memory extracted metadata persistence', () => {
109110
});
110111
});
111112

113+
it('updates markdown working memory from an end-to-end LibSQL observation run', async () => {
114+
const threadId = randomUUID();
115+
const resourceId = randomUUID();
116+
const observerOutput = `<observations>
117+
- User shared durable profile details.
118+
</observations>
119+
<working-memory># User Profile
120+
- Name: Tyler
121+
- Location: Seattle
122+
</working-memory>`;
123+
const model = new MockLanguageModelV2({
124+
doStream: async () => ({
125+
stream: convertArrayToReadableStream([
126+
{ type: 'stream-start', warnings: [] },
127+
{ type: 'response-metadata', id: 'obs-wm-1', modelId: 'mock-observer', timestamp: new Date() },
128+
{ type: 'text-start', id: 'text-wm-1' },
129+
{ type: 'text-delta', id: 'text-wm-1', delta: observerOutput },
130+
{ type: 'text-end', id: 'text-wm-1' },
131+
{ type: 'finish', finishReason: 'stop', usage: { inputTokens: 100, outputTokens: 20, totalTokens: 120 } },
132+
]),
133+
rawCall: { rawPrompt: null, rawSettings: {} },
134+
warnings: [],
135+
}),
136+
});
137+
138+
const workingMemory = new Memory({
139+
storage: memory.storage,
140+
options: {
141+
workingMemory: {
142+
enabled: true,
143+
template: '# User Profile\n- Name:\n- Location:',
144+
agentManaged: false,
145+
},
146+
observationalMemory: {
147+
enabled: true,
148+
observation: {
149+
model,
150+
messageTokens: 1,
151+
bufferTokens: false,
152+
previousObserverTokens: 1000,
153+
extract: [new WorkingMemoryExtractor()],
154+
},
155+
},
156+
},
157+
});
158+
159+
await workingMemory.createThread({ threadId, resourceId, title: 'Working Memory Markdown' });
160+
await workingMemory.saveMessages({
161+
messages: [
162+
createMessage(
163+
threadId,
164+
resourceId,
165+
'user',
166+
'My name is Tyler and I live in Seattle.',
167+
'2026-06-24T18:00:00.000Z',
168+
),
169+
],
170+
});
171+
172+
const omEngine = await workingMemory.omEngine;
173+
const result = await omEngine!.observe({ threadId, resourceId });
174+
175+
expect(result.observed).toBe(true);
176+
await expect(workingMemory.getWorkingMemory({ threadId, resourceId })).resolves.toContain('Name: Tyler');
177+
await expect(workingMemory.getWorkingMemory({ threadId, resourceId })).resolves.toContain('Location: Seattle');
178+
179+
const updatedThread = await workingMemory.getThreadById({ threadId });
180+
expect(getThreadOMMetadata(updatedThread?.metadata)?.extracted?.['working-memory']).toBeUndefined();
181+
});
182+
183+
it('merges schema-backed working memory from an end-to-end LibSQL observation run', async () => {
184+
const threadId = randomUUID();
185+
const resourceId = randomUUID();
186+
const observerOutput = `<observations>
187+
- User shared durable schema-backed profile details.
188+
</observations>`;
189+
const model = new MockLanguageModelV2({
190+
doStream: async () => ({
191+
stream: convertArrayToReadableStream([
192+
{ type: 'stream-start', warnings: [] },
193+
{ type: 'response-metadata', id: 'obs-wm-2', modelId: 'mock-observer', timestamp: new Date() },
194+
{ type: 'text-start', id: 'text-wm-2' },
195+
{ type: 'text-delta', id: 'text-wm-2', delta: observerOutput },
196+
{ type: 'text-end', id: 'text-wm-2' },
197+
{ type: 'finish', finishReason: 'stop', usage: { inputTokens: 100, outputTokens: 20, totalTokens: 120 } },
198+
]),
199+
rawCall: { rawPrompt: null, rawSettings: {} },
200+
warnings: [],
201+
}),
202+
doGenerate: async () => ({
203+
rawCall: { rawPrompt: null, rawSettings: {} },
204+
finishReason: 'stop',
205+
usage: { inputTokens: 80, outputTokens: 10, totalTokens: 90 },
206+
warnings: [],
207+
content: [
208+
{
209+
type: 'text',
210+
text: JSON.stringify({ 'working-memory': { profile: { location: 'Seattle' }, preferences: ['weather'] } }),
211+
},
212+
],
213+
}),
214+
});
215+
216+
const workingMemory = new Memory({
217+
storage: memory.storage,
218+
options: {
219+
workingMemory: {
220+
enabled: true,
221+
schema: z.object({
222+
profile: z.object({ name: z.string().optional(), location: z.string().optional() }).optional(),
223+
preferences: z.array(z.string()).optional(),
224+
}),
225+
agentManaged: false,
226+
},
227+
observationalMemory: {
228+
enabled: true,
229+
observation: {
230+
model,
231+
messageTokens: 1,
232+
bufferTokens: false,
233+
previousObserverTokens: 1000,
234+
extract: [new WorkingMemoryExtractor()],
235+
},
236+
},
237+
},
238+
});
239+
240+
await workingMemory.createThread({ threadId, resourceId, title: 'Working Memory Schema' });
241+
await workingMemory.updateWorkingMemory({
242+
threadId,
243+
resourceId,
244+
workingMemory: JSON.stringify({ profile: { name: 'Tyler' } }),
245+
});
246+
await workingMemory.saveMessages({
247+
messages: [
248+
createMessage(
249+
threadId,
250+
resourceId,
251+
'user',
252+
'I live in Seattle and like weather updates.',
253+
'2026-06-24T18:05:00.000Z',
254+
),
255+
],
256+
});
257+
258+
const omEngine = await workingMemory.omEngine;
259+
const result = await omEngine!.observe({ threadId, resourceId });
260+
261+
expect(result.observed).toBe(true);
262+
await expect(workingMemory.getWorkingMemory({ threadId, resourceId })).resolves.toBe(
263+
JSON.stringify({ profile: { name: 'Tyler', location: 'Seattle' }, preferences: ['weather'] }),
264+
);
265+
266+
const updatedThread = await workingMemory.getThreadById({ threadId });
267+
expect(getThreadOMMetadata(updatedThread?.metadata)?.extracted?.['working-memory']).toBeUndefined();
268+
});
269+
112270
it('persists extracted values from an end-to-end LibSQL observation run', async () => {
113271
const threadId = randomUUID();
114272
const resourceId = randomUUID();

0 commit comments

Comments
 (0)