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
46 changes: 44 additions & 2 deletions agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,16 @@ protected void afterAgentExecution() {
unbindRuntimeContextFromHooks();
}

@Override
protected void afterCallScopeExecution(Object callScope) {
if (stateStore == null) {
return;
}
CallExecution scope = (CallExecution) callScope;
stateCache.remove(scope.slotKey, scope.state);
permissionEngineCache.remove(scope.slotKey, scope.permissionEngine);
}

private RuntimeContext buildMergedRuntimeContext(RuntimeContext run) {
if (run == null) {
if (toolExecutionContext != null) {
Expand Down Expand Up @@ -3758,6 +3768,38 @@ public void saveAgentState(String userId, String sessionId) {
}
}

/**
* Evicts the in-memory state and permission engine for one session.
*
* <p>This does not delete persisted state. A later access reloads the session from the
* configured {@link AgentStateStore}, or creates fresh in-memory state when no store is
* configured. Call this only after any in-flight call for the session has terminated.
*
* @param userId user identity for the slot (may be {@code null})
* @param sessionId session identity (falls back to the default session id when blank)
*/
public void evictSession(String userId, String sessionId) {
String sid = (sessionId == null || sessionId.isBlank()) ? defaultSessionId : sessionId;
String slot = slotKey(userId, sid);
stateCache.remove(slot);
permissionEngineCache.remove(slot);
}

/**
* Evicts all in-memory state and permission engines belonging to one user.
*
* <p>This does not delete persisted state. Call this only after any in-flight calls for the
* user have terminated.
*
* @param userId user identity ({@code null} or blank selects anonymous sessions)
*/
public void evictUser(String userId) {
String normalizedUser = userId == null || userId.isBlank() ? "__anon__" : userId;
String prefix = normalizedUser + "/";
stateCache.keySet().removeIf(slot -> slot.startsWith(prefix));
permissionEngineCache.keySet().removeIf(slot -> slot.startsWith(prefix));
}

/** Returns the {@link AgentStateStore} configured for state persistence, or {@code null}. */
public AgentStateStore getStateStore() {
return stateStore;
Expand Down Expand Up @@ -3841,8 +3883,8 @@ public static Builder builder() {

@Override
public void close() {
// No-op for the core ReActAgent. Subclasses / wrappers (HarnessAgent) may release
// additional resources here.
stateCache.clear();
permissionEngineCache.clear();
}

// ==================== Builder ====================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,11 @@ private Mono<Msg> runLifecycleBody(
.onErrorResume(
createErrorHandler(
msgs.toArray(new Msg[0]))));
return scope == null ? body : body.contextWrite(c -> c.put(CALL_SCOPE_KEY, scope));
if (scope == null) {
return body;
}
return body.contextWrite(c -> c.put(CALL_SCOPE_KEY, scope))
.doFinally(signal -> afterCallScopeExecution(scope));
}

/**
Expand Down Expand Up @@ -595,6 +599,15 @@ protected Object beforeAgentExecution(List<Msg> msgs, RuntimeContext rc) {
*/
protected void afterAgentExecution() {}

/**
* Invoked when a call-specific scope terminates after success, error, or cancellation.
* Subclasses can release resources owned by the exact scope without consulting shared
* instance fields that may already refer to another concurrent call.
*
* @param callScope the scope returned by {@link #beforeAgentExecution(List, RuntimeContext)}
*/
protected void afterCallScopeExecution(Object callScope) {}

/**
* Pushes {@code ctx} to all {@link RuntimeContextAware} hooks registered for this agent. The
* per-call {@link RuntimeContext} itself is no longer stored on a shared instance field; it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.agentscope.core.agent;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
Expand Down Expand Up @@ -110,6 +111,18 @@ protected Mono<Msg> handleInterrupt(InterruptContext context, Msg... originalArg
}
}

static class ScopedTestAgent extends TestAgent {

ScopedTestAgent(String name) {
super(name);
}

@Override
protected Object beforeAgentExecution(List<Msg> msgs, RuntimeContext rc) {
return new Object();
}
}

@BeforeEach
void setUp() {
agent = new TestAgent(TestConstants.TEST_AGENT_NAME);
Expand Down Expand Up @@ -153,6 +166,19 @@ void testSingleMessageInput() {
TestConstants.TEST_ASSISTANT_RESPONSE, text, "Response text should match expected");
}

@Test
@DisplayName("Should support call scope with the default terminal callback")
void testDefaultAfterCallScopeExecution() {
ScopedTestAgent scopedAgent = new ScopedTestAgent(TestConstants.TEST_AGENT_NAME);
Msg userMsg = TestUtils.createUserMessage("User", TestConstants.TEST_USER_INPUT);

assertDoesNotThrow(
() ->
scopedAgent
.call(userMsg)
.block(Duration.ofMillis(TestConstants.DEFAULT_TEST_TIMEOUT_MS)));
}

@Test
@DisplayName("Should handle multiple message input")
void testMultipleMessageInput() {
Expand Down
Loading
Loading