Skip to content
Open
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
78 changes: 62 additions & 16 deletions agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
Expand Down Expand Up @@ -277,6 +278,20 @@ public class ReActAgent extends AgentBase implements AutoCloseable {
private final ConcurrentHashMap<String, PermissionEngine> permissionEngineCache =
new ConcurrentHashMap<>();

/**
* Maximum number of slot entries to retain in the state and permission-engine caches.
* When the cache exceeds this limit the oldest entries are evicted, preventing
* unbounded memory growth in long-running singleton deployments.
*/
private static final int MAX_CACHED_SLOTS = 1000;

/**
* Insertion-order tracker for the slot caches. After each {@code put} or
* {@code computeIfAbsent} that adds a new key, the oldest entries are trimmed when
* the cache exceeds {@link #MAX_CACHED_SLOTS}.
*/
private final ConcurrentLinkedDeque<String> slotOrder = new ConcurrentLinkedDeque<>();

private final ModelConfig modelConfig;
private final ReactConfig reactConfig;

Expand Down Expand Up @@ -471,27 +486,32 @@ private CallExecution activateSlotForContext(RuntimeContext ctx) {
getAgentId(),
initialActiveToolGroups);
stateCache.put(slot, loaded);
recordSlotAccess(slot);
} else {
loaded =
stateCache.computeIfAbsent(
slot,
k ->
loadOrCreateAgentStateForSlot(
null,
finalUid,
finalSid,
initialPermissionContext,
getAgentId(),
initialActiveToolGroups));
k -> {
recordSlotAccess(slot);
return loadOrCreateAgentStateForSlot(
null,
finalUid,
finalSid,
initialPermissionContext,
getAgentId(),
initialActiveToolGroups);
});
}
PermissionEngine loadedEngine;
if (stateStore != null) {
loadedEngine = new PermissionEngine(loaded.getPermissionContext());
permissionEngineCache.put(slot, loadedEngine);
trimCaches();
} else {
loadedEngine =
permissionEngineCache.computeIfAbsent(
slot, k -> new PermissionEngine(loaded.getPermissionContext()));
trimCaches();
}
CallExecution scope = new CallExecution(loaded, loadedEngine, slot);
if (toolkit != null) {
Expand All @@ -500,6 +520,29 @@ private CallExecution activateSlotForContext(RuntimeContext ctx) {
return scope;
}

/**
* Records a slot access in the insertion-order tracker. If the key was already present it is
* promoted to the tail (most-recently-used position). Safe to call from multiple threads.
*/
private void recordSlotAccess(String slot) {
// Remove-then-add promotes an existing slot to the tail (most recent).
slotOrder.remove(slot);
slotOrder.addLast(slot);
}

/**
* Evicts the oldest entries from both caches when the slot count exceeds
* {@link #MAX_CACHED_SLOTS}. Call after each cache write that may have added a new entry.
*/
private void trimCaches() {
while (slotOrder.size() > MAX_CACHED_SLOTS) {
String oldest = slotOrder.pollFirst();
if (oldest == null) break;
stateCache.remove(oldest);
permissionEngineCache.remove(oldest);
}
}

// ==================== Config assembly helpers ====================

private static ModelConfig assembleModelConfig(Builder b) {
Expand Down Expand Up @@ -3663,14 +3706,17 @@ public AgentState getAgentState(String userId, String sessionId) {
String slot = slotKey(userId, sessionId);
return stateCache.computeIfAbsent(
slot,
k ->
loadOrCreateAgentStateForSlot(
stateStore,
userId,
sessionId,
initialPermissionContext,
getAgentId(),
initialActiveToolGroups));
k -> {
recordSlotAccess(slot);
trimCaches();
return loadOrCreateAgentStateForSlot(
stateStore,
userId,
sessionId,
initialPermissionContext,
getAgentId(),
initialActiveToolGroups);
});
}

/**
Expand Down
Loading