diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-spring-boot-starter/src/main/java/io/agentscope/spring/boot/AgentBuilderCustomizer.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-spring-boot-starter/src/main/java/io/agentscope/spring/boot/AgentBuilderCustomizer.java new file mode 100644 index 0000000000..d0c8aebd08 --- /dev/null +++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-spring-boot-starter/src/main/java/io/agentscope/spring/boot/AgentBuilderCustomizer.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.spring.boot; + +import io.agentscope.core.ReActAgent; +import java.util.function.Consumer; + +/** + * Customizer for {@link ReActAgent.Builder}. + * + *
Example usage: + * + *
{@code
+ * @Bean
+ * public AgentBuilderCustomizer agentBuilderCustomizer() {
+ * return builder -> builder.middleware(new MyMiddleware());
+ * }
+ * }
+ *
+ * @see AgentscopeAutoConfiguration#agentscopeReActAgent
+ */
+@FunctionalInterface
+public interface AgentBuilderCustomizer extends ConsumerIn addition to the core beans, this configuration provides the following + * conveniences when {@code agentscope.agent.enabled=true}: + * + *
Both conveniences are implemented as {@link AgentBuilderCustomizer} beans ordered with
+ * {@link Ordered#HIGHEST_PRECEDENCE}, so a user-defined {@code AgentBuilderCustomizer}
+ * without an explicit {@code @Order} (and therefore defaulting to
+ * {@link Ordered#LOWEST_PRECEDENCE}) runs afterwards and can override them. A user customizer
+ * that sets its own small {@code @Order} can run earlier instead.
*/
@AutoConfiguration
@EnableConfigurationProperties(AgentscopeProperties.class)
@ConditionalOnClass(ReActAgent.class)
public class AgentscopeAutoConfiguration {
+ private static final Logger logger = LoggerFactory.getLogger(AgentscopeAutoConfiguration.class);
+
/**
* Default Memory implementation backed by InMemoryMemory.
*
@@ -105,14 +136,109 @@ public Toolkit agentscopeToolkit() {
@ConditionalOnBean(Model.class)
@ConditionalOnProperty(prefix = "agentscope.agent", name = "enabled", havingValue = "true")
public ReActAgent agentscopeReActAgent(
- Model model, Memory memory, Toolkit toolkit, AgentscopeProperties properties) {
+ Model model,
+ Memory memory,
+ Toolkit toolkit,
+ AgentscopeProperties properties,
+ ObjectProvider Ordered at {@link Ordered#HIGHEST_PRECEDENCE}{@code + 10} — before the permission and
+ * hook customizers, and before any user-defined {@link AgentBuilderCustomizer}.
+ *
+ * Sharing contract: a {@code MiddlewareBase} bean is a singleton, and every agent
+ * built from this auto-configuration receives the same instance. Middleware must therefore be
+ * stateless / thread-safe — keep per-request state in {@code RuntimeContext}, never in
+ * instance fields.
+ *
+ * Disable with {@code agentscope.agent.auto-assemble-middleware=false} when middleware is
+ * wired manually, to avoid attaching the same middleware twice. To replace just the assembly
+ * logic, shadow the {@code middlewareAutoCustomizer} bean by name.
+ */
+ @Bean
+ @Order(Ordered.HIGHEST_PRECEDENCE + 10)
+ @ConditionalOnProperty(prefix = "agentscope.agent", name = "enabled", havingValue = "true")
+ @ConditionalOnProperty(
+ prefix = "agentscope.agent",
+ name = "auto-assemble-middleware",
+ havingValue = "true",
+ matchIfMissing = true)
+ @ConditionalOnMissingBean(name = "middlewareAutoCustomizer")
+ public AgentBuilderCustomizer middlewareAutoCustomizer(
+ ObjectProvider When more than one {@code PermissionContextState} bean is present the context is
+ * ambiguous, so nothing is injected and a warning is logged — the permission engine decides
+ * allow/approve/deny, so silently picking one would be a security-relevant choice.
+ *
+ * Ordered at {@link Ordered#HIGHEST_PRECEDENCE}{@code + 20} — after middleware assembly
+ * but still before the hook customizer and any user-defined {@link AgentBuilderCustomizer}.
+ *
+ * Backs off when a bean named {@code permissionContextAutoCustomizer} already exists
+ * ({@code @ConditionalOnMissingBean(name = ...)}), so supplying one disables or replaces this
+ * auto-injection.
+ */
+ @Bean
+ @Order(Ordered.HIGHEST_PRECEDENCE + 20)
+ @ConditionalOnProperty(prefix = "agentscope.agent", name = "enabled", havingValue = "true")
+ @ConditionalOnMissingBean(name = "permissionContextAutoCustomizer")
+ public AgentBuilderCustomizer permissionContextAutoCustomizer(
+ ObjectProvider Isolated from {@link AgentscopeAutoConfiguration} because {@link Hook} and
+ * {@link io.agentscope.core.hook.HookEvent} are
+ * {@link Deprecated @Deprecated} for removal since 2.0.0. This class will be
+ * removed together with the Hook API.
+ *
+ * Opt-in: hook auto-attach is disabled by default because {@link Hook} is deprecated
+ * for removal. Enable it with {@code agentscope.agent.auto-assemble-hooks=true}; when enabled,
+ * every {@link Hook} bean is attached to the agent builder, so applications that also attach
+ * hooks manually must remove the manual attachment to avoid registering the same hook twice.
+ *
+ * The injected hooks are ordered by
+ * {@link org.springframework.core.annotation.Order @Order}. The customizer
+ * itself is ordered at {@link Ordered#HIGHEST_PRECEDENCE}{@code + 30} so that it
+ * runs after the built-in middleware and permission customizers but still before
+ * any user-defined {@link AgentBuilderCustomizer}.
+ */
+@AutoConfiguration
+@ConditionalOnClass(Hook.class)
+@ConditionalOnProperty(prefix = "agentscope.agent", name = "enabled", havingValue = "true")
+@SuppressWarnings("deprecation")
+public class HookAutoConfiguration {
+
+ private static final Logger logger = LoggerFactory.getLogger(HookAutoConfiguration.class);
+
+ /**
+ * Auto-injects all {@link Hook} beans into the agent builder, ordered by
+ * {@link org.springframework.core.annotation.Order @Order}.
+ *
+ * Disabled by default; enable with {@code agentscope.agent.auto-assemble-hooks=true}.
+ *
+ * Backs off when a bean named {@code hookAutoCustomizer} already exists
+ * ({@code @ConditionalOnMissingBean(name = ...)}), so supplying one disables or replaces this
+ * auto-attach.
+ */
+ @Bean
+ @Order(Ordered.HIGHEST_PRECEDENCE + 30)
+ @ConditionalOnProperty(
+ prefix = "agentscope.agent",
+ name = "auto-assemble-hooks",
+ havingValue = "true")
+ @ConditionalOnMissingBean(name = "hookAutoCustomizer")
+ public AgentBuilderCustomizer hookAutoCustomizer(ObjectProvider Default {@code true}. Auto-assembly does not de-duplicate: a middleware an application
+ * already attaches itself via {@code builder.middleware(...)} would be registered twice. Set
+ * this to {@code false} to opt out and wire middleware yourself.
+ */
+ private boolean autoAssembleMiddleware = true;
+
+ /**
+ * Whether every {@code Hook} bean is auto-attached to the agent builder.
+ *
+ * Default {@code false}. {@code Hook} is deprecated for removal, so auto-attach is opt-in;
+ * when enabled it does not de-duplicate against hooks the application attaches itself. Set
+ * this to {@code true} to opt in.
+ */
+ private boolean autoAssembleHooks = false;
+
public boolean isEnabled() {
return enabled;
}
@@ -82,4 +100,20 @@ public int getMaxIters() {
public void setMaxIters(int maxIters) {
this.maxIters = maxIters;
}
+
+ public boolean isAutoAssembleMiddleware() {
+ return autoAssembleMiddleware;
+ }
+
+ public void setAutoAssembleMiddleware(boolean autoAssembleMiddleware) {
+ this.autoAssembleMiddleware = autoAssembleMiddleware;
+ }
+
+ public boolean isAutoAssembleHooks() {
+ return autoAssembleHooks;
+ }
+
+ public void setAutoAssembleHooks(boolean autoAssembleHooks) {
+ this.autoAssembleHooks = autoAssembleHooks;
+ }
}
diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-spring-boot-starter/src/main/java/io/agentscope/spring/boot/properties/AgentscopeProperties.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-spring-boot-starter/src/main/java/io/agentscope/spring/boot/properties/AgentscopeProperties.java
index 24fd7158c2..7b8d30c5e8 100644
--- a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-spring-boot-starter/src/main/java/io/agentscope/spring/boot/properties/AgentscopeProperties.java
+++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-spring-boot-starter/src/main/java/io/agentscope/spring/boot/properties/AgentscopeProperties.java
@@ -16,6 +16,7 @@
package io.agentscope.spring.boot.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
/**
* Root configuration properties for AgentScope Spring Boot starter.
@@ -26,13 +27,18 @@
* {@link NestedConfigurationProperty} makes the configuration metadata processor expand
+ * these nested groups into {@code spring-configuration-metadata.json}. Measured on this starter,
+ * omitting the annotation leaves the generated {@code properties} array empty, so it is required
+ * here for {@code agentscope.agent.*} / {@code agentscope.model.*} entries to appear.
*/
@ConfigurationProperties(prefix = "agentscope")
public class AgentscopeProperties {
- private final AgentProperties agent = new AgentProperties();
+ @NestedConfigurationProperty private final AgentProperties agent = new AgentProperties();
- private final ModelProperties model = new ModelProperties();
+ @NestedConfigurationProperty private final ModelProperties model = new ModelProperties();
public AgentProperties getAgent() {
return agent;
diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 69fe4fdd74..ab8a31fc4e 100644
--- a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -14,3 +14,4 @@
# limitations under the License.
#
io.agentscope.spring.boot.AgentscopeAutoConfiguration
+io.agentscope.spring.boot.HookAutoConfiguration
diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-spring-boot-starter/src/test/java/io/agentscope/spring/boot/AgentscopeAutoConfigurationTest.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-spring-boot-starter/src/test/java/io/agentscope/spring/boot/AgentscopeAutoConfigurationTest.java
index cb58146b48..b908b68ea6 100644
--- a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-spring-boot-starter/src/test/java/io/agentscope/spring/boot/AgentscopeAutoConfigurationTest.java
+++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-spring-boot-starter/src/test/java/io/agentscope/spring/boot/AgentscopeAutoConfigurationTest.java
@@ -21,10 +21,13 @@
import io.agentscope.core.memory.InMemoryMemory;
import io.agentscope.core.memory.Memory;
import io.agentscope.core.message.Msg;
+import io.agentscope.core.middleware.MiddlewareBase;
import io.agentscope.core.model.ChatResponse;
import io.agentscope.core.model.GenerateOptions;
import io.agentscope.core.model.Model;
import io.agentscope.core.model.ToolSchema;
+import io.agentscope.core.permission.PermissionContextState;
+import io.agentscope.core.permission.PermissionMode;
import io.agentscope.core.tool.Toolkit;
import java.util.List;
import org.junit.jupiter.api.Test;
@@ -100,6 +103,139 @@ void shouldBackOffWhenUserDefinesMemoryToolkitAndAgentBeans() {
});
}
+ // ------------------------------------------------------------------
+ // AgentBuilderCustomizer tests
+ // ------------------------------------------------------------------
+
+ @Test
+ void shouldApplyAgentBuilderCustomizer() {
+ contextRunner
+ .withUserConfiguration(
+ CustomModelConfiguration.class, MaxItersCustomizerConfiguration.class)
+ .run(
+ context -> {
+ ReActAgent agent = context.getBean(ReActAgent.class);
+ assertThat(agent.getMaxIters()).isEqualTo(99);
+ });
+ }
+
+ // ------------------------------------------------------------------
+ // Middleware auto-assembly tests
+ // ------------------------------------------------------------------
+
+ @Test
+ void shouldAutoInjectMiddlewareBeans() {
+ contextRunner
+ .withUserConfiguration(
+ CustomModelConfiguration.class, MiddlewareConfiguration.class)
+ .run(
+ context -> {
+ ReActAgent agent = context.getBean(ReActAgent.class);
+ assertThat(agent.getMiddlewares())
+ .anyMatch(mw -> mw instanceof TestMiddleware);
+ });
+ }
+
+ @Test
+ void shouldSkipMiddlewareAutoAssemblyWhenDisabled() {
+ contextRunner
+ .withUserConfiguration(
+ CustomModelConfiguration.class, MiddlewareConfiguration.class)
+ .withPropertyValues("agentscope.agent.auto-assemble-middleware=false")
+ .run(
+ context -> {
+ ReActAgent agent = context.getBean(ReActAgent.class);
+ assertThat(agent.getMiddlewares())
+ .noneMatch(mw -> mw instanceof TestMiddleware);
+ });
+ }
+
+ @Test
+ void userCustomizerShouldRunAfterAutoMiddlewareCustomizer() {
+ contextRunner
+ .withUserConfiguration(
+ CustomModelConfiguration.class,
+ MiddlewareConfiguration.class,
+ UserMiddlewareCustomizerConfiguration.class)
+ .run(
+ context -> {
+ List Isolated because {@link Hook} and {@link io.agentscope.core.hook.HookEvent}
+ * are {@link Deprecated} for removal.
+ */
+@SuppressWarnings("deprecation")
+class HookAutoConfigurationTest {
+
+ private final ApplicationContextRunner contextRunner =
+ new ApplicationContextRunner()
+ .withConfiguration(
+ AutoConfigurations.of(
+ AgentscopeAutoConfiguration.class, HookAutoConfiguration.class))
+ .withPropertyValues("agentscope.agent.enabled=true");
+
+ @Test
+ void shouldNotAutoInjectHooksByDefault() {
+ contextRunner
+ .withUserConfiguration(CustomModelConfiguration.class, HookConfiguration.class)
+ .run(
+ context -> {
+ ReActAgent agent = context.getBean(ReActAgent.class);
+ assertThat(agent.getHooks()).noneMatch(h -> h instanceof TestHook);
+ });
+ }
+
+ @Test
+ void shouldAutoInjectHookBeansWhenEnabled() {
+ contextRunner
+ .withUserConfiguration(CustomModelConfiguration.class, HookConfiguration.class)
+ .withPropertyValues("agentscope.agent.auto-assemble-hooks=true")
+ .run(
+ context -> {
+ ReActAgent agent = context.getBean(ReActAgent.class);
+ assertThat(agent.getHooks()).anyMatch(h -> h instanceof TestHook);
+ });
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ static class CustomModelConfiguration {
+
+ @Bean
+ Model customModel() {
+ return new TestModel();
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ static class HookConfiguration {
+
+ @Bean
+ TestHook testHook() {
+ return new TestHook();
+ }
+ }
+
+ static class TestHook implements Hook {
+ @Override
+ public