- reason=初始化默认评估套件配置");
}
/**
@@ -178,9 +176,7 @@ private EvaluationSuite createDefaultSuite() {
/**
* 创建默认的评估器注册表
*
- * Starter 层自动装配:
- * - LLM 评估器
- * - 经验检索评估器(如果 ExperienceProvider 可用)
+ *
Starter 层自动装配 LLM 评估器。
*/
private EvaluatorRegistry createDefaultEvaluatorRegistry() {
EvaluatorRegistry registry = new EvaluatorRegistry();
@@ -189,14 +185,15 @@ private EvaluatorRegistry createDefaultEvaluatorRegistry() {
LLMBasedEvaluator llmEvaluator = new LLMBasedEvaluator(chatModel, "llm-based");
registry.registerEvaluator(llmEvaluator);
-
- // 自动注册经验检索评估器(如果 ExperienceProvider 可用)
- if (experienceProvider != null && properties.getExperience().isEnabled()) {
- int maxExperiences = properties.getExperience().getMaxExperiencesPerType();
- RuleBasedEvaluator expEvaluator = ExperienceRetrievalEvaluatorFactory.createExperienceEvaluator(
- experienceProvider, maxExperiences);
- registry.registerEvaluator(expEvaluator);
- log.info("DefaultEvaluationSuiteConfig#createDefaultEvaluatorRegistry - reason=注册经验检索评估器");
+ if (customEvaluators != null) {
+ for (Evaluator evaluator : customEvaluators) {
+ if (evaluator == null || "llm-based".equals(evaluator.getEvaluatorId())) {
+ continue;
+ }
+ registry.registerEvaluator(evaluator);
+ log.info("DefaultEvaluationSuiteConfig#createDefaultEvaluatorRegistry - reason=注册自定义评估器, evaluatorId={}",
+ evaluator.getEvaluatorId());
+ }
}
return registry;
@@ -216,4 +213,3 @@ public EvaluatorRegistry evaluatorRegistry() {
return createDefaultEvaluatorRegistry();
}
}
-
diff --git a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/tools/FlexibleStringListDeserializer.java b/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/tools/FlexibleStringListDeserializer.java
index dcf8cd5d..a0768ac8 100644
--- a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/tools/FlexibleStringListDeserializer.java
+++ b/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/tools/FlexibleStringListDeserializer.java
@@ -53,14 +53,8 @@ public List deserialize(JsonParser p, DeserializationContext ctxt) throw
if (token == JsonToken.START_ARRAY) {
List result = new ArrayList<>();
- while (p.nextToken() != JsonToken.END_ARRAY) {
- String item = p.getValueAsString();
- if (item != null) {
- item = item.trim();
- if (!item.isEmpty()) {
- result.add(item);
- }
- }
+ while ((token = p.nextToken()) != JsonToken.END_ARRAY) {
+ collectArrayValue(p, token, result);
}
return result;
}
@@ -88,6 +82,30 @@ public List deserialize(JsonParser p, DeserializationContext ctxt) throw
return new ArrayList<>();
}
+ private void collectArrayValue(JsonParser p, JsonToken token, List result) throws IOException {
+ if (token == JsonToken.START_ARRAY) {
+ while ((token = p.nextToken()) != JsonToken.END_ARRAY) {
+ collectArrayValue(p, token, result);
+ }
+ return;
+ }
+
+ if (token == JsonToken.START_OBJECT) {
+ p.skipChildren();
+ return;
+ }
+
+ String item = p.getValueAsString();
+ if (item == null) {
+ return;
+ }
+
+ item = item.trim();
+ if (!item.isEmpty()) {
+ result.add(item);
+ }
+ }
+
@Override
public List getNullValue(DeserializationContext ctxt) {
return new ArrayList<>();
diff --git a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/tools/WriteCodeTool.java b/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/tools/WriteCodeTool.java
index 62473101..019a5fba 100644
--- a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/tools/WriteCodeTool.java
+++ b/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/tools/WriteCodeTool.java
@@ -19,15 +19,13 @@
import com.alibaba.assistant.agent.core.context.SessionCodeManager;
import com.alibaba.assistant.agent.core.executor.RuntimeEnvironmentManager;
import com.alibaba.assistant.agent.core.model.GeneratedCode;
-import com.alibaba.assistant.agent.extension.experience.fastintent.CodeFastIntentSupport;
-import com.alibaba.assistant.agent.extension.experience.model.Experience;
-import com.alibaba.assistant.agent.extension.experience.model.FastIntentConfig;
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.agent.tools.ToolContextConstants;
import com.alibaba.cloud.ai.graph.store.Store;
import com.alibaba.cloud.ai.graph.store.StoreItem;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,7 +37,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
import java.util.function.BiFunction;
/**
@@ -68,18 +65,10 @@ public class WriteCodeTool implements BiFunction skip code validation
- String fastIntentResult = tryFastIntent(request, toolContext);
- if (fastIntentResult != null) {
- return fastIntentResult;
- }
-
// 1. 验证必填参数
if (request.functionName == null || request.functionName.trim().isEmpty()) {
return "Error: functionName is required";
@@ -124,45 +107,6 @@ public String apply(Request request, ToolContext toolContext) {
}
}
- @SuppressWarnings("unchecked")
- private String tryFastIntent(Request request, ToolContext toolContext) {
- try {
- if (codeFastIntentSupport == null || toolContext == null) {
- return null;
- }
-
- String language = (codeContext != null && codeContext.getLanguage() != null) ? codeContext.getLanguage().name() : null;
- Map toolReq = CodeFastIntentSupport.toolReqOf(request.description, request.functionName, request.parameters);
- Optional hitOpt = codeFastIntentSupport.tryHit(toolContext, toolReq, language);
- if (hitOpt.isEmpty()) {
- return null;
- }
-
- Experience best = hitOpt.get().experience();
- String code = hitOpt.get().code();
- try {
- registerCode(request, code, toolContext);
- } catch (Exception e) {
- String err = e.getMessage();
- logger.warn("WriteCodeTool#tryFastIntent - reason=fast-intent register failed, expId={}, error={}",
- best.getId(), err);
-
- FastIntentConfig.FastIntentFallback fb = CodeFastIntentSupport.getOnRegisterFallback(best);
- if (fb == FastIntentConfig.FastIntentFallback.FAIL_FAST) {
- return "Error: FastIntent(Code) register failed: " + err;
- }
- return null;
- }
-
- logger.info("WriteCodeTool#tryFastIntent - reason=fast-intent HIT (skip codegen), expId={}", best.getId());
- return "FastIntent(Code) hit: " + best.getTitle() + "\n```python\n" + code + "\n```";
-
- } catch (Exception e) {
- logger.warn("WriteCodeTool#tryFastIntent - reason=fast-intent failed, fallback to normal flow, error={}", e.getMessage());
- return null;
- }
- }
-
/**
* 清理代码(移除 markdown 代码块标记)
*/
@@ -291,7 +235,8 @@ private void saveToStore(ToolContext toolContext, GeneratedCode code) {
必需参数:
- functionName:函数名称(snake_case 格式,例如 'calculate_sum')
- description:函数功能的简要描述
- - parameters:函数接受的参数名列表
+ - parameters:函数参数名的字符串数组。只写参数名,不要包含类型或描述。无参数时传空数组 []。
+ 正确示例:["a", "b"]、["query"]、[]
- code:完整的 Python 函数代码,包含 'def' 语句和完整实现
代码编写规范:
@@ -323,20 +268,22 @@ private void saveToStore(ToolContext toolContext, GeneratedCode code) {
- 只使用当前会话中明确可用的工具
- 不确定可用工具时,编写返回结果的纯 Python 代码
- Agent 可以在代码执行后向用户展示结果
+ - ⚠️ 代码整洁要求:生成的代码禁止包含任何注释(# 注释、多行注释、步骤说明、TODO 等),只保留纯净的业务逻辑。函数定义处可保留一行简洁的 docstring
- 示例(纯计算):
+ 示例(有参数):
write_code(
functionName='calculate_sum',
description='计算两个数的和',
parameters=['a', 'b'],
- code='''def calculate_sum(a, b):
- \"\"\"计算两个数的和\"\"\"
- try:
- result = a + b
- return {"success": True, "sum": result, "message": f"{a} + {b} = {result}"}
- except Exception as e:
- return {"success": False, "error": str(e)}
- '''
+ code='def calculate_sum(a, b):\\n \"\"\"计算两个数的和\"\"\"\\n try:\\n result = a + b\\n return {"success": True, "sum": result}\\n except Exception as e:\\n return {"success": False, "error": str(e)}'
+ )
+
+ 示例(无参数):
+ write_code(
+ functionName='get_current_time',
+ description='获取当前时间',
+ parameters=[],
+ code='def get_current_time():\\n \"\"\"获取当前时间\"\"\"\\n import datetime\\n now = datetime.datetime.now()\\n return {"success": True, "time": str(now)}'
)
""";
@@ -345,10 +292,9 @@ private void saveToStore(ToolContext toolContext, GeneratedCode code) {
*/
public static ToolCallback createWriteCodeToolCallback(
CodeContext codeContext,
- RuntimeEnvironmentManager environmentManager,
- CodeFastIntentSupport codeFastIntentSupport) {
+ RuntimeEnvironmentManager environmentManager) {
- WriteCodeTool tool = new WriteCodeTool(codeContext, environmentManager, codeFastIntentSupport);
+ WriteCodeTool tool = new WriteCodeTool(codeContext, environmentManager);
return FunctionToolCallback.builder("write_code", tool)
.description(WRITE_CODE_DESCRIPTION)
@@ -356,12 +302,6 @@ public static ToolCallback createWriteCodeToolCallback(
.build();
}
- public static ToolCallback createWriteCodeToolCallback(
- CodeContext codeContext,
- RuntimeEnvironmentManager environmentManager) {
- return createWriteCodeToolCallback(codeContext, environmentManager, null);
- }
-
/**
* Request for writing code
*/
@@ -392,7 +332,5 @@ public Request(String functionName, String description, List parameters,
this.parameters = parameters;
this.code = code;
}
-
}
}
-
diff --git a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/tools/WriteConditionCodeTool.java b/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/tools/WriteConditionCodeTool.java
index be6700a1..70cc7c50 100644
--- a/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/tools/WriteConditionCodeTool.java
+++ b/assistant-agent-autoconfigure/src/main/java/com/alibaba/assistant/agent/autoconfigure/tools/WriteConditionCodeTool.java
@@ -19,9 +19,6 @@
import com.alibaba.assistant.agent.core.context.SessionCodeManager;
import com.alibaba.assistant.agent.core.executor.RuntimeEnvironmentManager;
import com.alibaba.assistant.agent.core.model.GeneratedCode;
-import com.alibaba.assistant.agent.extension.experience.fastintent.CodeFastIntentSupport;
-import com.alibaba.assistant.agent.extension.experience.model.Experience;
-import com.alibaba.assistant.agent.extension.experience.model.FastIntentConfig;
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.agent.tools.ToolContextConstants;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -35,8 +32,6 @@
import java.util.ArrayList;
import java.util.List;
-import java.util.Map;
-import java.util.Optional;
import java.util.function.BiFunction;
/**
@@ -65,18 +60,10 @@ public class WriteConditionCodeTool implements BiFunction skip validation
- String fastIntentResult = tryFastIntent(request, toolContext);
- if (fastIntentResult != null) {
- return fastIntentResult;
- }
-
// 1. 验证必填参数
if (request.functionName == null || request.functionName.trim().isEmpty()) {
return "Error: functionName is required";
@@ -121,50 +102,6 @@ public String apply(Request request, ToolContext toolContext) {
}
}
- @SuppressWarnings("unchecked")
- private String tryFastIntent(Request request, ToolContext toolContext) {
- try {
- if (codeFastIntentSupport == null || toolContext == null) {
- return null;
- }
-
- String language = (codeContext != null && codeContext.getLanguage() != null) ? codeContext.getLanguage().name() : null;
- Map toolReq = CodeFastIntentSupport.toolReqOf(request.description, request.functionName, request.parameters);
- Optional hitOpt = codeFastIntentSupport.tryHit(toolContext, toolReq, language);
- if (hitOpt.isEmpty()) {
- return null;
- }
-
- Experience best = hitOpt.get().experience();
- String code = hitOpt.get().code();
- if (code == null) {
- return null;
- }
-
- try {
- registerCode(request, code, toolContext);
- } catch (Exception e) {
- String err = e.getMessage();
- logger.warn("WriteConditionCodeTool#tryFastIntent - reason=fast-intent register failed, expId={}, error={}",
- best != null ? best.getId() : "unknown", err);
-
- FastIntentConfig.FastIntentFallback fb = CodeFastIntentSupport.getOnRegisterFallback(best);
- if (fb == FastIntentConfig.FastIntentFallback.FAIL_FAST) {
- return "Error: FastIntent(Code) register failed: " + err;
- }
- return null;
- }
-
- logger.info("WriteConditionCodeTool#tryFastIntent - reason=fast-intent HIT (skip codegen), expId={}",
- best != null ? best.getId() : "unknown");
- return "FastIntent(Code) hit: " + (best != null ? best.getTitle() : "matched") + "\n```python\n" + code + "\n```";
-
- } catch (Exception e) {
- logger.warn("WriteConditionCodeTool#tryFastIntent - reason=fast-intent failed, fallback to normal flow, error={}", e.getMessage());
- return null;
- }
- }
-
/**
* 清理代码(移除 markdown 代码块标记)
*/
@@ -314,19 +251,6 @@ public static ToolCallback createWriteConditionCodeToolCallback(
.build();
}
- public static ToolCallback createWriteConditionCodeToolCallback(
- CodeContext codeContext,
- RuntimeEnvironmentManager environmentManager,
- CodeFastIntentSupport codeFastIntentSupport) {
-
- WriteConditionCodeTool tool = new WriteConditionCodeTool(codeContext, environmentManager, codeFastIntentSupport);
-
- return FunctionToolCallback.builder("write_condition_code", tool)
- .description(WRITE_CONDITION_CODE_DESCRIPTION)
- .inputType(Request.class)
- .build();
- }
-
/**
* Request for writing condition code
*/
diff --git a/assistant-agent-autoconfigure/src/test/java/com/alibaba/assistant/agent/autoconfigure/tools/WriteCodeToolRequestDeserializationTest.java b/assistant-agent-autoconfigure/src/test/java/com/alibaba/assistant/agent/autoconfigure/tools/WriteCodeToolRequestDeserializationTest.java
new file mode 100644
index 00000000..2c5b85d7
--- /dev/null
+++ b/assistant-agent-autoconfigure/src/test/java/com/alibaba/assistant/agent/autoconfigure/tools/WriteCodeToolRequestDeserializationTest.java
@@ -0,0 +1,58 @@
+package com.alibaba.assistant.agent.autoconfigure.tools;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class WriteCodeToolRequestDeserializationTest {
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ @Test
+ void shouldDeserializeEmptyParametersArray() throws Exception {
+ WriteCodeTool.Request request = objectMapper.readValue("""
+ {
+ "functionName": "get_current_time",
+ "description": "获取当前时间",
+ "parameters": [],
+ "code": "def get_current_time():\\n return {\\"success\\": True}"
+ }
+ """, WriteCodeTool.Request.class);
+
+ assertNotNull(request.parameters);
+ assertEquals(List.of(), request.parameters);
+ }
+
+ @Test
+ void shouldTolerateNestedEmptyParametersArray() throws Exception {
+ WriteCodeTool.Request request = objectMapper.readValue("""
+ {
+ "functionName": "get_broadcast_subscription_apply_link",
+ "description": "获取申请链接",
+ "parameters": [[]],
+ "code": "def get_broadcast_subscription_apply_link():\\n return {\\"success\\": True}"
+ }
+ """, WriteCodeTool.Request.class);
+
+ assertNotNull(request.parameters);
+ assertEquals(List.of(), request.parameters);
+ }
+
+ @Test
+ void shouldFlattenNestedStringArrays() throws Exception {
+ WriteCodeTool.Request request = objectMapper.readValue("""
+ {
+ "functionName": "search_info",
+ "description": "搜索信息",
+ "parameters": [["query"], "limit"],
+ "code": "def search_info(query, limit):\\n return {\\"success\\": True}"
+ }
+ """, WriteCodeTool.Request.class);
+
+ assertEquals(List.of("query", "limit"), request.parameters);
+ }
+}
diff --git a/assistant-agent-common/pom.xml b/assistant-agent-common/pom.xml
index 37db5057..df2cc5c2 100644
--- a/assistant-agent-common/pom.xml
+++ b/assistant-agent-common/pom.xml
@@ -6,7 +6,7 @@
com.alibaba.agent.assistant
assistant-agent
- 0.2.4
+ 0.2.5
assistant-agent-common
diff --git a/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/constant/CodeactStateKeys.java b/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/constant/CodeactStateKeys.java
index 47a6c434..0064933c 100644
--- a/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/constant/CodeactStateKeys.java
+++ b/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/constant/CodeactStateKeys.java
@@ -48,24 +48,12 @@ private CodeactStateKeys() {
*/
public static final String CURRENT_EXECUTION = "current_execution";
- /**
- * Key for storing the current programming language
- * Type: Language
- */
- public static final String CURRENT_LANGUAGE = "current_language";
-
/**
* Key for storing user ID (for Store namespace)
* Type: String
*/
public static final String USER_ID = "user_id";
- /**
- * Key for storing whether initial code generation is complete
- * Type: Boolean
- */
- public static final String INITIAL_CODE_GEN_DONE = "initial_code_gen_done";
-
// ==================== 工具白名单配置 ====================
/**
@@ -81,29 +69,6 @@ private CodeactStateKeys() {
*/
public static final String AVAILABLE_TOOL_NAMES = "available_tool_names";
- /**
- * 可用工具组白名单
- *
- * 类型:List<String>
- *
示例:["search", "reply", "app_helper"]
- *
用途:按工具组筛选,组名对应 CodeactToolMetadata.targetClassName()
- *
为空或不存在时:不按组筛选
- */
- public static final String AVAILABLE_TOOL_GROUPS = "available_tool_groups";
-
- /**
- * 白名单模式
- *
- *
类型:String
- *
可选值:
- * - "INTERSECTION"(默认):名称白名单和组白名单取交集
- * - "UNION":名称白名单和组白名单取并集
- * - "NAME_ONLY":仅使用名称白名单
- * - "GROUP_ONLY":仅使用组白名单
- *
为空或不存在时:默认为 INTERSECTION
- */
- public static final String WHITELIST_MODE = "tool_whitelist_mode";
-
// ==================== 工具上下文(只读) ====================
/**
@@ -138,47 +103,58 @@ private CodeactStateKeys() {
public static final String CODEACT_TOOL_METADATA_LIST = "codeact_tool_metadata_list";
/**
- * 注入的全部 codeact 工具列表
+ * 编程语言
*
- *
类型:List<CodeactTool>
+ *
类型:String
+ *
示例:"python", "java"
*
由 CodeGeneratorSubAgent.init_context 节点注入
- *
上层应用可读取此列表进行评估
- *
- * @deprecated 由于 CodeactTool 包含不可序列化的组件,不应将其写入 State。
- * 请使用 {@link #CODEACT_TOOL_NAMES} 存储工具名称,并通过 CodeactToolRegistry 获取完整工具对象。
*/
- @Deprecated
- public static final String CODEACT_TOOLS = "codeact_tools";
+ public static final String LANGUAGE = "language";
+
+ // ==================== Experience progressive disclosure ====================
/**
- * 筛选后的 codeact 工具列表
+ * 首轮预取使用的 disclosure query。
*
- *
类型:List<CodeactTool>
- *
由 CodeGeneratorNode 筛选后写入(可选)
- *
用于调试和审计
+ *
类型:String
*/
- public static final String FILTERED_CODEACT_TOOLS = "filtered_codeact_tools";
+ public static final String EXPERIENCE_PREFETCH_QUERY = "experience_prefetch_query";
/**
- * 编程语言
+ * 首轮预取状态。
*
*
类型:String
- *
示例:"python", "java"
- *
由 CodeGeneratorSubAgent.init_context 节点注入
+ *
取值:NOT_RUN / SKIPPED / COMPLETED
*/
- public static final String LANGUAGE = "language";
+ public static final String EXPERIENCE_PREFETCH_STATUS = "experience_prefetch_status";
+
+ /**
+ * 首轮预取得到的 grouped experience candidate cards。
+ *
+ *
类型:GroupedExperienceCandidates
+ */
+ public static final String EXPERIENCE_PREFETCHED_CANDIDATES = "experience_prefetched_candidates";
+
+ /**
+ * {@code read_exp} 返回的 detail cache。
+ *
+ *
类型:Map<String, ReadExpResponse>
+ */
+ public static final String EXPERIENCE_DETAIL_CACHE = "experience_detail_cache";
- // ==================== Hook 通信 ====================
+ /**
+ * 已满足 DIRECT 披露条件、可直接注入 prompt 的经验内容。
+ *
+ *
类型:List<DirectExperienceGrounding>
+ */
+ public static final String EXPERIENCE_DIRECT_GROUNDINGS = "experience_direct_groundings";
/**
- * CodeGeneratorNode 执行时传入的 messages 列表(含 beforeModel Hook 注入的内容)
+ * 当前轮允许以 React 直调方式暴露的工具名集合。
*
- *
类型:List<org.springframework.ai.chat.messages.Message>
- *
由 CodeGeneratorNode 在调用模型后写入,供 afterModel Hook 读取与审计。
- *
写端:CodeGeneratorNode#apply(key = "codeact_node_messages")
- *
读端:afterModel Hook(如需访问本次代码生成的完整 messages 上下文)
+ *
类型:List<String>
*/
- public static final String CODEACT_NODE_MESSAGES = "codeact_node_messages";
+ public static final String EXPERIENCE_ALLOWED_REACT_TOOL_NAMES = "experience_allowed_react_tool_names";
// ==================== Session级别代码存储 ====================
@@ -192,4 +168,3 @@ private CodeactStateKeys() {
*/
public static final String SESSION_GENERATED_CODES = "session_generated_codes";
}
-
diff --git a/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/tools/definition/MapShapeNode.java b/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/tools/definition/MapShapeNode.java
new file mode 100644
index 00000000..366cc50b
--- /dev/null
+++ b/assistant-agent-common/src/main/java/com/alibaba/assistant/agent/common/tools/definition/MapShapeNode.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2024-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 com.alibaba.assistant.agent.common.tools.definition;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Map 类型节点 - 表示动态 key 的字典结构(键为运行时数据,值具有一致的 shape)。
+ *
+ *
区别于 {@link ObjectShapeNode}(固定 schema 字段),{@code MapShapeNode}
+ * 用于表示 {@code Map} 语义:key 在编译期未知(如应用名、ID 等),
+ * 但所有 value 具有相同的类型结构。
+ *
+ * @author Assistant Agent Team
+ * @since 1.0.0
+ */
+public class MapShapeNode extends ShapeNode {
+
+ private ShapeNode valueShape;
+
+ private final Set observedKeys;
+
+ public MapShapeNode() {
+ super();
+ this.valueShape = new UnknownShapeNode();
+ this.observedKeys = new LinkedHashSet<>();
+ }
+
+ public MapShapeNode(ShapeNode valueShape) {
+ super();
+ this.valueShape = valueShape != null ? valueShape : new UnknownShapeNode();
+ this.observedKeys = new LinkedHashSet<>();
+ }
+
+ public MapShapeNode(ShapeNode valueShape, boolean optional, String description) {
+ super(optional, description);
+ this.valueShape = valueShape != null ? valueShape : new UnknownShapeNode();
+ this.observedKeys = new LinkedHashSet<>();
+ }
+
+ public ShapeNode getValueShape() {
+ return valueShape;
+ }
+
+ public void setValueShape(ShapeNode valueShape) {
+ this.valueShape = valueShape != null ? valueShape : new UnknownShapeNode();
+ }
+
+ public List getObservedKeys() {
+ return Collections.unmodifiableList(new ArrayList<>(observedKeys));
+ }
+
+ public void addObservedKey(String key) {
+ if (key != null && !key.isBlank()) {
+ observedKeys.add(key);
+ }
+ }
+
+ public void addObservedKeys(Iterable keys) {
+ if (keys == null) {
+ return;
+ }
+ for (String key : keys) {
+ addObservedKey(key);
+ }
+ }
+
+ @Override
+ public String getPythonTypeHint() {
+ String valueHint = valueShape.getPythonTypeHint();
+ String hint = "Dict[str, " + valueHint + "]";
+ if (optional) {
+ return "Optional[" + hint + "]";
+ }
+ return hint;
+ }
+
+ @Override
+ public String getTypeName() {
+ return "map";
+ }
+
+ public boolean isMap() {
+ return true;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ MapShapeNode that = (MapShapeNode) o;
+ return Objects.equals(valueShape, that.valueShape);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(valueShape);
+ }
+
+ @Override
+ public String toString() {
+ return "MapShapeNode{valueShape=" + valueShape + ", observedKeys=" + observedKeys + ", optional=" + optional
+ + '}';
+ }
+
+}
diff --git a/assistant-agent-core/pom.xml b/assistant-agent-core/pom.xml
index 602e8f4b..12c89a11 100644
--- a/assistant-agent-core/pom.xml
+++ b/assistant-agent-core/pom.xml
@@ -6,7 +6,7 @@
com.alibaba.agent.assistant
assistant-agent
- 0.2.4
+ 0.2.5
assistant-agent-core
@@ -75,6 +75,11 @@
io.opentelemetry
opentelemetry-context
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
-
\ No newline at end of file
+
diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/tool/definition/ReturnSchemaMerger.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/tool/definition/ReturnSchemaMerger.java
index 8d833910..c3e224be 100644
--- a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/tool/definition/ReturnSchemaMerger.java
+++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/tool/definition/ReturnSchemaMerger.java
@@ -16,6 +16,7 @@
package com.alibaba.assistant.agent.core.tool.definition;
import com.alibaba.assistant.agent.common.tools.definition.ArrayShapeNode;
+import com.alibaba.assistant.agent.common.tools.definition.MapShapeNode;
import com.alibaba.assistant.agent.common.tools.definition.ObjectShapeNode;
import com.alibaba.assistant.agent.common.tools.definition.PrimitiveShapeNode;
import com.alibaba.assistant.agent.common.tools.definition.ReturnSchema;
@@ -141,6 +142,10 @@ private static ShapeNode mergeSameTypeShapes(ShapeNode existing, ShapeNode obser
return existing;
}
+ if (existing instanceof MapShapeNode existingMap && observed instanceof MapShapeNode observedMap) {
+ return mergeMapShapes(existingMap, observedMap);
+ }
+
if (existing instanceof ObjectShapeNode existingObj && observed instanceof ObjectShapeNode observedObj) {
return mergeObjectShapes(existingObj, observedObj);
}
@@ -156,6 +161,17 @@ private static ShapeNode mergeSameTypeShapes(ShapeNode existing, ShapeNode obser
return existing;
}
+ /**
+ * 合并两个 MapShapeNode:value shape 合并一次,observed keys 做并集。
+ */
+ private static MapShapeNode mergeMapShapes(MapShapeNode existing, MapShapeNode observed) {
+ ShapeNode mergedValue = mergeShapes(existing.getValueShape(), observed.getValueShape());
+ MapShapeNode result = new MapShapeNode(mergedValue);
+ result.addObservedKeys(existing.getObservedKeys());
+ result.addObservedKeys(observed.getObservedKeys());
+ return result;
+ }
+
/**
* 合并两个 ObjectShapeNode。
*/
@@ -287,4 +303,3 @@ private static boolean areEquivalent(ShapeNode a, ShapeNode b) {
}
}
-
diff --git a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/tool/definition/ShapeExtractor.java b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/tool/definition/ShapeExtractor.java
index 2e120170..0a415c07 100644
--- a/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/tool/definition/ShapeExtractor.java
+++ b/assistant-agent-core/src/main/java/com/alibaba/assistant/agent/core/tool/definition/ShapeExtractor.java
@@ -16,6 +16,7 @@
package com.alibaba.assistant.agent.core.tool.definition;
import com.alibaba.assistant.agent.common.tools.definition.ArrayShapeNode;
+import com.alibaba.assistant.agent.common.tools.definition.MapShapeNode;
import com.alibaba.assistant.agent.common.tools.definition.ObjectShapeNode;
import com.alibaba.assistant.agent.common.tools.definition.PrimitiveShapeNode;
import com.alibaba.assistant.agent.common.tools.definition.PrimitiveType;
@@ -26,7 +27,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.ArrayList;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
/**
@@ -130,25 +133,124 @@ private static ArrayShapeNode extractArrayShape(JsonNode arrayNode) {
}
/**
- * 从对象节点提取 ObjectShapeNode。
+ * 从对象节点提取 ShapeNode。
+ *
+ * 若 JSON object 的 key 包含非标识符字符(如连字符 {@code -}、点 {@code .}、空格等),
+ * 说明 key 是运行时动态数据(应用名、ID 等),整体应视为 {@link MapShapeNode};
+ * 否则视为固定 schema 字段,返回 {@link ObjectShapeNode}。
+ *
* @param objectNode 对象节点
- * @return ObjectShapeNode
+ * @return ObjectShapeNode 或 MapShapeNode
*/
- private static ObjectShapeNode extractObjectShape(JsonNode objectNode) {
+ private static ShapeNode extractObjectShape(JsonNode objectNode) {
+ if (objectNode.isEmpty()) {
+ return new ObjectShapeNode();
+ }
+
+ if (isMapLikeObject(objectNode)) {
+ return extractMapShape(objectNode);
+ }
+
ObjectShapeNode shapeNode = new ObjectShapeNode();
+ Iterator> fields = objectNode.fields();
+ while (fields.hasNext()) {
+ Map.Entry field = fields.next();
+ ShapeNode fieldShape = extractFromJsonNode(field.getValue());
+ shapeNode.putField(field.getKey(), fieldShape);
+ }
+ return shapeNode;
+ }
+ /**
+ * 从 map-like 对象节点提取 MapShapeNode,合并所有 value 的 shape。
+ */
+ private static MapShapeNode extractMapShape(JsonNode objectNode) {
+ MapShapeNode mapShape = new MapShapeNode();
+ ShapeNode valueShape = null;
Iterator> fields = objectNode.fields();
while (fields.hasNext()) {
Map.Entry field = fields.next();
- String fieldName = field.getKey();
- JsonNode fieldValue = field.getValue();
+ mapShape.addObservedKey(field.getKey());
+ ShapeNode current = extractFromJsonNode(field.getValue());
+ valueShape = (valueShape == null) ? current : ReturnSchemaMerger.mergeShapes(valueShape, current);
+ }
+ mapShape.setValueShape(valueShape != null ? valueShape : new UnknownShapeNode());
+ return mapShape;
+ }
- ShapeNode fieldShape = extractFromJsonNode(fieldValue);
- shapeNode.putField(fieldName, fieldShape);
+ /**
+ * 判断 JSON object 是否为 map-like(key 为动态数据而非固定 schema 字段)。
+ *
+ * 判断依据:
+ *
+ * 若 key 含有非标识符字符,则认为 key 本身就是动态数据
+ * 否则若多个字段的 value shape 一致/可合并,则也视为 Map;
+ * 此时 key 只是数据实例,schema 只需保留一份 value 结构
+ *
+ */
+ private static boolean isMapLikeObject(JsonNode objectNode) {
+ List> entries = new ArrayList<>();
+ Iterator> fields = objectNode.fields();
+ while (fields.hasNext()) {
+ Map.Entry entry = fields.next();
+ entries.add(entry);
+ if (!isIdentifierKey(entry.getKey())) {
+ return true;
+ }
}
+ return hasHomogeneousValueShapes(entries);
+ }
- return shapeNode;
+ private static boolean hasHomogeneousValueShapes(List> entries) {
+ if (entries.size() < 2) {
+ return false;
+ }
+ ShapeNode merged = null;
+ for (Map.Entry entry : entries) {
+ ShapeNode current = extractFromJsonNode(entry.getValue());
+ if (merged == null) {
+ merged = current;
+ continue;
+ }
+ if (!isCompatibleMapValueShape(merged, current)) {
+ return false;
+ }
+ merged = ReturnSchemaMerger.mergeShapes(merged, current);
+ }
+ return true;
}
-}
+ private static boolean isCompatibleMapValueShape(ShapeNode left, ShapeNode right) {
+ if (left == null || right == null) {
+ return false;
+ }
+ if (left.isUnknown() || right.isUnknown()) {
+ return true;
+ }
+ if (left instanceof PrimitiveShapeNode leftPrimitive && right instanceof PrimitiveShapeNode rightPrimitive) {
+ return leftPrimitive.getType() == rightPrimitive.getType();
+ }
+ return left.getClass().equals(right.getClass());
+ }
+ /**
+ * 判断字符串是否是合法的标识符格式(仅含字母、数字、下划线、$,且不以数字开头)。
+ */
+ private static boolean isIdentifierKey(String key) {
+ if (key == null || key.isEmpty()) {
+ return false;
+ }
+ char first = key.charAt(0);
+ if (!Character.isLetter(first) && first != '_' && first != '$') {
+ return false;
+ }
+ for (int i = 1; i < key.length(); i++) {
+ char c = key.charAt(i);
+ if (!Character.isLetterOrDigit(c) && c != '_' && c != '$') {
+ return false;
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/assistant-agent-core/src/test/java/com/alibaba/assistant/agent/core/tool/definition/ShapeExtractorTest.java b/assistant-agent-core/src/test/java/com/alibaba/assistant/agent/core/tool/definition/ShapeExtractorTest.java
new file mode 100644
index 00000000..548f2526
--- /dev/null
+++ b/assistant-agent-core/src/test/java/com/alibaba/assistant/agent/core/tool/definition/ShapeExtractorTest.java
@@ -0,0 +1,106 @@
+package com.alibaba.assistant.agent.core.tool.definition;
+
+import com.alibaba.assistant.agent.common.tools.definition.MapShapeNode;
+import com.alibaba.assistant.agent.common.tools.definition.ObjectShapeNode;
+import com.alibaba.assistant.agent.common.tools.definition.PrimitiveShapeNode;
+import com.alibaba.assistant.agent.common.tools.definition.PrimitiveType;
+import com.alibaba.assistant.agent.common.tools.definition.ShapeNode;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertIterableEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ShapeExtractorTest {
+
+ @Test
+ void shouldExtractMapShapeNodeForDynamicKeys() {
+ ShapeNode shape = ShapeExtractor.extract("""
+ {
+ "app-center": {
+ "owner": "alice",
+ "memberCount": 3
+ },
+ "meow-agent": {
+ "owner": "bob"
+ }
+ }
+ """);
+
+ MapShapeNode mapShape = assertInstanceOf(MapShapeNode.class, shape);
+ ObjectShapeNode valueShape = assertInstanceOf(ObjectShapeNode.class, mapShape.getValueShape());
+ assertIterableEquals(java.util.List.of("app-center", "meow-agent"), mapShape.getObservedKeys());
+
+ PrimitiveShapeNode ownerShape = assertInstanceOf(PrimitiveShapeNode.class, valueShape.getField("owner"));
+ assertEquals(PrimitiveType.STRING, ownerShape.getType());
+
+ PrimitiveShapeNode memberCountShape =
+ assertInstanceOf(PrimitiveShapeNode.class, valueShape.getField("memberCount"));
+ assertEquals(PrimitiveType.INTEGER, memberCountShape.getType());
+ assertTrue(memberCountShape.isOptional());
+ }
+
+ @Test
+ void shouldExtractMapShapeNodeForIndexedSiblingKeys() {
+ ShapeNode shape = ShapeExtractor.extract("""
+ {
+ "tag1": {
+ "id": 1,
+ "name": "tag1",
+ "value": "value1"
+ },
+ "tag2": {
+ "id": 2,
+ "name": "tag2",
+ "value": "value2"
+ }
+ }
+ """);
+
+ MapShapeNode mapShape = assertInstanceOf(MapShapeNode.class, shape);
+ ObjectShapeNode valueShape = assertInstanceOf(ObjectShapeNode.class, mapShape.getValueShape());
+ assertIterableEquals(java.util.List.of("tag1", "tag2"), mapShape.getObservedKeys());
+ assertTrue(valueShape.hasField("id"));
+ assertTrue(valueShape.hasField("name"));
+ assertTrue(valueShape.hasField("value"));
+ }
+
+ @Test
+ void shouldMergeMapValueShapesWithoutExpandingDynamicKeys() {
+ MapShapeNode first = new MapShapeNode(new ObjectShapeNode());
+ first.addObservedKey("tag1");
+ ((ObjectShapeNode) first.getValueShape()).putField("owner", new PrimitiveShapeNode(PrimitiveType.STRING));
+
+ MapShapeNode second = new MapShapeNode(new ObjectShapeNode());
+ second.addObservedKey("tag2");
+ ((ObjectShapeNode) second.getValueShape()).putField("repoCount", new PrimitiveShapeNode(PrimitiveType.INTEGER));
+
+ ShapeNode merged = ReturnSchemaMerger.mergeShapes(first, second);
+
+ MapShapeNode mergedMap = assertInstanceOf(MapShapeNode.class, merged);
+ ObjectShapeNode mergedValue = assertInstanceOf(ObjectShapeNode.class, mergedMap.getValueShape());
+ assertIterableEquals(java.util.List.of("tag1", "tag2"), mergedMap.getObservedKeys());
+ assertEquals(2, mergedValue.getFieldCount());
+ assertTrue(mergedValue.hasField("owner"));
+ assertTrue(mergedValue.hasField("repoCount"));
+ assertTrue(mergedValue.getField("owner").isOptional());
+ assertTrue(mergedValue.getField("repoCount").isOptional());
+ }
+
+ @Test
+ void shouldKeepRegularObjectWhenValueShapesAreDifferent() {
+ ShapeNode shape = ShapeExtractor.extract("""
+ {
+ "owner": "alice",
+ "repoCount": 3,
+ "enabled": true
+ }
+ """);
+
+ ObjectShapeNode objectShape = assertInstanceOf(ObjectShapeNode.class, shape);
+ assertTrue(objectShape.hasField("owner"));
+ assertTrue(objectShape.hasField("repoCount"));
+ assertTrue(objectShape.hasField("enabled"));
+ }
+}
diff --git a/assistant-agent-evaluation/pom.xml b/assistant-agent-evaluation/pom.xml
index 76a65179..69d67670 100644
--- a/assistant-agent-evaluation/pom.xml
+++ b/assistant-agent-evaluation/pom.xml
@@ -6,7 +6,7 @@
com.alibaba.agent.assistant
assistant-agent
- 0.2.4
+ 0.2.5
assistant-agent-evaluation
diff --git a/assistant-agent-extensions/pom.xml b/assistant-agent-extensions/pom.xml
index 145f3a05..f288a2b2 100644
--- a/assistant-agent-extensions/pom.xml
+++ b/assistant-agent-extensions/pom.xml
@@ -6,7 +6,7 @@
com.alibaba.agent.assistant
assistant-agent
- 0.2.4
+ 0.2.5
assistant-agent-extensions
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/dynamic/tool/AbstractDynamicCodeactTool.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/dynamic/tool/AbstractDynamicCodeactTool.java
index 6755abf7..05c6f1bf 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/dynamic/tool/AbstractDynamicCodeactTool.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/dynamic/tool/AbstractDynamicCodeactTool.java
@@ -83,13 +83,9 @@ protected AbstractDynamicCodeactTool(ObjectMapper objectMapper, ToolDefinition t
private CodeactToolDefinition buildCodeactDefinition(ToolDefinition toolDef) {
String inputSchema = toolDef.inputSchema();
ParameterTree parameterTree = parseParameterTree(inputSchema);
- String desc = toolDef.description();
- if (desc.contains("\nReturn schema: \n")) {
- desc = desc.substring(0, desc.indexOf("\nReturn schema: \n"));
- }
return DefaultCodeactToolDefinition.builder()
.name(toolDef.name())
- .description(desc)
+ .description(toolDef.description())
.inputSchema(inputSchema)
.parameterTree(parameterTree)
.returnDescription("工具执行结果")
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/evaluation/config/CodeactEvaluationContextFactory.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/evaluation/config/CodeactEvaluationContextFactory.java
index 36ad0a5d..05cf2105 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/evaluation/config/CodeactEvaluationContextFactory.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/evaluation/config/CodeactEvaluationContextFactory.java
@@ -55,7 +55,7 @@ public EvaluationContext createInputRoutingContext(OverAllState state, RunnableC
Map executionResult = new HashMap<>();
// 提取用户输入 - 从 state 的 messages key 中获取
- // 注意:由于其他 Hook(如 ReactExperienceAgentHook)可能已经向 messages 中注入了
+ // 注意:由于其他 Hook(如 ExperiencePrefetchHook)可能已经向 messages 中注入了
// AssistantMessage 和 ToolResponseMessage,这里需要遍历查找最后一条 UserMessage
Optional> messagesOpt = state.value("messages");
if (messagesOpt.isPresent()) {
@@ -265,7 +265,7 @@ public EvaluationContext createSessionSummaryContext(
/**
* 从消息列表中查找最后一条 UserMessage 的内容
*
- * 由于其他 Hook(如 ReactExperienceAgentHook)可能在 beforeAgent 阶段向 messages 中
+ *
由于其他 Hook(如 ExperiencePrefetchHook)可能在 beforeAgent 阶段向 messages 中
* 注入 AssistantMessage 和 ToolResponseMessage,直接取最后一条消息可能不是用户输入。
* 此方法从后往前遍历,找到最后一条 UserMessage。
*
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/evaluation/experience/ExperienceRetrievalEvaluatorFactory.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/evaluation/experience/ExperienceRetrievalEvaluatorFactory.java
index de3aff16..1edecd75 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/evaluation/experience/ExperienceRetrievalEvaluatorFactory.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/evaluation/experience/ExperienceRetrievalEvaluatorFactory.java
@@ -284,10 +284,6 @@ private static String formatSingleExperience(Experience exp) {
StringBuilder sb = new StringBuilder();
sb.append(String.format("【%s】(%s)\n", exp.getTitle(), exp.getType()));
- if (StringUtils.hasText(exp.getLanguage())) {
- sb.append(String.format("语言: %s\n", exp.getLanguage()));
- }
-
Set tags = exp.getTags();
if (tags != null && !tags.isEmpty()) {
sb.append(String.format("标签: %s\n", String.join(", ", tags)));
@@ -310,10 +306,6 @@ private static String formatExperiences(List experiences, String pha
sb.append(String.format("=== 经验 %d: %s ===\n", i + 1, exp.getTitle()));
sb.append(String.format("类型: %s\n", exp.getType()));
- if (StringUtils.hasText(exp.getLanguage())) {
- sb.append(String.format("语言: %s\n", exp.getLanguage()));
- }
-
Set tags = exp.getTags();
if (tags != null && !tags.isEmpty()) {
sb.append(String.format("标签: %s\n", String.join(", ", tags)));
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/ExperienceDataInitializer.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/ExperienceDataInitializer.java
index 4a25210a..ec6a543d 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/ExperienceDataInitializer.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/ExperienceDataInitializer.java
@@ -2,7 +2,6 @@
import com.alibaba.assistant.agent.extension.experience.model.Experience;
import com.alibaba.assistant.agent.extension.experience.model.ExperienceArtifact;
-import com.alibaba.assistant.agent.extension.experience.model.ExperienceScope;
import com.alibaba.assistant.agent.extension.experience.model.ExperienceType;
import com.alibaba.assistant.agent.extension.experience.model.FastIntentConfig;
import com.alibaba.assistant.agent.extension.experience.model.*;
@@ -39,20 +38,19 @@ public void run(String... args) throws Exception {
}
private void initializeDemoData() {
- // 初始化代码经验
- initializeCodeExperiences();
-
- // 初始化React经验
+ // 初始化React经验(包含原CODE经验迁移后的数据)
initializeReactExperiences();
// 初始化常识经验
initializeCommonExperiences();
}
- private void initializeCodeExperiences() {
- // Java日志规范经验
+ private void initializeReactExperiences() {
+ // === 原 CODE 经验迁移为 REACT 类型 ===
+
+ // Java日志规范经验(原CODE类型,迁移为REACT)
Experience javaLoggingExp = new Experience(
- ExperienceType.CODE,
+ ExperienceType.REACT,
"Java日志格式规范",
"按照要求,日志格式应该为:类名 + 目标方法 + 打印原因\n\n" +
"```java\n" +
@@ -61,17 +59,16 @@ private void initializeCodeExperiences() {
" log.info(\"ClassName#methodName - reason=具体原因描述\");\n" +
"}\n" +
"```\n\n" +
- "这样的格式便于日志分析和问题排查。",
- ExperienceScope.GLOBAL
+ "这样的格式便于日志分析和问题排查。"
);
- javaLoggingExp.setLanguage("java");
javaLoggingExp.setTags(Set.of("logging", "format", "standard"));
javaLoggingExp.getMetadata().setSource("manual");
javaLoggingExp.getMetadata().setConfidence(1.0);
+ javaLoggingExp.setDisclosureStrategy(DisclosureStrategy.PROGRESSIVE);
- // Spring Boot异常处理经验
+ // Spring Boot异常处理经验(原CODE类型,迁移为REACT)
Experience springExceptionExp = new Experience(
- ExperienceType.CODE,
+ ExperienceType.REACT,
"Spring Boot统一异常处理",
"使用@ControllerAdvice进行全局异常处理:\n\n" +
"```java\n" +
@@ -84,26 +81,26 @@ private void initializeCodeExperiences() {
" return ResponseEntity.status(500).body(new ErrorResponse(\"Internal Server Error\"));\n" +
" }\n" +
"}\n" +
- "```",
- ExperienceScope.TEAM
+ "```"
);
- springExceptionExp.setLanguage("java");
+ springExceptionExp.getMetadata().setTenantIdList(List.of("demo-team"));
springExceptionExp.setTags(Set.of("spring-boot", "exception-handling", "controller-advice"));
springExceptionExp.getMetadata().setSource("manual");
springExceptionExp.getMetadata().setConfidence(0.9);
+ springExceptionExp.setDisclosureStrategy(DisclosureStrategy.PROGRESSIVE);
experienceRepository.save(javaLoggingExp);
experienceRepository.save(springExceptionExp);
- // FastIntent example: CODE experience (skip code-generator when matched)
- Experience fastCode = new Experience(
- ExperienceType.CODE,
+ // FastIntent example: 原CODE经验迁移为REACT + write_code ToolCallSpec
+ Experience fastWriteCode = new Experience(
+ ExperienceType.REACT,
"FastIntent: tenant=demo direct function execution",
- "When tenant=demo, skip LLM code generation and directly execute handle_demo_request.",
- ExperienceScope.GLOBAL
+ "When tenant=demo, skip LLM code generation and directly execute handle_demo_request via write_code."
);
- fastCode.setLanguage("python");
- fastCode.setTags(Set.of("fast-intent", "demo"));
+ fastWriteCode.getMetadata().setTenantIdList(List.of("demo"));
+ fastWriteCode.setTags(Set.of("fast-intent", "demo"));
+ fastWriteCode.setDisclosureStrategy(DisclosureStrategy.DIRECT);
FastIntentConfig cfg = new FastIntentConfig();
cfg.setEnabled(true);
@@ -115,21 +112,28 @@ private void initializeCodeExperiences() {
FastIntentConfig.MatchExpression match = new FastIntentConfig.MatchExpression();
match.setCondition(cond);
cfg.setMatch(match);
- fastCode.setFastIntentConfig(cfg);
+ fastWriteCode.setFastIntentConfig(cfg);
+ // 使用 ReactArtifact + write_code ToolCallSpec 替代原 CodeArtifact
ExperienceArtifact artifact = new ExperienceArtifact();
- ExperienceArtifact.CodeArtifact codeArtifact = new ExperienceArtifact.CodeArtifact();
- codeArtifact.setLanguage("python");
- codeArtifact.setFunctionName("handle_demo_request");
- codeArtifact.setParameters(List.of());
- codeArtifact.setCode("def handle_demo_request():\n return 'I am MoLiHong'\n");
- artifact.setCode(codeArtifact);
- fastCode.setArtifact(artifact);
-
- experienceRepository.save(fastCode);
- }
+ ExperienceArtifact.ReactArtifact reactArtifact = new ExperienceArtifact.ReactArtifact();
+ ExperienceArtifact.ToolPlan plan = new ExperienceArtifact.ToolPlan();
+ ExperienceArtifact.ToolCallSpec writeCodeCall = new ExperienceArtifact.ToolCallSpec();
+ writeCodeCall.setToolName("write_code");
+ writeCodeCall.setArguments(java.util.Map.of(
+ "functionName", "handle_demo_request",
+ "description", "Handle demo request",
+ "parameters", List.of(),
+ "code", "def handle_demo_request():\n return 'I am MoLiHong'\n"
+ ));
+ plan.setToolCalls(List.of(writeCodeCall));
+ reactArtifact.setPlan(plan);
+ artifact.setReact(reactArtifact);
+ fastWriteCode.setArtifact(artifact);
- private void initializeReactExperiences() {
+ experienceRepository.save(fastWriteCode);
+
+ // === 原有 REACT 经验 ===
// 代码生成策略经验
Experience codeGenExp = new Experience(
ExperienceType.REACT,
@@ -141,9 +145,9 @@ private void initializeReactExperiences() {
"4. 生成代码时保持与项目风格一致\n" +
"5. 添加适当的注释和文档\n" +
"6. 考虑异常处理和边界情况\n\n" +
- "优先使用项目中已有的工具类和框架,避免重复造轮子。",
- ExperienceScope.TEAM
+ "优先使用项目中已有的工具类和框架,避免重复造轮子。"
);
+ codeGenExp.getMetadata().setTenantIdList(List.of("demo-team"));
codeGenExp.setTags(Set.of("code-generation", "best-practice", "strategy"));
codeGenExp.getMetadata().setSource("learned");
codeGenExp.getMetadata().setConfidence(0.8);
@@ -157,8 +161,7 @@ private void initializeReactExperiences() {
"- 代码分析:使用analyze_code工具\n" +
"- 项目构建:使用build_project工具\n" +
"- 测试执行:使用run_tests工具\n\n" +
- "在使用工具前,先检查必要的参数是否齐全,避免工具调用失败。",
- ExperienceScope.GLOBAL
+ "在使用工具前,先检查必要的参数是否齐全,避免工具调用失败。"
);
toolSelectionExp.setTags(Set.of("tool-selection", "strategy", "efficiency"));
toolSelectionExp.getMetadata().setSource("learned");
@@ -171,8 +174,7 @@ private void initializeReactExperiences() {
Experience fastReact = new Experience(
ExperienceType.REACT,
"FastIntent: 前缀 ping 走固定工具链",
- "当用户以 ping 开头时,直接执行固定工具链。",
- ExperienceScope.GLOBAL
+ "当用户以 ping 开头时,直接执行固定工具链。"
);
fastReact.setTags(Set.of("fast-intent", "react", "demo"));
@@ -188,15 +190,15 @@ private void initializeReactExperiences() {
fastReact.setFastIntentConfig(rCfg);
ExperienceArtifact rArtifact = new ExperienceArtifact();
- ExperienceArtifact.ReactArtifact reactArtifact = new ExperienceArtifact.ReactArtifact();
- reactArtifact.setAssistantText("我将直接执行固定工具链。");
- ExperienceArtifact.ToolPlan plan = new ExperienceArtifact.ToolPlan();
+ ExperienceArtifact.ReactArtifact fastReactArtifact = new ExperienceArtifact.ReactArtifact();
+ fastReactArtifact.setAssistantText("我将直接执行固定工具链。");
+ ExperienceArtifact.ToolPlan fastPlan = new ExperienceArtifact.ToolPlan();
ExperienceArtifact.ToolCallSpec call1 = new ExperienceArtifact.ToolCallSpec();
call1.setToolName("reply");
call1.setArguments(java.util.Map.of("content", "pong"));
- plan.setToolCalls(List.of(call1));
- reactArtifact.setPlan(plan);
- rArtifact.setReact(reactArtifact);
+ fastPlan.setToolCalls(List.of(call1));
+ fastReactArtifact.setPlan(fastPlan);
+ rArtifact.setReact(fastReactArtifact);
fastReact.setArtifact(rArtifact);
experienceRepository.save(fastReact);
@@ -213,8 +215,7 @@ private void initializeCommonExperiences() {
"3. 使用参数化查询,避免SQL注入\n" +
"4. 及时更新依赖库,修复已知安全漏洞\n" +
"5. 对敏感操作添加权限检查\n\n" +
- "这些规范是强制性的,不得违反。",
- ExperienceScope.GLOBAL
+ "这些规范是强制性的,不得违反。"
);
securityExp.setTags(Set.of("security", "mandatory", "best-practice"));
securityExp.getMetadata().setSource("policy");
@@ -231,8 +232,7 @@ private void initializeCommonExperiences() {
"4. 单元测试覆盖率不低于70%\n" +
"5. 避免代码重复,提取公共逻辑\n" +
"6. 日志格式统一为:类名 + 目标方法 + 打印原因\n\n" +
- "请在代码生成时严格遵循这些要求。",
- ExperienceScope.GLOBAL
+ "请在代码生成时严格遵循这些要求。"
);
qualityExp.setTags(Set.of("code-quality", "standards", "mandatory"));
qualityExp.getMetadata().setSource("policy");
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/config/ExperienceDisclosureAutoConfiguration.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/config/ExperienceDisclosureAutoConfiguration.java
new file mode 100644
index 00000000..7a549900
--- /dev/null
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/config/ExperienceDisclosureAutoConfiguration.java
@@ -0,0 +1,119 @@
+package com.alibaba.assistant.agent.extension.experience.config;
+
+import com.alibaba.assistant.agent.extension.experience.disclosure.DefaultExperienceDisclosureContextResolver;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosureContextResolver;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePromptContributor;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosureService;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperiencePrefetchHook;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceRuntimeModelInterceptor;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceRuntimeToolStateInterceptor;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceToolInvocationClassifier;
+import com.alibaba.assistant.agent.extension.experience.spi.ExperienceProvider;
+import com.alibaba.assistant.agent.extension.experience.spi.ExperienceRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+
+/**
+ * Auto-configures the progressive disclosure runtime for the experience extension.
+ *
+ * This configuration is intentionally separated from the older experience hook
+ * auto-config so business applications can adopt the disclosure runtime without being
+ * forced to re-enable the legacy COMMON/REACT prompt-injection path.
+ *
+ *
Must load after {@link ExperienceExtensionAutoConfiguration} so that
+ * {@link ExperienceProvider} and {@link ExperienceRepository} beans are already
+ * available via standard Spring DI.
+ */
+@Configuration(proxyBeanMethods = false)
+@EnableConfigurationProperties(ExperienceExtensionProperties.class)
+@AutoConfigureAfter(ExperienceExtensionAutoConfiguration.class)
+@ConditionalOnProperty(prefix = "spring.ai.alibaba.codeact.extension.experience",
+ name = "enabled",
+ havingValue = "true",
+ matchIfMissing = true)
+public class ExperienceDisclosureAutoConfiguration {
+
+ private static final Logger log = LoggerFactory.getLogger(ExperienceDisclosureAutoConfiguration.class);
+
+ @Bean
+ @ConditionalOnMissingBean(ExperienceDisclosureContextResolver.class)
+ public ExperienceDisclosureContextResolver experienceDisclosureContextResolver() {
+ return new DefaultExperienceDisclosureContextResolver();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public ExperienceToolInvocationClassifier experienceToolInvocationClassifier(ExperienceExtensionProperties properties) {
+ return new ExperienceToolInvocationClassifier(properties);
+ }
+
+ /**
+ * 创建 ExperienceDisclosureService。
+ *
不使用 @ConditionalOnBean 以避免与 @AutoConfigureAfter 的排序竞争问题。
+ * 两个配置类共用同一个 property gate(experience.enabled),所以 ExperienceProvider /
+ * ExperienceRepository 一定会被 ExperienceExtensionAutoConfiguration 创建。
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ public ExperienceDisclosureService experienceDisclosureService(ExperienceProvider experienceProvider,
+ ExperienceRepository experienceRepository,
+ ExperienceExtensionProperties properties,
+ ExperienceToolInvocationClassifier classifier) {
+ log.info("ExperienceDisclosureAutoConfiguration#experienceDisclosureService - reason=创建 ExperienceDisclosureService");
+ return new ExperienceDisclosureService(experienceProvider, experienceRepository, properties, classifier);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public ExperiencePrefetchHook experiencePrefetchHook(ExperienceDisclosureService service,
+ ExperienceDisclosureContextResolver contextResolver,
+ ExperienceExtensionProperties properties) {
+ log.info("ExperienceDisclosureAutoConfiguration#experiencePrefetchHook - reason=创建 ExperiencePrefetchHook");
+ return new ExperiencePrefetchHook(service, contextResolver, properties);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public ExperienceRuntimeModelInterceptor experienceRuntimeModelInterceptor(ExperienceDisclosureService service,
+ ExperienceDisclosureContextResolver contextResolver,
+ ExperienceToolInvocationClassifier classifier) {
+ return new ExperienceRuntimeModelInterceptor(service, contextResolver, classifier);
+ }
+
+ /**
+ * 将 search_exp / read_exp 工具暴露为独立 Bean,
+ * 供 Agent Builder 注入(ModelInterceptor 的 getTools 不会被自动收集)。
+ */
+ @Bean(name = "experienceDisclosureTools")
+ public List experienceDisclosureTools(ExperienceRuntimeModelInterceptor interceptor) {
+ List tools = interceptor.getTools();
+ log.info("ExperienceDisclosureAutoConfiguration#experienceDisclosureTools - reason=暴露披露工具, count={}", tools.size());
+ return tools;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public ExperienceRuntimeToolStateInterceptor experienceRuntimeToolStateInterceptor() {
+ return new ExperienceRuntimeToolStateInterceptor();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean(ExperienceDisclosurePromptContributor.class)
+ @ConditionalOnProperty(prefix = "spring.ai.alibaba.codeact.extension.experience",
+ name = "disclosure-prompt-enabled",
+ havingValue = "true",
+ matchIfMissing = true)
+ public ExperienceDisclosurePromptContributor experienceDisclosurePromptContributor() {
+ log.info("ExperienceDisclosureAutoConfiguration#experienceDisclosurePromptContributor - reason=创建 ExperienceDisclosurePromptContributor");
+ return new ExperienceDisclosurePromptContributor();
+ }
+}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/config/ExperienceExtensionAutoConfiguration.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/config/ExperienceExtensionAutoConfiguration.java
index a1f71a79..8cab5bb9 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/config/ExperienceExtensionAutoConfiguration.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/config/ExperienceExtensionAutoConfiguration.java
@@ -1,10 +1,6 @@
package com.alibaba.assistant.agent.extension.experience.config;
-import com.alibaba.assistant.agent.extension.experience.hook.CommonSenseExperienceModelHook;
import com.alibaba.assistant.agent.extension.experience.hook.FastIntentReactHook;
-import com.alibaba.assistant.agent.extension.experience.hook.ReactExperienceAgentHook;
-import com.alibaba.assistant.agent.extension.experience.tool.CommonSenseInjectionTool;
-import com.alibaba.assistant.agent.extension.experience.tool.ReactStrategyInjectionTool;
import com.alibaba.assistant.agent.extension.experience.fastintent.FastIntentService;
import com.alibaba.assistant.agent.extension.experience.internal.InMemoryExperienceProvider;
import com.alibaba.assistant.agent.extension.experience.internal.InMemoryExperienceRepository;
@@ -25,6 +21,10 @@
/**
* 经验模块自动配置类
*
+ * 提供经验仓库、经验提供者、快速意图等基础 Bean。
+ * 旧版的 ReactExperienceAgentHook / CommonSenseExperienceModelHook 已在 4.2
+ * 重构中被渐进式披露系统(ExperienceDisclosureAutoConfiguration)取代并移除。
+ *
* @author Assistant Agent Team
*/
@Configuration(proxyBeanMethods = false)
@@ -76,7 +76,7 @@ public FastIntentService fastIntentService(ObjectProvider这个工具与 CommonSenseExperienceModelHook 配套使用。
- * Hook 会注入 AssistantMessage + ToolResponseMessage 配对来模拟工具调用,
- * 注册这个工具可以让 ReactAgent 的路由逻辑正确识别和处理。
- */
- @Bean
- @ConditionalOnProperty(prefix = "spring.ai.alibaba.codeact.extension.experience",
- name = "common-experience-enabled",
- havingValue = "true",
- matchIfMissing = true)
- public CommonSenseInjectionTool commonSenseInjectionTool() {
- log.info("ExperienceExtensionAutoConfiguration#commonSenseInjectionTool - reason=creating common sense injection tool bean");
- return new CommonSenseInjectionTool();
- }
-
- /**
- * 配置React策略经验注入假工具
- *
- * 这个工具与 ReactExperienceAgentHook 配套使用。
- * Hook 会注入 AssistantMessage + ToolResponseMessage 配对来模拟工具调用,
- * 注册这个工具可以让 ReactAgent 的路由逻辑正确识别和处理。
- *
- *
如果 LLM 错误地尝试调用这个工具,会返回错误提示。
- */
- @Bean
- @ConditionalOnProperty(prefix = "spring.ai.alibaba.codeact.extension.experience",
- name = "react-experience-enabled",
- havingValue = "true",
- matchIfMissing = true)
- public ReactStrategyInjectionTool reactStrategyInjectionTool() {
- log.info("ExperienceExtensionAutoConfiguration#reactStrategyInjectionTool - reason=creating react strategy injection tool bean");
- return new ReactStrategyInjectionTool();
- }
}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/config/ExperienceExtensionProperties.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/config/ExperienceExtensionProperties.java
index b021d01f..b6ad79c8 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/config/ExperienceExtensionProperties.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/config/ExperienceExtensionProperties.java
@@ -53,6 +53,17 @@ public class ExperienceExtensionProperties {
*/
private List fastIntentAllowedTools = new ArrayList<>();
+ /**
+ * Progressive disclosure 下允许直接以 React function call 形式暴露的工具名白名单。
+ *
+ * 首版采用显式 allowlist:
+ *
+ * 未配置的 TOOL 经验仍然只能通过 write_code / execute_code 走 CodeAct 路径
+ * 已配置的工具会额外注册到 React 阶段,但运行时仍会根据预取/检索结果做二次裁剪
+ *
+ */
+ private List reactDirectToolNames = new ArrayList<>();
+
/**
* 单次查询最大返回经验条数
*/
@@ -234,6 +245,15 @@ public List getFastIntentAllowedTools() {
public void setFastIntentAllowedTools(List fastIntentAllowedTools) {
this.fastIntentAllowedTools = fastIntentAllowedTools != null ? fastIntentAllowedTools : new ArrayList<>();
}
+
+ public List getReactDirectToolNames() {
+ return reactDirectToolNames;
+ }
+
+ public void setReactDirectToolNames(List reactDirectToolNames) {
+ this.reactDirectToolNames = reactDirectToolNames != null ? reactDirectToolNames : new ArrayList<>();
+ }
+
public int getMaxItemsPerQuery() {
return maxItemsPerQuery;
}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/DefaultExperienceDisclosureContextResolver.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/DefaultExperienceDisclosureContextResolver.java
new file mode 100644
index 00000000..c78e6104
--- /dev/null
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/DefaultExperienceDisclosureContextResolver.java
@@ -0,0 +1,43 @@
+package com.alibaba.assistant.agent.extension.experience.disclosure;
+
+import com.alibaba.assistant.agent.extension.experience.model.ExperienceQueryContext;
+import com.alibaba.cloud.ai.graph.OverAllState;
+import com.alibaba.cloud.ai.graph.RunnableConfig;
+import org.springframework.util.StringUtils;
+
+/**
+ * Default disclosure context resolver for generic Assistant Agent usage.
+ *
+ * This fallback keeps the extension usable out of the box by resolving the query from
+ * the raw {@code input} field and by deriving tenant context from common state/metadata
+ * keys when no business-specific resolver is provided.
+ */
+public class DefaultExperienceDisclosureContextResolver implements ExperienceDisclosureContextResolver {
+
+ @Override
+ public String resolveQuery(OverAllState state, RunnableConfig config) {
+ if (state == null) {
+ return null;
+ }
+ return state.value("input", String.class).orElse(null);
+ }
+
+ @Override
+ public ExperienceQueryContext buildQueryContext(OverAllState state, RunnableConfig config, String userQuery) {
+ ExperienceQueryContext context = new ExperienceQueryContext();
+ context.setUserQuery(userQuery);
+ if (state != null) {
+ state.value("tenant_id", String.class).ifPresent(context::setTenantId);
+ if (!StringUtils.hasText(context.getTenantId())) {
+ state.value("user_id", String.class).ifPresent(context::setTenantId);
+ }
+ }
+ if (!StringUtils.hasText(context.getTenantId()) && config != null) {
+ config.metadata("tenant_id").ifPresent(value -> context.setTenantId(String.valueOf(value)));
+ if (!StringUtils.hasText(context.getTenantId())) {
+ config.metadata("user_id").ifPresent(value -> context.setTenantId(String.valueOf(value)));
+ }
+ }
+ return context;
+ }
+}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceDisclosureContextResolver.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceDisclosureContextResolver.java
new file mode 100644
index 00000000..e8f6d164
--- /dev/null
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceDisclosureContextResolver.java
@@ -0,0 +1,28 @@
+package com.alibaba.assistant.agent.extension.experience.disclosure;
+
+import com.alibaba.assistant.agent.extension.experience.model.ExperienceQueryContext;
+import com.alibaba.cloud.ai.graph.OverAllState;
+import com.alibaba.cloud.ai.graph.RunnableConfig;
+import org.springframework.util.StringUtils;
+
+/**
+ * Strategy interface for resolving disclosure query inputs from runtime state.
+ *
+ *
The extension layer owns the disclosure runtime itself, while each business agent
+ * can provide its own resolver to decide which query to prefetch and which tenant/user
+ * context should be attached to retrieval.
+ */
+public interface ExperienceDisclosureContextResolver {
+
+ String resolveQuery(OverAllState state, RunnableConfig config);
+
+ ExperienceQueryContext buildQueryContext(OverAllState state, RunnableConfig config, String userQuery);
+
+ default boolean shouldPrefetch(OverAllState state, RunnableConfig config, String query) {
+ if (!StringUtils.hasText(query)) {
+ return false;
+ }
+ Integer currentRound = state != null ? state.value("current_round", Integer.class).orElse(0) : 0;
+ return currentRound == null || currentRound <= 0;
+ }
+}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceDisclosurePayloads.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceDisclosurePayloads.java
new file mode 100644
index 00000000..e76e9130
--- /dev/null
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceDisclosurePayloads.java
@@ -0,0 +1,522 @@
+package com.alibaba.assistant.agent.extension.experience.disclosure;
+
+import com.alibaba.assistant.agent.extension.experience.model.ExperienceArtifact;
+import com.alibaba.assistant.agent.extension.experience.model.ExperienceType;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Shared DTOs for experience progressive disclosure runtime.
+ *
+ *
This file defines the in-memory/request-response models exchanged between the
+ * prefetch hook, runtime tools, prompt contributors, and state cache. Keeping them in
+ * the extension layer makes {@code search_exp}/{@code read_exp} reusable across
+ * different business agents.
+ */
+public final class ExperienceDisclosurePayloads {
+
+ private ExperienceDisclosurePayloads() {
+ }
+
+ public enum ToolInvocationPath {
+ REACT_DIRECT,
+ CODE_ONLY
+ }
+
+ public enum PrefetchStatus {
+ NOT_RUN,
+ SKIPPED,
+ COMPLETED
+ }
+
+ public static class ExperienceCandidateCard implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private String id;
+ private ExperienceType experienceType;
+ private String title;
+ private String description;
+ private String snippet;
+ private String disclosureStrategy;
+ private Double score;
+ private List associatedTools = new ArrayList<>();
+ private List relatedExperiences = new ArrayList<>();
+ private ToolInvocationPath toolInvocationPath;
+ private String callableToolName;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public ExperienceType getExperienceType() {
+ return experienceType;
+ }
+
+ public void setExperienceType(ExperienceType experienceType) {
+ this.experienceType = experienceType;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getSnippet() {
+ return snippet;
+ }
+
+ public void setSnippet(String snippet) {
+ this.snippet = snippet;
+ }
+
+ public String getDisclosureStrategy() {
+ return disclosureStrategy;
+ }
+
+ public void setDisclosureStrategy(String disclosureStrategy) {
+ this.disclosureStrategy = disclosureStrategy;
+ }
+
+ public Double getScore() {
+ return score;
+ }
+
+ public void setScore(Double score) {
+ this.score = score;
+ }
+
+ public List getAssociatedTools() {
+ return associatedTools;
+ }
+
+ public void setAssociatedTools(List associatedTools) {
+ this.associatedTools = associatedTools != null ? associatedTools : new ArrayList<>();
+ }
+
+ public List getRelatedExperiences() {
+ return relatedExperiences;
+ }
+
+ public void setRelatedExperiences(List relatedExperiences) {
+ this.relatedExperiences = relatedExperiences != null ? relatedExperiences : new ArrayList<>();
+ }
+
+ public ToolInvocationPath getToolInvocationPath() {
+ return toolInvocationPath;
+ }
+
+ public void setToolInvocationPath(ToolInvocationPath toolInvocationPath) {
+ this.toolInvocationPath = toolInvocationPath;
+ }
+
+ public String getCallableToolName() {
+ return callableToolName;
+ }
+
+ public void setCallableToolName(String callableToolName) {
+ this.callableToolName = callableToolName;
+ }
+ }
+
+ public static class DirectExperienceGrounding implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private String id;
+ private ExperienceType experienceType;
+ private String title;
+ private String description;
+ private String content;
+ private String disclosureStrategy;
+ private Double score;
+ private ToolInvocationPath toolInvocationPath;
+ private String callableToolName;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public ExperienceType getExperienceType() {
+ return experienceType;
+ }
+
+ public void setExperienceType(ExperienceType experienceType) {
+ this.experienceType = experienceType;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ public String getDisclosureStrategy() {
+ return disclosureStrategy;
+ }
+
+ public void setDisclosureStrategy(String disclosureStrategy) {
+ this.disclosureStrategy = disclosureStrategy;
+ }
+
+ public Double getScore() {
+ return score;
+ }
+
+ public void setScore(Double score) {
+ this.score = score;
+ }
+
+ public ToolInvocationPath getToolInvocationPath() {
+ return toolInvocationPath;
+ }
+
+ public void setToolInvocationPath(ToolInvocationPath toolInvocationPath) {
+ this.toolInvocationPath = toolInvocationPath;
+ }
+
+ public String getCallableToolName() {
+ return callableToolName;
+ }
+
+ public void setCallableToolName(String callableToolName) {
+ this.callableToolName = callableToolName;
+ }
+ }
+
+ public static class GroupedExperienceCandidates implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private List commonCandidates = new ArrayList<>();
+ private List reactCandidates = new ArrayList<>();
+ private List toolCandidates = new ArrayList<>();
+
+ public List getCommonCandidates() {
+ return commonCandidates;
+ }
+
+ public void setCommonCandidates(List commonCandidates) {
+ this.commonCandidates = commonCandidates != null ? commonCandidates : new ArrayList<>();
+ }
+
+ public List getReactCandidates() {
+ return reactCandidates;
+ }
+
+ public void setReactCandidates(List reactCandidates) {
+ this.reactCandidates = reactCandidates != null ? reactCandidates : new ArrayList<>();
+ }
+
+ public List getToolCandidates() {
+ return toolCandidates;
+ }
+
+ public void setToolCandidates(List toolCandidates) {
+ this.toolCandidates = toolCandidates != null ? toolCandidates : new ArrayList<>();
+ }
+ }
+
+ public static class SearchExpRequest implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private String query;
+ private Integer commonLimit;
+ private Integer reactLimit;
+ private Integer toolLimit;
+
+ public String getQuery() {
+ return query;
+ }
+
+ public void setQuery(String query) {
+ this.query = query;
+ }
+
+ public Integer getCommonLimit() {
+ return commonLimit;
+ }
+
+ public void setCommonLimit(Integer commonLimit) {
+ this.commonLimit = commonLimit;
+ }
+
+ public Integer getReactLimit() {
+ return reactLimit;
+ }
+
+ public void setReactLimit(Integer reactLimit) {
+ this.reactLimit = reactLimit;
+ }
+
+ public Integer getToolLimit() {
+ return toolLimit;
+ }
+
+ public void setToolLimit(Integer toolLimit) {
+ this.toolLimit = toolLimit;
+ }
+ }
+
+ public static class SearchExpResponse implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private String query;
+ private GroupedExperienceCandidates candidates = new GroupedExperienceCandidates();
+ private List directGroundings = new ArrayList<>();
+
+ public String getQuery() {
+ return query;
+ }
+
+ public void setQuery(String query) {
+ this.query = query;
+ }
+
+ public GroupedExperienceCandidates getCandidates() {
+ return candidates;
+ }
+
+ public void setCandidates(GroupedExperienceCandidates candidates) {
+ this.candidates = candidates != null ? candidates : new GroupedExperienceCandidates();
+ }
+
+ public List getDirectGroundings() {
+ return directGroundings;
+ }
+
+ public void setDirectGroundings(List directGroundings) {
+ this.directGroundings = directGroundings != null ? directGroundings : new ArrayList<>();
+ }
+ }
+
+ public static class ReadExpRequest implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private String id;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+ }
+
+ public static class ReadExpResponse implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private boolean found;
+ private String id;
+ private ExperienceType experienceType;
+ private String title;
+ private String description;
+ private String content;
+ private String disclosureStrategy;
+ private Double score;
+ private List associatedTools = new ArrayList<>();
+ private List relatedExperiences = new ArrayList<>();
+ private ToolInvocationPath toolInvocationPath;
+ private String callableToolName;
+ private ExperienceArtifact artifact;
+
+ public boolean isFound() {
+ return found;
+ }
+
+ public void setFound(boolean found) {
+ this.found = found;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public ExperienceType getExperienceType() {
+ return experienceType;
+ }
+
+ public void setExperienceType(ExperienceType experienceType) {
+ this.experienceType = experienceType;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ public String getDisclosureStrategy() {
+ return disclosureStrategy;
+ }
+
+ public void setDisclosureStrategy(String disclosureStrategy) {
+ this.disclosureStrategy = disclosureStrategy;
+ }
+
+ public Double getScore() {
+ return score;
+ }
+
+ public void setScore(Double score) {
+ this.score = score;
+ }
+
+ public List getAssociatedTools() {
+ return associatedTools;
+ }
+
+ public void setAssociatedTools(List associatedTools) {
+ this.associatedTools = associatedTools != null ? associatedTools : new ArrayList<>();
+ }
+
+ public List getRelatedExperiences() {
+ return relatedExperiences;
+ }
+
+ public void setRelatedExperiences(List relatedExperiences) {
+ this.relatedExperiences = relatedExperiences != null ? relatedExperiences : new ArrayList<>();
+ }
+
+ public ToolInvocationPath getToolInvocationPath() {
+ return toolInvocationPath;
+ }
+
+ public void setToolInvocationPath(ToolInvocationPath toolInvocationPath) {
+ this.toolInvocationPath = toolInvocationPath;
+ }
+
+ public String getCallableToolName() {
+ return callableToolName;
+ }
+
+ public void setCallableToolName(String callableToolName) {
+ this.callableToolName = callableToolName;
+ }
+
+ public ExperienceArtifact getArtifact() {
+ return artifact;
+ }
+
+ public void setArtifact(ExperienceArtifact artifact) {
+ this.artifact = artifact;
+ }
+ }
+
+ public static class PrefetchedExperienceSnapshot implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private String query;
+ private PrefetchStatus status = PrefetchStatus.NOT_RUN;
+ private GroupedExperienceCandidates candidates = new GroupedExperienceCandidates();
+ private List directGroundings = new ArrayList<>();
+
+ public String getQuery() {
+ return query;
+ }
+
+ public void setQuery(String query) {
+ this.query = query;
+ }
+
+ public PrefetchStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(PrefetchStatus status) {
+ this.status = status != null ? status : PrefetchStatus.NOT_RUN;
+ }
+
+ public GroupedExperienceCandidates getCandidates() {
+ return candidates;
+ }
+
+ public void setCandidates(GroupedExperienceCandidates candidates) {
+ this.candidates = candidates != null ? candidates : new GroupedExperienceCandidates();
+ }
+
+ public List getDirectGroundings() {
+ return directGroundings;
+ }
+
+ public void setDirectGroundings(List directGroundings) {
+ this.directGroundings = directGroundings != null ? directGroundings : new ArrayList<>();
+ }
+ }
+}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceDisclosurePromptContributor.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceDisclosurePromptContributor.java
new file mode 100644
index 00000000..5a40717c
--- /dev/null
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceDisclosurePromptContributor.java
@@ -0,0 +1,253 @@
+package com.alibaba.assistant.agent.extension.experience.disclosure;
+
+import com.alibaba.assistant.agent.common.constant.CodeactStateKeys;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.DirectExperienceGrounding;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.ExperienceCandidateCard;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.GroupedExperienceCandidates;
+import com.alibaba.assistant.agent.prompt.PromptContribution;
+import com.alibaba.assistant.agent.prompt.PromptContributor;
+import com.alibaba.assistant.agent.prompt.PromptContributorContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.chat.messages.UserMessage;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Framework-level prompt contributor for experience progressive disclosure.
+ *
+ * When prefetched experience candidates or direct groundings are present in the agent
+ * state, this contributor injects a structured prompt that teaches the LLM how to work
+ * with the disclosure system:
+ *
+ * What the three experience types (COMMON, REACT, TOOL) mean
+ * How DIRECT vs PROGRESSIVE disclosure works
+ * When and how to use {@code search_exp} and {@code read_exp} tools
+ * The current round's candidate cards and already-disclosed content
+ *
+ *
+ * Business applications can override this bean via {@code @ConditionalOnMissingBean}
+ * to provide a customised disclosure prompt (e.g. with domain-specific guidance).
+ */
+public class ExperienceDisclosurePromptContributor implements PromptContributor {
+
+ private static final Logger log = LoggerFactory.getLogger(ExperienceDisclosurePromptContributor.class);
+
+ private static final int DEFAULT_PRIORITY = 18;
+ private static final int MAX_CARDS_PER_SECTION = 3;
+ private static final int MAX_DESCRIPTION_LENGTH = 100;
+ private static final int MAX_DIRECT_CONTENT_LENGTH = 500;
+
+ private final int priority;
+
+ public ExperienceDisclosurePromptContributor() {
+ this(DEFAULT_PRIORITY);
+ }
+
+ public ExperienceDisclosurePromptContributor(int priority) {
+ this.priority = priority;
+ }
+
+ @Override
+ public String getName() {
+ return "ExperienceDisclosurePromptContributor";
+ }
+
+ @Override
+ public int getPriority() {
+ return priority;
+ }
+
+ @Override
+ public boolean shouldContribute(PromptContributorContext context) {
+ boolean hasCandidates = context.getAttribute(
+ CodeactStateKeys.EXPERIENCE_PREFETCHED_CANDIDATES, GroupedExperienceCandidates.class)
+ .map(this::hasAnyCandidates)
+ .orElse(false);
+ boolean hasDirectGroundings = context.getAttribute(
+ CodeactStateKeys.EXPERIENCE_DIRECT_GROUNDINGS, List.class)
+ .map(this::normalizeDirectGroundings)
+ .map(list -> !list.isEmpty())
+ .orElse(false);
+ boolean result = hasCandidates || hasDirectGroundings;
+ log.info("ExperienceDisclosurePromptContributor#shouldContribute - " +
+ "hasCandidates={}, hasDirectGroundings={}, result={}", hasCandidates, hasDirectGroundings, result);
+ return result;
+ }
+
+ @Override
+ public PromptContribution contribute(PromptContributorContext context) {
+ GroupedExperienceCandidates candidates = context.getAttribute(
+ CodeactStateKeys.EXPERIENCE_PREFETCHED_CANDIDATES, GroupedExperienceCandidates.class).orElse(null);
+ List directGroundings = context
+ .getAttribute(CodeactStateKeys.EXPERIENCE_DIRECT_GROUNDINGS, List.class)
+ .map(this::normalizeDirectGroundings)
+ .orElse(List.of());
+ if ((candidates == null || !hasAnyCandidates(candidates)) && directGroundings.isEmpty()) {
+ return PromptContribution.empty();
+ }
+ return PromptContribution.builder()
+ .append(new UserMessage(buildPrompt(candidates, directGroundings)))
+ .build();
+ }
+
+ // ─── Prompt construction ──────────────────────────────────────────
+
+ /**
+ * Builds the complete disclosure prompt. Subclasses may override this to customise
+ * the prompt text while reusing the candidate rendering helpers.
+ */
+ protected String buildPrompt(GroupedExperienceCandidates candidates,
+ List directGroundings) {
+ StringBuilder sb = new StringBuilder();
+ Set disclosedDirectIds = collectDirectIds(directGroundings);
+
+ sb.append("\n");
+ appendGuidanceText(sb);
+ appendDirectGroundings(sb, directGroundings);
+ if (candidates != null) {
+ appendSection(sb, "COMMON grounding candidates",
+ filterAlreadyDisclosedCandidates(candidates.getCommonCandidates(), disclosedDirectIds));
+ appendSection(sb, "REACT workflow candidates",
+ filterAlreadyDisclosedCandidates(candidates.getReactCandidates(), disclosedDirectIds));
+ appendSection(sb, "TOOL capability candidates",
+ filterAlreadyDisclosedCandidates(candidates.getToolCandidates(), disclosedDirectIds));
+ }
+ sb.append(" ");
+ return sb.toString();
+ }
+
+ /**
+ * 追加披露机制的引导说明文本。
+ * 子类可覆写以提供领域特定的说明。
+ */
+ protected void appendGuidanceText(StringBuilder sb) {
+ sb.append("经验分为三类:COMMON 用于解释概念/产品术语,REACT 用于提供流程与策略参考,TOOL 用于能力判断、工具选择与调用路径判断。\n");
+ sb.append("披露方式分为两种:`DIRECT` 表示内容已满足高置信短文本条件,可直接利用;`PROGRESSIVE` 表示当前只给候选卡,需要完整正文时调用 `read_exp`。\n");
+ sb.append("优先复用已披露的 DIRECT 内容,再从候选中选择 id;只有当前候选明显不足或缺少方向时,再调用 `search_exp`。\n");
+ sb.append("TOOL 候选如果标记为 `REACT_DIRECT`,表示它具备被直接作为 React function call 调用的资格;");
+ sb.append("但只有在该单个工具即可直接完成任务时,才优先 direct call。");
+ sb.append("若需要多个工具配合、顺序编排、条件分支、循环重试或结果再加工,请改走 `write_code` + `execute_code`。");
+ sb.append("若标记为 `CODE_ONLY`,则只能通过 `write_code` + `execute_code` 使用。\n\n");
+ }
+
+ // ─── Section rendering helpers ────────────────────────────────────
+
+ protected void appendDirectGroundings(StringBuilder sb, List directGroundings) {
+ if (directGroundings == null || directGroundings.isEmpty()) {
+ return;
+ }
+ sb.append("DIRECT grounding already available:\n");
+ for (DirectExperienceGrounding grounding : directGroundings) {
+ sb.append("- id=").append(grounding.getId())
+ .append(", type=").append(grounding.getExperienceType())
+ .append(", title=").append(safe(grounding.getTitle()));
+ if (grounding.getScore() != null) {
+ sb.append(", score=").append(grounding.getScore());
+ }
+ if (grounding.getToolInvocationPath() != null) {
+ sb.append(", invocationPath=").append(grounding.getToolInvocationPath());
+ }
+ if (grounding.getCallableToolName() != null && !grounding.getCallableToolName().isBlank()) {
+ sb.append(", toolName=").append(grounding.getCallableToolName());
+ }
+ sb.append("\n");
+ sb.append(" content=").append(trimDirectContent(grounding.getContent())).append("\n");
+ }
+ sb.append("\n");
+ }
+
+ protected void appendSection(StringBuilder sb, String title, List cards) {
+ if (cards == null || cards.isEmpty()) {
+ return;
+ }
+ sb.append(title).append(":\n");
+ int limit = Math.min(cards.size(), MAX_CARDS_PER_SECTION);
+ for (int i = 0; i < limit; i++) {
+ ExperienceCandidateCard card = cards.get(i);
+ sb.append("- id=").append(card.getId())
+ .append(", type=").append(card.getExperienceType())
+ .append(", title=").append(safe(card.getTitle()));
+ if (card.getDescription() != null && !card.getDescription().isBlank()) {
+ String desc = card.getDescription();
+ if (desc.length() > MAX_DESCRIPTION_LENGTH) {
+ desc = desc.substring(0, MAX_DESCRIPTION_LENGTH) + "...";
+ }
+ sb.append(", description=").append(desc);
+ }
+ if (card.getToolInvocationPath() != null) {
+ sb.append(", invocationPath=").append(card.getToolInvocationPath());
+ }
+ if (card.getCallableToolName() != null && !card.getCallableToolName().isBlank()) {
+ sb.append(", toolName=").append(card.getCallableToolName());
+ }
+ sb.append("\n");
+ }
+ if (cards.size() > limit) {
+ sb.append("- ... ").append(cards.size() - limit).append(" more candidates omitted\n");
+ }
+ sb.append("\n");
+ }
+
+ // ─── Utilities ────────────────────────────────────────────────────
+
+ private boolean hasAnyCandidates(GroupedExperienceCandidates candidates) {
+ return !(candidates.getCommonCandidates().isEmpty()
+ && candidates.getReactCandidates().isEmpty()
+ && candidates.getToolCandidates().isEmpty());
+ }
+
+ private Set collectDirectIds(List directGroundings) {
+ Set directIds = new LinkedHashSet<>();
+ if (directGroundings == null || directGroundings.isEmpty()) {
+ return directIds;
+ }
+ for (DirectExperienceGrounding grounding : directGroundings) {
+ if (grounding != null && grounding.getId() != null && !grounding.getId().isBlank()) {
+ directIds.add(grounding.getId());
+ }
+ }
+ return directIds;
+ }
+
+ private List filterAlreadyDisclosedCandidates(
+ List cards, Set disclosedDirectIds) {
+ if (cards == null || cards.isEmpty() || disclosedDirectIds == null || disclosedDirectIds.isEmpty()) {
+ return cards;
+ }
+ return cards.stream()
+ .filter(card -> card != null)
+ .filter(card -> !disclosedDirectIds.contains(card.getId()))
+ .toList();
+ }
+
+ private String safe(String value) {
+ return value == null ? "" : value;
+ }
+
+ private String trimDirectContent(String content) {
+ if (content == null) {
+ return "";
+ }
+ if (content.length() <= MAX_DIRECT_CONTENT_LENGTH) {
+ return content.replace("\n", "\\n");
+ }
+ return content.substring(0, MAX_DIRECT_CONTENT_LENGTH).replace("\n", "\\n") + "...";
+ }
+
+ @SuppressWarnings("unchecked")
+ private List normalizeDirectGroundings(List> values) {
+ if (values == null || values.isEmpty()) {
+ return List.of();
+ }
+ java.util.ArrayList normalized = new java.util.ArrayList<>();
+ for (Object value : values) {
+ if (value instanceof DirectExperienceGrounding grounding) {
+ normalized.add(grounding);
+ }
+ }
+ return normalized;
+ }
+}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceDisclosureService.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceDisclosureService.java
new file mode 100644
index 00000000..0ecc5a10
--- /dev/null
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceDisclosureService.java
@@ -0,0 +1,266 @@
+package com.alibaba.assistant.agent.extension.experience.disclosure;
+
+import com.alibaba.assistant.agent.extension.experience.config.ExperienceExtensionProperties;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.ExperienceCandidateCard;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.DirectExperienceGrounding;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.GroupedExperienceCandidates;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.PrefetchedExperienceSnapshot;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.ReadExpResponse;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.SearchExpResponse;
+import com.alibaba.assistant.agent.extension.experience.model.DisclosureStrategy;
+import com.alibaba.assistant.agent.extension.experience.model.Experience;
+import com.alibaba.assistant.agent.extension.experience.model.ExperienceQuery;
+import com.alibaba.assistant.agent.extension.experience.model.ExperienceQueryContext;
+import com.alibaba.assistant.agent.extension.experience.model.ExperienceType;
+import com.alibaba.assistant.agent.extension.experience.spi.ExperienceProvider;
+import com.alibaba.assistant.agent.extension.experience.spi.ExperienceRepository;
+import org.springframework.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import static com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.PrefetchStatus;
+
+/**
+ * Core retrieval service behind experience progressive disclosure.
+ *
+ * It turns repository/provider data into grouped lightweight candidate cards for
+ * prefetch/search and full-detail payloads for {@code read_exp}, while preserving
+ * tenant filtering, disclosure strategy, and TOOL invocation metadata.
+ */
+public class ExperienceDisclosureService {
+
+ private static final int DEFAULT_COMMON_LIMIT = 3;
+ private static final int SNIPPET_LENGTH = 280;
+ private static final int DIRECT_CONTENT_MAX_LENGTH = 500;
+ private static final double DIRECT_CONFIDENCE_THRESHOLD = 0.8D;
+
+ private final ExperienceProvider experienceProvider;
+ private final ExperienceRepository experienceRepository;
+ private final ExperienceExtensionProperties properties;
+ private final ExperienceToolInvocationClassifier toolInvocationClassifier;
+
+ public ExperienceDisclosureService(ExperienceProvider experienceProvider,
+ ExperienceRepository experienceRepository,
+ ExperienceExtensionProperties properties,
+ ExperienceToolInvocationClassifier toolInvocationClassifier) {
+ this.experienceProvider = experienceProvider;
+ this.experienceRepository = experienceRepository;
+ this.properties = properties;
+ this.toolInvocationClassifier = toolInvocationClassifier;
+ }
+
+ public PrefetchedExperienceSnapshot prefetch(String query, ExperienceQueryContext context) {
+ PrefetchedExperienceSnapshot snapshot = new PrefetchedExperienceSnapshot();
+ snapshot.setQuery(query);
+ if (!StringUtils.hasText(query)) {
+ snapshot.setStatus(PrefetchStatus.SKIPPED);
+ return snapshot;
+ }
+ List commonExperiences = queryByType(ExperienceType.COMMON, query, DEFAULT_COMMON_LIMIT, context);
+ List reactExperiences = queryByType(ExperienceType.REACT, query, resolveActionLimit(), context);
+ List toolExperiences = queryByType(ExperienceType.TOOL, query, resolveActionLimit(), context);
+ snapshot.setCandidates(searchGrouped(commonExperiences, reactExperiences, toolExperiences));
+ snapshot.setDirectGroundings(extractDirectGroundings(commonExperiences, reactExperiences, toolExperiences));
+ snapshot.setStatus(PrefetchStatus.COMPLETED);
+ return snapshot;
+ }
+
+ public SearchExpResponse search(String query, Integer commonLimit, Integer reactLimit, Integer toolLimit,
+ ExperienceQueryContext context) {
+ SearchExpResponse response = new SearchExpResponse();
+ response.setQuery(query);
+ List commonExperiences = queryByType(
+ ExperienceType.COMMON, query, normalizeLimit(commonLimit, DEFAULT_COMMON_LIMIT), context);
+ List reactExperiences = queryByType(
+ ExperienceType.REACT, query, normalizeLimit(reactLimit, resolveActionLimit()), context);
+ List toolExperiences = queryByType(
+ ExperienceType.TOOL, query, normalizeLimit(toolLimit, resolveActionLimit()), context);
+ response.setCandidates(searchGrouped(commonExperiences, reactExperiences, toolExperiences));
+ response.setDirectGroundings(extractDirectGroundings(commonExperiences, reactExperiences, toolExperiences));
+ return response;
+ }
+
+ public ReadExpResponse read(String id) {
+ ReadExpResponse response = new ReadExpResponse();
+ response.setId(id);
+ if (!StringUtils.hasText(id)) {
+ response.setFound(false);
+ return response;
+ }
+ Optional experienceOptional = experienceRepository.findById(id);
+ if (experienceOptional.isEmpty()) {
+ response.setFound(false);
+ return response;
+ }
+
+ Experience experience = experienceOptional.get();
+ response.setFound(true);
+ response.setExperienceType(experience.getType());
+ response.setTitle(experience.getName());
+ response.setDescription(experience.getDescription());
+ response.setContent(experience.getContent());
+ response.setDisclosureStrategy(experience.getDisclosureStrategy() != null
+ ? experience.getDisclosureStrategy().name() : null);
+ response.setScore(resolveScore(experience));
+ response.setAssociatedTools(new ArrayList<>(experience.getAssociatedTools()));
+ response.setRelatedExperiences(new ArrayList<>(experience.getRelatedExperiences()));
+ response.setArtifact(experience.getArtifact());
+
+ if (experience.getType() == ExperienceType.TOOL) {
+ response.setToolInvocationPath(toolInvocationClassifier.classify(experience));
+ response.setCallableToolName(toolInvocationClassifier.resolveCallableToolName(experience));
+ }
+ return response;
+ }
+
+ private GroupedExperienceCandidates searchGrouped(List commonExperiences,
+ List reactExperiences,
+ List toolExperiences) {
+ GroupedExperienceCandidates candidates = new GroupedExperienceCandidates();
+ candidates.setCommonCandidates(toCards(commonExperiences));
+ candidates.setReactCandidates(toCards(reactExperiences));
+ candidates.setToolCandidates(toCards(toolExperiences));
+ return candidates;
+ }
+
+ private List queryByType(ExperienceType type, String query, int limit, ExperienceQueryContext context) {
+ ExperienceQuery experienceQuery = new ExperienceQuery(type);
+ experienceQuery.setText(query);
+ experienceQuery.setLimit(limit);
+ if (type == ExperienceType.COMMON) {
+ experienceQuery.setDisclosureStrategy(DisclosureStrategy.DIRECT);
+ }
+ return experienceProvider.query(experienceQuery, context);
+ }
+
+ private List toCards(List experiences) {
+ List cards = new ArrayList<>();
+ for (Experience experience : experiences) {
+ ExperienceCandidateCard card = new ExperienceCandidateCard();
+ card.setId(experience.getId());
+ card.setExperienceType(experience.getType());
+ card.setTitle(experience.getName());
+ card.setDescription(experience.getDescription());
+ card.setSnippet(snippet(experience.getContent()));
+ card.setDisclosureStrategy(experience.getDisclosureStrategy() != null
+ ? experience.getDisclosureStrategy().name() : null);
+ card.setScore(resolveScore(experience));
+ card.setAssociatedTools(new ArrayList<>(experience.getAssociatedTools()));
+ card.setRelatedExperiences(new ArrayList<>(experience.getRelatedExperiences()));
+ if (experience.getType() == ExperienceType.TOOL) {
+ card.setToolInvocationPath(toolInvocationClassifier.classify(experience));
+ card.setCallableToolName(toolInvocationClassifier.resolveCallableToolName(experience));
+ }
+ cards.add(card);
+ }
+ return cards;
+ }
+
+ private List extractDirectGroundings(List commonExperiences,
+ List reactExperiences,
+ List toolExperiences) {
+ List groundings = new ArrayList<>();
+ appendDirectGroundings(groundings, commonExperiences);
+ appendDirectGroundings(groundings, reactExperiences);
+ appendDirectGroundings(groundings, toolExperiences);
+ return groundings;
+ }
+
+ private void appendDirectGroundings(List groundings, List experiences) {
+ if (experiences == null || experiences.isEmpty()) {
+ return;
+ }
+ for (Experience experience : experiences) {
+ Double score = resolveScore(experience);
+ String disclosureStrategy = experience.getDisclosureStrategy() != null
+ ? experience.getDisclosureStrategy().name() : null;
+ if (!isDirectGroundingEligible(disclosureStrategy, experience.getContent(), score)) {
+ continue;
+ }
+ DirectExperienceGrounding grounding = new DirectExperienceGrounding();
+ grounding.setId(experience.getId());
+ grounding.setExperienceType(experience.getType());
+ grounding.setTitle(experience.getName());
+ grounding.setDescription(experience.getDescription());
+ grounding.setContent(experience.getContent());
+ grounding.setDisclosureStrategy(disclosureStrategy);
+ grounding.setScore(score);
+ if (experience.getType() == ExperienceType.TOOL) {
+ grounding.setToolInvocationPath(toolInvocationClassifier.classify(experience));
+ grounding.setCallableToolName(toolInvocationClassifier.resolveCallableToolName(experience));
+ }
+ groundings.add(grounding);
+ }
+ }
+
+ public boolean isDirectGroundingEligible(ReadExpResponse response) {
+ if (response == null || !response.isFound()) {
+ return false;
+ }
+ return isDirectGroundingEligible(response.getDisclosureStrategy(), response.getContent(), response.getScore());
+ }
+
+ public DirectExperienceGrounding toDirectGrounding(ReadExpResponse response) {
+ if (!isDirectGroundingEligible(response)) {
+ return null;
+ }
+ DirectExperienceGrounding grounding = new DirectExperienceGrounding();
+ grounding.setId(response.getId());
+ grounding.setExperienceType(response.getExperienceType());
+ grounding.setTitle(response.getTitle());
+ grounding.setDescription(response.getDescription());
+ grounding.setContent(response.getContent());
+ grounding.setDisclosureStrategy(response.getDisclosureStrategy());
+ grounding.setScore(response.getScore());
+ grounding.setToolInvocationPath(response.getToolInvocationPath());
+ grounding.setCallableToolName(response.getCallableToolName());
+ return grounding;
+ }
+
+ public boolean isDirectGroundingEligible(DirectExperienceGrounding grounding) {
+ if (grounding == null) {
+ return false;
+ }
+ return isDirectGroundingEligible(grounding.getDisclosureStrategy(), grounding.getContent(), grounding.getScore());
+ }
+
+ private boolean isDirectGroundingEligible(String disclosureStrategy, String content, Double score) {
+ if (!DisclosureStrategy.DIRECT.name().equals(disclosureStrategy)) {
+ return false;
+ }
+ if (!StringUtils.hasText(content) || content.length() > DIRECT_CONTENT_MAX_LENGTH) {
+ return false;
+ }
+ return score == null || score >= DIRECT_CONFIDENCE_THRESHOLD;
+ }
+
+ private Double resolveScore(Experience experience) {
+ if (experience == null || experience.getMetadata() == null) {
+ return null;
+ }
+ return experience.getMetadata().getConfidence();
+ }
+
+ private int resolveActionLimit() {
+ return Math.max(1, properties.getMaxItemsPerQuery());
+ }
+
+ private int normalizeLimit(Integer requestedLimit, int fallback) {
+ if (requestedLimit == null || requestedLimit <= 0) {
+ return fallback;
+ }
+ return requestedLimit;
+ }
+
+ private String snippet(String content) {
+ if (!StringUtils.hasText(content)) {
+ return null;
+ }
+ if (content.length() <= SNIPPET_LENGTH) {
+ return content;
+ }
+ return content.substring(0, SNIPPET_LENGTH) + "...";
+ }
+}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperiencePrefetchHook.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperiencePrefetchHook.java
new file mode 100644
index 00000000..d5304212
--- /dev/null
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperiencePrefetchHook.java
@@ -0,0 +1,115 @@
+package com.alibaba.assistant.agent.extension.experience.disclosure;
+
+import com.alibaba.assistant.agent.common.constant.CodeactStateKeys;
+import com.alibaba.assistant.agent.extension.experience.config.ExperienceExtensionProperties;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.ExperienceCandidateCard;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.PrefetchedExperienceSnapshot;
+import com.alibaba.assistant.agent.extension.experience.model.ExperienceQueryContext;
+import com.alibaba.cloud.ai.graph.OverAllState;
+import com.alibaba.cloud.ai.graph.RunnableConfig;
+import com.alibaba.cloud.ai.graph.agent.Prioritized;
+import com.alibaba.cloud.ai.graph.agent.hook.AgentHook;
+import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
+import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
+import com.alibaba.cloud.ai.graph.agent.hook.JumpTo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Before-agent hook that performs first-round experience prefetch.
+ *
+ * On the first React round it writes lightweight grouped candidates into state so the
+ * prompt can disclose likely relevant experiences up front, and it seeds the allowlist
+ * of direct-callable React tools inferred from TOOL candidates.
+ */
+@HookPositions(HookPosition.BEFORE_AGENT)
+public class ExperiencePrefetchHook extends AgentHook implements Prioritized {
+
+ private static final Logger log = LoggerFactory.getLogger(ExperiencePrefetchHook.class);
+
+ private static final int PREFETCH_HOOK_ORDER = 150;
+
+ private final ExperienceDisclosureService experienceDisclosureService;
+ private final ExperienceDisclosureContextResolver contextResolver;
+ private final ExperienceExtensionProperties properties;
+
+ public ExperiencePrefetchHook(ExperienceDisclosureService experienceDisclosureService,
+ ExperienceDisclosureContextResolver contextResolver,
+ ExperienceExtensionProperties properties) {
+ this.experienceDisclosureService = experienceDisclosureService;
+ this.contextResolver = contextResolver;
+ this.properties = properties;
+ }
+
+ @Override
+ public String getName() {
+ return "ExperiencePrefetchHook";
+ }
+
+ @Override
+ public int getOrder() {
+ return PREFETCH_HOOK_ORDER;
+ }
+
+ @Override
+ public List canJumpTo() {
+ return List.of();
+ }
+
+ @Override
+ public CompletableFuture> beforeAgent(OverAllState state, RunnableConfig config) {
+ String query = contextResolver.resolveQuery(state, config);
+ log.info("ExperiencePrefetchHook#beforeAgent - reason=开始预取经验, query={}, experienceEnabled={}",
+ query, properties.isEnabled());
+
+ PrefetchedExperienceSnapshot snapshot = new PrefetchedExperienceSnapshot();
+ snapshot.setQuery(query);
+ if (properties.isEnabled() && contextResolver.shouldPrefetch(state, config, query)) {
+ ExperienceQueryContext queryContext = contextResolver.buildQueryContext(state, config, query);
+ log.info("ExperiencePrefetchHook#beforeAgent - reason=执行预取, tenantId={}", queryContext.getTenantId());
+ snapshot = experienceDisclosureService.prefetch(query, queryContext);
+ log.info("ExperiencePrefetchHook#beforeAgent - reason=预取完成, status={}, " +
+ "commonCandidates={}, reactCandidates={}, toolCandidates={}, directGroundings={}",
+ snapshot.getStatus(),
+ snapshot.getCandidates().getCommonCandidates().size(),
+ snapshot.getCandidates().getReactCandidates().size(),
+ snapshot.getCandidates().getToolCandidates().size(),
+ snapshot.getDirectGroundings().size());
+ } else {
+ snapshot.setStatus(ExperienceDisclosurePayloads.PrefetchStatus.SKIPPED);
+ boolean shouldPrefetch = contextResolver.shouldPrefetch(state, config, query);
+ log.info("ExperiencePrefetchHook#beforeAgent - reason=跳过预取, " +
+ "experienceEnabled={}, shouldPrefetch={}, queryPresent={}",
+ properties.isEnabled(), shouldPrefetch, query != null);
+ }
+
+ Map updates = new LinkedHashMap<>();
+ updates.put(CodeactStateKeys.EXPERIENCE_PREFETCH_QUERY, snapshot.getQuery());
+ updates.put(CodeactStateKeys.EXPERIENCE_PREFETCH_STATUS, snapshot.getStatus().name());
+ updates.put(CodeactStateKeys.EXPERIENCE_PREFETCHED_CANDIDATES, snapshot.getCandidates());
+ updates.put(CodeactStateKeys.EXPERIENCE_DIRECT_GROUNDINGS, snapshot.getDirectGroundings());
+ updates.put(CodeactStateKeys.EXPERIENCE_DETAIL_CACHE, Map.of());
+ updates.put(CodeactStateKeys.EXPERIENCE_ALLOWED_REACT_TOOL_NAMES,
+ collectDirectToolNames(snapshot.getCandidates().getToolCandidates()));
+ return CompletableFuture.completedFuture(updates);
+ }
+
+ private List collectDirectToolNames(List toolCandidates) {
+ LinkedHashSet toolNames = new LinkedHashSet<>();
+ for (ExperienceCandidateCard toolCandidate : toolCandidates) {
+ if (toolCandidate.getToolInvocationPath() == ExperienceDisclosurePayloads.ToolInvocationPath.REACT_DIRECT) {
+ if (toolCandidate.getCallableToolName() != null && !toolCandidate.getCallableToolName().isBlank()) {
+ toolNames.add(toolCandidate.getCallableToolName());
+ }
+ }
+ }
+ return new ArrayList<>(toolNames);
+ }
+}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceRuntimeModelInterceptor.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceRuntimeModelInterceptor.java
new file mode 100644
index 00000000..047ead0d
--- /dev/null
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceRuntimeModelInterceptor.java
@@ -0,0 +1,163 @@
+package com.alibaba.assistant.agent.extension.experience.disclosure;
+
+import com.alibaba.assistant.agent.common.constant.CodeactStateKeys;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.ReadExpRequest;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.ReadExpResponse;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.SearchExpRequest;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.SearchExpResponse;
+import com.alibaba.assistant.agent.extension.experience.model.ExperienceQueryContext;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.cloud.ai.graph.OverAllState;
+import com.alibaba.cloud.ai.graph.RunnableConfig;
+import com.alibaba.cloud.ai.graph.agent.interceptor.ModelCallHandler;
+import com.alibaba.cloud.ai.graph.agent.interceptor.ModelInterceptor;
+import com.alibaba.cloud.ai.graph.agent.interceptor.ModelRequest;
+import com.alibaba.cloud.ai.graph.agent.interceptor.ModelResponse;
+import org.springframework.ai.chat.model.ToolContext;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.function.FunctionToolCallback;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiFunction;
+
+import static com.alibaba.cloud.ai.graph.agent.tools.ToolContextConstants.AGENT_CONFIG_CONTEXT_KEY;
+import static com.alibaba.cloud.ai.graph.agent.tools.ToolContextConstants.AGENT_STATE_CONTEXT_KEY;
+
+/**
+ * Exposes {@code search_exp}/{@code read_exp} and gates direct TOOL calls per request.
+ *
+ * This interceptor is the runtime bridge between the experience store and the React
+ * loop: it registers the disclosure tools and trims the visible direct-call tool set so
+ * only experiences disclosed in the current state can be invoked directly by the model.
+ */
+public class ExperienceRuntimeModelInterceptor extends ModelInterceptor {
+
+ public static final String SEARCH_EXP_TOOL_NAME = "search_exp";
+ public static final String READ_EXP_TOOL_NAME = "read_exp";
+
+ private final ExperienceDisclosureService experienceDisclosureService;
+ private final ExperienceDisclosureContextResolver contextResolver;
+ private final ExperienceToolInvocationClassifier toolInvocationClassifier;
+ private final List tools;
+
+ public ExperienceRuntimeModelInterceptor(ExperienceDisclosureService experienceDisclosureService,
+ ExperienceDisclosureContextResolver contextResolver,
+ ExperienceToolInvocationClassifier toolInvocationClassifier) {
+ this.experienceDisclosureService = experienceDisclosureService;
+ this.contextResolver = contextResolver;
+ this.toolInvocationClassifier = toolInvocationClassifier;
+ this.tools = List.of(buildSearchTool(), buildReadTool());
+ }
+
+ @Override
+ public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) {
+ if (request.getTools() == null || request.getTools().isEmpty()) {
+ return handler.call(request);
+ }
+ Set allowedDirectToolNames = extractAllowedDirectToolNames(request.getContext());
+ List filteredTools = new ArrayList<>();
+ for (String toolName : request.getTools()) {
+ if (!isReactDirectFunctionTool(toolName) || allowedDirectToolNames.contains(toolName)) {
+ filteredTools.add(toolName);
+ }
+ }
+ ModelRequest filteredRequest = ModelRequest.builder(request)
+ .tools(filteredTools)
+ .build();
+ return handler.call(filteredRequest);
+ }
+
+ @Override
+ public String getName() {
+ return "ExperienceRuntimeModelInterceptor";
+ }
+
+ @Override
+ public List getTools() {
+ return tools;
+ }
+
+ private ToolCallback buildSearchTool() {
+ BiFunction function = (request, toolContext) -> {
+ OverAllState state = extractState(toolContext);
+ RunnableConfig config = extractConfig(toolContext);
+ String query = request != null ? request.getQuery() : null;
+ if (!StringUtils.hasText(query)) {
+ query = contextResolver.resolveQuery(state, config);
+ }
+ ExperienceQueryContext queryContext = contextResolver.buildQueryContext(state, config, query);
+ SearchExpResponse response = experienceDisclosureService.search(
+ query,
+ request != null ? request.getCommonLimit() : null,
+ request != null ? request.getReactLimit() : null,
+ request != null ? request.getToolLimit() : null,
+ queryContext
+ );
+ return JSON.toJSONString(response);
+ };
+ return FunctionToolCallback.builder(SEARCH_EXP_TOOL_NAME, function)
+ .description("按关键词搜索经验候选,返回 COMMON(概念术语)、REACT(工作流策略)、TOOL(工具能力)三类分组结果。")
+ .inputType(SearchExpRequest.class)
+ .build();
+ }
+
+ private ToolCallback buildReadTool() {
+ BiFunction function = (request, toolContext) -> {
+ ReadExpResponse response = experienceDisclosureService.read(request != null ? request.getId() : null);
+ return JSON.toJSONString(response);
+ };
+ return FunctionToolCallback.builder(READ_EXP_TOOL_NAME, function)
+ .description("根据经验 id 读取完整详情,包括内容正文、关联工件、调用路径等。用于获取 PROGRESSIVE 候选的完整内容。")
+ .inputType(ReadExpRequest.class)
+ .build();
+ }
+
+ private Set extractAllowedDirectToolNames(Map context) {
+ if (context == null) {
+ return Set.of();
+ }
+ Object value = context.get(CodeactStateKeys.EXPERIENCE_ALLOWED_REACT_TOOL_NAMES);
+ if (!(value instanceof List> list) || CollectionUtils.isEmpty(list)) {
+ return Set.of();
+ }
+ LinkedHashSet names = new LinkedHashSet<>();
+ for (Object item : list) {
+ if (item != null) {
+ names.add(String.valueOf(item));
+ }
+ }
+ return names;
+ }
+
+ private boolean isReactDirectFunctionTool(String toolName) {
+ if (!StringUtils.hasText(toolName)) {
+ return false;
+ }
+ if (toolName.contains(".")) {
+ return true;
+ }
+ return toolInvocationClassifier != null && toolInvocationClassifier.isReactDirectTool(toolName);
+ }
+
+ private OverAllState extractState(ToolContext toolContext) {
+ if (toolContext == null || toolContext.getContext() == null) {
+ return null;
+ }
+ Object state = toolContext.getContext().get(AGENT_STATE_CONTEXT_KEY);
+ return state instanceof OverAllState overAllState ? overAllState : null;
+ }
+
+ private RunnableConfig extractConfig(ToolContext toolContext) {
+ if (toolContext == null || toolContext.getContext() == null) {
+ return null;
+ }
+ Object config = toolContext.getContext().get(AGENT_CONFIG_CONTEXT_KEY);
+ return config instanceof RunnableConfig runnableConfig ? runnableConfig : null;
+ }
+}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceRuntimeToolStateInterceptor.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceRuntimeToolStateInterceptor.java
new file mode 100644
index 00000000..b11b4262
--- /dev/null
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceRuntimeToolStateInterceptor.java
@@ -0,0 +1,188 @@
+package com.alibaba.assistant.agent.extension.experience.disclosure;
+
+import com.alibaba.assistant.agent.common.constant.CodeactStateKeys;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.DirectExperienceGrounding;
+import com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.ReadExpResponse;
+import com.alibaba.cloud.ai.graph.OverAllState;
+import com.alibaba.cloud.ai.graph.agent.interceptor.ToolCallExecutionContext;
+import com.alibaba.cloud.ai.graph.agent.interceptor.ToolCallHandler;
+import com.alibaba.cloud.ai.graph.agent.interceptor.ToolCallRequest;
+import com.alibaba.cloud.ai.graph.agent.interceptor.ToolCallResponse;
+import com.alibaba.cloud.ai.graph.agent.interceptor.ToolInterceptor;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Persists disclosure tool results back into agent state.
+ *
+ * After {@code search_exp} or {@code read_exp} returns, this interceptor updates the
+ * current round's direct-tool allowlist and caches read details so later runtime stages
+ * can reuse the disclosed experience information without re-querying storage.
+ */
+public class ExperienceRuntimeToolStateInterceptor extends ToolInterceptor {
+
+ private static final int DIRECT_CONTENT_MAX_LENGTH = 500;
+ private static final double DIRECT_CONFIDENCE_THRESHOLD = 0.8D;
+
+ @Override
+ public String getName() {
+ return "ExperienceRuntimeToolStateInterceptor";
+ }
+
+ @Override
+ public ToolCallResponse interceptToolCall(ToolCallRequest request, ToolCallHandler handler) {
+ ToolCallResponse response = handler.call(request);
+ if (!ExperienceRuntimeModelInterceptor.SEARCH_EXP_TOOL_NAME.equals(request.getToolName())
+ && !ExperienceRuntimeModelInterceptor.READ_EXP_TOOL_NAME.equals(request.getToolName())) {
+ return response;
+ }
+
+ Optional executionContext = request.getExecutionContext();
+ if (executionContext.isEmpty()) {
+ return response;
+ }
+ OverAllState state = executionContext.get().state();
+ mergeDirectToolNames(state, response.getResult(), request.getToolName());
+ maybeCacheReadDetail(state, response.getResult(), request.getToolName());
+ mergeDirectGroundings(state, response.getResult(), request.getToolName());
+ return response;
+ }
+
+ private void mergeDirectToolNames(OverAllState state, String json, String toolName) {
+ LinkedHashSet merged = new LinkedHashSet<>();
+ Object existing = state.value(CodeactStateKeys.EXPERIENCE_ALLOWED_REACT_TOOL_NAMES).orElse(null);
+ if (existing instanceof List> list) {
+ for (Object item : list) {
+ if (item != null) {
+ merged.add(String.valueOf(item));
+ }
+ }
+ }
+
+ if (ExperienceRuntimeModelInterceptor.SEARCH_EXP_TOOL_NAME.equals(toolName)) {
+ JSONObject root = JSON.parseObject(json);
+ JSONObject candidates = root.getJSONObject("candidates");
+ if (candidates != null) {
+ com.alibaba.fastjson.JSONArray toolCandidatesArray = candidates.getJSONArray("toolCandidates");
+ if (toolCandidatesArray != null) {
+ List toolCandidates = toolCandidatesArray.toJavaList(JSONObject.class);
+ for (JSONObject candidate : toolCandidates) {
+ String invocationPath = candidate.getString("toolInvocationPath");
+ if (isReactDirect(invocationPath)) {
+ String callableToolName = candidate.getString("callableToolName");
+ if (callableToolName != null && !callableToolName.isBlank()) {
+ merged.add(callableToolName);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (ExperienceRuntimeModelInterceptor.READ_EXP_TOOL_NAME.equals(toolName)) {
+ JSONObject root = JSON.parseObject(json);
+ String invocationPath = root.getString("toolInvocationPath");
+ if (isReactDirect(invocationPath)) {
+ String callableToolName = root.getString("callableToolName");
+ if (callableToolName != null && !callableToolName.isBlank()) {
+ merged.add(callableToolName);
+ }
+ }
+ }
+
+ state.updateState(Map.of(CodeactStateKeys.EXPERIENCE_ALLOWED_REACT_TOOL_NAMES, new ArrayList<>(merged)));
+ }
+
+ private void maybeCacheReadDetail(OverAllState state, String json, String toolName) {
+ if (!ExperienceRuntimeModelInterceptor.READ_EXP_TOOL_NAME.equals(toolName)) {
+ return;
+ }
+ ReadExpResponse response = JSON.parseObject(json, ReadExpResponse.class);
+ if (!response.isFound() || response.getId() == null) {
+ return;
+ }
+
+ Map cache = new LinkedHashMap<>();
+ Object existing = state.value(CodeactStateKeys.EXPERIENCE_DETAIL_CACHE).orElse(null);
+ if (existing instanceof Map, ?> existingMap) {
+ for (Map.Entry, ?> entry : existingMap.entrySet()) {
+ if (entry.getKey() != null) {
+ cache.put(String.valueOf(entry.getKey()), entry.getValue());
+ }
+ }
+ }
+ cache.put(response.getId(), response);
+ state.updateState(Map.of(CodeactStateKeys.EXPERIENCE_DETAIL_CACHE, cache));
+ }
+
+ private void mergeDirectGroundings(OverAllState state, String json, String toolName) {
+ LinkedHashMap merged = new LinkedHashMap<>();
+ Object existing = state.value(CodeactStateKeys.EXPERIENCE_DIRECT_GROUNDINGS).orElse(null);
+ if (existing instanceof List> list) {
+ for (Object item : list) {
+ if (item instanceof DirectExperienceGrounding grounding && grounding.getId() != null) {
+ merged.put(grounding.getId(), grounding);
+ }
+ }
+ }
+
+ if (ExperienceRuntimeModelInterceptor.SEARCH_EXP_TOOL_NAME.equals(toolName)) {
+ JSONObject root = JSON.parseObject(json);
+ com.alibaba.fastjson.JSONArray directGroundings = root.getJSONArray("directGroundings");
+ if (directGroundings != null) {
+ List groundings =
+ directGroundings.toJavaList(DirectExperienceGrounding.class);
+ for (DirectExperienceGrounding grounding : groundings) {
+ if (grounding != null && grounding.getId() != null) {
+ merged.put(grounding.getId(), grounding);
+ }
+ }
+ }
+ }
+
+ if (ExperienceRuntimeModelInterceptor.READ_EXP_TOOL_NAME.equals(toolName)) {
+ ReadExpResponse response = JSON.parseObject(json, ReadExpResponse.class);
+ if (isDirectGroundingEligible(response)) {
+ DirectExperienceGrounding grounding = new DirectExperienceGrounding();
+ grounding.setId(response.getId());
+ grounding.setExperienceType(response.getExperienceType());
+ grounding.setTitle(response.getTitle());
+ grounding.setDescription(response.getDescription());
+ grounding.setContent(response.getContent());
+ grounding.setDisclosureStrategy(response.getDisclosureStrategy());
+ grounding.setScore(response.getScore());
+ grounding.setToolInvocationPath(response.getToolInvocationPath());
+ grounding.setCallableToolName(response.getCallableToolName());
+ merged.put(grounding.getId(), grounding);
+ }
+ }
+
+ state.updateState(Map.of(CodeactStateKeys.EXPERIENCE_DIRECT_GROUNDINGS, new ArrayList<>(merged.values())));
+ }
+
+ private boolean isReactDirect(String invocationPath) {
+ return "REACT_DIRECT".equals(invocationPath);
+ }
+
+ private boolean isDirectGroundingEligible(ReadExpResponse response) {
+ if (response == null || !response.isFound()) {
+ return false;
+ }
+ if (!"DIRECT".equals(response.getDisclosureStrategy())) {
+ return false;
+ }
+ String content = response.getContent();
+ if (content == null || content.isBlank() || content.length() > DIRECT_CONTENT_MAX_LENGTH) {
+ return false;
+ }
+ Double score = response.getScore();
+ return score == null || score >= DIRECT_CONFIDENCE_THRESHOLD;
+ }
+}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceToolInvocationClassifier.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceToolInvocationClassifier.java
new file mode 100644
index 00000000..f27fc4db
--- /dev/null
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/disclosure/ExperienceToolInvocationClassifier.java
@@ -0,0 +1,99 @@
+package com.alibaba.assistant.agent.extension.experience.disclosure;
+
+import com.alibaba.assistant.agent.extension.experience.config.ExperienceExtensionProperties;
+import com.alibaba.assistant.agent.extension.experience.model.Experience;
+import com.alibaba.assistant.agent.extension.experience.model.ExperienceArtifact;
+import org.springframework.util.StringUtils;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import static com.alibaba.assistant.agent.extension.experience.disclosure.ExperienceDisclosurePayloads.ToolInvocationPath;
+
+/**
+ * Classifies TOOL experiences into React-direct or code-only invocation paths.
+ *
+ * The classifier reads TOOL metadata plus the configured allowlist and tells the
+ * runtime whether a matched TOOL experience may be surfaced as a direct function call
+ * or must stay on the write-code/execute-code path.
+ */
+public class ExperienceToolInvocationClassifier {
+
+ public static final String TOOL_INVOCATION_PATH_PROPERTY = "toolInvocationPath";
+
+ private final ExperienceExtensionProperties properties;
+
+ public ExperienceToolInvocationClassifier(ExperienceExtensionProperties properties) {
+ this.properties = properties;
+ }
+
+ public ToolInvocationPath classify(Experience experience) {
+ ToolInvocationPath explicitInvocationPath = resolveExplicitInvocationPath(experience);
+ if (explicitInvocationPath != null) {
+ return explicitInvocationPath;
+ }
+ String toolName = resolveCallableToolName(experience);
+ if (!StringUtils.hasText(toolName)) {
+ return ToolInvocationPath.CODE_ONLY;
+ }
+ return isReactDirectTool(toolName) ? ToolInvocationPath.REACT_DIRECT : ToolInvocationPath.CODE_ONLY;
+ }
+
+ public boolean isReactDirectTool(String toolName) {
+ if (!StringUtils.hasText(toolName)) {
+ return false;
+ }
+ Set configuredNames = getConfiguredReactDirectToolNames();
+ if (configuredNames.contains(toolName)) {
+ return true;
+ }
+ int lastDot = toolName.lastIndexOf('.');
+ if (lastDot >= 0 && lastDot + 1 < toolName.length()) {
+ return configuredNames.contains(toolName.substring(lastDot + 1));
+ }
+ return false;
+ }
+
+ public Set getConfiguredReactDirectToolNames() {
+ return new LinkedHashSet<>(properties.getReactDirectToolNames());
+ }
+
+ public String resolveCallableToolName(Experience experience) {
+ if (experience == null) {
+ return null;
+ }
+ ExperienceArtifact artifact = experience.getArtifact();
+ if (artifact != null && artifact.getTool() != null && StringUtils.hasText(artifact.getTool().getCodeactToolName())) {
+ return artifact.getTool().getCodeactToolName();
+ }
+ List associatedTools = experience.getAssociatedTools();
+ if (associatedTools != null) {
+ for (String associatedTool : associatedTools) {
+ if (StringUtils.hasText(associatedTool)) {
+ return associatedTool;
+ }
+ }
+ }
+ return null;
+ }
+
+ private ToolInvocationPath resolveExplicitInvocationPath(Experience experience) {
+ if (experience == null || experience.getMetadata() == null) {
+ return null;
+ }
+ Object configuredValue = experience.getMetadata().getProperty(TOOL_INVOCATION_PATH_PROPERTY);
+ if (configuredValue == null) {
+ return null;
+ }
+ String invocationPath = String.valueOf(configuredValue).trim();
+ if (!StringUtils.hasText(invocationPath)) {
+ return null;
+ }
+ try {
+ return ToolInvocationPath.valueOf(invocationPath);
+ } catch (IllegalArgumentException ignored) {
+ return null;
+ }
+ }
+}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/fastintent/CodeFastIntentSupport.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/fastintent/CodeFastIntentSupport.java
deleted file mode 100644
index 2a8904c9..00000000
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/fastintent/CodeFastIntentSupport.java
+++ /dev/null
@@ -1,167 +0,0 @@
-package com.alibaba.assistant.agent.extension.experience.fastintent;
-
-import com.alibaba.assistant.agent.common.constant.HookPriorityConstants;
-import com.alibaba.assistant.agent.extension.experience.config.ExperienceExtensionProperties;
-import com.alibaba.assistant.agent.extension.experience.model.Experience;
-import com.alibaba.assistant.agent.extension.experience.model.ExperienceArtifact;
-import com.alibaba.assistant.agent.extension.experience.model.ExperienceQuery;
-import com.alibaba.assistant.agent.extension.experience.model.ExperienceQueryContext;
-import com.alibaba.assistant.agent.extension.experience.model.ExperienceType;
-import com.alibaba.assistant.agent.extension.experience.model.FastIntentConfig;
-import com.alibaba.assistant.agent.extension.experience.spi.ExperienceProvider;
-import com.alibaba.cloud.ai.graph.OverAllState;
-import com.alibaba.cloud.ai.graph.RunnableConfig;
-import com.alibaba.cloud.ai.graph.agent.tools.ToolContextConstants;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.ai.chat.messages.Message;
-import org.springframework.ai.chat.model.ToolContext;
-import org.springframework.util.StringUtils;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-/**
- * CodeFastIntentSupport - CODE FastIntent 命中判断与产物提取(只负责“命中选择”,不负责注册/执行代码)
- *
- * 设计目标:让 write_code / write_condition_code 等多个入口复用同一套 fast-intent 命中逻辑,避免逻辑重复
- */
-public class CodeFastIntentSupport {
-
- private static final Logger log = LoggerFactory.getLogger(CodeFastIntentSupport.class);
-
- private final ExperienceProvider experienceProvider;
- private final ExperienceExtensionProperties properties;
- private final FastIntentService fastIntentService;
-
- public CodeFastIntentSupport(ExperienceProvider experienceProvider,
- ExperienceExtensionProperties properties,
- FastIntentService fastIntentService) {
- this.experienceProvider = experienceProvider;
- this.properties = properties;
- this.fastIntentService = fastIntentService;
- }
-
- public Optional tryHit(ToolContext toolContext, Map toolRequest, String language) {
- try {
- if (experienceProvider == null || properties == null || fastIntentService == null) {
- return Optional.empty();
- }
- if (!properties.isEnabled()
- || !properties.isCodeExperienceEnabled()
- || !properties.isFastIntentEnabled()
- || !properties.isFastIntentCodeEnabled()) {
- return Optional.empty();
- }
-
- OverAllState state = (OverAllState) toolContext.getContext()
- .get(ToolContextConstants.AGENT_STATE_CONTEXT_KEY);
- RunnableConfig config = (RunnableConfig) toolContext.getContext()
- .get(ToolContextConstants.AGENT_CONFIG_CONTEXT_KEY);
-
- if (isFastIntentActive(state)) {
- log.info("CodeFastIntentSupport#tryHit - reason=skip CODE fast-intent because REACT fast-intent is already active");
- return Optional.empty();
- }
-
- String input = state != null ? state.value("input", String.class).orElse(null) : null;
- Map md = config != null ? config.metadata().orElse(Map.of()) : Map.of();
- @SuppressWarnings("unchecked")
- List messages = state != null ? state.value("messages", List.class).orElse(List.of()) : List.of();
-
- FastIntentContext ctx = new FastIntentContext(input, messages, md, state, toolRequest);
-
- ExperienceQueryContext queryContext = buildQueryContext(state, config, language, input);
- // 使用 write_code 的 description 参数作为搜索文本,提升向量搜索召回率
- String description = toolRequest != null ? (String) toolRequest.get("description") : null;
- if (StringUtils.hasText(description)) {
- queryContext.setUserQuery(description);
- }
- ExperienceQuery query = new ExperienceQuery(ExperienceType.CODE);
- query.setText(description);
- query.setLimit(Math.max(40, properties.getMaxItemsPerQuery()));
-
- log.info("CodeFastIntentSupport#tryHit - reason=querying code experiences, text={}, limit={}",
- description != null ? (description.length() > 50 ? description.substring(0, 50) + "..." : description) : "null",
- query.getLimit());
-
- List candidates = experienceProvider.query(query, queryContext);
- Optional bestOpt = fastIntentService.selectBestMatch(candidates, ctx);
- if (bestOpt.isEmpty()) {
- return Optional.empty();
- }
-
- Experience best = bestOpt.get();
- ExperienceArtifact.CodeArtifact codeArtifact = best.getArtifact() != null ? best.getArtifact().getCode() : null;
- if (codeArtifact == null || codeArtifact.getCode() == null || codeArtifact.getCode().isBlank()) {
- return Optional.empty();
- }
-
- return Optional.of(new Hit(best, codeArtifact));
-
- } catch (Exception e) {
- // fail-open: hit 判断失败则回退到正常 LLM 流程
- log.warn("CodeFastIntentSupport#tryHit - reason=fast-intent failed, fallback to normal flow, error={}", e.getMessage());
- return Optional.empty();
- }
- }
-
- @SuppressWarnings("unchecked")
- private boolean isFastIntentActive(OverAllState state) {
- if (state == null) {
- return false;
- }
- Object fastIntentObj = state.value(HookPriorityConstants.FAST_INTENT_STATE_KEY).orElse(null);
- if (!(fastIntentObj instanceof Map, ?> fastIntentState)) {
- return false;
- }
- Object hit = ((Map) fastIntentState).get("hit");
- return Boolean.TRUE.equals(hit);
- }
-
- private ExperienceQueryContext buildQueryContext(OverAllState state, RunnableConfig config, String language, String userQuery) {
- ExperienceQueryContext queryContext = new ExperienceQueryContext();
- // 关键修复:设置userQuery,用于向量搜索
- if (userQuery != null && !userQuery.isBlank()) {
- queryContext.setUserQuery(userQuery);
- }
- if (state != null) {
- state.value("user_id", String.class).ifPresent(queryContext::setUserId);
- state.value("project_id", String.class).ifPresent(queryContext::setProjectId);
- state.value("task_type", String.class).ifPresent(queryContext::setTaskType);
- }
- if (config != null) {
- config.metadata("agent_name").ifPresent(name -> queryContext.setAgentName(String.valueOf(name)));
- config.metadata("task_type").ifPresent(type -> queryContext.setTaskType(String.valueOf(type)));
- }
- if (language != null && !language.isBlank()) {
- queryContext.setLanguage(language);
- }
- return queryContext;
- }
-
- public static FastIntentConfig.FastIntentFallback getOnRegisterFallback(Experience experience) {
- if (experience == null || experience.getFastIntentConfig() == null || experience.getFastIntentConfig().getOnMatch() == null) {
- return FastIntentConfig.FastIntentFallback.REFERENCE_ONLY;
- }
- FastIntentConfig.FastIntentFallback fb = experience.getFastIntentConfig().getOnMatch().getFallback();
- return fb != null ? fb : FastIntentConfig.FastIntentFallback.REFERENCE_ONLY;
- }
-
- public static Map toolReqOf(String description, String functionName, List parameters) {
- Map toolReq = new HashMap<>();
- toolReq.put("description", description);
- toolReq.put("functionName", functionName);
- toolReq.put("parameters", parameters != null ? parameters : List.of());
- return toolReq;
- }
-
- public record Hit(Experience experience, ExperienceArtifact.CodeArtifact codeArtifact) {
- public String code() {
- return codeArtifact != null ? codeArtifact.getCode() : null;
- }
- }
-}
-
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/hook/CommonSenseExperienceModelHook.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/hook/CommonSenseExperienceModelHook.java
deleted file mode 100644
index 728b910b..00000000
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/hook/CommonSenseExperienceModelHook.java
+++ /dev/null
@@ -1,244 +0,0 @@
-package com.alibaba.assistant.agent.extension.experience.hook;
-
-import com.alibaba.assistant.agent.extension.experience.config.ExperienceExtensionProperties;
-import com.alibaba.assistant.agent.extension.experience.model.Experience;
-import com.alibaba.assistant.agent.extension.experience.model.ExperienceQuery;
-import com.alibaba.assistant.agent.extension.experience.model.ExperienceQueryContext;
-import com.alibaba.assistant.agent.extension.experience.model.ExperienceType;
-import com.alibaba.assistant.agent.extension.experience.spi.ExperienceProvider;
-import com.alibaba.cloud.ai.graph.OverAllState;
-import com.alibaba.cloud.ai.graph.RunnableConfig;
-import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
-import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
-import com.alibaba.cloud.ai.graph.agent.hook.JumpTo;
-import com.alibaba.cloud.ai.graph.agent.hook.ModelHook;
-import com.alibaba.fastjson.JSON;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.ai.chat.messages.AssistantMessage;
-import org.springframework.ai.chat.messages.Message;
-import org.springframework.ai.chat.messages.ToolResponseMessage;
-import org.springframework.util.CollectionUtils;
-import org.springframework.util.StringUtils;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
-
-/**
- * 常识经验提示模型Hook
- * 参考记忆模块实现,在BEFORE_MODEL阶段直接修改messages列表,注入常识经验
- *
- * 核心设计:
- * 1. 从ExperienceProvider查询COMMON类型的经验
- * 2. 格式化为SystemMessage内容
- * 3. 查找现有SystemMessage并追加,或添加新的SystemMessage
- * 4. 通过返回Map.of("messages", newMessages)更新OverAllState
- *
- * @author Assistant Agent Team
- */
-@HookPositions(HookPosition.BEFORE_MODEL)
-public class CommonSenseExperienceModelHook extends ModelHook {
-
- private static final Logger log = LoggerFactory.getLogger(CommonSenseExperienceModelHook.class);
-
- private final ExperienceProvider experienceProvider;
- private final ExperienceExtensionProperties properties;
-
- public CommonSenseExperienceModelHook(ExperienceProvider experienceProvider,
- ExperienceExtensionProperties properties) {
- this.experienceProvider = experienceProvider;
- this.properties = properties;
- }
-
- @Override
- public String getName() {
- return "CommonSenseExperienceModelHook";
- }
-
- @Override
- public List canJumpTo() {
- return List.of();
- }
-
- @Override
- @SuppressWarnings("unchecked")
- public CompletableFuture> beforeModel(OverAllState state, RunnableConfig config) {
- log.info("CommonSenseExperienceModelHook#beforeModel - reason=开始注入常识经验");
-
- try {
- // 检查模块是否启用
- if (!properties.isEnabled() || !properties.isCommonExperienceEnabled()) {
- log.info("CommonSenseExperienceModelHook#beforeModel - reason=常识经验模块未启用,跳过");
- return CompletableFuture.completedFuture(Map.of());
- }
-
- // 获取用户输入,用于向量搜索
- String userInput = state != null ? state.value("input", String.class).orElse(null) : null;
-
- // 构造查询上下文
- ExperienceQueryContext context = buildQueryContext(state, config, userInput);
-
- // 查询常识经验
- ExperienceQuery query = new ExperienceQuery(ExperienceType.COMMON);
- query.setLimit(Math.min(properties.getMaxItemsPerQuery(), 3));
- // 关键修复:设置查询文本,用于向量搜索
- if (userInput != null && !userInput.isBlank()) {
- query.setText(userInput);
- }
-
- List experiences = experienceProvider.query(query, context);
-
- if (CollectionUtils.isEmpty(experiences)) {
- log.info("CommonSenseExperienceModelHook#beforeModel - reason=未找到常识经验");
- return CompletableFuture.completedFuture(Map.of());
- }
-
- log.info("CommonSenseExperienceModelHook#beforeModel - reason=找到常识经验: {}", JSON.toJSONString(experiences));
-
- // 🔥 核心:参考记忆模块,直接修改messages列表
- CompletableFuture> result = injectExperienceToMessages(state, experiences);
-
- // 添加日志确认返回值
- result.thenAccept(updates -> {
- log.info("CommonSenseExperienceModelHook#beforeModel - reason=Hook执行完成,返回updates: keys={}, messagesCount={}",
- updates.keySet(),
- updates.containsKey("messages") ? ((List>)updates.get("messages")).size() : "N/A");
- });
-
- return result;
-
- } catch (Exception e) {
- log.error("CommonSenseExperienceModelHook#beforeModel - reason=注入常识经验失败", e);
- return CompletableFuture.completedFuture(Map.of());
- }
- }
-
- /**
- * 🔥 核心方法:注入常识经验到messages
- * 使用 AssistantMessage + ToolResponseMessage 配对方式
- */
- @SuppressWarnings("unchecked")
- private CompletableFuture> injectExperienceToMessages(OverAllState state, List experiences) {
- log.info("CommonSenseExperienceModelHook#injectExperienceToMessages - reason=开始处理messages");
-
- try {
- Optional messagesOpt = state.value("messages");
- if (messagesOpt.isEmpty()) {
- log.warn("CommonSenseExperienceModelHook#injectExperienceToMessages - reason=state中没有messages,跳过");
- return CompletableFuture.completedFuture(Map.of());
- }
-
- List messages = (List) messagesOpt.get();
- log.debug("CommonSenseExperienceModelHook#injectExperienceToMessages - reason=当前messages数量={}", messages.size());
-
- // 🔥 检查是否已经注入过常识经验
- for (Message msg : messages) {
- if (msg instanceof ToolResponseMessage toolMsg) {
- for (ToolResponseMessage.ToolResponse response : toolMsg.getResponses()) {
- if ("common_sense_injection".equals(response.name())) {
- log.info("CommonSenseExperienceModelHook#injectExperienceToMessages - reason=检测到已注入常识经验,跳过");
- return CompletableFuture.completedFuture(Map.of());
- }
- }
- }
- }
-
- // 构建经验内容
- String experienceContent = buildExperienceContent(experiences);
- log.debug("CommonSenseExperienceModelHook#injectExperienceToMessages - reason=经验内容构建完成,长度={}", experienceContent.length());
-
- // 🔥 构造 AssistantMessage + ToolResponseMessage 配对
- String toolCallId = "common_sense_" + UUID.randomUUID().toString().substring(0, 8);
-
- // 1. AssistantMessage with toolCall
- AssistantMessage assistantMessage = AssistantMessage.builder()
- .toolCalls(List.of(
- new AssistantMessage.ToolCall(
- toolCallId,
- "function",
- "common_sense_injection",
- "{}" // 空参数
- )
- ))
- .build();
-
- // 2. ToolResponseMessage with response
- ToolResponseMessage.ToolResponse toolResponse = new ToolResponseMessage.ToolResponse(
- toolCallId,
- "common_sense_injection",
- experienceContent
- );
-
- ToolResponseMessage toolResponseMessage = ToolResponseMessage.builder()
- .responses(List.of(toolResponse))
- .build();
-
- log.info("CommonSenseExperienceModelHook#injectExperienceToMessages - reason=准备注入常识经验(AssistantMessage + ToolResponseMessage)");
-
- // 🔥 返回配对的两条消息
- Map updates = Map.of("messages", List.of(assistantMessage, toolResponseMessage));
- log.info("CommonSenseExperienceModelHook#injectExperienceToMessages - reason=准备返回updates,keys={}", updates.keySet());
-
- return CompletableFuture.completedFuture(updates);
-
- } catch (Exception e) {
- log.error("CommonSenseExperienceModelHook#injectExperienceToMessages - reason=修改messages失败", e);
- return CompletableFuture.completedFuture(Map.of());
- }
- }
-
- /**
- * 构建常识经验内容,格式化为SystemMessage文本
- */
- private String buildExperienceContent(List experiences) {
- StringBuilder content = new StringBuilder();
-
- content.append("=== 补充的常识 ===\n\n");
-
- for (Experience experience : experiences) {
- // 通用格式化,不做特殊判断
- content.append("📋 ").append(experience.getTitle()).append("\n");
-
- if (StringUtils.hasText(experience.getContent())) {
- String trimmedContent = experience.getContent();
- if (trimmedContent.length() > properties.getMaxContentLength()) {
- trimmedContent = trimmedContent.substring(0, properties.getMaxContentLength()) + "...";
- }
- content.append(trimmedContent).append("\n\n");
- }
- }
-
- content.append("请在回答中遵循以上规范。");
-
- return content.toString();
- }
-
- /**
- * 从State和Config构造查询上下文
- */
- private ExperienceQueryContext buildQueryContext(OverAllState state, RunnableConfig config, String userQuery) {
- ExperienceQueryContext context = new ExperienceQueryContext();
-
- // 关键修复:设置userQuery,用于向量搜索
- if (userQuery != null && !userQuery.isBlank()) {
- context.setUserQuery(userQuery);
- }
-
- // 从state提取上下文
- if (state != null) {
- state.value("user_id", String.class).ifPresent(context::setUserId);
- state.value("project_id", String.class).ifPresent(context::setProjectId);
- }
-
- // 从config提取Agent信息
- if (config != null) {
- config.metadata("user_id").ifPresent(id -> context.setUserId(id.toString()));
- config.metadata("agent_name").ifPresent(name -> context.setAgentName(name.toString()));
- }
-
- return context;
- }
-}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/hook/FastIntentReactHook.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/hook/FastIntentReactHook.java
index 2229f87f..feb69272 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/hook/FastIntentReactHook.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/hook/FastIntentReactHook.java
@@ -243,13 +243,12 @@ private ExperienceQueryContext buildQueryContext(OverAllState state, RunnableCon
context.setUserQuery(userQuery);
}
if (state != null) {
- state.value("user_id", String.class).ifPresent(context::setUserId);
- state.value("project_id", String.class).ifPresent(context::setProjectId);
- state.value("task_type", String.class).ifPresent(context::setTaskType);
+ state.value("tenant_id", String.class).ifPresent(context::setTenantId);
+ state.value("user_id", String.class).ifPresent(context::setTenantId);
}
if (config != null) {
- config.metadata("agent_name").ifPresent(name -> context.setAgentName(name.toString()));
- config.metadata("task_type").ifPresent(type -> context.setTaskType(type.toString()));
+ config.metadata("tenant_id").ifPresent(id -> context.setTenantId(String.valueOf(id)));
+ config.metadata("user_id").ifPresent(id -> context.setTenantId(String.valueOf(id)));
}
return context;
}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/hook/ReactExperienceAgentHook.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/hook/ReactExperienceAgentHook.java
deleted file mode 100644
index 9b3ecc7e..00000000
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/hook/ReactExperienceAgentHook.java
+++ /dev/null
@@ -1,245 +0,0 @@
-package com.alibaba.assistant.agent.extension.experience.hook;
-
-import com.alibaba.assistant.agent.common.constant.HookPriorityConstants;
-import com.alibaba.cloud.ai.graph.agent.Prioritized;
-import com.alibaba.assistant.agent.extension.experience.config.ExperienceExtensionProperties;
-import com.alibaba.assistant.agent.extension.experience.model.Experience;
-import com.alibaba.assistant.agent.extension.experience.model.ExperienceQuery;
-import com.alibaba.assistant.agent.extension.experience.model.ExperienceQueryContext;
-import com.alibaba.assistant.agent.extension.experience.model.ExperienceType;
-import com.alibaba.assistant.agent.extension.experience.spi.ExperienceProvider;
-import com.alibaba.cloud.ai.graph.OverAllState;
-import com.alibaba.cloud.ai.graph.RunnableConfig;
-import com.alibaba.cloud.ai.graph.agent.hook.AgentHook;
-import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
-import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
-import com.alibaba.cloud.ai.graph.agent.hook.JumpTo;
-import com.alibaba.fastjson.JSON;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.ai.chat.messages.AssistantMessage;
-import org.springframework.ai.chat.messages.Message;
-import org.springframework.ai.chat.messages.ToolResponseMessage;
-import org.springframework.ai.chat.messages.UserMessage;
-import org.springframework.util.CollectionUtils;
-import org.springframework.util.StringUtils;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
-
-/**
- * React经验Agent Hook
- * 在BEFORE_AGENT阶段注入React行为策略经验
- *
- * 核心设计:
- * 1. 在Agent启动前查询React策略经验
- * 2. 将策略经验注入到初始messages中
- * 3. 影响Agent的整体行为模式
- *
- * 优先级:{@link HookPriorityConstants#REACT_EXPERIENCE_HOOK}(20),
- * 确保在快速意图 Hook(50)之前执行。
- *
- * @author Assistant Agent Team
- */
-@HookPositions(HookPosition.BEFORE_AGENT)
-public class ReactExperienceAgentHook extends AgentHook implements Prioritized {
-
- private static final Logger log = LoggerFactory.getLogger(ReactExperienceAgentHook.class);
-
- private final ExperienceProvider experienceProvider;
- private final ExperienceExtensionProperties properties;
-
- public ReactExperienceAgentHook(ExperienceProvider experienceProvider,
- ExperienceExtensionProperties properties) {
- this.experienceProvider = experienceProvider;
- this.properties = properties;
- }
-
- @Override
- public String getName() {
- return "ReactExperienceAgentHook";
- }
-
- @Override
- public int getOrder() {
- return HookPriorityConstants.REACT_EXPERIENCE_HOOK;
- }
-
- @Override
- public List canJumpTo() {
- return List.of();
- }
-
- @Override
- @SuppressWarnings("unchecked")
- public CompletableFuture> beforeAgent(OverAllState state, RunnableConfig config) {
- log.info("ReactExperienceAgentHook#beforeAgent - reason=开始注入React策略经验");
-
- try {
- // 检查模块是否启用
- if (!properties.isEnabled() || !properties.isReactExperienceEnabled()) {
- log.info("ReactExperienceAgentHook#beforeAgent - reason=React经验模块未启用,跳过");
- return CompletableFuture.completedFuture(Map.of());
- }
-
- // 获取用户输入,用于向量搜索
- String userInput = state != null ? state.value("input", String.class).orElse(null) : null;
-
- // 构造查询上下文
- ExperienceQueryContext context = buildQueryContext(state, config, userInput);
-
- // 查询React经验
- ExperienceQuery query = new ExperienceQuery(ExperienceType.REACT);
- query.setLimit(Math.min(properties.getMaxItemsPerQuery(), 30));
- // 关键修复:设置查询文本,用于向量搜索
- if (StringUtils.hasText(userInput)) {
- query.setText(userInput);
- }
-
- List experiences = experienceProvider.query(query, context);
-
- if (CollectionUtils.isEmpty(experiences)) {
- log.info("ReactExperienceAgentHook#beforeAgent - reason=未找到React策略经验");
- return CompletableFuture.completedFuture(Map.of());
- }
-
- log.info("ReactExperienceAgentHook#beforeAgent - reason=找到React策略经验: {}", JSON.toJSONString(experiences));
-
- // 🔥 核心:注入策略经验到messages
- return injectReactExperienceToMessages(state, experiences);
-
- } catch (Exception e) {
- log.error("ReactExperienceAgentHook#beforeAgent - reason=注入React经验失败", e);
- return CompletableFuture.completedFuture(Map.of());
- }
- }
-
- /**
- * 🔥 核心方法:注入React策略经验到messages
- * 使用 AssistantMessage + ToolResponseMessage 配对方式
- */
- @SuppressWarnings("unchecked")
- private CompletableFuture> injectReactExperienceToMessages(OverAllState state, List experiences) {
- log.info("ReactExperienceAgentHook#injectReactExperienceToMessages - reason=开始处理messages");
-
- try {
- Optional messagesOpt = state.value("messages");
- if (messagesOpt.isEmpty()) {
- log.warn("ReactExperienceAgentHook#injectReactExperienceToMessages - reason=state中没有messages,跳过");
- return CompletableFuture.completedFuture(Map.of());
- }
-
- List messages = (List) messagesOpt.get();
- log.debug("ReactExperienceAgentHook#injectReactExperienceToMessages - reason=当前messages数量={}", messages.size());
-
- // 🔥 检查是否已经注入过React策略经验
- for (Message msg : messages) {
- if (msg instanceof ToolResponseMessage toolMsg) {
- for (ToolResponseMessage.ToolResponse response : toolMsg.getResponses()) {
- if ("react_strategy_injection".equals(response.name())) {
- log.info("ReactExperienceAgentHook#injectReactExperienceToMessages - reason=检测到已注入React策略经验,跳过");
- return CompletableFuture.completedFuture(Map.of());
- }
- }
- }
- }
-
- // 构建React策略内容
- String reactStrategyContent = buildReactStrategyContent(experiences);
- log.debug("ReactExperienceAgentHook#injectReactExperienceToMessages - reason=经验内容构建完成,长度={}", reactStrategyContent.length());
-
- // 🔥 构造 AssistantMessage + ToolResponseMessage 配对
- String toolCallId = "react_strategy_" + UUID.randomUUID().toString().substring(0, 8);
-
- // 1. AssistantMessage with toolCall
- AssistantMessage assistantMessage = AssistantMessage.builder()
- .toolCalls(List.of(
- new AssistantMessage.ToolCall(
- toolCallId,
- "function",
- "react_strategy_injection",
- "{}" // 空参数
- )
- ))
- .build();
-
- // 2. ToolResponseMessage with response
- ToolResponseMessage.ToolResponse toolResponse = new ToolResponseMessage.ToolResponse(
- toolCallId,
- "react_strategy_injection",
- reactStrategyContent
- );
-
- ToolResponseMessage toolResponseMessage = ToolResponseMessage.builder()
- .responses(List.of(toolResponse))
- .build();
-
- log.info("ReactExperienceAgentHook#injectReactExperienceToMessages - reason=准备注入React策略经验(AssistantMessage + ToolResponseMessage)");
-
- // 🔥 返回配对的两条消息
- Map updates = Map.of("messages", List.of(assistantMessage, toolResponseMessage));
- log.info("ReactExperienceAgentHook#injectReactExperienceToMessages - reason=准备返回updates,keys={}", updates.keySet());
-
- return CompletableFuture.completedFuture(updates);
-
- } catch (Exception e) {
- log.error("ReactExperienceAgentHook#injectReactExperienceToMessages - reason=修改messages失败", e);
- return CompletableFuture.completedFuture(Map.of());
- }
- }
-
- /**
- * 构建React策略内容
- */
- private String buildReactStrategyContent(List experiences) {
- StringBuilder content = new StringBuilder();
-
- content.append("=== Agent行为策略指导 ===\n\n");
-
- for (Experience experience : experiences) {
- content.append("🎯 策略:").append(experience.getTitle()).append("\n");
-
- if (StringUtils.hasText(experience.getContent())) {
- String trimmedContent = experience.getContent();
- if (trimmedContent.length() > properties.getMaxContentLength()) {
- trimmedContent = trimmedContent.substring(0, properties.getMaxContentLength()) + "...";
- }
- content.append(trimmedContent).append("\n\n");
- }
- }
-
- content.append("请在执行任务时遵循以上策略指导。");
-
- return content.toString();
- }
-
- /**
- * 构建查询上下文
- */
- private ExperienceQueryContext buildQueryContext(OverAllState state, RunnableConfig config, String userQuery) {
- ExperienceQueryContext context = new ExperienceQueryContext();
-
- // 关键修复:设置userQuery,用于向量搜索
- if (StringUtils.hasText(userQuery)) {
- context.setUserQuery(userQuery);
- }
-
- // 从state提取
- if (state != null) {
- state.value("user_id", String.class).ifPresent(context::setUserId);
- state.value("project_id", String.class).ifPresent(context::setProjectId);
- state.value("task_type", String.class).ifPresent(context::setTaskType);
- }
-
- // 从config提取
- if (config != null) {
- config.metadata("agent_name").ifPresent(name -> context.setAgentName(name.toString()));
- config.metadata("task_type").ifPresent(type -> context.setTaskType(type.toString()));
- }
-
- return context;
- }
-}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/internal/InMemoryExperienceProvider.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/internal/InMemoryExperienceProvider.java
index c1877830..36624c2e 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/internal/InMemoryExperienceProvider.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/internal/InMemoryExperienceProvider.java
@@ -3,13 +3,11 @@
import com.alibaba.assistant.agent.extension.experience.model.Experience;
import com.alibaba.assistant.agent.extension.experience.model.ExperienceQuery;
import com.alibaba.assistant.agent.extension.experience.model.ExperienceQueryContext;
-import com.alibaba.assistant.agent.extension.experience.model.ExperienceScope;
-import com.alibaba.assistant.agent.extension.experience.model.*;
+import com.alibaba.assistant.agent.extension.experience.model.ExperienceType;
import com.alibaba.assistant.agent.extension.experience.spi.ExperienceProvider;
import com.alibaba.assistant.agent.extension.experience.spi.ExperienceRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.*;
@@ -41,23 +39,9 @@ public List query(ExperienceQuery query, ExperienceQueryContext cont
return new ArrayList<>();
}
- List candidates = new ArrayList<>();
-
- // 根据scope优先级查询
- List scopes = determinePriorityScopes(query, context);
-
- for (ExperienceScope scope : scopes) {
- String ownerId = getOwnerIdForScope(scope, context);
- String projectId = getProjectIdForScope(scope, context);
-
- List scopedExperiences = experienceRepository.findByTypeAndScope(
- query.getType(), scope, ownerId, projectId);
-
- candidates.addAll(scopedExperiences);
-
- log.debug("InMemoryExperienceProvider#query - reason=found {} experiences for scope={}",
- scopedExperiences.size(), scope);
- }
+ List candidates = experienceRepository.findByTypeAndTenantId(
+ query.getType(),
+ context != null ? context.getTenantId() : null);
// 应用过滤条件
List filtered = applyFilters(candidates, query, context);
@@ -71,112 +55,29 @@ public List query(ExperienceQuery query, ExperienceQueryContext cont
return results;
}
- /**
- * 根据查询条件和上下文确定scope优先级
- */
- private List determinePriorityScopes(ExperienceQuery query, ExperienceQueryContext context) {
- if (!CollectionUtils.isEmpty(query.getScopes())) {
- return query.getScopes();
- }
-
- // 默认优先级: USER+PROJECT -> USER -> TEAM+PROJECT -> TEAM -> PROJECT -> GLOBAL
- List scopes = new ArrayList<>();
-
- if (context != null && StringUtils.hasText(context.getUserId())) {
- if (StringUtils.hasText(context.getProjectId())) {
- // USER + PROJECT 优先级最高,这里通过多次查询实现
- scopes.add(ExperienceScope.USER);
- }
- scopes.add(ExperienceScope.USER);
- scopes.add(ExperienceScope.TEAM);
- }
-
- if (context != null && StringUtils.hasText(context.getProjectId())) {
- scopes.add(ExperienceScope.PROJECT);
- }
-
- scopes.add(ExperienceScope.GLOBAL);
-
- return scopes.stream().distinct().collect(Collectors.toList());
- }
-
- /**
- * 根据scope确定查询用的ownerId
- */
- private String getOwnerIdForScope(ExperienceScope scope, ExperienceQueryContext context) {
- if (context == null) {
- return null;
- }
-
- return switch (scope) {
- case USER -> context.getUserId();
- case TEAM ->
- // TODO: 从context中获取团队ID,这里暂时使用userId的前缀
- StringUtils.hasText(context.getUserId()) ?
- context.getUserId().split("@")[0] : null;
- default -> null;
- };
- }
-
- /**
- * 根据scope确定查询用的projectId
- */
- private String getProjectIdForScope(ExperienceScope scope, ExperienceQueryContext context) {
- if (context == null) {
- return null;
- }
-
- if (scope == ExperienceScope.PROJECT || scope == ExperienceScope.USER || scope == ExperienceScope.TEAM) {
- return context.getProjectId();
- }
-
- return null;
- }
-
/**
* 应用过滤条件
*/
private List applyFilters(List experiences, ExperienceQuery query, ExperienceQueryContext context) {
return experiences.stream()
- .filter(experience -> matchesLanguage(experience, query, context))
.filter(experience -> matchesTags(experience, query))
.filter(experience -> matchesText(experience, query))
- .distinct() // 去重,可能同一经验在不同scope下被找到
+ .filter(experience -> matchesDisclosureStrategy(experience, query))
+ .distinct()
.collect(Collectors.toList());
}
- /**
- * 语言匹配检查
- */
- private boolean matchesLanguage(Experience experience, ExperienceQuery query, ExperienceQueryContext context) {
- String queryLanguage = query.getLanguage();
- if (!StringUtils.hasText(queryLanguage) && context != null) {
- queryLanguage = context.getLanguage();
- }
-
- if (!StringUtils.hasText(queryLanguage)) {
- return true; // 没有语言限制
- }
-
- String experienceLanguage = experience.getLanguage();
- if (!StringUtils.hasText(experienceLanguage)) {
- return true; // 经验没有语言限制
- }
-
- return queryLanguage.equalsIgnoreCase(experienceLanguage);
- }
-
/**
* 标签匹配检查
*/
private boolean matchesTags(Experience experience, ExperienceQuery query) {
Set queryTags = query.getTags();
- if (CollectionUtils.isEmpty(queryTags)) {
+ if (queryTags == null || queryTags.isEmpty()) {
return true; // 没有标签限制
}
Set experienceTags = experience.getTags();
- if (CollectionUtils.isEmpty(experienceTags)) {
+ if (experienceTags == null || experienceTags.isEmpty()) {
return false; // 经验没有标签,但查询有标签要求
}
@@ -186,6 +87,7 @@ private boolean matchesTags(Experience experience, ExperienceQuery query) {
/**
* 文本匹配检查 (基于子串匹配数量)
+ * 要求匹配分数达到最低阈值,避免仅靠 "结果"/"是多" 等常见 2 字词就命中。
*/
private boolean matchesText(Experience experience, ExperienceQuery query) {
String queryText = query.getText();
@@ -193,13 +95,35 @@ private boolean matchesText(Experience experience, ExperienceQuery query) {
return true; // 没有文本限制
}
+ int minScore = Math.max(3, queryText.length() / 3);
+
String content = experience.getContent();
- if (!StringUtils.hasText(content)) {
- return false;
+ String name = experience.getName();
+ String description = experience.getDescription();
+
+ // 匹配 content、name 或 description 任一
+ boolean hasMatch = false;
+ if (StringUtils.hasText(content) && calculateMatchScore(content, queryText) >= minScore) {
+ hasMatch = true;
+ }
+ if (!hasMatch && StringUtils.hasText(name) && calculateMatchScore(name, queryText) >= minScore) {
+ hasMatch = true;
}
+ if (!hasMatch && StringUtils.hasText(description) && calculateMatchScore(description, queryText) >= minScore) {
+ hasMatch = true;
+ }
+
+ return hasMatch;
+ }
- // 只要有任意子串匹配,就认为匹配(由排序决定相关性)
- return calculateMatchScore(content, queryText) > 0;
+ /**
+ * 披露策略匹配检查
+ */
+ private boolean matchesDisclosureStrategy(Experience experience, ExperienceQuery query) {
+ if (query.getDisclosureStrategy() == null) {
+ return true;
+ }
+ return query.getDisclosureStrategy().equals(experience.getDisclosureStrategy());
}
/**
@@ -261,8 +185,8 @@ private Comparator getComparator(ExperienceQuery query) {
}
return switch (query.getOrderBy()) {
- case CREATED_AT -> (e1, e2) -> e2.getCreatedAt().compareTo(e1.getCreatedAt());
- case UPDATED_AT -> (e1, e2) -> e2.getUpdatedAt().compareTo(e1.getUpdatedAt());
+ case CREATED_AT -> Comparator.comparing(Experience::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder()));
+ case UPDATED_AT -> Comparator.comparing(Experience::getUpdatedAt, Comparator.nullsLast(Comparator.reverseOrder()));
case SCORE ->
// TODO: 实现基于置信度的评分排序
(e1, e2) -> {
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/internal/InMemoryExperienceRepository.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/internal/InMemoryExperienceRepository.java
index ad8de1e8..f8f0e42a 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/internal/InMemoryExperienceRepository.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/internal/InMemoryExperienceRepository.java
@@ -1,7 +1,7 @@
package com.alibaba.assistant.agent.extension.experience.internal;
import com.alibaba.assistant.agent.extension.experience.model.Experience;
-import com.alibaba.assistant.agent.extension.experience.model.ExperienceScope;
+import com.alibaba.assistant.agent.extension.experience.model.ExperienceMetadata;
import com.alibaba.assistant.agent.extension.experience.model.ExperienceType;
import com.alibaba.assistant.agent.extension.experience.spi.ExperienceRepository;
import org.slf4j.Logger;
@@ -112,19 +112,17 @@ public Optional findById(String id) {
}
@Override
- public List findByTypeAndScope(ExperienceType type, ExperienceScope scope, String ownerId, String projectId) {
- log.debug("InMemoryExperienceRepository#findByTypeAndScope - reason=start finding experiences type={}, scope={}, ownerId={}, projectId={}",
- type, scope, ownerId, projectId);
+ public List findByTypeAndTenantId(ExperienceType type, String tenantId) {
+ log.debug("InMemoryExperienceRepository#findByTypeAndTenantId - reason=start finding experiences type={}, tenantId={}",
+ type, tenantId);
List results = experienceStore.values().stream()
.filter(experience -> type == null || type.equals(experience.getType()))
- .filter(experience -> scope == null || scope.equals(experience.getScope()))
- .filter(experience -> ownerId == null || ownerId.equals(experience.getOwnerId()))
- .filter(experience -> projectId == null || projectId.equals(experience.getProjectId()))
+ .filter(experience -> matchesTenantId(experience, tenantId))
.sorted((e1, e2) -> e2.getUpdatedAt().compareTo(e1.getUpdatedAt())) // 按更新时间倒序
.collect(Collectors.toList());
- log.info("InMemoryExperienceRepository#findByTypeAndScope - reason=find completed, found {} experiences",
+ log.info("InMemoryExperienceRepository#findByTypeAndTenantId - reason=find completed, found {} experiences",
results.size());
return results;
@@ -138,17 +136,40 @@ public long count() {
}
@Override
- public long countByTypeAndScope(ExperienceType type, ExperienceScope scope) {
- log.debug("InMemoryExperienceRepository#countByTypeAndScope - reason=start counting experiences type={}, scope={}",
- type, scope);
+ public long countByType(ExperienceType type) {
+ log.debug("InMemoryExperienceRepository#countByType - reason=start counting experiences type={}", type);
long count = experienceStore.values().stream()
.filter(experience -> type == null || type.equals(experience.getType()))
- .filter(experience -> scope == null || scope.equals(experience.getScope()))
.count();
- log.debug("InMemoryExperienceRepository#countByTypeAndScope - reason=count completed, result={}", count);
+ log.debug("InMemoryExperienceRepository#countByType - reason=count completed, result={}", count);
return count;
}
+
+ @Override
+ public List findAllByType(ExperienceType type) {
+ log.debug("InMemoryExperienceRepository#findAllByType - reason=start finding all experiences type={}", type);
+
+ if (type == null) {
+ log.warn("InMemoryExperienceRepository#findAllByType - reason=type is null, return all");
+ return new ArrayList<>(experienceStore.values());
+ }
+
+ List results = experienceStore.values().stream()
+ .filter(experience -> type.equals(experience.getType()))
+ .sorted((e1, e2) -> e2.getUpdatedAt().compareTo(e1.getUpdatedAt()))
+ .collect(Collectors.toList());
+
+ log.info("InMemoryExperienceRepository#findAllByType - reason=find completed, type={}, found {} experiences",
+ type, results.size());
+
+ return results;
+ }
+
+ private boolean matchesTenantId(Experience experience, String tenantId) {
+ ExperienceMetadata metadata = experience.getMetadata() != null ? experience.getMetadata() : new ExperienceMetadata();
+ return metadata.matchesTenantId(tenantId);
+ }
}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/DisclosureStrategy.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/DisclosureStrategy.java
new file mode 100644
index 00000000..bf9de0cf
--- /dev/null
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/DisclosureStrategy.java
@@ -0,0 +1,21 @@
+package com.alibaba.assistant.agent.extension.experience.model;
+
+/**
+ * 经验披露策略枚举
+ * 对齐 SKILLS 标准(agentskills.io)的渐进式披露机制
+ *
+ * @author Assistant Agent Team
+ */
+public enum DisclosureStrategy {
+
+ /**
+ * 渐进式披露 - 搜索返回摘要(name+description),需要显式读取才获取全文
+ * 对应 SKILLS 标准 Level 1→Level 2 模式
+ */
+ PROGRESSIVE,
+
+ /**
+ * 直接披露 - 高置信、短内容经验可直接注入 prompt,无需再次 read
+ */
+ DIRECT
+}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/Experience.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/Experience.java
index e63628d5..a1d7a73b 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/Experience.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/Experience.java
@@ -1,7 +1,9 @@
package com.alibaba.assistant.agent.extension.experience.model;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
import java.util.UUID;
@@ -23,49 +25,44 @@ public class Experience {
private ExperienceType type;
/**
- * 简要标题或名称
+ * 简要名称(原 title 字段,重命名对齐 SKILLS 标准)
*/
- private String title;
+ private String name;
/**
- * 经验主体内容
+ * 经验摘要描述(用于搜索结果预览,对齐 SKILLS 标准 Level 1)
*/
- private String content;
+ private String description;
/**
- * 可执行产物(FastPath Intent 使用);不影响既有 prompt 注入逻辑
- */
- private ExperienceArtifact artifact;
-
- /**
- * FastPath Intent 配置(每条经验可选)
+ * 经验主体内容
*/
- private FastIntentConfig fastIntentConfig;
+ private String content;
/**
- * 经验生效范围
+ * 披露策略(对齐 SKILLS 标准渐进式披露机制)
*/
- private ExperienceScope scope;
+ private DisclosureStrategy disclosureStrategy;
/**
- * 经验所属用户或主体标识
+ * 关联工具名列表(CodeactTool 名称)
*/
- private String ownerId;
+ private List associatedTools = new ArrayList<>();
/**
- * 经验所属的项目或仓库
+ * 关联经验ID列表
*/
- private String projectId;
+ private List relatedExperiences = new ArrayList<>();
/**
- * 仓库ID
+ * 可执行产物(FastPath Intent 使用);不影响既有 prompt 注入逻辑
*/
- private String repoId;
+ private ExperienceArtifact artifact;
/**
- * 编程语言或自然语言
+ * FastPath Intent 配置(每条经验可选)
*/
- private String language;
+ private FastIntentConfig fastIntentConfig;
/**
* 标签
@@ -94,12 +91,11 @@ public Experience() {
this.metadata = new ExperienceMetadata();
}
- public Experience(ExperienceType type, String title, String content, ExperienceScope scope) {
+ public Experience(ExperienceType type, String name, String content) {
this();
this.type = type;
- this.title = title;
+ this.name = name;
this.content = content;
- this.scope = scope;
}
public String getId() {
@@ -118,66 +114,45 @@ public void setType(ExperienceType type) {
this.type = type;
}
- public String getTitle() {
- return title;
+ public String getName() {
+ return name;
}
- public void setTitle(String title) {
- this.title = title;
+ public void setName(String name) {
+ this.name = name;
}
- public String getContent() {
- return content;
+ /**
+ * 兼容别名:返回 name 字段值
+ */
+ public String getTitle() {
+ return name;
}
/**
- * 获取有效内容:合并 content 与 artifact.code,两者均有值时拼接返回
- *
- * 先拼接 content(如有),再拼接从 artifact.code 生成的内容(如有)
+ * 兼容别名:设置 name 字段值
*/
- public String getEffectiveContent() {
- StringBuilder result = new StringBuilder();
+ public void setTitle(String title) {
+ this.name = title;
+ }
- // 1. 追加 content
- if (content != null && !content.isBlank()) {
- result.append(content);
- }
+ public String getDescription() {
+ return description;
+ }
- // 2. 追加 artifact.code 生成的内容
- if (artifact != null && artifact.getCode() != null) {
- ExperienceArtifact.CodeArtifact codeArtifact = artifact.getCode();
- if (codeArtifact.getCode() != null && !codeArtifact.getCode().isBlank()) {
- if (!result.isEmpty()) {
- result.append("\n\n");
- }
- result.append(buildContentFromCodeArtifact(codeArtifact));
- }
- }
+ public void setDescription(String description) {
+ this.description = description;
+ }
- return result.isEmpty() ? content : result.toString();
+ public String getContent() {
+ return content;
}
/**
- * 从 CodeArtifact 自动生成 content
+ * 获取有效内容:直接返回 content
*/
- private String buildContentFromCodeArtifact(ExperienceArtifact.CodeArtifact codeArtifact) {
- StringBuilder sb = new StringBuilder();
-
- // 添加描述
- if (codeArtifact.getDescription() != null && !codeArtifact.getDescription().isBlank()) {
- sb.append(codeArtifact.getDescription()).append("\n\n");
- }
-
- // 添加代码块
- String lang = codeArtifact.getLanguage() != null ? codeArtifact.getLanguage() : "python";
- sb.append("```").append(lang).append("\n");
- sb.append(codeArtifact.getCode());
- if (!codeArtifact.getCode().endsWith("\n")) {
- sb.append("\n");
- }
- sb.append("```");
-
- return sb.toString();
+ public String getEffectiveContent() {
+ return content;
}
public void setContent(String content) {
@@ -200,46 +175,6 @@ public void setFastIntentConfig(FastIntentConfig fastIntentConfig) {
this.fastIntentConfig = fastIntentConfig;
}
- public ExperienceScope getScope() {
- return scope;
- }
-
- public void setScope(ExperienceScope scope) {
- this.scope = scope;
- }
-
- public String getOwnerId() {
- return ownerId;
- }
-
- public void setOwnerId(String ownerId) {
- this.ownerId = ownerId;
- }
-
- public String getProjectId() {
- return projectId;
- }
-
- public void setProjectId(String projectId) {
- this.projectId = projectId;
- }
-
- public String getRepoId() {
- return repoId;
- }
-
- public void setRepoId(String repoId) {
- this.repoId = repoId;
- }
-
- public String getLanguage() {
- return language;
- }
-
- public void setLanguage(String language) {
- this.language = language;
- }
-
public Set getTags() {
return tags;
}
@@ -272,6 +207,30 @@ public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
+ public DisclosureStrategy getDisclosureStrategy() {
+ return disclosureStrategy;
+ }
+
+ public void setDisclosureStrategy(DisclosureStrategy disclosureStrategy) {
+ this.disclosureStrategy = disclosureStrategy;
+ }
+
+ public List getAssociatedTools() {
+ return associatedTools;
+ }
+
+ public void setAssociatedTools(List associatedTools) {
+ this.associatedTools = associatedTools != null ? associatedTools : new ArrayList<>();
+ }
+
+ public List getRelatedExperiences() {
+ return relatedExperiences;
+ }
+
+ public void setRelatedExperiences(List relatedExperiences) {
+ this.relatedExperiences = relatedExperiences != null ? relatedExperiences : new ArrayList<>();
+ }
+
public ExperienceMetadata getMetadata() {
return metadata;
}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceArtifact.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceArtifact.java
index b4978fe2..74589d5a 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceArtifact.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceArtifact.java
@@ -1,5 +1,7 @@
package com.alibaba.assistant.agent.extension.experience.model;
+import java.io.Serial;
+import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -10,22 +12,17 @@
* 设计目标:
*
* REACT:可直接构造 AssistantMessage(toolCalls + 可选文本) 进入 tool 执行
- * CODE:提供可注册/可执行的函数代码(跳过 code-generator LLM)
+ * TOOL:提供工具连接信息和Schema,用于创建 CodeactTool
*
*/
-public class ExperienceArtifact {
+public class ExperienceArtifact implements Serializable {
- private CodeArtifact code;
+ @Serial
+ private static final long serialVersionUID = 1L;
private ReactArtifact react;
- public CodeArtifact getCode() {
- return code;
- }
-
- public void setCode(CodeArtifact code) {
- this.code = code;
- }
+ private ToolArtifact tool;
public ReactArtifact getReact() {
return react;
@@ -35,66 +32,18 @@ public void setReact(ReactArtifact react) {
this.react = react;
}
- public static class CodeArtifact {
-
- /**
- * e.g. python
- */
- private String language;
-
- private String functionName;
-
- private List parameters = new ArrayList<>();
-
- /**
- * Full function code (should be parsable by RuntimeEnvironmentManager.extractFunctionName)
- */
- private String code;
-
- private String description;
-
- public String getLanguage() {
- return language;
- }
-
- public void setLanguage(String language) {
- this.language = language;
- }
-
- public String getFunctionName() {
- return functionName;
- }
-
- public void setFunctionName(String functionName) {
- this.functionName = functionName;
- }
-
- public List getParameters() {
- return parameters;
- }
-
- public void setParameters(List parameters) {
- this.parameters = parameters != null ? parameters : new ArrayList<>();
- }
-
- public String getCode() {
- return code;
- }
-
- public void setCode(String code) {
- this.code = code;
- }
-
- public String getDescription() {
- return description;
- }
+ public ToolArtifact getTool() {
+ return tool;
+ }
- public void setDescription(String description) {
- this.description = description;
- }
+ public void setTool(ToolArtifact tool) {
+ this.tool = tool;
}
- public static class ReactArtifact {
+ public static class ReactArtifact implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
/**
* Optional assistant text shown to the model/user (align with AssistantMessage text + toolCalls)
@@ -120,7 +69,10 @@ public void setPlan(ToolPlan plan) {
}
}
- public static class ToolPlan {
+ public static class ToolPlan implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
private List toolCalls = new ArrayList<>();
@@ -133,7 +85,10 @@ public void setToolCalls(List toolCalls) {
}
}
- public static class ToolCallSpec {
+ public static class ToolCallSpec implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
private String toolName;
@@ -158,6 +113,167 @@ public void setArguments(Map arguments) {
this.arguments = arguments;
}
}
-}
+ /**
+ * ToolArtifact - TOOL 类型经验的技术产物
+ * 包含工具来源、连接信息、Schema 等运行时需要的技术信息
+ */
+ public static class ToolArtifact implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 工具来源类型:mcp / a2a / http
+ */
+ private String source;
+
+ // --- MCP 连接信息 ---
+ private String mcpServerCode;
+ private String mcpServerName;
+ private String mcpToolName;
+
+ // --- A2A 连接信息 ---
+ private String a2aAgentCardUrl;
+ private String a2aAgentName;
+ private String a2aSkillName;
+
+ // --- HTTP 连接信息 ---
+ private String httpMethod;
+ private String httpUrl;
+ private String httpBodyTemplate;
+
+ /**
+ * 工具输入 Schema(JSON Schema 格式字符串)
+ */
+ private String inputSchema;
+
+ /**
+ * 返回值描述
+ */
+ private String returnDescription;
+
+ /**
+ * 是否直接返回结果给用户(不经过 LLM 总结)
+ */
+ private boolean returnDirect;
+
+ /**
+ * 对应的 CodeactTool 名称(注册到 ToolRegistry 的名称)
+ */
+ private String codeactToolName;
+
+ public String getSource() {
+ return source;
+ }
+
+ public void setSource(String source) {
+ this.source = source;
+ }
+
+ public String getMcpServerCode() {
+ return mcpServerCode;
+ }
+
+ public void setMcpServerCode(String mcpServerCode) {
+ this.mcpServerCode = mcpServerCode;
+ }
+
+ public String getMcpServerName() {
+ return mcpServerName;
+ }
+
+ public void setMcpServerName(String mcpServerName) {
+ this.mcpServerName = mcpServerName;
+ }
+
+ public String getMcpToolName() {
+ return mcpToolName;
+ }
+
+ public void setMcpToolName(String mcpToolName) {
+ this.mcpToolName = mcpToolName;
+ }
+
+ public String getA2aAgentCardUrl() {
+ return a2aAgentCardUrl;
+ }
+
+ public void setA2aAgentCardUrl(String a2aAgentCardUrl) {
+ this.a2aAgentCardUrl = a2aAgentCardUrl;
+ }
+
+ public String getA2aAgentName() {
+ return a2aAgentName;
+ }
+
+ public void setA2aAgentName(String a2aAgentName) {
+ this.a2aAgentName = a2aAgentName;
+ }
+
+ public String getA2aSkillName() {
+ return a2aSkillName;
+ }
+
+ public void setA2aSkillName(String a2aSkillName) {
+ this.a2aSkillName = a2aSkillName;
+ }
+
+ public String getHttpMethod() {
+ return httpMethod;
+ }
+
+ public void setHttpMethod(String httpMethod) {
+ this.httpMethod = httpMethod;
+ }
+
+ public String getHttpUrl() {
+ return httpUrl;
+ }
+
+ public void setHttpUrl(String httpUrl) {
+ this.httpUrl = httpUrl;
+ }
+
+ public String getHttpBodyTemplate() {
+ return httpBodyTemplate;
+ }
+
+ public void setHttpBodyTemplate(String httpBodyTemplate) {
+ this.httpBodyTemplate = httpBodyTemplate;
+ }
+
+ public String getInputSchema() {
+ return inputSchema;
+ }
+
+ public void setInputSchema(String inputSchema) {
+ this.inputSchema = inputSchema;
+ }
+
+ public String getReturnDescription() {
+ return returnDescription;
+ }
+
+ public void setReturnDescription(String returnDescription) {
+ this.returnDescription = returnDescription;
+ }
+
+ public boolean isReturnDirect() {
+ return returnDirect;
+ }
+
+ public void setReturnDirect(boolean returnDirect) {
+ this.returnDirect = returnDirect;
+ }
+
+ public String getCodeactToolName() {
+ return codeactToolName;
+ }
+
+ public void setCodeactToolName(String codeactToolName) {
+ this.codeactToolName = codeactToolName;
+ }
+ }
+}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceMetadata.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceMetadata.java
index b7d35eb5..734d1409 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceMetadata.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceMetadata.java
@@ -1,6 +1,12 @@
package com.alibaba.assistant.agent.extension.experience.model;
+import org.springframework.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
import java.util.Map;
/**
@@ -78,4 +84,74 @@ public void putProperty(String key, Object value) {
public Object getProperty(String key) {
return this.properties.get(key);
}
+
+ public List getTenantIdList() {
+ Object val = properties.get("tenantIdList");
+ if (val instanceof Collection> collection) {
+ return normalizeTenantIdList(collection);
+ }
+ return new ArrayList<>();
+ }
+
+ public void setTenantIdList(Collection tenantIdList) {
+ List normalized = normalizeTenantIdList(tenantIdList);
+ if (normalized.isEmpty()) {
+ properties.remove("tenantIdList");
+ return;
+ }
+ properties.put("tenantIdList", normalized);
+ }
+
+ public void addTenantId(String tenantId) {
+ if (!StringUtils.hasText(tenantId)) {
+ return;
+ }
+ List current = new ArrayList<>(getTenantIdList());
+ current.add(tenantId);
+ setTenantIdList(current);
+ }
+
+ public void clearTenantIdList() {
+ properties.remove("tenantIdList");
+ }
+
+ public boolean isGlobal() {
+ return getTenantIdList().isEmpty();
+ }
+
+ public boolean matchesTenantId(String tenantId) {
+ return matchesTenantId(tenantId, true);
+ }
+
+ public boolean matchesTenantId(String tenantId, boolean includeGlobal) {
+ List experienceTenantIdList = getTenantIdList();
+ if ("global".equalsIgnoreCase(tenantId != null ? tenantId.trim() : null)) {
+ return experienceTenantIdList.isEmpty();
+ }
+ if (experienceTenantIdList.isEmpty()) {
+ return includeGlobal;
+ }
+ if (!StringUtils.hasText(tenantId)) {
+ return false;
+ }
+ return experienceTenantIdList.contains(tenantId.trim());
+ }
+
+ private List normalizeTenantIdList(Collection> rawTenantIds) {
+ if (rawTenantIds == null || rawTenantIds.isEmpty()) {
+ return new ArrayList<>();
+ }
+ LinkedHashSet normalized = new LinkedHashSet<>();
+ for (Object rawTenantId : rawTenantIds) {
+ if (rawTenantId == null) {
+ continue;
+ }
+ String tenantId = String.valueOf(rawTenantId).trim();
+ if (!StringUtils.hasText(tenantId) || "global".equalsIgnoreCase(tenantId)) {
+ continue;
+ }
+ normalized.add(tenantId);
+ }
+ return new ArrayList<>(normalized);
+ }
}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceQuery.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceQuery.java
index dec42e95..b9485cae 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceQuery.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceQuery.java
@@ -1,6 +1,5 @@
package com.alibaba.assistant.agent.extension.experience.model;
-import java.util.List;
import java.util.Set;
/**
@@ -15,11 +14,6 @@ public class ExperienceQuery {
*/
private ExperienceType type;
- /**
- * 生效范围列表,查询时会按优先级顺序查找
- */
- private List scopes;
-
/**
* 标签过滤
*/
@@ -30,11 +24,6 @@ public class ExperienceQuery {
*/
private String text;
- /**
- * 编程语言或自然语言
- */
- private String language;
-
/**
* 最多返回多少条,默认5条
*/
@@ -51,14 +40,9 @@ public class ExperienceQuery {
private OrderBy orderBy = OrderBy.UPDATED_AT;
/**
- * 用户ID过滤
+ * 披露策略过滤
*/
- private String ownerId;
-
- /**
- * 项目ID过滤
- */
- private String projectId;
+ private DisclosureStrategy disclosureStrategy;
/**
* 排序枚举
@@ -98,14 +82,6 @@ public void setType(ExperienceType type) {
this.type = type;
}
- public List getScopes() {
- return scopes;
- }
-
- public void setScopes(List scopes) {
- this.scopes = scopes;
- }
-
public Set getTags() {
return tags;
}
@@ -122,14 +98,6 @@ public void setText(String text) {
this.text = text;
}
- public String getLanguage() {
- return language;
- }
-
- public void setLanguage(String language) {
- this.language = language;
- }
-
public int getLimit() {
return limit;
}
@@ -154,19 +122,11 @@ public void setOrderBy(OrderBy orderBy) {
this.orderBy = orderBy;
}
- public String getOwnerId() {
- return ownerId;
- }
-
- public void setOwnerId(String ownerId) {
- this.ownerId = ownerId;
- }
-
- public String getProjectId() {
- return projectId;
+ public DisclosureStrategy getDisclosureStrategy() {
+ return disclosureStrategy;
}
- public void setProjectId(String projectId) {
- this.projectId = projectId;
+ public void setDisclosureStrategy(DisclosureStrategy disclosureStrategy) {
+ this.disclosureStrategy = disclosureStrategy;
}
}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceQueryContext.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceQueryContext.java
index f812fcae..ae34e780 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceQueryContext.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceQueryContext.java
@@ -1,5 +1,7 @@
package com.alibaba.assistant.agent.extension.experience.model;
+import org.springframework.util.StringUtils;
+
/**
* 经验查询上下文
* Hook基于OverAllState/RunnableConfig构造的上下文信息
@@ -14,49 +16,9 @@ public class ExperienceQueryContext {
private String userQuery;
/**
- * 用户ID
- */
- private String userId;
-
- /**
- * 项目ID
- */
- private String projectId;
-
- /**
- * 仓库ID
- */
- private String repoId;
-
- /**
- * 当前文件路径
- */
- private String currentFilePath;
-
- /**
- * 任务类型
- */
- private String taskType;
-
- /**
- * Agent名称
- */
- private String agentName;
-
- /**
- * Agent类型
- */
- private String agentType;
-
- /**
- * 场景标签
- */
- private String sceneTags;
-
- /**
- * 编程语言
+ * 租户ID
*/
- private String language;
+ private String tenantId;
public ExperienceQueryContext() {
}
@@ -69,75 +31,12 @@ public void setUserQuery(String userQuery) {
this.userQuery = userQuery;
}
- public String getUserId() {
- return userId;
- }
-
- public void setUserId(String userId) {
- this.userId = userId;
- }
-
- public String getProjectId() {
- return projectId;
- }
-
- public void setProjectId(String projectId) {
- this.projectId = projectId;
- }
-
- public String getRepoId() {
- return repoId;
- }
-
- public void setRepoId(String repoId) {
- this.repoId = repoId;
- }
-
- public String getCurrentFilePath() {
- return currentFilePath;
- }
-
- public void setCurrentFilePath(String currentFilePath) {
- this.currentFilePath = currentFilePath;
- }
-
- public String getTaskType() {
- return taskType;
- }
-
- public void setTaskType(String taskType) {
- this.taskType = taskType;
- }
-
- public String getAgentName() {
- return agentName;
- }
-
- public void setAgentName(String agentName) {
- this.agentName = agentName;
- }
-
- public String getAgentType() {
- return agentType;
+ public String getTenantId() {
+ return tenantId;
}
- public void setAgentType(String agentType) {
- this.agentType = agentType;
+ public void setTenantId(String tenantId) {
+ this.tenantId = StringUtils.hasText(tenantId) ? tenantId.trim() : null;
}
- public String getSceneTags() {
- return sceneTags;
- }
-
- public void setSceneTags(String sceneTags) {
- this.sceneTags = sceneTags;
- }
-
- public String getLanguage() {
- return language;
- }
-
- public void setLanguage(String language) {
- this.language = language;
- }
}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceScope.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceScope.java
deleted file mode 100644
index 928a4d93..00000000
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceScope.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.alibaba.assistant.agent.extension.experience.model;
-
-/**
- * 经验生效范围枚举
- * 查询时优先级从高到低:USER + PROJECT -> USER -> TEAM + PROJECT -> TEAM -> PROJECT -> GLOBAL
- *
- * @author Assistant Agent Team
- */
-public enum ExperienceScope {
-
- /**
- * 全局生效,对所有用户与项目可见
- */
- GLOBAL,
-
- /**
- * 团队级,在同一团队/组织下共享
- */
- TEAM,
-
- /**
- * 仅关联用户本人可见
- */
- USER,
-
- /**
- * 项目/仓库级别,在特定项目下共享
- */
- PROJECT
-}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceType.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceType.java
index cfb7843c..7cd71cbf 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceType.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/model/ExperienceType.java
@@ -8,14 +8,14 @@
public enum ExperienceType {
/**
- * 代码经验 - 包含代码片段、风格规范与最佳实践
+ * React经验 - Agent行为经验与策略建议(流程经验)
*/
- CODE,
+ REACT,
/**
- * React经验 - Agent行为经验与策略建议
+ * 工具经验 - 单个工具的使用经验(MCP/A2A/HTTP工具)
*/
- REACT,
+ TOOL,
/**
* 通用常识经验 - 规范、注意事项、安全提示等
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/spi/ExperienceRepository.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/spi/ExperienceRepository.java
index b0a56ccf..0f29472b 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/spi/ExperienceRepository.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/spi/ExperienceRepository.java
@@ -1,7 +1,6 @@
package com.alibaba.assistant.agent.extension.experience.spi;
import com.alibaba.assistant.agent.extension.experience.model.Experience;
-import com.alibaba.assistant.agent.extension.experience.model.ExperienceScope;
import com.alibaba.assistant.agent.extension.experience.model.ExperienceType;
import java.util.Collection;
@@ -49,15 +48,13 @@ public interface ExperienceRepository {
Optional findById(String id);
/**
- * 根据类型和范围查找经验
+ * 根据类型和租户查找经验
*
* @param type 经验类型
- * @param scope 生效范围
- * @param ownerId 所有者ID,可为null
- * @param projectId 项目ID,可为null
+ * @param tenantId 当前请求租户ID;为空时仅返回全局经验
* @return 经验列表
*/
- List findByTypeAndScope(ExperienceType type, ExperienceScope scope, String ownerId, String projectId);
+ List findByTypeAndTenantId(ExperienceType type, String tenantId);
/**
* 统计经验数量
@@ -67,11 +64,18 @@ public interface ExperienceRepository {
long count();
/**
- * 根据条件统计经验数量
+ * 根据类型统计经验数量
*
* @param type 经验类型,可为null
- * @param scope 生效范围,可为null
* @return 符合条件的经验数量
*/
- long countByTypeAndScope(ExperienceType type, ExperienceScope scope);
+ long countByType(ExperienceType type);
+
+ /**
+ * 根据类型获取所有经验(用于启动时全量加载 TOOL 经验)
+ *
+ * @param type 经验类型
+ * @return 该类型的所有经验列表
+ */
+ List findAllByType(ExperienceType type);
}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/tool/CommonSenseInjectionTool.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/tool/CommonSenseInjectionTool.java
deleted file mode 100644
index e9799039..00000000
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/tool/CommonSenseInjectionTool.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.alibaba.assistant.agent.extension.experience.tool;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.ai.tool.annotation.Tool;
-
-/**
- * 常识经验注入假工具
- *
- * 这个工具不执行任何实际操作,仅用于支持 CommonSenseExperienceModelHook
- * 通过 AssistantMessage + ToolResponseMessage 配对方式注入常识经验。
- *
- *
注册这个工具可以让 ReactAgent 的路由逻辑正确识别和处理经验注入。
- *
- * @author Assistant Agent Team
- */
-public class CommonSenseInjectionTool {
-
- private static final Logger log = LoggerFactory.getLogger(CommonSenseInjectionTool.class);
-
- /**
- * 常识经验注入方法 - 这是一个占位工具,实际不会被调用
- *
- *
Hook 会预先构造 AssistantMessage(toolCall) + ToolResponseMessage 配对,
- * 模拟已经完成的工具调用,所以这个方法不会被真正执行。
- *
- * @return 空字符串(实际不会被调用)
- */
- @Tool(name = "common_sense_injection",
- description = "内部工具:用于注入常识经验到对话上下文。此工具由系统自动调用,无需用户手动触发。")
- public String inject() {
- log.warn("CommonSenseInjectionTool#inject - reason=此工具不应被直接调用,仅作为占位工具存在");
- return "";
- }
-}
-
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/tool/ReactDirectCodeactToolCallbackWrapper.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/tool/ReactDirectCodeactToolCallbackWrapper.java
new file mode 100644
index 00000000..116b8d7a
--- /dev/null
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/tool/ReactDirectCodeactToolCallbackWrapper.java
@@ -0,0 +1,74 @@
+package com.alibaba.assistant.agent.extension.experience.tool;
+
+import com.alibaba.assistant.agent.common.tools.CodeactTool;
+import com.alibaba.fastjson.JSON;
+import org.springframework.ai.chat.model.ToolContext;
+import org.springframework.ai.tool.function.FunctionToolCallback;
+import org.springframework.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+/**
+ * Adapts {@link CodeactTool} instances into React-stage {@link FunctionToolCallback}s.
+ *
+ *
Progressive disclosure uses this wrapper when a TOOL experience is allowed to be
+ * called directly in the React loop. The wrapped callback preserves the original
+ * CodeAct tool implementation and only changes how the runtime exposes it to the model.
+ */
+public final class ReactDirectCodeactToolCallbackWrapper {
+
+ private ReactDirectCodeactToolCallbackWrapper() {
+ }
+
+ @SuppressWarnings("unchecked")
+ public static FunctionToolCallback, String> wrap(CodeactTool codeactTool) {
+ String reactToolName = resolveReactToolName(codeactTool);
+ BiFunction, ToolContext, String> function = (mapInput, toolContext) ->
+ codeactTool.call(JSON.toJSONString(mapInput), toolContext);
+
+ return FunctionToolCallback
+ ., String>builder(reactToolName, function)
+ .description(codeactTool.getToolDefinition().description())
+ .inputType((Class>) (Class>) Map.class)
+ .inputSchema(codeactTool.getToolDefinition().inputSchema())
+ .build();
+ }
+
+ public static List, String>> wrapAll(List codeactTools) {
+ List, String>> callbacks = new ArrayList<>();
+ if (codeactTools == null) {
+ return callbacks;
+ }
+ for (CodeactTool codeactTool : codeactTools) {
+ if (hasReactToolName(codeactTool)) {
+ callbacks.add(wrap(codeactTool));
+ }
+ }
+ return callbacks;
+ }
+
+ public static boolean hasReactToolName(CodeactTool codeactTool) {
+ return StringUtils.hasText(resolveReactToolName(codeactTool));
+ }
+
+ public static String resolveReactToolName(CodeactTool codeactTool) {
+ if (codeactTool == null || codeactTool.getToolDefinition() == null) {
+ return null;
+ }
+ String runtimeToolName = codeactTool.getToolDefinition().name();
+ if (codeactTool.getCodeactMetadata() != null) {
+ String invocationTemplate = codeactTool.getCodeactMetadata().codeInvocationTemplate();
+ if (StringUtils.hasText(invocationTemplate) && invocationTemplate.contains("(")) {
+ runtimeToolName = invocationTemplate.substring(0, invocationTemplate.indexOf("("));
+ }
+ String targetClassName = codeactTool.getCodeactMetadata().targetClassName();
+ if (StringUtils.hasText(targetClassName) && StringUtils.hasText(runtimeToolName)) {
+ return targetClassName + "." + runtimeToolName;
+ }
+ }
+ return null;
+ }
+}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/tool/ReactStrategyInjectionTool.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/tool/ReactStrategyInjectionTool.java
deleted file mode 100644
index 9fddbba9..00000000
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/experience/tool/ReactStrategyInjectionTool.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package com.alibaba.assistant.agent.extension.experience.tool;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.ai.tool.annotation.Tool;
-
-/**
- * React策略经验注入假工具
- *
- * 这个工具不执行任何实际操作,仅用于支持 ReactExperienceAgentHook
- * 通过 AssistantMessage + ToolResponseMessage 配对方式注入 React 策略经验。
- *
- *
注册这个工具可以让 ReactAgent 的路由逻辑正确识别和处理经验注入。
- *
- *
重要 :LLM 不应该主动调用这个工具。如果 LLM 尝试调用,
- * 会返回错误提示,引导 LLM 使用正确的工具。
- *
- * @author Assistant Agent Team
- */
-public class ReactStrategyInjectionTool {
-
- private static final Logger log = LoggerFactory.getLogger(ReactStrategyInjectionTool.class);
-
- /**
- * 工具名称常量,需要与 ReactExperienceAgentHook 中使用的名称保持一致
- */
- public static final String TOOL_NAME = "react_strategy_injection";
-
- /**
- * React策略经验注入方法 - 这是一个内部系统工具,禁止 LLM 主动调用
- *
- *
Hook 会预先构造 AssistantMessage(toolCall) + ToolResponseMessage 配对,
- * 模拟已经完成的工具调用,所以这个方法不应该被真正执行。
- *
- *
如果 LLM 主动调用了这个工具,说明 LLM 没有遵循工具描述,
- * 此时返回错误提示,引导 LLM 正确行为。
- *
- * @return 错误提示信息
- */
- @Tool(name = TOOL_NAME,
- description = "[内部系统工具 - 请勿调用] " +
- "这是一个由框架自动调用的内部系统工具,用于注入 React 策略指导。" +
- "你绝对不能直接调用此工具。" +
- "你需要的策略指导已经在对话上下文中提供。" +
- "如果需要执行操作,请使用适当的工具,如 write_code、execute_code 等。" +
- "调用此工具将导致错误。")
- public String inject() {
- log.error("ReactStrategyInjectionTool#inject - reason=LLM错误调用了内部系统工具, " +
- "这表明LLM没有遵循工具描述中的禁止调用说明");
-
- return """
- {
- "error": true,
- "error_type": "FORBIDDEN_TOOL_CALL",
- "message": "ERROR: You called react_strategy_injection which is a forbidden internal system tool. This tool is automatically invoked by the framework to provide you with React strategy guidance - you should NEVER call it directly. The strategy guidance you need is already provided in the conversation. Please proceed with your task using the appropriate tools (like write_code, execute_code, send_message, etc.). Do NOT attempt to call react_strategy_injection again."
- }
- """;
- }
-}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/learning/extractor/ExperienceLearningExtractor.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/learning/extractor/ExperienceLearningExtractor.java
index 2e97b9c1..909e3262 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/learning/extractor/ExperienceLearningExtractor.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/learning/extractor/ExperienceLearningExtractor.java
@@ -16,8 +16,8 @@
package com.alibaba.assistant.agent.extension.learning.extractor;
+import com.alibaba.assistant.agent.common.constant.CodeactStateKeys;
import com.alibaba.assistant.agent.extension.experience.model.Experience;
-import com.alibaba.assistant.agent.extension.experience.model.ExperienceScope;
import com.alibaba.assistant.agent.extension.experience.model.ExperienceType;
import com.alibaba.assistant.agent.extension.learning.model.LearningContext;
import com.alibaba.assistant.agent.extension.learning.spi.LearningExtractor;
@@ -55,10 +55,6 @@ public class ExperienceLearningExtractor implements LearningExtractor llmExtractExperiences(LearningContext context) {
你是智能学习系统的提取器。从Agent执行中提取可复用经验。
提取类型:
- 1. CODE:代码实现模式、算法方法、可复用代码
- 2. COMMON:需求理解、解决思路、最佳实践
- 3. REACT:工具使用策略、决策流程、处理模式
+ 1. COMMON:需求理解、通用知识、解决思路、最佳实践、安全边界
+ 2. REACT:多步处理策略、决策流程、代码生成模式、任务编排方法
+ 3. TOOL:单个工具的使用前提、调用方式、适用边界、常见注意事项
JSON格式输出:
```json
[
{
- "type": "CODE|COMMON|REACT",
+ "type": "COMMON|REACT|TOOL",
"title": "简短标题(10字内)",
"summary": "核心要点(50字内)",
"content": "详细内容(200字内,重点可复用性)",
@@ -261,7 +257,7 @@ private String buildContextSummary(LearningContext context, OverAllState state)
);
// 代码生成
- if (state.value(GENERATED_CODES).isPresent()) {
+ if (state.value(CodeactStateKeys.GENERATED_CODES).isPresent()) {
summary.append("生成了代码\n");
}
@@ -295,7 +291,7 @@ private String buildDetailedContext(LearningContext context) {
});
// 生成的代码
- state.value(GENERATED_CODES).ifPresent(codes -> {
+ state.value(CodeactStateKeys.GENERATED_CODES).ifPresent(codes -> {
details.append("## 生成的代码\n");
details.append(summarizeCodes(codes)).append("\n\n");
});
@@ -379,7 +375,7 @@ private String extractJsonArray(String output) {
@SuppressWarnings("unchecked")
private Experience buildExperienceFromMap(Map item) {
String typeStr = (String) item.get("type");
- ExperienceType type = ExperienceType.valueOf(typeStr.toUpperCase());
+ ExperienceType type = resolveExperienceType(typeStr);
String title = (String) item.get("title");
String summary = (String) item.getOrDefault("summary", "");
@@ -394,11 +390,10 @@ private Experience buildExperienceFromMap(Map item) {
String fullContent = summary.isEmpty() ? content : summary + "\n\n" + content;
// 使用构造函数创建Experience
- Experience experience = new Experience(type, title, fullContent, ExperienceScope.GLOBAL);
-
- // 设置标签
- for (String tag : tags) {
- experience.addTag(tag);
+ Experience experience = new Experience(type, title, fullContent);
+ // 设置标签
+ for (String tag : tags) {
+ experience.addTag(tag);
}
// 设置时间
@@ -408,6 +403,14 @@ private Experience buildExperienceFromMap(Map item) {
return experience;
}
+ private ExperienceType resolveExperienceType(String typeStr) {
+ if (typeStr == null || typeStr.isBlank()) {
+ throw new IllegalArgumentException("Experience type is required");
+ }
+ String normalized = typeStr.trim().toUpperCase(Locale.ROOT);
+ return ExperienceType.valueOf(normalized);
+ }
+
/**
* 总结代码信息
*/
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/prompt/CodeactToolSignatureInjectionToolCallback.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/prompt/CodeactToolSignatureInjectionToolCallback.java
new file mode 100644
index 00000000..71e743c2
--- /dev/null
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/prompt/CodeactToolSignatureInjectionToolCallback.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2024-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 com.alibaba.assistant.agent.extension.prompt;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.chat.model.ToolContext;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.definition.ToolDefinition;
+import org.springframework.lang.NonNull;
+
+/**
+ * Placeholder ToolCallback for the fake tool injected by {@code CodeactToolSignatureAgentHook}.
+ */
+public class CodeactToolSignatureInjectionToolCallback implements ToolCallback {
+
+ private static final Logger log = LoggerFactory.getLogger(CodeactToolSignatureInjectionToolCallback.class);
+
+ public static final String TOOL_NAME = "codeact_tool_signature_injection";
+
+ private static final String TOOL_DESCRIPTION =
+ "[INTERNAL SYSTEM TOOL - DO NOT CALL] "
+ + "This is an internal system tool that is automatically invoked by the framework. "
+ + "You must NEVER call this tool directly. "
+ + "Codeact tool signatures are injected automatically into the conversation for write_code usage. "
+ + "Calling this tool will result in an error.";
+
+ private static final String INPUT_SCHEMA = """
+ {
+ "type": "object",
+ "properties": {},
+ "required": []
+ }
+ """;
+
+ private final ToolDefinition toolDefinition;
+
+ public CodeactToolSignatureInjectionToolCallback() {
+ this.toolDefinition = ToolDefinition.builder()
+ .name(TOOL_NAME)
+ .description(TOOL_DESCRIPTION)
+ .inputSchema(INPUT_SCHEMA)
+ .build();
+ }
+
+ @Override
+ public ToolDefinition getToolDefinition() {
+ return toolDefinition;
+ }
+
+ @Override
+ public String call(@NonNull String input) {
+ return call(input, null);
+ }
+
+ @Override
+ public String call(@NonNull String input, ToolContext toolContext) {
+ log.warn("CodeactToolSignatureInjectionToolCallback#call - reason=LLM incorrectly called internal tool, input={}", input);
+ return "{\"error\": true, \"message\": \"ERROR: codeact_tool_signature_injection is an internal system tool. "
+ + "Do NOT call it. The tool signatures are already provided in the conversation for write_code usage. "
+ + "Proceed with your task using the appropriate tools.\"}";
+ }
+
+ public static CodeactToolSignatureInjectionToolCallback getInstance() {
+ return SingletonHolder.INSTANCE;
+ }
+
+ private static class SingletonHolder {
+ private static final CodeactToolSignatureInjectionToolCallback INSTANCE =
+ new CodeactToolSignatureInjectionToolCallback();
+ }
+}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/prompt/PromptContributionToolCallback.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/prompt/PromptContributionToolCallback.java
new file mode 100644
index 00000000..e9965bc4
--- /dev/null
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/prompt/PromptContributionToolCallback.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2024-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 com.alibaba.assistant.agent.extension.prompt;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.chat.model.ToolContext;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.definition.ToolDefinition;
+import org.springframework.lang.NonNull;
+
+/**
+ * Placeholder ToolCallback for the {@code __prompt_contribution__} internal tool.
+ *
+ * {@link PromptContributorModelHook} injects fake AssistantMessage + ToolResponseMessage
+ * pairs using this tool name. The agent framework needs a registered ToolCallback so that:
+ *
+ * The tool appears in the LLM's tool list, preventing confusion
+ * If the LLM mistakenly calls it, a clear error message is returned
+ *
+ *
+ * This tool is never meant to be called directly. The description explicitly tells
+ * the LLM not to invoke it.
+ *
+ * @author Assistant Agent Team
+ */
+public class PromptContributionToolCallback implements ToolCallback {
+
+ private static final Logger log = LoggerFactory.getLogger(PromptContributionToolCallback.class);
+
+ public static final String TOOL_NAME = "__get_system_guidance__";
+
+ private static final String TOOL_DESCRIPTION =
+ "[INTERNAL SYSTEM TOOL - DO NOT CALL] "
+ + "This is an internal system tool that is automatically invoked by the framework. "
+ + "You must NEVER call this tool directly. "
+ + "If you need system guidance, it will be provided automatically in tags. "
+ + "Calling this tool will result in an error.";
+
+ private static final String INPUT_SCHEMA = """
+ {
+ "type": "object",
+ "properties": {},
+ "required": []
+ }
+ """;
+
+ private final ToolDefinition toolDefinition;
+
+ public PromptContributionToolCallback() {
+ this.toolDefinition = ToolDefinition.builder()
+ .name(TOOL_NAME)
+ .description(TOOL_DESCRIPTION)
+ .inputSchema(INPUT_SCHEMA)
+ .build();
+ }
+
+ @Override
+ public ToolDefinition getToolDefinition() {
+ return toolDefinition;
+ }
+
+ @Override
+ public String call(@NonNull String input) {
+ return call(input, null);
+ }
+
+ @Override
+ public String call(@NonNull String input, ToolContext toolContext) {
+ log.warn("PromptContributionToolCallback#call - reason=LLM incorrectly called internal tool, input={}", input);
+ return "{\"error\": true, \"message\": \"ERROR: __get_system_guidance__ is an internal system tool. "
+ + "Do NOT call it. The guidance is already provided in tags. "
+ + "Proceed with your task using the appropriate tools.\"}";
+ }
+
+ public static PromptContributionToolCallback getInstance() {
+ return SingletonHolder.INSTANCE;
+ }
+
+ private static class SingletonHolder {
+ private static final PromptContributionToolCallback INSTANCE = new PromptContributionToolCallback();
+ }
+}
diff --git a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/prompt/PromptContributorModelHook.java b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/prompt/PromptContributorModelHook.java
index 233bfdfe..830b951d 100644
--- a/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/prompt/PromptContributorModelHook.java
+++ b/assistant-agent-extensions/src/main/java/com/alibaba/assistant/agent/extension/prompt/PromptContributorModelHook.java
@@ -16,6 +16,7 @@
package com.alibaba.assistant.agent.extension.prompt;
import com.alibaba.assistant.agent.prompt.PromptContribution;
+import com.alibaba.assistant.agent.prompt.PromptContributor;
import com.alibaba.assistant.agent.prompt.PromptContributorContext;
import com.alibaba.assistant.agent.prompt.PromptContributorManager;
import com.alibaba.cloud.ai.graph.OverAllState;
@@ -31,10 +32,15 @@
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.ToolResponseMessage;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -42,6 +48,10 @@
* 将 PromptContributor 机制接入 ModelHook 的抽象基类
* 在 BEFORE_MODEL 阶段执行,将 PromptContribution 注入到 messages
*
+ * 去重策略:将已注入贡献的 guidanceId(contributorName + content MD5)存储在
+ * OverAllState 的 {@value #STATE_KEY_INJECTED_IDS} 中,每次注入前检查是否已存在,
+ * 注入内容保持纯文本格式,避免 XML 标签干扰 LLM 工具调用的 JSON 生成。
+ *
* @author Assistant Agent Team
* @see ReactPromptContributorModelHook
*/
@@ -53,7 +63,12 @@ public abstract class PromptContributorModelHook extends ModelHook implements Pr
/**
* 注入工具名称
*/
- private static final String INJECTION_TOOL_NAME = "__prompt_contribution__";
+ private static final String INJECTION_TOOL_NAME = "__get_system_guidance__";
+
+ /**
+ * 存储已注入 guidance id 的 state key
+ */
+ static final String STATE_KEY_INJECTED_IDS = "__prompt_contribution_injected_ids__";
/**
* 默认的 order 值,设置为较大值以确保在评估 Hook(order=10)之后执行
@@ -93,25 +108,26 @@ public CompletableFuture> beforeModel(OverAllState state, Ru
log.debug("{}#beforeModel - reason=开始执行 Prompt 贡献", hookName);
try {
- // 1. 构造上下文
- // 注意:systemMessage 传 null,因为框架中 AgentLlmNode 的 systemMessage
- // 来自构建时的固定值 this.systemPrompt,不会存入 OverAllState,
- // 所以无法从 state 中获取。如需获取 systemMessage,应改用 ModelInterceptor。
+ // 1. 提取已注入的 guidance id(从 state 中读取)
+ Set existingGuidanceIds = getInjectedIds(state);
+
+ // 2. 构造上下文
PromptContributorContext context = new OverAllStatePromptContributorContext(
state, null, "REACT");
- // 2. 组装所有贡献
- PromptContribution contribution = contributorManager.assemble(context);
+ // 3. 逐个处理 contributor,收集去重后的新贡献
+ List newEntries = collectAndFilterGuidance(context, existingGuidanceIds);
- if (contribution == null || contribution.isEmpty()) {
- log.debug("{}#beforeModel - reason=无 Prompt 贡献内容", hookName);
+ if (newEntries.isEmpty()) {
+ log.debug("{}#beforeModel - reason=无新 Prompt 贡献(全部被去重或为空)", hookName);
return CompletableFuture.completedFuture(Map.of());
}
- // 3. 将贡献内容注入到 messages
- Map updates = injectContribution(contribution);
+ // 4. 注入纯文本内容并更新已注入 id 集合
+ Map updates = injectContributions(newEntries, existingGuidanceIds);
- log.info("{}#beforeModel - reason=已注入 Prompt 贡献", hookName);
+ log.info("{}#beforeModel - reason=已注入 Prompt 贡献, newCount={}, existingSkipped={}",
+ hookName, newEntries.size(), existingGuidanceIds.size());
return CompletableFuture.completedFuture(updates);
} catch (Exception e) {
@@ -120,63 +136,161 @@ public CompletableFuture> beforeModel(OverAllState state, Ru
}
}
- private Map injectContribution(PromptContribution contribution) {
- Map updates = new HashMap<>();
-
- // spring-ai-alibaba框架中 AgentLlmNode 的 systemMessage 来自构建时的固定值 this.systemPrompt,hook中无法修改它
- // 当前只能通过 messagesToAppend 注入内容
+ // ─── Deduplication (state-based) ──────────────────────────────────
- if (contribution.systemTextToAppend() != null || contribution.systemTextToPrepend() != null) {
- // System 文本通过特殊的 state key 传递(注意:当前不会生效,见上方说明)
- if (contribution.systemTextToPrepend() != null) {
- log.warn("PromptContributorModelHook#injectContribution - " +
- "reason=systemTextToPrepend 设置了但不会生效, 请改用 messagesToAppend");
+ /**
+ * 从 OverAllState 中获取已注入的 guidance id 集合
+ */
+ @SuppressWarnings("unchecked")
+ private Set getInjectedIds(OverAllState state) {
+ try {
+ Object idsObj = state.value(STATE_KEY_INJECTED_IDS).orElse(null);
+ if (idsObj instanceof Set) {
+ return new HashSet<>((Set) idsObj);
}
- if (contribution.systemTextToAppend() != null) {
- log.warn("PromptContributorModelHook#injectContribution - " +
- "reason=systemTextToAppend 设置了但不会生效, 请改用 messagesToAppend");
+ } catch (Exception e) {
+ log.debug("PromptContributorModelHook#getInjectedIds - reason=读取失败", e);
+ }
+ return new HashSet<>();
+ }
+
+ /**
+ * 生成 guidance id(Contributor 名称 + 内容的 MD5 前8位)
+ */
+ private String generateGuidanceId(String contributorName, String content) {
+ String toHash = contributorName + ":" + (content != null ? content : "");
+ String md5Hex = md5Hex(toHash);
+ return contributorName + "_" + md5Hex.substring(0, 8);
+ }
+
+ // ─── Collection & filtering ───────────────────────────────────────
+
+ private List