feat(agent): AgentStateStoreResolver 映射 MemoryStorageMode 到 v2 AgentStateStore (NONE/JVM/LOCAL_FILE/REDIS/MYSQL/OSS)

- 新增 AgentStateStoreResolver.resolve(AgentConfig): 按 MemoryStorageMode 选 v2
  AgentStateStore,替代 Task 0 删除的 AgentSessionFactory SPI 层。
- NONE→null(v2 stateStore 默认 null,save/load 短路,不造 NoOp 类);
  JVM→InMemoryAgentStateStore;LOCAL_FILE→JsonFileAgentStateStore(root/.agent-session);
  REDIS→反射构造 RedisAgentStateStore(Redisson/Jedis/Lettuce 三选一,沿用 1.0 延迟失败范式);
  MYSQL→反射构造 MysqlAgentStateStore(DataSource + 可选 db/table/createIfNotExist 重载)。
- findings R1-ext 新增 Redis/MySQL/OSS 扩展 store 反射探针签名(含 OSS,供将来扩展枚举复用)。
- 测试 5 例(NONE/JVM/LOCAL_FILE + REDIS/MYSQL beanName 缺失快速失败)全绿,
  用 javac + JUnit console-standalone 跑(sibling 测试 WIP,留待 Task 3.2 修后正式 mvn 跑)。
- react-agent-core / liteflow-core 均 BUILD SUCCESS(green commit 不破)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
everywhere.z 2026-06-19 23:11:04 +08:00
parent 70d7bbadc4
commit f0616e8fb8
4 changed files with 400 additions and 0 deletions

View File

@ -57,6 +57,61 @@ default void close(); // 默认 no
---
## R1-extRedis / MySQL / OSS 扩展 `AgentStateStore` 实现签名Task 2.1 探针确认)
**来源:** 对 `~/.m2/repository/io/agentscope/agentscope-extensions-{redis,mysql,oss}/2.0.0-RC3/*.jar``javap -p`Task 2.1JDK 21。三个扩展 jar 已在 `liteflow-react-agent-core` pom 以 `<optional>true</optional>` 声明,故 core 编译期不产生硬依赖resolver 用反射构造,运行期缺失驱动时按"延迟失败"抛 `AgentConfigException`
### Redis — `io.agentscope.extensions.redis.state.RedisAgentStateStore`
- 接口:`implements io.agentscope.core.state.AgentStateStore`。
- 构造:**私有的 `RedisAgentStateStore(Builder)`**,对外只有 `public static Builder builder()`
- `RedisAgentStateStore.Builder`(实测全量方法):
```java
Builder(); // 无参
Builder keyPrefix(String);
Builder jedisClient(redis.clients.jedis.UnifiedJedis);
Builder lettuceClient(io.lettuce.core.RedisClient);
Builder lettuceClusterClient(io.lettuce.core.cluster.RedisClusterClient); // v2 新增(集群)
Builder redissonClient(org.redisson.api.RedissonClient);
Builder clientAdapter(io.agentscope.extensions.redis.state.RedisClientAdapter); // 通用 SPI 逃生口
RedisAgentStateStore build();
```
- **另有 `RedissonAgentStateStore``...state.redisson`/ `JedisAgentStateStore``...state.jedis`)两个独立实现**,各自带 `Builder`、各自只接单一客户端类型。Task 2.1 **不使用**这两个(用户配的 `clientType` 三选一 + 单一 `RedisAgentStateStore.Builder` 已覆盖 REDISSON/JEDIS/LETTUCE 全部场景,语义与 1.0 `RedisAgentSessionFactory` 的反射路径一致)。
- `RedisClientAdapter` 是内部 SPI 接口(`set/get/rightPushList/rangeList/getListLength/deleteKeys/addToSet/getSetMembers/getSetSize/keyExists/findKeysByPattern/close`),用户通常不直接实现。
- 对 `RedisMemoryConfig.RedisClientType` 的映射(沿用 1.0 范式):
- `REDISSON``redissonClient(RedissonClient)`
- `JEDIS``jedisClient(UnifiedJedis)`
- `LETTUCE``lettuceClient(RedisClient)`**非集群**;集群需 v2 新的 `lettuceClusterClient`,但 `RedisClientType` 枚举目前未区分集群,按非集群走)
### MySQL — `io.agentscope.extensions.mysql.state.MysqlAgentStateStore`
- 接口:`implements io.agentscope.core.state.AgentStateStore`。
- 构造:**没有 Builder直接 public 构造器**
```java
MysqlAgentStateStore(javax.sql.DataSource);
MysqlAgentStateStore(javax.sql.DataSource, boolean createIfNotExist);
MysqlAgentStateStore(javax.sql.DataSource, String databaseName, String tableName, boolean createIfNotExist);
```
- 默认库名 `agentscope`、表名 `agentscope_sessions``DEFAULT_DATABASE_NAME`/`DEFAULT_TABLE_NAME` 私有常量)。
- 对 `MysqlMemoryConfig` 的映射:
- `dataSourceBeanName`(必填)→ `ContextAwareHolder.getBean(...)``DataSource`
- `databaseName`/`tableName` 均留空 → 走 `(DataSource, createIfNotExist)` 双参重载。
- 任一非空 → 走 `(DataSource, databaseName, tableName, createIfNotExist)` 四参重载(留空的那个传 null由 store 内部回退默认)。
- `createIfNotExist` → 第 2/4 参数。
### OSS — `io.agentscope.extensions.oss.OssAgentStateStore`
- 接口:`implements io.agentscope.core.state.AgentStateStore`。
- 构造:私有的 `OssAgentStateStore(Builder)`,对外 `public static Builder builder()`
- `OssAgentStateStore.Builder`(实测全量方法):
```java
Builder();
Builder ossClient(com.aliyun.oss.OSS);
Builder bucketName(String);
Builder keyPrefix(String);
OssAgentStateStore build();
```
- **注意:** `MemoryStorageMode` 枚举目前只有 NONE/JVM/LOCAL_FILE/REDIS/MYSQL 五档,**没有 OSS 档**。Task 2.1 resolver 不实现 OSS无对应 mode 可映射);此签名记录于此供将来扩展 `MemoryStorageMode.OSS` 时直接复用,避免重复探针。
---
## R4各 vendor `XxxChatModel.builder()` 方法名(名字有分歧!)
**全部位于 `io.agentscope.core.model` 包**(不在独立 vendor jar都在 core 里)。`public static Builder builder()` 工厂每个都有。**关键差异stream 标志与 generateOptions 的方法名因 vendor 不同**。

View File

@ -0,0 +1,233 @@
package com.yomahub.liteflow.agent.state;
import com.yomahub.liteflow.agent.exception.AgentConfigException;
import com.yomahub.liteflow.property.agent.AgentConfig;
import com.yomahub.liteflow.property.agent.LocalFileMemoryConfig;
import com.yomahub.liteflow.property.agent.MemoryStorageMode;
import com.yomahub.liteflow.property.agent.MysqlMemoryConfig;
import com.yomahub.liteflow.property.agent.RedisMemoryConfig;
import com.yomahub.liteflow.spi.holder.ContextAwareHolder;
import io.agentscope.core.state.AgentStateStore;
import io.agentscope.core.state.InMemoryAgentStateStore;
import io.agentscope.core.state.JsonFileAgentStateStore;
import javax.sql.DataSource;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* liteflow {@link MemoryStorageMode} 映射到 agentscope v2
* {@link AgentStateStore}替代 Task 0 删除的整个 {@code AgentSessionFactory} SPI
*
* <p>这是会话/状态层重设计迁移 spec §4.4的第一块resolver 只负责
* "按 mode 选 store 实现"不负责拼存储 keyv2 store 内部按
* {@code (userId, sessionId)} 对寻址{@code userId = conversationId}
* {@code sessionId = agentKey} Task 2.3 {@code RuntimeContext} 里填
*
* <h2>mode 映射</h2>
* <ul>
* <li>{@link MemoryStorageMode#NONE} 返回 {@code null}v2 {@code ReActAgent.builder()}
* {@code stateStore} 字段默认即 {@code null}{@code ReActAgent} 构造期对
* {@code stateStore} 全部用 {@code if (stateStore != null)} 守卫null
* save/load/exists 全部短路等价于 NONEfindings R1 实测<b>不造 NoOp </b></li>
* <li>{@link MemoryStorageMode#JVM} {@code new InMemoryAgentStateStore()}core 自带</li>
* <li>{@link MemoryStorageMode#LOCAL_FILE}
* {@code new JsonFileAgentStateStore(<workspace.root/.agent-session>)}core 自带
* 子目录名固定为 {@link LocalFileMemoryConfig#SUB_DIR}与各 session workspace 平级</li>
* <li>{@link MemoryStorageMode#REDIS} 反射构造
* {@code io.agentscope.extensions.redis.state.RedisAgentStateStore}optional 扩展 jar
* 客户端 bean 由用户在容器中注册Redisson/Jedis/Lettuce 三选一
* {@code liteflow.agent.session.memory.redis.beanName} {@link ContextAwareHolder} 查找</li>
* <li>{@link MemoryStorageMode#MYSQL} 反射构造
* {@code io.agentscope.extensions.mysql.state.MysqlAgentStateStore}optional 扩展 jar
* {@code javax.sql.DataSource} bean 同样由用户注册 {@code dataSourceBeanName} 查找</li>
* </ul>
*
* <p><b>延迟失败语义沿用 1.0 {@code RedisAgentSessionFactory}/{@code MysqlAgentSessionFactory}
* 范式</b> core 模块编译期对 Redis/MySQL 驱动与扩展 jar 都是 optional无硬依赖
* 若用户选了 REDIS/MYSQL classpath 缺扩展 jar或容器里找不到对应 beanresolver
* {@code process()} 首次调用时而不是框架启动时 {@link AgentConfigException}
*
* <p>{@code MemoryStorageMode} 目前没有 OSS findings R1-ext故本类不处理 OSS
* 将来若新增 OSS 其构造签名{@code OssAgentStateStore.builder().ossClient(OSS)
* .bucketName(String).keyPrefix(String).build()}已在 R1-ext 记录可直接复用
*/
public final class AgentStateStoreResolver {
/** Redis 扩展 store 全名core 对其无编译期依赖,反射加载)。 */
private static final String REDIS_STORE_CLASS =
"io.agentscope.extensions.redis.state.RedisAgentStateStore";
/** MySQL 扩展 store 全名core 对其无编译期依赖,反射加载)。 */
private static final String MYSQL_STORE_CLASS =
"io.agentscope.extensions.mysql.state.MysqlAgentStateStore";
private AgentStateStoreResolver() {
}
/**
* {@code cfg.session.memory.mode} v2 {@link AgentStateStore}
*
* @param cfg agent 配置 {@code workspace.root}{@code session.memory.*}
* @return 对应的 state store{@link MemoryStorageMode#NONE} 返回 {@code null}
* 调用方 {@code builder.stateStore(null)} NONE 语义
* @throws AgentConfigException REDIS/MYSQL 模式下扩展 jar/bean 缺失或类型不匹配
*/
public static AgentStateStore resolve(AgentConfig cfg) {
if (cfg == null) {
// 防御无配置时按 NONE 语义不持久化处理 1.0 默认行为一致
return null;
}
MemoryStorageMode mode = cfg.getSession().getMemory().getMode();
if (mode == null) {
return null;
}
switch (mode) {
case NONE:
return null;
case JVM:
return new InMemoryAgentStateStore();
case LOCAL_FILE:
return newLocalFileStore(cfg);
case REDIS:
return newRedisStore(cfg);
case MYSQL:
return newMysqlStore(cfg);
default:
// 未来新增 mode OSS在此显式报错而非静默返回 null
throw new AgentConfigException(
"Unsupported MemoryStorageMode: " + mode
+ " (AgentStateStoreResolver 仅支持 NONE/JVM/LOCAL_FILE/REDIS/MYSQL)");
}
}
/* ----- LOCAL_FILE ----- */
private static AgentStateStore newLocalFileStore(AgentConfig cfg) {
String root = cfg.getWorkspace().getRoot();
if (root == null || root.trim().isEmpty()) {
throw new AgentConfigException(
"liteflow.agent.workspace.root is required when memory.mode=LOCAL_FILE");
}
Path dir = Paths.get(root).resolve(LocalFileMemoryConfig.SUB_DIR);
return new JsonFileAgentStateStore(dir);
}
/* ----- REDIS反射构造core 不硬依赖 Redisson/Jedis/Lettuce/扩展 jar----- */
private static AgentStateStore newRedisStore(AgentConfig cfg) {
RedisMemoryConfig rc = cfg.getSession().getMemory().getRedis();
if (rc == null) {
throw new AgentConfigException(
"liteflow.agent.session.memory.redis is required when mode=REDIS");
}
String beanName = rc.getBeanName();
if (beanName == null || beanName.trim().isEmpty()) {
throw new AgentConfigException(
"liteflow.agent.session.memory.redis.beanName is required when mode=REDIS");
}
Object client = ContextAwareHolder.loadContextAware().getBean(beanName);
if (client == null) {
throw new AgentConfigException("Redis client bean not found: " + beanName);
}
RedisMemoryConfig.RedisClientType clientType = rc.getClientType();
String builderMethod;
String clientFqn;
if (clientType == null) {
clientType = RedisMemoryConfig.RedisClientType.REDISSON;
}
switch (clientType) {
case REDISSON:
builderMethod = "redissonClient";
clientFqn = "org.redisson.api.RedissonClient";
break;
case JEDIS:
builderMethod = "jedisClient";
clientFqn = "redis.clients.jedis.UnifiedJedis";
break;
case LETTUCE:
builderMethod = "lettuceClient";
clientFqn = "io.lettuce.core.RedisClient";
break;
default:
throw new AgentConfigException("Unsupported redis client type: " + clientType);
}
try {
Class<?> storeClass = Class.forName(REDIS_STORE_CLASS);
Object builder = storeClass.getMethod("builder").invoke(null);
Class<?> clientTypeClass = Class.forName(clientFqn);
if (!clientTypeClass.isInstance(client)) {
throw new AgentConfigException("Bean '" + beanName + "' is not a "
+ clientFqn + "; got " + client.getClass().getName());
}
Method setter = builder.getClass().getMethod(builderMethod, clientTypeClass);
setter.invoke(builder, client);
if (rc.getKeyPrefix() != null && !rc.getKeyPrefix().isEmpty()) {
builder.getClass().getMethod("keyPrefix", String.class).invoke(builder, rc.getKeyPrefix());
}
return (AgentStateStore) builder.getClass().getMethod("build").invoke(builder);
} catch (AgentConfigException e) {
throw e;
} catch (ClassNotFoundException e) {
throw new AgentConfigException(
"Class not found while building RedisAgentStateStore: " + e.getMessage()
+ ". Add agentscope-extensions-redis (+ Redisson/Jedis/Lettuce driver) to the classpath.", e);
} catch (Exception e) {
throw new AgentConfigException("Failed to build RedisAgentStateStore", e);
}
}
/* ----- MYSQL反射构造core 不硬依赖扩展 jar----- */
private static AgentStateStore newMysqlStore(AgentConfig cfg) {
MysqlMemoryConfig mc = cfg.getSession().getMemory().getMysql();
if (mc == null) {
throw new AgentConfigException(
"liteflow.agent.session.memory.mysql is required when mode=MYSQL");
}
String dsBeanName = mc.getDataSourceBeanName();
if (dsBeanName == null || dsBeanName.trim().isEmpty()) {
throw new AgentConfigException(
"liteflow.agent.session.memory.mysql.dataSourceBeanName is required when mode=MYSQL");
}
Object ds = ContextAwareHolder.loadContextAware().getBean(dsBeanName);
if (ds == null) {
throw new AgentConfigException("DataSource bean not found: " + dsBeanName);
}
if (!(ds instanceof DataSource)) {
throw new AgentConfigException("Bean '" + dsBeanName + "' is not a javax.sql.DataSource; got "
+ ds.getClass().getName());
}
DataSource dataSource = (DataSource) ds;
boolean createIfNotExist = mc.isCreateIfNotExist();
String db = mc.getDatabaseName();
String table = mc.getTableName();
try {
Class<?> storeClass = Class.forName(MYSQL_STORE_CLASS);
// 三个 public 构造器 findings R1-ext
// 任一自定义库名/表名非空 -> 走四参重载留空那个传 nullstore 内部回退默认
if ((db != null && !db.isEmpty()) || (table != null && !table.isEmpty())) {
return (AgentStateStore) storeClass
.getConstructor(DataSource.class, String.class, String.class, boolean.class)
.newInstance(dataSource, nullIfEmpty(db), nullIfEmpty(table), createIfNotExist);
}
return (AgentStateStore) storeClass
.getConstructor(DataSource.class, boolean.class)
.newInstance(dataSource, createIfNotExist);
} catch (ClassNotFoundException e) {
throw new AgentConfigException(
"Class not found while building MysqlAgentStateStore: " + e.getMessage()
+ ". Add agentscope-extensions-mysql to the classpath.", e);
} catch (Exception e) {
throw new AgentConfigException("Failed to build MysqlAgentStateStore", e);
}
}
private static String nullIfEmpty(String s) {
return (s == null || s.isEmpty()) ? null : s;
}
}

View File

@ -0,0 +1,55 @@
package com.yomahub.liteflow.test.agent.v2;
import com.yomahub.liteflow.agent.exception.AgentConfigException;
import com.yomahub.liteflow.agent.state.AgentStateStoreResolver;
import com.yomahub.liteflow.property.agent.AgentConfig;
import com.yomahub.liteflow.property.agent.MemoryStorageMode;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Task 2.1 REDIS/MYSQL 配置校验测试
*
* <p>这里覆盖的是 <b>配置缺失/bean 缺失的快速失败路径</b>不需要真实的 Redis/MySQL
* 驱动或扩展 jar纯单元测试反射构造的 happy-pathbean 存在 + 扩展 jar
* classpath属于集成测试范畴需要把 {@code agentscope-extensions-redis/mysql}
* 作为 test 依赖显式引入留待后续 Task 3.2 react-agent 集成测试里覆盖
*
* <p>Resolver 的反射构造签名见 findings R1-ext
* <ul>
* <li>Redis{@code RedisAgentStateStore.builder().redissonClient|jedisClient|lettuceClient(...).keyPrefix(...).build()}</li>
* <li>MySQL{@code new MysqlAgentStateStore(DataSource[, createIfNotExist])}
* {@code new MysqlAgentStateStore(DataSource, db, table, createIfNotExist)}</li>
* </ul>
*/
class AgentStateStoreResolverExtTest {
private AgentConfig cfg(MemoryStorageMode mode) {
AgentConfig c = new AgentConfig();
c.getWorkspace().setRoot("/tmp/unused");
c.getSession().getMemory().setMode(mode);
return c;
}
@Test
void redis_missingBeanName_throwsConfigException() {
// beanName 未配 -> 启动期配置校验直接报错不会走到反射
AgentConfig c = cfg(MemoryStorageMode.REDIS);
// redis.beanName 默认 null
AgentConfigException ex = assertThrows(AgentConfigException.class,
() -> AgentStateStoreResolver.resolve(c));
assertTrue(ex.getMessage().contains("beanName"),
"missing beanName must be reported; got: " + ex.getMessage());
}
@Test
void mysql_missingDataSourceBeanName_throwsConfigException() {
AgentConfig c = cfg(MemoryStorageMode.MYSQL);
AgentConfigException ex = assertThrows(AgentConfigException.class,
() -> AgentStateStoreResolver.resolve(c));
assertTrue(ex.getMessage().contains("dataSourceBeanName"),
"missing dataSourceBeanName must be reported; got: " + ex.getMessage());
}
}

View File

@ -0,0 +1,57 @@
package com.yomahub.liteflow.test.agent.v2;
import com.yomahub.liteflow.agent.state.AgentStateStoreResolver;
import com.yomahub.liteflow.property.agent.AgentConfig;
import com.yomahub.liteflow.property.agent.MemoryStorageMode;
import io.agentscope.core.state.AgentStateStore;
import io.agentscope.core.state.InMemoryAgentStateStore;
import io.agentscope.core.state.JsonFileAgentStateStore;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Task 2.1 单元测试验证 {@link AgentStateStoreResolver}
* {@link MemoryStorageMode} 正确映射到 v2 {@link AgentStateStore}
*
* <p>仅覆盖 NONE/JVM/LOCAL_FILE 三档这三档的签名已在 findings R1 实测确认
* 无外部依赖纯单元测试不启动 SpringREDIS/MYSQL mock-bean 覆盖见
* {@code AgentStateStoreResolverRedisTest}/{@code ...MysqlTest}可选依赖扩展 jar classpath
*/
class AgentStateStoreResolverTest {
@TempDir
Path tmp;
private AgentConfig cfg(MemoryStorageMode mode) {
AgentConfig c = new AgentConfig();
c.getWorkspace().setRoot(tmp.toString());
c.getSession().getMemory().setMode(mode);
return c;
}
@Test
void none_returnsNull() {
// NONE 语义 = 不设 stateStoreresolver 返回 null调用方 builder.stateStore(null)
assertNull(AgentStateStoreResolver.resolve(cfg(MemoryStorageMode.NONE)));
}
@Test
void jvm_mapsTo_inMemory() {
AgentStateStore s = AgentStateStoreResolver.resolve(cfg(MemoryStorageMode.JVM));
assertNotNull(s);
assertTrue(s instanceof InMemoryAgentStateStore, s.getClass().getName());
}
@Test
void localFile_mapsTo_jsonFile() {
AgentStateStore s = AgentStateStoreResolver.resolve(cfg(MemoryStorageMode.LOCAL_FILE));
assertNotNull(s);
assertTrue(s instanceof JsonFileAgentStateStore, s.getClass().getName());
}
}