diff --git a/.gitignore b/.gitignore
index 9a8ec89b0..6382cfe58 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,6 +24,7 @@
*.del
*.pmd
.tern-project
+*.factorypath
# Logs and databases #
@@ -52,8 +53,10 @@ Thumbs.db
# Build output directies
/target
*/target
+**/target
/build
*/build
+**/build
# IntelliJ specific files/directories
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..87b0a885f
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,237 @@
+# AGENTS.md
+
+This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
+
+## Overview
+
+LiteFlow (v2.15.3) is a lightweight rules engine framework for complex component-based business orchestration. It uses a DSL to drive workflows with support for hot reload and 11 scripting languages. The project targets Java 8+ (up to JDK 25) and has 2000+ test cases.
+
+**Official Documentation**: https://liteflow.cc/pages/5816c5/
+
+## Commands
+
+### Build
+```bash
+# Build entire project (uses 'compile' profile by default, includes test modules)
+mvn clean package -DskipTests
+
+# Build with tests
+mvn clean package
+
+# Build specific module
+mvn clean package -DskipTests -pl liteflow-core
+
+# Build for release (production modules only, excludes tests)
+mvn clean package -DskipTests -P release
+```
+
+### Run Tests
+```bash
+# Run all tests
+mvn test
+
+# Run tests for specific module (30+ test modules in liteflow-testcase-el/)
+mvn test -pl liteflow-testcase-el/liteflow-testcase-el-springboot
+
+# Run single test class
+mvn test -pl liteflow-core -Dtest=FlowExecutorTest
+
+# Run specific test method
+mvn test -pl liteflow-core -Dtest=FlowExecutorTest#testExecute
+```
+
+### Other Commands
+```bash
+# Dependency tree
+mvn dependency:tree
+
+# View module structure
+ls liteflow-*/pom.xml
+```
+
+## High-Level Architecture
+
+### Core Execution Model
+
+**FlowExecutor** → **Chain** → **Condition Tree** → **Node Components**
+
+1. **FlowExecutor**: Entry point for executing workflows (`execute2Resp(chainId, param)`)
+2. **FlowBus**: Central metadata registry for all chains and nodes (thread-safe, supports hot reload)
+3. **Chain**: Named workflow composed of an EL expression that compiles to a Condition tree
+4. **Slot/DataBus**: Thread-safe context management using slot pooling (configurable `slotSize`)
+5. **NodeComponent**: Base class for all business logic components
+
+### Key Architectural Patterns
+
+#### 1. Two-Stage Parsing
+Chains are built in two phases to handle circular dependencies:
+- **Phase 1**: Register chain IDs (creates placeholder chains)
+- **Phase 2**: Build complete condition trees with EL parsing
+
+#### 2. EL Expression Language
+Uses QLExpress to parse declarative workflows. Example operators:
+- **THEN(a, b, c)**: Sequential execution
+- **WHEN(a, b, c)**: Parallel execution (async)
+- **IF(condition, trueNode, falseNode)**: Conditional branching
+- **SWITCH(selector).to(a, b, c)**: Multi-way branching
+- **FOR(count).DO(loop)**: Fixed-count loop
+- **WHILE(condition).DO(loop)**: Condition-based loop
+- **ITERATOR(iterator).DO(loop)**: Iterator-based loop
+- **RETRY(node).times(3).forException(Ex.class)**: Retry mechanism
+- **CATCH(node).DO(handler)**: Exception handling
+- **TIMEOUT(node).time(1000)**: Execution timeout (ms)
+- **PRE(a, b)**: Pre-conditions (always run before chain, even on error)
+- **FINALLY(a, b)**: Finally-conditions (always run after chain)
+- **AND(a, b)**, **OR(a, b)**, **NOT(a)**: Boolean logic for IF conditions
+- **node.tag("t")**, **.data("k","v")**, **.id("id")**: Per-node modifiers
+
+Rules are defined in XML/JSON/YML:
+```xml
+
+ THEN(a, WHEN(b, c).maxWaitSeconds(5), IF(e, f, g));
+
+```
+
+#### 3. Component Types
+All extend `NodeComponent` but have specialized behaviors:
+- **NodeComponent**: Standard synchronous component (`process()` method)
+- **NodeBooleanComponent**: Returns boolean for IF conditions (`processBoolean()`)
+- **NodeSwitchComponent**: Returns string for SWITCH routing (`processSwitch()`)
+- **NodeIteratorComponent**: Provides iteration logic for ITERATOR construct
+- **NodeForComponent**: Controls FOR loop behavior
+- **ScriptComponent**: Script-based components (Groovy, JS, Python, etc.)
+
+**Declarative Component Pattern**: Any Spring bean can become a component without extending base classes by using `@LiteflowCmpDefine` (class-level, specifies `NodeTypeEnum`) and `@LiteflowMethod` (method-level, maps to `LiteFlowMethodEnum`). This avoids class hierarchy constraints.
+
+**Component Lifecycle Hooks** (override in NodeComponent subclasses or via `@LiteflowMethod`):
+- `isAccess()` – gate check before execution; return `false` to skip
+- `beforeProcess()` / `afterProcess()` – pre/post hooks per component
+- `onSuccess()` / `onError()` – outcome callbacks
+- `isContinueOnError()` – whether WHEN continues if this node fails
+- `isEnd()` – signals chain should stop after this node
+- `rollback()` – called in reverse order on failure
+
+**`@FallbackCmp`**: Annotate a fallback component that activates when the primary component is not found or throws.
+
+#### 4. Slot-Based Context
+Thread-safe execution context:
+- `DataBus.offerSlot(chainId)` acquires a slot from pool
+- Slot contains execution metadata, context beans, and step tracking
+- `DataBus.releaseSlot(slotIndex)` returns slot to pool
+- Pool size configurable via `slotSize` property
+
+#### 5. Parse Mode Strategies
+Three modes (`ParseModeEnum`):
+- **PARSE_ALL_ON_START**: Parse all chains at startup (default)
+- **PARSE_ONE_ON_FIRST_EXEC**: Lazy parse each chain on first use (faster startup)
+- **PARSE_ALL_ON_FIRST_EXEC**: Parse all chains on first any chain execution
+
+### Module Structure
+
+#### Core Modules
+- **liteflow-core**: Core engine (FlowExecutor, FlowBus, DataBus, Slot, Condition system, component model)
+- **liteflow-el-builder**: Programmatic chain builder API using QLExpress
+
+#### Rule Source Plugins (6 implementations in `liteflow-rule-plugin/`)
+- **liteflow-rule-zk**: ZooKeeper configuration source
+- **liteflow-rule-sql**: SQL database configuration source
+- **liteflow-rule-nacos**: Nacos configuration center
+- **liteflow-rule-etcd**: etcd configuration source
+- **liteflow-rule-apollo**: Apollo configuration center
+- **liteflow-rule-redis**: Redis configuration source
+
+#### Script Plugins (11 languages in `liteflow-script-plugin/`)
+- **liteflow-script-groovy**: Groovy scripting
+- **liteflow-script-javascript**: Rhino JavaScript (JSR223)
+- **liteflow-script-graaljs**: GraalVM JavaScript
+- **liteflow-script-qlexpress**: Alibaba QLExpress
+- **liteflow-script-python**: Jython (Python on JVM)
+- **liteflow-script-lua**: LuaJ
+- **liteflow-script-aviator**: Aviator expression language
+- **liteflow-script-java**: Janino (Java compiler)
+- **liteflow-script-javax**: JSR223 standard Java compiler
+- **liteflow-script-javax-pro**: Liquor-based Java compiler (enhanced)
+- **liteflow-script-kotlin**: Kotlin scripting
+
+#### Framework Integration
+- **liteflow-spring**: Spring framework integration (component scanning, bean lifecycle, AOP)
+- **liteflow-spring-boot-starter**: Spring Boot auto-configuration with `@ConfigurationProperties`
+- **liteflow-solon-plugin**: Solon framework integration (lightweight alternative to Spring)
+
+#### Testing Infrastructure
+30+ test modules in `liteflow-testcase-el/` organized by:
+1. **Framework**: springboot, springnative, solon, nospring
+2. **Config Sources**: zk, nacos, etcd, apollo, redis, sql
+3. **Scripts**: One module per language + multi-language scenarios
+4. **Features**: builder, declare, routechain, monitoring, timeout, etc.
+
+Test pattern:
+```java
+@SpringBootTest
+@TestPropertySource(value = "classpath:/application.properties")
+public class MyTest {
+ @Resource private FlowExecutor flowExecutor;
+
+ @Test
+ public void test() {
+ LiteflowResponse response = flowExecutor.execute2Resp("chainId", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ }
+}
+```
+
+### Important Design Patterns
+
+#### SPI Pattern for Extensibility
+Used extensively for:
+- `ContextAware`: Framework abstraction (Spring vs non-Spring)
+- `PathContentParser`: Custom file path resolution
+- `CmpAroundAspect`: Global component lifecycle hooks
+- `DeclComponentParser`: Declarative component parsing
+- Script executors for each language
+
+#### Lifecycle Hooks
+Multiple extension points:
+- `PostProcessChainBuildLifeCycle`: Before/after chain building
+- `PostProcessNodeBuildLifeCycle`: Before/after node building
+- `PostProcessChainExecuteLifeCycle`: Before/after chain execution
+- `PostProcessFlowExecuteLifeCycle`: Before/after flow execution
+
+#### Rollback Mechanism
+Components implement `rollback()` for automatic rollback on failure (executed in reverse order via `executeSteps` deque).
+
+#### Hot Reload Support
+- `MonitorFile` watches rule files for changes
+- `reloadRule()` refreshes without restart
+- Copy-on-write collections (unless `fastLoad=true`) prevent concurrent modification
+
+#### Metadata Caching
+- `FlowBus` uses CopyOnWriteHashMap (or regular HashMap with `fastLoad`)
+- EL MD5 caching for expression reuse
+- Chain caching configurable (`chainCacheEnabled`, `chainCacheCapacity`)
+
+### Critical Configuration (`LiteflowConfig`)
+- **ruleSource**: Rule file locations (supports XML, JSON, YML)
+- **parseMode**: Parsing strategy (affects startup performance)
+- **slotSize**: Context slot pool size
+- **enableMonitorFile**: Hot reload of rule files
+- **supportMultipleType**: Mix XML/JSON/YML rules
+- **whenMaxWaitTime**: Timeout for WHEN parallel execution
+- **fastLoad**: Disable CopyOnWrite for faster startup
+- **enableVirtualThread**: Use virtual threads (JDK 21+)
+
+### Key File Locations
+- Core engine: `liteflow-core/src/main/java/com/yomahub/liteflow/flow/`
+- FlowExecutor: `liteflow-core/src/main/java/com/yomahub/liteflow/core/FlowExecutor.java`
+- FlowBus: `liteflow-core/src/main/java/com/yomahub/liteflow/flow/FlowBus.java`
+- Component base: `liteflow-core/src/main/java/com/yomahub/liteflow/core/NodeComponent.java`
+- Condition system: `liteflow-core/src/main/java/com/yomahub/liteflow/flow/element/condition/`
+- EL parser: `liteflow-core/src/main/java/com/yomahub/liteflow/parser/el/`
+
+### Important Conventions
+- **Naming**: Components identified by `nodeId`, chains by `chainId`
+- **Thread Safety**: Extensive ThreadLocal and concurrent collections
+- **Fail-Fast**: Validation at parse time with detailed error messages
+- **Fluent APIs**: Builder pattern for chain construction (EL builder)
+- **Namespaces**: Chains can be organized into namespaces
+- **Versioning**: Uses `${revision}` placeholder (currently 2.15.3) via flatten-maven-plugin
diff --git a/CLAUDE.md b/CLAUDE.md
index f9d2965b9..8783cc488 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -71,12 +71,19 @@ Chains are built in two phases to handle circular dependencies:
#### 2. EL Expression Language
Uses QLExpress to parse declarative workflows. Example operators:
- **THEN(a, b, c)**: Sequential execution
-- **WHEN(a, b, c)**: Parallel execution
-- **IF(condition, THEN(x), ELSE(y))**: Conditional branching
+- **WHEN(a, b, c)**: Parallel execution (async)
+- **IF(condition, trueNode, falseNode)**: Conditional branching
- **SWITCH(selector).to(a, b, c)**: Multi-way branching
-- **FOR(count).DO(loop)**: Loop execution
-- **RETRY(node).times(3).forException()**: Retry mechanism
+- **FOR(count).DO(loop)**: Fixed-count loop
+- **WHILE(condition).DO(loop)**: Condition-based loop
+- **ITERATOR(iterator).DO(loop)**: Iterator-based loop
+- **RETRY(node).times(3).forException(Ex.class)**: Retry mechanism
- **CATCH(node).DO(handler)**: Exception handling
+- **TIMEOUT(node).time(1000)**: Execution timeout (ms)
+- **PRE(a, b)**: Pre-conditions (always run before chain, even on error)
+- **FINALLY(a, b)**: Finally-conditions (always run after chain)
+- **AND(a, b)**, **OR(a, b)**, **NOT(a)**: Boolean logic for IF conditions
+- **node.tag("t")**, **.data("k","v")**, **.id("id")**: Per-node modifiers
Rules are defined in XML/JSON/YML:
```xml
@@ -94,6 +101,18 @@ All extend `NodeComponent` but have specialized behaviors:
- **NodeForComponent**: Controls FOR loop behavior
- **ScriptComponent**: Script-based components (Groovy, JS, Python, etc.)
+**Declarative Component Pattern**: Any Spring bean can become a component without extending base classes by using `@LiteflowCmpDefine` (class-level, specifies `NodeTypeEnum`) and `@LiteflowMethod` (method-level, maps to `LiteFlowMethodEnum`). This avoids class hierarchy constraints.
+
+**Component Lifecycle Hooks** (override in NodeComponent subclasses or via `@LiteflowMethod`):
+- `isAccess()` – gate check before execution; return `false` to skip
+- `beforeProcess()` / `afterProcess()` – pre/post hooks per component
+- `onSuccess()` / `onError()` – outcome callbacks
+- `isContinueOnError()` – whether WHEN continues if this node fails
+- `isEnd()` – signals chain should stop after this node
+- `rollback()` – called in reverse order on failure
+
+**`@FallbackCmp`**: Annotate a fallback component that activates when the primary component is not found or throws.
+
#### 4. Slot-Based Context
Thread-safe execution context:
- `DataBus.offerSlot(chainId)` acquires a slot from pool
diff --git a/docs/superpowers/plans/2026-04-29-react-agent-model-spec.md b/docs/superpowers/plans/2026-04-29-react-agent-model-spec.md
index ce4328bf0..8d2fb319b 100644
--- a/docs/superpowers/plans/2026-04-29-react-agent-model-spec.md
+++ b/docs/superpowers/plans/2026-04-29-react-agent-model-spec.md
@@ -90,7 +90,7 @@ class ModelSpecTest {
/** 仅用于测试的最小 ModelSpec 子类。 */
static class TestSpec extends ModelSpec {
- @Override protected Model resolve(AgentConfig cfg) { return null; }
+ @Override public Model resolve(AgentConfig cfg) { return null; }
}
@Test
@@ -192,11 +192,16 @@ public abstract class ModelSpec> {
* 把本描述符解析为 agentscope {@link Model} 实例。
* 实现需从 {@link AgentConfig} 中读取对应平台的 credential,
* 并把共性 + 个性参数翻译成 agentscope 的 GenerateOptions。
+ *
+ * 本方法是框架 SPI:{@code ReActAgentComponent} 在不同包中调用,
+ * 因此必须为 {@code public}(subclass override 同样需要 {@code public})。
*/
- protected abstract Model resolve(AgentConfig cfg);
+ public abstract Model resolve(AgentConfig cfg);
}
```
+> **重要**:`resolve` 必须是 `public`,因为 Task 8 中 `ReActAgentComponent`(在 `com.yomahub.liteflow.agent.component` 包)会调用 `model(ctx).resolve(agentConfig())`。Java 不允许 override 缩小可见性,所以后续 Tasks 3-7 中所有 spec 子类的 `resolve` override 也必须是 `public`,测试中的 `TestSpec.resolve` 同理。
+
- [ ] **Step 4: 再次运行测试,确认通过**
```bash
@@ -513,7 +518,7 @@ public class OpenAISpec extends ModelSpec {
public Double getPresencePenalty() { return presencePenalty; }
@Override
- protected Model resolve(AgentConfig cfg) {
+ public Model resolve(AgentConfig cfg) {
PlatformCredential cred = CredentialResolver.requireFirstClass(
cfg.getOpenai(), "liteflow.agent.openai");
return buildModel(cred.getApiKey(), cred.getBaseUrl());
@@ -740,7 +745,7 @@ public class OpenAICompatibleSpec extends OpenAISpec {
}
@Override
- protected Model resolve(AgentConfig cfg) {
+ public Model resolve(AgentConfig cfg) {
PlatformCredential cred = CredentialResolver.requireCompatible(
cfg.getOpenaiCompatible(), configKey, "liteflow.agent.openai-compatible");
String baseUrl = (cred.getBaseUrl() != null && !cred.getBaseUrl().isBlank())
@@ -1018,7 +1023,7 @@ public class AnthropicSpec extends ModelSpec {
public Boolean getThinkingEnabled() { return thinkingEnabled; }
@Override
- protected Model resolve(AgentConfig cfg) {
+ public Model resolve(AgentConfig cfg) {
PlatformCredential cred;
if (compatibleConfigKey == null) {
cred = CredentialResolver.requireFirstClass(
@@ -1239,7 +1244,7 @@ public class GeminiSpec extends ModelSpec {
public Integer getThinkingBudget() { return thinkingBudget; }
@Override
- protected Model resolve(AgentConfig cfg) {
+ public Model resolve(AgentConfig cfg) {
PlatformCredential cred = CredentialResolver.requireFirstClass(
cfg.getGemini(), "liteflow.agent.gemini");
@@ -1429,7 +1434,7 @@ public class DashScopeSpec extends ModelSpec {
public Integer getThinkingBudget() { return thinkingBudget; }
@Override
- protected Model resolve(AgentConfig cfg) {
+ public Model resolve(AgentConfig cfg) {
PlatformCredential cred = CredentialResolver.requireFirstClass(
cfg.getDashscope(), "liteflow.agent.dashscope");
diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/AgentConfig.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/AgentConfig.java
index 71f7d5684..631317dc8 100644
--- a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/AgentConfig.java
+++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/AgentConfig.java
@@ -13,6 +13,7 @@ public class AgentConfig {
private PlatformCredential gemini = new PlatformCredential();
private PlatformCredential dashscope = new PlatformCredential();
private Map openaiCompatible = new LinkedHashMap<>();
+ private Map anthropicCompatible = new LinkedHashMap<>();
public WorkspaceConfig getWorkspace() { return workspace; }
public void setWorkspace(WorkspaceConfig v) { this.workspace = v; }
@@ -32,4 +33,6 @@ public class AgentConfig {
public void setDashscope(PlatformCredential v) { this.dashscope = v; }
public Map getOpenaiCompatible() { return openaiCompatible; }
public void setOpenaiCompatible(Map v) { this.openaiCompatible = v; }
+ public Map getAnthropicCompatible() { return anthropicCompatible; }
+ public void setAnthropicCompatible(Map v) { this.anthropicCompatible = v; }
}
diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MemoryStorageConfig.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MemoryStorageConfig.java
new file mode 100644
index 000000000..870a02606
--- /dev/null
+++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MemoryStorageConfig.java
@@ -0,0 +1,40 @@
+package com.yomahub.liteflow.property.agent;
+
+/**
+ * Memory persistence settings for ReActAgent sessions.
+ *
+ *
This config is intentionally orthogonal to {@link SessionConfig} (which
+ * controls JVM-side session caching, idle timeout, LRU eviction). Memory
+ * storage decides where the agent's conversation history is durably
+ * kept; session config decides how long a hot agent is held in memory.
+ */
+public class MemoryStorageConfig {
+ /** Default is {@link MemoryStorageMode#JVM} so existing deployments behave unchanged. */
+ private MemoryStorageMode mode = MemoryStorageMode.JVM;
+
+ private WorkspaceMemoryConfig workspace = new WorkspaceMemoryConfig();
+ private RedisMemoryConfig redis = new RedisMemoryConfig();
+ private MysqlMemoryConfig mysql = new MysqlMemoryConfig();
+
+ /** Whether to lazily load existing session state on first {@code process()}. */
+ private boolean loadOnFirstUse = true;
+ /** Whether to save session state after a successful {@code process()}. */
+ private boolean saveAfterCall = true;
+ /** Whether to save session state when {@code process()} throws. */
+ private boolean saveOnError = true;
+
+ public MemoryStorageMode getMode() { return mode; }
+ public void setMode(MemoryStorageMode mode) { this.mode = mode; }
+ public WorkspaceMemoryConfig getWorkspace() { return workspace; }
+ public void setWorkspace(WorkspaceMemoryConfig workspace) { this.workspace = workspace; }
+ public RedisMemoryConfig getRedis() { return redis; }
+ public void setRedis(RedisMemoryConfig redis) { this.redis = redis; }
+ public MysqlMemoryConfig getMysql() { return mysql; }
+ public void setMysql(MysqlMemoryConfig mysql) { this.mysql = mysql; }
+ public boolean isLoadOnFirstUse() { return loadOnFirstUse; }
+ public void setLoadOnFirstUse(boolean loadOnFirstUse) { this.loadOnFirstUse = loadOnFirstUse; }
+ public boolean isSaveAfterCall() { return saveAfterCall; }
+ public void setSaveAfterCall(boolean saveAfterCall) { this.saveAfterCall = saveAfterCall; }
+ public boolean isSaveOnError() { return saveOnError; }
+ public void setSaveOnError(boolean saveOnError) { this.saveOnError = saveOnError; }
+}
diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MemoryStorageMode.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MemoryStorageMode.java
new file mode 100644
index 000000000..cfd2515c1
--- /dev/null
+++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MemoryStorageMode.java
@@ -0,0 +1,24 @@
+package com.yomahub.liteflow.property.agent;
+
+/**
+ * Storage backend used to persist a ReActAgent's memory across executions
+ * within a session.
+ *
+ *
+ *
{@link #NONE} – do not persist or even hold memory; equivalent to a stateless agent
+ *
{@link #JVM} – keep memory in JVM heap only (default; behaviour identical to pre-2.15.4 releases)
+ *
{@link #WORKSPACE_FILE} – persist memory as JSON files under each session's workspace directory
+ * using AgentScope's JsonSession
+ *
{@link #REDIS} – persist memory through AgentScope's RedisSession; requires the user to
+ * provide a {@code RedissonClient} / {@code UnifiedJedis} / {@code RedisClient} bean
+ *
{@link #MYSQL} – persist memory through AgentScope's MysqlSession; requires the user to
+ * provide a {@code javax.sql.DataSource} bean
+ *
+ */
+public enum MemoryStorageMode {
+ NONE,
+ JVM,
+ WORKSPACE_FILE,
+ REDIS,
+ MYSQL
+}
diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MysqlMemoryConfig.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MysqlMemoryConfig.java
new file mode 100644
index 000000000..5f491c27c
--- /dev/null
+++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MysqlMemoryConfig.java
@@ -0,0 +1,30 @@
+package com.yomahub.liteflow.property.agent;
+
+/**
+ * Settings that only apply when {@link MemoryStorageMode#MYSQL} is selected.
+ *
+ *
The DataSource is supplied by the user via {@code beanName} and looked up
+ * through ContextAware. LiteFlow never builds a JDBC connection pool itself.
+ */
+public class MysqlMemoryConfig {
+ /** Bean name of the {@link javax.sql.DataSource} to look up via ContextAware. */
+ private String dataSourceBeanName;
+
+ /** Database name passed to {@code MysqlSession}. Empty means use AgentScope's default ({@code agentscope}). */
+ private String databaseName;
+
+ /** Table name passed to {@code MysqlSession}. Empty means use AgentScope's default ({@code agentscope_sessions}). */
+ private String tableName;
+
+ /** When true, AgentScope auto-creates database & table; defaults to false to respect production constraints. */
+ private boolean createIfNotExist = false;
+
+ public String getDataSourceBeanName() { return dataSourceBeanName; }
+ public void setDataSourceBeanName(String dataSourceBeanName) { this.dataSourceBeanName = dataSourceBeanName; }
+ public String getDatabaseName() { return databaseName; }
+ public void setDatabaseName(String databaseName) { this.databaseName = databaseName; }
+ public String getTableName() { return tableName; }
+ public void setTableName(String tableName) { this.tableName = tableName; }
+ public boolean isCreateIfNotExist() { return createIfNotExist; }
+ public void setCreateIfNotExist(boolean createIfNotExist) { this.createIfNotExist = createIfNotExist; }
+}
diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/RedisMemoryConfig.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/RedisMemoryConfig.java
new file mode 100644
index 000000000..fb8752231
--- /dev/null
+++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/RedisMemoryConfig.java
@@ -0,0 +1,28 @@
+package com.yomahub.liteflow.property.agent;
+
+/**
+ * Settings that only apply when {@link MemoryStorageMode#REDIS} is selected.
+ *
+ *
Connections are not created by LiteFlow. Users must provide a Redis client
+ * bean (Redisson / Jedis / Lettuce) through the framework's {@code ContextAware}
+ * (Spring bean lookup, Solon container, etc.).
+ */
+public class RedisMemoryConfig {
+ /** Bean name of the Redis client to look up via ContextAware. */
+ private String beanName;
+
+ /** Type of the configured client; affects how AgentScope adapts it. */
+ private RedisClientType clientType = RedisClientType.REDISSON;
+
+ /** Key prefix used inside Redis. */
+ private String keyPrefix = "liteflow:agent:session";
+
+ public String getBeanName() { return beanName; }
+ public void setBeanName(String beanName) { this.beanName = beanName; }
+ public RedisClientType getClientType() { return clientType; }
+ public void setClientType(RedisClientType clientType) { this.clientType = clientType; }
+ public String getKeyPrefix() { return keyPrefix; }
+ public void setKeyPrefix(String keyPrefix) { this.keyPrefix = keyPrefix; }
+
+ public enum RedisClientType { REDISSON, JEDIS, LETTUCE }
+}
diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/SessionConfig.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/SessionConfig.java
index 78caa927c..af4d6955a 100644
--- a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/SessionConfig.java
+++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/SessionConfig.java
@@ -6,6 +6,7 @@ public class SessionConfig {
private Duration idleTimeout = Duration.ofMinutes(30);
private Duration cleanupInterval = Duration.ofMinutes(1);
private int maxSessions = 10_000;
+ private MemoryStorageConfig memory = new MemoryStorageConfig();
public Duration getIdleTimeout() { return idleTimeout; }
public void setIdleTimeout(Duration v) { this.idleTimeout = v; }
@@ -13,4 +14,6 @@ public class SessionConfig {
public void setCleanupInterval(Duration v) { this.cleanupInterval = v; }
public int getMaxSessions() { return maxSessions; }
public void setMaxSessions(int v) { this.maxSessions = v; }
+ public MemoryStorageConfig getMemory() { return memory; }
+ public void setMemory(MemoryStorageConfig memory) { this.memory = memory; }
}
diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/WorkspaceMemoryConfig.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/WorkspaceMemoryConfig.java
new file mode 100644
index 000000000..fc12ae168
--- /dev/null
+++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/WorkspaceMemoryConfig.java
@@ -0,0 +1,14 @@
+package com.yomahub.liteflow.property.agent;
+
+/**
+ * Settings that only apply when {@link MemoryStorageMode#WORKSPACE_FILE} is selected.
+ *
+ *
The persistence sub-directory is hard-coded to {@code .agent-session} so it
+ * stays out of the way of tool-produced files in the per-session workspace and
+ * is not configurable on purpose; users who want a custom location can plug in
+ * their own {@code AgentSessionFactory} via SPI.
+ */
+public class WorkspaceMemoryConfig {
+ /** Fixed sub-directory created under the workspace root that holds session JSON files. */
+ public static final String SUB_DIR = ".agent-session";
+}
diff --git a/liteflow-core/src/test/java/com/yomahub/liteflow/property/agent/AgentConfigTest.java b/liteflow-core/src/test/java/com/yomahub/liteflow/property/agent/AgentConfigTest.java
index 9429d4c7e..2e5071b7f 100644
--- a/liteflow-core/src/test/java/com/yomahub/liteflow/property/agent/AgentConfigTest.java
+++ b/liteflow-core/src/test/java/com/yomahub/liteflow/property/agent/AgentConfigTest.java
@@ -15,6 +15,8 @@ class AgentConfigTest {
assertNotNull(c.getDefaults());
assertNotNull(c.getOpenaiCompatible());
assertTrue(c.getOpenaiCompatible().isEmpty());
+ assertNotNull(c.getAnthropicCompatible());
+ assertTrue(c.getAnthropicCompatible().isEmpty());
WorkspaceConfig w = c.getWorkspace();
assertNull(w.getRoot());
@@ -43,8 +45,12 @@ class AgentConfigTest {
void platform_credentials_are_independent_instances() {
AgentConfig c = new AgentConfig();
c.getOpenai().setApiKey("k1");
+ c.getOpenaiCompatible().put("compatible-openai", new PlatformCredential());
+ c.getAnthropicCompatible().put("compatible-anthropic", new PlatformCredential());
assertNull(c.getAnthropic().getApiKey());
assertNull(c.getGemini().getApiKey());
assertNull(c.getDashscope().getApiKey());
+ assertFalse(c.getOpenaiCompatible().containsKey("compatible-anthropic"));
+ assertFalse(c.getAnthropicCompatible().containsKey("compatible-openai"));
}
}
diff --git a/liteflow-react-agent/liteflow-react-agent-anthropic/README.md b/liteflow-react-agent/liteflow-react-agent-anthropic/README.md
deleted file mode 100644
index fd547f2df..000000000
--- a/liteflow-react-agent/liteflow-react-agent-anthropic/README.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# liteflow-react-agent-anthropic
-
-Anthropic Claude 模型支持。
-
-## 使用
-
-```java
-AnthropicChatModel model = AnthropicModelFactory.of(apiKey, "claude-sonnet-4-6");
-```
\ No newline at end of file
diff --git a/liteflow-react-agent/liteflow-react-agent-anthropic/pom.xml b/liteflow-react-agent/liteflow-react-agent-anthropic/pom.xml
index 6da3601d8..11e0f6135 100644
--- a/liteflow-react-agent/liteflow-react-agent-anthropic/pom.xml
+++ b/liteflow-react-agent/liteflow-react-agent-anthropic/pom.xml
@@ -14,8 +14,8 @@
jar
- 21
- 21
+ 17
+ 17
@@ -34,23 +34,5 @@
anthropic-java1.2.0
-
- org.junit.jupiter
- junit-jupiter
- ${junit.version}
- test
-
-
-
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
-
- false
-
-
-
-
\ No newline at end of file
diff --git a/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/AnthropicModelFactory.java b/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/AnthropicModelFactory.java
index 7cbb2dc39..560989e96 100644
--- a/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/AnthropicModelFactory.java
+++ b/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/AnthropicModelFactory.java
@@ -11,4 +11,12 @@ public final class AnthropicModelFactory {
.modelName(modelName)
.build();
}
-}
\ No newline at end of file
+
+ public static AnthropicChatModel custom(String apiKey, String baseUrl, String modelName) {
+ return AnthropicChatModel.builder()
+ .apiKey(apiKey)
+ .baseUrl(baseUrl)
+ .modelName(modelName)
+ .build();
+ }
+}
diff --git a/liteflow-react-agent/liteflow-react-agent-core/README.md b/liteflow-react-agent/liteflow-react-agent-core/README.md
deleted file mode 100644
index ff311dabc..000000000
--- a/liteflow-react-agent/liteflow-react-agent-core/README.md
+++ /dev/null
@@ -1,73 +0,0 @@
-# liteflow-react-agent-core
-
-## 快速上手
-
-### 1. 添加依赖(选择至少一个平台模块)
-
-```xml
-
- com.yomahub
- liteflow-react-agent-openai
- ${liteflow.version}
-
-```
-
-### 2. 配置
-
-```yaml
-liteflow:
- agent:
- workspace:
- root: /var/lib/liteflow/agent-workspaces
- shell:
- mode: whitelist
- whitelist: [ls, cat, grep]
- openai-compatible:
- deepseek:
- api-key: ${DEEPSEEK_API_KEY}
- base-url: https://api.deepseek.com/v1
-```
-
-### 3. 定义 Agent
-
-```java
-@LiteflowComponent("reviewAgent")
-public class ReviewAgent extends ReActAgentComponent {
- @Override protected Model buildModel(ReActAgentContext ctx) {
- return OpenAICompatiblePresets.deepseek(
- agentConfig().getOpenaiCompatible().get("deepseek").getApiKey(),
- "deepseek-chat"
- );
- }
- @Override protected String systemPrompt(ReActAgentContext ctx) { return "你是审核专家"; }
- @Override protected String userPrompt(ReActAgentContext ctx) {
- return ctx.getSlot().getRequestData(String.class);
- }
-}
-```
-
-### 4. EL 编排
-
-```xml
-
- THEN(prepare, reviewAgent, notify);
-
-```
-
-## 核心概念
-
-- **Session**:由 `resolveSessionId` 决定;默认 `slot.getRequestId()`(一次性)。覆写后可复用 memory 与 workspace 实现多轮对话。
-- **Workspace**:每 session 一个目录,在 `liteflow.agent.workspace.root` 之下。内置 `WorkspaceFileTools` 强制路径围栏。
-- **Shell**:`ManagedShellCommandTool` 按 `liteflow.agent.shell.mode` 做 whitelist/blacklist/disabled 校验,首 token 不在策略内即拒绝。
-
-## 可选覆写
-
-| 方法 | 默认行为 | 说明 |
-|------|----------|------|
-| `tools(ctx)` | 空列表 | 注册自定义 agentscope @Tool 对象 |
-| `resolveSessionId(slot)` | `slot.getRequestId()` | 覆写以实现多轮对话 |
-| `maxIterations()` | 配置文件默认值 (15) | ReAct 最大推理轮数 |
-| `enableShellTool()` | true | 是否启用受管 shell 工具 |
-| `enableWorkspaceFileTools()` | true | 是否启用受管文件工具 |
-| `hooks(ctx)` | 空列表 | agentscope Hook 列表 |
-| `handleReply(reply, ctx)` | 写入 slot.responseData | 自定义回复处理 |
\ No newline at end of file
diff --git a/liteflow-react-agent/liteflow-react-agent-core/pom.xml b/liteflow-react-agent/liteflow-react-agent-core/pom.xml
index baa49cbd8..4146a332b 100644
--- a/liteflow-react-agent/liteflow-react-agent-core/pom.xml
+++ b/liteflow-react-agent/liteflow-react-agent-core/pom.xml
@@ -16,8 +16,8 @@
jar
- 21
- 21
+ 17
+ 17
@@ -43,36 +43,5 @@
hutool-core${hutool.version}
-
-
- org.junit.jupiter
- junit-jupiter
- ${junit.version}
- test
-
-
- org.mockito
- mockito-core
- 4.11.0
- test
-
-
- org.junit.platform
- junit-platform-console-standalone
- 1.8.2
- test
-
-
-
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
-
- false
-
-
-
-
diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/package-info.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/package-info.java
index 1e40fd8e3..5cbc5376f 100644
--- a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/package-info.java
+++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/package-info.java
@@ -1,4 +1,4 @@
/**
- * LiteFlow ReAct Agent core module.
+ * LiteFlow ReAct Agent 核心模块。
*/
package com.yomahub.liteflow.agent;
diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/AgentSessionManager.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/AgentSessionManager.java
index 2b35fc2ec..30301e927 100644
--- a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/AgentSessionManager.java
+++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/AgentSessionManager.java
@@ -1,7 +1,13 @@
package com.yomahub.liteflow.agent.session;
import com.yomahub.liteflow.agent.exception.AgentConfigException;
+import com.yomahub.liteflow.agent.session.factory.AgentSessionFactoryRegistry;
import com.yomahub.liteflow.property.agent.AgentConfig;
+import com.yomahub.liteflow.property.agent.MemoryStorageConfig;
+import com.yomahub.liteflow.property.agent.MemoryStorageMode;
+import io.agentscope.core.ReActAgent;
+import io.agentscope.core.session.Session;
+import io.agentscope.core.session.SessionManager;
import java.io.IOException;
import java.net.URLEncoder;
@@ -18,6 +24,19 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
+/**
+ * 跟踪当前 JVM 中存活的 AgentSession,并桥接到可插拔的
+ * {@link Session}(由 {@link AgentSessionFactoryRegistry} 提供)。
+ *
+ *
这里将两类职责保持独立:
+ *
+ *
JVM 内缓存、加锁和 LRU 淘汰(本类负责)
+ *
持久化存储(委托给 AgentScope 的 Session 抽象)
+ *
+ *
+ *
淘汰(空闲或超过容量)只移除缓存的 agent 实例。
+ * workspace 文件以及磁盘、Redis、MySQL 中的持久化 session 数据会被保留。
+ */
public class AgentSessionManager implements AutoCloseable {
private static final Pattern SAFE = Pattern.compile("[a-zA-Z0-9_\\-]+");
@@ -26,6 +45,8 @@ public class AgentSessionManager implements AutoCloseable {
private final Path root;
private final Map sessions = new ConcurrentHashMap<>();
private final ScheduledExecutorService cleaner;
+ /** memory 模式为 NONE 时可能为 null。 */
+ private final Session storage;
public AgentSessionManager(AgentConfig config) {
this.config = config;
@@ -42,6 +63,7 @@ public class AgentSessionManager implements AutoCloseable {
} else if (!Files.isDirectory(root)) {
throw new AgentConfigException("workspace root does not exist: " + root);
}
+ this.storage = AgentSessionFactoryRegistry.createSession(config);
long every = Math.max(20, config.getSession().getCleanupInterval().toMillis());
this.cleaner = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "liteflow-agent-session-cleaner");
@@ -68,6 +90,35 @@ public class AgentSessionManager implements AutoCloseable {
return sessions.containsKey(safeId(sessionId));
}
+ /**
+ * 将之前持久化的状态懒加载恢复到 agent 中。
+ * 同一个 session id 在当前 JVM 生命周期内应只调用一次,并且应在 agent
+ * 构建完成后、首次 {@code agent.call(...)} 前调用。
+ */
+ public void loadIfExists(AgentSession session, ReActAgent agent) {
+ if (storage == null || agent == null) return;
+ MemoryStorageConfig mc = config.getSession().getMemory();
+ if (!mc.isLoadOnFirstUse()) return;
+ if (mc.getMode() == MemoryStorageMode.NONE) return;
+ SessionManager.forSessionId(session.getSessionId())
+ .withSession(storage)
+ .addComponent(agent)
+ .loadIfExists();
+ }
+
+ /** 持久化 agent 当前状态。失败会向调用方暴露。 */
+ public void save(AgentSession session, ReActAgent agent) {
+ if (storage == null || agent == null) return;
+ MemoryStorageConfig mc = config.getSession().getMemory();
+ if (mc.getMode() == MemoryStorageMode.NONE) return;
+ SessionManager.forSessionId(session.getSessionId())
+ .withSession(storage)
+ .addComponent(agent)
+ .saveSession();
+ }
+
+ public Session storage() { return storage; }
+
static String safeId(String raw) {
if (raw == null || raw.isEmpty()) return "_";
if (SAFE.matcher(raw).matches()) return raw;
@@ -79,7 +130,8 @@ public class AgentSessionManager implements AutoCloseable {
while (sessions.size() > max) {
sessions.values().stream()
.min(Comparator.comparing(AgentSession::getLastActive))
- .ifPresent(victim -> remove(victim, true));
+ // LRU 淘汰只移除 JVM 内缓存;持久化数据保持不变。
+ .ifPresent(victim -> evictFromCache(victim, false));
}
}
@@ -89,14 +141,20 @@ public class AgentSessionManager implements AutoCloseable {
if (s.getLastActive().isAfter(cutoff)) continue;
if (!s.getLock().tryLock()) continue;
try {
- remove(s, config.getWorkspace().isCleanupOnSessionExpire());
+ evictFromCache(s, config.getWorkspace().isCleanupOnSessionExpire());
} finally {
s.getLock().unlock();
}
}
}
- private void remove(AgentSession s, boolean cleanWorkspace) {
+ /**
+ * @param cleanWorkspace 为 true 时,同时删除磁盘上的 workspace 目录
+ * (保留历史行为)。存储在其他位置的持久化 session
+ * 状态(例如 workspaceRoot/.agent-session、Redis、
+ * MySQL)不会在这里被删除。
+ */
+ private void evictFromCache(AgentSession s, boolean cleanWorkspace) {
sessions.remove(s.getSessionId(), s);
if (cleanWorkspace) {
deleteRecursively(s.getWorkspaceDir());
@@ -118,6 +176,9 @@ public class AgentSessionManager implements AutoCloseable {
if (config.getWorkspace().isCleanupOnJvmShutdown()) {
sessions.values().forEach(s -> deleteRecursively(s.getWorkspaceDir()));
}
+ if (storage != null) {
+ try { storage.close(); } catch (Exception ignored) {}
+ }
sessions.clear();
}
}
diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/AgentSessionFactory.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/AgentSessionFactory.java
new file mode 100644
index 000000000..f5c90469a
--- /dev/null
+++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/AgentSessionFactory.java
@@ -0,0 +1,30 @@
+package com.yomahub.liteflow.agent.session.factory;
+
+import com.yomahub.liteflow.property.agent.AgentConfig;
+import com.yomahub.liteflow.property.agent.MemoryStorageMode;
+import io.agentscope.core.session.Session;
+
+/**
+ * 用于为 {@link io.agentscope.core.session.Session} 接入额外持久化后端的 SPI。
+ *
+ *