feat: 完善 Agent 标准交互与安全运行时

- 接入 AG-UI 运行投影、Turn 时间线和审批隔离

- 增加 Agent Skill 冻结绑定与运行时消费闭环

- 增加受控工作区、内置工具和私有 Artifact 生命周期
This commit is contained in:
2026-08-19 22:13:41 +08:00
parent 91d66e636d
commit 4e8640dcaf
241 changed files with 24382 additions and 2777 deletions

View File

@@ -78,6 +78,39 @@ public class ChatSseEmitter {
return send("needSaveMessage", envelope);
}
/**
* 发送不带私有事件包装的 SSE data 数据。
*
* @param data 已完成协议序列化的数据
* @return 发送成功时为 true
*/
public boolean sendData(String data) {
if (closed.get()) {
return false;
}
try {
emitter.send(SseEmitter.event().data(data));
return true;
} catch (IOException exception) {
markDisconnected("data", exception);
return false;
} catch (IllegalStateException exception) {
closed.compareAndSet(false, true);
LOG.warn("ChatSseEmitter data send failed, message={}, exception={}",
exception.getMessage(), exception.toString());
return false;
} catch (Exception exception) {
if (isClientDisconnected(exception)) {
markDisconnected("data", exception);
return false;
}
LOG.error("ChatSseEmitter data send unexpected failed, message={}, exception={}",
exception.getMessage(), exception.toString(), exception);
safeCompleteWithError(exception);
return false;
}
}
/** SSE 底层发送 */
private boolean send(String event, ChatEnvelope<?> envelope) {
if (closed.get()) {

View File

@@ -17,6 +17,8 @@ public class ChatAssistantAccumulator {
private final List<Map<String, Object>> chains = new ArrayList<>();
private final List<Map<String, Object>> messageChain = new ArrayList<>();
private final List<Map<String, Object>> toolMessages = new ArrayList<>();
private final Map<String, Map<String, Object>> skillInvocationStatuses = new LinkedHashMap<>();
private final Map<String, Map<String, Object>> artifacts = new LinkedHashMap<>();
private Map<String, Object> latestToolCallAssistant;
private boolean toolCallBatchOpen;
@@ -113,6 +115,63 @@ public class ChatAssistantAccumulator {
toolCallBatchOpen = false;
}
/**
* 记录可安全回放的 Skill 调用状态,并按稳定状态键原位覆盖。
*
* @param status 仅含展示白名单字段的状态
*/
public void appendSkillInvocationStatus(Map<String, Object> status) {
if (status == null || status.get("statusKey") == null) {
return;
}
String key = String.valueOf(status.get("statusKey"));
Map<String, Object> safe = new LinkedHashMap<>();
for (String field : List.of("statusKey", "status", "skillId", "skillName",
"skillDisplayName", "toolCallId", "message")) {
if (status.get(field) != null) {
safe.put(field, status.get(field));
}
}
skillInvocationStatuses.put(key, safe);
}
/**
* 记录可安全持久化并回放的 Agent 产物投影。
*
* @param artifact 产物安全字段
*/
public void appendArtifact(Map<String, Object> artifact) {
if (artifact == null || artifact.get("artifactId") == null) {
return;
}
Map<String, Object> safe = new LinkedHashMap<>();
for (String field : List.of("schemaVersion", "artifactId", "fileName", "mimeType",
"size", "sha256", "downloadUrl", "status")) {
if (artifact.get(field) != null) {
safe.put(field, artifact.get(field));
}
}
artifacts.put(String.valueOf(artifact.get("artifactId")), safe);
}
/**
* 将仍在运行的 Skill 状态收口为指定终态。
*
* @param terminalStatus FAILED 或 CANCELLED
* @param message 可恢复提示
*/
public void finalizePendingSkillInvocations(String terminalStatus, String message) {
for (Map<String, Object> status : skillInvocationStatuses.values()) {
if (!"RUNNING".equals(String.valueOf(status.get("status")))) {
continue;
}
status.put("status", terminalStatus);
if (message != null && !message.isBlank()) {
status.put("message", message);
}
}
}
/**
* 获取当前 assistant 片段的文本内容。
*
@@ -161,7 +220,24 @@ public class ChatAssistantAccumulator {
if (!finalAssistantMessage.isEmpty()) {
payloadMessageChain.add(finalAssistantMessage);
}
return ChatRuntimeHistoryPayloadHelper.buildPayload(payloadMessageChain, toolMessages, payloadChains);
Map<String, Object> payload = ChatRuntimeHistoryPayloadHelper.buildPayload(
payloadMessageChain, toolMessages, payloadChains);
if (!skillInvocationStatuses.isEmpty()) {
List<Map<String, Object>> statuses = new ArrayList<>();
for (Map<String, Object> current : skillInvocationStatuses.values()) {
Map<String, Object> copy = new LinkedHashMap<>(current);
if ("RUNNING".equals(String.valueOf(copy.get("status")))) {
copy.put("status", "INCOMPLETE");
copy.putIfAbsent("message", "技能调用未完成");
}
statuses.add(copy);
}
payload.put("skillInvocationStatuses", statuses);
}
if (!artifacts.isEmpty()) {
payload.put("artifacts", new ArrayList<>(artifacts.values()));
}
return payload;
}
private Map<String, Object> findToolChain(String id, String name) {

View File

@@ -0,0 +1,39 @@
package tech.easyflow.core.runtime;
import org.junit.Assert;
import org.junit.Test;
import java.util.List;
import java.util.Map;
/**
* Assistant 历史 payload 的 Artifact 安全投影测试。
*/
public class ChatAssistantAccumulatorArtifactTest {
/**
* 验证只持久化安全字段,并按 artifactId 原位去重。
*/
@Test
public void buildPayloadShouldPersistOnlySafeArtifactFields() {
ChatAssistantAccumulator accumulator = new ChatAssistantAccumulator();
accumulator.appendArtifact(Map.of(
"schemaVersion", 1,
"artifactId", "a1",
"fileName", "report.csv",
"mimeType", "text/csv",
"size", 12L,
"sha256", "abc",
"downloadUrl", "/api/v1/agent/artifacts/a1/content",
"status", "AVAILABLE",
"objectKey", "private/object/key"));
Map<String, Object> payload = accumulator.buildPayload("done");
@SuppressWarnings("unchecked")
List<Map<String, Object>> artifacts = (List<Map<String, Object>>) payload.get("artifacts");
Assert.assertEquals(1, artifacts.size());
Assert.assertEquals("a1", artifacts.get(0).get("artifactId"));
Assert.assertFalse(artifacts.get(0).containsKey("objectKey"));
}
}