Describe the Bug
When Observational Memory (OM) is enabled, client-side tool (tool without execute function) execution results are lost on the next request. The agent cannot see the tool result and acts as if it never arrived.
OM's filterObservedMessages removes messages that have been "observed", but has no awareness of tool-call / tool-result pairing. On the second request (when the client sends back the tool result), OM removes the tool-call message because its ID is in observedMessageIds, leaving the tool-result orphaned — the LLM sees a result without the corresponding call.
This differs from #14282 which only fixed provider-executed tools (e.g. Anthropic web_search) by deferring observation when state: 'call' is detected mid-stream. Client-side tools return in a separate HTTP request where the call already has state: 'result', so that guard doesn't help.
Steps To Reproduce
- Create an agent with OM enabled and a client-side tool:
const agent = new Agent({
name: 'test-agent',
instructions: 'You are a helpful assistant.',
model: openai('gpt-4o'),
tools: {
myTool: createTool({
id: 'my-tool',
description: 'A client-side tool',
inputSchema: z.object({ query: z.string() }),
// no execute → client-side tool
}),
},
memory: new Memory({
options: { observationalMemory: { enabled: true } },
}),
});
-
Start a conversation. The agent generates a tool call (state: 'call') and returns it to the client.
-
Client executes the tool locally and sends the result back in a new request (messages include the tool-call + tool-result).
-
Expected: Agent sees the tool result and continues the conversation.
-
Actual: Agent does not see the tool result — it's effectively lost due to OM filtering.
Link to Minimal Reproducible Example
/
Expected Behavior
The agent should see the client-side tool result and continue the conversation normally. filterObservedMessages (packages/memory/src/processors/observational-memory/message-utils.ts) should check for tool-call/tool-result pair integrity before removing messages. If a message containing a tool-invocation part is about to be removed, the filter should verify that no remaining message has a tool-result part referencing the same toolCallId. If an orphaned result would be created, either keep the tool-call message or remove both.
Workaround: Converting client-side tools to server-side tools (adding an execute function) avoids this, since results are resolved within the same request's agentic loop before OM's cross-request filtering kicks in.
Environment Information
- `@mastra/core`: >= 1.22.0
- `@mastra/memory`: >= 1.13.0
Verification
Describe the Bug
When Observational Memory (OM) is enabled, client-side tool (tool without
executefunction) execution results are lost on the next request. The agent cannot see the tool result and acts as if it never arrived.OM's
filterObservedMessagesremoves messages that have been "observed", but has no awareness of tool-call / tool-result pairing. On the second request (when the client sends back the tool result), OM removes the tool-call message because its ID is inobservedMessageIds, leaving the tool-result orphaned — the LLM sees a result without the corresponding call.This differs from #14282 which only fixed provider-executed tools (e.g. Anthropic
web_search) by deferring observation whenstate: 'call'is detected mid-stream. Client-side tools return in a separate HTTP request where the call already hasstate: 'result', so that guard doesn't help.Steps To Reproduce
Start a conversation. The agent generates a tool call (
state: 'call') and returns it to the client.Client executes the tool locally and sends the result back in a new request (messages include the tool-call + tool-result).
Expected: Agent sees the tool result and continues the conversation.
Actual: Agent does not see the tool result — it's effectively lost due to OM filtering.
Link to Minimal Reproducible Example
/
Expected Behavior
The agent should see the client-side tool result and continue the conversation normally.
filterObservedMessages(packages/memory/src/processors/observational-memory/message-utils.ts) should check for tool-call/tool-result pair integrity before removing messages. If a message containing atool-invocationpart is about to be removed, the filter should verify that no remaining message has atool-resultpart referencing the sametoolCallId. If an orphaned result would be created, either keep the tool-call message or remove both.Workaround: Converting client-side tools to server-side tools (adding an
executefunction) avoids this, since results are resolved within the same request's agentic loop before OM's cross-request filtering kicks in.Environment Information
Verification