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
75 changes: 58 additions & 17 deletions agentscope-core/src/main/java/io/agentscope/core/ReActAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -2034,19 +2034,7 @@ private Mono<Msg> reasoning(int iter, boolean ignoreMaxIters) {
.onErrorResume(
InterruptedException.class,
error -> {
Msg msg = context.buildFinalMessage();
if (msg != null) {
boolean discard =
state.interruptControl().getSource()
== InterruptSource.SYSTEM
&& shutdownManager
.getConfig()
.partialReasoningPolicy()
== PartialReasoningPolicy.DISCARD;
if (!discard) {
state.contextMutable().add(msg);
}
}
persistInterruptedReasoningMessage(context.buildFinalMessage());
return Mono.error(error);
})
.flatMap(
Expand All @@ -2072,22 +2060,23 @@ private Mono<Msg> runPostReasoningPipeline(Msg msg, int iter) {
.flatMap(
event -> {
Msg eventMsg = event.getReasoningMessage();
if (eventMsg != null) {
state.contextMutable().add(eventMsg);
}

// HITL stop
if (event.isStopRequested()) {
if (eventMsg == null) {
return Mono.empty();
}
state.contextMutable().add(eventMsg);
return Mono.just(
eventMsg.withGenerateReason(
GenerateReason.REASONING_STOP_REQUESTED));
}

// gotoReasoning requested (e.g., by a PostReasoning hook)
if (event.isGotoReasoningRequested()) {
if (eventMsg != null) {
state.contextMutable().add(eventMsg);
}
List<Msg> gotoMsgs = event.getGotoReasoningMsgs();
if (gotoMsgs != null) {
state.contextMutable().addAll(gotoMsgs);
Expand All @@ -2097,11 +2086,26 @@ private Mono<Msg> runPostReasoningPipeline(Msg msg, int iter) {

// Check finish conditions
if (isFinished(eventMsg)) {
if (eventMsg != null) {
state.contextMutable().add(eventMsg);
}
return Mono.justOrEmpty(eventMsg);
}

// Continue to acting
return checkInterrupted().then(acting(iter));
return checkInterrupted()
.onErrorResume(
InterruptedException.class,
error -> {
persistInterruptedReasoningMessage(eventMsg);
return Mono.error(error);
})
.then(
Mono.defer(
() -> {
state.contextMutable().add(eventMsg);
return acting(iter);
}));
})
.switchIfEmpty(
Mono.defer(
Expand All @@ -2111,6 +2115,43 @@ private Mono<Msg> runPostReasoningPipeline(Msg msg, int iter) {
}));
}

/**
* Persist partial reasoning after an interrupt without leaving user-aborted tool calls in
* the conversation. Acting has not started at either call site, so retaining those calls
* would create orphaned tool uses with no possible result. System interrupts keep their
* existing resume policy.
*/
private void persistInterruptedReasoningMessage(Msg msg) {
if (msg == null) {
return;
}

InterruptSource source = state.interruptControl().getSource();
boolean discard =
source == InterruptSource.SYSTEM
&& shutdownManager.getConfig().partialReasoningPolicy()
== PartialReasoningPolicy.DISCARD;
if (discard) {
return;
}

Msg interruptedMsg = source == InterruptSource.USER ? withoutToolUseBlocks(msg) : msg;
if (interruptedMsg != null) {
state.contextMutable().add(interruptedMsg);
}
}

private static Msg withoutToolUseBlocks(Msg msg) {
if (!msg.hasContentBlocks(ToolUseBlock.class)) {
return msg;
}
List<ContentBlock> retained =
msg.getContent().stream()
.filter(block -> !(block instanceof ToolUseBlock))
.toList();
return retained.isEmpty() ? null : msg.withContent(retained);
}

/**
* Stream fine-grained {@link AgentEvent}s from a model call during reasoning.
*
Expand Down
Loading
Loading