feat(harness): add rag and log projections

This commit is contained in:
zhuyongxin
2026-07-21 20:12:18 +08:00
parent 0dbdd7d8d3
commit 3e602781d6
20 changed files with 1140 additions and 0 deletions
@@ -0,0 +1,94 @@
package com.superbiz.agent.harness.tool.adapter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.superbiz.agent.harness.core.RunContext;
import com.superbiz.agent.harness.tool.boundary.ToolBoundary;
import com.superbiz.agent.harness.tool.boundary.ToolBoundaryErrorCode;
import com.superbiz.agent.harness.tool.boundary.ToolBoundaryResult;
import com.superbiz.agent.harness.tool.boundary.ToolCallRequestEnvelope;
import com.superbiz.agent.harness.tool.contract.LogQueryScope;
import com.superbiz.agent.harness.tool.contract.LogTopic;
import com.superbiz.agent.harness.tool.contract.QueryLogsRequest;
import com.superbiz.agent.harness.tool.projection.QueryLogsResultProjector;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
/** Bridges logical query-log requests and the existing Mock tool through ToolBoundary. */
public final class QueryLogsToolAdapter {
@FunctionalInterface
public interface LegacyExecutor {
String execute(String region, String legacyTopic, String query, Integer limit) throws Exception;
}
private static final String DEFAULT_REGION = "ap-guangzhou";
private static final int DEFAULT_LOOKBACK_MINUTES = 30;
private static final int DEFAULT_LEGACY_LIMIT = 100;
private final ToolBoundary boundary;
private final ObjectMapper objectMapper;
private final QueryLogsResultProjector projector;
private final LegacyExecutor legacyExecutor;
private final Clock clock;
private final String region;
private final int legacyLimit;
public QueryLogsToolAdapter(ToolBoundary boundary, ObjectMapper objectMapper,
QueryLogsResultProjector projector, LegacyExecutor legacyExecutor,
Clock clock) {
this(boundary, objectMapper, projector, legacyExecutor, clock, DEFAULT_REGION, DEFAULT_LEGACY_LIMIT);
}
public QueryLogsToolAdapter(ToolBoundary boundary, ObjectMapper objectMapper,
QueryLogsResultProjector projector, LegacyExecutor legacyExecutor,
Clock clock, String region, int legacyLimit) {
this.boundary = Objects.requireNonNull(boundary, "boundary must not be null");
this.objectMapper = Objects.requireNonNull(objectMapper, "objectMapper must not be null");
this.projector = Objects.requireNonNull(projector, "projector must not be null");
this.legacyExecutor = Objects.requireNonNull(legacyExecutor, "legacyExecutor must not be null");
this.clock = Objects.requireNonNull(clock, "clock must not be null");
if (region == null || region.isBlank() || legacyLimit <= 0) {
throw new IllegalArgumentException("legacy region and limit must be valid");
}
this.region = region;
this.legacyLimit = legacyLimit;
}
public ToolBoundaryResult execute(RunContext context, ToolCallRequestEnvelope envelope) {
try {
QueryLogsRequest request = objectMapper.readValue(envelope.requestJson(), QueryLogsRequest.class);
if (request.topic() == null || request.query() == null || request.query().isBlank()) {
return ToolBoundaryResult.error(envelope.toolCallId(), ToolBoundaryErrorCode.INVALID_REQUEST);
}
int lookback = request.lookbackMinutes() == null
? DEFAULT_LOOKBACK_MINUTES : request.lookbackMinutes();
if (lookback <= 0 || lookback > 24 * 60) {
return ToolBoundaryResult.error(envelope.toolCallId(), ToolBoundaryErrorCode.INVALID_REQUEST);
}
Instant end = clock.instant();
LogQueryScope scope = new LogQueryScope(
request.topic(), request.query(),
DateTimeFormatter.ISO_INSTANT.format(end.minus(Duration.ofMinutes(lookback))),
DateTimeFormatter.ISO_INSTANT.format(end));
String legacyTopic = legacyTopic(request.topic());
return boundary.execute(context, envelope,
ignored -> legacyExecutor.execute(region, legacyTopic, request.query(), legacyLimit),
raw -> projector.project(request, envelope.toolCallId(), scope, raw));
} catch (Exception e) {
return ToolBoundaryResult.error(envelope == null ? null : envelope.toolCallId(),
ToolBoundaryErrorCode.INVALID_REQUEST);
}
}
private static String legacyTopic(LogTopic topic) {
return switch (topic) {
case APPLICATION -> "application-logs";
case DATABASE_SLOW_QUERY -> "database-slow-query";
case SYSTEM_EVENTS -> "system-events";
};
}
}
@@ -0,0 +1,49 @@
package com.superbiz.agent.harness.tool.adapter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.superbiz.agent.harness.tool.boundary.ToolBoundary;
import com.superbiz.agent.harness.tool.boundary.ToolBoundaryErrorCode;
import com.superbiz.agent.harness.tool.boundary.ToolBoundaryResult;
import com.superbiz.agent.harness.tool.boundary.ToolCallRequestEnvelope;
import com.superbiz.agent.harness.core.RunContext;
import com.superbiz.agent.harness.tool.contract.RagToolRequest;
import com.superbiz.agent.harness.tool.projection.RagResultProjector;
import java.util.Objects;
/** Bridges a typed RAG request and a legacy knowledge executor through ToolBoundary. */
public final class RagToolAdapter {
@FunctionalInterface
public interface LegacyExecutor {
Object execute(String query) throws Exception;
}
private final ToolBoundary boundary;
private final ObjectMapper objectMapper;
private final RagResultProjector projector;
private final LegacyExecutor legacyExecutor;
public RagToolAdapter(ToolBoundary boundary, ObjectMapper objectMapper,
RagResultProjector projector, LegacyExecutor legacyExecutor) {
this.boundary = Objects.requireNonNull(boundary, "boundary must not be null");
this.objectMapper = Objects.requireNonNull(objectMapper, "objectMapper must not be null");
this.projector = Objects.requireNonNull(projector, "projector must not be null");
this.legacyExecutor = Objects.requireNonNull(legacyExecutor, "legacyExecutor must not be null");
}
public ToolBoundaryResult execute(RunContext context, ToolCallRequestEnvelope envelope) {
try {
RagToolRequest request = objectMapper.readValue(envelope.requestJson(), RagToolRequest.class);
if (request.query() == null || request.query().isBlank()) {
return ToolBoundaryResult.error(envelope.toolCallId(), ToolBoundaryErrorCode.INVALID_REQUEST);
}
return boundary.execute(context, envelope,
ignored -> objectMapper.writeValueAsString(legacyExecutor.execute(request.query())),
raw -> projector.project(request, envelope.toolCallId(), raw));
} catch (Exception e) {
return ToolBoundaryResult.error(envelope == null ? null : envelope.toolCallId(),
ToolBoundaryErrorCode.INVALID_REQUEST);
}
}
}
@@ -0,0 +1,208 @@
package com.superbiz.agent.harness.tool.projection;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.superbiz.agent.harness.contract.EvidenceStatus;
import com.superbiz.agent.harness.tool.boundary.ProjectedToolResult;
import com.superbiz.agent.harness.tool.contract.LogEvent;
import com.superbiz.agent.harness.tool.contract.LogPattern;
import com.superbiz.agent.harness.tool.contract.LogQueryScope;
import com.superbiz.agent.harness.tool.contract.LogSourceKind;
import com.superbiz.agent.harness.tool.contract.QueryLogsRequest;
import com.superbiz.agent.harness.tool.contract.QueryLogsToolResult;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/** Projects legacy Mock log JSON into the frozen query_logs contract. */
public final class QueryLogsResultProjector {
private static final Pattern SECRET = Pattern.compile(
"(?i)(password|token|secret|api[_-]?key)\\s*[:=]\\s*[^\\s,;]+" );
private static final Pattern POD = Pattern.compile("\\bpod-[A-Za-z0-9-]+\\b");
private static final Pattern HOST = Pattern.compile(
"\\b(?:redis|mysql|kafka|rabbitmq|node|host)-[A-Za-z0-9._-]+(?::\\d+)?\\b",
Pattern.CASE_INSENSITIVE);
private static final Pattern PID = Pattern.compile("(?i)\\bPID\\s*:\\s*\\d+");
private static final Pattern IP = Pattern.compile("\\b(?:\\d{1,3}\\.){3}\\d{1,3}(?::\\d+)?\\b");
private static final Pattern SQL_LITERAL = Pattern.compile("'[^']*'");
private static final Pattern STACK_SUFFIX = Pattern.compile("\\s+at\\s+[\\w.$]+\\([^)]*\\).*$", Pattern.CASE_INSENSITIVE);
private final ObjectMapper objectMapper;
private final ToolProjectionLimits limits;
public QueryLogsResultProjector(ObjectMapper objectMapper, ToolProjectionLimits limits) {
this.objectMapper = objectMapper;
this.limits = limits;
}
public ProjectedToolResult project(QueryLogsRequest request, String toolCallId,
LogQueryScope scope, String rawResponse) throws Exception {
if (request == null || toolCallId == null || toolCallId.isBlank() || scope == null) {
throw new IllegalArgumentException("request, scope and tool call ID are required");
}
JsonNode root = objectMapper.readTree(rawResponse);
if (root == null || !root.isObject() || !root.path("success").isBoolean()
|| !root.path("success").asBoolean()) {
throw new IllegalArgumentException("log response indicates failure");
}
JsonNode logs = root.get("logs");
if (logs == null || !logs.isArray()) {
throw new IllegalArgumentException("log response has no logs array");
}
boolean truncated = logs.size() > limits.maxEvents();
List<LogEvent> allEvents = new ArrayList<>();
Map<String, PatternAccumulator> aggregates = new LinkedHashMap<>();
for (JsonNode log : logs) {
String timestamp = bounded(text(log, "timestamp"), limits.maxMessageChars());
String level = bounded(text(log, "level"), 32);
String service = bounded(text(log, "service"), 128);
String message = sanitize(text(log, "message"));
message = bounded(message, limits.maxMessageChars());
if (message.isBlank()) {
continue;
}
String example = message;
allEvents.add(new LogEvent(nullable(timestamp), nullable(level), nullable(service), message));
String patternKey = level + "\u0000" + service + "\u0000" + normalizePattern(message);
aggregates.computeIfAbsent(patternKey, ignored -> new PatternAccumulator(level, service, example))
.add(timestamp);
}
List<LogEvent> events = sample(allEvents, limits.maxEvents());
truncated |= events.size() < allEvents.size();
List<LogPattern> patterns = aggregates.values().stream()
.sorted(Comparator.comparingLong(PatternAccumulator::count).reversed()
.thenComparing(PatternAccumulator::example))
.limit(limits.maxPatterns())
.map(PatternAccumulator::toPattern)
.toList();
truncated |= aggregates.size() > patterns.size();
long matchCount = root.path("total").canConvertToLong() ? root.path("total").asLong() : allEvents.size();
QueryLogsToolResult result = new QueryLogsToolResult(
allEvents.isEmpty() ? EvidenceStatus.NO_EVIDENCE : EvidenceStatus.EVIDENCE_FOUND,
toolCallId,
LogSourceKind.MOCK,
scope,
Math.max(0, matchCount),
events.size(),
patterns,
events,
truncated);
result = fitBudget(result);
return new ProjectedToolResult(objectMapper.writeValueAsString(result), result.evidenceStatus());
}
private QueryLogsToolResult fitBudget(QueryLogsToolResult result) throws Exception {
QueryLogsToolResult current = result;
while (bytes(objectMapper.writeValueAsString(current)) > limits.maxAgentUtf8Bytes()
&& (!current.events().isEmpty() || !current.patterns().isEmpty())) {
List<LogEvent> events = new ArrayList<>(current.events());
List<LogPattern> patterns = new ArrayList<>(current.patterns());
if (!events.isEmpty()) {
events.remove(events.size() - 1);
} else {
patterns.remove(patterns.size() - 1);
}
current = new QueryLogsToolResult(
current.evidenceStatus(), current.toolCallId(), current.sourceKind(), current.scope(),
current.matchCount(), events.size(), patterns, events, true);
}
if (bytes(objectMapper.writeValueAsString(current)) > limits.maxAgentUtf8Bytes()) {
throw new IllegalArgumentException("log projection exceeds total budget");
}
return current;
}
private static List<LogEvent> sample(List<LogEvent> events, int max) {
if (events.size() <= max) {
return List.copyOf(events);
}
List<LogEvent> sampled = new ArrayList<>(max);
for (int i = 0; i < max; i++) {
int index = max == 1 ? 0 : Math.round((float) i * (events.size() - 1) / (max - 1));
sampled.add(events.get(index));
}
return sampled;
}
private static String sanitize(String message) {
String value = SECRET.matcher(message).replaceAll("$1=[REDACTED]");
value = POD.matcher(value).replaceAll("[REDACTED_POD]");
value = HOST.matcher(value).replaceAll("[REDACTED_HOST]");
value = PID.matcher(value).replaceAll("PID:[REDACTED]");
value = IP.matcher(value).replaceAll("[REDACTED_IP]");
value = SQL_LITERAL.matcher(value).replaceAll("'[REDACTED_LITERAL]'");
return STACK_SUFFIX.matcher(value).replaceAll("").trim();
}
private static String normalizePattern(String message) {
return message.replaceAll("\\b\\d+(?:\\.\\d+)?\\b", "<n>")
.replaceAll("\\s+", " ").trim();
}
private static String text(JsonNode node, String field) {
JsonNode value = node == null ? null : node.get(field);
return value == null || value.isNull() ? "" : value.asText("");
}
private static String bounded(String value, int max) {
if (value == null) {
return "";
}
return value.length() <= max ? value : value.substring(0, max);
}
private static String nullable(String value) {
return value == null || value.isBlank() ? null : value;
}
private static int bytes(String value) {
return value.getBytes(StandardCharsets.UTF_8).length;
}
private static final class PatternAccumulator {
private final String level;
private final String service;
private final String example;
private long count;
private String firstSeen;
private String lastSeen;
private PatternAccumulator(String level, String service, String example) {
this.level = level;
this.service = service;
this.example = example;
}
private PatternAccumulator add(String timestamp) {
count++;
if (firstSeen == null || timestamp.compareTo(firstSeen) < 0) {
firstSeen = timestamp;
}
if (lastSeen == null || timestamp.compareTo(lastSeen) > 0) {
lastSeen = timestamp;
}
return this;
}
private long count() {
return count;
}
private String example() {
return example;
}
private LogPattern toPattern() {
return new LogPattern(count, firstSeen, lastSeen, nullable(level), nullable(service), example);
}
}
}
@@ -0,0 +1,133 @@
package com.superbiz.agent.harness.tool.projection;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.superbiz.agent.harness.contract.EvidenceStatus;
import com.superbiz.agent.harness.tool.boundary.ProjectedToolResult;
import com.superbiz.agent.harness.tool.contract.RagEvidence;
import com.superbiz.agent.harness.tool.contract.RagToolRequest;
import com.superbiz.agent.harness.tool.contract.RagToolResult;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** Projects legacy knowledge retrieval JSON into the frozen RAG contract. */
public final class RagResultProjector {
private final ObjectMapper objectMapper;
private final ToolProjectionLimits limits;
public RagResultProjector(ObjectMapper objectMapper, ToolProjectionLimits limits) {
this.objectMapper = objectMapper;
this.limits = limits;
}
public ProjectedToolResult project(RagToolRequest request, String toolCallId,
String rawResponse) throws Exception {
if (request == null || toolCallId == null || toolCallId.isBlank()) {
throw new IllegalArgumentException("request and tool call ID are required");
}
JsonNode root = objectMapper.readTree(rawResponse);
if (root == null || !root.isObject()) {
throw new IllegalArgumentException("RAG response must be a JSON object");
}
boolean truncated = false;
String query = bounded(request.query(), limits.maxQueryChars());
truncated = !query.equals(request.query());
List<RagEvidence> evidence = new ArrayList<>();
Set<String> documentIds = new HashSet<>();
JsonNode blocks = root.has("evidenceBlocks") ? root.get("evidenceBlocks") : root.get("evidence_blocks");
if (blocks != null && blocks.isArray()) {
int ordinal = 0;
for (JsonNode block : blocks) {
ordinal++;
if (evidence.size() >= limits.maxEvidence()) {
truncated = true;
break;
}
String excerpt = text(block, "content");
if (excerpt.isBlank()) {
excerpt = text(block, "excerpt");
}
if (excerpt.isBlank()) {
continue;
}
String source = text(block, "source");
String title = text(block, "title");
String documentId = firstNonBlank(text(block, "document_id"), source, title,
"legacy-document-" + ordinal);
if (!documentIds.add(documentId)) {
truncated = true;
continue;
}
String boundedExcerpt = bounded(excerpt, limits.maxExcerptChars());
truncated |= !boundedExcerpt.equals(excerpt);
evidence.add(new RagEvidence(
documentId,
nullable(source),
nullable(title),
nullable(text(block, "breadcrumb")),
boundedExcerpt));
}
if (blocks.size() > limits.maxEvidence()) {
truncated = true;
}
}
EvidenceStatus status = evidence.isEmpty()
? EvidenceStatus.NO_EVIDENCE
: EvidenceStatus.EVIDENCE_FOUND;
RagToolResult result = new RagToolResult(status, toolCallId, query, evidence, evidence.size(), truncated);
result = fitBudget(result, truncated);
return new ProjectedToolResult(objectMapper.writeValueAsString(result), result.evidenceStatus());
}
private RagToolResult fitBudget(RagToolResult result, boolean truncated) throws Exception {
RagToolResult current = result;
while (bytes(objectMapper.writeValueAsString(current)) > limits.maxAgentUtf8Bytes()
&& !current.evidence().isEmpty()) {
List<RagEvidence> reduced = new ArrayList<>(current.evidence());
reduced.remove(reduced.size() - 1);
current = new RagToolResult(
reduced.isEmpty() ? EvidenceStatus.NO_EVIDENCE : EvidenceStatus.EVIDENCE_FOUND,
current.toolCallId(), current.query(), reduced, reduced.size(), true);
}
if (bytes(objectMapper.writeValueAsString(current)) > limits.maxAgentUtf8Bytes()) {
throw new IllegalArgumentException("RAG projection exceeds total budget");
}
return current;
}
private static String text(JsonNode node, String field) {
JsonNode value = node == null ? null : node.get(field);
return value == null || value.isNull() ? "" : value.asText("");
}
private static String firstNonBlank(String... values) {
for (String value : values) {
if (value != null && !value.isBlank()) {
return value;
}
}
return "unknown-document";
}
private static String nullable(String value) {
return value == null || value.isBlank() ? null : value;
}
private static String bounded(String value, int max) {
if (value == null) {
return "";
}
return value.length() <= max ? value : value.substring(0, max);
}
private static int bytes(String value) {
return value.getBytes(StandardCharsets.UTF_8).length;
}
}
@@ -0,0 +1,24 @@
package com.superbiz.agent.harness.tool.projection;
/** Bounds applied to Agent-facing projections. */
public record ToolProjectionLimits(
int maxEvidence,
int maxExcerptChars,
int maxPatterns,
int maxEvents,
int maxMessageChars,
int maxQueryChars,
int maxAgentUtf8Bytes) {
public ToolProjectionLimits {
if (maxEvidence <= 0 || maxExcerptChars <= 0 || maxPatterns <= 0
|| maxEvents <= 0 || maxMessageChars <= 0 || maxQueryChars <= 0
|| maxAgentUtf8Bytes <= 0) {
throw new IllegalArgumentException("projection limits must be positive");
}
}
public static ToolProjectionLimits defaults() {
return new ToolProjectionLimits(8, 1200, 12, 30, 1000, 500, 16_384);
}
}
@@ -0,0 +1,132 @@
package com.superbiz.agent.harness.tool.adapter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.superbiz.agent.harness.contract.EvidenceStatus;
import com.superbiz.agent.harness.core.DiagnosisHarnessCore;
import com.superbiz.agent.harness.core.HarnessCoreFixtures;
import com.superbiz.agent.harness.core.MutableClock;
import com.superbiz.agent.harness.core.RunContext;
import com.superbiz.agent.harness.tool.boundary.ProjectedToolResult;
import com.superbiz.agent.harness.tool.boundary.ToolBoundary;
import com.superbiz.agent.harness.tool.boundary.ToolBoundaryResult;
import com.superbiz.agent.harness.tool.boundary.ToolCallRequestEnvelope;
import com.superbiz.agent.harness.tool.contract.LogQueryScope;
import com.superbiz.agent.harness.tool.projection.QueryLogsResultProjector;
import com.superbiz.agent.harness.tool.projection.RagResultProjector;
import com.superbiz.agent.harness.tool.projection.ToolProjectionLimits;
import com.superbiz.agent.harness.tool.store.CanonicalInvocationLimits;
import com.superbiz.agent.harness.tool.store.CanonicalInvocationStore;
import com.superbiz.agent.harness.tool.store.CanonicalToolInvocation;
import com.superbiz.agent.harness.tool.store.DuplicateInvocationException;
import com.superbiz.agent.harness.tool.store.ToolCallKeyFactory;
import org.junit.jupiter.api.Test;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ToolAdapterTest {
private final ObjectMapper objectMapper = new ObjectMapper();
@Test
void ragAdapterUsesFrameworkIdAndCanonicalBoundary() throws Exception {
MutableClock clock = new MutableClock(Instant.parse("2026-07-21T10:00:00Z"));
FakeStore store = new FakeStore();
ToolBoundary boundary = boundary(clock, store);
RagToolAdapter adapter = new RagToolAdapter(boundary, objectMapper,
new RagResultProjector(objectMapper, ToolProjectionLimits.defaults()),
query -> Map.of("found", true, "evidenceBlocks", java.util.List.of(
Map.of("source", "doc-1", "content", "exact excerpt"))));
RunContext context = HarnessCoreFixtures.core(clock).startRun("session", "run-rag");
ToolBoundaryResult result = adapter.execute(context, envelope("run-rag", "framework-rag-1",
"{\"query\":\"timeout\"}"));
assertEquals("framework-rag-1", result.toolCallId());
assertEquals(EvidenceStatus.EVIDENCE_FOUND, result.evidenceStatus());
assertTrue(result.agentResult().contains("exact excerpt"));
assertEquals("framework-rag-1", store.records.values().iterator().next().toolCallId());
}
@Test
void queryLogsAdapterInjectsLegacyControlsAndPreservesLogicalScope() throws Exception {
MutableClock clock = new MutableClock(Instant.parse("2026-07-21T10:00:00Z"));
FakeStore store = new FakeStore();
AtomicReference<String> topic = new AtomicReference<>();
AtomicReference<String> region = new AtomicReference<>();
ToolBoundary boundary = boundary(clock, store);
QueryLogsToolAdapter adapter = new QueryLogsToolAdapter(boundary, objectMapper,
new QueryLogsResultProjector(objectMapper, ToolProjectionLimits.defaults()),
(actualRegion, actualTopic, query, limit) -> {
region.set(actualRegion);
topic.set(actualTopic);
return "{\"success\":true,\"total\":1,\"logs\":[{\"timestamp\":\"2026-07-21 09:59:00\",\"level\":\"WARN\",\"service\":\"order-service\",\"message\":\"timeout\"}]}";
}, clock);
RunContext context = HarnessCoreFixtures.core(clock).startRun("session", "run-log");
ToolBoundaryResult result = adapter.execute(context, envelope("run-log", "framework-log-1",
"{\"topic\":\"APPLICATION\",\"query\":\"timeout\",\"lookback_minutes\":30}"));
assertEquals("ap-guangzhou", region.get());
assertEquals("application-logs", topic.get());
assertEquals(EvidenceStatus.EVIDENCE_FOUND, result.evidenceStatus());
assertTrue(result.agentResult().contains("2026-07-21T09:30:00Z"));
assertTrue(result.agentResult().contains("2026-07-21T10:00:00Z"));
assertEquals("framework-log-1", result.toolCallId());
}
private ToolBoundary boundary(MutableClock clock, FakeStore store) {
DiagnosisHarnessCore core = HarnessCoreFixtures.core(clock);
return new ToolBoundary(core, new ToolCallKeyFactory("superbiz:harness:tool-call"),
store, objectMapper, clock);
}
private ToolCallRequestEnvelope envelope(String runId, String toolCallId, String request) {
return new ToolCallRequestEnvelope(runId, toolCallId, "tool", request, true, true);
}
private static final class FakeStore implements CanonicalInvocationStore {
private final CanonicalInvocationLimits limits = new CanonicalInvocationLimits(Duration.ofHours(1), 20_000, 10_000);
private final Map<String, CanonicalToolInvocation> records = new HashMap<>();
@Override
public CanonicalInvocationLimits limits() {
return limits;
}
@Override
public void begin(String key, CanonicalToolInvocation invocation) {
if (records.putIfAbsent(key, invocation) != null) {
throw new DuplicateInvocationException();
}
}
@Override
public Optional<CanonicalToolInvocation> find(String key) {
return Optional.ofNullable(records.get(key));
}
@Override
public CanonicalToolInvocation markReady(String key, String rawResponse, String agentResult,
EvidenceStatus evidenceStatus, Instant completedAt) {
CanonicalToolInvocation updated = records.get(key).markReady(rawResponse, agentResult, evidenceStatus, completedAt);
records.put(key, updated);
return updated;
}
@Override
public CanonicalToolInvocation markError(String key, String rawResponse, String errorCode, Instant completedAt) {
CanonicalToolInvocation updated = records.get(key).markError(rawResponse, errorCode, completedAt);
records.put(key, updated);
return updated;
}
}
}
@@ -0,0 +1,60 @@
package com.superbiz.agent.harness.tool.projection;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.superbiz.agent.harness.contract.EvidenceStatus;
import com.superbiz.agent.harness.tool.boundary.ProjectedToolResult;
import com.superbiz.agent.harness.tool.contract.LogQueryScope;
import com.superbiz.agent.harness.tool.contract.LogTopic;
import com.superbiz.agent.harness.tool.contract.QueryLogsRequest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class QueryLogsResultProjectorTest {
private final ObjectMapper objectMapper = new ObjectMapper();
private final LogQueryScope scope = new LogQueryScope(LogTopic.APPLICATION, "timeout",
"2026-07-21T10:00:00Z", "2026-07-21T10:30:00Z");
@Test
void projectsMockScopePatternsAndRedactsSensitiveFields() throws Exception {
QueryLogsResultProjector projector = new QueryLogsResultProjector(objectMapper,
new ToolProjectionLimits(2, 1200, 2, 2, 200, 100, 4096));
String raw = """
{"success":true,"total":3,"logs":[
{"timestamp":"2026-07-21 10:29:00","level":"ERROR","service":"order-service","instance":"pod-secret-1","message":"password=abc token:xyz pod-order-1 10.0.0.1 SELECT * FROM x WHERE name='alice' at a.B.c(A.java:1)","metrics":{"secret":"x"}},
{"timestamp":"2026-07-21 10:28:00","level":"ERROR","service":"order-service","instance":"pod-secret-2","message":"password=def token:qwe pod-order-2 10.0.0.2 SELECT * FROM x WHERE name='bob'"},
{"timestamp":"2026-07-21 10:27:00","level":"WARN","service":"order-service","message":"other"}]}
""";
ProjectedToolResult projected = projector.project(new QueryLogsRequest(LogTopic.APPLICATION, "timeout", 30),
"framework-log-1", scope, raw);
JsonNode json = objectMapper.readTree(projected.agentResult());
assertEquals(EvidenceStatus.EVIDENCE_FOUND, projected.evidenceStatus());
assertEquals("MOCK", json.path("source_kind").asText());
assertEquals("APPLICATION", json.path("scope").path("topic").asText());
assertEquals(3, json.path("match_count").asInt());
assertTrue(json.path("truncated").asBoolean());
assertFalse(projected.agentResult().contains("abc"));
assertFalse(projected.agentResult().contains("xyz"));
assertFalse(projected.agentResult().contains("pod-order"));
assertFalse(projected.agentResult().contains("10.0.0.1"));
assertFalse(projected.agentResult().contains("metrics"));
assertFalse(projected.agentResult().contains("instance"));
}
@Test
void returnsNoEvidenceForSuccessfulEmptyLogs() throws Exception {
QueryLogsResultProjector projector = new QueryLogsResultProjector(objectMapper, ToolProjectionLimits.defaults());
ProjectedToolResult projected = projector.project(new QueryLogsRequest(LogTopic.SYSTEM_EVENTS, "missing", 5),
"framework-log-2", scope, "{\"success\":true,\"total\":0,\"logs\":[]}");
assertEquals(EvidenceStatus.NO_EVIDENCE, projected.evidenceStatus());
assertEquals(0, objectMapper.readTree(projected.agentResult()).path("events").size());
}
}
@@ -0,0 +1,53 @@
package com.superbiz.agent.harness.tool.projection;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.superbiz.agent.harness.contract.EvidenceStatus;
import com.superbiz.agent.harness.tool.boundary.ProjectedToolResult;
import com.superbiz.agent.harness.tool.contract.RagToolRequest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class RagResultProjectorTest {
private final ObjectMapper objectMapper = new ObjectMapper();
@Test
void projectsOnlyDeduplicatedBoundedEvidence() throws Exception {
RagResultProjector projector = new RagResultProjector(objectMapper,
new ToolProjectionLimits(2, 8, 2, 2, 20, 30, 4096));
String raw = """
{"found":true,"evidenceBlocks":[
{"source":"doc-1","title":"One","breadcrumb":"a","content":"123456789","score":0.99},
{"source":"doc-1","title":"Duplicate","content":"ignored"},
{"source":"doc-2","title":"Two","content":"second"}],
"contextPack":{"packedText":"secret internal context"},"retrievalTrace":{"attempts":[]},"rerankTrace":{}}
""";
ProjectedToolResult projected = projector.project(new RagToolRequest("timeout"), "framework-1", raw);
JsonNode json = objectMapper.readTree(projected.agentResult());
assertEquals(EvidenceStatus.EVIDENCE_FOUND, projected.evidenceStatus());
assertEquals("framework-1", json.path("tool_call_id").asText());
assertEquals(2, json.path("returned_count").asInt());
assertEquals("12345678", json.path("evidence").get(0).path("excerpt").asText());
assertTrue(json.path("truncated").asBoolean());
assertFalse(projected.agentResult().contains("contextPack"));
assertFalse(projected.agentResult().contains("retrievalTrace"));
assertFalse(projected.agentResult().contains("score"));
}
@Test
void returnsNoEvidenceForEmptyUsableBlocks() throws Exception {
RagResultProjector projector = new RagResultProjector(objectMapper, ToolProjectionLimits.defaults());
ProjectedToolResult projected = projector.project(new RagToolRequest("unknown"), "framework-2",
"{\"found\":false,\"evidenceBlocks\":[{\"source\":\"doc\",\"content\":\"\"}]}");
assertEquals(EvidenceStatus.NO_EVIDENCE, projected.evidenceStatus());
assertEquals(0, objectMapper.readTree(projected.agentResult()).path("evidence").size());
}
}