Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0058bff

Browse files
authored
feat(memory): support async recording for StaticLongTermMemoryHook. (#1177)
## AgentScope-Java Version io.agentscope:agentscope-parent:pom:1.0.12-SNAPSHOT ## Description LongTermMemory will block the agent when the LLM stops responding. This commit enables the framework to record long term memory asynchronously. Added `longTermMemoryAsyncRecord` option to `ReActAgent.Builder` to support fire-and-forget long-term memory recording in `STATIC_CONTROL` / `BOTH` mode. When enabled, the `StaticLongTermMemoryHook` performs `record()` asynchronously without blocking the agent's response, reducing latency in `STATIC_CONTROL` mode. Agent-controlled recording (`AGENT_CONTROL`) remains synchronous. ## Checklist Please check the following items before code is ready to be reviewed. - `ReActAgent.Builder` — new `longTermMemoryAsyncRecord(boolean)` setter - `StaticLongTermMemoryHook` — overloaded constructor accepting `asyncRecord` flag; `handlePostCall()` splits sync/async paths - `StaticLongTermMemoryHookTest` — added 3 async recording test cases - [X] Code has been formatted with `mvn spotless:apply` - [X] All tests are passing (`mvn test`) - [X] Javadoc comments are complete and follow project conventions - [X] Related documentation has been updated (e.g. links, examples, etc.) - [X] Code is ready for review
1 parent a951efe commit 0058bff

4 files changed

Lines changed: 438 additions & 12 deletions

File tree

agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,7 @@ public static class Builder {
11201120
// Long-term memory configuration
11211121
private LongTermMemory longTermMemory;
11221122
private LongTermMemoryMode longTermMemoryMode = LongTermMemoryMode.BOTH;
1123+
private boolean longTermMemoryAsyncRecord = false;
11231124

11241125
// State persistence configuration
11251126
private StatePersistence statePersistence;
@@ -1418,6 +1419,29 @@ public Builder longTermMemoryMode(LongTermMemoryMode mode) {
14181419
return this;
14191420
}
14201421

1422+
/**
1423+
* Sets whether long-term memory recording should be performed asynchronously.
1424+
*
1425+
* <p>When enabled, the framework will record memories to long-term storage
1426+
* in a fire-and-forget manner, without blocking the agent's main execution flow.
1427+
* This improves response latency but means memory persistence is not guaranteed
1428+
* before the agent returns its response.
1429+
*
1430+
* <p>When disabled (default), the framework waits for the recording operation
1431+
* to complete before returning the agent's response. This ensures memory
1432+
* persistence is finalized but may increase response latency.
1433+
*
1434+
* <p>Note: This setting only affects the static control mode (STATIC_CONTROL, BOTH).
1435+
* Agent-controlled recording through tools is always synchronous.
1436+
*
1437+
* @param asyncRecord Whether to record memories asynchronously
1438+
* @return This builder instance for method chaining
1439+
*/
1440+
public Builder longTermMemoryAsyncRecord(boolean asyncRecord) {
1441+
this.longTermMemoryAsyncRecord = asyncRecord;
1442+
return this;
1443+
}
1444+
14211445
/**
14221446
* Sets the state persistence configuration.
14231447
*
@@ -1591,7 +1615,8 @@ private void configureLongTermMemory(Toolkit agentToolkit) {
15911615
if (longTermMemoryMode == LongTermMemoryMode.STATIC_CONTROL
15921616
|| longTermMemoryMode == LongTermMemoryMode.BOTH) {
15931617
StaticLongTermMemoryHook hook =
1594-
new StaticLongTermMemoryHook(longTermMemory, memory);
1618+
new StaticLongTermMemoryHook(
1619+
longTermMemory, memory, longTermMemoryAsyncRecord);
15951620
hooks.add(hook);
15961621
}
15971622
}

agentscope-core/src/main/java/io/agentscope/core/memory/StaticLongTermMemoryHook.java

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import org.slf4j.Logger;
3030
import org.slf4j.LoggerFactory;
3131
import reactor.core.publisher.Mono;
32+
import reactor.core.scheduler.Scheduler;
33+
import reactor.core.scheduler.Schedulers;
3234

3335
/**
3436
* Static Long-Term Memory Hook for automatic memory management.
@@ -73,18 +75,38 @@
7375
public class StaticLongTermMemoryHook implements Hook {
7476

7577
private static final Logger log = LoggerFactory.getLogger(StaticLongTermMemoryHook.class);
78+
// Dedicated async scheduler for long-term memory recording:
79+
// - max 1 concurrent workers
80+
// - max 3 queued tasks (new tasks are rejected when saturated)
81+
// This avoids unbounded queuing on the global boundedElastic scheduler.
82+
private static final Scheduler ASYNC_RECORD_SCHEDULER =
83+
Schedulers.newBoundedElastic(1, 3, "long-term-memory-record");
7684

7785
private final LongTermMemory longTermMemory;
7886
private final Memory memory;
87+
private final boolean asyncRecord;
7988

8089
/**
81-
* Creates a new StaticLongTermMemoryHook.
90+
* Creates a new StaticLongTermMemoryHook with synchronous recording.
8291
*
8392
* @param longTermMemory The long-term memory instance for persistent storage
8493
* @param memory The agent's memory for accessing conversation history
8594
* @throws IllegalArgumentException if longTermMemory or memory is null
8695
*/
8796
public StaticLongTermMemoryHook(LongTermMemory longTermMemory, Memory memory) {
97+
this(longTermMemory, memory, false);
98+
}
99+
100+
/**
101+
* Creates a new StaticLongTermMemoryHook.
102+
*
103+
* @param longTermMemory The long-term memory instance for persistent storage
104+
* @param memory The agent's memory for accessing conversation history
105+
* @param asyncRecord Whether to record memories asynchronously (fire-and-forget)
106+
* @throws IllegalArgumentException if longTermMemory or memory is null
107+
*/
108+
public StaticLongTermMemoryHook(
109+
LongTermMemory longTermMemory, Memory memory, boolean asyncRecord) {
88110
if (longTermMemory == null) {
89111
throw new IllegalArgumentException("Long-term memory cannot be null");
90112
}
@@ -93,6 +115,7 @@ public StaticLongTermMemoryHook(LongTermMemory longTermMemory, Memory memory) {
93115
}
94116
this.longTermMemory = longTermMemory;
95117
this.memory = memory;
118+
this.asyncRecord = asyncRecord;
96119
}
97120

98121
@Override
@@ -179,6 +202,16 @@ private Mono<PreCallEvent> handlePreCall(PreCallEvent event) {
179202
* the long-term memory backend (e.g., Mem0) to extract memorable information from
180203
* the entire conversation context.
181204
*
205+
* <p>When {@code asyncRecord} is enabled, the recording is performed in a
206+
* fire-and-forget manner that does not block the agent's response. Async recording
207+
* uses a bounded scheduler (1 workers, queue size 3). When saturated, new record
208+
* tasks are dropped and logged.
209+
*
210+
* <p><b>Trade-offs:</b> This async path intentionally decouples recording from the
211+
* main event chain. The returned subscription is not retained in this mode, so
212+
* in-flight record tasks are not explicitly cancelled by this class.
213+
* Otherwise, the recording completes before returning the event.
214+
*
182215
* @param event the PostCallEvent
183216
* @return Mono containing the unmodified event
184217
*/
@@ -190,16 +223,39 @@ private Mono<PostCallEvent> handlePostCall(PostCallEvent event) {
190223
}
191224

192225
// Record to long-term memory
193-
return longTermMemory
194-
.record(allMessages)
195-
.thenReturn(event)
196-
.onErrorResume(
197-
error -> {
198-
// Log error but don't interrupt the flow
199-
log.warn(
200-
"Failed to record to long-term memory: {}", error.getMessage());
201-
return Mono.just(event);
202-
});
226+
if (asyncRecord) {
227+
// Fire-and-forget: schedule on a dedicated bounded scheduler so the agent's
228+
// response is not blocked while still limiting backlog growth.
229+
return Mono.deferContextual(
230+
ctxView -> {
231+
longTermMemory
232+
.record(allMessages)
233+
.subscribeOn(ASYNC_RECORD_SCHEDULER)
234+
.contextWrite(context -> context.putAll(ctxView))
235+
.onErrorResume(
236+
error -> {
237+
log.warn(
238+
"Failed to asynchronously record to long-term"
239+
+ " memory: {}",
240+
error.getMessage());
241+
return Mono.empty();
242+
})
243+
.subscribe();
244+
return Mono.just(event);
245+
});
246+
} else {
247+
return longTermMemory
248+
.record(allMessages)
249+
.thenReturn(event)
250+
.onErrorResume(
251+
error -> {
252+
// Log error but don't interrupt the flow
253+
log.warn(
254+
"Failed to record to long-term memory: {}",
255+
error.getMessage());
256+
return Mono.just(event);
257+
});
258+
}
203259
}
204260

205261
/**

0 commit comments

Comments
 (0)