Skip to content

Commit b735edc

Browse files
committed
agent memory
1 parent bce6a05 commit b735edc

7 files changed

Lines changed: 400 additions & 3 deletions

File tree

skills/neuron-agent/SKILL.md

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,59 @@ declares that identity — the agent adopts it, and a disagreement with an
306306
explicit `threadId:` throws. `Agent::getThreadId(): ?string` reads the
307307
resolved identity back; null means the run is not findable by its thread.
308308

309-
### Memory Types
309+
### Long-term Memory
310+
311+
Why configure memory on the Agent: chat history and long-term memory belong to
312+
the same conversation. Wiring them once prevents nodes and middleware from
313+
clearing one store while leaving stale information in the other.
314+
315+
Pass the application's memory implementation with `setMemory()` before the
316+
Agent starts:
317+
318+
```php
319+
use NeuronAI\Chat\History\SQLChatHistory;
320+
321+
$agent = MyAgent::make(threadId: $threadId)
322+
->setChatHistory(new SQLChatHistory($pdo))
323+
->setMemory($memory);
324+
325+
$state = $agent->chat(new UserMessage($input));
326+
```
327+
328+
The order of `setChatHistory()` and `setMemory()` does not matter. Do not create
329+
`MemoryAwareChatHistory` yourself: the framework wraps the configured history
330+
internally and gives that same instance to all Agent nodes and middleware.
331+
332+
For class-based configuration, use the lazy hook:
333+
334+
```php
335+
use NeuronAI\Agent\Memory\MemoryInterface;
336+
337+
class MyAgent extends Agent
338+
{
339+
protected function memory(): ?MemoryInterface
340+
{
341+
return new ProjectMemory(/* ... */);
342+
}
343+
}
344+
```
345+
346+
An explicit `setMemory()` call takes precedence over `memory()`. Memory must be
347+
configured before the Agent graph is composed or execution starts; late
348+
configuration throws an `AgentException` instead of leaving already-created
349+
nodes with a stale history instance.
350+
351+
When any node or middleware calls `flushAll()` on chat history, the framework
352+
first calls `$memory->forget($threadId)` and then clears the chat history. This
353+
also applies to `Summarization` and custom middleware. If forgetting memory
354+
fails, chat history is not cleared and the exception is propagated. Use
355+
`flushAll()` only when both the conversation history and its long-term memory
356+
should be reset.
357+
358+
Without `setMemory()` or the `memory()` hook, chat history works exactly as
359+
before.
360+
361+
### Chat History Backends
310362
- `InMemoryChatHistory` - Default, session-based
311363
- `FileChatHistory` - Persist to file
312364
- `SQLChatHistory` - Database-backed

src/Agent/AGENTS.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,49 @@ O(1) instead of embedding the conversation. Consequences:
199199
response; `AgentState::getSteps()` reports the current execution cycle's
200200
messages only (transient, available even on an interrupted final state).
201201

202+
### Memory-aware history wiring
203+
204+
Long-term memory is configured on the Agent, not on individual nodes or
205+
middleware:
206+
207+
```php
208+
$agent = SupportAgent::make(threadId: $threadId)
209+
->setChatHistory($history)
210+
->setMemory($memory);
211+
```
212+
213+
`setMemory(MemoryInterface $memory)` makes the Agent wrap its chat history in
214+
the internal `MemoryAwareChatHistory` decorator. Developers must not construct
215+
or attach this decorator themselves. The Agent creates it once and injects the
216+
same effective history into every node, so middleware reading
217+
`$node->getChatHistory()` participates automatically. Calling `setMemory()`
218+
before or after `setChatHistory()` produces the same result.
219+
220+
Subclasses may provide the dependency through the lazy hook instead:
221+
222+
```php
223+
protected function memory(): ?MemoryInterface
224+
{
225+
return new ProjectMemory(/* ... */);
226+
}
227+
```
228+
229+
An explicit `setMemory()` call wins over the hook. Configure memory before the
230+
Agent graph is composed or execution begins; changing it afterwards throws an
231+
`AgentException`, because already-composed nodes would otherwise keep a stale
232+
history dependency.
233+
234+
`ChatHistoryInterface::flushAll()` is the shared destructive boundary. On a
235+
memory-aware Agent it first calls `MemoryInterface::forget($threadId)` and only
236+
then clears chat history. This covers calls made by built-in middleware such as
237+
`Summarization` and by developer middleware without adding a separate callback
238+
or event. If forgetting fails, the chat history remains untouched and the
239+
exception propagates. Consequently, custom code must call `flushAll()` only
240+
when both stores should be reset for that thread.
241+
242+
Without configured memory, `getChatHistory()` returns the original history and
243+
Agent behavior is unchanged.
244+
202245
## Persistence & Tool Approval
203246

204247
`ToolNode` gates tool execution behind human approval — there is no middleware

src/Agent/Agent.php

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use NeuronAI\Agent\Events\AgentStartEvent;
99
use NeuronAI\Agent\Events\AIInferenceEvent;
1010
use NeuronAI\Agent\Events\ToolCallEvent;
11+
use NeuronAI\Agent\Memory\MemoryAwareChatHistory;
12+
use NeuronAI\Agent\Memory\MemoryInterface;
1113
use NeuronAI\Agent\Nodes\ChatNode;
1214
use NeuronAI\Agent\Nodes\ParallelToolNode;
1315
use NeuronAI\Agent\Nodes\StartNode;
@@ -42,6 +44,16 @@ class Agent extends Workflow implements AgentInterface
4244

4345
protected ChatHistoryInterface $chatHistory;
4446

47+
/**
48+
* The history instance shared by the composed nodes. When memory is
49+
* configured this is a decorator around the developer-supplied history.
50+
*/
51+
protected ?ChatHistoryInterface $effectiveChatHistory = null;
52+
53+
protected ?MemoryInterface $memory = null;
54+
55+
protected bool $memoryResolved = false;
56+
4557
/**
4658
* The conversation this run belongs to, and the run's declared workflow
4759
* ID. Assigned exactly once through adoptThreadId() and NEVER generated —
@@ -109,6 +121,42 @@ protected function attachChatHistory(ChatHistoryInterface $chatHistory): void
109121
}
110122

111123
$this->chatHistory = $chatHistory;
124+
$this->effectiveChatHistory = null;
125+
}
126+
127+
/**
128+
* Provide the default long-term memory implementation. Subclasses may
129+
* override this hook; null keeps the Agent memory-free.
130+
*/
131+
protected function memory(): ?MemoryInterface
132+
{
133+
return null;
134+
}
135+
136+
/**
137+
* Configure long-term memory before the Agent graph is composed.
138+
*/
139+
public function setMemory(MemoryInterface $memory): self
140+
{
141+
if ($this->eventNodeMap !== []) {
142+
throw new AgentException('Memory must be configured before the Agent starts executing.');
143+
}
144+
145+
$this->memory = $memory;
146+
$this->memoryResolved = true;
147+
$this->effectiveChatHistory = null;
148+
149+
return $this;
150+
}
151+
152+
public function getMemory(): ?MemoryInterface
153+
{
154+
if (!$this->memoryResolved) {
155+
$this->memory = $this->memory();
156+
$this->memoryResolved = true;
157+
}
158+
159+
return $this->memory;
112160
}
113161

114162
/**
@@ -139,7 +187,14 @@ public function getChatHistory(): ChatHistoryInterface
139187
$this->attachChatHistory($this->chatHistory());
140188
}
141189

142-
return $this->chatHistory;
190+
if ($this->effectiveChatHistory === null) {
191+
$memory = $this->getMemory();
192+
$this->effectiveChatHistory = $memory instanceof MemoryInterface
193+
? new MemoryAwareChatHistory($this->chatHistory, $memory)
194+
: $this->chatHistory;
195+
}
196+
197+
return $this->effectiveChatHistory;
143198
}
144199

145200
/**

src/Agent/AgentInterface.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44

55
namespace NeuronAI\Agent;
66

7+
use Generator;
8+
use NeuronAI\Agent\Memory\MemoryInterface;
79
use NeuronAI\Chat\History\ChatHistoryInterface;
810
use NeuronAI\Chat\Messages\Message;
911
use NeuronAI\Chat\Messages\Stream\Adapters\StreamAdapterInterface;
1012
use NeuronAI\Chat\Messages\SystemMessage;
1113
use NeuronAI\Providers\AIProviderInterface;
1214
use NeuronAI\Tools\ToolInterface;
1315
use NeuronAI\Tools\Toolkits\ToolkitInterface;
14-
use Generator;
1516

1617
interface AgentInterface
1718
{
@@ -42,6 +43,10 @@ public function setChatHistory(ChatHistoryInterface $chatHistory): AgentInterfac
4243

4344
public function getChatHistory(): ChatHistoryInterface;
4445

46+
public function setMemory(MemoryInterface $memory): AgentInterface;
47+
48+
public function getMemory(): ?MemoryInterface;
49+
4550
/**
4651
* The agent's thread identity — the conversation this run belongs to and
4752
* the run's declared workflow ID — or null when the run is not
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace NeuronAI\Agent\Memory;
6+
7+
use NeuronAI\Chat\History\ChatHistoryInterface;
8+
use NeuronAI\Chat\Messages\Message;
9+
use NeuronAI\Exceptions\ChatHistoryException;
10+
11+
/**
12+
* Internal decorator that keeps explicit conversation deletion consistent
13+
* across chat history and long-term memory.
14+
*/
15+
final class MemoryAwareChatHistory implements ChatHistoryInterface
16+
{
17+
public function __construct(
18+
protected ChatHistoryInterface $history,
19+
protected MemoryInterface $memory,
20+
) {
21+
}
22+
23+
public function setThreadId(string $threadId): void
24+
{
25+
$this->history->setThreadId($threadId);
26+
}
27+
28+
public function getThreadId(): ?string
29+
{
30+
return $this->history->getThreadId();
31+
}
32+
33+
public function addMessage(Message $message): ChatHistoryInterface
34+
{
35+
$this->history->addMessage($message);
36+
37+
return $this;
38+
}
39+
40+
public function getMessages(): array
41+
{
42+
return $this->history->getMessages();
43+
}
44+
45+
public function getLastMessage(): Message|false
46+
{
47+
return $this->history->getLastMessage();
48+
}
49+
50+
public function flushAll(): ChatHistoryInterface
51+
{
52+
$threadId = $this->getThreadId() ?? throw new ChatHistoryException(
53+
'Cannot clear memory for an unbound chat history.'
54+
);
55+
56+
// Forget first: an unavailable memory store must not leave recalled
57+
// content behind while the chat history appears to be deleted.
58+
$this->memory->forget($threadId);
59+
$this->history->flushAll();
60+
61+
return $this;
62+
}
63+
64+
public function calculateTotalUsage(): int
65+
{
66+
return $this->history->calculateTotalUsage();
67+
}
68+
69+
public function jsonSerialize(): array
70+
{
71+
return $this->history->jsonSerialize();
72+
}
73+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace NeuronAI\Agent\Memory;
6+
7+
interface MemoryInterface
8+
{
9+
/**
10+
* Permanently remove every memory associated with a conversation.
11+
*/
12+
public function forget(string $threadId): void;
13+
}

0 commit comments

Comments
 (0)