refactor(harness): remove legacy agent architecture

This commit is contained in:
zhuyongxin
2026-07-22 18:02:01 +08:00
parent bc36248cd8
commit 8ee7cc0b70
148 changed files with 3091 additions and 13889 deletions
@@ -0,0 +1,170 @@
package com.superbiz.agent.harness.audit;
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.messages.AgentCommand;
import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesModelHook;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.superbiz.agent.domain.entity.AgentStep;
import com.superbiz.agent.repository.AgentStepRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
@HookPositions({HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL})
public final class HarnessAgentAuditHook extends MessagesModelHook {
private static final Logger log = LoggerFactory.getLogger(HarnessAgentAuditHook.class);
private final AgentStepRepository repository;
private final ObjectMapper objectMapper;
private final String agentName;
private final ConcurrentHashMap<String, Integer> stepCounters = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, PendingStep> pendingSteps = new ConcurrentHashMap<>();
public HarnessAgentAuditHook(AgentStepRepository repository, ObjectMapper objectMapper, String agentName) {
this.repository = Objects.requireNonNull(repository, "repository must not be null");
this.objectMapper = Objects.requireNonNull(objectMapper, "objectMapper must not be null");
if (agentName == null || agentName.isBlank()) {
throw new IllegalArgumentException("agentName must not be blank");
}
this.agentName = agentName;
}
@Override
public String getName() {
return "harness_agent_audit";
}
@Override
public AgentCommand beforeModel(List<Message> messages, RunnableConfig config) {
AuditIdentity identity = identity(config);
if (identity == null) {
log.warn("Skipping AgentStep audit because RunnableConfig identity is missing");
return new AgentCommand(messages);
}
int stepIndex = stepCounters.merge(identity.runId(), 0, (current, ignored) -> current + 1);
try {
AgentStep saved = repository.save(AgentStep.builder()
.sessionId(identity.sessionId())
.runId(identity.runId())
.stepIndex(stepIndex)
.agentName(agentName)
.modelInput(write(inputMetadata(messages)))
.hasToolCall(false)
.build());
pendingSteps.put(stepKey(identity.runId(), stepIndex),
new PendingStep(saved.getId(), System.nanoTime()));
} catch (RuntimeException exception) {
log.warn("Failed to persist AgentStep audit before model: agent={}", agentName);
}
return new AgentCommand(messages);
}
@Override
public AgentCommand afterModel(List<Message> messages, RunnableConfig config) {
AuditIdentity identity = identity(config);
if (identity == null) {
log.warn("Skipping AgentStep audit completion because RunnableConfig identity is missing");
return new AgentCommand(messages);
}
int stepIndex = stepCounters.getOrDefault(identity.runId(), 0);
PendingStep pending = pendingSteps.remove(stepKey(identity.runId(), stepIndex));
if (pending == null || pending.id() == null) {
return new AgentCommand(messages);
}
try {
AgentStep step = repository.findById(pending.id()).orElse(null);
if (step != null) {
AssistantMessage assistant = lastAssistant(messages);
List<String> toolNames = assistant == null || assistant.getToolCalls() == null
? List.of()
: assistant.getToolCalls().stream().map(AssistantMessage.ToolCall::name)
.distinct().sorted().toList();
step.setModelOutput(write(outputMetadata(assistant, toolNames)));
step.setThought(null);
step.setHasToolCall(!toolNames.isEmpty());
step.setDurationMs(durationMillis(pending.startedNanos()));
repository.save(step);
}
} catch (RuntimeException exception) {
log.warn("Failed to complete AgentStep audit: agent={}", agentName);
}
return new AgentCommand(messages);
}
private Map<String, Object> inputMetadata(List<Message> messages) {
List<Message> safeMessages = messages == null ? List.of() : messages;
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("message_count", safeMessages.size());
metadata.put("roles", safeMessages.stream()
.map(message -> message.getMessageType().getValue())
.toList());
return metadata;
}
private Map<String, Object> outputMetadata(AssistantMessage assistant, List<String> toolNames) {
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("has_text", assistant != null
&& assistant.getText() != null && !assistant.getText().isBlank());
metadata.put("tool_names", toolNames);
return metadata;
}
private AuditIdentity identity(RunnableConfig config) {
if (config == null) {
return null;
}
String sessionId = config.metadata("sessionId").map(Object::toString).orElse(null);
String runId = config.metadata("runId").map(Object::toString).orElse(null);
return hasText(sessionId) && hasText(runId) ? new AuditIdentity(sessionId, runId) : null;
}
private AssistantMessage lastAssistant(List<Message> messages) {
if (messages == null) {
return null;
}
for (int index = messages.size() - 1; index >= 0; index--) {
if (messages.get(index) instanceof AssistantMessage assistant) {
return assistant;
}
}
return null;
}
private String write(Map<String, Object> value) {
try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException exception) {
throw new IllegalStateException("Audit metadata is not serializable", exception);
}
}
private static int durationMillis(long startedNanos) {
long value = Math.max(0L, (System.nanoTime() - startedNanos) / 1_000_000L);
return value >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) value;
}
private static String stepKey(String runId, int stepIndex) {
return runId + ':' + stepIndex;
}
private static boolean hasText(String value) {
return value != null && !value.isBlank();
}
private record AuditIdentity(String sessionId, String runId) {
}
private record PendingStep(Long id, long startedNanos) {
}
}
@@ -0,0 +1,72 @@
package com.superbiz.agent.harness.audit;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.superbiz.agent.domain.entity.ToolInvocation;
import com.superbiz.agent.harness.contract.InvocationStatus;
import com.superbiz.agent.repository.ToolInvocationRepository;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
@Component
public final class JpaToolInvocationAuditSink implements ToolInvocationAuditSink {
private final ToolInvocationRepository repository;
private final ObjectMapper objectMapper;
public JpaToolInvocationAuditSink(ToolInvocationRepository repository, ObjectMapper objectMapper) {
this.repository = Objects.requireNonNull(repository, "repository must not be null");
this.objectMapper = Objects.requireNonNull(objectMapper, "objectMapper must not be null");
}
@Override
public void record(ToolInvocationAuditEvent event) {
Objects.requireNonNull(event, "event must not be null");
repository.save(ToolInvocation.builder()
.sessionId(event.sessionId())
.runId(event.runId())
.toolName(event.toolName())
.inputParams(write(inputMetadata(event)))
.outputPreview("status=%s,evidence_status=%s".formatted(
event.status(), event.evidenceStatus()))
.outputLength(event.agentResultBytes())
.retrievalLayer("HARNESS")
.isTruncated(false)
.relevanceLevel(event.evidenceStatus().name())
.retrievalDetails(write(resultMetadata(event)))
.durationMs(event.durationMs())
.success(event.status() == InvocationStatus.READY)
.errorMessage(event.errorCode())
.build());
}
private Map<String, Object> inputMetadata(ToolInvocationAuditEvent event) {
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("tool_call_id", event.toolCallId());
metadata.put("request_bytes", event.requestBytes());
return metadata;
}
private Map<String, Object> resultMetadata(ToolInvocationAuditEvent event) {
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("tool_call_id", event.toolCallId());
metadata.put("status", event.status().name());
metadata.put("evidence_status", event.evidenceStatus().name());
metadata.put("agent_result_bytes", event.agentResultBytes());
if (event.errorCode() != null) {
metadata.put("error_code", event.errorCode());
}
return metadata;
}
private String write(Map<String, Object> value) {
try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException exception) {
throw new IllegalStateException("Tool audit metadata is not serializable", exception);
}
}
}
@@ -0,0 +1,43 @@
package com.superbiz.agent.harness.audit;
import com.superbiz.agent.harness.contract.EvidenceStatus;
import com.superbiz.agent.harness.contract.InvocationStatus;
import java.util.Objects;
public record ToolInvocationAuditEvent(
String sessionId,
String runId,
String toolCallId,
String toolName,
InvocationStatus status,
EvidenceStatus evidenceStatus,
String errorCode,
int durationMs,
int requestBytes,
int agentResultBytes) {
public ToolInvocationAuditEvent {
requireText(sessionId, "sessionId");
requireText(runId, "runId");
requireText(toolCallId, "toolCallId");
requireText(toolName, "toolName");
Objects.requireNonNull(status, "status must not be null");
Objects.requireNonNull(evidenceStatus, "evidenceStatus must not be null");
if (durationMs < 0 || requestBytes < 0 || agentResultBytes < 0) {
throw new IllegalArgumentException("Audit counters must not be negative");
}
if (status == InvocationStatus.READY && errorCode != null) {
throw new IllegalArgumentException("READY audit must not contain errorCode");
}
if (status == InvocationStatus.ERROR && (errorCode == null || errorCode.isBlank())) {
throw new IllegalArgumentException("ERROR audit requires errorCode");
}
}
private static void requireText(String value, String name) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(name + " must not be blank");
}
}
}
@@ -0,0 +1,12 @@
package com.superbiz.agent.harness.audit;
@FunctionalInterface
public interface ToolInvocationAuditSink {
void record(ToolInvocationAuditEvent event);
static ToolInvocationAuditSink noop() {
return ignored -> {
};
}
}
@@ -3,6 +3,8 @@ package com.superbiz.agent.harness.tool.boundary;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.superbiz.agent.harness.audit.ToolInvocationAuditEvent;
import com.superbiz.agent.harness.audit.ToolInvocationAuditSink;
import com.superbiz.agent.harness.contract.EvidenceStatus;
import com.superbiz.agent.harness.core.BudgetExceededException;
import com.superbiz.agent.harness.core.DiagnosisHarnessCore;
@@ -18,6 +20,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
public final class ToolBoundary {
@@ -29,23 +34,44 @@ public final class ToolBoundary {
private final CanonicalInvocationStore store;
private final ObjectMapper objectMapper;
private final Clock clock;
private final ToolInvocationAuditSink auditSink;
public ToolBoundary(DiagnosisHarnessCore core,
ToolCallKeyFactory keyFactory,
CanonicalInvocationStore store,
ObjectMapper objectMapper,
Clock clock) {
this(core, keyFactory, store, objectMapper, clock, ToolInvocationAuditSink.noop());
}
public ToolBoundary(DiagnosisHarnessCore core,
ToolCallKeyFactory keyFactory,
CanonicalInvocationStore store,
ObjectMapper objectMapper,
Clock clock,
ToolInvocationAuditSink auditSink) {
this.core = Objects.requireNonNull(core, "core must not be null");
this.keyFactory = Objects.requireNonNull(keyFactory, "keyFactory must not be null");
this.store = Objects.requireNonNull(store, "store must not be null");
this.objectMapper = Objects.requireNonNull(objectMapper, "objectMapper must not be null");
this.clock = Objects.requireNonNull(clock, "clock must not be null");
this.auditSink = Objects.requireNonNull(auditSink, "auditSink must not be null");
}
public ToolBoundaryResult execute(RunContext context,
ToolCallRequestEnvelope request,
ToolExecutor executor,
ToolResultProjector projector) {
Instant startedAt = clock.instant();
ToolBoundaryResult result = executeCanonical(context, request, executor, projector);
auditSafely(context, request, result, startedAt);
return result;
}
private ToolBoundaryResult executeCanonical(RunContext context,
ToolCallRequestEnvelope request,
ToolExecutor executor,
ToolResultProjector projector) {
String toolCallId = request == null ? null : request.toolCallId();
String key;
try {
@@ -193,6 +219,36 @@ public final class ToolBoundary {
return ToolBoundaryErrorCode.INVALID_REQUEST;
}
private void auditSafely(RunContext context,
ToolCallRequestEnvelope request,
ToolBoundaryResult result,
Instant startedAt) {
if (context == null || request == null || result == null
|| !context.runId().equals(request.runId())
|| request.toolCallId() == null || request.toolCallId().isBlank()
|| request.toolName() == null || request.toolName().isBlank()) {
return;
}
try {
auditSink.record(new ToolInvocationAuditEvent(
context.sessionId(), context.runId(), request.toolCallId(), request.toolName(),
result.status(), result.evidenceStatus(), result.errorCode(),
saturatingInt(Math.max(0L, Duration.between(startedAt, clock.instant()).toMillis())),
utf8Bytes(request.requestJson()), utf8Bytes(result.agentResult())));
} catch (RuntimeException exception) {
log.warn("Failed to persist Tool durable audit: tool={}, status={}",
request.toolName(), result.status());
}
}
private static int utf8Bytes(String value) {
return value == null ? 0 : saturatingInt(value.getBytes(StandardCharsets.UTF_8).length);
}
private static int saturatingInt(long value) {
return value >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) value;
}
private static final class InvalidToolCallIdException extends IllegalArgumentException {
private InvalidToolCallIdException(Throwable cause) {
super("Invalid tool call ID", cause);