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
Original file line number Diff line number Diff line change
Expand Up @@ -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
// -----------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Loading