Skip to content

Commit 861f111

Browse files
rphansen91claude
andauthored
fix: call processInputStep in legacy path for Observational Memory with v4 models (#13358)
## Summary - Observational Memory (OM) was completely broken when using AI SDK v4 models (e.g., `openai('gpt-4o-mini')`) — the agent had no conversation history and no observations - The legacy stream/generate path (`agent-legacy.ts`) only ran the `processInput` phase but never `processInputStep`. Since OM implements `processInputStep` (not `processInput`), and `MessageHistory` is skipped when OM is enabled, nothing loaded conversation context - Adds `__runProcessInputStep()` to the Agent class and exposes it via legacy capabilities so both `before()` paths call it after `__runInputProcessors` ## Test plan - [x] `pnpm build:core` passes - [x] `pnpm build:memory` passes - [ ] Start example agent server with `OM_DEBUG=1`, send messages to script-agent, verify `[OM:processInputStep:ENTER]` appears in om-debug.log - [ ] Verify agent remembers context across messages when using v4 models with OM enabled 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Bug Fixes * Fixed Observational Memory not functioning with AI SDK v4 models in the legacy execution path, restoring the ability to properly inject conversation history and observations during agent processing. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a306abf commit 861f111

4 files changed

Lines changed: 148 additions & 2 deletions

File tree

.changeset/icy-symbols-count.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 Observational Memory not working with AI SDK v4 models (legacy path). The legacy stream/generate path now calls processInputStep, enabling processors like Observational Memory to inject conversation history and observations.

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

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,21 @@ export interface AgentLegacyCapabilities {
113113
processorId?: string;
114114
};
115115
}>;
116+
/** Run processInputStep phase on input processors (for legacy path compatibility) */
117+
__runProcessInputStep(args: {
118+
requestContext: RequestContext;
119+
tracingContext: TracingContext;
120+
messageList: MessageList;
121+
stepNumber?: number;
122+
}): Promise<{
123+
messageList: MessageList;
124+
tripwire?: {
125+
reason: string;
126+
retry?: boolean;
127+
metadata?: unknown;
128+
processorId?: string;
129+
};
130+
}>;
116131
/** Get most recent user message */
117132
getMostRecentUserMessage(
118133
messages: Array<UIMessage | UIMessageWithMetadata>,
@@ -306,6 +321,26 @@ export class AgentLegacyHandler {
306321
tracingContext: innerTracingContext,
307322
messageList,
308323
});
324+
// Run processInputStep for step 0 (legacy path compatibility)
325+
if (!tripwire) {
326+
const inputStepResult = await this.capabilities.__runProcessInputStep({
327+
requestContext,
328+
tracingContext: innerTracingContext,
329+
messageList,
330+
stepNumber: 0,
331+
});
332+
if (inputStepResult.tripwire) {
333+
return {
334+
messageObjects: [],
335+
convertedTools,
336+
threadExists: false,
337+
thread: undefined,
338+
messageList,
339+
agentSpan,
340+
tripwire: inputStepResult.tripwire,
341+
};
342+
}
343+
}
309344
return {
310345
messageObjects: tripwire ? [] : messageList.get.all.prompt(),
311346
convertedTools,
@@ -391,8 +426,32 @@ export class AgentLegacyHandler {
391426
});
392427
messageList = processedMessageList;
393428

394-
// Messages are already processed by __runInputProcessors above
395-
// which includes memory processors (WorkingMemory, MessageHistory, etc.)
429+
// Run processInputStep phase for step 0 (legacy path compatibility).
430+
// The v5 agentic loop runs this per-step in llm-execution-step, but the legacy
431+
// path doesn't have that loop. This is needed for processors like Observational Memory
432+
// that implement processInputStep (not processInput) to inject context.
433+
if (!tripwire) {
434+
const inputStepResult = await this.capabilities.__runProcessInputStep({
435+
requestContext,
436+
tracingContext: innerTracingContext,
437+
messageList,
438+
stepNumber: 0,
439+
});
440+
if (inputStepResult.tripwire) {
441+
return {
442+
convertedTools,
443+
thread: threadObject,
444+
messageList,
445+
messageObjects: [],
446+
agentSpan,
447+
tripwire: inputStepResult.tripwire,
448+
threadExists: !!existingThread,
449+
};
450+
}
451+
}
452+
453+
// Messages are already processed by __runInputProcessors and __runProcessInputStep above
454+
// which includes memory processors (WorkingMemory, MessageHistory, OM, etc.)
396455
const processedList = messageList.get.all.prompt();
397456

398457
return {

packages/core/src/agent/agent.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,6 +1128,7 @@ export class Agent<
11281128
convertTools: this.convertTools.bind(this),
11291129
getMemoryMessages: (...args) => this.getMemoryMessages(...args),
11301130
__runInputProcessors: this.__runInputProcessors.bind(this),
1131+
__runProcessInputStep: this.__runProcessInputStep.bind(this),
11311132
getMostRecentUserMessage: this.getMostRecentUserMessage.bind(this),
11321133
genTitle: this.genTitle.bind(this),
11331134
resolveTitleGenerationConfig: this.resolveTitleGenerationConfig.bind(this),
@@ -2012,6 +2013,82 @@ export class Agent<
20122013
};
20132014
}
20142015

2016+
/**
2017+
* Runs processInputStep phase on input processors.
2018+
* Used by legacy path to execute per-step input processing (e.g., Observational Memory)
2019+
* that would otherwise only run in the v5 agentic loop.
2020+
* @internal
2021+
*/
2022+
private async __runProcessInputStep({
2023+
requestContext,
2024+
tracingContext,
2025+
messageList,
2026+
stepNumber = 0,
2027+
processorStates,
2028+
}: {
2029+
requestContext: RequestContext;
2030+
tracingContext: TracingContext;
2031+
messageList: MessageList;
2032+
stepNumber?: number;
2033+
processorStates?: Map<string, ProcessorState>;
2034+
}): Promise<{
2035+
messageList: MessageList;
2036+
tripwire?: {
2037+
reason: string;
2038+
retry?: boolean;
2039+
metadata?: unknown;
2040+
processorId?: string;
2041+
};
2042+
}> {
2043+
let tripwire: { reason: string; retry?: boolean; metadata?: unknown; processorId?: string } | undefined;
2044+
2045+
if (this.#inputProcessors || this.#memory) {
2046+
const runner = await this.getProcessorRunner({
2047+
requestContext,
2048+
processorStates,
2049+
});
2050+
try {
2051+
const llm = await this.getLLM({ requestContext });
2052+
const model = llm.getModel();
2053+
await runner.runProcessInputStep({
2054+
messageList,
2055+
stepNumber,
2056+
steps: [],
2057+
tracingContext,
2058+
requestContext,
2059+
// Cast needed: legacy v1 models return LanguageModelV1 which doesn't satisfy MastraLanguageModel.
2060+
// OM's processInputStep doesn't use the model parameter, so this is safe.
2061+
model: model as MastraLanguageModel,
2062+
retryCount: 0,
2063+
});
2064+
} catch (error) {
2065+
if (error instanceof TripWire) {
2066+
tripwire = {
2067+
reason: error.message,
2068+
retry: error.options?.retry,
2069+
metadata: error.options?.metadata,
2070+
processorId: error.processorId,
2071+
};
2072+
} else {
2073+
throw new MastraError(
2074+
{
2075+
id: 'AGENT_INPUT_STEP_PROCESSOR_ERROR',
2076+
domain: ErrorDomain.AGENT,
2077+
category: ErrorCategory.USER,
2078+
text: `[Agent:${this.name}] - Input step processor error`,
2079+
},
2080+
error,
2081+
);
2082+
}
2083+
}
2084+
}
2085+
2086+
return {
2087+
messageList,
2088+
tripwire,
2089+
};
2090+
}
2091+
20152092
/**
20162093
* Executes output processors on the message list after LLM processing.
20172094
* @internal

packages/memory/src/processors/observational-memory/observational-memory.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3325,8 +3325,13 @@ ${suggestedResponse}
33253325
const { messageList, requestContext, stepNumber, state: _state, writer, abortSignal, abort } = args;
33263326
const state = _state ?? ({} as Record<string, unknown>);
33273327

3328+
omDebug(
3329+
`[OM:processInputStep:ENTER] step=${stepNumber}, hasMastraMemory=${!!requestContext?.get('MastraMemory')}, hasMemoryInfo=${!!messageList?.serialize()?.memoryInfo?.threadId}`,
3330+
);
3331+
33283332
const context = this.getThreadContext(requestContext, messageList);
33293333
if (!context) {
3334+
omDebug(`[OM:processInputStep:NO-CONTEXT] getThreadContext returned null — returning early`);
33303335
return messageList;
33313336
}
33323337

0 commit comments

Comments
 (0)