Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,7 @@ public static class Builder {
// Long-term memory configuration
private LongTermMemory longTermMemory;
private LongTermMemoryMode longTermMemoryMode = LongTermMemoryMode.BOTH;
private boolean longTermMemoryAsyncRecord = false;

// State persistence configuration
private StatePersistence statePersistence;
Expand Down Expand Up @@ -1418,6 +1419,29 @@ public Builder longTermMemoryMode(LongTermMemoryMode mode) {
return this;
}

/**
* Sets whether long-term memory recording should be performed asynchronously.
*
* <p>When enabled, the framework will record memories to long-term storage
* in a fire-and-forget manner, without blocking the agent's main execution flow.
* This improves response latency but means memory persistence is not guaranteed
* before the agent returns its response.
*
* <p>When disabled (default), the framework waits for the recording operation
* to complete before returning the agent's response. This ensures memory
* persistence is finalized but may increase response latency.
*
* <p>Note: This setting only affects the static control mode (STATIC_CONTROL, BOTH).
* Agent-controlled recording through tools is always synchronous.
*
* @param asyncRecord Whether to record memories asynchronously
* @return This builder instance for method chaining
*/
public Builder longTermMemoryAsyncRecord(boolean asyncRecord) {
this.longTermMemoryAsyncRecord = asyncRecord;
return this;
}

/**
* Sets the state persistence configuration.
*
Expand Down Expand Up @@ -1591,7 +1615,8 @@ private void configureLongTermMemory(Toolkit agentToolkit) {
if (longTermMemoryMode == LongTermMemoryMode.STATIC_CONTROL
|| longTermMemoryMode == LongTermMemoryMode.BOTH) {
StaticLongTermMemoryHook hook =
new StaticLongTermMemoryHook(longTermMemory, memory);
new StaticLongTermMemoryHook(
longTermMemory, memory, longTermMemoryAsyncRecord);
hooks.add(hook);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

/**
* Static Long-Term Memory Hook for automatic memory management.
Expand Down Expand Up @@ -76,15 +77,29 @@ public class StaticLongTermMemoryHook implements Hook {

private final LongTermMemory longTermMemory;
private final Memory memory;
private final boolean asyncRecord;

/**
* Creates a new StaticLongTermMemoryHook.
* Creates a new StaticLongTermMemoryHook with synchronous recording.
*
* @param longTermMemory The long-term memory instance for persistent storage
* @param memory The agent's memory for accessing conversation history
* @throws IllegalArgumentException if longTermMemory or memory is null
*/
public StaticLongTermMemoryHook(LongTermMemory longTermMemory, Memory memory) {
this(longTermMemory, memory, false);
}

/**
* Creates a new StaticLongTermMemoryHook.
*
* @param longTermMemory The long-term memory instance for persistent storage
* @param memory The agent's memory for accessing conversation history
* @param asyncRecord Whether to record memories asynchronously (fire-and-forget)
* @throws IllegalArgumentException if longTermMemory or memory is null
*/
public StaticLongTermMemoryHook(
LongTermMemory longTermMemory, Memory memory, boolean asyncRecord) {
if (longTermMemory == null) {
throw new IllegalArgumentException("Long-term memory cannot be null");
}
Expand All @@ -93,6 +108,7 @@ public StaticLongTermMemoryHook(LongTermMemory longTermMemory, Memory memory) {
}
this.longTermMemory = longTermMemory;
this.memory = memory;
this.asyncRecord = asyncRecord;
}

@Override
Expand Down Expand Up @@ -180,6 +196,10 @@ private Mono<PreCallEvent> handlePreCall(PreCallEvent event) {
* the long-term memory backend (e.g., Mem0) to extract memorable information from
* the entire conversation context.
*
* <p>When {@code asyncRecord} is enabled, the recording is performed in a
* fire-and-forget manner that does not block the agent's response. Otherwise,
* the recording completes before returning the event.
*
* @param event the PostCallEvent
* @return Mono containing the unmodified event
*/
Expand All @@ -191,16 +211,35 @@ private Mono<PostCallEvent> handlePostCall(PostCallEvent event) {
}

// Record to long-term memory
return longTermMemory
.record(allMessages)
.thenReturn(event)
.onErrorResume(
error -> {
// Log error but don't interrupt the flow
log.warn(
"Failed to record to long-term memory: {}", error.getMessage());
return Mono.just(event);
});
if (asyncRecord) {
// Fire-and-forget: schedule on boundedElastic so the agent's
// response is not blocked. subscribe() is intentional here —
// the record pipeline runs independently of the event chain.
longTermMemory
.record(allMessages)
.subscribeOn(Schedulers.boundedElastic())
.onErrorResume(
error -> {
log.warn(
"Failed to asynchronously record to long-term memory: {}",
error.getMessage());
return Mono.empty();
})
.subscribe();
return Mono.just(event);
} else {
return longTermMemory
.record(allMessages)
.thenReturn(event)
.onErrorResume(
error -> {
// Log error but don't interrupt the flow
log.warn(
"Failed to record to long-term memory: {}",
error.getMessage());
return Mono.just(event);
});
}
}

/**
Expand Down
Loading
Loading