fix(agent): migrate deprecated AgentScope hooks to middleware (#675)

This commit is contained in:
matevip 2026-11-02 01:02:25 -05:00
parent e46f176697
commit 952f73ddfa
2 changed files with 86 additions and 73 deletions

View File

@ -12,9 +12,6 @@ import io.agentscope.core.agent.Agent;
import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.event.AgentEvent; import io.agentscope.core.event.AgentEvent;
import io.agentscope.core.event.RequestStopEvent; import io.agentscope.core.event.RequestStopEvent;
import io.agentscope.core.hook.Hook;
import io.agentscope.core.hook.HookEvent;
import io.agentscope.core.hook.PostReasoningEvent;
import io.agentscope.core.message.ContentBlock; import io.agentscope.core.message.ContentBlock;
import io.agentscope.core.message.GenerateReason; import io.agentscope.core.message.GenerateReason;
import io.agentscope.core.message.Msg; import io.agentscope.core.message.Msg;
@ -22,7 +19,7 @@ import io.agentscope.core.message.TextBlock;
import io.agentscope.core.message.ToolUseBlock; import io.agentscope.core.message.ToolUseBlock;
import io.agentscope.core.middleware.AgentInput; import io.agentscope.core.middleware.AgentInput;
import io.agentscope.core.middleware.MiddlewareBase; import io.agentscope.core.middleware.MiddlewareBase;
import io.agentscope.core.middleware.ModelCallInput; import io.agentscope.core.middleware.ReasoningInput;
import io.agentscope.core.model.ChatResponse; import io.agentscope.core.model.ChatResponse;
import io.agentscope.core.model.GenerateOptions; import io.agentscope.core.model.GenerateOptions;
import io.agentscope.core.model.Model; import io.agentscope.core.model.Model;
@ -186,7 +183,6 @@ public final class NativePolicyDriver implements AgentRuntime {
.name("native-policy-driver") .name("native-policy-driver")
.model(session.model()) .model(session.model())
.toolkit(toolkit) .toolkit(toolkit)
.hook(session)
.middleware(toolkit.boundaryMiddleware()) .middleware(toolkit.boundaryMiddleware())
.middleware(session.middleware()) .middleware(session.middleware())
.maxIters(hardRoundCeiling + 1) .maxIters(hardRoundCeiling + 1)
@ -208,7 +204,7 @@ public final class NativePolicyDriver implements AgentRuntime {
}); });
} }
private final class Session implements Hook { private final class Session {
private final RunState state; private final RunState state;
private final Sinks.Many<RunStep> steps = Sinks.many().unicast().onBackpressureBuffer(); private final Sinks.Many<RunStep> steps = Sinks.many().unicast().onBackpressureBuffer();
private final AtomicBoolean cancelled = new AtomicBoolean(); private final AtomicBoolean cancelled = new AtomicBoolean();
@ -288,18 +284,10 @@ public final class NativePolicyDriver implements AgentRuntime {
return Mono.fromCallable( return Mono.fromCallable(
() -> { () -> {
ensureRunning(); ensureRunning();
if (terminal || rounds >= hardRoundCeiling)
throw new IllegalStateException(
"Unexpected SDK model call beyond application"
+ " boundary");
rounds++;
state.applyPatch(Map.of(TOOL_CALLS, List.of()));
node(REASONING_NODE, policies.reasoning());
reasoningRoute = edge(policies.afterReasoning());
List<ContentBlock> blocks = new ArrayList<>(); List<ContentBlock> blocks = new ArrayList<>();
scratchArguments.clear(); scratchArguments.clear();
for (AssistantMessage.ToolCall call : for (AssistantMessage.ToolCall call :
ACTION_NODE.equals(reasoningRoute) !terminal && ACTION_NODE.equals(reasoningRoute)
? calls() ? calls()
: List.<AssistantMessage.ToolCall>of()) { : List.<AssistantMessage.ToolCall>of()) {
if (call.id() == null if (call.id() == null
@ -352,8 +340,8 @@ public final class NativePolicyDriver implements AgentRuntime {
.reduce((first, last) -> last) .reduce((first, last) -> last)
.map(AssistantMessage::getText) .map(AssistantMessage::getText)
.orElse(""); .orElse("");
// SDK drops fully empty responses before its // Keep a visible SDK scratch response even when
// post-reasoning routing hook. // the application policy produced no visible text.
// This marker exists only in discarded SDK scratch // This marker exists only in discarded SDK scratch
// history, never application history. // history, never application history.
blocks.add( blocks.add(
@ -367,7 +355,9 @@ public final class NativePolicyDriver implements AgentRuntime {
return ChatResponse.builder() return ChatResponse.builder()
.content(blocks) .content(blocks)
.finishReason( .finishReason(
calls().isEmpty() ? "stop" : "tool_calls") terminal || calls().isEmpty()
? "stop"
: "tool_calls")
.build(); .build();
}) })
.subscribeOn(Schedulers.boundedElastic()) .subscribeOn(Schedulers.boundedElastic())
@ -480,52 +470,51 @@ public final class NativePolicyDriver implements AgentRuntime {
return false; return false;
} }
@Override /**
public <T extends HookEvent> Mono<T> onEvent(T event) { * Prepare one application policy round before entering the SDK reasoning core. Middleware
if (!(event instanceof PostReasoningEvent post)) return Mono.just(event); * repeats policy-only routes without fabricating tool calls. Actual tool batches still return
return Mono.fromCallable( * to the SDK's native reasoning/acting iteration.
() -> { */
ensureRunning(); private boolean prepareReasoning() throws Exception {
String route = ensureRunning();
Objects.requireNonNull( if (terminal) return false;
reasoningRoute, if (rounds >= hardRoundCeiling) {
"Reasoning route must be captured once"); ceiling();
if (ACTION_NODE.equals(route)) { return false;
if (calls().isEmpty()) }
throw new IllegalStateException( rounds++;
"Action route requires real tool calls"); state.applyPatch(Map.of(TOOL_CALLS, List.of()));
return event; node(REASONING_NODE, policies.reasoning());
} reasoningRoute =
switch (route) { Objects.requireNonNull(
case REASONING_NODE -> { edge(policies.afterReasoning()),
if (rounds >= hardRoundCeiling) { "Reasoning route must be captured once");
ceiling(); switch (reasoningRoute) {
post.stopAgent(); case ACTION_NODE -> {
} else post.gotoReasoning(); if (calls().isEmpty())
} throw new IllegalStateException("Action route requires real tool calls");
case SUMMARIZING_NODE -> { return false;
node(SUMMARIZING_NODE, policies.summarizing()); }
if (rounds >= hardRoundCeiling) { case REASONING_NODE -> {
ceiling(); if (rounds >= hardRoundCeiling) ceiling();
post.stopAgent(); return !terminal;
} else post.gotoReasoning(); }
} case SUMMARIZING_NODE -> {
case LIMIT_EXCEEDED_NODE -> { node(SUMMARIZING_NODE, policies.summarizing());
node(LIMIT_EXCEEDED_NODE, policies.limitExceeded()); if (rounds >= hardRoundCeiling) ceiling();
if (finish(true)) post.gotoReasoning(); return !terminal;
else post.stopAgent(); }
} case LIMIT_EXCEEDED_NODE -> {
case FINAL_ANSWER_NODE -> { node(LIMIT_EXCEEDED_NODE, policies.limitExceeded());
if (finish(true)) post.gotoReasoning(); return finish(true);
else post.stopAgent(); }
} case FINAL_ANSWER_NODE -> {
default -> return finish(true);
throw new IllegalArgumentException( }
"Unsupported reasoning route: " + route); default ->
} throw new IllegalArgumentException(
return event; "Unsupported reasoning route: " + reasoningRoute);
}) }
.subscribeOn(Schedulers.boundedElastic());
} }
private MiddlewareBase middleware() { private MiddlewareBase middleware() {
@ -540,17 +529,25 @@ public final class NativePolicyDriver implements AgentRuntime {
} }
@Override @Override
public Flux<AgentEvent> onModelCall( public Flux<AgentEvent> onReasoning(
Agent agent, Agent agent,
RuntimeContext context, RuntimeContext context,
ModelCallInput input, ReasoningInput input,
Function<ModelCallInput, Flux<AgentEvent>> next) { Function<ReasoningInput, Flux<AgentEvent>> next) {
if (terminal || rounds >= hardRoundCeiling) return Mono.fromCallable(Session.this::prepareReasoning)
return Flux.just( .subscribeOn(Schedulers.boundedElastic())
new RequestStopEvent( .repeat(() -> !terminal && !ACTION_NODE.equals(reasoningRoute))
"application_terminal", .thenMany(Flux.defer(() -> next.apply(input)))
GenerateReason.MIDDLEWARE_STOP_REQUESTED)); .concatWith(
return next.apply(input); Flux.defer(
() ->
terminal
? Flux.just(
new RequestStopEvent(
"application_terminal",
GenerateReason
.MIDDLEWARE_STOP_REQUESTED))
: Flux.empty()));
} }
}; };
} }

View File

@ -37,6 +37,22 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
class NativePolicyDriverTest { class NativePolicyDriverTest {
@Test
void nativeRuntimeDoesNotLinkToForRemovalHookApi() throws Exception {
var types = new java.util.ArrayDeque<Class<?>>();
types.add(NativePolicyDriver.class);
while (!types.isEmpty()) {
Class<?> type = types.removeFirst();
types.addAll(List.of(type.getDeclaredClasses()));
try (var bytes = type.getResourceAsStream("/" + type.getName().replace('.', '/') + ".class")) {
assertNotNull(bytes);
String constants = new String(bytes.readAllBytes(), java.nio.charset.StandardCharsets.ISO_8859_1);
assertFalse(constants.contains("io/agentscope/core/hook/"),
type.getName() + " must use supported middleware rather than for-removal hooks");
}
}
}
private static final List<ToolSchema> SCHEMAS = private static final List<ToolSchema> SCHEMAS =
List.of( List.of(
ToolSchema.builder() ToolSchema.builder()