feat: 完善 Agent 标准交互与安全运行时
- 接入 AG-UI 运行投影、Turn 时间线和审批隔离 - 增加 Agent Skill 冻结绑定与运行时消费闭环 - 增加受控工作区、内置工具和私有 Artifact 生命周期
This commit is contained in:
@@ -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()) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user