diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java index 33c717a56..93c260431 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java @@ -459,6 +459,48 @@ public void interrupt(Msg msg) { delegate.interrupt(msg); } + /** + * Interrupts the in-flight call identified by the given {@link RuntimeContext}. + * + * @param ctx the runtime context identifying the session to interrupt + */ + public void interrupt(RuntimeContext ctx) { + delegate.interrupt(ctx); + } + + /** + * Interrupts the in-flight call identified by the given {@link RuntimeContext} with an + * associated user message. + * + * @param ctx the runtime context identifying the session to interrupt + * @param msg optional user message to attach to the interrupt signal + */ + public void interrupt(RuntimeContext ctx, Msg msg) { + delegate.interrupt(ctx, msg); + } + + /** + * Interrupts the in-flight call for a specific {@code (userId, sessionId)} session. + * + * @param userId the user id ({@code null} = anonymous / single-tenant) + * @param sessionId the session id + */ + public void interrupt(String userId, String sessionId) { + delegate.interrupt(userId, sessionId); + } + + /** + * Interrupts the in-flight call for a specific {@code (userId, sessionId)} session with an + * associated user message. + * + * @param userId the user id ({@code null} = anonymous / single-tenant) + * @param sessionId the session id + * @param msg optional user message to attach to the interrupt signal + */ + public void interrupt(String userId, String sessionId, Msg msg) { + delegate.interrupt(userId, sessionId, msg); + } + // ----------------------------------------------------------------- // Channel / Gateway // ----------------------------------------------------------------- diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java index abfda8e61..b49c68f08 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java @@ -758,4 +758,120 @@ private static AgentTool mockAgentTool(String name) { when(tool.getParameters()).thenReturn(Map.of("type", "object", "properties", Map.of())); return tool; } + + @Test + void perSessionInterruptPassthrough_targetByUserIdAndSessionId() throws Exception { + Files.createDirectories(workspace); + Model model = stubModel("ok"); + HarnessAgent agent = + HarnessAgent.builder() + .name("t") + .model(model) + .workspace(workspace) + .abstractFilesystem(new LocalFilesystem(workspace)) + .build(); + + // Activate two distinct sessions by accessing their state. + String uid = "u1"; + String sidA = "sess-A"; + String sidB = "sess-B"; + agent.getDelegate().getAgentState(uid, sidA); + agent.getDelegate().getAgentState(uid, sidB); + + // Interrupt session A only. + agent.interrupt(uid, sidA); + + assertTrue( + agent.getDelegate().getAgentState(uid, sidA).interruptControl().isInterrupted(), + "session A should be interrupted"); + assertFalse( + agent.getDelegate().getAgentState(uid, sidB).interruptControl().isInterrupted(), + "session B should NOT be interrupted"); + } + + @Test + void perSessionInterruptPassthrough_targetByUserIdAndSessionIdWithMsg() throws Exception { + Files.createDirectories(workspace); + Model model = stubModel("ok"); + HarnessAgent agent = + HarnessAgent.builder() + .name("t") + .model(model) + .workspace(workspace) + .abstractFilesystem(new LocalFilesystem(workspace)) + .build(); + + String uid = "u1"; + String sid = "sess-X"; + agent.getDelegate().getAgentState(uid, sid); + + Msg interruptMsg = + Msg.builder() + .name("user") + .role(MsgRole.USER) + .content(TextBlock.builder().text("stop now").build()) + .build(); + agent.interrupt(uid, sid, interruptMsg); + + var ctrl = agent.getDelegate().getAgentState(uid, sid).interruptControl(); + assertTrue(ctrl.isInterrupted()); + assertNotNull(ctrl.getUserMessage()); + assertEquals("stop now", ctrl.getUserMessage().getTextContent()); + } + + @Test + void perSessionInterruptPassthrough_targetByRuntimeContext() throws Exception { + Files.createDirectories(workspace); + Model model = stubModel("ok"); + HarnessAgent agent = + HarnessAgent.builder() + .name("t") + .model(model) + .workspace(workspace) + .abstractFilesystem(new LocalFilesystem(workspace)) + .build(); + + RuntimeContext ctx = RuntimeContext.builder().userId("u2").sessionId("sess-RC").build(); + agent.getDelegate().getAgentState(ctx.getUserId(), ctx.getSessionId()); + + agent.interrupt(ctx); + + assertTrue( + agent.getDelegate() + .getAgentState(ctx.getUserId(), ctx.getSessionId()) + .interruptControl() + .isInterrupted()); + } + + @Test + void perSessionInterruptPassthrough_targetByRuntimeContextWithMsg() throws Exception { + Files.createDirectories(workspace); + Model model = stubModel("ok"); + HarnessAgent agent = + HarnessAgent.builder() + .name("t") + .model(model) + .workspace(workspace) + .abstractFilesystem(new LocalFilesystem(workspace)) + .build(); + + RuntimeContext ctx = RuntimeContext.builder().userId("u3").sessionId("sess-RC2").build(); + agent.getDelegate().getAgentState(ctx.getUserId(), ctx.getSessionId()); + + Msg interruptMsg = + Msg.builder() + .name("user") + .role(MsgRole.USER) + .content(TextBlock.builder().text("cancel").build()) + .build(); + agent.interrupt(ctx, interruptMsg); + + var ctrl = + agent.getDelegate() + .getAgentState(ctx.getUserId(), ctx.getSessionId()) + .interruptControl(); + assertTrue(ctrl.isInterrupted()); + assertNotNull(ctrl.getUserMessage()); + assertEquals("cancel", ctrl.getUserMessage().getTextContent()); + } }