From 28f412eefbb097c12087b97da513ee894397690e Mon Sep 17 00:00:00 2001 From: jiangzhen Date: Fri, 9 Jan 2026 21:46:32 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=9A=82=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runtime/engine/DeployManager.java | 3 + .../runtime/LocalDeployManager.java | 321 ++++++++++-------- .../io/agentscope/runtime/app/AgentApp.java | 65 +++- .../lifecycle/AbstractAppLifecycleHook.java | 11 + .../runtime/lifecycle/AppLifecycleHook.java | 45 +++ 5 files changed, 297 insertions(+), 148 deletions(-) create mode 100644 web/src/main/java/io/agentscope/runtime/lifecycle/AbstractAppLifecycleHook.java create mode 100644 web/src/main/java/io/agentscope/runtime/lifecycle/AppLifecycleHook.java diff --git a/core/src/main/java/io/agentscope/runtime/engine/DeployManager.java b/core/src/main/java/io/agentscope/runtime/engine/DeployManager.java index aa4b5295..0247efc2 100644 --- a/core/src/main/java/io/agentscope/runtime/engine/DeployManager.java +++ b/core/src/main/java/io/agentscope/runtime/engine/DeployManager.java @@ -15,6 +15,9 @@ */ package io.agentscope.runtime.engine; +import org.springframework.context.ApplicationContext; + public interface DeployManager { void deploy(Runner runner); + void undeploy(); } diff --git a/web/src/main/java/io/agentscope/runtime/LocalDeployManager.java b/web/src/main/java/io/agentscope/runtime/LocalDeployManager.java index fe509a11..9ecefb2c 100644 --- a/web/src/main/java/io/agentscope/runtime/LocalDeployManager.java +++ b/web/src/main/java/io/agentscope/runtime/LocalDeployManager.java @@ -15,6 +15,7 @@ */ package io.agentscope.runtime; +import io.agentscope.runtime.app.AgentApp; import io.agentscope.runtime.autoconfigure.DeployProperties; import io.agentscope.runtime.engine.DeployManager; import io.agentscope.runtime.engine.Runner; @@ -23,9 +24,11 @@ import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; import org.springframework.context.annotation.ComponentScan; @@ -33,6 +36,11 @@ import org.springframework.context.support.GenericApplicationContext; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.function.HandlerFunction; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerRequest; +import org.springframework.web.servlet.function.ServerResponse; import java.util.HashMap; import java.util.List; @@ -40,149 +48,172 @@ import java.util.function.Consumer; public class LocalDeployManager implements DeployManager { - private static final Logger logger = LoggerFactory.getLogger(LocalDeployManager.class); - - private ConfigurableApplicationContext applicationContext; - - private final String endpointName; - private final String host; - private final int port; - private final List protocols; - private final List protocolConfigs; - private final Consumer corsConfigurer; - - private LocalDeployManager(LocalDeployerManagerBuilder builder) { - this.endpointName = builder.endpointName; - this.host = builder.host; - this.port = builder.port; - this.protocols = builder.protocols; - this.protocolConfigs = builder.protocolConfigs; - this.corsConfigurer = builder.corsConfigurer; - } - - @Override - public synchronized void deploy(Runner runner) { - if (this.applicationContext != null && this.applicationContext.isActive()) { - logger.info("Application context is already active, skipping deployment"); - return; - } - - Map serverProps = new HashMap<>(); - if (this.port > 0) { - serverProps.put("server.port", this.port); - } - if (this.host != null && !this.host.isBlank()) { - serverProps.put("server.address", this.host); - } - - logger.info("Starting streaming deployment for endpoint: {}", endpointName); - - this.applicationContext = new SpringApplicationBuilder() - .sources(LocalDeployConfig.class) - .web(WebApplicationType.SERVLET) - .properties(serverProps) - .initializers((GenericApplicationContext ctx) -> { - // Register Runner instance as a bean - ctx.registerBean(Runner.class, () -> runner); - // Register DeployProperties instance as a bean - ctx.registerBean(DeployProperties.class, () -> new DeployProperties(port, host, endpointName)); - // Scan additional packages based on protocols - ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(ctx); - scanner.scan("io.agentscope.runtime.lifecycle"); - Map protocolConfigMap = null != protocolConfigs ? protocolConfigs.stream() - .collect(HashMap::new, (map, config) -> map.put(config.type(), config), HashMap::putAll) - : Map.of(); - for (Protocol protocol : protocols) { - String packageName = "io.agentscope.runtime.protocol." + protocol.name().toLowerCase(); - scanner.scan(packageName); - if (protocolConfigMap.containsKey(protocol)) { - ProtocolConfig protocolConfig = protocolConfigMap.get(protocol); - ctx.registerBean(protocolConfig.name(), ProtocolConfig.class, () -> protocolConfig); - } - } - if (corsConfigurer != null) { - ctx.registerBean(WebMvcConfigurer.class, () -> new WebMvcConfigurer() { - @Override - public void addCorsMappings(@NotNull CorsRegistry registry) { - corsConfigurer.accept(registry); - } - }); - } - - }) - .run(); - - logger.info("Streaming deployment completed for endpoint: {}", endpointName); - } - - /** - * Shutdown the application and clean up resources - */ - public synchronized void shutdown() { - if (this.applicationContext != null && this.applicationContext.isActive()) { - logger.info("Shutting down LocalDeployManager..."); - this.applicationContext.close(); - this.applicationContext = null; - logger.info("LocalDeployManager shutdown completed"); - } - } - - /** - * Configuration class for local deployment of streaming services. - * This class enables component scanning for A2A controllers and other Spring components. - */ - @Configuration - @EnableAutoConfiguration - @ComponentScan(basePackages = { - "io.agentscope.runtime.autoconfigure" - }) - public static class LocalDeployConfig { - } - - public static LocalDeployerManagerBuilder builder(){ - return new LocalDeployerManagerBuilder(); - } - - public static class LocalDeployerManagerBuilder { - private String endpointName; - private String host; - private int port = 8080; - private List protocols = List.of(Protocol.A2A, Protocol.ResponseAPI); - private List protocolConfigs = List.of(); - private Consumer corsConfigurer; - - public LocalDeployerManagerBuilder endpointName(String endpointName) { - this.endpointName = endpointName; - return this; - } - - public LocalDeployerManagerBuilder host(String host) { - this.host = host; - return this; - } - - public LocalDeployerManagerBuilder port(int port) { - this.port = port; - return this; - } - - public LocalDeployerManagerBuilder protocols(List protocols) { - this.protocols = protocols; - return this; - } - - public LocalDeployerManagerBuilder protocolConfigs(List protocolConfigs) { - this.protocolConfigs = protocolConfigs; - return this; - } - - public LocalDeployerManagerBuilder corsConfigurer(Consumer corsConfigurer) { - this.corsConfigurer = corsConfigurer; - return this; - } - - public LocalDeployManager build() { - return new LocalDeployManager(this); - } - } + private static final Logger logger = LoggerFactory.getLogger(LocalDeployManager.class); + + private ConfigurableApplicationContext applicationContext; + + private final String endpointName; + private final String host; + private final int port; + private final List protocols; + private final List protocolConfigs; + private final Consumer corsConfigurer; + private final List customEndpoints; + + + private LocalDeployManager(LocalDeployerManagerBuilder builder) { + this.endpointName = builder.endpointName; + this.host = builder.host; + this.port = builder.port; + this.protocols = builder.protocols; + this.protocolConfigs = builder.protocolConfigs; + this.corsConfigurer = builder.corsConfigurer; + this.customEndpoints = builder.customEndpoints; + } + + @Override + public synchronized void deploy(Runner runner) { + if (this.applicationContext != null && this.applicationContext.isActive()) { + logger.info("Application context is already active, skipping deployment"); + return; + } + + Map serverProps = new HashMap<>(); + if (this.port > 0) { + serverProps.put("server.port", this.port); + } + if (this.host != null && !this.host.isBlank()) { + serverProps.put("server.address", this.host); + } + + logger.info("Starting streaming deployment for endpoint: {}", endpointName); + + this.applicationContext = new SpringApplicationBuilder() + .sources(LocalDeployConfig.class) + .web(WebApplicationType.SERVLET) + .properties(serverProps) + .initializers((GenericApplicationContext ctx) -> { + // Register Runner instance as a bean + ctx.registerBean(Runner.class, () -> runner); + // Register DeployProperties instance as a bean + ctx.registerBean(DeployProperties.class, () -> new DeployProperties(port, host, endpointName)); + // Scan additional packages based on protocols + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(ctx); + scanner.scan("io.agentscope.runtime.lifecycle"); + Map protocolConfigMap = null != protocolConfigs ? protocolConfigs.stream() + .collect(HashMap::new, (map, config) -> map.put(config.type(), config), HashMap::putAll) + : Map.of(); + for (Protocol protocol : protocols) { + String packageName = "io.agentscope.runtime.protocol." + protocol.name().toLowerCase(); + scanner.scan(packageName); + if (protocolConfigMap.containsKey(protocol)) { + ProtocolConfig protocolConfig = protocolConfigMap.get(protocol); + ctx.registerBean(protocolConfig.name(), ProtocolConfig.class, () -> protocolConfig); + } + } + if (corsConfigurer != null) { + ctx.registerBean(WebMvcConfigurer.class, () -> new WebMvcConfigurer() { + @Override + public void addCorsMappings(@NotNull CorsRegistry registry) { + corsConfigurer.accept(registry); + } + }); + } + if (customEndpoints != null) { + RouterFunctions.Builder route = RouterFunctions.route(); + for (AgentApp.EndpointInfo customEndpoint : customEndpoints) { + route.POST(customEndpoint.path, request -> customEndpoint.handler.apply(request)); + + + } + ctx.registerBean(RouterFunction.class,route.build()); + } + + }) + .run(); + + logger.info("Streaming deployment completed for endpoint: {}", endpointName); + } + + @Override + public void undeploy() { + shutdown(); + } + + /** + * Shutdown the application and clean up resources + */ + public synchronized void shutdown() { + if (this.applicationContext != null && this.applicationContext.isActive()) { + logger.info("Shutting down LocalDeployManager..."); + this.applicationContext.close(); + this.applicationContext = null; + logger.info("LocalDeployManager shutdown completed"); + } + } + + /** + * Configuration class for local deployment of streaming services. + * This class enables component scanning for A2A controllers and other Spring components. + */ + @Configuration + @EnableAutoConfiguration + @ComponentScan(basePackages = { + "io.agentscope.runtime.autoconfigure" + }) + public static class LocalDeployConfig { + } + + public static LocalDeployerManagerBuilder builder() { + return new LocalDeployerManagerBuilder(); + } + + public static class LocalDeployerManagerBuilder { + private String endpointName; + private String host; + private int port = 8080; + private List protocols = List.of(Protocol.A2A, Protocol.ResponseAPI); + private List protocolConfigs = List.of(); + private Consumer corsConfigurer; + private List customEndpoints; + + public LocalDeployerManagerBuilder endpointName(String endpointName) { + this.endpointName = endpointName; + return this; + } + + public LocalDeployerManagerBuilder host(String host) { + this.host = host; + return this; + } + + public LocalDeployerManagerBuilder port(int port) { + this.port = port; + return this; + } + + public LocalDeployerManagerBuilder protocols(List protocols) { + this.protocols = protocols; + return this; + } + + public LocalDeployerManagerBuilder protocolConfigs(List protocolConfigs) { + this.protocolConfigs = protocolConfigs; + return this; + } + + public LocalDeployerManagerBuilder corsConfigurer(Consumer corsConfigurer) { + this.corsConfigurer = corsConfigurer; + return this; + } + + public LocalDeployerManagerBuilder customEndpoints(List customEndpoints) { + this.customEndpoints = customEndpoints; + return this; + } + + public LocalDeployManager build() { + return new LocalDeployManager(this); + } + } } diff --git a/web/src/main/java/io/agentscope/runtime/app/AgentApp.java b/web/src/main/java/io/agentscope/runtime/app/AgentApp.java index 7ebd5880..4bd6f379 100644 --- a/web/src/main/java/io/agentscope/runtime/app/AgentApp.java +++ b/web/src/main/java/io/agentscope/runtime/app/AgentApp.java @@ -23,11 +23,13 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Properties; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.function.Function; @@ -42,6 +44,7 @@ import io.agentscope.runtime.engine.services.memory.service.MemoryService; import io.agentscope.runtime.engine.services.memory.service.SessionHistoryService; import io.agentscope.runtime.engine.services.sandbox.SandboxService; +import io.agentscope.runtime.lifecycle.AppLifecycleHook; import io.agentscope.runtime.protocol.ProtocolConfig; import io.agentscope.runtime.sandbox.manager.SandboxManager; @@ -50,10 +53,18 @@ import org.slf4j.LoggerFactory; import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.function.ServerRequest; +import org.springframework.web.servlet.function.ServerResponse; import picocli.CommandLine; import picocli.CommandLine.Option; +import static io.agentscope.runtime.lifecycle.AppLifecycleHook.AFTER_RUN; +import static io.agentscope.runtime.lifecycle.AppLifecycleHook.AFTER_STOP; +import static io.agentscope.runtime.lifecycle.AppLifecycleHook.BEFORE_RUN; +import static io.agentscope.runtime.lifecycle.AppLifecycleHook.BEFORE_STOP; +import static io.agentscope.runtime.lifecycle.AppLifecycleHook.JVM_EXIT; + /** * AgentApp class represents an application that runs as an agent. @@ -93,7 +104,9 @@ public class AgentApp { private String responseType = "sse"; private Consumer corsConfigurer; - private List customEndpoints = new ArrayList<>(); + private final List customEndpoints = new ArrayList<>(); + private final List hooks = new ArrayList<>(); + private final AtomicBoolean stopped = new AtomicBoolean(false); private List protocolConfigs; private static final String DEFAULT_ENV_FILE_PATH = ".env"; private static final String CLASS_SUFFIX = ".class"; @@ -450,10 +463,21 @@ public void run(String host, int port) { buildRunner(); logger.info("[AgentApp] Starting AgentApp with endpoint: {}, host: {}, port: {}", endpointPath, host, port); - + AgentApp app = this; + List lifecycleHooks = hooks.stream() + .sorted(Comparator.comparingInt(AppLifecycleHook::priority)).toList(); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> lifecycleHooks.stream().filter(e -> ((JVM_EXIT & e.operation()) != 0)) + .forEach(lifecycle -> lifecycle.onJvmExit(app, deployManager)))); + lifecycleHooks.stream() + .filter(e -> ((BEFORE_RUN & e.operation()) != 0)) + .forEach(lifecycle -> lifecycle.beforeRun(app, deployManager)); // Deploy via DeployManager deployManager.deploy(runner); logger.info("[AgentApp] AgentApp started successfully on {}:{}{}", host, port, endpointPath); + lifecycleHooks.stream() + .filter(e -> ((AFTER_RUN & e.operation()) != 0)) + .forEach(lifecycle -> lifecycle.afterRun(app, deployManager)); } /** @@ -515,7 +539,7 @@ public List getCustomEndpoints() { */ public static class EndpointInfo { public String path; - public Function, Object> handler; + public Function handler; public List methods; } @@ -613,5 +637,40 @@ public void setSandboxService(SandboxService sandboxService) { this.sandboxService = sandboxService; } } + + public void stop() { + if (stopped.get()) { + return; + } + synchronized (stopped) { + if (stopped.get()) { + return; + } + if (Objects.nonNull(deployManager)) { + AgentApp app = this; + List lifecycleHooks = hooks.stream() + .sorted(Comparator.comparingInt(AppLifecycleHook::priority)).toList(); + lifecycleHooks.stream() + .filter(e -> ((BEFORE_STOP & e.operation()) != 0)) + .forEach(lifecycle -> lifecycle.beforeStop(app, deployManager)); + deployManager.undeploy(); + stopped.set(true); + lifecycleHooks.stream() + .filter(e -> ((AFTER_STOP & e.operation()) != 0)) + .forEach(lifecycle -> lifecycle.afterStop(app, deployManager)); + } + } + } + + public AgentApp hooks(AppLifecycleHook... hooks) { + if (Objects.nonNull(hooks)) { + for (AppLifecycleHook hook : hooks) { + if (!this.hooks.contains(hook)) { + this.hooks.add(hook); + } + } + } + return this; + } } diff --git a/web/src/main/java/io/agentscope/runtime/lifecycle/AbstractAppLifecycleHook.java b/web/src/main/java/io/agentscope/runtime/lifecycle/AbstractAppLifecycleHook.java new file mode 100644 index 00000000..30e11de3 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/lifecycle/AbstractAppLifecycleHook.java @@ -0,0 +1,11 @@ +package io.agentscope.runtime.lifecycle; + +public abstract class AbstractAppLifecycleHook implements AppLifecycleHook { + + protected int operation; + + public AbstractAppLifecycleHook(){ + this.operation = ALL; + } + +} diff --git a/web/src/main/java/io/agentscope/runtime/lifecycle/AppLifecycleHook.java b/web/src/main/java/io/agentscope/runtime/lifecycle/AppLifecycleHook.java new file mode 100644 index 00000000..e3e31c55 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/lifecycle/AppLifecycleHook.java @@ -0,0 +1,45 @@ +package io.agentscope.runtime.lifecycle; + +import io.agentscope.runtime.app.AgentApp; +import io.agentscope.runtime.engine.DeployManager; + +public interface AppLifecycleHook { + + int BEFORE_RUN = 1; + + int AFTER_RUN = 1 << 1; + + int BEFORE_STOP = 1 << 2; + + int AFTER_STOP = 1 << 3; + + int JVM_EXIT = 1 << 4; + + int ALL = BEFORE_RUN | AFTER_RUN | BEFORE_STOP | AFTER_STOP | JVM_EXIT; + + default void beforeRun(AgentApp app, DeployManager deployManager) { + + } + + default void afterRun(AgentApp app, DeployManager deployManager) { + + } + + default void beforeStop(AgentApp app, DeployManager deployManager) { + + } + + default void afterStop(AgentApp app, DeployManager deployManager) { + + } + + default void onJvmExit(AgentApp app, DeployManager deployManager) { + + } + + default int priority() { + return 0; + } + + int operation(); +} From 057fabe3ff58db346c3b3ede7d94b58380dafcc8 Mon Sep 17 00:00:00 2001 From: jiangzhen Date: Mon, 12 Jan 2026 17:10:36 +0800 Subject: [PATCH 2/5] =?UTF-8?q?support=20customize=20endpoint=E3=80=81life?= =?UTF-8?q?cycle=20hook=20and=20middleware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cookbook/en/deployment/agent_app.md | 90 +++++++++ cookbook/zh/deployment/agent_app.md | 92 +++++++++ .../runtime/engine/DeployManager.java | 1 - ...opeDeployWithCustomizeEndpointExample.java | 48 +++++ ...ntScopeDeployWithLifecycleHookExample.java | 59 ++++++ ...AgentScopeDeployWithMiddlewareExample.java | 64 ++++++ .../runtime/LocalDeployManager.java | 63 ++++-- .../io/agentscope/runtime/app/AgentApp.java | 59 ++++-- .../hook/AbstractAppLifecycleHook.java | 27 +++ .../runtime/hook/AppLifecycleHook.java | 58 ++++++ .../agentscope/runtime/hook/HookContext.java | 47 +++++ .../lifecycle/AbstractAppLifecycleHook.java | 11 -- .../runtime/lifecycle/AppLifecycleHook.java | 45 ----- .../deployer/AgentScopeDeployTests.java | 187 ++++++++++++++++++ 14 files changed, 765 insertions(+), 86 deletions(-) create mode 100644 examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithCustomizeEndpointExample.java create mode 100644 examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithLifecycleHookExample.java create mode 100644 examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithMiddlewareExample.java create mode 100644 web/src/main/java/io/agentscope/runtime/hook/AbstractAppLifecycleHook.java create mode 100644 web/src/main/java/io/agentscope/runtime/hook/AppLifecycleHook.java create mode 100644 web/src/main/java/io/agentscope/runtime/hook/HookContext.java delete mode 100644 web/src/main/java/io/agentscope/runtime/lifecycle/AbstractAppLifecycleHook.java delete mode 100644 web/src/main/java/io/agentscope/runtime/lifecycle/AppLifecycleHook.java create mode 100644 web/src/test/java/io/agentscope/runtime/deployer/AgentScopeDeployTests.java diff --git a/cookbook/en/deployment/agent_app.md b/cookbook/en/deployment/agent_app.md index ef4c978a..8cb6bb78 100644 --- a/cookbook/en/deployment/agent_app.md +++ b/cookbook/en/deployment/agent_app.md @@ -85,6 +85,96 @@ agentApp.run("localhost", 10001); ------ + + + +## Custom endpoint + +**Feature** + +Allows the injection of custom endpoints when starting the 'AgentApp'. +**Usage Example** + +```java +AgentApp agentApp = new AgentApp(agentHandler); +agentApp.endpoint("/test/post", List.of("POST"), serverRequest -> ServerResponse.ok().body("OK")); +agentApp.run("localhost", 10001); +``` + +**Notes** + +- The incoming lambda will act directly on Spring's 'RouterFunction', route according to the http method, the input parameter is limited to ServerRequest, and the return value is limited to ServerResponse. + +------ + +## Lifecycle hooks + +**Feature** + +Allows before and after launching the 'AgentApp'; before and after destroying 'AgentApp'; Exit 'jvm' and do the corresponding processing. +**Usage Example** + +```java +AgentApp agentApp = new AgentApp(agentHandler); +agentApp.hooks(new AbstractAppLifecycleHook() { + @Override + public int operation() { + return BEFORE_RUN | AFTER_RUN | JVM_EXIT; + } + + @Override + public void beforeRun(HookContext context) { + System.out.println("beforeRun"); + } + + @Override + public void afterRun(HookContext context) { + System.out.println("afterRun"); + } +}); +agentApp.run("localhost", 10001); +``` + +**Notes** + +- The incoming AbstractAppLifecycleHook implementation class will act directly on the 'AgentApp' and sort by weight (natural order by integer), and pass in the 'HookContext' (wrapping 'AgentApp', 'DeployManager') instance for processing, and the context's extraInfo is used to pass cross-method, cross-thread variables.------ + + +## middleware + +**Feature** + +Allows the injection of Filter in the spring context when starting 'AgentApp'. +**Usage Example** + +```java +AgentApp agentApp = new AgentApp(agentHandler); +FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); + filterRegistrationBean.setFilter(new MyFilter()); + filterRegistrationBean.setBeanName("myFilter"); + filterRegistrationBean.setUrlPatterns(List.of("/test/get")); + filterRegistrationBean.setOrder(-1); +agentApp.middleware(filterRegistrationBean); +agentApp.run("localhost", 10001); + + +public static class MyFilter implements Filter{ + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + LOG.info("Current request uri = {}",request.getRequestURI()); + filterChain.doFilter(servletRequest,servletResponse); + } +} +``` + +**Notes** + +- Based on the servlet specification, it will directly act on the spring context object associated with 'AgentApp' for authentication, request counting, etc., and webflux is not supported at this time. +------ + + ## A2A Streaming Output (SSE) **Features** diff --git a/cookbook/zh/deployment/agent_app.md b/cookbook/zh/deployment/agent_app.md index d8992b92..f737c438 100644 --- a/cookbook/zh/deployment/agent_app.md +++ b/cookbook/zh/deployment/agent_app.md @@ -83,6 +83,98 @@ agentApp.run("localhost", 10001); ------ + +## 自定义服务端点 + +**功能** + +允许在启动 `AgentApp` 时注入自定义端点。 + +**用法示例** + +```java +AgentApp agentApp = new AgentApp(agentHandler); +agentApp.endpoint("/test/post", List.of("POST"), serverRequest -> ServerResponse.ok().body("OK")); +agentApp.run("localhost", 10001); +``` + +**说明** + +- 传入的 lambda 将直接作用于 Spring 的 `RouterFunction`,根据http method的不同进行route,入参限制为ServerRequest,返回值限制为ServerResponse。 + +------ + +## 生命周期钩子 + +**功能** + +允许在启动 `AgentApp` 前、后;销毁`AgentApp`前、后;退出`jvm`后做对应处理。 + +**用法示例** + +```java +AgentApp agentApp = new AgentApp(agentHandler); +agentApp.hooks(new AbstractAppLifecycleHook() { + @Override + public int operation() { + return BEFORE_RUN | AFTER_RUN | JVM_EXIT; + } + + @Override + public void beforeRun(HookContext context) { + System.out.println("beforeRun"); + } + + @Override + public void afterRun(HookContext context) { + System.out.println("afterRun"); + } +}); +agentApp.run("localhost", 10001); +``` + +**说明** + +- 传入的 AbstractAppLifecycleHook实现类 将直接作用于 `AgentApp`并按权重(整数自然排序),并传入`HookContext`(包装`AgentApp`、`DeployManager`)实例进行处理,context的extraInfo用于传递跨方法、跨线程变量。 +------ + + +## 中间件 + +**功能** + +允许在启动 `AgentApp` 前往spring上下文注入Filter。 + +**用法示例** + +```java +AgentApp agentApp = new AgentApp(agentHandler); +FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); + filterRegistrationBean.setFilter(new MyFilter()); + filterRegistrationBean.setBeanName("myFilter"); + filterRegistrationBean.setUrlPatterns(List.of("/test/get")); + filterRegistrationBean.setOrder(-1); +agentApp.middleware(filterRegistrationBean); +agentApp.run("localhost", 10001); + + +public static class MyFilter implements Filter{ + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + LOG.info("Current request uri = {}",request.getRequestURI()); + filterChain.doFilter(servletRequest,servletResponse); + } +} +``` + +**说明** + +- 基于servlet规范,将直接作用于 `AgentApp`关联的spring上下文对象,用于鉴权、请求计数等,目前暂不支持webflux。 +------ + + ## A2A 流式输出(SSE) **功能** diff --git a/engine-core/src/main/java/io/agentscope/runtime/engine/DeployManager.java b/engine-core/src/main/java/io/agentscope/runtime/engine/DeployManager.java index 0247efc2..57c5e2c0 100644 --- a/engine-core/src/main/java/io/agentscope/runtime/engine/DeployManager.java +++ b/engine-core/src/main/java/io/agentscope/runtime/engine/DeployManager.java @@ -15,7 +15,6 @@ */ package io.agentscope.runtime.engine; -import org.springframework.context.ApplicationContext; public interface DeployManager { void deploy(Runner runner); diff --git a/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithCustomizeEndpointExample.java b/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithCustomizeEndpointExample.java new file mode 100644 index 00000000..a45c49f6 --- /dev/null +++ b/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithCustomizeEndpointExample.java @@ -0,0 +1,48 @@ +/* + * Copyright 2025 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 + * + * https://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; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +import io.agentscope.runtime.app.AgentApp; + +import org.springframework.web.servlet.function.ServerResponse; + +public class AgentScopeDeployWithCustomizeEndpointExample { + + private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + + public static void main(String[] args) { + String[] commandLine = new String[2]; + commandLine[0] = "-f"; + commandLine[1] = Objects.requireNonNull(Thread.currentThread().getContextClassLoader().getResource(".env")).getPath(); + AgentApp app = new AgentApp(commandLine); + app.endpoint("/test/post", List.of("POST"),serverRequest -> { + String time = sdf.format(new Date()); + return ServerResponse.ok().body(String.format("time=%s,post successful",time)); + }); + app.endpoint("/test/get", List.of("GET"),serverRequest -> { + String time = sdf.format(new Date()); + return ServerResponse.ok().body(String.format("time=%s,get successful",time)); + }); + app.run(); + } +} diff --git a/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithLifecycleHookExample.java b/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithLifecycleHookExample.java new file mode 100644 index 00000000..d3890911 --- /dev/null +++ b/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithLifecycleHookExample.java @@ -0,0 +1,59 @@ +/* + * Copyright 2025 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 + * + * https://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; + +import java.util.Objects; + +import io.agentscope.runtime.app.AgentApp; +import io.agentscope.runtime.hook.AbstractAppLifecycleHook; +import io.agentscope.runtime.hook.HookContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AgentScopeDeployWithLifecycleHookExample { + + private static final Logger LOG = LoggerFactory.getLogger(AgentScopeDeployWithLifecycleHookExample.class); + + public static void main(String[] args) { + String[] commandLine = new String[2]; + commandLine[0] = "-f"; + commandLine[1] = Objects.requireNonNull(Thread.currentThread().getContextClassLoader().getResource(".env")).getPath(); + AgentApp app = new AgentApp(commandLine); + app.hooks(new AbstractAppLifecycleHook() { + @Override + public int operation() { + return BEFORE_RUN | AFTER_RUN | JVM_EXIT; + } + + @Override + public void beforeRun(HookContext context) { + LOG.info("beforeRun"); + } + + @Override + public void afterRun(HookContext context) { + LOG.info("afterRun"); + } + + @Override + public void onJvmExit(HookContext context) { + LOG.info("onJvmExit"); + } + }); + app.run(); + } +} diff --git a/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithMiddlewareExample.java b/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithMiddlewareExample.java new file mode 100644 index 00000000..a57dc0bb --- /dev/null +++ b/examples/simple_agent_use_examples/agentscope_use_example/src/main/java/io/agentscope/AgentScopeDeployWithMiddlewareExample.java @@ -0,0 +1,64 @@ +/* + * Copyright 2025 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 + * + * https://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; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +import io.agentscope.runtime.app.AgentApp; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; + +public class AgentScopeDeployWithMiddlewareExample { + + private static final Logger LOG = LoggerFactory.getLogger(AgentScopeDeployWithMiddlewareExample.class); + + public static void main(String[] args) { + String[] commandLine = new String[2]; + commandLine[0] = "-f"; + commandLine[1] = Objects.requireNonNull(Thread.currentThread().getContextClassLoader().getResource(".env")).getPath(); + AgentApp app = new AgentApp(commandLine); + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); + filterRegistrationBean.setFilter(new MyFilter()); + filterRegistrationBean.setBeanName("myFilter"); + filterRegistrationBean.setUrlPatterns(List.of("/test/get")); + filterRegistrationBean.setOrder(-1); + app.middleware(filterRegistrationBean); + app.run(); + } + + + + public static class MyFilter implements Filter{ + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + LOG.info("Current request uri = {}",request.getRequestURI()); + filterChain.doFilter(servletRequest,servletResponse); + } + } +} diff --git a/web/src/main/java/io/agentscope/runtime/LocalDeployManager.java b/web/src/main/java/io/agentscope/runtime/LocalDeployManager.java index 9ecefb2c..ef233740 100644 --- a/web/src/main/java/io/agentscope/runtime/LocalDeployManager.java +++ b/web/src/main/java/io/agentscope/runtime/LocalDeployManager.java @@ -21,6 +21,7 @@ import io.agentscope.runtime.engine.Runner; import io.agentscope.runtime.protocol.Protocol; import io.agentscope.runtime.protocol.ProtocolConfig; +import jakarta.servlet.Filter; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,18 +29,17 @@ import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.context.ApplicationContext; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.http.HttpMethod; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.springframework.web.servlet.function.HandlerFunction; import org.springframework.web.servlet.function.RouterFunction; import org.springframework.web.servlet.function.RouterFunctions; -import org.springframework.web.servlet.function.ServerRequest; import org.springframework.web.servlet.function.ServerResponse; import java.util.HashMap; @@ -59,6 +59,7 @@ public class LocalDeployManager implements DeployManager { private final List protocolConfigs; private final Consumer corsConfigurer; private final List customEndpoints; + private final List> middlewares; private LocalDeployManager(LocalDeployerManagerBuilder builder) { @@ -69,6 +70,7 @@ private LocalDeployManager(LocalDeployerManagerBuilder builder) { this.protocolConfigs = builder.protocolConfigs; this.corsConfigurer = builder.corsConfigurer; this.customEndpoints = builder.customEndpoints; + this.middlewares = builder.middlewares; } @Override @@ -119,22 +121,53 @@ public void addCorsMappings(@NotNull CorsRegistry registry) { } }); } - if (customEndpoints != null) { - RouterFunctions.Builder route = RouterFunctions.route(); - for (AgentApp.EndpointInfo customEndpoint : customEndpoints) { - route.POST(customEndpoint.path, request -> customEndpoint.handler.apply(request)); - - + if (customEndpoints != null && !customEndpoints.isEmpty()) { + RouterFunction routerFunction = routerFunction(); + ctx.registerBean(routerFunction.getClass(), routerFunction); + } + if (middlewares != null && !middlewares.isEmpty()) { + for (FilterRegistrationBean bean : middlewares) { + ctx.registerBean(bean.getFilterName(), FilterRegistrationBean.class, () -> bean); } - ctx.registerBean(RouterFunction.class,route.build()); } - }) .run(); - logger.info("Streaming deployment completed for endpoint: {}", endpointName); } + + protected RouterFunction routerFunction() { + RouterFunctions.Builder route = RouterFunctions.route(); + for (AgentApp.EndpointInfo customEndpoint : customEndpoints) { + List methods = customEndpoint.methods; + if (methods == null || methods.isEmpty()) { + continue; + } + if (methods.contains(HttpMethod.GET.name())) { + route.GET(customEndpoint.path, request -> customEndpoint.handler.apply(request)); + } + if (methods.contains(HttpMethod.HEAD.name())) { + route.HEAD(customEndpoint.path, request -> customEndpoint.handler.apply(request)); + } + if (methods.contains(HttpMethod.POST.name())) { + route.POST(customEndpoint.path, request -> customEndpoint.handler.apply(request)); + } + if (methods.contains(HttpMethod.PUT.name())) { + route.PUT(customEndpoint.path, request -> customEndpoint.handler.apply(request)); + } + if (methods.contains(HttpMethod.DELETE.name())) { + route.DELETE(customEndpoint.path, request -> customEndpoint.handler.apply(request)); + } + if (methods.contains(HttpMethod.PATCH.name())) { + route.PATCH(customEndpoint.path, request -> customEndpoint.handler.apply(request)); + } + if (methods.contains(HttpMethod.OPTIONS.name())) { + route.OPTIONS(customEndpoint.path, request -> customEndpoint.handler.apply(request)); + } + } + return route.build(); + } + @Override public void undeploy() { shutdown(); @@ -176,6 +209,7 @@ public static class LocalDeployerManagerBuilder { private List protocolConfigs = List.of(); private Consumer corsConfigurer; private List customEndpoints; + private List> middlewares; public LocalDeployerManagerBuilder endpointName(String endpointName) { this.endpointName = endpointName; @@ -212,6 +246,11 @@ public LocalDeployerManagerBuilder customEndpoints(List c return this; } + public LocalDeployerManagerBuilder middlewares(List> middlewares) { + this.middlewares = middlewares; + return this; + } + public LocalDeployManager build() { return new LocalDeployManager(this); } diff --git a/web/src/main/java/io/agentscope/runtime/app/AgentApp.java b/web/src/main/java/io/agentscope/runtime/app/AgentApp.java index 581f4d90..55f21c74 100644 --- a/web/src/main/java/io/agentscope/runtime/app/AgentApp.java +++ b/web/src/main/java/io/agentscope/runtime/app/AgentApp.java @@ -43,17 +43,20 @@ import io.agentscope.runtime.engine.services.memory.persistence.session.InMemorySessionHistoryService; import io.agentscope.runtime.engine.services.memory.service.MemoryService; import io.agentscope.runtime.engine.services.memory.service.SessionHistoryService; - -import io.agentscope.runtime.lifecycle.AppLifecycleHook; - +import io.agentscope.runtime.hook.AppLifecycleHook; +import io.agentscope.runtime.hook.HookContext; import io.agentscope.runtime.protocol.ProtocolConfig; import io.agentscope.runtime.sandbox.manager.ManagerConfig; import io.agentscope.runtime.sandbox.manager.SandboxService; +import io.agentscope.runtime.sandbox.manager.client.container.BaseClientStarter; +import io.agentscope.runtime.sandbox.manager.client.container.docker.DockerClientStarter; +import jakarta.servlet.Filter; import okio.Path; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.function.ServerRequest; import org.springframework.web.servlet.function.ServerResponse; @@ -61,11 +64,11 @@ import picocli.CommandLine; import picocli.CommandLine.Option; -import static io.agentscope.runtime.lifecycle.AppLifecycleHook.AFTER_RUN; -import static io.agentscope.runtime.lifecycle.AppLifecycleHook.AFTER_STOP; -import static io.agentscope.runtime.lifecycle.AppLifecycleHook.BEFORE_RUN; -import static io.agentscope.runtime.lifecycle.AppLifecycleHook.BEFORE_STOP; -import static io.agentscope.runtime.lifecycle.AppLifecycleHook.JVM_EXIT; +import static io.agentscope.runtime.hook.AppLifecycleHook.AFTER_RUN; +import static io.agentscope.runtime.hook.AppLifecycleHook.AFTER_STOP; +import static io.agentscope.runtime.hook.AppLifecycleHook.BEFORE_RUN; +import static io.agentscope.runtime.hook.AppLifecycleHook.BEFORE_STOP; +import static io.agentscope.runtime.hook.AppLifecycleHook.JVM_EXIT; /** @@ -97,6 +100,7 @@ public class AgentApp { private AgentHandler adapter; private volatile Runner runner; private DeployManager deployManager; + private volatile HookContext hookContext; // Configuration private String endpointPath; @@ -105,6 +109,7 @@ public class AgentApp { private boolean stream = true; private String responseType = "sse"; private Consumer corsConfigurer; + private final List> middlewares = new ArrayList<>(); private final List customEndpoints = new ArrayList<>(); private final List hooks = new ArrayList<>(); @@ -173,7 +178,8 @@ public AgentApp(String[] args) { StateService stateService = component(properties, STATE_SERVICE_PROVIDER, StateServiceProvider.class, new InMemoryStateService()); SessionHistoryService sessionHistoryService = component(properties, SESSION_HISTORY_SERVICE_PROVIDER, SessionHistoryServiceProvider.class, new InMemorySessionHistoryService()); MemoryService memoryService = component(properties, MEMORY_SERVICE_PROVIDER, MemoryServiceProvider.class, new InMemoryMemoryService()); - SandboxService sandboxService = component(properties, SANDBOX_SERVICE_PROVIDER, SandboxServiceProvider.class, new SandboxService(ManagerConfig.builder().build())); + BaseClientStarter clientConfig = DockerClientStarter.builder().build(); + SandboxService sandboxService = component(properties, SANDBOX_SERVICE_PROVIDER, SandboxServiceProvider.class, new SandboxService(ManagerConfig.builder().clientStarter(clientConfig).build())); ServiceComponentManager serviceComponentManager = new ServiceComponentManager(); serviceComponentManager.setStateService(stateService); serviceComponentManager.setSessionHistoryService(sessionHistoryService); @@ -460,32 +466,34 @@ public void run(String host, int port) { .endpointName(endpointPath) .protocolConfigs(protocolConfigs) .corsConfigurer(corsConfigurer) + .customEndpoints(customEndpoints) + .middlewares(middlewares) .build(); } buildRunner(); logger.info("[AgentApp] Starting AgentApp with endpoint: {}, host: {}, port: {}", endpointPath, host, port); - AgentApp app = this; + this.hookContext = new HookContext(this,deployManager); List lifecycleHooks = hooks.stream() .sorted(Comparator.comparingInt(AppLifecycleHook::priority)).toList(); Runtime.getRuntime().addShutdownHook(new Thread(() -> lifecycleHooks.stream().filter(e -> ((JVM_EXIT & e.operation()) != 0)) - .forEach(lifecycle -> lifecycle.onJvmExit(app, deployManager)))); + .forEach(lifecycle -> lifecycle.onJvmExit(hookContext)))); lifecycleHooks.stream() .filter(e -> ((BEFORE_RUN & e.operation()) != 0)) - .forEach(lifecycle -> lifecycle.beforeRun(app, deployManager)); + .forEach(lifecycle -> lifecycle.beforeRun(hookContext)); // Deploy via DeployManager deployManager.deploy(runner); logger.info("[AgentApp] AgentApp started successfully on {}:{}{}", host, port, endpointPath); lifecycleHooks.stream() .filter(e -> ((AFTER_RUN & e.operation()) != 0)) - .forEach(lifecycle -> lifecycle.afterRun(app, deployManager)); + .forEach(lifecycle -> lifecycle.afterRun(hookContext)); } /** * Register custom endpoint. */ - public AgentApp endpoint(String path, List methods, Function handler) { + public AgentApp endpoint(String path, List methods, Function handler) { if (methods == null || methods.isEmpty()) { methods = Arrays.asList("POST"); } @@ -499,6 +507,22 @@ public AgentApp endpoint(String path, List methods, Function filter) { + this.middlewares.add(filter); + return this; + } + + public List> middlewares() { + return middlewares; + } + + public List hooks() { + return hooks; + } + // Getters and setters public String getEndpointPath() { return endpointPath; @@ -536,6 +560,8 @@ public List getCustomEndpoints() { return customEndpoints; } + + /** * Endpoint information. */ @@ -649,17 +675,16 @@ public void stop() { return; } if (Objects.nonNull(deployManager)) { - AgentApp app = this; List lifecycleHooks = hooks.stream() .sorted(Comparator.comparingInt(AppLifecycleHook::priority)).toList(); lifecycleHooks.stream() .filter(e -> ((BEFORE_STOP & e.operation()) != 0)) - .forEach(lifecycle -> lifecycle.beforeStop(app, deployManager)); + .forEach(lifecycle -> lifecycle.beforeStop(hookContext)); deployManager.undeploy(); stopped.set(true); lifecycleHooks.stream() .filter(e -> ((AFTER_STOP & e.operation()) != 0)) - .forEach(lifecycle -> lifecycle.afterStop(app, deployManager)); + .forEach(lifecycle -> lifecycle.afterStop(hookContext)); } } } diff --git a/web/src/main/java/io/agentscope/runtime/hook/AbstractAppLifecycleHook.java b/web/src/main/java/io/agentscope/runtime/hook/AbstractAppLifecycleHook.java new file mode 100644 index 00000000..e5de914f --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/hook/AbstractAppLifecycleHook.java @@ -0,0 +1,27 @@ +/* + * Copyright 2025 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 + * + * https://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.runtime.hook; + +public abstract class AbstractAppLifecycleHook implements AppLifecycleHook { + + protected int operation; + + public AbstractAppLifecycleHook(){ + this.operation = ALL; + } + +} diff --git a/web/src/main/java/io/agentscope/runtime/hook/AppLifecycleHook.java b/web/src/main/java/io/agentscope/runtime/hook/AppLifecycleHook.java new file mode 100644 index 00000000..ca86af74 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/hook/AppLifecycleHook.java @@ -0,0 +1,58 @@ +/* + * Copyright 2025 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 + * + * https://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.runtime.hook; + +public interface AppLifecycleHook { + + int BEFORE_RUN = 1; + + int AFTER_RUN = 1 << 1; + + int BEFORE_STOP = 1 << 2; + + int AFTER_STOP = 1 << 3; + + int JVM_EXIT = 1 << 4; + + int ALL = BEFORE_RUN | AFTER_RUN | BEFORE_STOP | AFTER_STOP | JVM_EXIT; + + default void beforeRun(HookContext context) { + + } + + default void afterRun(HookContext context) { + + } + + default void beforeStop(HookContext context) { + + } + + default void afterStop(HookContext context) { + + } + + default void onJvmExit(HookContext context) { + + } + + default int priority() { + return 0; + } + + int operation(); +} diff --git a/web/src/main/java/io/agentscope/runtime/hook/HookContext.java b/web/src/main/java/io/agentscope/runtime/hook/HookContext.java new file mode 100644 index 00000000..d4aab331 --- /dev/null +++ b/web/src/main/java/io/agentscope/runtime/hook/HookContext.java @@ -0,0 +1,47 @@ +/* + * Copyright 2025 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 + * + * https://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.runtime.hook; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.agentscope.runtime.app.AgentApp; +import io.agentscope.runtime.engine.DeployManager; + +public class HookContext { + private final AgentApp app; + private final DeployManager deployManager; + private final Map extraInfo = new ConcurrentHashMap<>(); + + public HookContext(AgentApp app, DeployManager deployManager) { + this.app = app; + this.deployManager = deployManager; + } + + public AgentApp getApp() { + return app; + } + + public DeployManager getDeployManager() { + return deployManager; + } + + public Map getExtraInfo() { + return extraInfo; + } +} diff --git a/web/src/main/java/io/agentscope/runtime/lifecycle/AbstractAppLifecycleHook.java b/web/src/main/java/io/agentscope/runtime/lifecycle/AbstractAppLifecycleHook.java deleted file mode 100644 index 30e11de3..00000000 --- a/web/src/main/java/io/agentscope/runtime/lifecycle/AbstractAppLifecycleHook.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.agentscope.runtime.lifecycle; - -public abstract class AbstractAppLifecycleHook implements AppLifecycleHook { - - protected int operation; - - public AbstractAppLifecycleHook(){ - this.operation = ALL; - } - -} diff --git a/web/src/main/java/io/agentscope/runtime/lifecycle/AppLifecycleHook.java b/web/src/main/java/io/agentscope/runtime/lifecycle/AppLifecycleHook.java deleted file mode 100644 index e3e31c55..00000000 --- a/web/src/main/java/io/agentscope/runtime/lifecycle/AppLifecycleHook.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.agentscope.runtime.lifecycle; - -import io.agentscope.runtime.app.AgentApp; -import io.agentscope.runtime.engine.DeployManager; - -public interface AppLifecycleHook { - - int BEFORE_RUN = 1; - - int AFTER_RUN = 1 << 1; - - int BEFORE_STOP = 1 << 2; - - int AFTER_STOP = 1 << 3; - - int JVM_EXIT = 1 << 4; - - int ALL = BEFORE_RUN | AFTER_RUN | BEFORE_STOP | AFTER_STOP | JVM_EXIT; - - default void beforeRun(AgentApp app, DeployManager deployManager) { - - } - - default void afterRun(AgentApp app, DeployManager deployManager) { - - } - - default void beforeStop(AgentApp app, DeployManager deployManager) { - - } - - default void afterStop(AgentApp app, DeployManager deployManager) { - - } - - default void onJvmExit(AgentApp app, DeployManager deployManager) { - - } - - default int priority() { - return 0; - } - - int operation(); -} diff --git a/web/src/test/java/io/agentscope/runtime/deployer/AgentScopeDeployTests.java b/web/src/test/java/io/agentscope/runtime/deployer/AgentScopeDeployTests.java new file mode 100644 index 00000000..fe74e32d --- /dev/null +++ b/web/src/test/java/io/agentscope/runtime/deployer/AgentScopeDeployTests.java @@ -0,0 +1,187 @@ +/* + * Copyright 2025 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 + * + * https://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.runtime.deployer; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +import io.agentscope.runtime.adapters.AgentHandler; +import io.agentscope.runtime.adapters.agentscope.MyAgentScopeAgentHandler; +import io.agentscope.runtime.app.AgentApp; +import io.agentscope.runtime.hook.AbstractAppLifecycleHook; +import io.agentscope.runtime.hook.HookContext; +import io.agentscope.runtime.sandbox.manager.ManagerConfig; +import io.agentscope.runtime.sandbox.manager.SandboxService; +import jakarta.servlet.Filter; +import jakarta.servlet.http.HttpServletRequest; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.web.servlet.function.ServerResponse; + +import static io.agentscope.runtime.hook.AppLifecycleHook.AFTER_RUN; +import static io.agentscope.runtime.hook.AppLifecycleHook.BEFORE_RUN; + +public class AgentScopeDeployTests { + + private static AgentApp app; + + private static AtomicInteger flag; + + private static UUID uuid; + + @BeforeAll + static void setUp() { + URL resource = Thread.currentThread().getContextClassLoader().getResource(".env"); + AgentApp app; + if (Objects.nonNull(resource)) { + String[] commandLine = new String[2]; + commandLine[0] = "-f"; + commandLine[1] = Objects.requireNonNull(resource).getPath(); + app = new AgentApp(commandLine); + } + else { + // ci can not get the .env file, here it is injected through the system properties + System.setProperty("agent.app.port", "7779"); + System.setProperty("agent.app.handler.provider.class", "io.agentscope.runtime.deployer.AgentScopeDeployWithCommandLineTests$MyAgentHandlerProvider"); + System.setProperty("agent.app.sandbox.service.provider.class", "io.agentscope.runtime.deployer.AgentScopeDeployWithCommandLineTests$MySandboxServiceProvider"); + System.setProperty("AI_DASHSCOPE_API_KEY", "your key"); + app = new AgentApp(new String[0]); + } + AgentScopeDeployTests.app = app; + flag = new AtomicInteger(0); + app.hooks(new AbstractAppLifecycleHook() { + @Override + public int operation() { + return BEFORE_RUN | AFTER_RUN | JVM_EXIT; + } + + @Override + public void beforeRun(HookContext context) { + flag.set(flag.get() | BEFORE_RUN); + } + + @Override + public void afterRun(HookContext context) { + flag.set(flag.get() | AFTER_RUN); + } + }); + uuid = UUID.randomUUID(); + AgentScopeDeployTests.app.endpoint("/test/get", List.of("GET"), serverRequest -> ServerResponse.ok() + .body(uuid.toString())); + AgentScopeDeployTests.app.endpoint("/test/post", List.of("POST"), serverRequest -> ServerResponse.ok() + .body("OK")); + FilterRegistrationBean filterRegistrationBean = authFilter(); + AgentScopeDeployTests.app.middleware(filterRegistrationBean); + AgentScopeDeployTests.app.run(); + + } + + @NotNull + private static FilterRegistrationBean authFilter() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); + filterRegistrationBean.setFilter((servletRequest, servletResponse, filterChain) -> { + HttpServletRequest request = (HttpServletRequest) servletRequest; + String token = request.getHeader("token"); + if (Objects.isNull(token) || !token.equals("token")) { + servletResponse.getWriter().write("Unauthorized"); + return; + } + filterChain.doFilter(servletRequest, servletResponse); + }); + filterRegistrationBean.setBeanName("authFilter"); + filterRegistrationBean.setOrder(-1); + filterRegistrationBean.setUrlPatterns(List.of("/test/post")); + return filterRegistrationBean; + } + + @Test + void testHooks() { + Assertions.assertTrue((flag.get() & BEFORE_RUN) > 0); + Assertions.assertTrue((flag.get() & AFTER_RUN) > 0); + } + + @Test + void testCustomizeEndpoint() throws IOException, InterruptedException { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:7779/test/get")) + .GET() + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + Assertions.assertEquals(response.body(), uuid.toString()); + + + } + + @Test + void testMiddleware() throws IOException, InterruptedException { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest requestWithoutToken = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:7779/test/post")) + .POST(HttpRequest.BodyPublishers.ofString("")) + .build(); + HttpResponse response1 = client.send(requestWithoutToken, HttpResponse.BodyHandlers.ofString()); + Assertions.assertEquals("Unauthorized", response1.body()); + + HttpRequest requestWithToken = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:7779/test/post")) + .header("token", "token") // 添加token请求头 + .POST(HttpRequest.BodyPublishers.ofString("")) + .build(); + + HttpResponse response2 = client.send(requestWithToken, HttpResponse.BodyHandlers.ofString()); + Assertions.assertEquals("OK", response2.body()); + + } + + public static class MyAgentHandlerProvider implements AgentApp.AgentHandlerProvider { + + @Override + public AgentHandler get(Properties properties, AgentApp.ServiceComponentManager serviceComponentManager) { + MyAgentScopeAgentHandler handler = new MyAgentScopeAgentHandler(); + handler.setStateService(serviceComponentManager.getStateService()); + handler.setSandboxService(serviceComponentManager.getSandboxService()); + handler.setMemoryService(serviceComponentManager.getMemoryService()); + handler.setSessionHistoryService(serviceComponentManager.getSessionHistoryService()); + return handler; + } + } + + public static class MySandboxServiceProvider implements AgentApp.SandboxServiceProvider { + @Override + public SandboxService get(Properties properties) { + ManagerConfig managerConfig = ManagerConfig.builder() + .build(); + return new SandboxService( + managerConfig); + } + } +} From b122897e20286d271a80dc73072f93c0dfe4a963 Mon Sep 17 00:00:00 2001 From: jiangzhen Date: Mon, 12 Jan 2026 17:20:46 +0800 Subject: [PATCH 3/5] port --- .../deployer/AgentScopeDeployTests.java | 29 +++++-------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/web/src/test/java/io/agentscope/runtime/deployer/AgentScopeDeployTests.java b/web/src/test/java/io/agentscope/runtime/deployer/AgentScopeDeployTests.java index fe74e32d..c7eb67fe 100644 --- a/web/src/test/java/io/agentscope/runtime/deployer/AgentScopeDeployTests.java +++ b/web/src/test/java/io/agentscope/runtime/deployer/AgentScopeDeployTests.java @@ -18,7 +18,6 @@ import java.io.IOException; import java.net.URI; -import java.net.URL; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -59,23 +58,11 @@ public class AgentScopeDeployTests { @BeforeAll static void setUp() { - URL resource = Thread.currentThread().getContextClassLoader().getResource(".env"); - AgentApp app; - if (Objects.nonNull(resource)) { - String[] commandLine = new String[2]; - commandLine[0] = "-f"; - commandLine[1] = Objects.requireNonNull(resource).getPath(); - app = new AgentApp(commandLine); - } - else { - // ci can not get the .env file, here it is injected through the system properties - System.setProperty("agent.app.port", "7779"); - System.setProperty("agent.app.handler.provider.class", "io.agentscope.runtime.deployer.AgentScopeDeployWithCommandLineTests$MyAgentHandlerProvider"); - System.setProperty("agent.app.sandbox.service.provider.class", "io.agentscope.runtime.deployer.AgentScopeDeployWithCommandLineTests$MySandboxServiceProvider"); - System.setProperty("AI_DASHSCOPE_API_KEY", "your key"); - app = new AgentApp(new String[0]); - } - AgentScopeDeployTests.app = app; + System.setProperty("agent.app.port", "7778"); + System.setProperty("agent.app.handler.provider.class", "io.agentscope.runtime.deployer.AgentScopeDeployWithCommandLineTests$MyAgentHandlerProvider"); + System.setProperty("agent.app.sandbox.service.provider.class", "io.agentscope.runtime.deployer.AgentScopeDeployWithCommandLineTests$MySandboxServiceProvider"); + System.setProperty("AI_DASHSCOPE_API_KEY", "your key"); + AgentScopeDeployTests.app = new AgentApp(new String[0]); flag = new AtomicInteger(0); app.hooks(new AbstractAppLifecycleHook() { @Override @@ -132,7 +119,7 @@ void testHooks() { void testCustomizeEndpoint() throws IOException, InterruptedException { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:7779/test/get")) + .uri(URI.create("http://localhost:7778/test/get")) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); @@ -145,14 +132,14 @@ void testCustomizeEndpoint() throws IOException, InterruptedException { void testMiddleware() throws IOException, InterruptedException { HttpClient client = HttpClient.newHttpClient(); HttpRequest requestWithoutToken = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:7779/test/post")) + .uri(URI.create("http://localhost:7778/test/post")) .POST(HttpRequest.BodyPublishers.ofString("")) .build(); HttpResponse response1 = client.send(requestWithoutToken, HttpResponse.BodyHandlers.ofString()); Assertions.assertEquals("Unauthorized", response1.body()); HttpRequest requestWithToken = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:7779/test/post")) + .uri(URI.create("http://localhost:7778/test/post")) .header("token", "token") // 添加token请求头 .POST(HttpRequest.BodyPublishers.ofString("")) .build(); From a2db4e8c004454a76fc9acbcef8c1f5be3ab9cee Mon Sep 17 00:00:00 2001 From: jiangzhen Date: Tue, 13 Jan 2026 09:47:42 +0800 Subject: [PATCH 4/5] add shutdown ut,remove chinese code annotation --- .../java/io/agentscope/runtime/app/AgentApp.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/web/src/main/java/io/agentscope/runtime/app/AgentApp.java b/web/src/main/java/io/agentscope/runtime/app/AgentApp.java index 55f21c74..25d4ba18 100644 --- a/web/src/main/java/io/agentscope/runtime/app/AgentApp.java +++ b/web/src/main/java/io/agentscope/runtime/app/AgentApp.java @@ -677,14 +677,18 @@ public void stop() { if (Objects.nonNull(deployManager)) { List lifecycleHooks = hooks.stream() .sorted(Comparator.comparingInt(AppLifecycleHook::priority)).toList(); - lifecycleHooks.stream() - .filter(e -> ((BEFORE_STOP & e.operation()) != 0)) - .forEach(lifecycle -> lifecycle.beforeStop(hookContext)); + if (Objects.nonNull(hookContext)){ + lifecycleHooks.stream() + .filter(e -> ((BEFORE_STOP & e.operation()) != 0)) + .forEach(lifecycle -> lifecycle.beforeStop(hookContext)); + } deployManager.undeploy(); stopped.set(true); - lifecycleHooks.stream() - .filter(e -> ((AFTER_STOP & e.operation()) != 0)) - .forEach(lifecycle -> lifecycle.afterStop(hookContext)); + if (Objects.nonNull(hookContext)){ + lifecycleHooks.stream() + .filter(e -> ((AFTER_STOP & e.operation()) != 0)) + .forEach(lifecycle -> lifecycle.afterStop(hookContext)); + } } } } From 83b16eceafd3dc5f2e1c8b86a31e7a93246ba629 Mon Sep 17 00:00:00 2001 From: jiangzhen Date: Tue, 13 Jan 2026 09:51:46 +0800 Subject: [PATCH 5/5] add shutdown ut,remove chinese code annotation --- .../deployer/AgentScopeDeployTests.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/web/src/test/java/io/agentscope/runtime/deployer/AgentScopeDeployTests.java b/web/src/test/java/io/agentscope/runtime/deployer/AgentScopeDeployTests.java index c7eb67fe..c7f50148 100644 --- a/web/src/test/java/io/agentscope/runtime/deployer/AgentScopeDeployTests.java +++ b/web/src/test/java/io/agentscope/runtime/deployer/AgentScopeDeployTests.java @@ -46,7 +46,9 @@ import org.springframework.web.servlet.function.ServerResponse; import static io.agentscope.runtime.hook.AppLifecycleHook.AFTER_RUN; +import static io.agentscope.runtime.hook.AppLifecycleHook.AFTER_STOP; import static io.agentscope.runtime.hook.AppLifecycleHook.BEFORE_RUN; +import static io.agentscope.runtime.hook.AppLifecycleHook.BEFORE_STOP; public class AgentScopeDeployTests { @@ -67,7 +69,7 @@ static void setUp() { app.hooks(new AbstractAppLifecycleHook() { @Override public int operation() { - return BEFORE_RUN | AFTER_RUN | JVM_EXIT; + return ALL; } @Override @@ -79,6 +81,16 @@ public void beforeRun(HookContext context) { public void afterRun(HookContext context) { flag.set(flag.get() | AFTER_RUN); } + + @Override + public void beforeStop(HookContext context) { + flag.set(flag.get() | BEFORE_STOP); + } + + @Override + public void afterStop(HookContext context) { + flag.set(flag.get() | AFTER_STOP); + } }); uuid = UUID.randomUUID(); AgentScopeDeployTests.app.endpoint("/test/get", List.of("GET"), serverRequest -> ServerResponse.ok() @@ -125,7 +137,6 @@ void testCustomizeEndpoint() throws IOException, InterruptedException { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); Assertions.assertEquals(response.body(), uuid.toString()); - } @Test @@ -140,7 +151,7 @@ void testMiddleware() throws IOException, InterruptedException { HttpRequest requestWithToken = HttpRequest.newBuilder() .uri(URI.create("http://localhost:7778/test/post")) - .header("token", "token") // 添加token请求头 + .header("token", "token") // add token to header .POST(HttpRequest.BodyPublishers.ofString("")) .build(); @@ -149,6 +160,13 @@ void testMiddleware() throws IOException, InterruptedException { } + @Test + void testShutdown() { + app.stop(); + Assertions.assertTrue((flag.get() & BEFORE_STOP) > 0); + Assertions.assertTrue((flag.get() & AFTER_STOP) > 0); + } + public static class MyAgentHandlerProvider implements AgentApp.AgentHandlerProvider { @Override