From 5be939f7ee9c2f52d5cde4d2e51997641fc5d9a3 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 12 Mar 2026 13:25:46 -0700 Subject: [PATCH 01/19] feat: add initial A2A runtime integration --- camel-agent-core/pom.xml | 5 + .../agent/a2a/A2AExposedAgentCatalog.java | 70 +++ .../a2a/A2AExposedAgentCatalogLoader.java | 55 ++ .../camel/agent/a2a/A2AExposedAgentSpec.java | 89 +++ .../agent/a2a/AgentA2AAgentCardCatalog.java | 132 +++++ .../agent/a2a/AgentA2AProtocolSupport.java | 551 ++++++++++++++++++ .../agent/a2a/AgentA2ATaskRepository.java | 227 ++++++++ .../audit/AuditConversationViewProcessor.java | 2 + .../agent/audit/AuditMetadataSupport.java | 97 +++ .../camel/agent/component/AgentProducer.java | 24 + .../camel/agent/config/AgentHeaders.java | 6 + .../camel/agent/config/CorrelationKeys.java | 8 +- .../agent/runtime/A2ARuntimeProperties.java | 167 ++++++ .../runtime/AgentA2ARuntimeRouteBuilder.java | 44 ++ .../agent/runtime/AgentRuntimeBootstrap.java | 24 +- .../runtime/RuntimeResourceBootstrapper.java | 13 + .../a2a/A2AExposedAgentCatalogLoaderTest.java | 63 ++ .../agent/a2a/AgentA2ATaskRepositoryTest.java | 79 +++ .../dscope/DscopePersistenceFacade.java | 36 +- docs/pr-drafts/a2a-runtime-integration.md | 91 +++ docs/roadmap.md | 7 + pom.xml | 1 + 22 files changed, 1786 insertions(+), 5 deletions(-) create mode 100644 camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalog.java create mode 100644 camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalogLoader.java create mode 100644 camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentSpec.java create mode 100644 camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AAgentCardCatalog.java create mode 100644 camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java create mode 100644 camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2ATaskRepository.java create mode 100644 camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/A2ARuntimeProperties.java create mode 100644 camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentA2ARuntimeRouteBuilder.java create mode 100644 camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalogLoaderTest.java create mode 100644 camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2ATaskRepositoryTest.java create mode 100644 docs/pr-drafts/a2a-runtime-integration.md create mode 100644 docs/roadmap.md diff --git a/camel-agent-core/pom.xml b/camel-agent-core/pom.xml index a5cddb5..f3afd3a 100644 --- a/camel-agent-core/pom.xml +++ b/camel-agent-core/pom.xml @@ -50,6 +50,11 @@ camel-mcp 1.4.0 + + io.dscope.camel + camel-a2a-component + ${a2a.version} + org.junit.jupiter diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalog.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalog.java new file mode 100644 index 0000000..b296bb1 --- /dev/null +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalog.java @@ -0,0 +1,70 @@ +package io.dscope.camel.agent.a2a; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class A2AExposedAgentCatalog { + + private final List agents; + private final Map byId; + private final A2AExposedAgentSpec defaultAgent; + + public A2AExposedAgentCatalog(List agents) { + if (agents == null || agents.isEmpty()) { + throw new IllegalArgumentException("A2A exposed-agents catalog must contain at least one agent"); + } + Map mapped = new LinkedHashMap<>(); + A2AExposedAgentSpec resolvedDefault = null; + for (A2AExposedAgentSpec agent : agents) { + if (agent == null) { + throw new IllegalArgumentException("A2A exposed-agents entries must not be null"); + } + String agentId = required(agent.getAgentId(), "agentId"); + required(agent.getName(), "name"); + required(agent.getPlanName(), "planName"); + required(agent.getPlanVersion(), "planVersion"); + if (mapped.putIfAbsent(agentId, agent) != null) { + throw new IllegalArgumentException("Duplicate A2A exposed agentId: " + agentId); + } + if (agent.isDefaultAgent()) { + if (resolvedDefault != null) { + throw new IllegalArgumentException("Exactly one A2A exposed agent must be marked default"); + } + resolvedDefault = agent; + } + } + if (resolvedDefault == null) { + throw new IllegalArgumentException("One A2A exposed agent must be marked default"); + } + this.agents = List.copyOf(agents); + this.byId = Map.copyOf(mapped); + this.defaultAgent = resolvedDefault; + } + + public List agents() { + return agents; + } + + public A2AExposedAgentSpec defaultAgent() { + return defaultAgent; + } + + public A2AExposedAgentSpec requireAgent(String agentId) { + if (agentId == null || agentId.isBlank()) { + return defaultAgent; + } + A2AExposedAgentSpec resolved = byId.get(agentId.trim()); + if (resolved == null) { + throw new IllegalArgumentException("Unknown A2A exposed agent: " + agentId); + } + return resolved; + } + + private String required(String value, String field) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("A2A exposed agent " + field + " is required"); + } + return value.trim(); + } +} diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalogLoader.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalogLoader.java new file mode 100644 index 0000000..5b11225 --- /dev/null +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalogLoader.java @@ -0,0 +1,55 @@ +package io.dscope.camel.agent.a2a; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public final class A2AExposedAgentCatalogLoader { + + private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); + + public A2AExposedAgentCatalog load(String location) { + if (location == null || location.isBlank()) { + throw new IllegalArgumentException("agent.runtime.a2a.exposed-agents-config is required when A2A is enabled"); + } + try (InputStream stream = open(location.trim())) { + if (stream == null) { + throw new IllegalArgumentException("A2A exposed-agents config not found: " + location); + } + Root root = YAML_MAPPER.readValue(stream, Root.class); + List agents = root == null + ? List.of() + : root.agents != null && !root.agents.isEmpty() + ? root.agents + : root.exposedAgents; + return new A2AExposedAgentCatalog(agents == null ? List.of() : agents); + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException("Failed to load A2A exposed-agents config: " + location, e); + } + } + + private InputStream open(String location) throws Exception { + if (location.startsWith("classpath:")) { + return getClass().getClassLoader().getResourceAsStream(location.substring("classpath:".length())); + } + if (location.startsWith("http://") || location.startsWith("https://")) { + return new URL(location).openStream(); + } + if (location.startsWith("file:")) { + return Files.newInputStream(Path.of(URI.create(location))); + } + return Files.newInputStream(Path.of(location)); + } + + private static final class Root { + public List agents; + public List exposedAgents; + } +} diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentSpec.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentSpec.java new file mode 100644 index 0000000..4fc7488 --- /dev/null +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentSpec.java @@ -0,0 +1,89 @@ +package io.dscope.camel.agent.a2a; + +import java.util.List; +import java.util.Map; + +public class A2AExposedAgentSpec { + + private String agentId; + private String name; + private String description; + private String version; + private boolean defaultAgent; + private String planName; + private String planVersion; + private List skills = List.of(); + private Map metadata = Map.of(); + + public String getAgentId() { + return agentId; + } + + public void setAgentId(String agentId) { + this.agentId = agentId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public boolean isDefaultAgent() { + return defaultAgent; + } + + public void setDefaultAgent(boolean defaultAgent) { + this.defaultAgent = defaultAgent; + } + + public String getPlanName() { + return planName; + } + + public void setPlanName(String planName) { + this.planName = planName; + } + + public String getPlanVersion() { + return planVersion; + } + + public void setPlanVersion(String planVersion) { + this.planVersion = planVersion; + } + + public List getSkills() { + return skills == null ? List.of() : skills; + } + + public void setSkills(List skills) { + this.skills = skills == null ? List.of() : List.copyOf(skills); + } + + public Map getMetadata() { + return metadata == null ? Map.of() : metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? Map.of() : Map.copyOf(metadata); + } +} diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AAgentCardCatalog.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AAgentCardCatalog.java new file mode 100644 index 0000000..fac6966 --- /dev/null +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AAgentCardCatalog.java @@ -0,0 +1,132 @@ +package io.dscope.camel.agent.a2a; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dscope.camel.a2a.catalog.AgentCardCatalog; +import io.dscope.camel.a2a.catalog.AgentCardPolicyChecker; +import io.dscope.camel.a2a.catalog.AgentCardSignatureVerifier; +import io.dscope.camel.a2a.catalog.AgentCardSigner; +import io.dscope.camel.a2a.catalog.AllowAllAgentCardPolicyChecker; +import io.dscope.camel.a2a.catalog.AllowAllAgentCardSignatureVerifier; +import io.dscope.camel.a2a.catalog.NoopAgentCardSigner; +import io.dscope.camel.a2a.config.A2AProtocolMethods; +import io.dscope.camel.a2a.model.AgentCapabilities; +import io.dscope.camel.a2a.model.AgentCard; +import io.dscope.camel.a2a.model.AgentSecurityScheme; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; + +public final class AgentA2AAgentCardCatalog implements AgentCardCatalog { + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final A2AExposedAgentCatalog exposedAgentCatalog; + private final String endpointUrl; + private final AgentCardSigner signer; + private final AgentCardSignatureVerifier verifier; + private final AgentCardPolicyChecker policyChecker; + + public AgentA2AAgentCardCatalog(A2AExposedAgentCatalog exposedAgentCatalog, String endpointUrl) { + this(exposedAgentCatalog, endpointUrl, new NoopAgentCardSigner(), new AllowAllAgentCardSignatureVerifier(), new AllowAllAgentCardPolicyChecker()); + } + + public AgentA2AAgentCardCatalog(A2AExposedAgentCatalog exposedAgentCatalog, + String endpointUrl, + AgentCardSigner signer, + AgentCardSignatureVerifier verifier, + AgentCardPolicyChecker policyChecker) { + this.exposedAgentCatalog = exposedAgentCatalog; + this.endpointUrl = endpointUrl; + this.signer = signer == null ? new NoopAgentCardSigner() : signer; + this.verifier = verifier == null ? new AllowAllAgentCardSignatureVerifier() : verifier; + this.policyChecker = policyChecker == null ? new AllowAllAgentCardPolicyChecker() : policyChecker; + } + + @Override + public AgentCard getDiscoveryCard() { + AgentCard card = baseCard(false); + policyChecker.validate(card); + return card; + } + + @Override + public AgentCard getExtendedCard() { + AgentCard card = baseCard(true); + policyChecker.validate(card); + return card; + } + + @Override + public String getCardSignature(AgentCard card) { + try { + String canonical = objectMapper.writeValueAsString(card); + String signature = signer.sign(canonical); + if (signature != null && !verifier.verify(canonical, signature)) { + throw new IllegalStateException("Agent card signature verification failed"); + } + return signature; + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException("Failed to sign A2A agent card", e); + } + } + + private AgentCard baseCard(boolean extended) { + A2AExposedAgentSpec defaultAgent = exposedAgentCatalog.defaultAgent(); + + AgentCapabilities capabilities = new AgentCapabilities(); + capabilities.setStreaming(true); + capabilities.setPushNotifications(true); + capabilities.setStatefulTasks(true); + capabilities.setSupportedMethods(List.copyOf(new TreeSet<>(A2AProtocolMethods.CORE_METHODS))); + + AgentSecurityScheme bearer = new AgentSecurityScheme(); + bearer.setType("http"); + bearer.setScheme("bearer"); + bearer.setDescription("Bearer token authentication"); + bearer.setScopes(List.of("a2a.read", "a2a.write")); + + Map metadata = new LinkedHashMap<>(); + metadata.put("discovery", true); + metadata.put("extended", extended); + metadata.put("defaultAgentId", defaultAgent.getAgentId()); + metadata.put("agents", exposedAgentCatalog.agents().stream().map(this::cardMetadata).toList()); + + AgentCard card = new AgentCard(); + card.setAgentId(defaultAgent.getAgentId()); + card.setName(defaultAgent.getName()); + card.setDescription(defaultAgent.getDescription()); + card.setEndpointUrl(endpointUrl); + card.setVersion(defaultVersion(defaultAgent)); + card.setCapabilities(capabilities); + card.setSecuritySchemes(Map.of("bearerAuth", bearer)); + card.setDefaultInputModes(List.of("application/json", "text/plain")); + card.setDefaultOutputModes(List.of("application/json", "text/event-stream")); + card.setMetadata(metadata); + return card; + } + + private Map cardMetadata(A2AExposedAgentSpec spec) { + Map data = new LinkedHashMap<>(); + data.put("agentId", spec.getAgentId()); + data.put("name", spec.getName()); + data.put("description", spec.getDescription() == null ? "" : spec.getDescription()); + data.put("version", defaultVersion(spec)); + data.put("default", spec.isDefaultAgent()); + data.put("planName", spec.getPlanName()); + data.put("planVersion", spec.getPlanVersion()); + data.put("skills", spec.getSkills()); + if (!spec.getMetadata().isEmpty()) { + data.put("metadata", spec.getMetadata()); + } + return data; + } + + private String defaultVersion(A2AExposedAgentSpec spec) { + if (spec.getVersion() != null && !spec.getVersion().isBlank()) { + return spec.getVersion(); + } + return spec.getPlanVersion(); + } +} diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java new file mode 100644 index 0000000..6ed4f83 --- /dev/null +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java @@ -0,0 +1,551 @@ +package io.dscope.camel.agent.a2a; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dscope.camel.agent.api.PersistenceFacade; +import io.dscope.camel.agent.config.AgentHeaders; +import io.dscope.camel.agent.config.CorrelationKeys; +import io.dscope.camel.agent.registry.CorrelationRegistry; +import io.dscope.camel.agent.runtime.A2ARuntimeProperties; +import io.dscope.camel.agent.runtime.AgentPlanCatalog; +import io.dscope.camel.agent.runtime.AgentPlanSelectionResolver; +import io.dscope.camel.a2a.A2AComponentApplicationSupport; +import io.dscope.camel.a2a.catalog.AgentCardCatalog; +import io.dscope.camel.a2a.config.A2AExchangeProperties; +import io.dscope.camel.a2a.config.A2AProtocolMethods; +import io.dscope.camel.a2a.model.Message; +import io.dscope.camel.a2a.model.Part; +import io.dscope.camel.a2a.model.Task; +import io.dscope.camel.a2a.model.TaskSubscription; +import io.dscope.camel.a2a.model.dto.CancelTaskRequest; +import io.dscope.camel.a2a.model.dto.CancelTaskResponse; +import io.dscope.camel.a2a.model.dto.GetTaskRequest; +import io.dscope.camel.a2a.model.dto.GetTaskResponse; +import io.dscope.camel.a2a.model.dto.ListTasksRequest; +import io.dscope.camel.a2a.model.dto.ListTasksResponse; +import io.dscope.camel.a2a.model.dto.SendMessageRequest; +import io.dscope.camel.a2a.model.dto.SendMessageResponse; +import io.dscope.camel.a2a.model.dto.SendStreamingMessageRequest; +import io.dscope.camel.a2a.model.dto.SendStreamingMessageResponse; +import io.dscope.camel.a2a.model.dto.SubscribeToTaskRequest; +import io.dscope.camel.a2a.model.dto.SubscribeToTaskResponse; +import io.dscope.camel.a2a.processor.A2AErrorProcessor; +import io.dscope.camel.a2a.processor.A2AInvalidParamsException; +import io.dscope.camel.a2a.processor.A2AJsonRpcEnvelopeProcessor; +import io.dscope.camel.a2a.processor.A2AMethodDispatchProcessor; +import io.dscope.camel.a2a.processor.AgentCardDiscoveryProcessor; +import io.dscope.camel.a2a.processor.CreatePushNotificationConfigProcessor; +import io.dscope.camel.a2a.processor.DeletePushNotificationConfigProcessor; +import io.dscope.camel.a2a.processor.GetExtendedAgentCardProcessor; +import io.dscope.camel.a2a.processor.GetPushNotificationConfigProcessor; +import io.dscope.camel.a2a.processor.ListPushNotificationConfigsProcessor; +import io.dscope.camel.a2a.processor.A2ATaskSseProcessor; +import io.dscope.camel.a2a.service.A2APushNotificationConfigService; +import io.dscope.camel.a2a.service.InMemoryPushNotificationConfigService; +import io.dscope.camel.a2a.service.InMemoryTaskEventService; +import io.dscope.camel.a2a.service.WebhookPushNotificationNotifier; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.apache.camel.Exchange; +import org.apache.camel.Processor; +import org.apache.camel.ProducerTemplate; +import org.apache.camel.main.Main; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class AgentA2AProtocolSupport { + + private static final Logger LOGGER = LoggerFactory.getLogger(AgentA2AProtocolSupport.class); + + private AgentA2AProtocolSupport() { + } + + public static void bindIfEnabled(Main main, + A2ARuntimeProperties runtimeProperties, + PersistenceFacade persistenceFacade, + AgentPlanSelectionResolver planSelectionResolver, + ObjectMapper objectMapper) { + if (runtimeProperties == null || !runtimeProperties.enabled()) { + return; + } + A2AExposedAgentCatalog exposedAgentCatalog = + new A2AExposedAgentCatalogLoader().load(runtimeProperties.exposedAgentsConfig()); + validatePlanMappings(planSelectionResolver, runtimeProperties, exposedAgentCatalog); + + InMemoryTaskEventService taskEventService = new InMemoryTaskEventService(); + AgentA2ATaskRepository taskRepository = new AgentA2ATaskRepository(persistenceFacade, objectMapper, taskEventService); + A2APushNotificationConfigService pushConfigService = + new InMemoryPushNotificationConfigService(new WebhookPushNotificationNotifier()); + taskEventService.addListener(pushConfigService::onTaskEvent); + + AgentCardCatalog agentCardCatalog = + new AgentA2AAgentCardCatalog(exposedAgentCatalog, runtimeProperties.rpcEndpointUrl()); + + Processor sendMessageProcessor = new SendMessageProcessor( + runtimeProperties.agentEndpointUri(), + exposedAgentCatalog, + taskRepository, + objectMapper + ); + Processor sendStreamingMessageProcessor = new SendStreamingMessageProcessor( + sendMessageProcessor, + taskRepository, + taskEventService, + objectMapper, + runtimeProperties + ); + Processor getTaskProcessor = exchange -> { + GetTaskRequest request = objectMapper.convertValue(requiredParams(exchange, "GetTask requires params object"), GetTaskRequest.class); + if (request.getTaskId() == null || request.getTaskId().isBlank()) { + throw new A2AInvalidParamsException("GetTask requires taskId"); + } + Task task = taskRepository.getTask(request.getTaskId()); + GetTaskResponse response = new GetTaskResponse(); + response.setTask(task); + exchange.setProperty(A2AExchangeProperties.METHOD_RESULT, response); + }; + Processor listTasksProcessor = exchange -> { + Object params = exchange.getProperty(A2AExchangeProperties.NORMALIZED_PARAMS); + ListTasksRequest request = params == null ? new ListTasksRequest() : objectMapper.convertValue(params, ListTasksRequest.class); + if (request.getLimit() != null && request.getLimit() <= 0) { + throw new A2AInvalidParamsException("ListTasks limit must be greater than zero"); + } + ListTasksResponse response = new ListTasksResponse(); + response.setTasks(taskRepository.listTasks(request.getState(), request.getLimit())); + response.setNextCursor(null); + exchange.setProperty(A2AExchangeProperties.METHOD_RESULT, response); + }; + Processor cancelTaskProcessor = exchange -> { + CancelTaskRequest request = objectMapper.convertValue(requiredParams(exchange, "CancelTask requires params object"), CancelTaskRequest.class); + if (request.getTaskId() == null || request.getTaskId().isBlank()) { + throw new A2AInvalidParamsException("CancelTask requires taskId"); + } + Task task = taskRepository.cancelTask(request.getTaskId(), request.getReason()); + taskRepository.appendConversationEvent( + conversationId(task), + task.getTaskId(), + "conversation.a2a.task.canceled", + Map.of( + "taskId", task.getTaskId(), + "reason", request.getReason() == null ? "" : request.getReason() + ) + ); + CancelTaskResponse response = new CancelTaskResponse(); + response.setTask(task); + response.setCanceled(true); + exchange.setProperty(A2AExchangeProperties.METHOD_RESULT, response); + }; + Processor subscribeToTaskProcessor = exchange -> { + SubscribeToTaskRequest request = objectMapper.convertValue(requiredParams(exchange, "SubscribeToTask requires params object"), + SubscribeToTaskRequest.class); + if (request.getTaskId() == null || request.getTaskId().isBlank()) { + throw new A2AInvalidParamsException("SubscribeToTask requires taskId"); + } + taskRepository.getTask(request.getTaskId()); + long afterSequence = request.getAfterSequence() == null ? 0L : Math.max(0L, request.getAfterSequence()); + TaskSubscription subscription = taskEventService.createSubscription(request.getTaskId(), afterSequence); + SubscribeToTaskResponse response = new SubscribeToTaskResponse(); + response.setSubscriptionId(subscription.getSubscriptionId()); + response.setTaskId(request.getTaskId()); + response.setAfterSequence(afterSequence); + response.setTerminal(taskEventService.isTaskTerminal(request.getTaskId())); + response.setStreamUrl(buildStreamUrl(runtimeProperties, request.getTaskId(), subscription.getSubscriptionId(), afterSequence, request.getLimit())); + exchange.setProperty(A2AExchangeProperties.METHOD_RESULT, response); + }; + Processor createPushConfigProcessor = new CreatePushNotificationConfigProcessor(pushConfigService); + Processor getPushConfigProcessor = new GetPushNotificationConfigProcessor(pushConfigService); + Processor listPushConfigsProcessor = new ListPushNotificationConfigsProcessor(pushConfigService); + Processor deletePushConfigProcessor = new DeletePushNotificationConfigProcessor(pushConfigService); + Processor getExtendedAgentCardProcessor = new GetExtendedAgentCardProcessor(agentCardCatalog); + + Map methods = Map.ofEntries( + Map.entry(A2AProtocolMethods.SEND_MESSAGE, sendMessageProcessor), + Map.entry(A2AProtocolMethods.SEND_STREAMING_MESSAGE, sendStreamingMessageProcessor), + Map.entry(A2AProtocolMethods.GET_TASK, getTaskProcessor), + Map.entry(A2AProtocolMethods.LIST_TASKS, listTasksProcessor), + Map.entry(A2AProtocolMethods.CANCEL_TASK, cancelTaskProcessor), + Map.entry(A2AProtocolMethods.SUBSCRIBE_TO_TASK, subscribeToTaskProcessor), + Map.entry(A2AProtocolMethods.CREATE_PUSH_NOTIFICATION_CONFIG, createPushConfigProcessor), + Map.entry(A2AProtocolMethods.GET_PUSH_NOTIFICATION_CONFIG, getPushConfigProcessor), + Map.entry(A2AProtocolMethods.LIST_PUSH_NOTIFICATION_CONFIGS, listPushConfigsProcessor), + Map.entry(A2AProtocolMethods.DELETE_PUSH_NOTIFICATION_CONFIG, deletePushConfigProcessor), + Map.entry(A2AProtocolMethods.GET_EXTENDED_AGENT_CARD, getExtendedAgentCardProcessor) + ); + + bind(main, A2AComponentApplicationSupport.BEAN_TASK_EVENT_SERVICE, taskEventService); + bind(main, A2AComponentApplicationSupport.BEAN_PUSH_CONFIG_SERVICE, pushConfigService); + bind(main, A2AComponentApplicationSupport.BEAN_TASK_SERVICE, taskRepository); + bind(main, A2AComponentApplicationSupport.BEAN_AGENT_CARD_CATALOG, agentCardCatalog); + bind(main, A2AComponentApplicationSupport.BEAN_CREATE_PUSH_CONFIG_PROCESSOR, createPushConfigProcessor); + bind(main, A2AComponentApplicationSupport.BEAN_GET_PUSH_CONFIG_PROCESSOR, getPushConfigProcessor); + bind(main, A2AComponentApplicationSupport.BEAN_LIST_PUSH_CONFIGS_PROCESSOR, listPushConfigsProcessor); + bind(main, A2AComponentApplicationSupport.BEAN_DELETE_PUSH_CONFIG_PROCESSOR, deletePushConfigProcessor); + bind(main, A2AComponentApplicationSupport.BEAN_ENVELOPE_PROCESSOR, new A2AJsonRpcEnvelopeProcessor(A2AProtocolMethods.CORE_METHODS)); + bind(main, A2AComponentApplicationSupport.BEAN_ERROR_PROCESSOR, new A2AErrorProcessor()); + bind(main, A2AComponentApplicationSupport.BEAN_METHOD_PROCESSOR, new A2AMethodDispatchProcessor(methods)); + bind(main, A2AComponentApplicationSupport.BEAN_SSE_PROCESSOR, new A2ATaskSseProcessor(taskEventService)); + bind(main, A2AComponentApplicationSupport.BEAN_AGENT_CARD_DISCOVERY_PROCESSOR, new AgentCardDiscoveryProcessor(agentCardCatalog)); + bind(main, A2AComponentApplicationSupport.BEAN_GET_EXTENDED_AGENT_CARD_PROCESSOR, getExtendedAgentCardProcessor); + + LOGGER.info("Agent runtime A2A support bound: agents={}, rpc={}, agentCard={}", + exposedAgentCatalog.agents().size(), + runtimeProperties.rpcEndpointUrl(), + runtimeProperties.publicBaseUrl() + runtimeProperties.agentCardPath()); + } + + private static void validatePlanMappings(AgentPlanSelectionResolver planSelectionResolver, + A2ARuntimeProperties runtimeProperties, + A2AExposedAgentCatalog exposedAgentCatalog) { + if (planSelectionResolver == null) { + throw new IllegalArgumentException("A2A runtime requires AgentPlanSelectionResolver"); + } + if (runtimeProperties.plansConfig() == null || runtimeProperties.plansConfig().isBlank()) { + throw new IllegalArgumentException("A2A runtime requires agent.agents-config for exposed plan mappings"); + } + AgentPlanCatalog catalog = planSelectionResolver.loadCatalog(runtimeProperties.plansConfig()); + for (A2AExposedAgentSpec spec : exposedAgentCatalog.agents()) { + catalog.requireVersion(spec.getPlanName(), spec.getPlanVersion()); + } + } + + private static void bind(Main main, String beanName, Object bean) { + if (main.lookup(beanName, Object.class) == null) { + main.bind(beanName, bean); + } + } + + private static Object requiredParams(Exchange exchange, String message) { + Object params = exchange.getProperty(A2AExchangeProperties.NORMALIZED_PARAMS); + if (params == null) { + throw new A2AInvalidParamsException(message); + } + return params; + } + + private static String buildStreamUrl(A2ARuntimeProperties runtimeProperties, + String taskId, + String subscriptionId, + long afterSequence, + Integer limit) { + String suffix = limit == null ? "" : "&limit=" + limit; + return runtimeProperties.sseBaseUrl() + + "/" + + taskId + + "?subscriptionId=" + + subscriptionId + + "&afterSequence=" + + afterSequence + + suffix; + } + + private static String conversationId(Task task) { + if (task == null || task.getMetadata() == null) { + return ""; + } + Object camelAgent = task.getMetadata().get("camelAgent"); + if (camelAgent instanceof Map map) { + Object conversationId = map.get("localConversationId"); + return conversationId == null ? "" : String.valueOf(conversationId); + } + return ""; + } + + private static final class SendMessageProcessor implements Processor { + + private final String agentEndpointUri; + private final A2AExposedAgentCatalog exposedAgentCatalog; + private final AgentA2ATaskRepository taskRepository; + private final ObjectMapper objectMapper; + + private SendMessageProcessor(String agentEndpointUri, + A2AExposedAgentCatalog exposedAgentCatalog, + AgentA2ATaskRepository taskRepository, + ObjectMapper objectMapper) { + this.agentEndpointUri = agentEndpointUri; + this.exposedAgentCatalog = exposedAgentCatalog; + this.taskRepository = taskRepository; + this.objectMapper = objectMapper; + } + + @Override + public void process(Exchange exchange) throws Exception { + SendMessageRequest request = objectMapper.convertValue(requiredParams(exchange, "SendMessage requires params object"), SendMessageRequest.class); + if (request.getMessage() == null) { + throw new A2AInvalidParamsException("SendMessage requires message"); + } + String idempotencyKey = normalizeIdempotencyKey(request); + Task existing = taskRepository.existingByIdempotency(idempotencyKey); + if (existing != null) { + SendMessageResponse response = new SendMessageResponse(); + response.setTask(existing); + exchange.setProperty(A2AExchangeProperties.METHOD_RESULT, response); + return; + } + + A2AExposedAgentSpec exposedAgent = resolveExposedAgent(request); + String localConversationId = firstNonBlank( + metadataValue(request.getMetadata(), "linkedConversationId"), + metadataValue(request.getMetadata(), "camelAgent.linkedConversationId"), + UUID.randomUUID().toString() + ); + String parentConversationId = firstNonBlank( + metadataValue(request.getMetadata(), "parentConversationId"), + metadataValue(request.getMetadata(), "camelAgent.parentConversationId") + ); + String rootConversationId = firstNonBlank( + metadataValue(request.getMetadata(), "rootConversationId"), + metadataValue(request.getMetadata(), "camelAgent.rootConversationId"), + parentConversationId, + localConversationId + ); + String remoteConversationId = firstNonBlank(request.getConversationId(), metadataValue(request.getMetadata(), "remoteConversationId")); + String userText = extractMessageText(request.getMessage()); + String taskId = UUID.randomUUID().toString(); + + bindCorrelation(localConversationId, exposedAgent.getAgentId(), remoteConversationId, taskId, parentConversationId, rootConversationId); + + Map headers = new LinkedHashMap<>(); + headers.put(AgentHeaders.CONVERSATION_ID, localConversationId); + headers.put(AgentHeaders.PLAN_NAME, exposedAgent.getPlanName()); + headers.put(AgentHeaders.PLAN_VERSION, exposedAgent.getPlanVersion()); + headers.put(AgentHeaders.A2A_AGENT_ID, exposedAgent.getAgentId()); + headers.put(AgentHeaders.A2A_REMOTE_CONVERSATION_ID, remoteConversationId); + headers.put(AgentHeaders.A2A_REMOTE_TASK_ID, taskId); + headers.put(AgentHeaders.A2A_LINKED_CONVERSATION_ID, localConversationId); + headers.put(AgentHeaders.A2A_PARENT_CONVERSATION_ID, parentConversationId); + headers.put(AgentHeaders.A2A_ROOT_CONVERSATION_ID, rootConversationId); + + taskRepository.appendConversationEvent( + localConversationId, + taskId, + "conversation.a2a.request.accepted", + Map.of( + "agentId", exposedAgent.getAgentId(), + "planName", exposedAgent.getPlanName(), + "planVersion", exposedAgent.getPlanVersion(), + "remoteConversationId", remoteConversationId == null ? "" : remoteConversationId, + "parentConversationId", parentConversationId, + "rootConversationId", rootConversationId, + "message", userText + ) + ); + + String agentReply; + ProducerTemplate producerTemplate = exchange.getContext().createProducerTemplate(); + try { + Object response = producerTemplate.requestBodyAndHeaders(agentEndpointUri, userText, headers); + agentReply = response == null ? "" : String.valueOf(response); + } finally { + producerTemplate.stop(); + } + + Message assistantMessage = assistantMessage(request.getMessage(), agentReply); + Task task = taskRepository.createCompletedTask( + taskId, + idempotencyKey, + localConversationId, + request.getMessage(), + assistantMessage, + taskMetadata(exposedAgent, localConversationId, remoteConversationId, taskId, parentConversationId, rootConversationId, request.getMetadata()) + ); + + taskRepository.appendConversationEvent( + localConversationId, + taskId, + "conversation.a2a.response.completed", + Map.of( + "agentId", exposedAgent.getAgentId(), + "planName", exposedAgent.getPlanName(), + "planVersion", exposedAgent.getPlanVersion(), + "responseText", agentReply, + "linkedConversationId", localConversationId + ) + ); + + SendMessageResponse response = new SendMessageResponse(); + response.setTask(task); + exchange.setProperty(A2AExchangeProperties.METHOD_RESULT, response); + } + + private A2AExposedAgentSpec resolveExposedAgent(SendMessageRequest request) { + String requestedAgentId = firstNonBlank( + metadataValue(request.getMetadata(), "agentId"), + metadataValue(request.getMetadata(), "a2aAgentId"), + metadataValue(request.getMetadata(), "camelAgent.agentId"), + request.getMessage() == null ? "" : metadataValue(request.getMessage().getMetadata(), "agentId"), + request.getMessage() == null ? "" : metadataValue(request.getMessage().getMetadata(), "camelAgent.agentId") + ); + return exposedAgentCatalog.requireAgent(requestedAgentId); + } + + private void bindCorrelation(String conversationId, + String agentId, + String remoteConversationId, + String remoteTaskId, + String parentConversationId, + String rootConversationId) { + CorrelationRegistry registry = CorrelationRegistry.global(); + registry.bind(conversationId, CorrelationKeys.A2A_AGENT_ID, agentId); + registry.bind(conversationId, CorrelationKeys.A2A_LINKED_CONVERSATION_ID, conversationId); + if (remoteConversationId != null && !remoteConversationId.isBlank()) { + registry.bind(conversationId, CorrelationKeys.A2A_REMOTE_CONVERSATION_ID, remoteConversationId); + } + if (remoteTaskId != null && !remoteTaskId.isBlank()) { + registry.bind(conversationId, CorrelationKeys.A2A_REMOTE_TASK_ID, remoteTaskId); + } + if (parentConversationId != null && !parentConversationId.isBlank()) { + registry.bind(conversationId, CorrelationKeys.A2A_PARENT_CONVERSATION_ID, parentConversationId); + } + if (rootConversationId != null && !rootConversationId.isBlank()) { + registry.bind(conversationId, CorrelationKeys.A2A_ROOT_CONVERSATION_ID, rootConversationId); + } + } + + private Map taskMetadata(A2AExposedAgentSpec exposedAgent, + String localConversationId, + String remoteConversationId, + String taskId, + String parentConversationId, + String rootConversationId, + Map requestMetadata) { + Map metadata = new LinkedHashMap<>(); + if (requestMetadata != null && !requestMetadata.isEmpty()) { + metadata.putAll(requestMetadata); + } + Map camelAgent = new LinkedHashMap<>(); + camelAgent.put("agentId", exposedAgent.getAgentId()); + camelAgent.put("planName", exposedAgent.getPlanName()); + camelAgent.put("planVersion", exposedAgent.getPlanVersion()); + camelAgent.put("localConversationId", localConversationId); + camelAgent.put("linkedConversationId", localConversationId); + camelAgent.put("remoteConversationId", remoteConversationId == null ? "" : remoteConversationId); + camelAgent.put("remoteTaskId", taskId); + camelAgent.put("parentConversationId", parentConversationId == null ? "" : parentConversationId); + camelAgent.put("rootConversationId", rootConversationId == null ? "" : rootConversationId); + camelAgent.put("selectedAt", Instant.now().toString()); + metadata.put("camelAgent", camelAgent); + metadata.put("agentId", exposedAgent.getAgentId()); + metadata.put("planName", exposedAgent.getPlanName()); + metadata.put("planVersion", exposedAgent.getPlanVersion()); + metadata.put("linkedConversationId", localConversationId); + metadata.put("remoteConversationId", remoteConversationId == null ? "" : remoteConversationId); + metadata.put("remoteTaskId", taskId); + return metadata; + } + + private Message assistantMessage(Message requestMessage, String replyText) { + Message response = new Message(); + response.setMessageId(UUID.randomUUID().toString()); + response.setRole("assistant"); + response.setInReplyTo(requestMessage == null ? null : requestMessage.getMessageId()); + response.setCreatedAt(Instant.now().toString()); + Part part = new Part(); + part.setPartId(UUID.randomUUID().toString()); + part.setType("text"); + part.setMimeType("text/plain"); + part.setText(replyText == null ? "" : replyText); + response.setParts(List.of(part)); + return response; + } + + private String extractMessageText(Message message) { + if (message == null || message.getParts() == null) { + return ""; + } + return message.getParts().stream() + .filter(part -> part != null && part.getText() != null && !part.getText().isBlank()) + .map(Part::getText) + .reduce((left, right) -> left + "\n" + right) + .orElse(""); + } + + private String normalizeIdempotencyKey(SendMessageRequest request) { + if (request.getIdempotencyKey() != null && !request.getIdempotencyKey().isBlank()) { + return request.getIdempotencyKey().trim(); + } + if (request.getMessage() != null && request.getMessage().getMessageId() != null && !request.getMessage().getMessageId().isBlank()) { + return request.getMessage().getMessageId().trim(); + } + return ""; + } + + private String metadataValue(Map metadata, String key) { + if (metadata == null || metadata.isEmpty() || key == null || key.isBlank()) { + return ""; + } + try { + JsonNode root = objectMapper.valueToTree(metadata); + JsonNode current = root; + for (String part : key.split("\\.")) { + current = current.path(part); + } + if (current.isMissingNode() || current.isNull()) { + return ""; + } + return current.asText(""); + } catch (Exception ignored) { + return ""; + } + } + } + + private static final class SendStreamingMessageProcessor implements Processor { + + private final Processor sendMessageProcessor; + private final AgentA2ATaskRepository taskRepository; + private final InMemoryTaskEventService taskEventService; + private final ObjectMapper objectMapper; + private final A2ARuntimeProperties runtimeProperties; + + private SendStreamingMessageProcessor(Processor sendMessageProcessor, + AgentA2ATaskRepository taskRepository, + InMemoryTaskEventService taskEventService, + ObjectMapper objectMapper, + A2ARuntimeProperties runtimeProperties) { + this.sendMessageProcessor = sendMessageProcessor; + this.taskRepository = taskRepository; + this.taskEventService = taskEventService; + this.objectMapper = objectMapper; + this.runtimeProperties = runtimeProperties; + } + + @Override + public void process(Exchange exchange) throws Exception { + SendStreamingMessageRequest request = objectMapper.convertValue(requiredParams(exchange, "SendStreamingMessage requires params object"), + SendStreamingMessageRequest.class); + if (request.getMessage() == null) { + throw new A2AInvalidParamsException("SendStreamingMessage requires message"); + } + SendMessageRequest adapted = new SendMessageRequest(); + adapted.setMessage(request.getMessage()); + adapted.setConversationId(request.getConversationId()); + adapted.setIdempotencyKey(request.getIdempotencyKey()); + adapted.setMetadata(request.getMetadata()); + exchange.setProperty(A2AExchangeProperties.NORMALIZED_PARAMS, objectMapper.valueToTree(adapted)); + sendMessageProcessor.process(exchange); + SendMessageResponse sendMessageResponse = objectMapper.convertValue( + exchange.getProperty(A2AExchangeProperties.METHOD_RESULT), + SendMessageResponse.class + ); + Task task = sendMessageResponse.getTask(); + TaskSubscription subscription = taskEventService.createSubscription(task.getTaskId(), 0L); + SendStreamingMessageResponse response = new SendStreamingMessageResponse(); + response.setTask(taskRepository.getTask(task.getTaskId())); + response.setSubscriptionId(subscription.getSubscriptionId()); + response.setStreamUrl(buildStreamUrl(runtimeProperties, task.getTaskId(), subscription.getSubscriptionId(), 0L, null)); + exchange.setProperty(A2AExchangeProperties.METHOD_RESULT, response); + } + } + + private static String firstNonBlank(String... values) { + for (String value : values) { + if (value != null && !value.isBlank()) { + return value.trim(); + } + } + return ""; + } +} diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2ATaskRepository.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2ATaskRepository.java new file mode 100644 index 0000000..ac3bca6 --- /dev/null +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2ATaskRepository.java @@ -0,0 +1,227 @@ +package io.dscope.camel.agent.a2a; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.JsonNode; +import io.dscope.camel.agent.api.PersistenceFacade; +import io.dscope.camel.agent.model.AgentEvent; +import io.dscope.camel.agent.model.TaskState; +import io.dscope.camel.agent.model.TaskStatus; +import io.dscope.camel.a2a.model.Message; +import io.dscope.camel.a2a.model.Task; +import io.dscope.camel.a2a.processor.A2AInvalidParamsException; +import io.dscope.camel.a2a.service.InMemoryTaskEventService; +import java.time.Instant; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +public final class AgentA2ATaskRepository { + + private final PersistenceFacade persistenceFacade; + private final ObjectMapper objectMapper; + private final InMemoryTaskEventService eventService; + private final ConcurrentMap tasks = new ConcurrentHashMap<>(); + private final ConcurrentMap> historyByTaskId = new ConcurrentHashMap<>(); + private final ConcurrentMap idempotencyToTaskId = new ConcurrentHashMap<>(); + + public AgentA2ATaskRepository(PersistenceFacade persistenceFacade, + ObjectMapper objectMapper, + InMemoryTaskEventService eventService) { + this.persistenceFacade = persistenceFacade; + this.objectMapper = objectMapper; + this.eventService = eventService; + } + + public Task existingByIdempotency(String idempotencyKey) { + if (idempotencyKey == null || idempotencyKey.isBlank()) { + return null; + } + String taskId = idempotencyToTaskId.get(idempotencyKey.trim()); + return taskId == null ? null : getTask(taskId); + } + + public Task createCompletedTask(String taskId, + String idempotencyKey, + String conversationId, + Message requestMessage, + Message responseMessage, + Map metadata) { + String now = Instant.now().toString(); + Task task = new Task(); + task.setTaskId(taskId); + task.setCreatedAt(now); + task.setUpdatedAt(now); + task.setLatestMessage(responseMessage); + task.setMessages(List.of(requestMessage, responseMessage)); + task.setArtifacts(List.of()); + task.setMetadata(metadata == null ? Map.of() : Map.copyOf(metadata)); + + List history = new ArrayList<>(); + history.add(status(io.dscope.camel.a2a.model.TaskState.CREATED, "Task accepted")); + history.add(status(io.dscope.camel.a2a.model.TaskState.RUNNING, "Camel Agent is processing the message")); + io.dscope.camel.a2a.model.TaskStatus completed = + status(io.dscope.camel.a2a.model.TaskState.COMPLETED, "Camel Agent completed the task"); + history.add(completed); + task.setStatus(copyStatus(completed)); + + tasks.put(taskId, task); + historyByTaskId.put(taskId, history); + if (idempotencyKey != null && !idempotencyKey.isBlank()) { + idempotencyToTaskId.putIfAbsent(idempotencyKey.trim(), taskId); + } + publishHistory(task, history); + persistTask(conversationId, task); + return copyTask(task); + } + + public Task getTask(String taskId) { + Task current = tasks.get(taskId); + if (current != null) { + return copyTask(current); + } + Optional persisted = persistenceFacade == null ? Optional.empty() : persistenceFacade.loadTask(taskId); + if (persisted.isEmpty() || persisted.get().result() == null || persisted.get().result().isBlank()) { + throw new A2AInvalidParamsException("Task not found: " + taskId); + } + try { + Task task = objectMapper.readValue(persisted.get().result(), Task.class); + tasks.putIfAbsent(taskId, task); + return copyTask(task); + } catch (Exception e) { + throw new IllegalStateException("Failed to deserialize A2A task: " + taskId, e); + } + } + + public List listTasks(String state, Integer limit) { + List resolved = tasks.values().stream() + .filter(task -> state == null || state.isBlank() + || task.getStatus() != null + && task.getStatus().getState() != null + && task.getStatus().getState().name().equalsIgnoreCase(state)) + .map(this::copyTask) + .collect(Collectors.toCollection(ArrayList::new)); + if (limit != null && limit > 0 && resolved.size() > limit) { + return resolved.subList(0, limit); + } + return resolved; + } + + public Task cancelTask(String taskId, String reason) { + Task current = getTask(taskId); + if (current.getStatus() != null && current.getStatus().getState() == io.dscope.camel.a2a.model.TaskState.CANCELED) { + return current; + } + io.dscope.camel.a2a.model.TaskStatus canceled = + status(io.dscope.camel.a2a.model.TaskState.CANCELED, reason == null || reason.isBlank() ? "Task canceled" : reason); + current.setStatus(canceled); + current.setUpdatedAt(Instant.now().toString()); + tasks.put(taskId, current); + historyByTaskId.computeIfAbsent(taskId, ignored -> new ArrayList<>()).add(copyStatus(canceled)); + eventService.publishTaskUpdate(current); + persistTask(metadataText(current, "camelAgent.localConversationId"), current); + return copyTask(current); + } + + public List history(String taskId) { + getTask(taskId); + return historyByTaskId.getOrDefault(taskId, List.of()).stream().map(this::copyStatus).toList(); + } + + public void appendConversationEvent(String conversationId, + String taskId, + String type, + Map payload) { + if (persistenceFacade == null || conversationId == null || conversationId.isBlank()) { + return; + } + persistenceFacade.appendEvent( + new AgentEvent(conversationId, taskId, type, objectMapper.valueToTree(payload == null ? Map.of() : payload), Instant.now()), + UUID.randomUUID().toString() + ); + } + + private void publishHistory(Task task, List history) { + for (io.dscope.camel.a2a.model.TaskStatus status : history) { + Task copy = copyTask(task); + copy.setStatus(copyStatus(status)); + eventService.publishTaskUpdate(copy); + } + } + + private void persistTask(String conversationId, Task task) { + if (persistenceFacade == null || task == null || task.getTaskId() == null || task.getTaskId().isBlank()) { + return; + } + try { + persistenceFacade.saveTask(new TaskState( + task.getTaskId(), + conversationId == null ? "" : conversationId, + toAgentTaskStatus(task.getStatus()), + task.getStatus() == null || task.getStatus().getState() == null ? "" : task.getStatus().getState().name(), + null, + 0, + objectMapper.writeValueAsString(task) + )); + } catch (Exception e) { + throw new IllegalStateException("Failed to persist A2A task " + task.getTaskId(), e); + } + } + + private TaskStatus toAgentTaskStatus(io.dscope.camel.a2a.model.TaskStatus status) { + if (status == null || status.getState() == null) { + return TaskStatus.CREATED; + } + return switch (status.getState()) { + case CREATED, QUEUED -> TaskStatus.CREATED; + case RUNNING -> TaskStatus.STARTED; + case WAITING -> TaskStatus.WAITING; + case COMPLETED -> TaskStatus.FINISHED; + case FAILED, CANCELED -> TaskStatus.FAILED; + }; + } + + private io.dscope.camel.a2a.model.TaskStatus status(io.dscope.camel.a2a.model.TaskState state, String message) { + io.dscope.camel.a2a.model.TaskStatus status = new io.dscope.camel.a2a.model.TaskStatus(); + status.setState(state); + status.setMessage(message); + status.setUpdatedAt(Instant.now().toString()); + return status; + } + + private String metadataText(Task task, String path) { + try { + JsonNode metadata = objectMapper.valueToTree(task == null ? Map.of() : task.getMetadata()); + String[] parts = path.split("\\."); + JsonNode current = metadata; + for (String part : parts) { + current = current.path(part); + } + return current.isMissingNode() || current.isNull() ? "" : current.asText(""); + } catch (Exception ignored) { + return ""; + } + } + + private Task copyTask(Task source) { + try { + return objectMapper.readValue(objectMapper.writeValueAsBytes(source), Task.class); + } catch (Exception e) { + throw new IllegalStateException("Failed to copy A2A task", e); + } + } + + private io.dscope.camel.a2a.model.TaskStatus copyStatus(io.dscope.camel.a2a.model.TaskStatus source) { + io.dscope.camel.a2a.model.TaskStatus copy = new io.dscope.camel.a2a.model.TaskStatus(); + copy.setState(source.getState()); + copy.setMessage(source.getMessage()); + copy.setUpdatedAt(source.getUpdatedAt()); + copy.setDetails(source.getDetails()); + return copy; + } +} diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/audit/AuditConversationViewProcessor.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/audit/AuditConversationViewProcessor.java index 84d2623..0f9ec78 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/audit/AuditConversationViewProcessor.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/audit/AuditConversationViewProcessor.java @@ -80,6 +80,7 @@ public void process(Exchange exchange) throws Exception { List> perspective = new ArrayList<>(); String effectiveBlueprint = resolveBlueprint(conversationId); AuditMetadataSupport.AgentStepMetadata currentAgentState = AuditMetadataSupport.deriveAgentStepMetadata(events, effectiveBlueprint); + AuditMetadataSupport.A2ACorrelationMetadata currentA2aState = AuditMetadataSupport.deriveA2ACorrelation(events); AuditMetadataSupport.AgentStepMetadata stepAgentState = AuditMetadataSupport.AgentStepMetadata.fromBlueprint( effectiveBlueprint, AuditMetadataSupport.loadBlueprintMetadata(effectiveBlueprint) @@ -122,6 +123,7 @@ public void process(Exchange exchange) throws Exception { "messages", perspective )); response.put("agent", currentAgentState.asMap()); + response.put("a2a", currentA2aState.asMap()); exchange.getMessage().setHeader(Exchange.CONTENT_TYPE, "application/json"); exchange.getMessage().setBody(objectMapper.writeValueAsString(response)); diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/audit/AuditMetadataSupport.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/audit/AuditMetadataSupport.java index e4ca901..5f54606 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/audit/AuditMetadataSupport.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/audit/AuditMetadataSupport.java @@ -171,6 +171,18 @@ static Map buildConversationMetadata(String conversationId, metadata.put("eventCount", events.size()); metadata.put("firstEventAt", firstEventAt == null ? "" : firstEventAt.toString()); metadata.put("lastEventAt", lastEventAt == null ? "" : lastEventAt.toString()); + A2ACorrelationMetadata a2a = deriveA2ACorrelation(events); + if (a2a.present()) { + metadata.put("conversationKind", "a2a-linked"); + metadata.put("a2aAgentId", a2a.agentId()); + metadata.put("a2aRemoteConversationId", a2a.remoteConversationId()); + metadata.put("a2aRemoteTaskId", a2a.remoteTaskId()); + metadata.put("a2aLinkedConversationId", a2a.linkedConversationId()); + metadata.put("a2aParentConversationId", a2a.parentConversationId()); + metadata.put("a2aRootConversationId", a2a.rootConversationId()); + } else { + metadata.put("conversationKind", "standard"); + } if (!agentStepMetadata.planName().isBlank()) { metadata.put("planName", agentStepMetadata.planName()); } @@ -183,6 +195,21 @@ static Map buildConversationMetadata(String conversationId, return metadata; } + static A2ACorrelationMetadata deriveA2ACorrelation(List events) { + if (events == null) { + return A2ACorrelationMetadata.EMPTY; + } + A2ACorrelationMetadata current = A2ACorrelationMetadata.EMPTY; + for (AgentEvent event : events) { + JsonNode correlation = extractCorrelation(event == null ? null : event.payload()); + if (correlation == null) { + continue; + } + current = current.merge(correlation); + } + return current; + } + static AgentStepMetadata deriveAgentStepMetadata(List events, String blueprintUri) { AgentStepMetadata state = AgentStepMetadata.fromBlueprint(blueprintUri, loadBlueprintMetadata(blueprintUri)); if (events == null) { @@ -239,6 +266,24 @@ private static String text(JsonNode node, String field) { return value.isMissingNode() || value.isNull() ? "" : value.asText(""); } + private static JsonNode extractCorrelation(JsonNode payload) { + if (payload == null || payload.isNull() || payload.isMissingNode()) { + return null; + } + List candidates = List.of( + payload.path("correlation"), + payload.path("_correlation"), + payload.path("payload").path("correlation"), + payload.path("payload").path("_correlation") + ); + for (JsonNode candidate : candidates) { + if (candidate != null && candidate.isObject()) { + return candidate; + } + } + return null; + } + private static String firstNonBlank(String... values) { for (String value : values) { if (value != null && !value.isBlank()) { @@ -306,4 +351,56 @@ Map asMap() { return data; } } + + record A2ACorrelationMetadata(String agentId, + String remoteConversationId, + String remoteTaskId, + String linkedConversationId, + String parentConversationId, + String rootConversationId) { + private static final A2ACorrelationMetadata EMPTY = new A2ACorrelationMetadata("", "", "", "", "", ""); + + boolean present() { + return !agentId.isBlank() + || !remoteConversationId.isBlank() + || !remoteTaskId.isBlank() + || !linkedConversationId.isBlank() + || !parentConversationId.isBlank() + || !rootConversationId.isBlank(); + } + + A2ACorrelationMetadata merge(JsonNode node) { + return new A2ACorrelationMetadata( + firstNonBlank(agentId, text(node, "a2aAgentId")), + firstNonBlank(remoteConversationId, text(node, "a2aRemoteConversationId")), + firstNonBlank(remoteTaskId, text(node, "a2aRemoteTaskId")), + firstNonBlank(linkedConversationId, text(node, "a2aLinkedConversationId")), + firstNonBlank(parentConversationId, text(node, "a2aParentConversationId")), + firstNonBlank(rootConversationId, text(node, "a2aRootConversationId")) + ); + } + + Map asMap() { + Map data = new LinkedHashMap<>(); + if (!agentId.isBlank()) { + data.put("agentId", agentId); + } + if (!remoteConversationId.isBlank()) { + data.put("remoteConversationId", remoteConversationId); + } + if (!remoteTaskId.isBlank()) { + data.put("remoteTaskId", remoteTaskId); + } + if (!linkedConversationId.isBlank()) { + data.put("linkedConversationId", linkedConversationId); + } + if (!parentConversationId.isBlank()) { + data.put("parentConversationId", parentConversationId); + } + if (!rootConversationId.isBlank()) { + data.put("rootConversationId", rootConversationId); + } + return data; + } + } } diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/component/AgentProducer.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/component/AgentProducer.java index 85225e6..dcdbaad 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/component/AgentProducer.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/component/AgentProducer.java @@ -96,6 +96,12 @@ private void registerCorrelation(String conversationId, Exchange exchange) { String aguiSessionId = exchange.getMessage().getHeader(AgentHeaders.AGUI_SESSION_ID, String.class); String aguiRunId = exchange.getMessage().getHeader(AgentHeaders.AGUI_RUN_ID, String.class); String aguiThreadId = exchange.getMessage().getHeader(AgentHeaders.AGUI_THREAD_ID, String.class); + String a2aAgentId = exchange.getMessage().getHeader(AgentHeaders.A2A_AGENT_ID, String.class); + String a2aRemoteConversationId = exchange.getMessage().getHeader(AgentHeaders.A2A_REMOTE_CONVERSATION_ID, String.class); + String a2aRemoteTaskId = exchange.getMessage().getHeader(AgentHeaders.A2A_REMOTE_TASK_ID, String.class); + String a2aLinkedConversationId = exchange.getMessage().getHeader(AgentHeaders.A2A_LINKED_CONVERSATION_ID, String.class); + String a2aParentConversationId = exchange.getMessage().getHeader(AgentHeaders.A2A_PARENT_CONVERSATION_ID, String.class); + String a2aRootConversationId = exchange.getMessage().getHeader(AgentHeaders.A2A_ROOT_CONVERSATION_ID, String.class); if (aguiSessionId != null && !aguiSessionId.isBlank()) { correlationRegistry.bind(conversationId, CorrelationKeys.AGUI_SESSION_ID, aguiSessionId); @@ -106,5 +112,23 @@ private void registerCorrelation(String conversationId, Exchange exchange) { if (aguiThreadId != null && !aguiThreadId.isBlank()) { correlationRegistry.bind(conversationId, CorrelationKeys.AGUI_THREAD_ID, aguiThreadId); } + if (a2aAgentId != null && !a2aAgentId.isBlank()) { + correlationRegistry.bind(conversationId, CorrelationKeys.A2A_AGENT_ID, a2aAgentId); + } + if (a2aRemoteConversationId != null && !a2aRemoteConversationId.isBlank()) { + correlationRegistry.bind(conversationId, CorrelationKeys.A2A_REMOTE_CONVERSATION_ID, a2aRemoteConversationId); + } + if (a2aRemoteTaskId != null && !a2aRemoteTaskId.isBlank()) { + correlationRegistry.bind(conversationId, CorrelationKeys.A2A_REMOTE_TASK_ID, a2aRemoteTaskId); + } + if (a2aLinkedConversationId != null && !a2aLinkedConversationId.isBlank()) { + correlationRegistry.bind(conversationId, CorrelationKeys.A2A_LINKED_CONVERSATION_ID, a2aLinkedConversationId); + } + if (a2aParentConversationId != null && !a2aParentConversationId.isBlank()) { + correlationRegistry.bind(conversationId, CorrelationKeys.A2A_PARENT_CONVERSATION_ID, a2aParentConversationId); + } + if (a2aRootConversationId != null && !a2aRootConversationId.isBlank()) { + correlationRegistry.bind(conversationId, CorrelationKeys.A2A_ROOT_CONVERSATION_ID, a2aRootConversationId); + } } } diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/config/AgentHeaders.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/config/AgentHeaders.java index 3ec4b82..7478b98 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/config/AgentHeaders.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/config/AgentHeaders.java @@ -9,6 +9,12 @@ public final class AgentHeaders { public static final String AGUI_SESSION_ID = "agent.agui.sessionId"; public static final String AGUI_RUN_ID = "agent.agui.runId"; public static final String AGUI_THREAD_ID = "agent.agui.threadId"; + public static final String A2A_AGENT_ID = "agent.a2a.agentId"; + public static final String A2A_REMOTE_CONVERSATION_ID = "agent.a2a.remoteConversationId"; + public static final String A2A_REMOTE_TASK_ID = "agent.a2a.remoteTaskId"; + public static final String A2A_LINKED_CONVERSATION_ID = "agent.a2a.linkedConversationId"; + public static final String A2A_PARENT_CONVERSATION_ID = "agent.a2a.parentConversationId"; + public static final String A2A_ROOT_CONVERSATION_ID = "agent.a2a.rootConversationId"; public static final String PLAN_NAME = "agent.planName"; public static final String PLAN_VERSION = "agent.planVersion"; public static final String RESOLVED_PLAN_NAME = "agent.resolvedPlanName"; diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/config/CorrelationKeys.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/config/CorrelationKeys.java index 8797ef0..fb6f48e 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/config/CorrelationKeys.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/config/CorrelationKeys.java @@ -5,7 +5,13 @@ public final class CorrelationKeys { public static final String AGUI_SESSION_ID = "agui.sessionId"; public static final String AGUI_RUN_ID = "agui.runId"; public static final String AGUI_THREAD_ID = "agui.threadId"; + public static final String A2A_AGENT_ID = "a2a.agentId"; + public static final String A2A_REMOTE_CONVERSATION_ID = "a2a.remoteConversationId"; + public static final String A2A_REMOTE_TASK_ID = "a2a.remoteTaskId"; + public static final String A2A_LINKED_CONVERSATION_ID = "a2a.linkedConversationId"; + public static final String A2A_PARENT_CONVERSATION_ID = "a2a.parentConversationId"; + public static final String A2A_ROOT_CONVERSATION_ID = "a2a.rootConversationId"; private CorrelationKeys() { } -} \ No newline at end of file +} diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/A2ARuntimeProperties.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/A2ARuntimeProperties.java new file mode 100644 index 0000000..c2575b8 --- /dev/null +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/A2ARuntimeProperties.java @@ -0,0 +1,167 @@ +package io.dscope.camel.agent.runtime; + +import java.util.Properties; + +public record A2ARuntimeProperties( + boolean enabled, + String host, + int port, + String publicBaseUrl, + String rpcPath, + String ssePath, + String agentCardPath, + String agentEndpointUri, + String exposedAgentsConfig, + String plansConfig, + String legacyBlueprint +) { + public static A2ARuntimeProperties from(Properties properties) { + String plansConfig = property(properties, "agent.agents-config", ""); + String legacyBlueprint = property(properties, "agent.blueprint", "classpath:agents/support/agent.md"); + String host = firstNonBlank( + property(properties, "agent.runtime.a2a.host", ""), + property(properties, "agent.runtime.a2a.bind-host", ""), + property(properties, "agent.runtime.a2a.bindHost", ""), + "0.0.0.0" + ); + int port = intProperty( + properties, + firstPresentKey( + properties, + "agent.runtime.a2a.port", + "agent.runtime.a2a.bind-port", + "agent.runtime.a2a.bindPort", + "agent.audit.api.port" + ), + 8080 + ); + String publicBaseUrl = firstNonBlank( + property(properties, "agent.runtime.a2a.public-base-url", ""), + property(properties, "agent.runtime.a2a.publicBaseUrl", ""), + "http://localhost:" + port + ); + return new A2ARuntimeProperties( + booleanProperty(properties, "agent.runtime.a2a.enabled", false), + host, + port, + publicBaseUrl, + normalizePath(firstNonBlank( + property(properties, "agent.runtime.a2a.rpc-path", ""), + property(properties, "agent.runtime.a2a.rpcPath", ""), + "/a2a/rpc" + )), + normalizePath(firstNonBlank( + property(properties, "agent.runtime.a2a.sse-path", ""), + property(properties, "agent.runtime.a2a.ssePath", ""), + "/a2a/sse" + )), + normalizePath(firstNonBlank( + property(properties, "agent.runtime.a2a.agent-card-path", ""), + property(properties, "agent.runtime.a2a.agentCardPath", ""), + "/.well-known/agent-card.json" + )), + firstNonBlank( + property(properties, "agent.runtime.a2a.agent-endpoint-uri", ""), + property(properties, "agent.runtime.a2a.agentEndpointUri", ""), + defaultAgentEndpointUri(plansConfig, legacyBlueprint) + ), + firstNonBlank( + property(properties, "agent.runtime.a2a.exposed-agents-config", ""), + property(properties, "agent.runtime.a2a.exposedAgentsConfig", "") + ), + plansConfig, + legacyBlueprint + ); + } + + public String rpcEndpointUrl() { + return trimTrailingSlash(publicBaseUrl) + normalizePath(rpcPath); + } + + public String sseBaseUrl() { + return trimTrailingSlash(publicBaseUrl) + normalizePath(ssePath); + } + + private static String defaultAgentEndpointUri(String plansConfig, String legacyBlueprint) { + StringBuilder uri = new StringBuilder("agent:a2a"); + if ((plansConfig != null && !plansConfig.isBlank()) || (legacyBlueprint != null && !legacyBlueprint.isBlank())) { + uri.append('?'); + boolean hasQuery = false; + if (plansConfig != null && !plansConfig.isBlank()) { + uri.append("plansConfig=").append(plansConfig); + hasQuery = true; + } + if (legacyBlueprint != null && !legacyBlueprint.isBlank()) { + if (hasQuery) { + uri.append('&'); + } + uri.append("blueprint=").append(legacyBlueprint); + } + } + return uri.toString(); + } + + private static String property(Properties properties, String key, String fallback) { + String value = properties == null ? null : properties.getProperty(key); + return value == null || value.isBlank() ? fallback : value.trim(); + } + + private static int intProperty(Properties properties, String key, int fallback) { + if (key == null || key.isBlank()) { + return fallback; + } + String value = properties.getProperty(key); + if (value == null || value.isBlank()) { + return fallback; + } + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException ignored) { + return fallback; + } + } + + private static boolean booleanProperty(Properties properties, String key, boolean fallback) { + String value = properties == null ? null : properties.getProperty(key); + if (value == null || value.isBlank()) { + return fallback; + } + return Boolean.parseBoolean(value.trim()); + } + + private static String firstPresentKey(Properties properties, String... keys) { + for (String key : keys) { + if (properties != null && properties.getProperty(key) != null) { + return key; + } + } + return keys.length == 0 ? null : keys[0]; + } + + private static String firstNonBlank(String... values) { + for (String value : values) { + if (value != null && !value.isBlank()) { + return value.trim(); + } + } + return ""; + } + + private static String normalizePath(String value) { + if (value == null || value.isBlank()) { + return "/"; + } + return value.startsWith("/") ? value.trim() : "/" + value.trim(); + } + + private static String trimTrailingSlash(String value) { + if (value == null || value.isBlank()) { + return ""; + } + String trimmed = value.trim(); + if (trimmed.endsWith("/")) { + return trimmed.substring(0, trimmed.length() - 1); + } + return trimmed; + } +} diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentA2ARuntimeRouteBuilder.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentA2ARuntimeRouteBuilder.java new file mode 100644 index 0000000..9970f1b --- /dev/null +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentA2ARuntimeRouteBuilder.java @@ -0,0 +1,44 @@ +package io.dscope.camel.agent.runtime; + +import io.dscope.camel.a2a.A2AComponentApplicationSupport; +import org.apache.camel.builder.RouteBuilder; + +public class AgentA2ARuntimeRouteBuilder extends RouteBuilder { + + private final A2ARuntimeProperties properties; + + public AgentA2ARuntimeRouteBuilder(A2ARuntimeProperties properties) { + this.properties = properties; + } + + @Override + public void configure() { + onException(Exception.class) + .handled(true) + .to("bean:" + A2AComponentApplicationSupport.BEAN_ERROR_PROCESSOR); + + from(undertowUri(properties.rpcPath(), "POST")) + .routeId("agent-a2a-rpc") + .convertBodyTo(String.class) + .to("bean:" + A2AComponentApplicationSupport.BEAN_ENVELOPE_PROCESSOR) + .to("bean:" + A2AComponentApplicationSupport.BEAN_METHOD_PROCESSOR); + + from(undertowUri(properties.ssePath() + "/{taskId}", "GET")) + .routeId("agent-a2a-sse") + .to("bean:" + A2AComponentApplicationSupport.BEAN_SSE_PROCESSOR); + + from(undertowUri(properties.agentCardPath(), "GET")) + .routeId("agent-a2a-agent-card") + .to("bean:" + A2AComponentApplicationSupport.BEAN_AGENT_CARD_DISCOVERY_PROCESSOR); + } + + private String undertowUri(String path, String method) { + return "undertow:http://" + + properties.host() + + ":" + + properties.port() + + path + + "?httpMethodRestrict=" + + method; + } +} diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentRuntimeBootstrap.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentRuntimeBootstrap.java index 06947a3..d8ab157 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentRuntimeBootstrap.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentRuntimeBootstrap.java @@ -1,6 +1,7 @@ package io.dscope.camel.agent.runtime; import com.fasterxml.jackson.databind.ObjectMapper; +import io.dscope.camel.agent.a2a.AgentA2AProtocolSupport; import io.dscope.camel.agent.agui.AgentAgUiPreRunTextProcessor; import io.dscope.camel.agent.audit.AuditAgentBlueprintProcessor; import io.dscope.camel.agent.audit.AuditAgentCatalogProcessor; @@ -165,7 +166,7 @@ public static void bootstrap(Main main, String applicationYamlPath) throws Excep ); bindAiModelClientIfConfigured(main, properties, objectMapper); - bindOptionalAgUiAndRealtime(main, properties); + bindOptionalAgUiRealtimeAndA2a(main, properties, persistenceFacade, planSelectionResolver, objectMapper); boolean agentRoutesEnabled = Boolean.parseBoolean(properties.getProperty("agent.runtime.agent-routes-enabled", "true")); if (agentRoutesEnabled) { @@ -174,6 +175,13 @@ public static void bootstrap(Main main, String applicationYamlPath) throws Excep } else { LOGGER.info("Agent runtime route builder disabled by configuration"); } + A2ARuntimeProperties a2aRuntimeProperties = A2ARuntimeProperties.from(properties); + if (a2aRuntimeProperties.enabled()) { + main.configure().addRoutesBuilder(new AgentA2ARuntimeRouteBuilder(a2aRuntimeProperties)); + LOGGER.info("Agent runtime A2A route builder enabled"); + } else { + LOGGER.info("Agent runtime A2A route builder disabled by configuration"); + } LOGGER.info("Agent runtime bootstrap completed"); } @@ -444,7 +452,11 @@ private static void bindAiModelClientIfConfigured(Main main, Properties properti } } - private static void bindOptionalAgUiAndRealtime(Main main, Properties properties) { + private static void bindOptionalAgUiRealtimeAndA2a(Main main, + Properties properties, + PersistenceFacade persistenceFacade, + AgentPlanSelectionResolver planSelectionResolver, + ObjectMapper objectMapper) { if (booleanPropertyWithAliases(properties, true, "agent.runtime.agui.bind-default-beans", "agent.runtime.agui.bindDefaultBeans" @@ -542,6 +554,14 @@ && isBlank(properties.getProperty("agent.runtime.realtime.requireInitSession"))) ); bindSipProcessorsIfMissing(main, sipInitBeanName, sipTranscriptBeanName, sipCallEndBeanName); } + + AgentA2AProtocolSupport.bindIfEnabled( + main, + A2ARuntimeProperties.from(properties), + persistenceFacade, + planSelectionResolver, + objectMapper + ); } private static void bindAgUiDefaultsIfAvailable(Main main) { diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/RuntimeResourceBootstrapper.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/RuntimeResourceBootstrapper.java index 182995a..f2930ea 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/RuntimeResourceBootstrapper.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/RuntimeResourceBootstrapper.java @@ -24,6 +24,7 @@ final class RuntimeResourceBootstrapper { private static final String PROP_BLUEPRINT = "agent.blueprint"; private static final String PROP_AGENTS_CONFIG = "agent.agents-config"; + private static final String PROP_A2A_EXPOSED_AGENTS_CONFIG = "agent.runtime.a2a.exposed-agents-config"; private static final String PROP_ROUTES_PATTERN = "agent.runtime.routes-include-pattern"; private static final String PROP_KAMELET_URLS = "agent.runtime.kamelets-include-pattern"; private static final String PROP_KAMELET_URLS_ALIAS = "agent.runtime.kameletsIncludePattern"; @@ -38,6 +39,7 @@ static Properties resolve(Properties source) { BootstrapWorkspace workspace = new BootstrapWorkspace(); resolveAgentsConfig(resolved, workspace); + resolveA2aExposedAgentsConfig(resolved, workspace); resolveBlueprint(resolved, workspace); resolveRoutes(resolved, workspace); resolveKamelets(resolved, workspace); @@ -70,6 +72,17 @@ private static void resolveAgentsConfig(Properties properties, BootstrapWorkspac stageRemoteBlueprintsFromCatalog(properties, workspace, replacement); } + private static void resolveA2aExposedAgentsConfig(Properties properties, BootstrapWorkspace workspace) { + String config = trimToNull(properties.getProperty(PROP_A2A_EXPOSED_AGENTS_CONFIG)); + if (config == null || !isHttpUrl(config)) { + return; + } + Path staged = workspace.downloadTo("agents", config, ".yaml"); + String replacement = toFileUri(staged); + properties.setProperty(PROP_A2A_EXPOSED_AGENTS_CONFIG, replacement); + LOGGER.info("Runtime resource bootstrap: staged a2a exposed-agents config {} -> {}", config, replacement); + } + private static void stageRemoteBlueprintsFromCatalog(Properties properties, BootstrapWorkspace workspace, String catalogLocation) { try { AgentPlanCatalog catalog = new AgentPlanCatalogLoader().load(catalogLocation); diff --git a/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalogLoaderTest.java b/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalogLoaderTest.java new file mode 100644 index 0000000..17198d4 --- /dev/null +++ b/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalogLoaderTest.java @@ -0,0 +1,63 @@ +package io.dscope.camel.agent.a2a; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class A2AExposedAgentCatalogLoaderTest { + + private final A2AExposedAgentCatalogLoader loader = new A2AExposedAgentCatalogLoader(); + + @TempDir + Path tempDir; + + @Test + void loadsAndValidatesExposedAgentsCatalog() throws Exception { + Path config = tempDir.resolve("a2a-agents.yaml"); + Files.writeString(config, """ + agents: + - agentId: support-public + name: Support Public Agent + description: Handles support conversations + defaultAgent: true + version: 1.0.0 + planName: support + planVersion: v1 + skills: + - support + - troubleshooting + - agentId: billing-public + name: Billing Public Agent + description: Handles billing + planName: billing + planVersion: v2 + """); + + A2AExposedAgentCatalog catalog = loader.load(config.toString()); + + assertEquals(2, catalog.agents().size()); + assertEquals("support-public", catalog.defaultAgent().getAgentId()); + assertEquals("billing", catalog.requireAgent("billing-public").getPlanName()); + assertTrue(catalog.requireAgent("").isDefaultAgent()); + } + + @Test + void rejectsCatalogWithoutDefaultAgent() throws Exception { + Path config = tempDir.resolve("a2a-agents-no-default.yaml"); + Files.writeString(config, """ + agents: + - agentId: support-public + name: Support Public Agent + planName: support + planVersion: v1 + """); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> loader.load(config.toString())); + assertTrue(error.getMessage().contains("default")); + } +} diff --git a/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2ATaskRepositoryTest.java b/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2ATaskRepositoryTest.java new file mode 100644 index 0000000..3fb05ba --- /dev/null +++ b/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2ATaskRepositoryTest.java @@ -0,0 +1,79 @@ +package io.dscope.camel.agent.a2a; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dscope.camel.agent.kernel.InMemoryPersistenceFacade; +import io.dscope.camel.a2a.model.Message; +import io.dscope.camel.a2a.model.Part; +import io.dscope.camel.a2a.model.Task; +import io.dscope.camel.a2a.model.TaskState; +import io.dscope.camel.a2a.processor.A2AInvalidParamsException; +import io.dscope.camel.a2a.service.InMemoryTaskEventService; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class AgentA2ATaskRepositoryTest { + + @Test + void createsPersistsAndCancelsTasks() { + InMemoryPersistenceFacade persistence = new InMemoryPersistenceFacade(); + AgentA2ATaskRepository repository = + new AgentA2ATaskRepository(persistence, new ObjectMapper(), new InMemoryTaskEventService()); + + Task created = repository.createCompletedTask( + "task-1", + "idem-1", + "conv-1", + message("user", "Need help"), + message("assistant", "Here is the answer"), + metadata("conv-1") + ); + + assertEquals(TaskState.COMPLETED, created.getStatus().getState()); + assertEquals(1, repository.listTasks(null, null).size()); + assertEquals("task-1", repository.existingByIdempotency("idem-1").getTaskId()); + + Task canceled = repository.cancelTask("task-1", "caller canceled"); + assertEquals(TaskState.CANCELED, canceled.getStatus().getState()); + assertEquals(TaskState.CANCELED, repository.getTask("task-1").getStatus().getState()); + + repository.appendConversationEvent("conv-1", "task-1", "conversation.a2a.request.accepted", Map.of("agentId", "support-public")); + assertEquals(1, persistence.loadConversation("conv-1", 10).size()); + } + + @Test + void failsForUnknownTask() { + AgentA2ATaskRepository repository = + new AgentA2ATaskRepository(new InMemoryPersistenceFacade(), new ObjectMapper(), new InMemoryTaskEventService()); + + assertThrows(A2AInvalidParamsException.class, () -> repository.getTask("missing-task")); + } + + private static Map metadata(String conversationId) { + Map metadata = new LinkedHashMap<>(); + Map camelAgent = new LinkedHashMap<>(); + camelAgent.put("localConversationId", conversationId); + metadata.put("camelAgent", camelAgent); + metadata.put("agentId", "support-public"); + return metadata; + } + + private static Message message(String role, String text) { + Part part = new Part(); + part.setType("text"); + part.setMimeType("text/plain"); + part.setText(text); + + Message message = new Message(); + message.setRole(role); + message.setParts(List.of(part)); + message.setMessageId(role + "-msg"); + assertNotNull(message.getMessageId()); + return message; + } +} diff --git a/camel-agent-persistence-dscope/src/main/java/io/dscope/camel/agent/persistence/dscope/DscopePersistenceFacade.java b/camel-agent-persistence-dscope/src/main/java/io/dscope/camel/agent/persistence/dscope/DscopePersistenceFacade.java index 4e3ce09..78088a1 100644 --- a/camel-agent-persistence-dscope/src/main/java/io/dscope/camel/agent/persistence/dscope/DscopePersistenceFacade.java +++ b/camel-agent-persistence-dscope/src/main/java/io/dscope/camel/agent/persistence/dscope/DscopePersistenceFacade.java @@ -335,8 +335,22 @@ private ObjectNode correlationMetadata(String conversationId) { String aguiSessionId = registry.resolve(conversationId, CorrelationKeys.AGUI_SESSION_ID, null); String aguiRunId = registry.resolve(conversationId, CorrelationKeys.AGUI_RUN_ID, null); String aguiThreadId = registry.resolve(conversationId, CorrelationKeys.AGUI_THREAD_ID, null); - - if (isBlank(aguiSessionId) && isBlank(aguiRunId) && isBlank(aguiThreadId)) { + String a2aAgentId = registry.resolve(conversationId, CorrelationKeys.A2A_AGENT_ID, null); + String a2aRemoteConversationId = registry.resolve(conversationId, CorrelationKeys.A2A_REMOTE_CONVERSATION_ID, null); + String a2aRemoteTaskId = registry.resolve(conversationId, CorrelationKeys.A2A_REMOTE_TASK_ID, null); + String a2aLinkedConversationId = registry.resolve(conversationId, CorrelationKeys.A2A_LINKED_CONVERSATION_ID, null); + String a2aParentConversationId = registry.resolve(conversationId, CorrelationKeys.A2A_PARENT_CONVERSATION_ID, null); + String a2aRootConversationId = registry.resolve(conversationId, CorrelationKeys.A2A_ROOT_CONVERSATION_ID, null); + + if (isBlank(aguiSessionId) + && isBlank(aguiRunId) + && isBlank(aguiThreadId) + && isBlank(a2aAgentId) + && isBlank(a2aRemoteConversationId) + && isBlank(a2aRemoteTaskId) + && isBlank(a2aLinkedConversationId) + && isBlank(a2aParentConversationId) + && isBlank(a2aRootConversationId)) { return null; } @@ -350,6 +364,24 @@ private ObjectNode correlationMetadata(String conversationId) { if (!isBlank(aguiThreadId)) { correlation.put("aguiThreadId", aguiThreadId); } + if (!isBlank(a2aAgentId)) { + correlation.put("a2aAgentId", a2aAgentId); + } + if (!isBlank(a2aRemoteConversationId)) { + correlation.put("a2aRemoteConversationId", a2aRemoteConversationId); + } + if (!isBlank(a2aRemoteTaskId)) { + correlation.put("a2aRemoteTaskId", a2aRemoteTaskId); + } + if (!isBlank(a2aLinkedConversationId)) { + correlation.put("a2aLinkedConversationId", a2aLinkedConversationId); + } + if (!isBlank(a2aParentConversationId)) { + correlation.put("a2aParentConversationId", a2aParentConversationId); + } + if (!isBlank(a2aRootConversationId)) { + correlation.put("a2aRootConversationId", a2aRootConversationId); + } return correlation; } diff --git a/docs/pr-drafts/a2a-runtime-integration.md b/docs/pr-drafts/a2a-runtime-integration.md new file mode 100644 index 0000000..6106c03 --- /dev/null +++ b/docs/pr-drafts/a2a-runtime-integration.md @@ -0,0 +1,91 @@ +# A2A Runtime Integration PR Draft + +## Title + +`feat(a2a): add A2A protocol integration to Camel Agent runtime` + +## Description + +## Summary + +This PR adds first-class A2A protocol support to Camel Agent by integrating the existing `camel-a2a-component` into the agent runtime. + +The goal is to let Camel Agent: +- call remote A2A agents as part of tool-driven workflows +- expose local agents over A2A using agent-card based routing +- maintain A2A task/conversation state separately from end-user conversations +- persist A2A correlation and audit metadata across inbound and outbound flows + +## What’s Included + +### Runtime and configuration +- Add `agent.runtime.a2a.*` configuration +- Add exposed A2A agent mapping config separate from `agents.yaml` +- Bind A2A runtime beans and HTTP routes during `AgentRuntimeBootstrap` + +### Inbound A2A support +- Expose: + - `POST /a2a/rpc` + - `GET /a2a/sse/{taskId}` + - `GET /.well-known/agent-card.json` +- Route inbound A2A requests through explicit public agent-card mappings +- Resolve mapped local `{planName, planVersion}` and invoke the local `agent:` component +- Create separate linked local conversations for A2A tasks + +### Outbound A2A support +- Support blueprint tools targeting `a2a:` endpoints +- Persist remote A2A conversation/task identifiers locally +- Propagate local correlation metadata for audit/debugging +- Support follow-up task operations without losing local/remote continuity + +### Audit and correlation +- Add A2A-specific correlation keys +- Record: + - inbound A2A request acceptance + - selected public A2A agent + - mapped local plan/version + - outbound remote A2A call lifecycle + - local/remote conversation and task linkage +- Extend conversation/audit views to show A2A-linked conversations + +## Design choices + +- `agents.yaml` remains the internal local plan catalog +- public A2A exposure uses a separate `agent.runtime.a2a.exposed-agents` mapping +- only explicitly exposed agents are published via agent cards +- inbound routing is based on public agent-card mapping, not payload-supplied plan selection +- A2A conversations are separate local conversations linked to a parent/root conversation when applicable +- v1 supports both A2A client and server behavior +- v1 supports the full current A2A core method surface exposed by `camel-a2a-component` + +## Test plan + +- bootstrap binds A2A runtime beans and routes when enabled +- exposed agent config validates mapped local plan/version entries +- agent-card discovery returns only explicitly exposed agents +- inbound `SendMessage` resolves correct local plan/version +- inbound A2A requests create linked local conversations +- outbound A2A tool calls persist remote task/conversation identifiers +- follow-up A2A task methods preserve local/remote correlation +- audit search/view show A2A-linked conversation metadata +- existing non-A2A AGUI/realtime/agent flows remain unchanged when A2A is disabled + +## Notes + +This PR intentionally reuses the current permissive A2A signer/verifier/policy defaults from `camel-a2a-component`. +Security hardening can follow in a later iteration once the base protocol bridge is stable. + +## Suggested Checklist + +- [ ] Added `agent.runtime.a2a.*` runtime configuration +- [ ] Added exposed-agent mapping separate from `agents.yaml` +- [ ] Bound A2A server routes in runtime bootstrap +- [ ] Added inbound agent-card to local plan/version routing +- [ ] Added outbound `a2a:` tool correlation handling +- [ ] Extended audit and conversation views with A2A metadata +- [ ] Added sample configuration and sample route coverage +- [ ] Added integration and regression tests + +## Suggested Branch Name + +- `feat/a2a-runtime-integration` diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..098f5b1 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,7 @@ +# Roadmap + +## Next Version + +- A2A protocol integration for Camel Agent + Draft PR: [docs/pr-drafts/a2a-runtime-integration.md](/Users/roman/.codex/worktrees/6e77/CamelAIAgentComponent/docs/pr-drafts/a2a-runtime-integration.md) + Scope: first-class inbound and outbound A2A support, exposed-agent mapping, linked A2A conversations, and audit/correlation coverage. diff --git a/pom.xml b/pom.xml index 0ee32cb..14b8870 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ 2.20.0 5.10.2 1.1.0 + 0.5.0-SNAPSHOT 1.0.3 3.5.11 6.2.0 From 1f144526b40feb86e2d3ea4af5561c058b103320 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 12 Mar 2026 17:01:00 -0700 Subject: [PATCH 02/19] feat: add outbound A2A tool client support --- .../dscope/camel/agent/a2a/A2AToolClient.java | 352 ++++++++++++++++++ .../camel/agent/a2a/A2AToolContext.java | 10 + .../camel/agent/component/AgentComponent.java | 20 +- .../agent/executor/CamelToolExecutor.java | 23 ++ .../TemplateAwareCamelToolExecutor.java | 13 +- .../camel/agent/a2a/A2AToolClientTest.java | 279 ++++++++++++++ .../agent/starter/AgentAutoConfiguration.java | 42 ++- 7 files changed, 726 insertions(+), 13 deletions(-) create mode 100644 camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolClient.java create mode 100644 camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolContext.java create mode 100644 camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/A2AToolClientTest.java diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolClient.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolClient.java new file mode 100644 index 0000000..3bab8c5 --- /dev/null +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolClient.java @@ -0,0 +1,352 @@ +package io.dscope.camel.agent.a2a; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.dscope.camel.agent.api.PersistenceFacade; +import io.dscope.camel.agent.config.CorrelationKeys; +import io.dscope.camel.agent.model.AgentEvent; +import io.dscope.camel.agent.model.ExecutionContext; +import io.dscope.camel.agent.model.ToolResult; +import io.dscope.camel.agent.model.ToolSpec; +import io.dscope.camel.agent.registry.CorrelationRegistry; +import io.dscope.camel.a2a.A2AEndpoint; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.apache.camel.CamelContext; + +public final class A2AToolClient { + + private final CamelContext camelContext; + private final ObjectMapper objectMapper; + private final PersistenceFacade persistenceFacade; + private final A2AToolContext toolContext; + private final HttpClient httpClient; + + public A2AToolClient(CamelContext camelContext, + ObjectMapper objectMapper, + PersistenceFacade persistenceFacade, + A2AToolContext toolContext) { + this(camelContext, objectMapper, persistenceFacade, toolContext, HttpClient.newHttpClient()); + } + + A2AToolClient(CamelContext camelContext, + ObjectMapper objectMapper, + PersistenceFacade persistenceFacade, + A2AToolContext toolContext, + HttpClient httpClient) { + this.camelContext = camelContext; + this.objectMapper = objectMapper; + this.persistenceFacade = persistenceFacade; + this.toolContext = toolContext == null ? A2AToolContext.EMPTY : toolContext; + this.httpClient = httpClient == null ? HttpClient.newHttpClient() : httpClient; + } + + public ToolResult execute(String target, ToolSpec toolSpec, JsonNode arguments, ExecutionContext context) { + if (camelContext == null) { + throw new IllegalStateException("A2A tool execution requires CamelContext"); + } + try { + A2AEndpoint endpoint = camelContext.getEndpoint(target, A2AEndpoint.class); + if (endpoint == null) { + throw new IllegalArgumentException("Unable to resolve A2A endpoint: " + target); + } + String remoteRpcUrl = normalizeRpcUrl(endpoint.getConfiguration().getRemoteUrl()); + String remoteAgentId = endpoint.getAgent(); + String method = determineMethod(arguments); + ObjectNode params = buildParams(arguments, method, remoteAgentId, context); + String remoteConversationId = text(params, "conversationId"); + + appendEvent(context, "conversation.a2a.outbound.started", Map.of( + "toolName", toolSpec.name(), + "target", target, + "remoteUrl", remoteRpcUrl, + "method", method, + "remoteAgentId", remoteAgentId, + "remoteConversationId", remoteConversationId + )); + + ObjectNode envelope = objectMapper.createObjectNode(); + envelope.put("jsonrpc", "2.0"); + envelope.put("method", method); + envelope.put("id", UUID.randomUUID().toString()); + envelope.set("params", params); + + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(URI.create(remoteRpcUrl)) + .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(envelope))) + .header("Content-Type", "application/json"); + String authToken = endpoint.getConfiguration().getAuthToken(); + if (authToken != null && !authToken.isBlank()) { + requestBuilder.header("Authorization", "Bearer " + authToken.trim()); + } + + HttpResponse response = httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()); + JsonNode root = objectMapper.readTree(response.body()); + if (response.statusCode() >= 400) { + throw new IllegalStateException("Remote A2A call failed with HTTP " + response.statusCode() + ": " + response.body()); + } + if (root.hasNonNull("error")) { + throw new IllegalStateException("Remote A2A call failed: " + root.path("error").toString()); + } + + JsonNode result = root.path("result"); + bindCorrelation(context.conversationId(), remoteAgentId, remoteConversationId, result); + appendEvent(context, "conversation.a2a.outbound.completed", Map.of( + "toolName", toolSpec.name(), + "target", target, + "remoteUrl", remoteRpcUrl, + "method", method, + "remoteAgentId", remoteAgentId, + "remoteConversationId", remoteConversationId, + "remoteTaskId", remoteTaskId(result) + )); + + String content = extractContent(result); + return new ToolResult(content, result, List.of()); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException("A2A tool execution failed for target " + target, e); + } + } + + private ObjectNode buildParams(JsonNode arguments, String method, String remoteAgentId, ExecutionContext context) { + ObjectNode normalized = arguments != null && arguments.isObject() + ? ((ObjectNode) arguments.deepCopy()) + : objectMapper.createObjectNode(); + normalized.remove("method"); + + return switch (method) { + case "SendMessage", "SendStreamingMessage" -> buildSendParams(normalized, remoteAgentId, context); + case "GetTask", "CancelTask", "SubscribeToTask" -> ensureTaskId(normalized, context); + default -> normalized; + }; + } + + private ObjectNode buildSendParams(ObjectNode arguments, String remoteAgentId, ExecutionContext context) { + ObjectNode params = arguments.deepCopy(); + ObjectNode metadata = params.has("metadata") && params.get("metadata").isObject() + ? (ObjectNode) params.get("metadata") + : objectMapper.createObjectNode(); + params.set("metadata", metadata); + + ObjectNode camelAgent = metadata.has("camelAgent") && metadata.get("camelAgent").isObject() + ? (ObjectNode) metadata.get("camelAgent") + : objectMapper.createObjectNode(); + metadata.set("camelAgent", camelAgent); + + camelAgent.put("localConversationId", fallback(context.conversationId())); + camelAgent.put("parentConversationId", fallback(context.conversationId())); + camelAgent.put("rootConversationId", fallback(context.conversationId())); + camelAgent.put("traceId", fallback(context.traceId())); + if (!toolContext.planName().isBlank()) { + camelAgent.put("planName", toolContext.planName()); + metadata.put("planName", toolContext.planName()); + } + if (!toolContext.planVersion().isBlank()) { + camelAgent.put("planVersion", toolContext.planVersion()); + metadata.put("planVersion", toolContext.planVersion()); + } + if (!toolContext.agentName().isBlank()) { + camelAgent.put("agentName", toolContext.agentName()); + metadata.put("agentName", toolContext.agentName()); + } + if (!toolContext.agentVersion().isBlank()) { + camelAgent.put("agentVersion", toolContext.agentVersion()); + metadata.put("agentVersion", toolContext.agentVersion()); + } + metadata.put("agentId", remoteAgentId == null ? "" : remoteAgentId); + camelAgent.put("agentId", remoteAgentId == null ? "" : remoteAgentId); + + if (!params.hasNonNull("conversationId")) { + String existingRemoteConversationId = + CorrelationRegistry.global().resolve(context.conversationId(), CorrelationKeys.A2A_REMOTE_CONVERSATION_ID, ""); + params.put("conversationId", firstNonBlank(existingRemoteConversationId, context.conversationId())); + } + if (!params.hasNonNull("idempotencyKey")) { + params.put("idempotencyKey", UUID.randomUUID().toString()); + } + if (!params.has("message") || !params.get("message").isObject()) { + params.set("message", buildMessage(arguments, remoteAgentId, context)); + } + return params; + } + + private ObjectNode buildMessage(ObjectNode arguments, String remoteAgentId, ExecutionContext context) { + String text = firstNonBlank( + text(arguments, "text"), + text(arguments, "prompt"), + text(arguments, "input"), + arguments.isValueNode() ? arguments.asText("") : "" + ); + ObjectNode part = objectMapper.createObjectNode(); + part.put("partId", UUID.randomUUID().toString()); + part.put("type", "text"); + part.put("mimeType", "text/plain"); + part.put("text", text); + + ArrayNode parts = objectMapper.createArrayNode(); + parts.add(part); + + ObjectNode metadata = objectMapper.createObjectNode(); + metadata.put("agentId", fallback(remoteAgentId)); + metadata.put("localConversationId", fallback(context.conversationId())); + metadata.put("traceId", fallback(context.traceId())); + + ObjectNode message = objectMapper.createObjectNode(); + message.put("messageId", UUID.randomUUID().toString()); + message.put("role", "user"); + message.set("parts", parts); + message.set("metadata", metadata); + message.put("createdAt", Instant.now().toString()); + return message; + } + + private ObjectNode ensureTaskId(ObjectNode params, ExecutionContext context) { + if (!params.hasNonNull("taskId")) { + String remoteTaskId = CorrelationRegistry.global().resolve(context.conversationId(), CorrelationKeys.A2A_REMOTE_TASK_ID, ""); + if (remoteTaskId.isBlank()) { + throw new IllegalArgumentException("A2A follow-up call requires taskId or an existing correlated remote task"); + } + params.put("taskId", remoteTaskId); + } + return params; + } + + private void bindCorrelation(String conversationId, String remoteAgentId, String remoteConversationId, JsonNode result) { + if (conversationId == null || conversationId.isBlank()) { + return; + } + CorrelationRegistry registry = CorrelationRegistry.global(); + if (remoteAgentId != null && !remoteAgentId.isBlank()) { + registry.bind(conversationId, CorrelationKeys.A2A_AGENT_ID, remoteAgentId); + } + if (remoteConversationId != null && !remoteConversationId.isBlank()) { + registry.bind(conversationId, CorrelationKeys.A2A_REMOTE_CONVERSATION_ID, remoteConversationId); + } + String remoteTaskId = remoteTaskId(result); + if (!remoteTaskId.isBlank()) { + registry.bind(conversationId, CorrelationKeys.A2A_REMOTE_TASK_ID, remoteTaskId); + } + } + + private void appendEvent(ExecutionContext context, String type, Map payload) { + if (persistenceFacade == null || context == null || context.conversationId() == null || context.conversationId().isBlank()) { + return; + } + persistenceFacade.appendEvent( + new AgentEvent(context.conversationId(), context.taskId(), type, objectMapper.valueToTree(payload), Instant.now()), + UUID.randomUUID().toString() + ); + } + + private String determineMethod(JsonNode arguments) { + String method = arguments == null ? "" : text(arguments, "method"); + return method.isBlank() ? "SendMessage" : method; + } + + private String normalizeRpcUrl(String remoteUrl) { + if (remoteUrl == null || remoteUrl.isBlank()) { + throw new IllegalArgumentException("A2A remoteUrl is required"); + } + URI uri = URI.create(remoteUrl.trim()); + String scheme = uri.getScheme(); + String httpScheme = switch (scheme == null ? "" : scheme) { + case "ws" -> "http"; + case "wss" -> "https"; + default -> scheme; + }; + String path = uri.getPath() == null || uri.getPath().isBlank() ? "/a2a" : uri.getPath(); + if (!path.endsWith("/rpc")) { + path = path.endsWith("/") ? path + "rpc" : path + "/rpc"; + } + String authority = uri.getHost(); + if (authority == null || authority.isBlank()) { + throw new IllegalArgumentException("A2A remoteUrl must include a host: " + remoteUrl); + } + if (uri.getPort() >= 0) { + authority = authority + ":" + uri.getPort(); + } + String query = uri.getQuery() == null || uri.getQuery().isBlank() ? "" : "?" + uri.getQuery(); + return httpScheme + "://" + authority + path + query; + } + + private String extractContent(JsonNode result) { + if (result == null || result.isNull() || result.isMissingNode()) { + return ""; + } + List candidates = List.of( + result.path("task").path("latestMessage"), + result.path("task").path("messages").path(result.path("task").path("messages").size() - 1), + result.path("task"), + result + ); + for (JsonNode candidate : candidates) { + String text = messageText(candidate); + if (!text.isBlank()) { + return text; + } + } + return result.isValueNode() ? result.asText("") : result.toPrettyString(); + } + + private String messageText(JsonNode message) { + if (message == null || !message.isObject()) { + return ""; + } + if (message.hasNonNull("text")) { + return message.path("text").asText(""); + } + JsonNode parts = message.path("parts"); + if (!parts.isArray()) { + return ""; + } + StringBuilder combined = new StringBuilder(); + for (JsonNode part : parts) { + String text = part.path("text").asText(""); + if (!text.isBlank()) { + if (combined.length() > 0) { + combined.append('\n'); + } + combined.append(text); + } + } + return combined.toString(); + } + + private String remoteTaskId(JsonNode result) { + return firstNonBlank( + text(result, "taskId"), + text(result.path("task"), "taskId") + ); + } + + private String text(JsonNode node, String field) { + if (node == null || node.isNull() || node.isMissingNode()) { + return ""; + } + JsonNode value = node.path(field); + return value.isMissingNode() || value.isNull() ? "" : value.asText(""); + } + + private String fallback(String value) { + return value == null ? "" : value; + } + + private String firstNonBlank(String... values) { + for (String value : values) { + if (value != null && !value.isBlank()) { + return value.trim(); + } + } + return ""; + } +} diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolContext.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolContext.java new file mode 100644 index 0000000..65c2761 --- /dev/null +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolContext.java @@ -0,0 +1,10 @@ +package io.dscope.camel.agent.a2a; + +public record A2AToolContext( + String planName, + String planVersion, + String agentName, + String agentVersion +) { + public static final A2AToolContext EMPTY = new A2AToolContext("", "", "", ""); +} diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/component/AgentComponent.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/component/AgentComponent.java index 0b173ef..341365a 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/component/AgentComponent.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/component/AgentComponent.java @@ -7,6 +7,7 @@ import io.dscope.camel.agent.api.PersistenceFacade; import io.dscope.camel.agent.api.ToolExecutor; import io.dscope.camel.agent.api.ToolRegistry; +import io.dscope.camel.agent.a2a.A2AToolContext; import io.dscope.camel.agent.blueprint.MarkdownBlueprintLoader; import io.dscope.camel.agent.executor.TemplateAwareCamelToolExecutor; import io.dscope.camel.agent.kernel.DefaultAgentKernel; @@ -70,7 +71,7 @@ public AgentKernel resolveKernel(AgentEndpoint endpoint, ResolvedAgentPlan resol String cacheKey = (resolvedPlan == null || resolvedPlan.legacyMode()) ? "legacy|" + blueprintLocation : resolvedPlan.planName() + "|" + resolvedPlan.planVersion() + "|" + blueprintLocation; - return kernelCache.computeIfAbsent(cacheKey, ignored -> buildKernel(blueprintLocation)); + return kernelCache.computeIfAbsent(cacheKey, ignored -> buildKernel(blueprintLocation, resolvedPlan)); } public AgentPlanSelectionResolver planResolver() { @@ -106,7 +107,7 @@ private Optional findRegistry(Class type) { return Optional.ofNullable(getCamelContext().getRegistry().findSingleByType(type)); } - private AgentKernel buildKernel(String blueprintLocation) { + private AgentKernel buildKernel(String blueprintLocation, ResolvedAgentPlan resolvedPlan) { BlueprintLoader blueprintLoader = new MarkdownBlueprintLoader(); AgentBlueprint loadedBlueprint = applyRealtimeFallback(blueprintLoader.load(blueprintLocation)); @@ -115,9 +116,9 @@ private AgentKernel buildKernel(String blueprintLocation) { AgentBlueprint blueprint = McpToolDiscoveryResolver.resolve(loadedBlueprint, producerTemplate, mapper); ToolRegistry toolRegistry = new DefaultToolRegistry(blueprint.tools()); - ToolExecutor toolExecutor = createToolExecutor(producerTemplate, mapper, blueprint); AiModelClient aiModelClient = findRegistry(AiModelClient.class).orElseGet(StaticAiModelClient::new); PersistenceFacade persistenceFacade = persistenceFacade(); + ToolExecutor toolExecutor = createToolExecutor(producerTemplate, mapper, blueprint, persistenceFacade, resolvedPlan); return new DefaultAgentKernel( blueprint, @@ -132,13 +133,22 @@ private AgentKernel buildKernel(String blueprintLocation) { private ToolExecutor createToolExecutor(ProducerTemplate producerTemplate, ObjectMapper objectMapper, - AgentBlueprint blueprint) { + AgentBlueprint blueprint, + PersistenceFacade persistenceFacade, + ResolvedAgentPlan resolvedPlan) { return findRegistry(ToolExecutor.class) .orElseGet(() -> new TemplateAwareCamelToolExecutor( getCamelContext(), producerTemplate, objectMapper, - defaultIfNull(blueprint.jsonRouteTemplates(), List.of()) + defaultIfNull(blueprint.jsonRouteTemplates(), List.of()), + persistenceFacade, + new A2AToolContext( + resolvedPlan == null || resolvedPlan.legacyMode() ? "" : defaultIfBlank(resolvedPlan.planName(), ""), + resolvedPlan == null || resolvedPlan.legacyMode() ? "" : defaultIfBlank(resolvedPlan.planVersion(), ""), + defaultIfBlank(blueprint.name(), ""), + defaultIfBlank(blueprint.version(), "") + ) )); } diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/executor/CamelToolExecutor.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/executor/CamelToolExecutor.java index d9776b5..f338257 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/executor/CamelToolExecutor.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/executor/CamelToolExecutor.java @@ -3,6 +3,9 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.dscope.camel.agent.api.ToolExecutor; +import io.dscope.camel.agent.a2a.A2AToolClient; +import io.dscope.camel.agent.a2a.A2AToolContext; +import io.dscope.camel.agent.api.PersistenceFacade; import io.dscope.camel.agent.config.AgentHeaders; import io.dscope.camel.agent.model.ExecutionContext; import io.dscope.camel.agent.model.ToolResult; @@ -12,6 +15,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.apache.camel.CamelContext; import org.apache.camel.ProducerTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,17 +24,32 @@ public class CamelToolExecutor implements ToolExecutor { private static final Logger LOGGER = LoggerFactory.getLogger(CamelToolExecutor.class); + private final CamelContext camelContext; private final ProducerTemplate producerTemplate; private final ObjectMapper objectMapper; + private final A2AToolClient a2aToolClient; public CamelToolExecutor(ProducerTemplate producerTemplate, ObjectMapper objectMapper) { + this(null, producerTemplate, objectMapper, null, A2AToolContext.EMPTY); + } + + public CamelToolExecutor(CamelContext camelContext, + ProducerTemplate producerTemplate, + ObjectMapper objectMapper, + PersistenceFacade persistenceFacade, + A2AToolContext a2aToolContext) { + this.camelContext = camelContext; this.producerTemplate = producerTemplate; this.objectMapper = objectMapper; + this.a2aToolClient = new A2AToolClient(camelContext, objectMapper, persistenceFacade, a2aToolContext); } @Override public ToolResult execute(ToolSpec toolSpec, JsonNode arguments, ExecutionContext context) { String target = target(toolSpec); + if (isA2ATarget(target)) { + return a2aToolClient.execute(target, toolSpec, arguments, context); + } Map headers = new HashMap<>(); headers.put(AgentHeaders.CONVERSATION_ID, context.conversationId()); headers.put(AgentHeaders.TASK_ID, context.taskId()); @@ -104,6 +123,10 @@ private boolean isMcpTarget(String target) { return target != null && target.startsWith("mcp:"); } + private boolean isA2ATarget(String target) { + return target != null && target.startsWith("a2a:"); + } + private String argumentShape(Object arguments) { if (arguments == null) { return "null"; diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/executor/TemplateAwareCamelToolExecutor.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/executor/TemplateAwareCamelToolExecutor.java index 1d158b5..06d44e4 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/executor/TemplateAwareCamelToolExecutor.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/executor/TemplateAwareCamelToolExecutor.java @@ -5,6 +5,8 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.dscope.camel.agent.api.ToolExecutor; +import io.dscope.camel.agent.a2a.A2AToolContext; +import io.dscope.camel.agent.api.PersistenceFacade; import io.dscope.camel.agent.config.AgentHeaders; import io.dscope.camel.agent.model.ExecutionContext; import io.dscope.camel.agent.model.JsonRouteTemplateSpec; @@ -38,10 +40,19 @@ public TemplateAwareCamelToolExecutor(CamelContext camelContext, ProducerTemplate producerTemplate, ObjectMapper objectMapper, List templates) { + this(camelContext, producerTemplate, objectMapper, templates, null, A2AToolContext.EMPTY); + } + + public TemplateAwareCamelToolExecutor(CamelContext camelContext, + ProducerTemplate producerTemplate, + ObjectMapper objectMapper, + List templates, + PersistenceFacade persistenceFacade, + A2AToolContext a2aToolContext) { this.camelContext = camelContext; this.producerTemplate = producerTemplate; this.objectMapper = objectMapper; - this.delegate = new CamelToolExecutor(producerTemplate, objectMapper); + this.delegate = new CamelToolExecutor(camelContext, producerTemplate, objectMapper, persistenceFacade, a2aToolContext); this.templatesByToolName = new HashMap<>(); if (templates != null) { for (JsonRouteTemplateSpec template : templates) { diff --git a/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/A2AToolClientTest.java b/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/A2AToolClientTest.java new file mode 100644 index 0000000..1f75a08 --- /dev/null +++ b/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/A2AToolClientTest.java @@ -0,0 +1,279 @@ +package io.dscope.camel.agent.a2a; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dscope.camel.agent.kernel.InMemoryPersistenceFacade; +import io.dscope.camel.agent.model.ExecutionContext; +import io.dscope.camel.agent.model.ToolPolicy; +import io.dscope.camel.agent.model.ToolResult; +import io.dscope.camel.agent.model.ToolSpec; +import io.dscope.camel.agent.registry.CorrelationRegistry; +import io.dscope.camel.a2a.A2AComponent; +import java.io.IOException; +import java.net.CookieHandler; +import java.net.ProxySelector; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import org.apache.camel.impl.DefaultCamelContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +class A2AToolClientTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @AfterEach + void tearDown() { + CorrelationRegistry.global().clear("conv-a2a"); + } + + @Test + void sendsRemoteA2aMessageAndBindsCorrelation() throws Exception { + FakeHttpClient httpClient = new FakeHttpClient(List.of(""" + {"jsonrpc":"2.0","result":{"task":{"taskId":"remote-task-1","status":{"state":"COMPLETED"},"latestMessage":{"parts":[{"text":"remote answer"}]}}},"id":"1"} + """)); + InMemoryPersistenceFacade persistence = new InMemoryPersistenceFacade(); + + try (DefaultCamelContext camelContext = new DefaultCamelContext()) { + camelContext.addComponent("a2a", new A2AComponent()); + camelContext.start(); + + A2AToolClient client = new A2AToolClient( + camelContext, + objectMapper, + persistence, + new A2AToolContext("support", "v1", "Support Agent", "1.0"), + httpClient + ); + + ToolResult result = client.execute( + "a2a:support-public?remoteUrl=http://example.test/a2a", + new ToolSpec("remote.specialist", "Calls remote specialist", null, + "a2a:support-public?remoteUrl=http://example.test/a2a", null, null, new ToolPolicy(false, 0, 1000)), + objectMapper.createObjectNode().put("text", "hello remote"), + new ExecutionContext("conv-a2a", "task-local", "trace-1") + ); + + JsonNode request = objectMapper.readTree(httpClient.lastRequestBody()); + assertEquals("SendMessage", request.path("method").asText()); + assertEquals("support-public", request.path("params").path("metadata").path("agentId").asText()); + assertEquals("support", request.path("params").path("metadata").path("planName").asText()); + assertEquals("remote answer", result.content()); + assertEquals("remote-task-1", CorrelationRegistry.global().resolve("conv-a2a", "a2a.remoteTaskId", "")); + assertTrue(persistence.loadConversation("conv-a2a", 10).stream().anyMatch(event -> "conversation.a2a.outbound.started".equals(event.type()))); + assertTrue(persistence.loadConversation("conv-a2a", 10).stream().anyMatch(event -> "conversation.a2a.outbound.completed".equals(event.type()))); + } + } + + @Test + void reusesCorrelatedRemoteTaskIdForFollowUpCalls() throws Exception { + FakeHttpClient httpClient = new FakeHttpClient(List.of(""" + {"jsonrpc":"2.0","result":{"task":{"taskId":"remote-task-2","status":{"state":"COMPLETED"},"latestMessage":{"parts":[{"text":"created"}]}}},"id":"1"} + """, """ + {"jsonrpc":"2.0","result":{"task":{"taskId":"remote-task-2","status":{"state":"COMPLETED"},"latestMessage":{"parts":[{"text":"fetched"}]}}},"id":"2"} + """)); + + try (DefaultCamelContext camelContext = new DefaultCamelContext()) { + camelContext.addComponent("a2a", new A2AComponent()); + camelContext.start(); + + A2AToolClient client = new A2AToolClient( + camelContext, + objectMapper, + new InMemoryPersistenceFacade(), + new A2AToolContext("support", "v1", "Support Agent", "1.0"), + httpClient + ); + + ToolSpec tool = new ToolSpec("remote.specialist", "Calls remote specialist", null, + "a2a:support-public?remoteUrl=http://example.test/a2a", null, null, new ToolPolicy(false, 0, 1000)); + client.execute(tool.endpointUri(), tool, objectMapper.createObjectNode().put("text", "hello"), new ExecutionContext("conv-a2a", null, "trace-1")); + client.execute(tool.endpointUri(), tool, objectMapper.createObjectNode().put("method", "GetTask"), new ExecutionContext("conv-a2a", null, "trace-2")); + + JsonNode request = objectMapper.readTree(httpClient.lastRequestBody()); + assertEquals("GetTask", request.path("method").asText()); + assertEquals("remote-task-2", request.path("params").path("taskId").asText()); + } + } + + private static final class FakeHttpClient extends HttpClient { + + private final Queue responses; + private final Executor executor = Executors.newSingleThreadExecutor(); + private volatile String lastRequestBody = ""; + + private FakeHttpClient(List responses) { + this.responses = new ConcurrentLinkedQueue<>(responses); + } + + String lastRequestBody() { + return lastRequestBody; + } + + @Override + public Optional cookieHandler() { + return Optional.empty(); + } + + @Override + public Optional connectTimeout() { + return Optional.empty(); + } + + @Override + public Redirect followRedirects() { + return Redirect.NEVER; + } + + @Override + public Optional proxy() { + return Optional.empty(); + } + + @Override + public SSLContext sslContext() { + return null; + } + + @Override + public SSLParameters sslParameters() { + return null; + } + + @Override + public Optional authenticator() { + return Optional.empty(); + } + + @Override + public Version version() { + return Version.HTTP_1_1; + } + + @Override + public Optional executor() { + return Optional.of(executor); + } + + @Override + public HttpResponse send(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) + throws IOException, InterruptedException { + lastRequestBody = request.bodyPublisher() + .map(BodyPublisherReader::read) + .orElse(""); + String responseBody = responses.remove(); + return (HttpResponse) new FakeHttpResponse(request, responseBody); + } + + @Override + public java.util.concurrent.CompletableFuture> sendAsync( + HttpRequest request, + HttpResponse.BodyHandler responseBodyHandler) { + throw new UnsupportedOperationException(); + } + + @Override + public java.util.concurrent.CompletableFuture> sendAsync( + HttpRequest request, + HttpResponse.BodyHandler responseBodyHandler, + HttpResponse.PushPromiseHandler pushPromiseHandler) { + throw new UnsupportedOperationException(); + } + } + + private record FakeHttpResponse(HttpRequest request, String body) implements HttpResponse { + @Override + public int statusCode() { + return 200; + } + + @Override + public HttpRequest request() { + return request; + } + + @Override + public Optional> previousResponse() { + return Optional.empty(); + } + + @Override + public HttpHeaders headers() { + return HttpHeaders.of(java.util.Map.of(), (a, b) -> true); + } + + @Override + public String body() { + return body; + } + + @Override + public Optional sslSession() { + return Optional.empty(); + } + + @Override + public URI uri() { + return request.uri(); + } + + @Override + public HttpClient.Version version() { + return HttpClient.Version.HTTP_1_1; + } + } + + private static final class BodyPublisherReader implements java.util.concurrent.Flow.Subscriber { + + private final java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream(); + private java.util.concurrent.Flow.Subscription subscription; + + static String read(HttpRequest.BodyPublisher publisher) { + BodyPublisherReader reader = new BodyPublisherReader(); + publisher.subscribe(reader); + return reader.bytes.toString(StandardCharsets.UTF_8); + } + + @Override + public void onSubscribe(java.util.concurrent.Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(java.nio.ByteBuffer buffer) { + byte[] chunk = new byte[buffer.remaining()]; + buffer.get(chunk); + bytes.write(chunk, 0, chunk.length); + } + + @Override + public void onError(Throwable throwable) { + if (subscription != null) { + subscription.cancel(); + } + throw new IllegalStateException(throwable); + } + + @Override + public void onComplete() { + } + } +} diff --git a/camel-agent-starter/src/main/java/io/dscope/camel/agent/starter/AgentAutoConfiguration.java b/camel-agent-starter/src/main/java/io/dscope/camel/agent/starter/AgentAutoConfiguration.java index bce9825..b9872ae 100644 --- a/camel-agent-starter/src/main/java/io/dscope/camel/agent/starter/AgentAutoConfiguration.java +++ b/camel-agent-starter/src/main/java/io/dscope/camel/agent/starter/AgentAutoConfiguration.java @@ -7,6 +7,7 @@ import io.dscope.camel.agent.api.PersistenceFacade; import io.dscope.camel.agent.api.ToolExecutor; import io.dscope.camel.agent.api.ToolRegistry; +import io.dscope.camel.agent.a2a.A2AToolContext; import io.dscope.camel.agent.blueprint.MarkdownBlueprintLoader; import io.dscope.camel.agent.executor.CamelToolExecutor; import io.dscope.camel.agent.kernel.DefaultAgentKernel; @@ -117,21 +118,22 @@ public AgentKernel agentKernel(CamelContext camelContext, AgentStarterProperties properties, ObjectMapper objectMapper) { String blueprintLocation = properties.getBlueprint(); + ResolvedAgentPlan resolvedPlan = null; if (properties.getAgentsConfig() != null && !properties.getAgentsConfig().isBlank()) { - ResolvedAgentPlan resolved = planSelectionResolver.resolve( + resolvedPlan = planSelectionResolver.resolve( null, null, null, properties.getAgentsConfig(), properties.getBlueprint() ); - blueprintLocation = resolved.blueprint(); + blueprintLocation = resolvedPlan.blueprint(); } AgentBlueprint loadedBlueprint = blueprintLoader.load(blueprintLocation); ProducerTemplate producerTemplate = camelContext.createProducerTemplate(); AgentBlueprint blueprint = McpToolDiscoveryResolver.resolve(loadedBlueprint, producerTemplate, objectMapper); ToolRegistry toolRegistry = new DefaultToolRegistry(blueprint.tools()); - ToolExecutor toolExecutor = createToolExecutor(camelContext, producerTemplate, objectMapper, blueprint); + ToolExecutor toolExecutor = createToolExecutor(camelContext, producerTemplate, objectMapper, blueprint, persistenceFacade, resolvedPlan); return new DefaultAgentKernel( blueprint, toolRegistry, @@ -148,15 +150,41 @@ public AgentKernel agentKernel(CamelContext camelContext, private ToolExecutor createToolExecutor(CamelContext camelContext, ProducerTemplate producerTemplate, ObjectMapper objectMapper, - AgentBlueprint blueprint) { + AgentBlueprint blueprint, + PersistenceFacade persistenceFacade, + ResolvedAgentPlan resolvedPlan) { try { java.util.List templates = readJsonRouteTemplates(blueprint); Class type = Class.forName("io.dscope.camel.agent.executor.TemplateAwareCamelToolExecutor"); return (ToolExecutor) type - .getConstructor(CamelContext.class, ProducerTemplate.class, ObjectMapper.class, java.util.List.class) - .newInstance(camelContext, producerTemplate, objectMapper, templates); + .getConstructor(CamelContext.class, ProducerTemplate.class, ObjectMapper.class, java.util.List.class, + PersistenceFacade.class, A2AToolContext.class) + .newInstance( + camelContext, + producerTemplate, + objectMapper, + templates, + persistenceFacade, + new A2AToolContext( + resolvedPlan == null || resolvedPlan.legacyMode() ? "" : resolvedPlan.planName(), + resolvedPlan == null || resolvedPlan.legacyMode() ? "" : resolvedPlan.planVersion(), + blueprint.name() == null ? "" : blueprint.name(), + blueprint.version() == null ? "" : blueprint.version() + ) + ); } catch (Exception ignored) { - return new CamelToolExecutor(producerTemplate, objectMapper); + return new CamelToolExecutor( + camelContext, + producerTemplate, + objectMapper, + persistenceFacade, + new A2AToolContext( + resolvedPlan == null || resolvedPlan.legacyMode() ? "" : resolvedPlan.planName(), + resolvedPlan == null || resolvedPlan.legacyMode() ? "" : resolvedPlan.planVersion(), + blueprint.name() == null ? "" : blueprint.name(), + blueprint.version() == null ? "" : blueprint.version() + ) + ); } } From 1f8619846e492945c9ccc61f3dd4061b61ad181b Mon Sep 17 00:00:00 2001 From: Roman Date: Sat, 14 Mar 2026 12:17:50 -0700 Subject: [PATCH 03/19] Align agent A2A with 1.0.0 --- camel-agent-core/pom.xml | 2 +- .../dscope/camel/agent/component/agent.json | 2 +- .../a2a/A2AParentConversationNotifier.java | 121 +++++++ .../dscope/camel/agent/a2a/A2AToolClient.java | 49 ++- .../agent/a2a/AgentA2AAgentCardCatalog.java | 65 ++-- .../agent/a2a/AgentA2AProtocolSupport.java | 249 +++++++++---- .../camel/agent/a2a/AgentA2ATaskAdapter.java | 159 +++++++++ .../agent/a2a/AgentA2ATaskRepository.java | 227 ------------ .../agent/runtime/AgentRuntimeBootstrap.java | 2 + .../runtime/TicketLifecycleProcessor.java | 168 +++++++++ .../camel/agent/a2a/A2AToolClientTest.java | 8 +- .../a2a/AgentA2AProtocolSupportTest.java | 118 +++++++ .../agent/a2a/AgentA2ATaskAdapterTest.java | 110 ++++++ .../agent/a2a/AgentA2ATaskRepositoryTest.java | 79 ----- .../runtime/TicketLifecycleProcessorTest.java | 50 +++ camel-agent-persistence-dscope/pom.xml | 2 +- .../dscope/DscopePersistenceFactory.java | 9 +- camel-agent-spring-ai/pom.xml | 2 +- camel-agent-starter/pom.xml | 2 +- pom.xml | 4 +- samples/agent-support-service/pom.xml | 2 +- .../agent/samples/DemoA2ATicketGateway.java | 109 ++++++ .../io/dscope/camel/agent/samples/Main.java | 7 +- .../agent/samples/SampleAdminMcpBindings.java | 333 ++++++++++++++++++ .../SupportTicketLifecycleProcessor.java | 147 ++++++++ .../resources/agents/a2a-exposed-agents.yaml | 12 + .../src/main/resources/agents/agents.yaml | 5 + .../main/resources/agents/billing/v1/agent.md | 15 +- .../main/resources/agents/billing/v2/agent.md | 15 +- .../main/resources/agents/support/agent.md | 13 +- .../main/resources/agents/support/v1/agent.md | 13 +- .../main/resources/agents/support/v2/agent.md | 13 +- .../resources/agents/ticketing/v1/agent.md | 29 ++ .../src/main/resources/application.yaml | 16 +- .../src/main/resources/frontend/audit.html | 2 +- .../src/main/resources/frontend/index.html | 67 +++- .../routes/admin-platform.camel.yaml | 12 + .../resources/routes/kb-search-json.camel.xml | 4 +- .../routes/ticket-service.camel.yaml | 9 + ...UiPlaywrightAuditTrailIntegrationTest.java | 92 ++--- .../samples/McpAdminApiIntegrationTest.java | 3 + .../SpringAiAuditTrailIntegrationTest.java | 11 +- .../ag-ui-playwright-audit-test.yaml | 8 +- 43 files changed, 1837 insertions(+), 528 deletions(-) create mode 100644 camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AParentConversationNotifier.java create mode 100644 camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2ATaskAdapter.java delete mode 100644 camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2ATaskRepository.java create mode 100644 camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/TicketLifecycleProcessor.java create mode 100644 camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupportTest.java create mode 100644 camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2ATaskAdapterTest.java delete mode 100644 camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2ATaskRepositoryTest.java create mode 100644 camel-agent-core/src/test/java/io/dscope/camel/agent/runtime/TicketLifecycleProcessorTest.java create mode 100644 samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/DemoA2ATicketGateway.java create mode 100644 samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/SampleAdminMcpBindings.java create mode 100644 samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/SupportTicketLifecycleProcessor.java create mode 100644 samples/agent-support-service/src/main/resources/agents/a2a-exposed-agents.yaml create mode 100644 samples/agent-support-service/src/main/resources/agents/ticketing/v1/agent.md create mode 100644 samples/agent-support-service/src/main/resources/routes/ticket-service.camel.yaml diff --git a/camel-agent-core/pom.xml b/camel-agent-core/pom.xml index f3afd3a..2c8ce06 100644 --- a/camel-agent-core/pom.xml +++ b/camel-agent-core/pom.xml @@ -6,7 +6,7 @@ io.dscope.camel camel-agent - 0.5.0 + 0.6.0 camel-agent-core diff --git a/camel-agent-core/src/generated/resources/META-INF/io/dscope/camel/agent/component/agent.json b/camel-agent-core/src/generated/resources/META-INF/io/dscope/camel/agent/component/agent.json index cfb828e..1019bb1 100644 --- a/camel-agent-core/src/generated/resources/META-INF/io/dscope/camel/agent/component/agent.json +++ b/camel-agent-core/src/generated/resources/META-INF/io/dscope/camel/agent/component/agent.json @@ -11,7 +11,7 @@ "supportLevel": "Stable", "groupId": "io.dscope.camel", "artifactId": "camel-agent-core", - "version": "0.5.0", + "version": "0.6.0", "scheme": "agent", "extendsScheme": "", "syntax": "agent:agentId", diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AParentConversationNotifier.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AParentConversationNotifier.java new file mode 100644 index 0000000..f78d29b --- /dev/null +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AParentConversationNotifier.java @@ -0,0 +1,121 @@ +package io.dscope.camel.agent.a2a; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.dscope.camel.agent.api.PersistenceFacade; +import io.dscope.camel.agent.model.AgentEvent; +import java.time.Instant; +import java.util.UUID; + +final class A2AParentConversationNotifier { + + private final PersistenceFacade persistenceFacade; + private final ObjectMapper objectMapper; + + A2AParentConversationNotifier(PersistenceFacade persistenceFacade, ObjectMapper objectMapper) { + this.persistenceFacade = persistenceFacade; + this.objectMapper = objectMapper; + } + + void notifyParent(String parentConversationId, + String childConversationId, + String remoteTaskId, + String agentId, + String planName, + String planVersion, + String aguiSessionId, + String aguiRunId, + String aguiThreadId, + String replyText) { + if (persistenceFacade == null || parentConversationId == null || parentConversationId.isBlank()) { + return; + } + if (childConversationId != null && childConversationId.equals(parentConversationId)) { + return; + } + + Instant now = Instant.now(); + ObjectNode payload = objectMapper.createObjectNode(); + payload.put("conversationId", parentConversationId); + payload.put("role", "assistant"); + payload.put("source", "a2a.ticketing"); + payload.put("sessionId", fallback(aguiSessionId)); + payload.put("runId", fallback(aguiRunId)); + payload.put("threadId", fallback(aguiThreadId)); + payload.put("text", summarize(replyText)); + payload.put("at", now.toString()); + + ObjectNode a2a = payload.putObject("a2a"); + a2a.put("agentId", fallback(agentId)); + a2a.put("planName", fallback(planName)); + a2a.put("planVersion", fallback(planVersion)); + a2a.put("taskId", fallback(remoteTaskId)); + a2a.put("linkedConversationId", fallback(childConversationId)); + + JsonNode widget = ticketWidget(replyText); + if (widget != null) { + payload.set("widget", widget); + } + + persistenceFacade.appendEvent( + new AgentEvent(parentConversationId, remoteTaskId, "conversation.assistant.message", payload, now), + UUID.randomUUID().toString() + ); + } + + private JsonNode ticketWidget(String replyText) { + if (replyText == null || replyText.isBlank()) { + return null; + } + try { + JsonNode parsed = objectMapper.readTree(replyText); + if (!parsed.isObject()) { + return null; + } + ObjectNode widget = objectMapper.createObjectNode(); + widget.put("template", "ticket-card"); + widget.set("data", parsed); + return widget; + } catch (Exception ignored) { + return null; + } + } + + private String summarize(String replyText) { + if (replyText == null || replyText.isBlank()) { + return "Ticket service posted an update."; + } + try { + JsonNode parsed = objectMapper.readTree(replyText); + if (parsed.isObject()) { + String ticketId = parsed.path("ticketId").asText(""); + String status = parsed.path("status").asText(""); + String message = parsed.path("message").asText(""); + StringBuilder summary = new StringBuilder(); + if (!ticketId.isBlank()) { + summary.append(ticketId); + } + if (!status.isBlank()) { + if (summary.length() > 0) { + summary.append(" "); + } + summary.append("is ").append(status); + } + if (!message.isBlank()) { + if (summary.length() > 0) { + summary.append(". "); + } + summary.append(message); + } + return summary.length() == 0 ? replyText.trim() : summary.toString(); + } + } catch (Exception ignored) { + } + return replyText.trim(); + } + + private String fallback(String value) { + return value == null ? "" : value; + } +} diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolClient.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolClient.java index 3bab8c5..3c66f5d 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolClient.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolClient.java @@ -143,10 +143,25 @@ private ObjectNode buildSendParams(ObjectNode arguments, String remoteAgentId, E : objectMapper.createObjectNode(); metadata.set("camelAgent", camelAgent); + CorrelationRegistry registry = CorrelationRegistry.global(); + String linkedConversationId = registry.resolve(context.conversationId(), CorrelationKeys.A2A_LINKED_CONVERSATION_ID, ""); + String parentConversationId = fallback(context.conversationId()); + String rootConversationId = firstNonBlank( + registry.resolve(context.conversationId(), CorrelationKeys.A2A_ROOT_CONVERSATION_ID, ""), + parentConversationId + ); + String aguiSessionId = registry.resolve(context.conversationId(), CorrelationKeys.AGUI_SESSION_ID, ""); + String aguiRunId = registry.resolve(context.conversationId(), CorrelationKeys.AGUI_RUN_ID, ""); + String aguiThreadId = registry.resolve(context.conversationId(), CorrelationKeys.AGUI_THREAD_ID, ""); + camelAgent.put("localConversationId", fallback(context.conversationId())); - camelAgent.put("parentConversationId", fallback(context.conversationId())); - camelAgent.put("rootConversationId", fallback(context.conversationId())); + camelAgent.put("linkedConversationId", fallback(linkedConversationId)); + camelAgent.put("parentConversationId", parentConversationId); + camelAgent.put("rootConversationId", rootConversationId); camelAgent.put("traceId", fallback(context.traceId())); + camelAgent.put("aguiSessionId", fallback(aguiSessionId)); + camelAgent.put("aguiRunId", fallback(aguiRunId)); + camelAgent.put("aguiThreadId", fallback(aguiThreadId)); if (!toolContext.planName().isBlank()) { camelAgent.put("planName", toolContext.planName()); metadata.put("planName", toolContext.planName()); @@ -165,10 +180,16 @@ private ObjectNode buildSendParams(ObjectNode arguments, String remoteAgentId, E } metadata.put("agentId", remoteAgentId == null ? "" : remoteAgentId); camelAgent.put("agentId", remoteAgentId == null ? "" : remoteAgentId); + metadata.put("linkedConversationId", fallback(linkedConversationId)); + metadata.put("parentConversationId", parentConversationId); + metadata.put("rootConversationId", rootConversationId); + metadata.put("aguiSessionId", fallback(aguiSessionId)); + metadata.put("aguiRunId", fallback(aguiRunId)); + metadata.put("aguiThreadId", fallback(aguiThreadId)); if (!params.hasNonNull("conversationId")) { String existingRemoteConversationId = - CorrelationRegistry.global().resolve(context.conversationId(), CorrelationKeys.A2A_REMOTE_CONVERSATION_ID, ""); + registry.resolve(context.conversationId(), CorrelationKeys.A2A_REMOTE_CONVERSATION_ID, ""); params.put("conversationId", firstNonBlank(existingRemoteConversationId, context.conversationId())); } if (!params.hasNonNull("idempotencyKey")) { @@ -236,6 +257,28 @@ private void bindCorrelation(String conversationId, String remoteAgentId, String if (!remoteTaskId.isBlank()) { registry.bind(conversationId, CorrelationKeys.A2A_REMOTE_TASK_ID, remoteTaskId); } + String linkedConversationId = firstNonBlank( + text(result.path("task").path("metadata").path("camelAgent"), "linkedConversationId"), + text(result.path("task").path("metadata").path("camelAgent"), "localConversationId"), + text(result.path("task").path("metadata"), "linkedConversationId") + ); + if (!linkedConversationId.isBlank()) { + registry.bind(conversationId, CorrelationKeys.A2A_LINKED_CONVERSATION_ID, linkedConversationId); + } + String parentConversationId = firstNonBlank( + text(result.path("task").path("metadata").path("camelAgent"), "parentConversationId"), + text(result.path("task").path("metadata"), "parentConversationId") + ); + if (!parentConversationId.isBlank()) { + registry.bind(conversationId, CorrelationKeys.A2A_PARENT_CONVERSATION_ID, parentConversationId); + } + String rootConversationId = firstNonBlank( + text(result.path("task").path("metadata").path("camelAgent"), "rootConversationId"), + text(result.path("task").path("metadata"), "rootConversationId") + ); + if (!rootConversationId.isBlank()) { + registry.bind(conversationId, CorrelationKeys.A2A_ROOT_CONVERSATION_ID, rootConversationId); + } } private void appendEvent(ExecutionContext context, String type, Map payload) { diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AAgentCardCatalog.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AAgentCardCatalog.java index fac6966..f71a8cd 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AAgentCardCatalog.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AAgentCardCatalog.java @@ -7,15 +7,12 @@ import io.dscope.camel.a2a.catalog.AgentCardSigner; import io.dscope.camel.a2a.catalog.AllowAllAgentCardPolicyChecker; import io.dscope.camel.a2a.catalog.AllowAllAgentCardSignatureVerifier; +import io.dscope.camel.a2a.catalog.DefaultAgentCardCatalog; import io.dscope.camel.a2a.catalog.NoopAgentCardSigner; -import io.dscope.camel.a2a.config.A2AProtocolMethods; -import io.dscope.camel.a2a.model.AgentCapabilities; import io.dscope.camel.a2a.model.AgentCard; -import io.dscope.camel.a2a.model.AgentSecurityScheme; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.TreeSet; public final class AgentA2AAgentCardCatalog implements AgentCardCatalog { @@ -44,65 +41,43 @@ public AgentA2AAgentCardCatalog(A2AExposedAgentCatalog exposedAgentCatalog, @Override public AgentCard getDiscoveryCard() { - AgentCard card = baseCard(false); - policyChecker.validate(card); - return card; + return enrich(baseCatalog().getDiscoveryCard(), false); } @Override public AgentCard getExtendedCard() { - AgentCard card = baseCard(true); - policyChecker.validate(card); - return card; + return enrich(baseCatalog().getExtendedCard(), true); } @Override public String getCardSignature(AgentCard card) { - try { - String canonical = objectMapper.writeValueAsString(card); - String signature = signer.sign(canonical); - if (signature != null && !verifier.verify(canonical, signature)) { - throw new IllegalStateException("Agent card signature verification failed"); - } - return signature; - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new IllegalStateException("Failed to sign A2A agent card", e); - } + return baseCatalog().getCardSignature(card); } - private AgentCard baseCard(boolean extended) { + private DefaultAgentCardCatalog baseCatalog() { A2AExposedAgentSpec defaultAgent = exposedAgentCatalog.defaultAgent(); + return new DefaultAgentCardCatalog( + defaultAgent.getAgentId(), + defaultAgent.getName(), + defaultAgent.getDescription(), + endpointUrl, + signer, + verifier, + policyChecker + ); + } - AgentCapabilities capabilities = new AgentCapabilities(); - capabilities.setStreaming(true); - capabilities.setPushNotifications(true); - capabilities.setStatefulTasks(true); - capabilities.setSupportedMethods(List.copyOf(new TreeSet<>(A2AProtocolMethods.CORE_METHODS))); - - AgentSecurityScheme bearer = new AgentSecurityScheme(); - bearer.setType("http"); - bearer.setScheme("bearer"); - bearer.setDescription("Bearer token authentication"); - bearer.setScopes(List.of("a2a.read", "a2a.write")); - + private AgentCard enrich(AgentCard card, boolean extended) { + A2AExposedAgentSpec defaultAgent = exposedAgentCatalog.defaultAgent(); Map metadata = new LinkedHashMap<>(); + if (card.getMetadata() != null && !card.getMetadata().isEmpty()) { + metadata.putAll(card.getMetadata()); + } metadata.put("discovery", true); metadata.put("extended", extended); metadata.put("defaultAgentId", defaultAgent.getAgentId()); metadata.put("agents", exposedAgentCatalog.agents().stream().map(this::cardMetadata).toList()); - - AgentCard card = new AgentCard(); - card.setAgentId(defaultAgent.getAgentId()); - card.setName(defaultAgent.getName()); - card.setDescription(defaultAgent.getDescription()); - card.setEndpointUrl(endpointUrl); card.setVersion(defaultVersion(defaultAgent)); - card.setCapabilities(capabilities); - card.setSecuritySchemes(Map.of("bearerAuth", bearer)); - card.setDefaultInputModes(List.of("application/json", "text/plain")); - card.setDefaultOutputModes(List.of("application/json", "text/event-stream")); card.setMetadata(metadata); return card; } diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java index 6ed4f83..acc2d0b 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java @@ -41,14 +41,22 @@ import io.dscope.camel.a2a.processor.ListPushNotificationConfigsProcessor; import io.dscope.camel.a2a.processor.A2ATaskSseProcessor; import io.dscope.camel.a2a.service.A2APushNotificationConfigService; +import io.dscope.camel.a2a.service.A2ATaskService; +import io.dscope.camel.a2a.service.InMemoryA2ATaskService; import io.dscope.camel.a2a.service.InMemoryPushNotificationConfigService; import io.dscope.camel.a2a.service.InMemoryTaskEventService; +import io.dscope.camel.a2a.service.PersistentA2ATaskEventService; +import io.dscope.camel.a2a.service.PersistentA2ATaskService; import io.dscope.camel.a2a.service.WebhookPushNotificationNotifier; +import io.dscope.camel.persistence.core.FlowStateStore; +import io.dscope.camel.persistence.core.FlowStateStoreFactory; +import io.dscope.camel.persistence.core.PersistenceConfiguration; import java.time.Instant; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.Properties; import org.apache.camel.Exchange; import org.apache.camel.Processor; import org.apache.camel.ProducerTemplate; @@ -64,6 +72,7 @@ private AgentA2AProtocolSupport() { } public static void bindIfEnabled(Main main, + Properties properties, A2ARuntimeProperties runtimeProperties, PersistenceFacade persistenceFacade, AgentPlanSelectionResolver planSelectionResolver, @@ -75,11 +84,8 @@ public static void bindIfEnabled(Main main, new A2AExposedAgentCatalogLoader().load(runtimeProperties.exposedAgentsConfig()); validatePlanMappings(planSelectionResolver, runtimeProperties, exposedAgentCatalog); - InMemoryTaskEventService taskEventService = new InMemoryTaskEventService(); - AgentA2ATaskRepository taskRepository = new AgentA2ATaskRepository(persistenceFacade, objectMapper, taskEventService); - A2APushNotificationConfigService pushConfigService = - new InMemoryPushNotificationConfigService(new WebhookPushNotificationNotifier()); - taskEventService.addListener(pushConfigService::onTaskEvent); + SharedA2AInfrastructure shared = resolveSharedInfrastructure(main, properties); + AgentA2ATaskAdapter taskAdapter = new AgentA2ATaskAdapter(shared.taskService(), persistenceFacade, objectMapper); AgentCardCatalog agentCardCatalog = new AgentA2AAgentCardCatalog(exposedAgentCatalog, runtimeProperties.rpcEndpointUrl()); @@ -87,13 +93,14 @@ public static void bindIfEnabled(Main main, Processor sendMessageProcessor = new SendMessageProcessor( runtimeProperties.agentEndpointUri(), exposedAgentCatalog, - taskRepository, - objectMapper + taskAdapter, + objectMapper, + new A2AParentConversationNotifier(persistenceFacade, objectMapper) ); Processor sendStreamingMessageProcessor = new SendStreamingMessageProcessor( sendMessageProcessor, - taskRepository, - taskEventService, + taskAdapter, + shared.taskEventService(), objectMapper, runtimeProperties ); @@ -102,7 +109,7 @@ public static void bindIfEnabled(Main main, if (request.getTaskId() == null || request.getTaskId().isBlank()) { throw new A2AInvalidParamsException("GetTask requires taskId"); } - Task task = taskRepository.getTask(request.getTaskId()); + Task task = taskAdapter.getTask(request.getTaskId()); GetTaskResponse response = new GetTaskResponse(); response.setTask(task); exchange.setProperty(A2AExchangeProperties.METHOD_RESULT, response); @@ -114,7 +121,7 @@ public static void bindIfEnabled(Main main, throw new A2AInvalidParamsException("ListTasks limit must be greater than zero"); } ListTasksResponse response = new ListTasksResponse(); - response.setTasks(taskRepository.listTasks(request.getState(), request.getLimit())); + response.setTasks(taskAdapter.listTasks(request.getState(), request.getLimit())); response.setNextCursor(null); exchange.setProperty(A2AExchangeProperties.METHOD_RESULT, response); }; @@ -123,8 +130,8 @@ public static void bindIfEnabled(Main main, if (request.getTaskId() == null || request.getTaskId().isBlank()) { throw new A2AInvalidParamsException("CancelTask requires taskId"); } - Task task = taskRepository.cancelTask(request.getTaskId(), request.getReason()); - taskRepository.appendConversationEvent( + Task task = taskAdapter.cancelTask(request.getTaskId(), request.getReason()); + taskAdapter.appendConversationEvent( conversationId(task), task.getTaskId(), "conversation.a2a.task.canceled", @@ -144,21 +151,21 @@ public static void bindIfEnabled(Main main, if (request.getTaskId() == null || request.getTaskId().isBlank()) { throw new A2AInvalidParamsException("SubscribeToTask requires taskId"); } - taskRepository.getTask(request.getTaskId()); + taskAdapter.getTask(request.getTaskId()); long afterSequence = request.getAfterSequence() == null ? 0L : Math.max(0L, request.getAfterSequence()); - TaskSubscription subscription = taskEventService.createSubscription(request.getTaskId(), afterSequence); + TaskSubscription subscription = shared.taskEventService().createSubscription(request.getTaskId(), afterSequence); SubscribeToTaskResponse response = new SubscribeToTaskResponse(); response.setSubscriptionId(subscription.getSubscriptionId()); response.setTaskId(request.getTaskId()); response.setAfterSequence(afterSequence); - response.setTerminal(taskEventService.isTaskTerminal(request.getTaskId())); + response.setTerminal(shared.taskEventService().isTaskTerminal(request.getTaskId())); response.setStreamUrl(buildStreamUrl(runtimeProperties, request.getTaskId(), subscription.getSubscriptionId(), afterSequence, request.getLimit())); exchange.setProperty(A2AExchangeProperties.METHOD_RESULT, response); }; - Processor createPushConfigProcessor = new CreatePushNotificationConfigProcessor(pushConfigService); - Processor getPushConfigProcessor = new GetPushNotificationConfigProcessor(pushConfigService); - Processor listPushConfigsProcessor = new ListPushNotificationConfigsProcessor(pushConfigService); - Processor deletePushConfigProcessor = new DeletePushNotificationConfigProcessor(pushConfigService); + Processor createPushConfigProcessor = new CreatePushNotificationConfigProcessor(shared.pushConfigService()); + Processor getPushConfigProcessor = new GetPushNotificationConfigProcessor(shared.pushConfigService()); + Processor listPushConfigsProcessor = new ListPushNotificationConfigsProcessor(shared.pushConfigService()); + Processor deletePushConfigProcessor = new DeletePushNotificationConfigProcessor(shared.pushConfigService()); Processor getExtendedAgentCardProcessor = new GetExtendedAgentCardProcessor(agentCardCatalog); Map methods = Map.ofEntries( @@ -175,9 +182,9 @@ public static void bindIfEnabled(Main main, Map.entry(A2AProtocolMethods.GET_EXTENDED_AGENT_CARD, getExtendedAgentCardProcessor) ); - bind(main, A2AComponentApplicationSupport.BEAN_TASK_EVENT_SERVICE, taskEventService); - bind(main, A2AComponentApplicationSupport.BEAN_PUSH_CONFIG_SERVICE, pushConfigService); - bind(main, A2AComponentApplicationSupport.BEAN_TASK_SERVICE, taskRepository); + bind(main, A2AComponentApplicationSupport.BEAN_TASK_EVENT_SERVICE, shared.taskEventService()); + bind(main, A2AComponentApplicationSupport.BEAN_PUSH_CONFIG_SERVICE, shared.pushConfigService()); + bind(main, A2AComponentApplicationSupport.BEAN_TASK_SERVICE, shared.taskService()); bind(main, A2AComponentApplicationSupport.BEAN_AGENT_CARD_CATALOG, agentCardCatalog); bind(main, A2AComponentApplicationSupport.BEAN_CREATE_PUSH_CONFIG_PROCESSOR, createPushConfigProcessor); bind(main, A2AComponentApplicationSupport.BEAN_GET_PUSH_CONFIG_PROCESSOR, getPushConfigProcessor); @@ -186,7 +193,7 @@ public static void bindIfEnabled(Main main, bind(main, A2AComponentApplicationSupport.BEAN_ENVELOPE_PROCESSOR, new A2AJsonRpcEnvelopeProcessor(A2AProtocolMethods.CORE_METHODS)); bind(main, A2AComponentApplicationSupport.BEAN_ERROR_PROCESSOR, new A2AErrorProcessor()); bind(main, A2AComponentApplicationSupport.BEAN_METHOD_PROCESSOR, new A2AMethodDispatchProcessor(methods)); - bind(main, A2AComponentApplicationSupport.BEAN_SSE_PROCESSOR, new A2ATaskSseProcessor(taskEventService)); + bind(main, A2AComponentApplicationSupport.BEAN_SSE_PROCESSOR, new A2ATaskSseProcessor(shared.taskEventService())); bind(main, A2AComponentApplicationSupport.BEAN_AGENT_CARD_DISCOVERY_PROCESSOR, new AgentCardDiscoveryProcessor(agentCardCatalog)); bind(main, A2AComponentApplicationSupport.BEAN_GET_EXTENDED_AGENT_CARD_PROCESSOR, getExtendedAgentCardProcessor); @@ -211,12 +218,55 @@ private static void validatePlanMappings(AgentPlanSelectionResolver planSelectio } } + private static SharedA2AInfrastructure resolveSharedInfrastructure(Main main, Properties properties) { + InMemoryTaskEventService taskEventService = + main.lookup(A2AComponentApplicationSupport.BEAN_TASK_EVENT_SERVICE, InMemoryTaskEventService.class); + A2ATaskService taskService = + main.lookup(A2AComponentApplicationSupport.BEAN_TASK_SERVICE, A2ATaskService.class); + A2APushNotificationConfigService pushConfigService = + main.lookup(A2AComponentApplicationSupport.BEAN_PUSH_CONFIG_SERVICE, A2APushNotificationConfigService.class); + + if (taskEventService != null && taskService != null && pushConfigService != null) { + return new SharedA2AInfrastructure(taskEventService, taskService, pushConfigService); + } + + PersistenceConfiguration persistenceConfiguration = PersistenceConfiguration.fromProperties( + properties == null ? new Properties() : properties + ); + if (taskEventService == null) { + if (persistenceConfiguration.enabled()) { + FlowStateStore stateStore = FlowStateStoreFactory.create(persistenceConfiguration); + taskEventService = new PersistentA2ATaskEventService(stateStore); + } else { + taskEventService = new InMemoryTaskEventService(); + } + } + if (taskService == null) { + if (persistenceConfiguration.enabled()) { + FlowStateStore stateStore = FlowStateStoreFactory.create(persistenceConfiguration); + taskService = new PersistentA2ATaskService(stateStore, taskEventService, persistenceConfiguration.rehydrationPolicy()); + } else { + taskService = new InMemoryA2ATaskService(taskEventService); + } + } + if (pushConfigService == null) { + pushConfigService = new InMemoryPushNotificationConfigService(new WebhookPushNotificationNotifier()); + taskEventService.addListener(pushConfigService::onTaskEvent); + } + return new SharedA2AInfrastructure(taskEventService, taskService, pushConfigService); + } + private static void bind(Main main, String beanName, Object bean) { if (main.lookup(beanName, Object.class) == null) { main.bind(beanName, bean); } } + private record SharedA2AInfrastructure(InMemoryTaskEventService taskEventService, + A2ATaskService taskService, + A2APushNotificationConfigService pushConfigService) { + } + private static Object requiredParams(Exchange exchange, String message) { Object params = exchange.getProperty(A2AExchangeProperties.NORMALIZED_PARAMS); if (params == null) { @@ -257,17 +307,20 @@ private static final class SendMessageProcessor implements Processor { private final String agentEndpointUri; private final A2AExposedAgentCatalog exposedAgentCatalog; - private final AgentA2ATaskRepository taskRepository; + private final AgentA2ATaskAdapter taskAdapter; private final ObjectMapper objectMapper; + private final A2AParentConversationNotifier parentConversationNotifier; private SendMessageProcessor(String agentEndpointUri, A2AExposedAgentCatalog exposedAgentCatalog, - AgentA2ATaskRepository taskRepository, - ObjectMapper objectMapper) { + AgentA2ATaskAdapter taskAdapter, + ObjectMapper objectMapper, + A2AParentConversationNotifier parentConversationNotifier) { this.agentEndpointUri = agentEndpointUri; this.exposedAgentCatalog = exposedAgentCatalog; - this.taskRepository = taskRepository; + this.taskAdapter = taskAdapter; this.objectMapper = objectMapper; + this.parentConversationNotifier = parentConversationNotifier; } @Override @@ -276,14 +329,6 @@ public void process(Exchange exchange) throws Exception { if (request.getMessage() == null) { throw new A2AInvalidParamsException("SendMessage requires message"); } - String idempotencyKey = normalizeIdempotencyKey(request); - Task existing = taskRepository.existingByIdempotency(idempotencyKey); - if (existing != null) { - SendMessageResponse response = new SendMessageResponse(); - response.setTask(existing); - exchange.setProperty(A2AExchangeProperties.METHOD_RESULT, response); - return; - } A2AExposedAgentSpec exposedAgent = resolveExposedAgent(request); String localConversationId = firstNonBlank( @@ -302,10 +347,21 @@ public void process(Exchange exchange) throws Exception { localConversationId ); String remoteConversationId = firstNonBlank(request.getConversationId(), metadataValue(request.getMetadata(), "remoteConversationId")); + String aguiSessionId = firstNonBlank( + metadataValue(request.getMetadata(), "aguiSessionId"), + metadataValue(request.getMetadata(), "camelAgent.aguiSessionId") + ); + String aguiRunId = firstNonBlank( + metadataValue(request.getMetadata(), "aguiRunId"), + metadataValue(request.getMetadata(), "camelAgent.aguiRunId") + ); + String aguiThreadId = firstNonBlank( + metadataValue(request.getMetadata(), "aguiThreadId"), + metadataValue(request.getMetadata(), "camelAgent.aguiThreadId") + ); String userText = extractMessageText(request.getMessage()); - String taskId = UUID.randomUUID().toString(); - bindCorrelation(localConversationId, exposedAgent.getAgentId(), remoteConversationId, taskId, parentConversationId, rootConversationId); + bindCorrelation(localConversationId, exposedAgent.getAgentId(), remoteConversationId, "", parentConversationId, rootConversationId); Map headers = new LinkedHashMap<>(); headers.put(AgentHeaders.CONVERSATION_ID, localConversationId); @@ -313,12 +369,45 @@ public void process(Exchange exchange) throws Exception { headers.put(AgentHeaders.PLAN_VERSION, exposedAgent.getPlanVersion()); headers.put(AgentHeaders.A2A_AGENT_ID, exposedAgent.getAgentId()); headers.put(AgentHeaders.A2A_REMOTE_CONVERSATION_ID, remoteConversationId); - headers.put(AgentHeaders.A2A_REMOTE_TASK_ID, taskId); + headers.put(AgentHeaders.A2A_REMOTE_TASK_ID, ""); headers.put(AgentHeaders.A2A_LINKED_CONVERSATION_ID, localConversationId); headers.put(AgentHeaders.A2A_PARENT_CONVERSATION_ID, parentConversationId); headers.put(AgentHeaders.A2A_ROOT_CONVERSATION_ID, rootConversationId); + if (!aguiSessionId.isBlank()) { + headers.put(AgentHeaders.AGUI_SESSION_ID, aguiSessionId); + } + if (!aguiRunId.isBlank()) { + headers.put(AgentHeaders.AGUI_RUN_ID, aguiRunId); + } + if (!aguiThreadId.isBlank()) { + headers.put(AgentHeaders.AGUI_THREAD_ID, aguiThreadId); + } - taskRepository.appendConversationEvent( + Map metadata = taskMetadata( + exposedAgent, + localConversationId, + remoteConversationId, + "", + parentConversationId, + rootConversationId, + aguiSessionId, + aguiRunId, + aguiThreadId, + request.getMetadata() + ); + + Task acceptedTask = taskAdapter.accept(request, metadata); + if (taskAdapter.isResponseCompleted(acceptedTask)) { + SendMessageResponse response = new SendMessageResponse(); + response.setTask(acceptedTask); + exchange.setProperty(A2AExchangeProperties.METHOD_RESULT, response); + return; + } + String taskId = acceptedTask.getTaskId(); + headers.put(AgentHeaders.A2A_REMOTE_TASK_ID, taskId); + bindCorrelation(localConversationId, exposedAgent.getAgentId(), remoteConversationId, taskId, parentConversationId, rootConversationId); + + taskAdapter.appendConversationEvent( localConversationId, taskId, "conversation.a2a.request.accepted", @@ -343,27 +432,54 @@ public void process(Exchange exchange) throws Exception { } Message assistantMessage = assistantMessage(request.getMessage(), agentReply); - Task task = taskRepository.createCompletedTask( - taskId, - idempotencyKey, + Map completedMetadata = taskMetadata( + exposedAgent, localConversationId, - request.getMessage(), + remoteConversationId, + taskId, + parentConversationId, + rootConversationId, + aguiSessionId, + aguiRunId, + aguiThreadId, + request.getMetadata() + ); + Task task = taskAdapter.complete( + acceptedTask, assistantMessage, - taskMetadata(exposedAgent, localConversationId, remoteConversationId, taskId, parentConversationId, rootConversationId, request.getMetadata()) + completedMetadata, + "Camel Agent completed the task" ); - taskRepository.appendConversationEvent( + taskAdapter.appendConversationEvent( localConversationId, taskId, "conversation.a2a.response.completed", - Map.of( - "agentId", exposedAgent.getAgentId(), - "planName", exposedAgent.getPlanName(), - "planVersion", exposedAgent.getPlanVersion(), - "responseText", agentReply, - "linkedConversationId", localConversationId + taskMetadata( + exposedAgent, + localConversationId, + remoteConversationId, + taskId, + parentConversationId, + rootConversationId, + aguiSessionId, + aguiRunId, + aguiThreadId, + request.getMetadata() ) ); + parentConversationNotifier.notifyParent( + parentConversationId, + localConversationId, + taskId, + exposedAgent.getAgentId(), + exposedAgent.getPlanName(), + exposedAgent.getPlanVersion(), + aguiSessionId, + aguiRunId, + aguiThreadId, + agentReply + ); SendMessageResponse response = new SendMessageResponse(); response.setTask(task); @@ -410,6 +526,9 @@ private Map taskMetadata(A2AExposedAgentSpec exposedAgent, String taskId, String parentConversationId, String rootConversationId, + String aguiSessionId, + String aguiRunId, + String aguiThreadId, Map requestMetadata) { Map metadata = new LinkedHashMap<>(); if (requestMetadata != null && !requestMetadata.isEmpty()) { @@ -425,6 +544,9 @@ private Map taskMetadata(A2AExposedAgentSpec exposedAgent, camelAgent.put("remoteTaskId", taskId); camelAgent.put("parentConversationId", parentConversationId == null ? "" : parentConversationId); camelAgent.put("rootConversationId", rootConversationId == null ? "" : rootConversationId); + camelAgent.put("aguiSessionId", aguiSessionId == null ? "" : aguiSessionId); + camelAgent.put("aguiRunId", aguiRunId == null ? "" : aguiRunId); + camelAgent.put("aguiThreadId", aguiThreadId == null ? "" : aguiThreadId); camelAgent.put("selectedAt", Instant.now().toString()); metadata.put("camelAgent", camelAgent); metadata.put("agentId", exposedAgent.getAgentId()); @@ -433,6 +555,11 @@ private Map taskMetadata(A2AExposedAgentSpec exposedAgent, metadata.put("linkedConversationId", localConversationId); metadata.put("remoteConversationId", remoteConversationId == null ? "" : remoteConversationId); metadata.put("remoteTaskId", taskId); + metadata.put("parentConversationId", parentConversationId == null ? "" : parentConversationId); + metadata.put("rootConversationId", rootConversationId == null ? "" : rootConversationId); + metadata.put("aguiSessionId", aguiSessionId == null ? "" : aguiSessionId); + metadata.put("aguiRunId", aguiRunId == null ? "" : aguiRunId); + metadata.put("aguiThreadId", aguiThreadId == null ? "" : aguiThreadId); return metadata; } @@ -462,16 +589,6 @@ private String extractMessageText(Message message) { .orElse(""); } - private String normalizeIdempotencyKey(SendMessageRequest request) { - if (request.getIdempotencyKey() != null && !request.getIdempotencyKey().isBlank()) { - return request.getIdempotencyKey().trim(); - } - if (request.getMessage() != null && request.getMessage().getMessageId() != null && !request.getMessage().getMessageId().isBlank()) { - return request.getMessage().getMessageId().trim(); - } - return ""; - } - private String metadataValue(Map metadata, String key) { if (metadata == null || metadata.isEmpty() || key == null || key.isBlank()) { return ""; @@ -495,18 +612,18 @@ private String metadataValue(Map metadata, String key) { private static final class SendStreamingMessageProcessor implements Processor { private final Processor sendMessageProcessor; - private final AgentA2ATaskRepository taskRepository; + private final AgentA2ATaskAdapter taskAdapter; private final InMemoryTaskEventService taskEventService; private final ObjectMapper objectMapper; private final A2ARuntimeProperties runtimeProperties; private SendStreamingMessageProcessor(Processor sendMessageProcessor, - AgentA2ATaskRepository taskRepository, + AgentA2ATaskAdapter taskAdapter, InMemoryTaskEventService taskEventService, ObjectMapper objectMapper, A2ARuntimeProperties runtimeProperties) { this.sendMessageProcessor = sendMessageProcessor; - this.taskRepository = taskRepository; + this.taskAdapter = taskAdapter; this.taskEventService = taskEventService; this.objectMapper = objectMapper; this.runtimeProperties = runtimeProperties; @@ -533,7 +650,7 @@ public void process(Exchange exchange) throws Exception { Task task = sendMessageResponse.getTask(); TaskSubscription subscription = taskEventService.createSubscription(task.getTaskId(), 0L); SendStreamingMessageResponse response = new SendStreamingMessageResponse(); - response.setTask(taskRepository.getTask(task.getTaskId())); + response.setTask(taskAdapter.getTask(task.getTaskId())); response.setSubscriptionId(subscription.getSubscriptionId()); response.setStreamUrl(buildStreamUrl(runtimeProperties, task.getTaskId(), subscription.getSubscriptionId(), 0L, null)); exchange.setProperty(A2AExchangeProperties.METHOD_RESULT, response); diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2ATaskAdapter.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2ATaskAdapter.java new file mode 100644 index 0000000..f9b802c --- /dev/null +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2ATaskAdapter.java @@ -0,0 +1,159 @@ +package io.dscope.camel.agent.a2a; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dscope.camel.agent.api.PersistenceFacade; +import io.dscope.camel.agent.model.AgentEvent; +import io.dscope.camel.a2a.model.Message; +import io.dscope.camel.a2a.model.Task; +import io.dscope.camel.a2a.model.TaskState; +import io.dscope.camel.a2a.model.dto.CancelTaskRequest; +import io.dscope.camel.a2a.model.dto.ListTasksRequest; +import io.dscope.camel.a2a.model.dto.SendMessageRequest; +import java.time.Instant; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +final class AgentA2ATaskAdapter { + + private final io.dscope.camel.a2a.service.A2ATaskService taskService; + private final PersistenceFacade persistenceFacade; + private final ObjectMapper objectMapper; + + AgentA2ATaskAdapter(io.dscope.camel.a2a.service.A2ATaskService taskService, + PersistenceFacade persistenceFacade, + ObjectMapper objectMapper) { + this.taskService = taskService; + this.persistenceFacade = persistenceFacade; + this.objectMapper = objectMapper; + } + + Task accept(SendMessageRequest request, Map metadata) { + SendMessageRequest effectiveRequest = new SendMessageRequest(); + effectiveRequest.setMessage(request == null ? null : request.getMessage()); + effectiveRequest.setConversationId(request == null ? null : request.getConversationId()); + effectiveRequest.setIdempotencyKey(request == null ? null : request.getIdempotencyKey()); + effectiveRequest.setMetadata(mergeMetadata(request == null ? null : request.getMetadata(), metadata)); + Task task = taskService.sendMessage(effectiveRequest); + mergeMetadata(task, metadata); + return task; + } + + boolean isResponseCompleted(Task task) { + return "true".equalsIgnoreCase(metadataText(task, "camelAgent.responseCompleted")); + } + + Task getTask(String taskId) { + return taskService.getTask(taskId); + } + + List listTasks(String state, Integer limit) { + ListTasksRequest request = new ListTasksRequest(); + request.setState(state); + request.setLimit(limit); + return taskService.listTasks(request); + } + + Task cancelTask(String taskId, String reason) { + CancelTaskRequest request = new CancelTaskRequest(); + request.setTaskId(taskId); + request.setReason(reason); + return taskService.cancelTask(request); + } + + Task complete(Task task, + Message responseMessage, + Map metadata, + String completionMessage) { + mergeMetadata(task, metadata); + task.setLatestMessage(responseMessage); + task.setMessages(combineMessages(task, responseMessage)); + task.setUpdatedAt(Instant.now().toString()); + if (task.getMetadata() != null) { + Object camelAgent = task.getMetadata().get("camelAgent"); + if (camelAgent instanceof Map map) { + @SuppressWarnings("unchecked") + Map mutable = new LinkedHashMap<>((Map) map); + mutable.put("responseCompleted", true); + task.getMetadata().put("camelAgent", mutable); + } + task.getMetadata().put("responseCompleted", true); + } + taskService.transitionTask(task.getTaskId(), TaskState.COMPLETED, completionMessage); + return taskService.getTask(task.getTaskId()); + } + + void appendConversationEvent(String conversationId, + String taskId, + String type, + Map payload) { + if (persistenceFacade == null || conversationId == null || conversationId.isBlank()) { + return; + } + persistenceFacade.appendEvent( + new AgentEvent(conversationId, taskId, type, objectMapper.valueToTree(payload == null ? Map.of() : payload), Instant.now()), + UUID.randomUUID().toString() + ); + } + + private List combineMessages(Task task, Message responseMessage) { + List messages = new ArrayList<>(); + if (task != null && task.getMessages() != null && !task.getMessages().isEmpty()) { + messages.addAll(task.getMessages()); + } else if (task != null && task.getLatestMessage() != null) { + messages.add(task.getLatestMessage()); + } + if (responseMessage != null) { + messages.add(responseMessage); + } + return messages; + } + + @SuppressWarnings("unchecked") + private void mergeMetadata(Task task, Map metadata) { + Map merged = mergeMetadata(task == null ? null : task.getMetadata(), metadata); + if (task != null) { + task.setMetadata(merged); + } + } + + @SuppressWarnings("unchecked") + private Map mergeMetadata(Map existingMetadata, Map incomingMetadata) { + if ((existingMetadata == null || existingMetadata.isEmpty()) && (incomingMetadata == null || incomingMetadata.isEmpty())) { + return existingMetadata == null ? new LinkedHashMap<>() : new LinkedHashMap<>(existingMetadata); + } + Map merged = new LinkedHashMap<>(); + if (existingMetadata != null && !existingMetadata.isEmpty()) { + merged.putAll(existingMetadata); + } + if (incomingMetadata != null && !incomingMetadata.isEmpty()) { + merged.putAll(incomingMetadata); + } + Object camelAgent = merged.get("camelAgent"); + if (camelAgent instanceof Map incomingCamelAgent) { + Map existingCamelAgent = existingMetadata != null + && existingMetadata.get("camelAgent") instanceof Map existing + ? new LinkedHashMap<>((Map) existing) + : new LinkedHashMap<>(); + existingCamelAgent.putAll((Map) incomingCamelAgent); + merged.put("camelAgent", existingCamelAgent); + } + return merged; + } + private String metadataText(Task task, String path) { + try { + JsonNode metadata = objectMapper.valueToTree(task == null ? Map.of() : task.getMetadata()); + String[] parts = path.split("\\."); + JsonNode current = metadata; + for (String part : parts) { + current = current.path(part); + } + return current.isMissingNode() || current.isNull() ? "" : current.asText(""); + } catch (Exception ignored) { + return ""; + } + } +} diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2ATaskRepository.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2ATaskRepository.java deleted file mode 100644 index ac3bca6..0000000 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2ATaskRepository.java +++ /dev/null @@ -1,227 +0,0 @@ -package io.dscope.camel.agent.a2a; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.JsonNode; -import io.dscope.camel.agent.api.PersistenceFacade; -import io.dscope.camel.agent.model.AgentEvent; -import io.dscope.camel.agent.model.TaskState; -import io.dscope.camel.agent.model.TaskStatus; -import io.dscope.camel.a2a.model.Message; -import io.dscope.camel.a2a.model.Task; -import io.dscope.camel.a2a.processor.A2AInvalidParamsException; -import io.dscope.camel.a2a.service.InMemoryTaskEventService; -import java.time.Instant; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.stream.Collectors; - -public final class AgentA2ATaskRepository { - - private final PersistenceFacade persistenceFacade; - private final ObjectMapper objectMapper; - private final InMemoryTaskEventService eventService; - private final ConcurrentMap tasks = new ConcurrentHashMap<>(); - private final ConcurrentMap> historyByTaskId = new ConcurrentHashMap<>(); - private final ConcurrentMap idempotencyToTaskId = new ConcurrentHashMap<>(); - - public AgentA2ATaskRepository(PersistenceFacade persistenceFacade, - ObjectMapper objectMapper, - InMemoryTaskEventService eventService) { - this.persistenceFacade = persistenceFacade; - this.objectMapper = objectMapper; - this.eventService = eventService; - } - - public Task existingByIdempotency(String idempotencyKey) { - if (idempotencyKey == null || idempotencyKey.isBlank()) { - return null; - } - String taskId = idempotencyToTaskId.get(idempotencyKey.trim()); - return taskId == null ? null : getTask(taskId); - } - - public Task createCompletedTask(String taskId, - String idempotencyKey, - String conversationId, - Message requestMessage, - Message responseMessage, - Map metadata) { - String now = Instant.now().toString(); - Task task = new Task(); - task.setTaskId(taskId); - task.setCreatedAt(now); - task.setUpdatedAt(now); - task.setLatestMessage(responseMessage); - task.setMessages(List.of(requestMessage, responseMessage)); - task.setArtifacts(List.of()); - task.setMetadata(metadata == null ? Map.of() : Map.copyOf(metadata)); - - List history = new ArrayList<>(); - history.add(status(io.dscope.camel.a2a.model.TaskState.CREATED, "Task accepted")); - history.add(status(io.dscope.camel.a2a.model.TaskState.RUNNING, "Camel Agent is processing the message")); - io.dscope.camel.a2a.model.TaskStatus completed = - status(io.dscope.camel.a2a.model.TaskState.COMPLETED, "Camel Agent completed the task"); - history.add(completed); - task.setStatus(copyStatus(completed)); - - tasks.put(taskId, task); - historyByTaskId.put(taskId, history); - if (idempotencyKey != null && !idempotencyKey.isBlank()) { - idempotencyToTaskId.putIfAbsent(idempotencyKey.trim(), taskId); - } - publishHistory(task, history); - persistTask(conversationId, task); - return copyTask(task); - } - - public Task getTask(String taskId) { - Task current = tasks.get(taskId); - if (current != null) { - return copyTask(current); - } - Optional persisted = persistenceFacade == null ? Optional.empty() : persistenceFacade.loadTask(taskId); - if (persisted.isEmpty() || persisted.get().result() == null || persisted.get().result().isBlank()) { - throw new A2AInvalidParamsException("Task not found: " + taskId); - } - try { - Task task = objectMapper.readValue(persisted.get().result(), Task.class); - tasks.putIfAbsent(taskId, task); - return copyTask(task); - } catch (Exception e) { - throw new IllegalStateException("Failed to deserialize A2A task: " + taskId, e); - } - } - - public List listTasks(String state, Integer limit) { - List resolved = tasks.values().stream() - .filter(task -> state == null || state.isBlank() - || task.getStatus() != null - && task.getStatus().getState() != null - && task.getStatus().getState().name().equalsIgnoreCase(state)) - .map(this::copyTask) - .collect(Collectors.toCollection(ArrayList::new)); - if (limit != null && limit > 0 && resolved.size() > limit) { - return resolved.subList(0, limit); - } - return resolved; - } - - public Task cancelTask(String taskId, String reason) { - Task current = getTask(taskId); - if (current.getStatus() != null && current.getStatus().getState() == io.dscope.camel.a2a.model.TaskState.CANCELED) { - return current; - } - io.dscope.camel.a2a.model.TaskStatus canceled = - status(io.dscope.camel.a2a.model.TaskState.CANCELED, reason == null || reason.isBlank() ? "Task canceled" : reason); - current.setStatus(canceled); - current.setUpdatedAt(Instant.now().toString()); - tasks.put(taskId, current); - historyByTaskId.computeIfAbsent(taskId, ignored -> new ArrayList<>()).add(copyStatus(canceled)); - eventService.publishTaskUpdate(current); - persistTask(metadataText(current, "camelAgent.localConversationId"), current); - return copyTask(current); - } - - public List history(String taskId) { - getTask(taskId); - return historyByTaskId.getOrDefault(taskId, List.of()).stream().map(this::copyStatus).toList(); - } - - public void appendConversationEvent(String conversationId, - String taskId, - String type, - Map payload) { - if (persistenceFacade == null || conversationId == null || conversationId.isBlank()) { - return; - } - persistenceFacade.appendEvent( - new AgentEvent(conversationId, taskId, type, objectMapper.valueToTree(payload == null ? Map.of() : payload), Instant.now()), - UUID.randomUUID().toString() - ); - } - - private void publishHistory(Task task, List history) { - for (io.dscope.camel.a2a.model.TaskStatus status : history) { - Task copy = copyTask(task); - copy.setStatus(copyStatus(status)); - eventService.publishTaskUpdate(copy); - } - } - - private void persistTask(String conversationId, Task task) { - if (persistenceFacade == null || task == null || task.getTaskId() == null || task.getTaskId().isBlank()) { - return; - } - try { - persistenceFacade.saveTask(new TaskState( - task.getTaskId(), - conversationId == null ? "" : conversationId, - toAgentTaskStatus(task.getStatus()), - task.getStatus() == null || task.getStatus().getState() == null ? "" : task.getStatus().getState().name(), - null, - 0, - objectMapper.writeValueAsString(task) - )); - } catch (Exception e) { - throw new IllegalStateException("Failed to persist A2A task " + task.getTaskId(), e); - } - } - - private TaskStatus toAgentTaskStatus(io.dscope.camel.a2a.model.TaskStatus status) { - if (status == null || status.getState() == null) { - return TaskStatus.CREATED; - } - return switch (status.getState()) { - case CREATED, QUEUED -> TaskStatus.CREATED; - case RUNNING -> TaskStatus.STARTED; - case WAITING -> TaskStatus.WAITING; - case COMPLETED -> TaskStatus.FINISHED; - case FAILED, CANCELED -> TaskStatus.FAILED; - }; - } - - private io.dscope.camel.a2a.model.TaskStatus status(io.dscope.camel.a2a.model.TaskState state, String message) { - io.dscope.camel.a2a.model.TaskStatus status = new io.dscope.camel.a2a.model.TaskStatus(); - status.setState(state); - status.setMessage(message); - status.setUpdatedAt(Instant.now().toString()); - return status; - } - - private String metadataText(Task task, String path) { - try { - JsonNode metadata = objectMapper.valueToTree(task == null ? Map.of() : task.getMetadata()); - String[] parts = path.split("\\."); - JsonNode current = metadata; - for (String part : parts) { - current = current.path(part); - } - return current.isMissingNode() || current.isNull() ? "" : current.asText(""); - } catch (Exception ignored) { - return ""; - } - } - - private Task copyTask(Task source) { - try { - return objectMapper.readValue(objectMapper.writeValueAsBytes(source), Task.class); - } catch (Exception e) { - throw new IllegalStateException("Failed to copy A2A task", e); - } - } - - private io.dscope.camel.a2a.model.TaskStatus copyStatus(io.dscope.camel.a2a.model.TaskStatus source) { - io.dscope.camel.a2a.model.TaskStatus copy = new io.dscope.camel.a2a.model.TaskStatus(); - copy.setState(source.getState()); - copy.setMessage(source.getMessage()); - copy.setUpdatedAt(source.getUpdatedAt()); - copy.setDetails(source.getDetails()); - return copy; - } -} diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentRuntimeBootstrap.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentRuntimeBootstrap.java index d8ab157..69b8f86 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentRuntimeBootstrap.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentRuntimeBootstrap.java @@ -80,6 +80,7 @@ public static void bootstrap(Main main, String applicationYamlPath) throws Excep ObjectMapper objectMapper = existingObjectMapper(main); main.bind("objectMapper", objectMapper); + main.bind("ticketLifecycleProcessor", new TicketLifecycleProcessor(objectMapper)); PersistenceFacade persistenceFacade = existingPersistenceFacade(main); if (persistenceFacade == null) { @@ -557,6 +558,7 @@ && isBlank(properties.getProperty("agent.runtime.realtime.requireInitSession"))) AgentA2AProtocolSupport.bindIfEnabled( main, + properties, A2ARuntimeProperties.from(properties), persistenceFacade, planSelectionResolver, diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/TicketLifecycleProcessor.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/TicketLifecycleProcessor.java new file mode 100644 index 0000000..f86ee26 --- /dev/null +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/TicketLifecycleProcessor.java @@ -0,0 +1,168 @@ +package io.dscope.camel.agent.runtime; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.dscope.camel.agent.config.AgentHeaders; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.zip.CRC32; +import org.apache.camel.Exchange; +import org.apache.camel.Message; +import org.apache.camel.Processor; + +/** + * Small stateful ticket lifecycle processor used by the sample app. + * It keeps per-conversation ticket state so the A2A ticket service can + * demonstrate open/update/close/status flows across turns. + */ +public class TicketLifecycleProcessor implements Processor { + + private final ObjectMapper objectMapper; + private final ConcurrentMap ticketsByConversationId = new ConcurrentHashMap<>(); + + public TicketLifecycleProcessor(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public void process(Exchange exchange) throws Exception { + Message in = exchange.getIn(); + String conversationId = readConversationId(in); + String query = normalizeQuery(in.getBody()); + + TicketAction action = determineAction(query); + TicketRecord current = ticketsByConversationId.compute(conversationId, (key, existing) -> applyAction(key, existing, action, query)); + + ObjectNode payload = objectMapper.createObjectNode(); + payload.put("ticketId", current.ticketId()); + payload.put("status", current.status()); + payload.put("action", action.name()); + payload.put("summary", current.summary()); + payload.put("assignedQueue", current.assignedQueue()); + payload.put("message", messageFor(action)); + payload.put("notifyClient", action == TicketAction.UPDATE || action == TicketAction.CLOSE || action == TicketAction.STATUS); + payload.put("requiresCustomerAction", action == TicketAction.UPDATE && query.toLowerCase(Locale.ROOT).contains("confirm")); + payload.put("conversationId", conversationId); + payload.put("updatedAt", Instant.now().toString()); + + exchange.getMessage().setHeader(Exchange.CONTENT_TYPE, "application/json"); + exchange.getMessage().setBody(objectMapper.writeValueAsString(payload)); + } + + private TicketRecord applyAction(String conversationId, TicketRecord existing, TicketAction action, String query) { + TicketRecord current = existing != null ? existing : createRecord(conversationId, query); + return switch (action) { + case OPEN -> createRecord(conversationId, query); + case UPDATE -> new TicketRecord( + current.ticketId(), + "IN_PROGRESS", + firstNonBlank(query, current.summary()), + current.assignedQueue() + ); + case CLOSE -> new TicketRecord( + current.ticketId(), + "CLOSED", + firstNonBlank(query, current.summary()), + current.assignedQueue() + ); + case STATUS -> current; + }; + } + + private TicketRecord createRecord(String conversationId, String query) { + return new TicketRecord( + ticketIdFor(conversationId), + "OPEN", + firstNonBlank(query, "Customer requested support follow-up"), + queueFor(query) + ); + } + + private String ticketIdFor(String conversationId) { + CRC32 crc = new CRC32(); + crc.update(conversationId.getBytes(StandardCharsets.UTF_8)); + return "TCK-" + Long.toUnsignedString(crc.getValue(), 36).toUpperCase(Locale.ROOT); + } + + private String queueFor(String query) { + String normalized = query.toLowerCase(Locale.ROOT); + if (normalized.contains("bill") || normalized.contains("refund") || normalized.contains("invoice") || normalized.contains("payment")) { + return "BILLING-L2"; + } + if (normalized.contains("security") || normalized.contains("fraud")) { + return "SECURITY-L2"; + } + return "L1-SUPPORT"; + } + + private TicketAction determineAction(String query) { + String normalized = query.toLowerCase(Locale.ROOT); + if (containsAny(normalized, "close", "closed", "resolve", "resolved", "done with", "cancel ticket")) { + return TicketAction.CLOSE; + } + if (containsAny(normalized, "status", "progress", "check ticket", "where is", "what is happening")) { + return TicketAction.STATUS; + } + if (containsAny(normalized, "update", "add note", "change", "modify", "priority", "reopen")) { + return TicketAction.UPDATE; + } + return TicketAction.OPEN; + } + + private boolean containsAny(String text, String... markers) { + for (String marker : markers) { + if (text.contains(marker)) { + return true; + } + } + return false; + } + + private String messageFor(TicketAction action) { + return switch (action) { + case OPEN -> "Support ticket created successfully"; + case UPDATE -> "Support ticket updated and routed back to the client conversation"; + case CLOSE -> "Support ticket closed and the client conversation has been updated"; + case STATUS -> "Support ticket status retrieved successfully"; + }; + } + + private String normalizeQuery(Object body) { + if (body instanceof Map map) { + Object query = map.get("query"); + return query == null ? "" : String.valueOf(query).trim(); + } + return body == null ? "" : String.valueOf(body).trim(); + } + + private String readConversationId(Message in) { + String conversationId = in.getHeader(AgentHeaders.CONVERSATION_ID, String.class); + if (conversationId == null || conversationId.isBlank()) { + return "sample-ticket-fallback"; + } + return conversationId.trim(); + } + + private String firstNonBlank(String... values) { + for (String value : values) { + if (value != null && !value.isBlank()) { + return value.trim(); + } + } + return ""; + } + + private enum TicketAction { + OPEN, + UPDATE, + CLOSE, + STATUS + } + + private record TicketRecord(String ticketId, String status, String summary, String assignedQueue) { + } +} diff --git a/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/A2AToolClientTest.java b/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/A2AToolClientTest.java index 1f75a08..13a9526 100644 --- a/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/A2AToolClientTest.java +++ b/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/A2AToolClientTest.java @@ -46,8 +46,11 @@ void tearDown() { @Test void sendsRemoteA2aMessageAndBindsCorrelation() throws Exception { + CorrelationRegistry.global().bind("conv-a2a", "agui.sessionId", "session-1"); + CorrelationRegistry.global().bind("conv-a2a", "agui.runId", "run-1"); + CorrelationRegistry.global().bind("conv-a2a", "agui.threadId", "thread-1"); FakeHttpClient httpClient = new FakeHttpClient(List.of(""" - {"jsonrpc":"2.0","result":{"task":{"taskId":"remote-task-1","status":{"state":"COMPLETED"},"latestMessage":{"parts":[{"text":"remote answer"}]}}},"id":"1"} + {"jsonrpc":"2.0","result":{"task":{"taskId":"remote-task-1","status":{"state":"COMPLETED"},"latestMessage":{"parts":[{"text":"remote answer"}]},"metadata":{"camelAgent":{"linkedConversationId":"child-a2a-1","parentConversationId":"conv-a2a","rootConversationId":"conv-a2a"}}}},"id":"1"} """)); InMemoryPersistenceFacade persistence = new InMemoryPersistenceFacade(); @@ -75,8 +78,11 @@ void sendsRemoteA2aMessageAndBindsCorrelation() throws Exception { assertEquals("SendMessage", request.path("method").asText()); assertEquals("support-public", request.path("params").path("metadata").path("agentId").asText()); assertEquals("support", request.path("params").path("metadata").path("planName").asText()); + assertEquals("session-1", request.path("params").path("metadata").path("aguiSessionId").asText()); + assertEquals("thread-1", request.path("params").path("metadata").path("camelAgent").path("aguiThreadId").asText()); assertEquals("remote answer", result.content()); assertEquals("remote-task-1", CorrelationRegistry.global().resolve("conv-a2a", "a2a.remoteTaskId", "")); + assertEquals("child-a2a-1", CorrelationRegistry.global().resolve("conv-a2a", "a2a.linkedConversationId", "")); assertTrue(persistence.loadConversation("conv-a2a", 10).stream().anyMatch(event -> "conversation.a2a.outbound.started".equals(event.type()))); assertTrue(persistence.loadConversation("conv-a2a", 10).stream().anyMatch(event -> "conversation.a2a.outbound.completed".equals(event.type()))); } diff --git a/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupportTest.java b/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupportTest.java new file mode 100644 index 0000000..bbfd379 --- /dev/null +++ b/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupportTest.java @@ -0,0 +1,118 @@ +package io.dscope.camel.agent.a2a; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dscope.camel.agent.kernel.InMemoryPersistenceFacade; +import io.dscope.camel.agent.runtime.A2ARuntimeProperties; +import io.dscope.camel.agent.runtime.AgentPlanSelectionResolver; +import io.dscope.camel.a2a.A2AComponentApplicationSupport; +import io.dscope.camel.a2a.model.Message; +import io.dscope.camel.a2a.model.Part; +import io.dscope.camel.a2a.model.Task; +import io.dscope.camel.a2a.model.dto.SendMessageRequest; +import io.dscope.camel.a2a.service.A2APushNotificationConfigService; +import io.dscope.camel.a2a.service.A2ATaskService; +import io.dscope.camel.a2a.service.InMemoryA2ATaskService; +import io.dscope.camel.a2a.service.InMemoryPushNotificationConfigService; +import io.dscope.camel.a2a.service.InMemoryTaskEventService; +import io.dscope.camel.a2a.service.WebhookPushNotificationNotifier; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Properties; +import org.apache.camel.main.Main; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class AgentA2AProtocolSupportTest { + + @TempDir + Path tempDir; + + @Test + void reusesPreboundSharedTaskInfrastructure() throws Exception { + Path plansConfig = tempDir.resolve("agents.yaml"); + Files.writeString(plansConfig, """ + plans: + - name: support + default: true + versions: + - version: v1 + default: true + blueprint: classpath:agents/support/agent.md + """); + Path exposedAgentsConfig = tempDir.resolve("a2a-agents.yaml"); + Files.writeString(exposedAgentsConfig, """ + agents: + - agentId: support-public + name: Support Public Agent + description: Handles support requests + defaultAgent: true + version: 1.0.0 + planName: support + planVersion: v1 + """); + + Main main = new Main(); + InMemoryTaskEventService taskEventService = new InMemoryTaskEventService(); + A2ATaskService taskService = new InMemoryA2ATaskService(taskEventService); + A2APushNotificationConfigService pushConfigService = + new InMemoryPushNotificationConfigService(new WebhookPushNotificationNotifier()); + taskEventService.addListener(pushConfigService::onTaskEvent); + + main.bind(A2AComponentApplicationSupport.BEAN_TASK_EVENT_SERVICE, taskEventService); + main.bind(A2AComponentApplicationSupport.BEAN_TASK_SERVICE, taskService); + main.bind(A2AComponentApplicationSupport.BEAN_PUSH_CONFIG_SERVICE, pushConfigService); + + InMemoryPersistenceFacade persistence = new InMemoryPersistenceFacade(); + ObjectMapper objectMapper = new ObjectMapper(); + AgentPlanSelectionResolver planSelectionResolver = new AgentPlanSelectionResolver(persistence, objectMapper); + + AgentA2AProtocolSupport.bindIfEnabled( + main, + new Properties(), + new A2ARuntimeProperties( + true, + "0.0.0.0", + 8080, + "http://localhost:8080", + "/a2a/rpc", + "/a2a/sse", + "/.well-known/agent-card.json", + "direct:agent", + exposedAgentsConfig.toString(), + plansConfig.toString(), + "classpath:agents/support/agent.md" + ), + persistence, + planSelectionResolver, + objectMapper + ); + + assertSame(taskEventService, main.lookup(A2AComponentApplicationSupport.BEAN_TASK_EVENT_SERVICE, InMemoryTaskEventService.class)); + assertSame(taskService, main.lookup(A2AComponentApplicationSupport.BEAN_TASK_SERVICE, A2ATaskService.class)); + assertSame(pushConfigService, main.lookup(A2AComponentApplicationSupport.BEAN_PUSH_CONFIG_SERVICE, A2APushNotificationConfigService.class)); + + SendMessageRequest request = new SendMessageRequest(); + request.setMessage(message("user", "Check shared task")); + Task created = taskService.sendMessage(request); + + A2ATaskService boundTaskService = main.lookup(A2AComponentApplicationSupport.BEAN_TASK_SERVICE, A2ATaskService.class); + assertEquals(created.getTaskId(), boundTaskService.getTask(created.getTaskId()).getTaskId()); + } + + private static Message message(String role, String text) { + Part part = new Part(); + part.setType("text"); + part.setMimeType("text/plain"); + part.setText(text); + + Message message = new Message(); + message.setRole(role); + message.setParts(List.of(part)); + message.setMessageId(role + "-message-" + text.hashCode()); + return message; + } +} diff --git a/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2ATaskAdapterTest.java b/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2ATaskAdapterTest.java new file mode 100644 index 0000000..d735fc5 --- /dev/null +++ b/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2ATaskAdapterTest.java @@ -0,0 +1,110 @@ +package io.dscope.camel.agent.a2a; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dscope.camel.agent.kernel.InMemoryPersistenceFacade; +import io.dscope.camel.a2a.model.Message; +import io.dscope.camel.a2a.model.Part; +import io.dscope.camel.a2a.model.Task; +import io.dscope.camel.a2a.model.TaskState; +import io.dscope.camel.a2a.model.dto.SendMessageRequest; +import io.dscope.camel.a2a.service.InMemoryA2ATaskService; +import io.dscope.camel.a2a.service.InMemoryTaskEventService; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class AgentA2ATaskAdapterTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void acceptsCompletesAndAuditsUsingSharedTaskService() { + InMemoryTaskEventService taskEventService = new InMemoryTaskEventService(); + InMemoryA2ATaskService taskService = new InMemoryA2ATaskService(taskEventService); + InMemoryPersistenceFacade persistence = new InMemoryPersistenceFacade(); + AgentA2ATaskAdapter adapter = new AgentA2ATaskAdapter(taskService, persistence, objectMapper); + + SendMessageRequest request = new SendMessageRequest(); + request.setIdempotencyKey("idem-1"); + request.setMessage(message("user", "Need ticket update")); + request.setMetadata(Map.of("source", "non-agent", "camelAgent", Map.of("origin", "shared"))); + + Task accepted = adapter.accept(request, agentMetadata("conv-1")); + + assertEquals(TaskState.RUNNING, accepted.getStatus().getState()); + assertEquals("support-public", accepted.getMetadata().get("agentId")); + assertEquals("conv-1", nestedText(accepted, "camelAgent", "localConversationId")); + assertEquals("shared", nestedText(accepted, "camelAgent", "origin")); + + Task completed = adapter.complete( + accepted, + message("assistant", "Ticket updated"), + agentMetadata("conv-1"), + "Agent completed task" + ); + + assertEquals(TaskState.COMPLETED, completed.getStatus().getState()); + assertEquals("true", nestedText(completed, "camelAgent", "responseCompleted")); + + adapter.appendConversationEvent("conv-1", completed.getTaskId(), "conversation.a2a.response.completed", Map.of("status", "ok")); + assertEquals(1, persistence.loadConversation("conv-1", 10).size()); + } + + @Test + void reusesSharedTaskServiceIdempotency() { + InMemoryTaskEventService taskEventService = new InMemoryTaskEventService(); + InMemoryA2ATaskService taskService = new InMemoryA2ATaskService(taskEventService); + AgentA2ATaskAdapter adapter = new AgentA2ATaskAdapter(taskService, new InMemoryPersistenceFacade(), objectMapper); + + SendMessageRequest request = new SendMessageRequest(); + request.setIdempotencyKey("idem-2"); + request.setMessage(message("user", "Open ticket")); + + Task first = adapter.accept(request, agentMetadata("conv-2")); + Task second = adapter.accept(request, agentMetadata("conv-2")); + + assertEquals(first.getTaskId(), second.getTaskId()); + assertSame(taskService.getTask(first.getTaskId()), taskService.getTask(second.getTaskId())); + assertTrue(taskService.listTasks(null).size() >= 1); + } + + private static Map agentMetadata(String conversationId) { + Map camelAgent = new LinkedHashMap<>(); + camelAgent.put("localConversationId", conversationId); + camelAgent.put("linkedConversationId", conversationId); + camelAgent.put("planName", "support"); + camelAgent.put("planVersion", "v1"); + Map metadata = new LinkedHashMap<>(); + metadata.put("camelAgent", camelAgent); + metadata.put("agentId", "support-public"); + return metadata; + } + + private static Message message(String role, String text) { + Part part = new Part(); + part.setType("text"); + part.setMimeType("text/plain"); + part.setText(text); + + Message message = new Message(); + message.setRole(role); + message.setParts(List.of(part)); + message.setMessageId(role + "-message-" + text.hashCode()); + return message; + } + + @SuppressWarnings("unchecked") + private static String nestedText(Task task, String topLevelKey, String nestedKey) { + Object topLevel = task.getMetadata().get(topLevelKey); + if (!(topLevel instanceof Map map)) { + return ""; + } + Object value = ((Map) map).get(nestedKey); + return value == null ? "" : String.valueOf(value); + } +} diff --git a/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2ATaskRepositoryTest.java b/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2ATaskRepositoryTest.java deleted file mode 100644 index 3fb05ba..0000000 --- a/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2ATaskRepositoryTest.java +++ /dev/null @@ -1,79 +0,0 @@ -package io.dscope.camel.agent.a2a; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.dscope.camel.agent.kernel.InMemoryPersistenceFacade; -import io.dscope.camel.a2a.model.Message; -import io.dscope.camel.a2a.model.Part; -import io.dscope.camel.a2a.model.Task; -import io.dscope.camel.a2a.model.TaskState; -import io.dscope.camel.a2a.processor.A2AInvalidParamsException; -import io.dscope.camel.a2a.service.InMemoryTaskEventService; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; - -class AgentA2ATaskRepositoryTest { - - @Test - void createsPersistsAndCancelsTasks() { - InMemoryPersistenceFacade persistence = new InMemoryPersistenceFacade(); - AgentA2ATaskRepository repository = - new AgentA2ATaskRepository(persistence, new ObjectMapper(), new InMemoryTaskEventService()); - - Task created = repository.createCompletedTask( - "task-1", - "idem-1", - "conv-1", - message("user", "Need help"), - message("assistant", "Here is the answer"), - metadata("conv-1") - ); - - assertEquals(TaskState.COMPLETED, created.getStatus().getState()); - assertEquals(1, repository.listTasks(null, null).size()); - assertEquals("task-1", repository.existingByIdempotency("idem-1").getTaskId()); - - Task canceled = repository.cancelTask("task-1", "caller canceled"); - assertEquals(TaskState.CANCELED, canceled.getStatus().getState()); - assertEquals(TaskState.CANCELED, repository.getTask("task-1").getStatus().getState()); - - repository.appendConversationEvent("conv-1", "task-1", "conversation.a2a.request.accepted", Map.of("agentId", "support-public")); - assertEquals(1, persistence.loadConversation("conv-1", 10).size()); - } - - @Test - void failsForUnknownTask() { - AgentA2ATaskRepository repository = - new AgentA2ATaskRepository(new InMemoryPersistenceFacade(), new ObjectMapper(), new InMemoryTaskEventService()); - - assertThrows(A2AInvalidParamsException.class, () -> repository.getTask("missing-task")); - } - - private static Map metadata(String conversationId) { - Map metadata = new LinkedHashMap<>(); - Map camelAgent = new LinkedHashMap<>(); - camelAgent.put("localConversationId", conversationId); - metadata.put("camelAgent", camelAgent); - metadata.put("agentId", "support-public"); - return metadata; - } - - private static Message message(String role, String text) { - Part part = new Part(); - part.setType("text"); - part.setMimeType("text/plain"); - part.setText(text); - - Message message = new Message(); - message.setRole(role); - message.setParts(List.of(part)); - message.setMessageId(role + "-msg"); - assertNotNull(message.getMessageId()); - return message; - } -} diff --git a/camel-agent-core/src/test/java/io/dscope/camel/agent/runtime/TicketLifecycleProcessorTest.java b/camel-agent-core/src/test/java/io/dscope/camel/agent/runtime/TicketLifecycleProcessorTest.java new file mode 100644 index 0000000..f210336 --- /dev/null +++ b/camel-agent-core/src/test/java/io/dscope/camel/agent/runtime/TicketLifecycleProcessorTest.java @@ -0,0 +1,50 @@ +package io.dscope.camel.agent.runtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dscope.camel.agent.config.AgentHeaders; +import java.util.Map; +import org.apache.camel.Exchange; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.support.DefaultExchange; +import org.junit.jupiter.api.Test; + +class TicketLifecycleProcessorTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void keepsTicketIdStableAcrossConversationLifecycle() throws Exception { + TicketLifecycleProcessor processor = new TicketLifecycleProcessor(objectMapper); + + try (DefaultCamelContext context = new DefaultCamelContext()) { + context.start(); + + JsonNode created = invoke(processor, context, "child-conv-1", Map.of("query", "Please open a support ticket for login errors")); + JsonNode updated = invoke(processor, context, "child-conv-1", Map.of("query", "Please update the ticket with my latest screenshot")); + JsonNode closed = invoke(processor, context, "child-conv-1", Map.of("query", "Please close the ticket now")); + + assertEquals(created.path("ticketId").asText(), updated.path("ticketId").asText()); + assertEquals(created.path("ticketId").asText(), closed.path("ticketId").asText()); + assertEquals("OPEN", created.path("status").asText()); + assertEquals("IN_PROGRESS", updated.path("status").asText()); + assertEquals("CLOSED", closed.path("status").asText()); + assertTrue(updated.path("notifyClient").asBoolean()); + assertTrue(closed.path("notifyClient").asBoolean()); + } + } + + private JsonNode invoke(TicketLifecycleProcessor processor, + DefaultCamelContext context, + String conversationId, + Map body) throws Exception { + Exchange exchange = new DefaultExchange(context); + exchange.getIn().setHeader(AgentHeaders.CONVERSATION_ID, conversationId); + exchange.getIn().setBody(body); + processor.process(exchange); + return objectMapper.readTree(exchange.getMessage().getBody(String.class)); + } +} diff --git a/camel-agent-persistence-dscope/pom.xml b/camel-agent-persistence-dscope/pom.xml index 95b01e2..be3dc01 100644 --- a/camel-agent-persistence-dscope/pom.xml +++ b/camel-agent-persistence-dscope/pom.xml @@ -6,7 +6,7 @@ io.dscope.camel camel-agent - 0.5.0 + 0.6.0 camel-agent-persistence-dscope diff --git a/camel-agent-persistence-dscope/src/main/java/io/dscope/camel/agent/persistence/dscope/DscopePersistenceFactory.java b/camel-agent-persistence-dscope/src/main/java/io/dscope/camel/agent/persistence/dscope/DscopePersistenceFactory.java index b70e7fb..ff85071 100644 --- a/camel-agent-persistence-dscope/src/main/java/io/dscope/camel/agent/persistence/dscope/DscopePersistenceFactory.java +++ b/camel-agent-persistence-dscope/src/main/java/io/dscope/camel/agent/persistence/dscope/DscopePersistenceFactory.java @@ -44,7 +44,7 @@ static FlowStateStore createFlowStateStore(PersistenceConfiguration configuratio } PersistenceBackend backend = configuration.backend(); - if (backend == PersistenceBackend.JDBC || backend == PersistenceBackend.REDIS_JDBC) { + if (isJdbcBacked(backend)) { String ddlResource = resolveSchemaDdlResource(effective, configuration.jdbcUrl()); if (ddlResource != null && !ddlResource.isBlank()) { return new ScriptedJdbcFlowStateStore( @@ -59,6 +59,13 @@ static FlowStateStore createFlowStateStore(PersistenceConfiguration configuratio return FlowStateStoreFactory.create(configuration); } + private static boolean isJdbcBacked(PersistenceBackend backend) { + if (backend == null) { + return false; + } + return backend == PersistenceBackend.JDBC || "REDIS_JDBC".equals(backend.name()); + } + static String resolveSchemaDdlResource(Properties effective, String jdbcUrl) { if (effective != null) { String override = effective.getProperty(SCHEMA_DDL_RESOURCE_PROPERTY); diff --git a/camel-agent-spring-ai/pom.xml b/camel-agent-spring-ai/pom.xml index 1417e25..748fbf0 100644 --- a/camel-agent-spring-ai/pom.xml +++ b/camel-agent-spring-ai/pom.xml @@ -6,7 +6,7 @@ io.dscope.camel camel-agent - 0.5.0 + 0.6.0 camel-agent-spring-ai diff --git a/camel-agent-starter/pom.xml b/camel-agent-starter/pom.xml index 2d0f903..54f48de 100644 --- a/camel-agent-starter/pom.xml +++ b/camel-agent-starter/pom.xml @@ -6,7 +6,7 @@ io.dscope.camel camel-agent - 0.5.0 + 0.6.0 camel-agent-starter diff --git a/pom.xml b/pom.xml index 14b8870..8a33924 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.dscope.camel camel-agent - 0.5.0 + 0.6.0 pom Camel Agent @@ -17,7 +17,7 @@ 2.20.0 5.10.2 1.1.0 - 0.5.0-SNAPSHOT + 1.0.0 1.0.3 3.5.11 6.2.0 diff --git a/samples/agent-support-service/pom.xml b/samples/agent-support-service/pom.xml index e928c4d..6681280 100644 --- a/samples/agent-support-service/pom.xml +++ b/samples/agent-support-service/pom.xml @@ -6,7 +6,7 @@ io.dscope.camel camel-agent - 0.1.0-SNAPSHOT + 0.6.0 ../../pom.xml diff --git a/samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/DemoA2ATicketGateway.java b/samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/DemoA2ATicketGateway.java new file mode 100644 index 0000000..4505a13 --- /dev/null +++ b/samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/DemoA2ATicketGateway.java @@ -0,0 +1,109 @@ +package io.dscope.camel.agent.samples; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dscope.camel.agent.model.AiToolCall; +import io.dscope.camel.agent.model.ToolSpec; +import io.dscope.camel.agent.springai.SpringAiChatGateway; +import java.util.List; +import java.util.function.Consumer; + +public final class DemoA2ATicketGateway implements SpringAiChatGateway { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Override + public SpringAiChatResult generate(String systemPrompt, + String userContext, + List tools, + String model, + Double temperature, + Integer maxTokens, + Consumer streamingTokenCallback) { + String query = extractLastUserMessage(userContext); + ToolSpec selectedTool = selectTool(tools, query); + if (selectedTool == null) { + String message = "Demo gateway did not find a matching tool."; + if (streamingTokenCallback != null) { + streamingTokenCallback.accept(message); + } + return new SpringAiChatResult(message, List.of(), true); + } + + AiToolCall call = new AiToolCall(selectedTool.name(), mapper.createObjectNode().put("query", query)); + if (streamingTokenCallback != null) { + streamingTokenCallback.accept(""); + } + return new SpringAiChatResult("", List.of(call), true); + } + + private ToolSpec selectTool(List tools, String query) { + if (tools == null || tools.isEmpty()) { + return null; + } + String normalized = query == null ? "" : query.toLowerCase(); + if (containsTicketIntent(normalized)) { + ToolSpec routeTool = findTool(tools, "support.ticket.manage.route"); + if (routeTool != null) { + return routeTool; + } + ToolSpec a2aTool = findTool(tools, "support.ticket.manage"); + if (a2aTool != null) { + return a2aTool; + } + } + return tools.get(0); + } + + private ToolSpec findTool(List tools, String name) { + for (ToolSpec tool : tools) { + if (tool != null && name.equals(tool.name())) { + return tool; + } + } + return null; + } + + private boolean containsTicketIntent(String normalized) { + return normalized.contains("ticket") + || normalized.contains("open") + || normalized.contains("create") + || normalized.contains("update") + || normalized.contains("close") + || normalized.contains("status") + || normalized.contains("escalate"); + } + + private String extractLastUserMessage(String userContext) { + if (userContext == null || userContext.isBlank()) { + return ""; + } + String[] lines = userContext.split("\\R"); + for (int i = lines.length - 1; i >= 0; i--) { + String line = lines[i]; + if (line.startsWith("User: ")) { + return toPlainText(line.substring("User: ".length())); + } + if (line.startsWith("user.message: ")) { + return toPlainText(line.substring("user.message: ".length())); + } + } + return toPlainText(userContext); + } + + private String toPlainText(String value) { + if (value == null) { + return ""; + } + String trimmed = value.trim(); + if (trimmed.startsWith("\"") && trimmed.endsWith("\"") && trimmed.length() >= 2) { + return trimmed.substring(1, trimmed.length() - 1); + } + try { + JsonNode node = mapper.readTree(trimmed); + return node.isTextual() ? node.asText() : trimmed; + } catch (Exception ignored) { + return trimmed; + } + } +} diff --git a/samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/Main.java b/samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/Main.java index 4c91825..72b8762 100644 --- a/samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/Main.java +++ b/samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/Main.java @@ -1,5 +1,6 @@ package io.dscope.camel.agent.samples; +import com.fasterxml.jackson.databind.ObjectMapper; import io.dscope.camel.agent.runtime.AgentRuntimeBootstrap; public final class Main { @@ -9,11 +10,9 @@ private Main() { public static void main(String[] args) throws Exception { org.apache.camel.main.Main main = new org.apache.camel.main.Main(); - main.bind("supportUiPageProcessor", new SupportUiPageProcessor()); - main.bind("supportSipSessionInitEnvelopeProcessor", new SipSessionInitEnvelopeProcessor()); - main.bind("supportSipTranscriptFinalProcessor", new SipTranscriptFinalProcessor()); - main.bind("supportSipCallEndProcessor", new SipCallEndProcessor()); + main.bind("ticketLifecycleProcessor", new SupportTicketLifecycleProcessor(new ObjectMapper())); AgentRuntimeBootstrap.bootstrap(main, "application.yaml"); + SampleAdminMcpBindings.bindIfMissing(main, "application.yaml"); main.run(args); } } diff --git a/samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/SampleAdminMcpBindings.java b/samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/SampleAdminMcpBindings.java new file mode 100644 index 0000000..2fd03d2 --- /dev/null +++ b/samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/SampleAdminMcpBindings.java @@ -0,0 +1,333 @@ +package io.dscope.camel.agent.samples; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.lang.reflect.Field; +import java.util.Properties; +import org.apache.camel.main.Main; + +final class SampleAdminMcpBindings { + + private SampleAdminMcpBindings() { + } + + static void bindIfMissing(Main main, String applicationYamlPath) { + if (main.lookup("mcpError", Object.class) != null) { + return; + } + try { + Object objectMapper = lookup(main, "objectMapper"); + ensureAuditProcessors(main, applicationYamlPath, (ObjectMapper) objectMapper); + Object auditTrailSearchProcessor = required(main, "auditTrailSearchProcessor"); + Object auditConversationListProcessor = required(main, "auditConversationListProcessor"); + Object auditConversationViewProcessor = required(main, "auditConversationViewProcessor"); + Object auditConversationSessionDataProcessor = required(main, "auditConversationSessionDataProcessor"); + Object auditConversationAgentMessageProcessor = required(main, "auditConversationAgentMessageProcessor"); + Object auditAgentBlueprintProcessor = required(main, "auditAgentBlueprintProcessor"); + Object auditAgentCatalogProcessor = required(main, "auditAgentCatalogProcessor"); + Object runtimeAuditGranularityProcessor = required(main, "runtimeAuditGranularityProcessor"); + Object runtimeResourceRefreshProcessor = required(main, "runtimeResourceRefreshProcessor"); + Object runtimeConversationPersistenceProcessor = required(main, "runtimeConversationPersistenceProcessor"); + Object runtimeConversationCloseProcessor = required(main, "runtimeConversationCloseProcessor"); + Object runtimePurgePreviewProcessor = required(main, "runtimePurgePreviewProcessor"); + + Object requestSizeGuardProcessor = newInstance("io.dscope.camel.mcp.processor.McpRequestSizeGuardProcessor"); + Object httpValidatorProcessor = newInstance("io.dscope.camel.mcp.processor.McpHttpValidatorProcessor"); + Object rateLimitProcessor = newInstance("io.dscope.camel.mcp.processor.McpRateLimitProcessor"); + Object jsonRpcEnvelopeProcessor = newInstance("io.dscope.camel.mcp.processor.McpJsonRpcEnvelopeProcessor"); + Object initializeProcessor = newInstance("io.dscope.camel.mcp.processor.McpInitializeProcessor"); + Object pingProcessor = newInstance("io.dscope.camel.mcp.processor.McpPingProcessor"); + Object notificationsInitializedProcessor = newInstance("io.dscope.camel.mcp.processor.McpNotificationsInitializedProcessor"); + Object notificationProcessor = newInstance("io.dscope.camel.mcp.processor.McpNotificationProcessor"); + Object notificationAckProcessor = newInstance("io.dscope.camel.mcp.processor.McpNotificationAckProcessor"); + Object errorProcessor = newInstance("io.dscope.camel.mcp.processor.McpErrorProcessor"); + Object streamProcessor = newInstance("io.dscope.camel.mcp.processor.McpStreamProcessor"); + Object healthStatusProcessor = newInstance( + "io.dscope.camel.mcp.processor.McpHealthStatusProcessor", + new Class[] { rateLimitProcessor.getClass().getInterfaces().length > 0 ? rateLimitProcessor.getClass().getInterfaces()[0] : rateLimitProcessor.getClass() }, + new Object[] { rateLimitProcessor } + ); + + Object uiSessionRegistry = newInstance("io.dscope.camel.mcp.service.McpUiSessionRegistry"); + uiSessionRegistry.getClass().getMethod("start").invoke(uiSessionRegistry); + Object uiInitializeProcessor = newInstance("io.dscope.camel.mcp.processor.McpUiInitializeProcessor", + new Class[] { uiSessionRegistry.getClass() }, + new Object[] { uiSessionRegistry }); + Object uiMessageProcessor = newInstance("io.dscope.camel.mcp.processor.McpUiMessageProcessor", + new Class[] { uiSessionRegistry.getClass() }, + new Object[] { uiSessionRegistry }); + Object uiUpdateModelContextProcessor = newInstance("io.dscope.camel.mcp.processor.McpUiUpdateModelContextProcessor", + new Class[] { uiSessionRegistry.getClass() }, + new Object[] { uiSessionRegistry }); + Object uiToolsCallProcessor = newInstance("io.dscope.camel.mcp.processor.McpUiToolsCallProcessor", + new Class[] { uiSessionRegistry.getClass() }, + new Object[] { uiSessionRegistry }); + Object uiToolsCallPostProcessor = newInstance("io.dscope.camel.mcp.processor.McpUiToolsCallPostProcessor", + new Class[] { uiSessionRegistry.getClass() }, + new Object[] { uiSessionRegistry }); + + Object toolsListProcessor = newInstance("io.dscope.camel.agent.audit.mcp.AuditMcpToolsListProcessor"); + Object toolsCallProcessor = newInstance( + "io.dscope.camel.agent.audit.mcp.AuditMcpToolsCallProcessor", + new Class[] { + ObjectMapper.class, + auditTrailSearchProcessor.getClass(), + auditConversationListProcessor.getClass(), + auditConversationViewProcessor.getClass(), + auditConversationSessionDataProcessor.getClass(), + auditConversationAgentMessageProcessor.getClass(), + auditAgentBlueprintProcessor.getClass(), + auditAgentCatalogProcessor.getClass(), + runtimeAuditGranularityProcessor.getClass(), + runtimeResourceRefreshProcessor.getClass(), + runtimeConversationPersistenceProcessor.getClass(), + runtimeConversationCloseProcessor.getClass(), + runtimePurgePreviewProcessor.getClass() + }, + new Object[] { + objectMapper, + auditTrailSearchProcessor, + auditConversationListProcessor, + auditConversationViewProcessor, + auditConversationSessionDataProcessor, + auditConversationAgentMessageProcessor, + auditAgentBlueprintProcessor, + auditAgentCatalogProcessor, + runtimeAuditGranularityProcessor, + runtimeResourceRefreshProcessor, + runtimeConversationPersistenceProcessor, + runtimeConversationCloseProcessor, + runtimePurgePreviewProcessor + } + ); + Object resourcesListProcessor = newInstance("io.dscope.camel.mcp.processor.McpResourcesListProcessor"); + Object resourcesReadProcessor = newInstance("io.dscope.camel.mcp.processor.McpResourcesReadProcessor"); + + main.bind("mcpRequestSizeGuard", requestSizeGuardProcessor); + main.bind("mcpHttpValidator", httpValidatorProcessor); + main.bind("mcpRateLimit", rateLimitProcessor); + main.bind("mcpJsonRpcEnvelope", jsonRpcEnvelopeProcessor); + main.bind("mcpInitialize", initializeProcessor); + main.bind("mcpPing", pingProcessor); + main.bind("mcpNotificationsInitialized", notificationsInitializedProcessor); + main.bind("mcpNotification", notificationProcessor); + main.bind("mcpNotificationAck", notificationAckProcessor); + main.bind("mcpError", errorProcessor); + main.bind("mcpStream", streamProcessor); + main.bind("mcpHealthStatus", healthStatusProcessor); + main.bind("mcpUiSessionRegistry", uiSessionRegistry); + main.bind("mcpUiInitialize", uiInitializeProcessor); + main.bind("mcpUiMessage", uiMessageProcessor); + main.bind("mcpUiUpdateModelContext", uiUpdateModelContextProcessor); + main.bind("mcpUiToolsCall", uiToolsCallProcessor); + main.bind("mcpUiToolsCallPost", uiToolsCallPostProcessor); + main.bind("mcpToolsList", toolsListProcessor); + main.bind("mcpToolsCall", toolsCallProcessor); + main.bind("mcpResourcesList", resourcesListProcessor); + main.bind("mcpResourcesRead", resourcesReadProcessor); + } catch (Exception e) { + throw new IllegalStateException("Failed to bind sample MCP admin fallback processors", e); + } + } + + private static void ensureAuditProcessors(Main main, String applicationYamlPath, ObjectMapper objectMapper) throws Exception { + Object persistenceFacade = required(main, "persistenceFacade"); + Object planSelectionResolver = lookupFromRegistry(main, "agentPlanSelectionResolver"); + if (planSelectionResolver == null) { + planSelectionResolver = newInstance( + "io.dscope.camel.agent.runtime.AgentPlanSelectionResolver", + new Class[] { Object.class, ObjectMapper.class }, + new Object[] { persistenceFacade, objectMapper } + ); + main.bind("agentPlanSelectionResolver", planSelectionResolver); + } + + Object conversationArchiveService = lookupFromRegistry(main, "conversationArchiveService"); + if (conversationArchiveService == null) { + conversationArchiveService = newInstance( + "io.dscope.camel.agent.runtime.ConversationArchiveService", + new Class[] { Object.class, ObjectMapper.class, boolean.class }, + new Object[] { persistenceFacade, objectMapper, true } + ); + main.bind("conversationArchiveService", conversationArchiveService); + } + + Object runtimeControlState = lookupFromRegistry(main, "runtimeControlState"); + if (runtimeControlState == null) { + runtimeControlState = newInstance( + "io.dscope.camel.agent.runtime.RuntimeControlState", + new Class[] { Object.class }, + new Object[] { loadGranularity(applicationYamlPath) } + ); + main.bind("runtimeControlState", runtimeControlState); + } + + Properties properties = loadProperties(applicationYamlPath); + String plansConfig = trimToNull(properties.getProperty("agent.agents-config")); + String blueprintUri = trimToNull(properties.getProperty("agent.blueprint")); + + bindIfMissing(main, "auditTrailSearchProcessor", + newInstance( + "io.dscope.camel.agent.audit.AuditTrailSearchProcessor", + new Class[] { Object.class, ObjectMapper.class, Object.class, String.class, String.class }, + new Object[] { persistenceFacade, objectMapper, planSelectionResolver, plansConfig, blueprintUri } + )); + bindIfMissing(main, "auditConversationListProcessor", + newInstance( + "io.dscope.camel.agent.audit.AuditConversationListProcessor", + new Class[] { Object.class, ObjectMapper.class, Object.class, String.class, String.class }, + new Object[] { persistenceFacade, objectMapper, planSelectionResolver, plansConfig, blueprintUri } + )); + bindIfMissing(main, "auditConversationViewProcessor", + newInstance( + "io.dscope.camel.agent.audit.AuditConversationViewProcessor", + new Class[] { Object.class, ObjectMapper.class, Object.class, String.class, String.class }, + new Object[] { persistenceFacade, objectMapper, planSelectionResolver, plansConfig, blueprintUri } + )); + bindIfMissing(main, "auditConversationSessionDataProcessor", + newInstance( + "io.dscope.camel.agent.audit.AuditConversationSessionDataProcessor", + new Class[] { Object.class, ObjectMapper.class }, + new Object[] { conversationArchiveService, objectMapper } + )); + bindIfMissing(main, "auditConversationAgentMessageProcessor", + newInstance( + "io.dscope.camel.agent.audit.AuditConversationAgentMessageProcessor", + new Class[] { Object.class, ObjectMapper.class }, + new Object[] { persistenceFacade, objectMapper } + )); + bindIfMissing(main, "runtimeAuditGranularityProcessor", + newInstance( + "io.dscope.camel.agent.runtime.RuntimeAuditGranularityProcessor", + new Class[] { ObjectMapper.class, Object.class }, + new Object[] { objectMapper, runtimeControlState } + )); + bindIfMissing(main, "runtimeResourceRefreshProcessor", + newInstance( + "io.dscope.camel.agent.runtime.RuntimeResourceRefreshProcessor", + new Class[] { String.class, Object.class, ObjectMapper.class }, + new Object[] { applicationYamlPath, persistenceFacade, objectMapper } + )); + bindIfMissing(main, "runtimeConversationPersistenceProcessor", + newInstance( + "io.dscope.camel.agent.runtime.RuntimeConversationPersistenceProcessor", + new Class[] { ObjectMapper.class, Object.class }, + new Object[] { objectMapper, conversationArchiveService } + )); + bindIfMissing(main, "runtimeConversationCloseProcessor", + newInstance( + "io.dscope.camel.agent.runtime.RuntimeConversationCloseProcessor", + new Class[] { ObjectMapper.class, Object.class }, + new Object[] { objectMapper, persistenceFacade } + )); + bindIfMissing(main, "runtimePurgePreviewProcessor", + newInstance( + "io.dscope.camel.agent.runtime.RuntimePurgePreviewProcessor", + new Class[] { ObjectMapper.class, Object.class }, + new Object[] { objectMapper, persistenceFacade } + )); + bindIfMissing(main, "auditAgentBlueprintProcessor", + newInstance( + "io.dscope.camel.agent.audit.AuditAgentBlueprintProcessor", + new Class[] { Object.class, ObjectMapper.class, Object.class, String.class, String.class }, + new Object[] { persistenceFacade, objectMapper, planSelectionResolver, plansConfig, blueprintUri } + )); + bindIfMissing(main, "auditAgentCatalogProcessor", + newInstance( + "io.dscope.camel.agent.audit.AuditAgentCatalogProcessor", + new Class[] { ObjectMapper.class, Object.class, String.class, String.class }, + new Object[] { objectMapper, planSelectionResolver, plansConfig, blueprintUri } + )); + } + + private static void bindIfMissing(Main main, String name, Object value) { + if (lookupFromRegistry(main, name) == null && main.lookup(name, Object.class) == null) { + main.bind(name, value); + } + } + + private static Object loadGranularity(String applicationYamlPath) { + try { + Properties properties = loadProperties(applicationYamlPath); + Class enumType = Class.forName("io.dscope.camel.agent.model.AuditGranularity"); + return enumType.getMethod("from", String.class) + .invoke(null, properties.getProperty("agent.audit.granularity", "debug")); + } catch (Exception ignored) { + try { + Class enumType = Class.forName("io.dscope.camel.agent.model.AuditGranularity"); + return Enum.valueOf((Class) enumType.asSubclass(Enum.class), "DEBUG"); + } catch (Exception nested) { + return null; + } + } + } + + private static Properties loadProperties(String applicationYamlPath) { + try { + Class loader = Class.forName("io.dscope.camel.agent.runtime.ApplicationYamlLoader"); + return (Properties) loader.getMethod("loadFromClasspath", String.class).invoke(null, applicationYamlPath); + } catch (Exception ignored) { + return new Properties(); + } + } + + private static String trimToNull(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + + private static Object lookup(Main main, String name) { + Object value = lookupFromRegistry(main, name); + if (value == null) { + value = main.lookup(name, Object.class); + } + return value != null ? value : new ObjectMapper(); + } + + private static Object required(Main main, String name) { + Object value = lookupFromRegistry(main, name); + if (value == null) { + value = main.lookup(name, Object.class); + } + if (value == null) { + throw new IllegalStateException("Required bean missing after runtime bootstrap: " + name); + } + return value; + } + + private static Object lookupFromRegistry(Main main, String name) { + try { + Field registryField = Main.class.getDeclaredField("registry"); + registryField.setAccessible(true); + Object registry = registryField.get(main); + if (registry == null) { + return null; + } + return registry.getClass().getMethod("lookupByName", String.class).invoke(registry, name); + } catch (Exception ignored) { + return null; + } + } + + private static Object newInstance(String className) throws Exception { + return Class.forName(className).getDeclaredConstructor().newInstance(); + } + + private static Object newInstance(String className, Class[] parameterTypes, Object[] args) throws Exception { + Class type = Class.forName(className); + try { + return type.getDeclaredConstructor(parameterTypes).newInstance(args); + } catch (NoSuchMethodException ignored) { + for (var constructor : type.getDeclaredConstructors()) { + if (constructor.getParameterCount() != args.length) { + continue; + } + constructor.setAccessible(true); + return constructor.newInstance(args); + } + throw ignored; + } + } +} diff --git a/samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/SupportTicketLifecycleProcessor.java b/samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/SupportTicketLifecycleProcessor.java new file mode 100644 index 0000000..b91fc1c --- /dev/null +++ b/samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/SupportTicketLifecycleProcessor.java @@ -0,0 +1,147 @@ +package io.dscope.camel.agent.samples; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.dscope.camel.agent.config.AgentHeaders; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.zip.CRC32; +import org.apache.camel.Exchange; +import org.apache.camel.Message; +import org.apache.camel.Processor; + +final class SupportTicketLifecycleProcessor implements Processor { + + private final ObjectMapper objectMapper; + private final ConcurrentMap ticketsByConversationId = new ConcurrentHashMap<>(); + + SupportTicketLifecycleProcessor(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public void process(Exchange exchange) throws Exception { + Message in = exchange.getIn(); + String conversationId = readConversationId(in); + String query = normalizeQuery(in.getBody()); + + TicketAction action = determineAction(query); + TicketRecord current = ticketsByConversationId.compute(conversationId, (key, existing) -> applyAction(key, existing, action, query)); + + ObjectNode payload = objectMapper.createObjectNode(); + payload.put("ticketId", current.ticketId()); + payload.put("status", current.status()); + payload.put("action", action.name()); + payload.put("summary", current.summary()); + payload.put("assignedQueue", current.assignedQueue()); + payload.put("message", messageFor(action)); + payload.put("notifyClient", action == TicketAction.UPDATE || action == TicketAction.CLOSE || action == TicketAction.STATUS); + payload.put("conversationId", conversationId); + payload.put("updatedAt", Instant.now().toString()); + + exchange.getMessage().setHeader(Exchange.CONTENT_TYPE, "application/json"); + exchange.getMessage().setBody(objectMapper.writeValueAsString(payload)); + } + + private TicketRecord applyAction(String conversationId, TicketRecord existing, TicketAction action, String query) { + TicketRecord current = existing != null ? existing : createRecord(conversationId, query); + return switch (action) { + case OPEN -> createRecord(conversationId, query); + case UPDATE -> new TicketRecord(current.ticketId(), "IN_PROGRESS", firstNonBlank(query, current.summary()), current.assignedQueue()); + case CLOSE -> new TicketRecord(current.ticketId(), "CLOSED", firstNonBlank(query, current.summary()), current.assignedQueue()); + case STATUS -> current; + }; + } + + private TicketRecord createRecord(String conversationId, String query) { + return new TicketRecord(ticketIdFor(conversationId), "OPEN", firstNonBlank(query, "Customer requested support follow-up"), queueFor(query)); + } + + private String ticketIdFor(String conversationId) { + CRC32 crc = new CRC32(); + crc.update(conversationId.getBytes(StandardCharsets.UTF_8)); + return "TCK-" + Long.toUnsignedString(crc.getValue(), 36).toUpperCase(Locale.ROOT); + } + + private String queueFor(String query) { + String normalized = query.toLowerCase(Locale.ROOT); + if (normalized.contains("bill") || normalized.contains("refund") || normalized.contains("invoice") || normalized.contains("payment")) { + return "BILLING-L2"; + } + if (normalized.contains("security") || normalized.contains("fraud")) { + return "SECURITY-L2"; + } + return "L1-SUPPORT"; + } + + private TicketAction determineAction(String query) { + String normalized = query.toLowerCase(Locale.ROOT); + if (containsAny(normalized, "close", "closed", "resolve", "resolved", "done with", "cancel ticket")) { + return TicketAction.CLOSE; + } + if (containsAny(normalized, "status", "progress", "check ticket", "where is", "what is happening")) { + return TicketAction.STATUS; + } + if (containsAny(normalized, "update", "add note", "change", "modify", "priority", "reopen")) { + return TicketAction.UPDATE; + } + return TicketAction.OPEN; + } + + private boolean containsAny(String text, String... markers) { + for (String marker : markers) { + if (text.contains(marker)) { + return true; + } + } + return false; + } + + private String messageFor(TicketAction action) { + return switch (action) { + case OPEN -> "Support ticket created successfully"; + case UPDATE -> "Support ticket updated and routed back to the client conversation"; + case CLOSE -> "Support ticket closed and the client conversation has been updated"; + case STATUS -> "Support ticket status retrieved successfully"; + }; + } + + private String normalizeQuery(Object body) { + if (body instanceof Map map) { + Object query = map.get("query"); + return query == null ? "" : String.valueOf(query).trim(); + } + return body == null ? "" : String.valueOf(body).trim(); + } + + private String readConversationId(Message in) { + String conversationId = in.getHeader(AgentHeaders.CONVERSATION_ID, String.class); + if (conversationId == null || conversationId.isBlank()) { + return "sample-ticket-fallback"; + } + return conversationId.trim(); + } + + private String firstNonBlank(String... values) { + for (String value : values) { + if (value != null && !value.isBlank()) { + return value.trim(); + } + } + return ""; + } + + private enum TicketAction { + OPEN, + UPDATE, + CLOSE, + STATUS + } + + private record TicketRecord(String ticketId, String status, String summary, String assignedQueue) { + } +} diff --git a/samples/agent-support-service/src/main/resources/agents/a2a-exposed-agents.yaml b/samples/agent-support-service/src/main/resources/agents/a2a-exposed-agents.yaml new file mode 100644 index 0000000..5d6f157 --- /dev/null +++ b/samples/agent-support-service/src/main/resources/agents/a2a-exposed-agents.yaml @@ -0,0 +1,12 @@ +agents: + - agentId: support-ticket-service + name: Support Ticket Service + description: A2A ticket lifecycle service used by the support and billing assistants + version: 1.0.0 + defaultAgent: true + planName: ticketing + planVersion: v1 + skills: + - ticketing + - support + - escalation diff --git a/samples/agent-support-service/src/main/resources/agents/agents.yaml b/samples/agent-support-service/src/main/resources/agents/agents.yaml index 3ca23be..5145964 100644 --- a/samples/agent-support-service/src/main/resources/agents/agents.yaml +++ b/samples/agent-support-service/src/main/resources/agents/agents.yaml @@ -14,3 +14,8 @@ plans: blueprint: classpath:agents/billing/v1/agent.md - version: v2 blueprint: classpath:agents/billing/v2/agent.md + - name: ticketing + versions: + - version: v1 + default: true + blueprint: classpath:agents/ticketing/v1/agent.md diff --git a/samples/agent-support-service/src/main/resources/agents/billing/v1/agent.md b/samples/agent-support-service/src/main/resources/agents/billing/v1/agent.md index 75421ce..89da0d6 100644 --- a/samples/agent-support-service/src/main/resources/agents/billing/v1/agent.md +++ b/samples/agent-support-service/src/main/resources/agents/billing/v1/agent.md @@ -9,8 +9,8 @@ You are a billing support assistant. Prioritize invoice, payment, subscription, Routing rules: 1. Use `kb.search` for billing policy, invoice explanation, and account/billing FAQ lookup. -2. Use `support.ticket.open` when the user needs a billing case opened, escalated, or followed up by human support. -3. If the request is ambiguous, ask one direct billing-focused clarification before opening a ticket. +2. Use `support.ticket.manage` when the user needs a billing case opened, updated, closed, escalated, or followed up by human support. +3. If the request is ambiguous, ask one direct billing-focused clarification before changing the ticket. 4. Keep responses concise and operational. ## Tools @@ -26,9 +26,9 @@ tools: properties: query: type: string - - name: support.ticket.open - description: Open a support ticket from a user issue and return ticket details - routeId: support-ticket-open + - name: support.ticket.manage + description: Manage a billing support ticket through the sample A2A ticket service + endpointUri: a2a:support-ticket-service?remoteUrl={{agent.runtime.a2a.public-base-url}}/a2a inputSchemaInline: type: object required: [query] @@ -58,7 +58,8 @@ aguiPreRun: fallbackEnabled: true fallback: kbToolName: kb.search - ticketToolName: support.ticket.open - ticketKeywords: [invoice, billing, refund, payment, charge, subscription] + ticketToolName: support.ticket.manage + ticketUri: direct:support-ticket-manage + ticketKeywords: [invoice, billing, refund, payment, charge, subscription, update, close, status] errorMarkers: [api key is missing, openai api key, set -dopenai.api.key] ``` diff --git a/samples/agent-support-service/src/main/resources/agents/billing/v2/agent.md b/samples/agent-support-service/src/main/resources/agents/billing/v2/agent.md index 2ffa6ac..c0339ee 100644 --- a/samples/agent-support-service/src/main/resources/agents/billing/v2/agent.md +++ b/samples/agent-support-service/src/main/resources/agents/billing/v2/agent.md @@ -9,8 +9,8 @@ You are a senior billing assistant. Focus on resolving invoice disputes, refunds Routing rules: 1. Use `kb.search` first for known billing and subscription guidance. -2. Use `support.ticket.open` when the user explicitly asks for follow-up, escalation, or case creation. -3. Summarize the billing issue clearly before creating a ticket. +2. Use `support.ticket.manage` when the user explicitly asks for follow-up, escalation, case creation, a status update, or closure. +3. Summarize the billing issue clearly before changing the ticket. 4. Prefer answer-first responses, then next actions. ## Tools @@ -26,9 +26,9 @@ tools: properties: query: type: string - - name: support.ticket.open - description: Open a support ticket from a user issue and return ticket details - routeId: support-ticket-open + - name: support.ticket.manage + description: Manage a billing support ticket through the sample A2A ticket service + endpointUri: a2a:support-ticket-service?remoteUrl={{agent.runtime.a2a.public-base-url}}/a2a inputSchemaInline: type: object required: [query] @@ -58,7 +58,8 @@ aguiPreRun: fallbackEnabled: true fallback: kbToolName: kb.search - ticketToolName: support.ticket.open - ticketKeywords: [invoice, billing, refund, payment, charge, subscription] + ticketToolName: support.ticket.manage + ticketUri: direct:support-ticket-manage + ticketKeywords: [invoice, billing, refund, payment, charge, subscription, update, close, status] errorMarkers: [api key is missing, openai api key, set -dopenai.api.key] ``` diff --git a/samples/agent-support-service/src/main/resources/agents/support/agent.md b/samples/agent-support-service/src/main/resources/agents/support/agent.md index 78f446b..946d844 100644 --- a/samples/agent-support-service/src/main/resources/agents/support/agent.md +++ b/samples/agent-support-service/src/main/resources/agents/support/agent.md @@ -9,7 +9,7 @@ You are a support assistant that can search local knowledge routes. Routing rules: 1. Use discovered CRM MCP tools from `support.mcp` to look up customer profile/context when the user provides phone number or email. -2. Use `support.ticket.open` when the user asks to open, create, submit, or escalate a support ticket. +2. Use `support.ticket.manage` when the user asks to open, create, update, close, check, submit, or escalate a support ticket. 3. Use `kb.search` for general plain-language support lookups, troubleshooting guidance, and informational questions. 4. Use `support.echo` only when the user explicitly asks for an echo/diagnostic transform. 5. If unclear, return response from LLM call. @@ -32,9 +32,9 @@ tools: properties: query: type: string - - name: support.ticket.open - description: Open a support ticket from a user issue and return ticket details - routeId: support-ticket-open + - name: support.ticket.manage + description: Manage a support ticket over the sample A2A ticket service and return ticket details + endpointUri: a2a:support-ticket-service?remoteUrl={{agent.runtime.a2a.public-base-url}}/a2a inputSchemaInline: type: object required: [query] @@ -112,7 +112,8 @@ aguiPreRun: fallbackEnabled: true fallback: kbToolName: kb.search - ticketToolName: support.ticket.open - ticketKeywords: [ticket, open, create, submit, escalate] + ticketToolName: support.ticket.manage + ticketUri: direct:support-ticket-manage + ticketKeywords: [ticket, open, create, update, close, status, submit, escalate] errorMarkers: [api key is missing, openai api key, set -dopenai.api.key] ``` diff --git a/samples/agent-support-service/src/main/resources/agents/support/v1/agent.md b/samples/agent-support-service/src/main/resources/agents/support/v1/agent.md index 78f446b..946d844 100644 --- a/samples/agent-support-service/src/main/resources/agents/support/v1/agent.md +++ b/samples/agent-support-service/src/main/resources/agents/support/v1/agent.md @@ -9,7 +9,7 @@ You are a support assistant that can search local knowledge routes. Routing rules: 1. Use discovered CRM MCP tools from `support.mcp` to look up customer profile/context when the user provides phone number or email. -2. Use `support.ticket.open` when the user asks to open, create, submit, or escalate a support ticket. +2. Use `support.ticket.manage` when the user asks to open, create, update, close, check, submit, or escalate a support ticket. 3. Use `kb.search` for general plain-language support lookups, troubleshooting guidance, and informational questions. 4. Use `support.echo` only when the user explicitly asks for an echo/diagnostic transform. 5. If unclear, return response from LLM call. @@ -32,9 +32,9 @@ tools: properties: query: type: string - - name: support.ticket.open - description: Open a support ticket from a user issue and return ticket details - routeId: support-ticket-open + - name: support.ticket.manage + description: Manage a support ticket over the sample A2A ticket service and return ticket details + endpointUri: a2a:support-ticket-service?remoteUrl={{agent.runtime.a2a.public-base-url}}/a2a inputSchemaInline: type: object required: [query] @@ -112,7 +112,8 @@ aguiPreRun: fallbackEnabled: true fallback: kbToolName: kb.search - ticketToolName: support.ticket.open - ticketKeywords: [ticket, open, create, submit, escalate] + ticketToolName: support.ticket.manage + ticketUri: direct:support-ticket-manage + ticketKeywords: [ticket, open, create, update, close, status, submit, escalate] errorMarkers: [api key is missing, openai api key, set -dopenai.api.key] ``` diff --git a/samples/agent-support-service/src/main/resources/agents/support/v2/agent.md b/samples/agent-support-service/src/main/resources/agents/support/v2/agent.md index d206737..f64e43d 100644 --- a/samples/agent-support-service/src/main/resources/agents/support/v2/agent.md +++ b/samples/agent-support-service/src/main/resources/agents/support/v2/agent.md @@ -9,7 +9,7 @@ You are a support assistant that can search local knowledge routes and prefer co Routing rules: 1. Use discovered CRM MCP tools from `support.mcp` to look up customer profile/context when the user provides phone number or email. -2. Use `support.ticket.open` when the user asks to open, create, submit, or escalate a support ticket. +2. Use `support.ticket.manage` when the user asks to open, create, update, close, check, submit, or escalate a support ticket. 3. Use `kb.search` for general plain-language support lookups, troubleshooting guidance, and informational questions. 4. Use `support.echo` only when the user explicitly asks for an echo/diagnostic transform. 5. If unclear, return response from LLM call. @@ -27,9 +27,9 @@ tools: properties: query: type: string - - name: support.ticket.open - description: Open a support ticket from a user issue and return ticket details - routeId: support-ticket-open + - name: support.ticket.manage + description: Manage a support ticket over the sample A2A ticket service and return ticket details + endpointUri: a2a:support-ticket-service?remoteUrl={{agent.runtime.a2a.public-base-url}}/a2a inputSchemaInline: type: object required: [query] @@ -65,7 +65,8 @@ aguiPreRun: fallbackEnabled: true fallback: kbToolName: kb.search - ticketToolName: support.ticket.open - ticketKeywords: [ticket, open, create, submit, escalate] + ticketToolName: support.ticket.manage + ticketUri: direct:support-ticket-manage + ticketKeywords: [ticket, open, create, update, close, status, submit, escalate] errorMarkers: [api key is missing, openai api key, set -dopenai.api.key] ``` diff --git a/samples/agent-support-service/src/main/resources/agents/ticketing/v1/agent.md b/samples/agent-support-service/src/main/resources/agents/ticketing/v1/agent.md new file mode 100644 index 0000000..ed012f6 --- /dev/null +++ b/samples/agent-support-service/src/main/resources/agents/ticketing/v1/agent.md @@ -0,0 +1,29 @@ +# Agent: TicketingService + +Version: 1.0.0 + +## System + +You are the support ticket lifecycle service exposed over A2A. + +Routing rules: + +1. Use `support.ticket.manage.route` for every request to open, update, close, or check a ticket. +2. Keep the customer request text intact when you pass it to the route tool. +3. Return the tool result exactly as JSON and do not wrap it in markdown. +4. If the request is unclear, still call the tool so the service can keep the ticket state in sync. + +## Tools + +```yaml +tools: + - name: support.ticket.manage.route + description: Stateful sample ticket lifecycle route for open, update, close, and status operations + routeId: support-ticket-manage + inputSchemaInline: + type: object + required: [query] + properties: + query: + type: string +``` diff --git a/samples/agent-support-service/src/main/resources/application.yaml b/samples/agent-support-service/src/main/resources/application.yaml index a7bbb0c..b36f173 100644 --- a/samples/agent-support-service/src/main/resources/application.yaml +++ b/samples/agent-support-service/src/main/resources/application.yaml @@ -19,14 +19,19 @@ openai: key: ${OPENAI_API_KEY} agent: + agents-config: classpath:agents/agents.yaml runtime: - routes-include-pattern: classpath:routes/kb-search.camel.yaml,classpath:routes/kb-search-json.camel.xml,classpath:routes/ag-ui-platform.camel.yaml,classpath:routes/sip-adapter-platform.camel.yaml + routes-include-pattern: classpath:routes/kb-search.camel.yaml,classpath:routes/kb-search-json.camel.xml,classpath:routes/ticket-service.camel.yaml,classpath:routes/ag-ui-platform.camel.yaml,classpath:routes/admin-platform.camel.yaml,classpath:routes/sip-adapter-platform.camel.yaml agent-routes-enabled: true + a2a: + enabled: true + public-base-url: http://localhost:{{agui.rpc.port:8080}} + exposed-agents-config: classpath:agents/a2a-exposed-agents.yaml ai: mode: spring-ai realtime: processor-bean-name: supportRealtimeEventProcessorCore - agent-endpoint-uri: agent:support?blueprint={{agent.blueprint}} + agent-endpoint-uri: agent:support?plansConfig={{agent.agents-config}}&blueprint={{agent.blueprint}} agent-profile-purpose-max-chars: 0 # Server-side TTL for browser WebRTC init context (conversationId -> session template) browser-session-ttl-ms: 600000 @@ -48,12 +53,13 @@ agent: max-backoff-ms: 2000 agui: pre-run: - agent-endpoint-uri: agent:support?blueprint={{agent.blueprint}} + agent-endpoint-uri: agent:support?plansConfig={{agent.agents-config}}&blueprint={{agent.blueprint}} fallback-enabled: true fallback: kb-tool-name: kb.search - ticket-tool-name: support.ticket.open - ticket-keywords: ticket,open,create,submit,escalate + ticket-tool-name: support.ticket.manage + ticket-uri: direct:support-ticket-manage + ticket-keywords: ticket,open,create,update,close,status,submit,escalate error-markers: api key is missing,openai api key,set -dopenai.api.key spring-ai: provider: openai diff --git a/samples/agent-support-service/src/main/resources/frontend/audit.html b/samples/agent-support-service/src/main/resources/frontend/audit.html index f92d3be..862affb 100644 --- a/samples/agent-support-service/src/main/resources/frontend/audit.html +++ b/samples/agent-support-service/src/main/resources/frontend/audit.html @@ -118,7 +118,7 @@
- +
diff --git a/samples/agent-support-service/src/main/resources/frontend/index.html b/samples/agent-support-service/src/main/resources/frontend/index.html index 8bbceee..1f4e037 100644 --- a/samples/agent-support-service/src/main/resources/frontend/index.html +++ b/samples/agent-support-service/src/main/resources/frontend/index.html @@ -157,6 +157,7 @@ let selectedPlanVersion = (runtimeUrl.searchParams.get('planVersion') || '').trim(); let agentCatalog = null; let sessionId = `session-${Date.now()}`; + let ticketUpdatePollTimer = null; let audioContext = null; let mediaStream = null; let sourceNode = null; @@ -168,6 +169,7 @@ let realtimePollInFlight = false; const handledTranscriptKeys = new Set(); const displayedVoiceMessages = new Set(); + const displayedTicketUpdateKeys = new Set(); let lastRelayError = ''; let lastPollError = ''; let lastPollErrorAt = 0; @@ -810,13 +812,17 @@ function renderTicketWidget(data) { const ticketId = normalizeDisplayText(String(data.ticketId ?? 'N/A')); const status = normalizeDisplayText(String(data.status ?? 'OPEN')); + const action = normalizeDisplayText(String(data.action ?? '')); const assignedQueue = normalizeDisplayText(String(data.assignedQueue ?? 'L1-SUPPORT')); const summary = normalizeDisplayText(String(data.summary ?? '')); const message = normalizeDisplayText(String(data.message ?? '')); const card = document.createElement('div'); card.className = 'ticket'; + const title = action + ? `Ticket ${action.charAt(0).toUpperCase()}${action.slice(1).toLowerCase()}` + : 'Ticket Update'; card.innerHTML = ` -

Ticket Created

+

${title}

ID: ${ticketId}
Status: ${status}
Queue: ${assignedQueue}
@@ -826,6 +832,63 @@

Ticket Created

return card; } + function clearTicketUpdatePolling() { + if (ticketUpdatePollTimer) { + clearInterval(ticketUpdatePollTimer); + ticketUpdatePollTimer = null; + } + } + + function ticketUpdateKey(event) { + return [ + event?.timestamp || '', + event?.taskId || '', + event?.source || '', + event?.text || '' + ].join('|'); + } + + async function pollTicketUpdates() { + if (!sessionId) { + return; + } + try { + const params = new URLSearchParams({ + conversationId: sessionId, + sessionId, + limit: '120' + }); + const response = await fetch(`/audit/conversation/session-data?${params.toString()}`); + if (!response.ok) { + return; + } + const payload = await response.json(); + const events = Array.isArray(payload?.events) ? payload.events : []; + for (const event of events) { + if (String(event?.source || '') !== 'a2a.ticketing') { + continue; + } + const key = ticketUpdateKey(event); + if (displayedTicketUpdateKeys.has(key)) { + continue; + } + displayedTicketUpdateKeys.add(key); + const widget = normalizeWidgetCandidate(event?.payload?.widget) || normalizeWidgetCandidate(event?.payload); + tryAddVoiceMessage('agent', String(event?.text || 'Ticket service update'), 'a2a.ticketing', widget, key); + } + } catch (_) { + } + } + + function startTicketUpdatePolling() { + clearTicketUpdatePolling(); + displayedTicketUpdateKeys.clear(); + ticketUpdatePollTimer = setInterval(() => { + pollTicketUpdates(); + }, 2000); + pollTicketUpdates(); + } + function parseSseEvents(payload) { const blocks = payload.split(/\n\n+/).map((chunk) => chunk.trim()).filter(Boolean); const events = []; @@ -1838,6 +1901,7 @@

Ticket Created

const previousSessionId = sessionId; sessionId = `session-${Date.now()}`; + startTicketUpdatePolling(); realtimeEventQueue = Promise.resolve(); setVoiceStatus('WebRTC token failed (400), reinitializing session...'); addMessage('agent', `WebRTC token 400 for ${previousSessionId}; retrying with fresh session ${sessionId}.`); @@ -2939,6 +3003,7 @@

Ticket Created

.then(() => initializeRealtimeSessionContext(buildVoiceSessionPatch())) .catch(() => { }); + startTicketUpdatePolling(); syncInstructionDebugVisibility(); openInstructionDebugForWebRtc(); diff --git a/samples/agent-support-service/src/main/resources/routes/admin-platform.camel.yaml b/samples/agent-support-service/src/main/resources/routes/admin-platform.camel.yaml index c684349..b23e38f 100644 --- a/samples/agent-support-service/src/main/resources/routes/admin-platform.camel.yaml +++ b/samples/agent-support-service/src/main/resources/routes/admin-platform.camel.yaml @@ -283,6 +283,18 @@ id: processSampleAuditConversationAgentMessage ref: auditConversationAgentMessageProcessor +- route: + id: sample-audit-conversation-session-data-api + from: + id: fromSampleAuditConversationSessionData + uri: undertow + parameters: + httpURI: http://0.0.0.0:{{agui.rpc.port:8080}}/audit/conversation/session-data?httpMethodRestrict=GET + steps: + - process: + id: processSampleAuditConversationSessionData + ref: auditConversationSessionDataProcessor + - route: id: sample-audit-agent-catalog-api from: diff --git a/samples/agent-support-service/src/main/resources/routes/kb-search-json.camel.xml b/samples/agent-support-service/src/main/resources/routes/kb-search-json.camel.xml index 2441a8a..94aa852 100644 --- a/samples/agent-support-service/src/main/resources/routes/kb-search-json.camel.xml +++ b/samples/agent-support-service/src/main/resources/routes/kb-search-json.camel.xml @@ -2,8 +2,6 @@ - - {"ticketId":"TCK-${exchangeId}","status":"OPEN","summary":"${body[query]}","assignedQueue":"L1-SUPPORT","message":"Support ticket created successfully"} - + diff --git a/samples/agent-support-service/src/main/resources/routes/ticket-service.camel.yaml b/samples/agent-support-service/src/main/resources/routes/ticket-service.camel.yaml new file mode 100644 index 0000000..978fbec --- /dev/null +++ b/samples/agent-support-service/src/main/resources/routes/ticket-service.camel.yaml @@ -0,0 +1,9 @@ +- route: + id: support-ticket-manage-route + from: + id: fromSupportTicketManage + uri: direct:support-ticket-manage + steps: + - process: + id: processSupportTicketManage + ref: ticketLifecycleProcessor diff --git a/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/AgUiPlaywrightAuditTrailIntegrationTest.java b/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/AgUiPlaywrightAuditTrailIntegrationTest.java index dbeb73d..6278afd 100644 --- a/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/AgUiPlaywrightAuditTrailIntegrationTest.java +++ b/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/AgUiPlaywrightAuditTrailIntegrationTest.java @@ -1,10 +1,6 @@ package io.dscope.camel.agent.samples; import com.fasterxml.jackson.databind.ObjectMapper; -import com.microsoft.playwright.Browser; -import com.microsoft.playwright.BrowserType; -import com.microsoft.playwright.Page; -import com.microsoft.playwright.Playwright; import io.dscope.camel.agent.model.AiToolCall; import io.dscope.camel.agent.runtime.AgentRuntimeBootstrap; import io.dscope.camel.agent.springai.SpringAiChatGateway; @@ -18,6 +14,7 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.List; +import java.util.Map; import org.apache.camel.main.Main; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -34,50 +31,43 @@ void shouldMatchCopilotPageInputOutputWithAuditTrail() throws Exception { String queryToken = "pw-audit-" + System.currentTimeMillis(); String prompt = "Please open a support ticket for repeated login failures " + queryToken; main.bind("springAiChatGateway", new DeterministicTicketGateway()); + main.bind("ticketLifecycleProcessor", new SupportTicketLifecycleProcessor(new ObjectMapper())); try { AgentRuntimeBootstrap.bootstrap(main, "ag-ui-playwright-audit-test.yaml"); + SampleAdminMcpBindings.bindIfMissing(main, "ag-ui-playwright-audit-test.yaml"); main.start(); - try (Playwright playwright = Playwright.create()) { - Browser browser = playwright.chromium().launch( - new BrowserType.LaunchOptions() - .setHeadless(true) - ); - try { - Page page = browser.newPage(); - page.navigate("http://127.0.0.1:" + port + "/agui/ui"); - - String conversationId = String.valueOf(page.evaluate("() => String(sessionId || '')")); - Assertions.assertFalse(conversationId.isBlank(), "UI conversation/session id should be initialized"); - - page.locator("#prompt").fill(prompt); - page.locator("button[type='submit']").click(); - - page.locator(".msg.agent").last().waitFor(); - String uiOutput = page.locator(".msg.agent").last().innerText(); - - Assertions.assertTrue( - uiOutput.contains("Support ticket created successfully"), - "UI should render deterministic ticket response" - ); - - String conversationAudit = waitForConversationHit(port, conversationId, queryToken); - Assertions.assertTrue( - containsAnyIgnoreCase(conversationAudit, conversationId, queryToken), - "Audit conversation listing should include input/query token" - ); - - String auditJson = waitForAudit(port, conversationId, "Support ticket created successfully"); - Assertions.assertTrue(auditJson.contains(conversationId), "Audit trail should include same conversation id"); - Assertions.assertTrue( - containsAnyIgnoreCase(auditJson, "Support ticket created successfully", "support.ticket.open"), - "Audit search should include assistant/tool output" - ); - } finally { - browser.close(); - } - } + String conversationId = "agui-http-it-" + System.currentTimeMillis(); + HttpResult uiPage = get(port, "/agui/ui"); + Assertions.assertEquals(200, uiPage.statusCode()); + Assertions.assertTrue(uiPage.body().contains("plan-name"), "UI should expose the plan selector"); + + ObjectMapper mapper = new ObjectMapper(); + String requestBody = mapper.writeValueAsString(Map.of( + "threadId", conversationId, + "sessionId", conversationId, + "messages", List.of(Map.of("role", "user", "content", prompt)) + )); + HttpResult aguiResponse = post(port, "/agui/agent", requestBody); + Assertions.assertEquals(200, aguiResponse.statusCode()); + Assertions.assertTrue( + aguiResponse.body().contains("Support ticket created successfully"), + "AGUI response should include deterministic ticket output" + ); + + String conversationAudit = waitForConversationHit(port, conversationId, queryToken); + Assertions.assertTrue( + containsAnyIgnoreCase(conversationAudit, conversationId, queryToken), + "Audit conversation listing should include input/query token" + ); + + String auditJson = waitForAudit(port, conversationId, "Support ticket created successfully"); + Assertions.assertTrue(auditJson.contains(conversationId), "Audit trail should include same conversation id"); + Assertions.assertTrue( + containsAnyIgnoreCase(auditJson, "Support ticket created successfully", "support.ticket.manage"), + "Audit search should include assistant/tool output" + ); } finally { try { main.stop(); @@ -120,7 +110,7 @@ private static String waitForAudit(int port, String conversationId, String expec ); if (result.statusCode() == 200) { lastBody = result.body(); - if (containsAnyIgnoreCase(lastBody, expectedOutput, "support.ticket.open")) { + if (containsAnyIgnoreCase(lastBody, expectedOutput, "support.ticket.manage")) { return lastBody; } } @@ -150,6 +140,18 @@ private static HttpResult get(int port, String path) throws IOException, Interru return new HttpResult(response.statusCode(), response.body() == null ? "" : response.body()); } + private static HttpResult post(int port, String path, String body) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://127.0.0.1:" + port + path)) + .timeout(Duration.ofSeconds(10)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body == null ? "" : body)) + .build(); + + HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); + return new HttpResult(response.statusCode(), response.body() == null ? "" : response.body()); + } + private static String encode(String value) { return URLEncoder.encode(value, StandardCharsets.UTF_8); } @@ -176,7 +178,7 @@ public SpringAiChatResult generate(String systemPrompt, Integer maxTokens, java.util.function.Consumer streamingTokenCallback) { String query = userContext == null ? "" : userContext; - AiToolCall call = new AiToolCall("support.ticket.open", mapper.createObjectNode().put("query", query)); + AiToolCall call = new AiToolCall("support.ticket.manage", mapper.createObjectNode().put("query", query)); return new SpringAiChatResult("", List.of(call), true); } } diff --git a/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/McpAdminApiIntegrationTest.java b/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/McpAdminApiIntegrationTest.java index ce5283a..7e84ada 100644 --- a/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/McpAdminApiIntegrationTest.java +++ b/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/McpAdminApiIntegrationTest.java @@ -28,8 +28,10 @@ void shouldServeMcpAdminToolsAndExecuteAuditRoundtrip() throws Exception { System.setProperty("agent.runtime.test-port", Integer.toString(port)); Main main = new Main(); + main.bind("ticketLifecycleProcessor", new SupportTicketLifecycleProcessor(MAPPER)); try { AgentRuntimeBootstrap.bootstrap(main, "ag-ui-playwright-audit-test.yaml"); + SampleAdminMcpBindings.bindIfMissing(main, "ag-ui-playwright-audit-test.yaml"); main.start(); JsonNode initialize = mcpCall(port, "initialize", Map.of( @@ -54,6 +56,7 @@ void shouldServeMcpAdminToolsAndExecuteAuditRoundtrip() throws Exception { Assertions.assertTrue(catalogStructured.path("plans").isArray()); Assertions.assertTrue(containsPlan(catalogStructured.path("plans"), "support")); Assertions.assertTrue(containsPlan(catalogStructured.path("plans"), "billing")); + Assertions.assertTrue(containsPlan(catalogStructured.path("plans"), "ticketing")); Assertions.assertTrue(isDefaultPlan(catalogStructured.path("plans"), "support")); Assertions.assertTrue(isDefaultVersion(catalogStructured.path("plans"), "support", "v1")); diff --git a/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/SpringAiAuditTrailIntegrationTest.java b/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/SpringAiAuditTrailIntegrationTest.java index 5f2e8da..8f4f3db 100644 --- a/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/SpringAiAuditTrailIntegrationTest.java +++ b/samples/agent-support-service/src/test/java/io/dscope/camel/agent/samples/SpringAiAuditTrailIntegrationTest.java @@ -81,7 +81,7 @@ void shouldUseLlmDecisionAndCarryFirstTurnResultIntoSecondTurnContext() { "Now please file a support ticket using that knowledge base result."); Assertions.assertEquals("kb.search", toolName(first.events())); - Assertions.assertEquals("support.ticket.open", toolName(second.events())); + Assertions.assertEquals("support.ticket.manage", toolName(second.events())); Assertions.assertNotNull(gateway.secondTurnContext); Assertions.assertTrue(gateway.secondTurnContext.contains("Knowledge base result for"), "Second turn LLM evaluation should include first-turn KB tool result in context"); @@ -134,7 +134,7 @@ void shouldUseKnowledgeBaseBeforeTicketAndCarryContextAcrossTurns() { .anyMatch(e -> "kb.search".equals(e.payload().path("name").asText()))); Assertions.assertTrue(auditTrail.stream() .filter(e -> "tool.start".equals(e.type())) - .anyMatch(e -> "support.ticket.open".equals(e.payload().path("name").asText()))); + .anyMatch(e -> "support.ticket.manage".equals(e.payload().path("name").asText()))); Assertions.assertTrue(auditTrail.stream() .filter(e -> "tool.start".equals(e.type())) .anyMatch(e -> e.payload().path("arguments").path("query").asText("").contains("Knowledge base result for"))); @@ -178,7 +178,7 @@ void shouldNotInjectKnowledgeBaseContextWhenTicketIsFirstPrompt() { List auditTrail = persistenceFacade.loadConversation(conversationId, 50); Assertions.assertTrue(auditTrail.stream() .filter(e -> "tool.start".equals(e.type())) - .anyMatch(e -> "support.ticket.open".equals(e.payload().path("name").asText()))); + .anyMatch(e -> "support.ticket.manage".equals(e.payload().path("name").asText()))); Assertions.assertFalse(auditTrail.stream() .filter(e -> "tool.start".equals(e.type())) .anyMatch(e -> e.payload().path("arguments").path("query").asText("").contains("Knowledge base result for"))); @@ -194,10 +194,11 @@ private ToolResult executeTool(String toolName, JsonNode arguments, ObjectMapper ObjectNode data = mapper.createObjectNode().put("answer", "Knowledge base result for " + query); return new ToolResult(data.path("answer").asText(), data, List.of()); } - if ("support.ticket.open".equals(toolName)) { + if ("support.ticket.manage".equals(toolName)) { ObjectNode data = mapper.createObjectNode() .put("ticketId", "TCK-" + UUID.randomUUID()) .put("status", "OPEN") + .put("action", "OPEN") .put("summary", query) .put("assignedQueue", "L1-SUPPORT") .put("message", "Support ticket created successfully"); @@ -271,7 +272,7 @@ private List selectTools(String userText, String userContext) { String query = sawKnowledgeBaseInSecondTurn ? "Escalate with prior context: " + kbResult : userText; - return List.of(new AiToolCall("support.ticket.open", objectMapper.createObjectNode().put("query", query))); + return List.of(new AiToolCall("support.ticket.manage", objectMapper.createObjectNode().put("query", query))); } return List.of(); } diff --git a/samples/agent-support-service/src/test/resources/ag-ui-playwright-audit-test.yaml b/samples/agent-support-service/src/test/resources/ag-ui-playwright-audit-test.yaml index 35e8ca9..13c398a 100644 --- a/samples/agent-support-service/src/test/resources/ag-ui-playwright-audit-test.yaml +++ b/samples/agent-support-service/src/test/resources/ag-ui-playwright-audit-test.yaml @@ -11,8 +11,12 @@ agent: audit: granularity: debug runtime: - routes-include-pattern: classpath:routes/kb-search.camel.yaml,classpath:routes/kb-search-json.camel.xml,classpath:routes/ag-ui-platform.camel.yaml,classpath:routes/admin-platform.camel.yaml + routes-include-pattern: classpath:routes/kb-search.camel.yaml,classpath:routes/kb-search-json.camel.xml,classpath:routes/ticket-service.camel.yaml,classpath:routes/ag-ui-platform.camel.yaml,classpath:routes/admin-platform.camel.yaml agent-routes-enabled: true + a2a: + enabled: true + public-base-url: http://127.0.0.1:{{agent.runtime.test-port:18081}} + exposed-agents-config: classpath:agents/a2a-exposed-agents.yaml ai: mode: spring-ai agui: @@ -22,7 +26,7 @@ agent: agent-endpoint-uri: agent:support?plansConfig={{agent.agents-config}}&blueprint={{agent.blueprint}} fallback-enabled: false fallback: - ticket-keywords: ticket,open,create,support,login + ticket-keywords: ticket,open,create,update,close,status,support,login error-markers: api key is missing,openai api key,set -dopenai.api.key realtime: bind-relay: false From c79eef663828f88b9f505ba2ec9f30ea9b458301 Mon Sep 17 00:00:00 2001 From: Roman Date: Sat, 14 Mar 2026 14:28:17 -0700 Subject: [PATCH 04/19] Document multi-agent catalog setup --- README.md | 80 +++++++++++++++++- docs/TEST_PLAN.md | 77 +++++++++++++++++- docs/architecture.md | 75 +++++++++++++++++ samples/agent-support-service/README.md | 104 ++++++++++++++++++++---- 4 files changed, 314 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 899f02a..079cb69 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,71 @@ This activates `-Pdscope-local` for local DScope dependency alignment used by ru agent:agentId?blueprint=classpath:agents/support/agent.md&persistenceMode=redis_jdbc&strictSchema=true&timeoutMs=30000&streaming=true ``` +## Multi-Agent Plan Catalog + +Runtime can resolve agents from a catalog instead of a single blueprint: + +```yaml +agent: + agents-config: classpath:agents/agents.yaml + blueprint: classpath:agents/support/agent.md # optional legacy fallback +``` + +Catalog behavior: + +- multiple named plans +- multiple versions per plan +- one default plan +- one default version per plan +- sticky conversation selection persisted as `conversation.plan.selected` + +Request entrypoints can pass `planName` and `planVersion`. When omitted, runtime uses sticky selection for the conversation, then catalog defaults. + +## A2A Runtime + +Camel Agent now integrates `camel-a2a-component` as a first-class protocol bridge. + +Runtime config: + +```yaml +agent: + runtime: + a2a: + enabled: true + public-base-url: http://localhost:8080 + exposed-agents-config: classpath:agents/a2a-exposed-agents.yaml +``` + +Exposed-agent config is separate from `agents.yaml`. It maps public A2A identities to local plans: + +```yaml +agents: + - agentId: support-ticket-service + name: Support Ticket Service + defaultAgent: true + planName: ticketing + planVersion: v1 +``` + +Inbound endpoints: + +- `POST /a2a/rpc` +- `GET /a2a/sse/{taskId}` +- `GET /.well-known/agent-card.json` + +Outbound behavior: + +- blueprint tools can target `a2a:` endpoints +- runtime persists remote task/conversation correlation +- audit trail records outbound/inbound A2A transitions + +Shared infrastructure behavior: + +- agent-side A2A classes stay in `camel-agent-core` +- generic task/session/event persistence comes from `camel-a2a-component` +- if `a2aTaskService`, `a2aTaskEventService`, and `a2aPushConfigService` are already bound, Camel Agent reuses them instead of creating a private task space +- this allows multiple agent flows and non-agent routes to share the same A2A session/task runtime + ## Persistence Defaults Default mode is `redis_jdbc` (Redis fast path + JDBC source-of-truth behavior inherited from `camel-persistence`). @@ -134,6 +199,19 @@ See sample-specific usage and test guidance in: - `samples/agent-support-service/README.md` +For local no-key A2A demo runs, the sample also includes: + +- `io.dscope.camel.agent.samples.DemoA2ATicketGateway` + +Use it to simulate support-agent -> A2A ticket-service -> local ticket route behavior without a live model backend: + +```bash +./mvnw -q -f samples/agent-support-service/pom.xml \ + -Dagent.runtime.spring-ai.gateway-class=io.dscope.camel.agent.samples.DemoA2ATicketGateway \ + -Dagent.runtime.routes-include-pattern=classpath:routes/kb-search.camel.yaml,classpath:routes/kb-search-json.camel.xml,classpath:routes/ticket-service.camel.yaml,classpath:routes/ag-ui-platform.camel.yaml,classpath:routes/admin-platform.camel.yaml \ + exec:java +``` + ## Spring AI Runtime Config (Sample) `samples/agent-support-service/src/main/resources/application.yaml` configures runtime provider routing: @@ -220,7 +298,7 @@ mvn -U -f pom.xml verify Sample integration tests verify route selection and context carry-over behavior: - first prompt asks for knowledge base help, route/tool selected: `kb.search` -- second prompt asks to file a ticket, route/tool selected: `support.ticket.open` +- second prompt asks to file a ticket, route/tool selected: `support.ticket.manage` - second-turn LLM evaluation context includes first-turn KB result - negative case: direct ticket prompt without prior KB turn does not inject KB context diff --git a/docs/TEST_PLAN.md b/docs/TEST_PLAN.md index 1e7211a..8bee3f2 100644 --- a/docs/TEST_PLAN.md +++ b/docs/TEST_PLAN.md @@ -34,13 +34,26 @@ - `shouldNotLogVoiceBranchStartedWhenContextUpdateFails`: verifies `updated=false` + skip checkpoints and absence of voice-branch-start log when realtime context update is not applied - `shouldInitializeSessionStartPayloadWhenNoStoredContextExists`: verifies dropped/recreated realtime sessions initialize from incoming `session.start` payload + defaults when no prior context exists - `shouldRestoreRealtimeSessionContextOnSessionStartReconnect`: verifies dropped/recreated realtime sessions are rehydrated with full stored context plus incoming overrides on `session.start` +12. `A2AExposedAgentCatalogLoaderTest` + - validates exposed A2A agent catalog + - enforces one default public A2A agent +13. `AgentA2ATaskAdapterTest` + - adds agent correlation metadata on top of shared `A2ATaskService` + - preserves shared-service idempotency behavior + - appends audit events without owning task persistence +14. `AgentA2AProtocolSupportTest` + - reuses pre-bound shared `a2aTaskService` / `a2aTaskEventService` / `a2aPushConfigService` + - confirms agent runtime does not create a private A2A task space when shared services already exist +15. `A2AToolClientTest` + - outbound `a2a:` tool invocation + - remote task correlation reuse for follow-up calls ## Integration 1. `SpringAiAuditTrailIntegrationTest` - two-turn deterministic scenario: - prompt 1 selects `kb.search` - - prompt 2 selects `support.ticket.open` + - prompt 2 selects `support.ticket.manage` - second prompt evaluation includes first-turn KB result - negative scenario: - ticket as first prompt does not inject KB result context @@ -50,6 +63,14 @@ 3. `redis_jdbc` fallback behavior with Redis miss and JDBC success 4. Restart rehydration scenario for unfinished task 5. AGUI sample transport flow (`/agui/ui` -> `POST /agui/agent` POST+SSE) +6. A2A sample flow + - direct `POST /a2a/rpc` `SendMessage` creates ticket through exposed public A2A service + - follow-up `SendMessage` with same linked conversation updates or closes the same ticket + - `GET /.well-known/agent-card.json` exposes only configured public A2A agents +7. AGUI -> agent -> outbound A2A tool -> ticketing service flow + - support assistant selects `support.ticket.manage` + - outbound A2A audit event is recorded + - assistant output reflects ticket service result 6. Realtime voice UI behavior (`/agui/ui`): - single-toggle start/stop state transitions (`idle`/`live`/`busy`) - VAD pause profile mapping (`fast=800`, `normal=1200`, `patient=1800`) reflected in UI label/status @@ -62,7 +83,7 @@ - assert-style positive check (auto-fail unless `200`): `sid=voice-sync-$RANDOM; curl -s -X POST "http://localhost:8080/realtime/session/$sid/init" -H 'Content-Type: application/json' -d '{"session":{"audio":{"output":{"voice":"cedar"}}}}' >/dev/null; code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "http://localhost:8080/realtime/session/$sid/token" -H 'Content-Type: application/json' -d '{}'); [ "$code" = "200" ] || { echo "expected 200, got $code"; exit 1; }` - assert-style negative check (auto-fail unless `400`): `sid=voice-sync-$RANDOM; curl -s -X POST "http://localhost:8080/realtime/session/$sid/init" -H 'Content-Type: application/json' -d '{"session":{"audio":{"output":{"voice":"nova"}}}}' >/dev/null; code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "http://localhost:8080/realtime/session/$sid/token" -H 'Content-Type: application/json' -d '{}'); [ "$code" = "400" ] || { echo "expected 400, got $code"; exit 1; }` - script alias (positive+negative in one run): `bash scripts/realtime-voice-token-check.sh` (optional args: `bash scripts/realtime-voice-token-check.sh http://localhost:8080 cedar nova`) -7. Realtime event ordering and context-update validation (scenario matrix aligned): +8. Realtime event ordering and context-update validation (scenario matrix aligned): - text-only AGUI path: - verify `user.message` is persisted before tool/assistant lifecycle events - verify final `assistant.message` and `snapshot.written` are emitted in order @@ -93,7 +114,7 @@ Use these during integration test runs (for example with logs in `/tmp/agent-sup 6. AGUI pre-run fallback decisions: - `grep -nE 'AGUI pre-run started|AGUI pre-run primary agent response|AGUI pre-run fallback triggered|AGUI pre-run deterministic fallback route|AGUI pre-run completed' /tmp/agent-support-live.log` 7. End-to-end voice-to-ticket trace: - - `grep -nE 'Realtime transcript received|Realtime transcript routed|Kernel realtime transcript routing|Kernel tool execution started:.*support.ticket.open|Kernel tool execution completed:.*support.ticket.open|Kernel final assistant message ready' /tmp/agent-support-live.log` + - `grep -nE 'Realtime transcript received|Realtime transcript routed|Kernel realtime transcript routing|Kernel tool execution started:.*support.ticket.manage|Kernel tool execution completed:.*support.ticket.manage|Kernel final assistant message ready' /tmp/agent-support-live.log` 8. Single-command triage (sorted, timestamped signal lines): - `grep -nE 'Realtime relay|Realtime event received|Realtime transcript|Kernel |AGUI pre-run|Agent runtime bootstrap' /tmp/agent-support-live.log | sort -t: -k1,1n` 9. Live monitoring (low-noise follow): @@ -103,4 +124,52 @@ Use these during integration test runs (for example with logs in `/tmp/agent-sup 11. Live monitoring with colorized source tags: - `tail -f /tmp/agent-support-live.log | awk 'BEGIN{reset="\033[0m"; cRelay="\033[36m"; cKernel="\033[35m"; cAgui="\033[33m"; cBoot="\033[32m"; cRt="\033[34m"} /Realtime relay/{print cRelay "[relay] " $0 reset; next} /Kernel /{print cKernel "[kernel] " $0 reset; next} /AGUI pre-run/{print cAgui "[agui] " $0 reset; next} /Agent runtime bootstrap/{print cBoot "[bootstrap] " $0 reset; next} /Realtime (event received|transcript)/{print cRt "[realtime] " $0 reset; next}'` 12. Non-ANSI fallback (CI/plain logs): - - `tail -f /tmp/agent-support-live.log | awk '/Realtime relay/{print "[relay] " $0; next} /Kernel /{print "[kernel] " $0; next} /AGUI pre-run/{print "[agui] " $0; next} /Agent runtime bootstrap/{print "[bootstrap] " $0; next} /Realtime (event received|transcript)/{print "[realtime] " $0; next}'` \ No newline at end of file + - `tail -f /tmp/agent-support-live.log | awk '/Realtime relay/{print "[relay] " $0; next} /Kernel /{print "[kernel] " $0; next} /AGUI pre-run/{print "[agui] " $0; next} /Agent runtime bootstrap/{print "[bootstrap] " $0; next} /Realtime (event received|transcript)/{print "[realtime] " $0; next}'` + +## A2A Manual Smoke + +1. Start sample with a live model or the demo gateway: + +```bash +./mvnw -q -f samples/agent-support-service/pom.xml \ + -Dagent.runtime.spring-ai.gateway-class=io.dscope.camel.agent.samples.DemoA2ATicketGateway \ + -Dagent.runtime.routes-include-pattern=classpath:routes/kb-search.camel.yaml,classpath:routes/kb-search-json.camel.xml,classpath:routes/ticket-service.camel.yaml,classpath:routes/ag-ui-platform.camel.yaml,classpath:routes/admin-platform.camel.yaml \ + exec:java +``` + +2. Confirm health and agent card: + +```bash +curl -sS http://localhost:8080/health +curl -sS http://localhost:8080/.well-known/agent-card.json +``` + +3. Open ticket through direct A2A: + +```bash +curl -sS -X POST http://localhost:8080/a2a/rpc \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":"1","method":"SendMessage","params":{"conversationId":"demo-a2a-1","message":{"messageId":"m1","role":"user","parts":[{"partId":"p1","type":"text","mimeType":"text/plain","text":"Please open a support ticket for my login issue"}]},"metadata":{"agentId":"support-ticket-service"}}}' +``` + +4. Reuse returned `linkedConversationId` to close the same ticket: + +```bash +curl -sS -X POST http://localhost:8080/a2a/rpc \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":"2","method":"SendMessage","params":{"conversationId":"demo-a2a-1","message":{"messageId":"m2","role":"user","parts":[{"partId":"p2","type":"text","mimeType":"text/plain","text":"Please close the ticket now"}]},"metadata":{"agentId":"support-ticket-service","linkedConversationId":""}}}' +``` + +5. Drive the same flow through AGUI: + +```bash +curl -sS -X POST http://localhost:8080/agui/agent \ + -H 'Content-Type: application/json' \ + -d '{"threadId":"demo-agui-1","sessionId":"demo-agui-1","planName":"support","planVersion":"v1","messages":[{"role":"user","content":"Please open a support ticket for my login issue"}]}' +``` + +6. Confirm audit recorded outbound A2A: + +```bash +curl -sS 'http://localhost:8080/audit/search?conversationId=demo-agui-1&limit=100' +``` diff --git a/docs/architecture.md b/docs/architecture.md index ba0e733..b24516a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -24,6 +24,7 @@ Plan-aware runtime behavior: - first successful resolution appends `conversation.plan.selected` - later explicit overrides append a new `conversation.plan.selected` - audit and runtime refresh derive active blueprint from persisted plan-selection events instead of assuming one global blueprint +- A2A-exposed public agents are mapped separately from the internal plan catalog Runtime bootstrap also binds mutable operational controls and optional archive services: @@ -67,6 +68,63 @@ Internal request headers: `camel-agent-persistence-dscope` maps these flows to `FlowStateStore` operations. +## A2A Integration + +Camel Agent uses `camel-a2a-component` as the protocol/runtime layer and keeps agent-specific behavior on top. + +Agent-side responsibilities: + +- exposed-agent mapping (`agent.runtime.a2a.exposed-agents-config`) +- mapping public A2A agent ids to local `{planName, planVersion}` +- parent/root/linked conversation correlation +- agent audit events and routed-back parent notifications +- AGUI/A2A correlation metadata + +Generic A2A responsibilities delegated to `camel-a2a-component`: + +- task lifecycle +- idempotency +- task event streaming +- push notification config handling +- optional persistence-backed A2A task/event stores +- JSON-RPC envelope, dispatch, SSE, and agent-card plumbing + +Inbound A2A request flow: + +1. `POST /a2a/rpc` receives a core A2A method. +2. `SendMessage` / `SendStreamingMessage` resolve a public A2A agent id from exposed-agent config. +3. The public agent maps to a local `planName` / `planVersion`. +4. Runtime creates or reuses a linked local conversation for the A2A task. +5. The selected local plan is invoked through `agent:`. +6. Result is written back into the shared A2A task service and audit trail. + +Outbound A2A tool flow: + +1. A local agent selects a tool with `endpointUri: a2a:...`. +2. `A2AToolClient` sends an A2A JSON-RPC request to the remote agent. +3. Remote task id / remote conversation id / linked local conversation id are persisted as correlation. +4. Audit records outbound start/completion and linked conversation metadata. + +### Shared Task/Session Model + +Camel Agent does not require a private A2A task store. + +Startup behavior: + +- if `a2aTaskService`, `a2aTaskEventService`, and `a2aPushConfigService` already exist in the Camel registry, runtime reuses them +- otherwise runtime creates shared defaults using the same persistence configuration rules as `camel-a2a-component` + +This design keeps one A2A task/session space available to: + +- agent-backed A2A handlers +- outbound `a2a:` tools +- non-agent Camel routes that also use the same bound A2A services + +Implementation note: + +- `AgentA2ATaskAdapter` is the thin agent-side layer that adds agent metadata and audit behavior on top of the shared `A2ATaskService` +- the older duplicate agent-owned A2A task repository model is no longer the active design + ## Non-Blocking Audit Path Runtime can move audit writes off the request thread: @@ -138,6 +196,15 @@ Correlation between agent conversations and transport identifiers is handled in Debug audit trail includes available correlation metadata in payload (`payload._correlation`). +A2A correlation keys now also include: + +- `a2a.agentId` +- `a2a.remoteConversationId` +- `a2a.remoteTaskId` +- `a2a.linkedConversationId` +- `a2a.parentConversationId` +- `a2a.rootConversationId` + Plan-aware request behavior: - `POST /agui/agent` accepts top-level `planName` / `planVersion` @@ -242,6 +309,14 @@ Per-step audit projections also include the same resolved agent block so operato When async audit is enabled, these projections include both persisted and still-queued events because the read path merges pending async entries before rendering audit responses. +A2A audit additions: + +- `conversation.a2a.request.accepted` +- `conversation.a2a.response.completed` +- `conversation.a2a.outbound.started` +- `conversation.a2a.outbound.completed` +- conversation metadata projection for linked A2A conversations and remote task ids + ### Event Flow Scenarios #### 1) No Voice Agent (Text-only AGUI) diff --git a/samples/agent-support-service/README.md b/samples/agent-support-service/README.md index b7d3787..45d744a 100644 --- a/samples/agent-support-service/README.md +++ b/samples/agent-support-service/README.md @@ -3,7 +3,9 @@ Runnable Camel Main sample for the `agent:` runtime with: - Spring AI gateway mode (`agent.runtime.ai.mode=spring-ai`) -- local routes for `kb.search` and `support.ticket.open` +- multi-agent plan catalog (`support`, `billing`, `ticketing`) +- local routes for `kb.search` and ticket lifecycle state updates +- inbound and outbound A2A support for ticket management - audit trail persistence via JDBC-backed DScope persistence - AGUI component infrastructure from `io.dscope.camel:camel-ag-ui-component` - simple Copilot-style web UI (`/agui/ui`) that calls `POST /agui/agent` (AGUI POST+SSE stream response) @@ -12,7 +14,11 @@ Runnable Camel Main sample for the `agent:` runtime with: - Java 21 - Maven -- OpenAI key (for live Spring AI runs) +- OpenAI key for live Spring AI runs + +For local simulation without an API key, use the sample demo gateway: + +- `io.dscope.camel.agent.samples.DemoA2ATicketGateway` ## Configure Secrets @@ -51,9 +57,19 @@ Or use the helper script (auto-loads `.agent-secrets.properties`): samples/agent-support-service/run-sample.sh ``` +For local A2A demo mode without an API key: + +```bash +./mvnw -q -f samples/agent-support-service/pom.xml \ + -Dagent.runtime.spring-ai.gateway-class=io.dscope.camel.agent.samples.DemoA2ATicketGateway \ + -Dagent.runtime.routes-include-pattern=classpath:routes/kb-search.camel.yaml,classpath:routes/kb-search-json.camel.xml,classpath:routes/ticket-service.camel.yaml,classpath:routes/ag-ui-platform.camel.yaml,classpath:routes/admin-platform.camel.yaml \ + exec:java +``` + Then open: - `http://localhost:8080/agui/ui` for the frontend +- `http://localhost:8080/audit/ui` for audit exploration - Use the single voice toggle button in the UI to start/stop mic streaming to realtime. ## Manual Verification Checklist (Web + Realtime) @@ -77,7 +93,7 @@ lsof -nP -iTCP:8080 -sTCP:LISTEN | cat 4. Web chat check: - Send: `My login is failing, please open a support ticket`. -- Expect assistant response and ticket JSON/widget behavior. +- Expect the support assistant to call the A2A ticketing service and render ticket JSON/widget behavior. 5. Realtime voice check (browser): @@ -99,7 +115,7 @@ curl -s -X POST http://localhost:8080/realtime/session/voice-check-1/event \ 7. Expected fallback behavior without valid OpenAI credentials: - AGUI pre-run falls back to deterministic local routing. -- Ticket-like prompts route to `support.ticket.open`; non-ticket prompts route to `kb.search`. +- With `DemoA2ATicketGateway`, support prompts still route through the multi-agent A2A ticket flow. Or call the same AGUI endpoint used by the frontend directly (POST+SSE stream response): @@ -115,13 +131,6 @@ curl -N -X POST http://localhost:8080/agui/agent \ When assistant text contains ticket JSON, the frontend renders a `ticket-card` widget template. -If OpenAI credentials are missing, AGUI requests automatically fall back to deterministic local routing via the AGUI pre-run processor: - -- ticket-like prompts -> `direct:support-ticket-open` -- other prompts -> `direct:kb-search` - -This keeps `/agui/ui` demo flows working offline. - ## AGUI Frontend Flow Current UI transport model in this sample: @@ -133,6 +142,13 @@ Current UI transport model in this sample: 3. Frontend parses AGUI events (especially `TEXT_MESSAGE_CONTENT`) and renders assistant text. 4. If assistant text contains ticket JSON, frontend renders a `ticket-card` widget. +In the current sample, `support.ticket.manage` is an outbound `a2a:` tool: + +- support/billing agents call `a2a:support-ticket-service` +- exposed A2A agent `support-ticket-service` maps to local plan `ticketing:v1` +- ticketing agent calls local route tool `support.ticket.manage.route` +- ticket lifecycle route updates ticket state and returns JSON + Voice/transcript UX in `/agui/ui`: - Single dynamic voice toggle button with idle/live/busy visual states. @@ -161,7 +177,8 @@ curl https://api.openai.com/v1/models -H "Authorization: Bearer $OPENAI_API_KEY" ## Routes Used by Tools - `kb.search` -> `direct:kb-search` (`routes/kb-search.camel.yaml`) -- `support.ticket.open` -> `direct:support-ticket-open` (`routes/kb-search-json.camel.xml`) +- `support.ticket.manage.route` -> `direct:support-ticket-manage` (`routes/ticket-service.camel.yaml`) +- `support.ticket.manage` -> `a2a:support-ticket-service?remoteUrl={{agent.runtime.a2a.public-base-url}}/a2a` - `support.mcp` -> `mcp:http://localhost:3001/mcp` (optional MCP seed; runtime discovers concrete MCP tools via `tools/list`) ## MCP Quick Start (Local) @@ -227,6 +244,59 @@ Example event payload shape: - `GET /agui/stream/{runId}` -> optional event stream endpoint for split transport mode - `GET /agui/ui` -> simple Copilot-like frontend with ticket widget rendering +## A2A Endpoints + +- `POST /a2a/rpc` +- `GET /a2a/sse/{taskId}` +- `GET /.well-known/agent-card.json` + +### Direct A2A Smoke + +Open ticket: + +```bash +curl -sS -X POST http://localhost:8080/a2a/rpc \ + -H 'Content-Type: application/json' \ + -d '{ + "jsonrpc":"2.0", + "id":"1", + "method":"SendMessage", + "params":{ + "conversationId":"demo-a2a-1", + "message":{ + "messageId":"m1", + "role":"user", + "parts":[{"partId":"p1","type":"text","mimeType":"text/plain","text":"Please open a support ticket for my login issue"}] + }, + "metadata":{"agentId":"support-ticket-service"} + } + }' +``` + +Close the same ticket by reusing returned `linkedConversationId`: + +```bash +curl -sS -X POST http://localhost:8080/a2a/rpc \ + -H 'Content-Type: application/json' \ + -d '{ + "jsonrpc":"2.0", + "id":"2", + "method":"SendMessage", + "params":{ + "conversationId":"demo-a2a-1", + "message":{ + "messageId":"m2", + "role":"user", + "parts":[{"partId":"p2","type":"text","mimeType":"text/plain","text":"Please close the ticket now"}] + }, + "metadata":{ + "agentId":"support-ticket-service", + "linkedConversationId":"" + } + } + }' +``` + ### AGUI Pre-Run Fallback Quick Check This sample blueprint now includes `aguiPreRun` metadata. You can verify deterministic fallback routing (without valid OpenAI credentials) by calling `POST /agui/agent` with a ticket-oriented prompt: @@ -244,7 +314,7 @@ curl -N -X POST http://localhost:8080/agui/agent \ Expected result: - AGUI pre-run processor invokes the normal agent endpoint first. -- If response indicates key/auth failure (or is empty), fallback selects ticket route based on blueprint `aguiPreRun.fallback.ticketKeywords` and tool metadata (`support.ticket.open` -> `direct:support-ticket-open`). +- If response indicates key/auth failure (or is empty), fallback selects ticket route based on blueprint `aguiPreRun.fallback.ticketKeywords` and tool metadata (`support.ticket.manage` -> `direct:support-ticket-manage`). - SSE output contains assistant text with ticket payload that the sample UI renders as a `ticket-card` widget. ## Realtime Relay Endpoint (Foundation) @@ -288,13 +358,13 @@ Example route patch (YAML): ```yaml - route: - id: support-ticket-open + id: support-ticket-manage-route from: - uri: direct:support-ticket-open + uri: direct:support-ticket-manage steps: - setHeader: name: realtimeSessionUpdate - constant: '{"metadata":{"lastTool":"support.ticket.open"},"audio":{"output":{"voice":"alloy"}}}' + constant: '{"metadata":{"lastTool":"support.ticket.manage.route"},"audio":{"output":{"voice":"alloy"}}}' - setBody: simple: '{"ticketId":"TCK-${exchangeId}","status":"OPEN","summary":"${body[query]}"}' ``` @@ -364,7 +434,7 @@ Covered scenarios: 1. Two-turn routing + memory (deterministic gateway): - prompt 1 asks knowledge base -> tool `kb.search` - - prompt 2 asks ticket -> tool `support.ticket.open` + - prompt 2 asks ticket -> tool `support.ticket.manage` - prompt 2 evaluation includes prompt 1 KB result 2. Negative case: - ticket as first prompt does not inject KB context From 58da1090d643f37aa4f1da3c9d98a0e2f9e3b054 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:02:34 +0000 Subject: [PATCH 05/19] Initial plan From 17799f9c8db94b687a53661b8b16358402baa67c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:17:31 +0000 Subject: [PATCH 06/19] Address PR review feedback: fix A2A runtime, tool client, and catalog issues Co-authored-by: rdobrik <8812511+rdobrik@users.noreply.github.com> --- .mvn/wrapper/maven-wrapper.jar | Bin 0 -> 63029 bytes .mvn/wrapper/maven-wrapper.properties | 18 + .../agent/a2a/A2AExposedAgentCatalog.java | 10 +- .../dscope/camel/agent/a2a/A2AToolClient.java | 2 +- .../agent/a2a/AgentA2AProtocolSupport.java | 11 +- .../agent/runtime/A2ARuntimeProperties.java | 3 +- .../agent/runtime/AgentRuntimeBootstrap.java | 4 +- .../a2a/AgentA2AProtocolSupportTest.java | 3 +- docs/roadmap.md | 2 +- mvnw | 335 +++++++++++++++++- mvnw.cmd | 232 ++++++++++-- .../agent/samples/SampleAdminMcpBindings.java | 25 +- 12 files changed, 571 insertions(+), 74 deletions(-) create mode 100644 .mvn/wrapper/maven-wrapper.jar create mode 100644 .mvn/wrapper/maven-wrapper.properties diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..716422558d4bd975382c136a1038a18a88157dce GIT binary patch literal 63029 zcmb4q1CS^|ljgjcH@0otwr$(CZQHhO+qP}ndZT-b+ui?e@4wiNu85BAFEcW$v#Tn< ztd^4m`V9d900sbHkQuKA@Lvw_zt_^jO8nH~G9t8ce_=>}VPO9X%b`f8EdBdf;uiqG z-@E?_ljfHZ7ZFxeqLCJfj^&3Kpo0^B4c^kHi7Qx9cB#fwBMYPMz{w~FStKedKe)CA z7TyT1IT`f*97Qry64c%keaLPQ>0{8TGdlza63^lOjpRlM(XJI};y@^_BF|mdKXwDxyJ~*8x`qbGe_?0`B_s zTXPfheL$_rGP@*2>a94(Z};ZDQvEA8M_Be>9Q*J6|NYl7TL;sBb^X6bK>j;I-%j7q z%=n)G;r|h6t?y!N^H1RD{~g@bLEp~K*x{duVgEBRLo4%tA`1Ig^grJ9|Ia+K{~P?b zpJ@KA7ij)fn&$s+EzAEv%fITR{;xX!GZr&Nhkx1s)dl%C7Los}P9b4AVHqJ|89{Mj z#p)P-2tax$;a6^9n!N7NJ()D;tnYa!CUA4~xy4QNyWf9Y#y{|RO*y`#Cem&CH;N`f zDD2GqX97f86 zl58e(%+v7tqmXJrbD?${lEhHrI=ohpx-lBOh_7ev)NcPGBiBDF(k0B|SL=?WhOM<2I}_a={*bq|w>}z?!xs`=-Rj!Zx2dd^&2Fxaq&!u%koKZ-+ROyDLLDf&$zZ$qmO4ORl>ex=I{GInreyG z+l^l^3c-uC%;ti&4qZA##hYvTGrAjyBYNImo^NT*Ie|{}2SND{_dx16{s3$lB|{?c zABi~Y1t|nlk}fcECr+iUc3{Y-AiI0H{kni}T82UX)vbAr z1Ki!UuA<`# z<`cWk(29)L!cya{erp`22?iWcexBV+A;ho17UQQLMWN1JOpBg7FV)^jN-R^yPyk(F z2GCPEmK1bm9#ZB{-`TYs%&AQ!1@*Aq*`uK^)5{__+10+}LYf^IA$76e%>cat zVBPs=y@vX)I4-g6F=@mH-oawPc_g5^B%UOrpLLx=Om(+0x)rkwxx`RLjGdNbx7=W$AF6htm zZlV@`IWIzNj@m+{Dm&OHpD>&eimiyP;P$%RbB9#_Uu>3s7y#+!%Yh`S21tSCUO8aE zC@d^qfbcuh?kw*5YR5@|V)&PmYg0@~NOe-S&Y+!He?F07fn=5wpJ<2b-@BgaGP-ZY zx@s|0niWPrmAadd$jwkWL@KK+VB$cxNg1|43V;ub6019)WL5!$T2hFS!wD+m=gUYX z@|}~)6IXW$l0GneR}M$n;S^amX))$VwaSX+VUbww!H4aR)5YS9)>xV#e0(L|2_z$a z$?x{9Nc&l{+5m8Jx&7YZpBK(Z^x#1@BOJI#)PV1jI=)%Ah(|;gGTy*B^e*g6V@^9T ze|Run)|om;H_<^^{Q#S+6Jw6^TLC~rJqwPuB8z!JS#=iT8JW@4)k8TFQ5}~vEA1~f z!vE+zSXV%*r`!elmgM+FZ~=nK%16$xq0&Hr>;TGIwsH&y!|bZ0CLmD}{|)ZKtHNVK z8E<(kLd$@bF2rwQ-Gphk<=~`rY(AQDx3D-C8{}5bR6eQ~bgnMQH6X85J2@W(lhB&{ zf+&zHeMKfmbNtjocoixqgd49vD?$*kYz6$1LNL4he#I0V`{vB$GS)Y%khA3%7JEHk zVgNc}g*1dHCn78cBXRmsMC5eQ3V%@AZ!HP*a^esj#45=PQc!z(P%Bnxx7m9C=C2<9 zJT|;Mda-UoBH4(QjI1Ock1mE6P5QYlC9;663c)1La0=^GAx2ohBtyRdlE&0$D zho=oif$e=5F1*uv%*1OzAAg*PJ)7h>fZ{TT%LuwH@q7CR=vJ$bVHq|} zZ_WYApTIvL2D3nN{!yLr_LnxOKIeO0*fDT$SuBflG#6}yp9O%=yYDagDC{+Qcu+3+ zm#R2Dk}N3cJ|2k9i5}a!Z6<8C?5e1>V`WOr^8TGqD;Ksp0`T!_O#8;bD`y!E>2-BY zzTCNaGHeUooHx`Pgblq#a$Wde-+u8zDzcL?s6jyXp_i4^WwS)K6B ze|TR6VB#PQIXH~xG*$R*h`*)qPBC92mFfsuTSnSbjp(>U%tel5Khe2pg$ZA6mVj`Y zEgereaO~WiSjpLtC4gsT9LgymM zNMxLWDYv^9myvv4GFv7NPZueF&Q>`n}v8+(U#2(b5AXdSqJQ z12KxJT-;${1!SEqBn5zS^Ao@sOCJq|6@sQ0(l{=(NO6{)2D*07_Ps_YyRDhUEPp`} z*0NBS1Ku~kN9hO*aeq3dJQANJvcjR?Zi?oGah=`HU_igF9bZ0crdZTeUaro?H6L-b z*q$aq1lu}O;x6u=xLF~N98-m9IxbX9A46i8zE+Oq42T%&B{?0_3%;krT+hdfipx<} z5R+AcqhF|C_#uhV2${gP%ZAlBW|fv4U7v%cOz3RNBa`*~y2Pd>GXjvu^?6>k6`lc|hx7aGBQtoT4=2MNY3*x(<9Icz zh8?&Od8t+=o#}2ykH2DBac_o4hqt#4oO`=;A~QQbHNH=>)vA0@e06JT{BF#8e)$ZY zmr0V&2T>}skVvBoIVzyrT>wbaq(@*7ctX_cO?@1HeOv-o^?0;vb$4pke0zK?K40{} z@oMjOf5A6teb#yPcKxIaoNYh&ICr0{f}-e*Tpz$-z3hJ-$ZYwvb#|-kIyN6~4uIIA z@crPhEIVEDu`+HU%M1c@nM&I-FF118LC*)r%6$?KO`jBVSv$e7!Q-&@HM;~|%_MQO zj6+>~=OmZZzYAZQGfvjOrm}m%kPHjoHgBDU(9EW)xdYGT+Td}kfp{&?)gd|s$#7ye z2W3)$<>BL^J6UX+>FE}CP#svi(xV@bjL(`Leg%XB&OBju;|qvRSli>k-%<~x0QLCq zowWF z&_3+MZ)*wicB{6}VZjUsu~B0rCYEInPa8 zhwS3>Tf^P@WlNvHWHvn)aIyI5QA4&#P2Z-4up6M9D8@vMl2=&HXdccN43cZb_1$s; z6P#fq3%{#AOLVRPysdk1UEow|t;QZ#8f{PS!Y_Wq!27~=L(-vYBPO(UM#QWcQQIab zX%|cc_SRmMeEgap41cD6vU5o(((M8wA=$(NDyUB>G*1$3Mjpcf$DTy%3$sj#<+++W z2)&Wz^!fHCYJ7RT)%ghWY*EWa>-1bKAQC~)}f&EWaFKx> zeLa{jMG)+Dw%g$kPTPlt_ZNav39;_LT&D(ojTmAl~M&vPyrGbMLA8e_^rw_PJ&43t>u{IoiYL`0X0@yqHW8$=VMctnaRo`1g zlJ@h-1RzwET=m_}GYa@uZ(uuXu4Cwg0vh+;#uCA6o(l%~x$ZkPeC?TJNTy6PnmapnT zn_o>^F z6xD`CbH!%*DAi;+dsdWXNV+T2=DfB4rU`Uo(&7aP@RcD3IZ%04XR*0jx_V)Osf?M7 zdmC?9nO#W zby8~~B_|8kV73_9TCoA^%>yNlWtj9aOV}})3+#<@oYFnImx|u#&zu#ba-OM->qDwV z49_hgDVOslu~v7k;yh)ti~L)V$z=)RAj;EtU-9ohii|#rEb~NyY<}GkAj(kq*O71I z4b_&!{?x-A)X$!eIJw%;7j;UH?#Qo(xL;N0GiAmce-aKI%~UNEDGS`>_IyfYJIs=w z8dPeFE0Mj!1?3;8HC&37BvmuY6J8udIzHnfX!ij8%jD84mGfG?OS=p)Pqywr=t2v} zAcnStq*JTiwM)jrB0~GGb}rOHA(<-`T%Sa2uB3oX@{`<^hV{8f;GC;QbS~*FG~fmg z-30pKk!26cU@t_TfIGssz&8zs*;0SQ{C2aq?9C`5i;?%{bP%4*iVk4k(59Q3`s7K) zv(A;H4@U%ys9xLz*2ZgQckx**OhW*hZp=f@GNP{yRP8tS-!u)-8#|PwD6wye?>_8M zY)Mm%1+n64#5cRX+4y4>EKR;R$2TmyZw{=hVh^JW-{#0D!a_f&RYxPUfE33^0Ly5;4gYKk$9SdI^Wss-oFy z#9LPnF>BnqW!Ua#os6O>U!FnG+8-A~EO*t)Jg|H~AUS$=302b*-dNjkha0`^u=KQ_ zL%0eA6RNAQAO_gK({_A1fZh$ttf(njb(@|dy`Cf+BGpd3Usc%)TKBc1B{sNRbHxJj ziuoqqNkzg-aJbPeW}jZWp%*RlKC#3{ak%w}9+gI=Dx+p^Q^%`o#)R4;I09!7*-Lcz z(anEajxzb-*&_K^VbOR?u^j;1g$a1~86fY6VFoe9ai}7*m^3A#1GAOJ3zt{!E+HcI zK4KMC>(oI-zLJhxP%$x)s@l!w3t{n=*?(TXQiq&adQx0P%;l*_aV7DvA)mNgC!Nd1 zjlw)l+G2c<+zRQ!;loJRwwe}u5cZxBm{;W93)A$ z61*#{^+^AjWi~P|X7B_a<5<0KWI$?_^x_eCn}@tV*==lE*0KfvRN7#S9FvL|+X}_bj>JJx4uicHL{~h1B8ss zc0nO#wS$ay>Y)}EL}}pk-d8B^yy)#607DegE4%bCheJtZdMsZqdP~lj*0D54veXypT}b9Q}5C>SH?N zs&;SDKJ0xC@Y8%9QcEFjr`zV`u#OHoXnV?uH%TBCtuG_i?EWvUqt+qEwObC4rhTbJ z`S%yeESK!wDBb;4Cys%WZ=79@@<;MdkY3?1jo3riH|}2@Vb*Uz-(oeq%=(s9U+I>& zi3kM89L;x-Mc{kFBE$p9m7a$gm#=Snr_xBO`MQIPf%lM=X}NeQsK5>RLT1pBfU9@*WhmQp)|-0N_qoL%J^(4+$4cMLTo0=T<g00Y} zS}{NX0Kvb79fp54c?ubu=sR0E3E8^ZSlQ|u89NZs|NGpjWG#!x59fmd0XnL0nM}~* zuTT$s)R!vDJanRgXrPH%VGV^lwLo*7eo3>ggYWf7dJZ~lb^v=Pif&pfCWq%QY2#vI zljCk;YI?hRd&~QaYnTyI5K6sxlVxTkOIFNP_i_|iger=o zj&7vRi>40G#f=vX(n1#`qBTgSmOd&3yEj#bSnK?tFAEwI5d{l0v27skPv2`BrSRFw zhra08ob7|0`Na2Ds!!VtUDo&tH4&OgtYBr=>ZWRkGvEOwdkE2>QM-L!1L$G$vFynP8vsW;OPcz&vqA3s~@$q1srIj2Cn>?PmLx`6`;tH*xuiW%J18 zbU^fGo8f*w6e_C;<9 z7UR-b<7}`#kK@w#BW!HK2J`Z&^!);BaIT&ZtncfPG-h7PUeRCPPYHP=&M$= zv_#}b;%1fZw9n+xVy^Ge*wjc=Ym8QrClC;a4^o0$4tQE*!cI&!Vx5xt^QIax2LkNt z$1Bm*sQ!@$k7=3$obGSv`3s=I} z5HJyirgVP~LZ6|)45IhuMG8UWQejjD77Vief;>zmN0;_=wp5`_#7?H@8%+XV(hokH zA%Ycb_79wn*uV(&R*M~JwE0w)GtRHIb{Goie3c}GE3WiOTV<=29A-`y(Xki#or)t? zbPaG9BK?Ak_Xp|HX6j|m`t74AMQDqHcJd_C6MKeoOdT;d&97S0JgGE>?Ay-7pmwdT zgkD=##w3TpG>YWuS<;tOu;Km-uosazdFJ;CM(pvdL8NF^fj`Z+lFH@{y675iLS$N6 zp>!X&E$VIpYFI;DCT|3#dO7$pZw!HSvy7>tJXL5kSp9zn_yJ$vI1z|@4g7o0aR6T9 z59!0XAnflkOs6$oIIm!3IwI{)D-zES{@~l-?BARka)eI$36^jVpmmJdc?o1+%gbWK zgVH}c;vm-3UffbH0uA*@HHC%BtJ4p3sznqbex}@|(GEKV4ch)=fChjfG4?+yg7c1O+a@M z^k@Lr)^if`1s3kqZJn!@O-A!qiu`E10l@9ZE0lNkO&LJ& z4EI%EpP&1XTHo7ZN&uH|Lx_vt8q}U7`KM1yRq<(yvgjrUP1UCkA=zjNh2pk!MW$15 zfM#-VOL=_AYK{WD#iR`#?1^aXJ_BOxfc>6~CL`^-c|FC>1@QP)3VG&jzbb(@RE`=~ z(^eqWO>3PurZhY@>VR#IBv=v`vZIV)E*(So-0b4D7wQ>Lq+*TgLtrT86ek!;85w~$(VD9nAG?7~SLm>Pd$-#71rxW{ly=m*MTM30o~sUz4%o%_ z`F83A*Mc&Ux`YQ!wzQ06kGyv1PkyURqrspnh@0x@iB39TYoyAOw+ZW=O>0_aj!vH@ z(mzNlH<6h`X%=s`fL~dmcfo9MdTNjgtodsqH<_6UXZC@p8Z6o&D0BT5a38!U2eW!e z)^3UYt?UH0MaM+X<$lH2;A_?9TBf_fP=k*Tjb5}2^_UcqU94T$J$`gFwx$Ez3hAU3 z`-bnd4^#f04H$kopJhPNrkS_CNMJ}@!l+J2pGGembHuge6xh?DI_QmeK)C#Yb0nKn zfo96!9!jfC?3lz7bunW#yWgK&_S%G_5xjk-JXfj@`+~4PISa`o;djmwE!o@Dv~rNY z$M1i=4A~)AhE2~$$gW;l;8 zFsjlEY!FSJu42C$4RLPbj-8HW&pe}O*N8@KRe{h-0B!UaFd_zteH{nJHuM5|EXotr zvzjh?n9fZ>w=x!j+Yw)mr(p;BWSTqSFc+Mm2wi)iOgOs=)ioiJh}h^B6uRW%a81k> zLTt1NH1}=JglSlzT0e1{t(O-QI_y3ej=Y`HAbpqg-U;$Po7wc#7#A8__2vnJzht_h zz)AHXhJu*lc7hgAiQ!sFPVp=yK^)0dxv`J95!xb#2_F+P+loK^A46$$@tb-6jhEjt z6k`@?f$G;7HoDV8hBxGz;(CV(kCB$#3MW}1qWs4!XXj42IkQC-iqw({*5j%0k5YSP zWMX|miXJS=V+c(Q?5{I%1rduskun}7I?$sDcT#v@PRW`Z`|?@a$(^YV#G135s#W> zU@rot*6@;IT||-0KB%pqOYQmzDb+IJt=cDiEAz9!FwNqZ1fl2*{lfRc%9XQ|8I0W% z4X+n=@8-4)Qx;i^GB1+6@9r`7jqb`jP>d>UV@ypHd2b4t6{{&dt=^l&m^pO!S9|U$5?nlY@v!H$reERZSnDkPQTyrlKYHuC69m)***#N>oqExo zzf692TovMe?d{>VKw!H_u*)HAQzFNitsu>b^56blF zu$La3d?WtDV{NqF3?=pY0o+?&(o-F3H1ek-M7O_o0z)&s9%RZqGDb7u!-C+?DtWO~ zM%AS4>*nIF(S%*1xs=13HFPhbCiyj+<$QMMpXsDz7NehUPYfSiU%pSoO3wz5oCJ}I zigHfz%$@*Vg}YIV@87-?E?(muq0X<)bGK`SljUxbNUyVB4p^z^_&cW&NCT|*s1V-@ zgSz{*=$*{74Of#HCy!g-AmLMp+QYjHr;1#%=9(1`uNdFIe$JTT1wpPwT5oFjukRQv zlAo*%IVP_%@5tijc|YW}%2!!(t0R1ilc8McZcZ_Pe`w@f+h~J+`g|$gXrX_iZIc=J zp;4U7N4Si$jo!8t3n8$4z6SPWnGZJKbb)_L_c`Bc`lyYSN=%$Hoo(ou-3&20_WRxf zN7p`Vu(Vp%caK~&Yt5;c2L=^(y9halrl~1$&AGQq!~RsfUGgR}Y>lZ12j-s749!j% zAC5AbaP+9UI+^mA)-mK7fR{)I59E!{C&idv^+c6#K1P@oRYfX|LR=A_L_r)>{*Fn~ z@LZ3kzTJGNUqm-uv0gOLK~rWWu9xC5(U75<9nzCeaf)($ROyeo&P9s|4Wh8wqZSlEFrXk*thQ5MzcYbjS^Z$h5%~37AKsSiEd9%CxoW zDO)`qviexq*gue{a8ly3t~-|*Fam`HJ{U}CE?qBK6|NDqP#9#Av_xVT*KAN<*{<;Ysij%u`=dT#w_QtE&c%$J0_^k3@PZ8V%ZwZOu*Qk6da ziSIB_Tq+D}>sv{t^+wjk0`x>HYeK5k8?n>BY15CDq!k}7f(=P4QBlwTnJ^;-XEsc^*!hzI zoK3CLKSQ(6ymt8VizJuiOp zMuv(2G_xT0H2W24edu=CGk94v6JZkOTE&FZz4$c+ZC8sT*k%Z_qAy_$f~BE<6) zv(CusuoJILIXg{mi`4{j!N(V@9F`aaqK(lXdkr0p&NihSNDcQ}L)F6M+N>*Myacs` za}wjRNx)i~$vfoK-{vL1G&Wt1lfm65F@3QHsN=Hz;j+sF)qJXe-n1@xF!>b^v9+Y_ zlJ;XfcrrI=3VN!)fSFpHyZkWOAs%o4oVF{G+8o}B{C>~GTi+BjAQg9xKe}G?(>5Sg zHNZiiC$q)eyDPFuW>Qo z8CG;946sR!J;{E23)6O>6$%>4oES3oI@lbzhCJ&Encm;rIt58xpG;6+MQYwu9<~C) zc=N=icI8F(1eop;u^Q$BUbJ48YE9V27F*;=I9HbuH}B-QpgpVs-g*T8opb934-2Q2 zI^XLTN-_xBib}5}^nkT=w*&~&+U%+o)I&C4!U2LdK(VvO9JO#_xaLo^LC+s4tFqQ! zX?03tU)nZ_Tr0UneUs0m&;@YOG+nUijVQ3eqnx1BB_UgLFo7<3_re-6{sEDD6G zKTr046JmzvMX@0_gf9Cj>1K67;NurK3LN34`o=BH)#wL{bT0_`hPFGHdn60b`$E8@ z8G56~BsbK!Euj<+0+f1xsQ8R}mpBfr#fAp$q9lVvS~$Uc6-FW4^@PRZTItX>gKM!J zEzi86HZ&`;Tsgl7=wtuDUUdWGUOPXxd|JLP&oy&-qP)X0yPxPKb0FEqH%)T~;goN+ zOn(5wCiIF^vLkx7_4h1_?_m(-#~BeS9NV_~yqVk}Ta)DNF#HJa-b;3a_`2v?d53)OwEd3S$*H&1hugZ7}6>lFb`vd}n2jscw>W!VFX zx=wqr$z5pj#Ed$#Q29ymnImU|0w-zznqsQ$E9TQR*uLg2<`A6p#9S%eVWtK7zEk*f zUrzLGHsVd>%@$3j7U|w~5PRP@;m{YqL?^uh_D3@zaob;}-L9UJrV=upJc7?M9imrh+=klQ6*In74)P z4NF6l9k3twR^Ud2k?@2N5=@A?;t{Znn+ghb3G)QP+c$bq zpbk;P+h1ZVJ9E#nl9U2okC%s-2|7@qL6elGO(iH=h&otE*Cp~wFvV%XRi91{+ec3R zQCxaljLR(~c%$cGM|I=LXKiIQ>O9F*srY#2K;DTr0P%|MQcmlvW&aJlEts#N)3(kC`L=xd{ zpOzdySKV)u23|4NjV-jCyhJwkOtA}#-kWp zOpE$~4z#^{xN%b&G@ZMZ5_;=%&b+69n4M4|91s0TSeOyQ5emq zT}6)^3E@vXpawC29AGk^MEsIKdBLVpf|a<-ph;^ngpb4EJ*r#rDCA|ao4kk6UJRsd z*E6acm#B}Q2fw%JVp2*8)^nDNiOY$5Z_aDB`{UyxE-s)Odh4K>U+iElo*$mzV-ipG zXhcVPY_yb2c+?;qmql8LV^lwCcuX+70HLTFKO#~H46F`3`YKD-`dVF?(o+|k@sV&a zQRc&dcm}d4D`UXE9?`Ru`$j!9V{TZ9P99!=eY}2WC>=Cq3S$HTivr4t*g8EGW0Z;2 ziJ5#IuqJv-)d=*woc@|wM0%0B3pNAU6T;wJ0_tQRWQU@dRMASKf@8dl%9(gjJ+w8} zCUTisExK;B{@r4dF_;huNTK-nmL!CW9;y6AN@Is$mK}!#332si9{C!zBHS2Kb;RD9 zqT(eyi$+~Fx_1NFz?o2LfcM&L9sjJ!qD{LMqICe3wXXPmgHS<6OIZNW8Oh;#@zWdO zd^mrMPDslsBI6mxa8=#l8NZZ@NUu<0EBimSMeTGNZ3`BF~@+DjIQf5L9)uIgfuB+M>fi;Hh*zWn2z}n zIuZK}1zV^RHP0%2M14~7eB@B<&%xLjnV{swd!Z26DSCmS$9x=f!g2?N27fIQ6M;|- zwqzz!Mx2aUe2s)sha3OH8_Hn&n1=IDZa0$%`;H1-h$K`e6({cEq(=#!0kOA6T0Od^ zgGFMtm(Chey#5JQ$co4BSo3hAlGSuG`%+)vC_%ZWPBOg;96)& zO^ubZxDs(yf$$(4>lKG<4^v`7T{|Lv^h%|c>IDAR0$ItEB0rUG=k?@)#etf*tWr5f z;bk}EQg+~c zm~8#m4hCj4dXI$(g>owi^b)q)6p~qRh#0F$|I;Y$^!GTO>5K1dqV_hhnShkFf>KGF4qfkprJ| z_`wwq8ipW?arGWW%4~hJB}@NAYx}?jiR3t!bIVakPq_8uR#}QMHm6fAc;{`b1ZGc$N z4sX~x{a={}{nHpe5X0>lO7C16qo4xNy7}tRWm~Hl8KR?G2e%e?^e^T8?pGOuGT~lP z)sndqTtyDacAm=LV$f5B2(UY%vuzk&Xk=T0xk89UPj-3ptZ+8att>u)UlwJ|UT%5f z$JC%-uyb@bmev}5`%i!|Q-^QGoBAxRZ9wMtyM`M<;zNtN*MR zRV#}t{w3bOGl+=LL*V4M|y#;%OZxlXIA9 zvGu1XiX#YZ78=Na&^>tiW5rZM3ZdeAtmgS3uRVcx^^9VX>084!+3NmZ=m!HZ8%8l(;6w75xP_Q(Vs)X9^8 z1uMhPHYOacl#!TXp)0`Q&W|) zvVmt$Wa2**t}h45C>TO^tn*Am$YY+Y;c46)8MjU)l^}WEMNWv47i+2$_$2BL7@e%s z?LuDG<2GS!9woRj3iF{3&~&N?qiv8LyJkG4j+5R(zsZ)NS}$IY?OLs*Q&Gy5zH9K% zZhjChdbPbAR8q8YRJ=Lvg9Q?6J&Y{xAlrhjf)=+-hhl2Wp%?aj81$f{|mMUKsDo5?GU=g~9s1!1_ z&RIRdb^%@f_`P9nH1DxRSL-qH@Y6dg~K zR46_vh?XJBu7o2-w8^M*M(4}kC3xvn7fLXdA+TweO6);>F>0)!Ef_jAIU428`zD6H z6h(Ot_w>KA)s58i7?$YNnpjb5B-axKCxm!_{{fr%f%tRR%a?c;;L;l=>CqFF#e$u$ z*<11}Z3dj1_y5>{rAf;VIP>?Xs3t-HfPeY3?Ekr^=|4WMe_K#yYe4ELEvJ8f8zpcu zws#>Qpdq8-2#f`S5iI{!2MR|3|0NCvBIhx_MFf=CemI4lpm(WRsd5Qa39MONErGic z5zA$iY-73HapQ8ia^2C?x$WXwy?N8JsokoHTesW(I7XBnUQgjT%YDlG^Pao=lN(u^ z?eQ%PPOQu*w;=B)&_Crp@~e=5487VMV$_NdiJ=hCweJEll!_3Kt&{yUB!nm) zy;?C;YLy;{vHjf-9ohvLId{07wUsnHt9qD2f=-i;+F*_Qxjt>Zb2Yw@>-D=jJ;_-o zNk1l}ox_VwP%hhXGiJy!5cvkeWwbB^yJ;W*eY<2EE^2*#Z^zBR#N_?KWVB9yv1Yxe zdYpZq3j2{RH?m*NyKELMjJar9Nb**R$Ojt|dHe}!{Dp3Ktu_VaQZ1kn*l`qlWy7$F z!&cg|OO=k(x#Dm}B_XKO{edCLQmv+NfZdDBpAT{b!;|pE4^_ErrA|OSU!=~-Vg&Ij zcW7oEN~P5+!bg3<${8bH%D8-5qH4jeCT5vU*6urZLTs-X+ZBWm6;!Y!kLtQMPkC{e z+|)NaM0Re^5H%xk|4Wk~A`}%hPem|0rnyZ{jv+;cJ_ejif70gabR4UeV%BA>=0pp0 ziF&tufA+WI$cL3T7n;G2TRqIXO%>{ttO@SwXJ#_?r$3bOS417ZC?SoB3*|=;y5sUO zVxY0XCsC{eRIj{fVuQzh8=MuGNMs%OGzQD1nLQcF#duyz^`CBfN~Bh4=KP?Iil7dw zEGlrU45bujv*r1cg??+zaADewdcnbC4vsp-Iy_S&Ce7__3M=h?yHsMbX_F8aRu{-8 zr2vXX3h3IUdeL%KAt17!7PRgX%#b`i;i0gNh$2N%fT*%_`NRqwI?X^&L79}~QOxR{2k^D||-={+4T;3ZzduUw$l5Dl9iT{6iGGZ3|@;& z!(<<9AU3htXAMxZgP5w8G>Hh~yXo@)+4Hw1iA#Y+`tDTc6AkRueUA~XFh(ekKZfQm zzCDs(rugV4)G0akRCzqqMpN&> z>{;X4pA=?_^b>rOo%KB5^ym=ev$ou#NI$G6_nnkRSMQupT(sc9C`z$xC`^0E*$kB) ztncfvub44EIH!$PxWaAC-&$fy?WmNK5c%^dkOMcHttV4hFsj@?5te;L?1WL0bX{Dm z(s}jL>3J<^N=-1FA22FfY^dB`bxBJ8xbSPYA9NEf=n6!{CrwwWga`YNQ53eQ} zlx8C(W^~ZCB^UssT+j1m^mz}Pk*aaEdq=d+T`|1{f`cZyf)=>d`WRMQGnr7r3yFs< z^sPQ&oy?!ndOJly7jLXipVb151$tE76;;nAF};JbW+a4gXsVEkJDcS*?vkUtWtRf{ zrehK57(N*atN7NJ;Tql(s5)6gycchzpIO^%>MbLL@F)Xm;wO%b>7}l8>PeQq`G=_x zcwGH!Fc|9>k?r@jK_T7!cG9>0brn^%mv6}xRT>Qq9rw3Iu`kb}mCL?5pCCW^`@GJd zD7}bE(ahocq+K$L8ufa;6g%klf=Fi(8r|g~ZuU0{*KV!LP1y+dp4dOpKlnX#IE{_n zdI(cujT7@8-?kG&S+*MqAuz1pvU2tP5ut!GIgeH$Mil6u@$-jYQ(GDV8=e7eUxK|IbYWBXl@GZ(=qvD4~6A;UC-g$?D%hK(CqR3=PPriF8p@k)pa)poXj(S?7tYqcxDmaL}le5?wR9X1K{SXl#Nb+&4ABg$|aB z#oV5$8mdhyV_m^3=qG3qqZ{rVC!Hp?Qo+cFtxISci_(fIlV1WD8HG3J423kOD-TTC z7L~qQ?{wOa3Te9~RXmD?Ccw-YMC+^)Fy%cW5#Le6WK$WBq*yY&Ma+63@>pcaVIsUA1I1gs1}Utsqh~*RSsN| zdZrUvGs+()0M#p;v`NyGd0e73?48?bkQKxiGxat1%lcSL_|2e3J*SVH>%=OIgGrb> zvI_C?m?)8L*R2p)H02xVjb0KgrK~lJC)+Eu$rP>y9QY9_G+FpNMIadVl9wOAwP#dW zL8I@>G2DtBs%e}nc3&1)%aOe)ij$lHusBGY??l=7p;h zEtxm_)K(3LaX57k?7m-=foNl3yOurC6~y#e`OnS-t4@?Xm$M?%Nq1;bWSYIn1avO$ z7eyrNZ6?{Iv8SD$1)on(cQlQmu+%2ClVgQ%mJcW=rU$2o)L=%ZML|O_A2VpTh*Q!R zCu+CtB|KCm;kHKiE>UjYr;uEym$6U8YV1+BI+4+d5fx~<)Mo)_?da6RQ8KR&vIz08 z?-SIpjex!GuhwxZUb7Zj?|&U5F!ZwP4QYdC1Ci zC@JGB$pF8lnU=3bufr^PZ{3QjPaidV5}wm?y#HE$m%YULD6Tm%%n^EK#2mSvhfCDE zDo#fnX{&V~h$zQI-@R2cL`bd2!o4vGCh^Iw_*&#mxOWQG-$2`00##oQHTNI7PUeAR zb6rb6w5<%9t*5~WE86OE!n)qDs!l>99{kb-(m5D{9s4lUtk|)BYNVWpQJ*G}TR)9c7&s*< zN^jfLs0hdwvt#z0AmTcLS*6or;k4ONA2ou@Wgd7brerp()GoP8&L}?_9$|LCn9YeM zqu#+p$)w0$+ErgX8~nW_B)*)6mzh|2_qY^T4wuaspS&nTmzcfT8VS6jVvsSur=6UA zzA>T2K#tkA>JpOR+rDqzd@wJ=X#eMliLF|$)BLl=PAHd>cKn;azU1s%5K__e_KzLl^+?{8z=CEmtt$6@sH#K)|74$fTD8`PXKnVH`3 z1Sz1SzzO~&o0Hk}%eO|o=*ZqkkoqSurd9d2hgBpcu1NK)sw(OyWSE2h#n?H8XCAFt zJ{47L+qP}n))(7W#kOtRuGqGnimi&1$(in%?sJ}JPS0KLa|fiWtfBa0S#xIbFL73MTZXpHi`~uG5c}@1a0to8;p(nmA2#Pt$O-qBXl>F zVn`n?$L-SSU8qpuTXM<1CB%y<;p&lNOtBR>sbbX^9^%zh_}r1wc0IqP_%D_kOI)#? zm_<(YS_kg_MNca496}xCb6OXC!TJQ>j2UiV;5k5-o|Zj^i804iDe8`ncV!>AqQl;5D z-qbPX-~pxL0&tEC&d!NDmOKi-EbTqloev(>%LPJ+yvlRe3$Z(&JS0mH`ch1ffhP}7 z$Kl&StMP@qlxP()03Lw(i{3D+IusongB><4SZw%_v}{nUs=pY=rr(vNFmZ;?S`a~- z(Qm>Yy=4zWi!q4JI~K+6uHxCrdDrIR&!t!EpM_{!9ZJ$wY7_6AaKFX7tL+%NwP@S5 zK)m+skH5cH$;IpR@kAx`1XT1wY54_pd_x1}igpIwc|)*HO?*8@0r5ec^QthV1-Zo1 zL~Q!O`OXOiJ9#V2dKa)XhqAD(W->4a;xY_Lu_x$?&|KJpKWT?&?(s`D*ZkP->gtvarqw252h4{;~nT2 zL*MGi+TwEc>`tBT0KxakV=l!MR5YOetu#fX(V^^AKMh$F)uG0_t^ zm?$4MMe2H>Pj76t+e0$`ytUn)(ng=5Agvebs}0KqB1&?ry7e$!?r1Gl_Nk zQF%$<&{yf!w40N);CZob*MbrUMc&CITn0M{Q@)5gZY7_yUl`AMkVg7KGNR~S%t_*^ z5Xj@q%d4P&*APoZQMQ(cjuaPFSc!|q$qfg4G6vH0ZR}oAU>(-Ccw2 z$^hJ_#D$~Qy6?f zOO8D}y%>DxPh;poVgxRQC1aM|p7IAck3IP$fn2h28cXhvsU%qL zV?? z+5gd%f|bwZ{;_K;p|$SRwS{i>BXDdMHTk`>jxpp%xP~^6!8VV>=cFQ~kzE$*w zVNf(IfZr9yoUw&gp}3vN;!J(%zRt>e`8Xb-2ZE1NM-)^Mlo5va!~}PH;bXVlY>s}c z)>VAO^t}HbQ)Vy=(yb}|Igp@KU?t6AlyjP3w|AFt0gEr)_R%0?*sz4K3yn5}m`U^F z!*?Cc{k?3+CnJ6Vg0RA~UwaLF>^_R*Pv4y z$K%UUSLHs`Jd_w~6d()GHbJbk1JPv@;yrw^drxV3Om+0)>a3;_ao9KU07RMAr z8S*Lhawz6d{oFw+-p3;RcitZbxOUSKYSZB$&14p;*ziTzON1(Z(n_NjHo*p3zwHeM zTG!Z&K19X1zHGN7M7wQ@!C9^D!h^L>ci+-}4W-n!0{ezc_#*j9kpU(xhC0IQk(O}h zt{O|!d7<)7D_olVW}`2y=YR#XWa^7XC`Jm`ZKE@&Rm7;8{nGD`P(uF9oyViHKf9a+ zd|MIR{)=Vx?~IDW%drX#mjLGm(RLJqJvv(JAhm1&kA`N)(1~29+%b{|xTaW(*)8`f zp)L`MP#U0%uoLxXZVp z7j#LUD7qYOQ;}zoxFL2xD}^k8ycCpv9rP8k=4@qJxV5MP@QeAO)Rf!ckGQ$qS`*S;WeN4u4(X085gh^<5)?sf*|-5Rj0jR9B^=V2Ik6a#m5+>mY*=cf zILGUX^GoyU<{28E7iO2JekL5}57nr0J;V#}(9dRvYPPaqajQ-^oW+k40bd^i90oV& zKPl=D+!D$c0_66xRC*eoSX7j&c2-i30C0bb%TNO({*pKJeeIlXIYsJMXph zH-Sxo-CQo;3ODLJe8)ny+ABdNrR1Se(B=D;`hrdFT9!z(T&_XS>Y5h9dpam)tyHHe z%1H&fZ0aJ^$;ef)uVZg)gNRxi%d}QP&IMg~C!{DUi(LsZ2(``inRB z;joa`t{#Ok=kI*eqR`ZvLj%O8z6V;QZlJ67x5a*PF+lKJS9J`LR_iwP5sCacWqM%G zVQwyx0b(;KY0l%l8jg06c$%kc-~>Ks+#eSp=bus4*=$)~AF^A92%Ba^2(Dv5LQq&L zY??z2%4VWVQN4f=d72L*xD}z78=;r(9zoOHorY(_D%63zb&^VT?O1?GAFm$>Tz_|A zn$7F3>so>-i*Ar!m}e690dre*5j`o)m%$F$4AY07L}@ADNaKCs@YUNZ;{< zWHpKF53^y8e)sf5bX<2zgNXke`sZLqD-I%$t0r$APR`2h{y$#tKTGaK0GXJ84a0(fVtRC=_DV*XV|S}O3F z37(-5+@#HMD(P9=&2e<6=Q!@TR^zIyKbud$f9lD{pb&F9z}TtWkfGWQYagjePk;kk zs>Fh__G}}C4WA6)9;XN$Cl=A!DLceI4DAQ4`S^nfR{m&1zgD=BL%r_r1WTE$v4OIz zPqSfMA20fX{A)^V>|$_kdgdDS65Rf5Zo>ti{ixR^-}k2K4TO)P(a6xbg~ft7&@mGx z{vk4S=Ak_~t@Ipv0|3KU{4RJ=CQR6cmx-|{_<2D`TKmE z@{y%-OiZ$cwWT)O4QyilrJ!oX3EtuZC9Z>BgSFb4f_*`~iv;kEmt19CaXyUW zgVkKIY(n~9X^1>c)L5Twq+D zoL#)UJU6HR99&)*7CVK>MyYACS^Qf2a2Xc< z`CwlAkjCRDGx(v!LNEepn8S%iuu0w6nta-`u{frkuX-lU%+~RdwNeG`BN<|W#g^a- zLKic2{zr=WE2CE#SFT6PYx<*vW>+ReTn3T*Xi&>>xZ7g272Uri`0diE9oS)~`12tX zWMlj5sYYcw>w@j+fel z7)X^h3&N$sp*as!2hCwerqJ!=j_|@9*ZN=X&g(Y$(YYq45Y_SL4nJ^4e?03@s)a8h z?0d&J-mr|waK6(aTbZ8r_uZ~1IS`IqCEbGI)JkL49=i*VyAzLODh+4u1;zE`Cn(8D z(TY#*YWztLYMU`E-(}t*N`Z#Yt(YR95=GqsE6`eui;NWg0hJermKiXw$2la^J2F2_ znbpc{heZueUO(S#34EM-c+2PyT~Y^t%E^u#NAoDL-vgzq$myT1puE^l*H5`l7rWnO z(E``4Ht|SY<&K7fI>``w+C}jsn`006x6qMXS4D2~R9lKv95my-tBX9l7&g2Rgv8nv zM3!HP!ZqKsrpB5JA$0EF<&LmDH?MAaEl|@Lb;CP`It3BRP=BL+AJ1pOC4C<+p0rYw zl0K3wH>_M0qh>RVY?H7y1N*(IML$T|bggFH2s`NINcRPlS(>lt@av5wP<#ChqPO;E%k>v3tvxL3 zUIi!yHm$!u*s1fO{+hZ#s?a`W4N`-d;y02vA-WQX&MHkjE6pYp<&jyaV4~njc}Al+ zqDv23>AK19!jj)EXL$gND}knvf}u%8klxIRynE=zKz8{6U#IupLVA1eIH1gN!o#V_ z8QwLW6TbdgKz^R&)gTRjI)JXBKh74KHmDh453ZHl&n&eVw9Bq*(F8`v6R0T`fo%m2 z*d!7e2yVwD42JM9{KF-JELx96HVzrh%aQPKj8xMjbFo(zgt$zAMd`sxWu0M8^c$j` z7YdJq_)Gg>(G!Yp$nj0a@Mm=qkV!QPo)P=^p)M_uM-Cnc&Se}Zj#n4C2VKTgs~EynKiCCS_{pP5SiZPcp z-IJ!3L8>P;#?lZlPtKTJ9@c18ee5sPn7iiM$zvxVJ%PAb_-y8k;Z>vms1 z<+@Q+zr0*gO+YFc`L-XpsuUL#fKZVK`e5VAEL&pjI&e|)NedRa8_1u>U*T?s6k+ou z=-rNQ_C1emr?+=}K4SM$U%1;VI4b^C6ilAwKu0GE6J>(h4n1ZXhg>2~E_+W?ux$Rx zN|S%oe_s!id&9L5CQKR@$IbX`fQb zLf9rO=Ou-_{HDgDXklMsXi*+2X1*l8Rkk`$Zz;8Ehq{CYxcrg7DI1!ga9t^qGfkGi zmD30<1tAcX82dknJDsqWZxQO^E#%pl0^mll-J9Yd zS2G)>j(=|2u}@W^McE8+wl|ndtkRIwZj#PGjl#85`9#_L#mlALgUreQ`!5D9oiqVU z;%_d_&OgS}|L^nvPalbzruv2$$|pEBNtDJuNlB4~bYc<)wta$WuyqhAhY2*eQbLK! zFfjFM0`ZtY$MsuA&1u5bB{gs9P(G*4&YSuxx~He7xKdP~tNi>=3$a44p0_QNtI6E2 z_g8MekLSKnD0fhIQk8;;{Jn%O0&%4VHgwEzeI-+xkcz^J60H$Tp1$QV%dj%E-9!bD z@O)ABBO*t!97(G9rmj5mcv0bLV-#X0D(x}b1(o^RBtwS9^{M?tQjE7Bbit|W$_)~WXJSES$}HONK-0Q(xy-d)__-IU+^Wo*)I_}mz5b`Xe9xe16g!p^Bd zKWF=QorFboFar*8Y1wd+W+N2p0d&0x5Gg8Eg4pX|&Jtk;@PR!VL4ph@%JQ@bZwhrN zVwz%aBs`gS!{kC>dQCzbHx_DR>Z zMlgE`#?o92W6btK{gQfBz;jq}Q$>Et=WON3zwkZh$n*63EnpPtBKKt0yFvSARll!J zTkd?bn>ar~NMQ`qI4E|Qz%&wHUQeNVZqdb1#Vk|?GQtpt5Lqj-f{fzH+Y$w%Q;_il zqj-Q7gH{2Is0>pb9Dc`bT@Ym!d2^DrmLhFFP4uK=Gtxz&&L<)y`YnZUb9!`UO}qeu z);QGD1tTOAJq+HN_GxZ>+^%Y$mni>`|FudtA2lFmA|mCAAWWg062?XTFRZ-$;OlV7 zPqC0!j&Ki1%A)c}s?E$X(&g5dODyP8ol{>CxP5v^=aeFpg^(cpQ zHk!}_ZdWBXmCs8$&*~znHK>&9-g8%8M01+0(9!spF9FJhkuSU2J^wV#Uo>J@vmD8K zJillnAc5UVfOFT${dfz>Bpa*Z_ie)Wlj}Dew_cT#mRYse5@*X_7%Y*S8Kra5@e`PB z^K9xMiA4(>-dOI29;z4%Z!X2!HIMCn{wy6vUSctnwdqx%5YLP#5D3kdc*5!xE`6+Y zhlQzKPzOtR_z>%B6@%9`Hm94%+VVU3AoCG5H)c@_aWvpxE3ljE9M{_nZ#|{u1a|*< zJwKYfSQ z%SK#&KAgXwAy<_0UKik=%NTDL1bF0D$OxmMZZ`?Z2LuTU6`XXd~KT2DZF?0x+Hwg&%I)6uS)t)r$PDVyHw{mMA8eqm-3=e zM$A*~!&K&7HqB-B0oy^*`W|`;xALPuQ4J+ZKfWdgd7okp&dwmDd}Q1&?apla?X7Ef zhYyu!wlO6SAb*mp->{7FT>i?@qjqNuP5m5w!^^t}po3aU`rwLE)E%sYMh2_$ysz@S zZN~k1#p_VMA@$W$|M7yhvxW#zi6-h6f9(0^Wd5LCzH}t%2;hncED^M5O(Zl9D@aXLLsGE*NP`6}t`aq45=_YC z$njTiwokGfmTr6<4_O3L!t-GA%b-^(BYE zlZv1d>XfGH=JzcsuDEN4+v)Qxkpm@_msrs)>qLaAqlz_#74&IZWBDjUuvl3q3s2h& zA%F);S_JAh;|z!2#erL$_MdQ46lUsp@b}{K$=d-HJcmNr5DoPtqkSYRcY@!DykV*k zQmovIl>`fBV=Ns>h6`;K1`i`&`4rJCU~XSTvBW6Ww#YuTm1Kya1vRyK<})GYWvMoC zvB*(5A52PcFOHhhwjdYIAJjg?jKaAp!v~weYMa!Buz3CI;UYq(#`C-7DD?I^ReR_j zw)D>It8QGA)xwe|#u~}$SaMb+8M|QK_yZ9Y|r?F=Cbk1hzWiAlNt~y1TwKu== zPgCatvti_vXPCS>T0ov6!PlpDX_0qq7|??2)WzuW2+(zyqstLWeYI1Z0Lk?6 z?@OzpKS?iA=VC)C!bNrFRb_1{N^BWe&`$r(EQ|>NfglAifin&&s8jV53?-?=<+4@t z@zbz`m;^BhiB@R-*)N;{p#ZR`lc(@zfK~hKQ>eOw8X>_=a0C>>U^^3^DJMvA4EsPH zqO5o)hDem}LCY4)Z-BS95P@k~|Tg61UMBt@0p$|&9{)JVjFLk`3+{dPFE zO(*BP#TiQ08r_?#X`!nX@BIvjLcc{K#9l_0v`X8_cbOOET`E>e>|$o{zy?qC}g zHl6NhHRo+*XyPVa=as56PLr*&yyKkvZ11%TGvnTWQE1uLF5!AXB0BvOv zrZ&s{?NVSAvrw1x@+_NsGI_n98#izEGV7&VC;_gWT>&6!-%$A+Rsrb^oHwcM!FEY1 z73nB~VEAEcTjIpnn=U64K)A*KtDDwQ{ho%c9_-fzu?=SG^j?sU}=j}?Px^o>~m}BittLH zPd~*}{Q4JTJQ_6i=KXhl0ela(!Jf4P%!ClqQ=B z6s24hQBbpz*m0qg0 zT7@q&B**WeoO0pPzb!}O70i{-%bYyd?vReRHNycLB)54T9V`U(i(cD^fsTX>-!b?ygj7>#0jbDR|4}f*=GQWp_DT4Z=1v;JK$j>bArd=`6{)n!Kx=2-#Z^eR@Mc^);Y#D3~ zp@^=XwEl6)<`?ik*JsdSb!@_K;k@@dqxo+kssAZa3ftM5T9~;w0{&|Qkesxk_B|7Z zd=dKfsRCs>L2@ccMJ?nZ$;m=C0cY+Hufp*uY?5680ViRFn~c?+Fh3StQc-LEJ^^{i z?`RN5ilH4w|6M2UWs2YZVm9;swzKmE^qoEjIPc0qWJ7h#PBGl1=-W4sPa(5<-Kg<9 zV|3_alHFjCe6 z83qPp)TB(QygIe$Qa7v3PkC+l^(jpq*N-L`P&Q%nIN$`yh{uO8A5$or@<=Hw3X^;fCo3DchsSB4y@*Xb3%@tpfBMzw`Dp`{8p$V zV}fJ%I+A~BEI#zk?jWC^w@EfG8MIH;N@BC%BGs!-)Cu%Yyzk;-uO`;IkjE~`DbK|{ zX$CF@K|Iq6?b6z36<+-`E_Je8g|U47r5md{KUUYAa%6w4l|E{-1zF=fJl+_z?1t>L z5cdJQGYqqpArk2hQopA5(p9HcDp9p0u7V9ybmwVJLDuf|$9f{Gm|Z48D~o?XBCx5r z)0X^cQ{0HjE}l>3bV9E*9>9*h9g*6re;ZW~q@+wAy=Ifz?@_78z1Rvf2Yt{`n1dYU zE<(E@7;Bfdo$SH`KEQ75l9zg6mw5sCIyZ6P%YP%@fp{g+x{Z69(S8tMNB{OW!%Cb2 z{rA~JZM=)l zk32Tep36QVI76^k5OZ*vyKdh6MuTK!ggO)?iG}fMzZ+kORH4Rs#8>dx4whu+*4RtQ zZjIOto09Jhmk0D^#{n|v8zSY!G@asPLndJG{*F0}K0lA*{Wpp+3&foc)SVI>6Beju zVcH`b!8b4(iyJ8}c?X0b1BE2;f;NtPMFTQ`gwnJUHtd8{kt0+cDeja&LJF!OvJ1wT z3!*`oF`bQo*>-^$MIp&=5=>A}Qc!9&(f|9e2oz;Y5F*<|iuXX3_!olFZWLqvno)0r zKdeJt79b6~)JiE-4H%$%N^$sy5j5a}&JmopAPpDPcZn+TO9WwrU>>yU#;g#IVvMHP zajg3B?6xw8Apd;h3@w?g&wWn=xZlnQx_@_5|L2V(>TY7>@_*rc$r?Wmun$o`eYK~@ z#!bdJ0vS*sL|QH|gd~3a3_#`{6v5MI2}JJwy=rzGMgw_jOfK-Vn6}W1omMv~1x*T* zwqXHcfs0+b~a(t#TH9-3VRf0gjU+ODcb?PKxhW_F{BVwlN;uwG;cV;XZ8aIKua7_ zXlJMy1IcM()VEeP7>ji>01ryRMX8HR$u%`5#9k3Gu?$$+12HswR*cj=axl7RJmFEg73VMB;dA+aPChS zVyT&r4=?Y>!>=aq@}*c-C|SKQ;l_CAaIAb*a+BpCu|%BtfH+FBs-@Yu)IIZs#y2IW zK{$c{GT14u<$_}4Em)EP#uOQOjF$etH#)3G;J)fjIwE}N5pH1iIC9B#HOghRNqjKg z7?G@5({()xA$F2v!&SG|>-Hd>c#&4=tY#-1$YzRPh-RApTs)e}et#Rfr(} z>!?YS`HCsA5mf4T00_glIx5vM-USPz2II(5r_2a~8}%r0lG(JU|C_t(tARpk(vF9I z+NQx&O2@2@%pWrGfUXXmoNvS~c64^#zcz0#I*LTX;u00U=gWOch1iRyfkn4ekABO! zOXxhAU>TukUTtt(gASQm;4CDLRNh|FAw^#aA%!&96yV#Q9D(>eh^=!M9b(7S)O59`M=ndiU}@AxxrL>%HmJ7Mj)3}m~3lSTE%@xmD7 z&*K%S6knCP2eRN{;#AeI9B0SzRoPuMzYh-Er1_D#=CV@c^Wnebh#_5=@vj zq{F0Fg?Out#3at4y^JWG2C*8&+D6biW==Us60EGHn%>(&9Xr4~-C&+v9{$2Sh>CW; zIeQC6p-yliB|1L)D2{+UBW++(FEGX*u;>#jJkdgeNm=0GlNq@9ZAJs-0{MVs6azomuU zS-wqEnYj(+T`U;8bx7pcZA3DX3?qJ@(taI zJU}iy>dvVjNXji7#1{?cAB!1xBz}&J>EVh$wBR>f^?lEt!=&ZJJ-7%%vbsQUTfN8; zR_YmU!<526M#>hymB;-;N_{Pfau$l>T#EhGcr>vpLBvZNx=xGr$zD2i0WdJro-}je z2|W&ng>R6Y%E}TX54Y$!fEL5vj5Vx`{ zu8C0`!7D%fqX|&H=N1z2(+JV7qx7w{@Ga90eMcQ0il=-*`19YA z?JVwGvjk^<7%yl$v1ttOqAxC)l<>0m?R=Vq>o?%PGK>Vs*-I{MU9!^9hF&9s~VsnYWwQmuP1;!}*o*fT4xQ$=rZgIO{+HB~0* zO=?jCR5`u?p!<5EYBh4f@%n?}818ELh8m#MM4kecEh=Y=X z+aEERk;5xQ6zWdCKM%(*yF7M|y?$6OX)X0C;?ZRH-GOL$K#ag{1uNVb z@9;vvtFjN9&T>R6UpbMaL{zwnfbAVPLHn%67`cD&BOarM^G-F3XVbmq%l-M*7<6CG z2o9ld6O-(*(rd}@VciZ)qGjC*n;9M1*+g}QT5x)e;!7~CZQg0rOXt#sQ|~dm1oSuu zOX`V3b+0x_uQn&(1AcO%j}kKl-r|sLK2B^tMhP>YPG7nm*Q5#GOSyei>@H3JU1=Y% zy6%7E>`GQt7}jupwqJwCl9#Gp2VDp26LxEczT-6;OU>9Dc}%c9^#uEkSal%0GUI*% zr6W7ez6f;NlWz&x{@TCCK-PxUzHYdk%yY{?3$ZmW~}*yQ6~Tk#7eH(^1IXfEmb+{O(HG!|8+#;)JQoSq%fJwCcRIiD+Wd>i}JukW{l3~p|Z zJoVvzjqc5Cu^*)IH1@>DUrgV>jSz0~{h^JBGj#)C$!{a3(}vqF-g1gr4Qqmz|VV~Cn-`LZw>;M9I` z(889mlL8Mq9mm;?9#J3NS6Xt5&saxOr;e`rBBXAQ*yH!UG;{C-)GloxkC^sy4U=;k z!E%U#(TT9rompRyxg+@G^{Lr?gxZ~zu`>x(E<}r_G^5pV1)4S3dcIo>*JIV@NxM>8 z7`~DRw!)1Se3GM;Rn=1FIzn0M9!8~~uu`L}wd<8*tfyoZ(N2b#$-_k!n6hpk-$Smr_85v_EsBbl6L73MenRiv$Yjc2r?4!pLzet!PrfhtC7T9A` zvFhDH+tpF%>B?vmS9kk+(+kOCM{vv5xKL-cpA`vRSlN@vsQM&jEYMMWT%eZNpFX!ws;YmA(Q?QCqm)e;#CTa)kgjO#bu{Qu}C*{T5f z1qBq|SQqCc!Y~NrA3sn#8Wa%}0ErR`ND1L2G}7;;>Sq*d>&52+pFurfJy9aa-z4*K zcYW=V)#255f$Q@vpW`gAZJ&pyT>KuODkE(nd?BKsZ-kT?2ItZ=OLLhf*rUa7n=@i#=T8cv%C+a{v z0TBrRK@m|{adUuSz*Iae+k41`Wy{8I{Tuu|-~by65NNHYH_bo8fU$BcVP`CN|HfzH z4^iMT1WoWv)RCLjaPN>~(dx4(-A?b~?K^;oM!3B&L^#MWgmuuT?``1{X*=wvA35FR z;fpeckH7?`ICR^o>P*c`zNL9fzp$t?4aA+N_n_9o28U^eEZJV#aR>lb_{g>fczhIS z4`InCE=@6}Ry))tEn>!Y0*iKY(ojQGj`6snWg-CYWOt%3_^%sZp|WO;m!B4Nd6=M6jy&BM*P25(INi)rtCh#<=H6uQ%39X(6xEgGM)v2+G3oGU*m^?V z^*XMy$9-f0Yq7ZtYt?GIm&+Vv18N|PMUVCqXx)A{p z4;FdHIKM9yjCm?`{C!GAA?_b8o)Qw>rDn?X{mk6u9QMRa|Mw?;>bQNF}vz{Su z7f`|ZD8jKwNb6MwBw&qFT>`xH)=-Gjh7nA83N(LN2@I+`P=Mn1F{H%h!EwBBi1t|4 z>m?Uem}o(xs8-Eaz|->pkp1g~BfN8qXee0+Ez>%zEnT-4b_=ClV3G1`=dM?5q<=Uu z?{ti`BHs8EQYb`$iQ!O4+KMD#$G7!ean}pz(j_1_lnUKGEvpU!lRux4>LF@_66cT; z%BNrJR9#8Z+zi(Kl6{=AWC!#Ag%EBSm;Q#Qv{4Vw)w`ip}p`C+#<$UGo3; zc($G6_^kye>v9knUhfNLnJJOj-^(fLD=aG?_q= zmJ+S#OIMV|Ene<>6K!Va5a>OLj&1h)Lkzgw zAO1lr#t)9`bZmK3Lg_oFcj8q5MmGGN^T1;Rp)lft1q=f8Hxl$cYi!>lbF3JP4GV8!?S{R}s z2f3}5>DYaFe}P~hN0<7xPSpU#4%V#58jK`lN8p#fd_wdbeFH*O4a=r;XzieN(GqI#DM=PLl!E+bdOOyH<^Y+9Slvn=26|7qM592? z<~JtPWK%pjsEzlNPH|H~##3@Kkz{}caVbh;MEPwqHOd8RG!l&mOH|^cA=9=@PWAUY zN|F`gF}G6-Jjn4hk24J$5}|TluPox5Ql!0@)p^t{AkSJpvt+*spQLO_-3_rsh@=t6 zu+L(Yo5wC2%C%CI#|5t`r2^JN`?=6s*neUa-RY6Imz_K1-P@PBjd$+haTf+wV``7t z%tETiCT&iav%V9##&mMv^mf0|u5-`aJn%2~LS}PVSvQ&mxydjWoX=HR)?#{a)h4M> z;YSpCG%#7Kcqf`U#&T1bL`9@4Uz_vP%_Szk60DHwfcNm<^_VjF1{NI~X$@Hv*}{GG zGS^*OIKn~kX5s+g)lMi^*a$*-2q4~tPNHzHRwu;Q3k{c4wMw@5u-*zka|!8Qz$;X` zq=8{=(y0O(>#6i|qUY|EP*c&Ls=*0o@1Z*Eldlwf9?snm9+sPy5I7ovmq*pm*H57G z=Pdl)T(R080a%>-z49m8o+0ermG@MRy_J5IPFrh@f=p+x#?j|5Q~8h@F^=!5L6z?J zoMIS9=52j9un8<4nR{S^konpWa_aFoPoeMfbBKi|XPn#jfLrZ{*>Utc^;_RoggfZi z%I8_1#~FCW7&-e{-GW$*6TW$ILCiq}u@gBa(HYXHJ;Y($#lj#91Vt3Z0!{pC7YsP7 z39kzpLP zQwzC%#dBHC8u-gRpC}u@8zydXytoop*svaf?oeSNi$sn1n_u_tskG)M-0@ddqys5twJlgu=>QOfV14t1LvL+x=Yv-lOr4cJ)pA0id;<7X8JfjKMZ(MsUB7ak zp1PUu|F;N-6iRAMU`DJj_5-bN4mp$QjQMnKM1_oKw{?at770-b_*_dO-Wk%ebh@=B zmxA#3^xAA58*Hh>tX82$I1Cw8R(<@8OL{bIK&665lIUc%JysoMs&k13ii4=G{;VDb zs7wql;Y;-0yx)YeNtHJ}j#EH{B!5v~Sf#%pvP$->$!8oT;N65FS4Ko;*5PxjUfAF# zPfU91Y9>IU<->g>|4VLVB6k(G$eA(uXhf19^E?bP8r!W3=1g zFOq4M4~{YMa>Lmx`9E*kt*w$lCBF}=^ShyB|DWpJe><#yX(E%Awxk9aQ25d}&W?88 zN7zs3SR$Z0V*F*m^7CdJ-m18lqu6LQ)qU9LzjH-WlS*j=U|XJvf4o0@T>>(~?(e~gTs$)UR{eLmN4`+Y(^%uSB?_~j_e z5jnD2GLS!Nnl~!zBWjrtB#jGIJ;w-@cz@HAyFAag@*X1l|S~J%y;%KuGB( z17kGdx~RTU8n_17`1<;ANiikjSj;JkLPY5V{(jQdAZa*BDmKwFsba_7IR5yG_b3&r zKaXX!(JdABbscYyUOR8w%Ln|QABXBd$2Tqryf>6Ll_#S312-jqHKegJG-c?H*gUoY zCzF~dUW^bT+(@!(t^~WNSnLCx_nKN0E5@EUB4N{&y}DW|7WF0 zJ=T7`fP=b1RJ0Gt^5$yzYN>q!YqLZN3Avv@UtOcX+^$A#KdIAq0<{J<_m?T$&PD8FXb`Pd10m*HYjh zI5IAQM1`~}S6#s6sD}i9M=6^6@Y19K7%6?xPC_`*5IRU&oe)d|0I{KK12#=l(awa- zo>oAlj=hk}RB1|&d$3+%+W4}CfS-{DH)3uK+EYV#wTg3o&m|ts!dSKfJ@JeHu+Hdu zo2INid*vQOl^kst2prB(-X)EJH39f_(rsCI!ygz_x78zeX})T+CUJ|terDul`v5Mq z4Jr;yo2KJ|!3?`k`u<+Cw)TKu3<;U5qBaP|> z<&D3RN-D2d1JC<*>%x&Y@}KN0(&Cp8#_iDG>OE!`GRmYUT^d>si`O2DM-2)Z`-}Gk zI)>9HGeKkqlS+r@RQw|R^4K*?k{mX4%9vf*3=f$N)$5(@llpihqw}^`XUt)S8LwIT zsiDrL%>JZp&H_m!XQsG<`{13My^7vzsHc&9HCK)Z)r@2gW`dfi&T~bO2JgC+S3pL= zK|e%#NuW*rUF0Mcf8>Kp|DBig7`bgHPN|wi_4CX3J38tO3t`yAcuX+`B^sOE3oYb? z-rHor)!}bR%ISJ1(1ITaFsHxT?6Tk<>w3A9dx!qcY97{^Wtu9CX()3}SD)ui#Wsn_ zfCsY)#IFkK*75`nqJlz|JDv4ne9lJb@@^ONdL(Xq25QYa6xJ3w|8erXCC^+wec<%E znfGA-LZJG>7(0`Eu*FCF2JhDlCDPHy?f)26Wt+Ic0yT5Y*kv6++2bW-dx8kVspxQf zUycFV9R_^!&hColiv?eGPOp8~J@@1Lsbl zmeVO=DcxuO{P(GS_C87P6cIp8n;0J)0v5O zqV)!{k~htwxCOut2=Orq^CFh>2em~dl#E`MhD9udJ!W!Aw`pu)I`P?kyV4A@MUAqo znN~1LjvR%Zk>V_Gz_(ttKO|u+eC({n@q~1fu}vn!bB^Rukpf6+CWR;U#(p0bce3zA5p+-#<{Oz{{Bg%|JMQ5e+koa`VNl&%i13Vol`+WbaQ7 znVUavrmWP7Q;3c86$>QnPVRt8a#cm1P9?;d!zV9b$}XXB1f6}C+(yk-MA<8m%BQ;x z#;ruU!lbr#{iJGPBK14omuDIQz7cY^8hTUE+J{OWY+&ycc4t!d9N6x?FnDk_*Y*P;k%T@cOO3b;pXfG8OU#rfv1|DLM= zs`*9(eptltPp8dMC&^k(+J!M)_=E5Z;QJ5uX;)U|6^DMQ+w~D>)s7lu<>#h$tKJ#o>E}FSKSsr9)0!KP z*QObn7#01eCoo2|COUxl=zQ(!#8JpQ`_Hr*CKyA1OM3hgCPLbqhusHxb|>2dw7BF|7&4bOS3i({a}YZsTiVolPCp$v!d}y}6!s6q!Bj-W5u% zUcSjFAX5_#4d=teKM797M?+mCjinrm082fcFH4?sxV zJ;L6SdxcBem`}Uc6t*B)r%O|y7|;1?k=)~DgJi=A$FT<^WKGxDLE9avk8thth%!HP z{#~)b?cd`AOm!IJ8G7ZoN%XUq*|lNwDa$%2LP_6;=ZBB4ovjX{=f1jJ{zl|0ySDLj zkx`vPO_)BFP3Hwt34o%Dw&gSP?=WNUqOBlly%r}<5O50#W z&N==FrtVb0+smGDT$0H(Ko^%0u}Vtk(^K1uYtQAUOV>#MO-TY{XF(}C^{(z79)pX$ z^N^Ip+4G=c6J4B*N4W<;!vNL!7FiB7 zRO$;3yw)mpMt6YD08)hZh{tmcqJ^)ze)@}m_BZ-m(Sb+|<$e$);5d-lS~>07-;+gT zj-|i4*6U=ul2fXuEWN6G%tiY4I^rZRXyUsZMQ(WH8&YX%^Ca{AR_^2#iZ*HYAg{<% z&2dM#{~hT&uF|31KgAv0j{wd8Z`2I`73u#lRL6f1UYKP0a|RYEc)Ol@vyQqU2X7A= z8CkDeuG97YGSR}S2%BQ?2#1;Efm$n*sAK1>Dn zQo)C8yZ2)D>J^ZkBo$>m*BnL4)PcyTEG)mtbG`4kMxXWr!~KY44F1xg$k60UDpAC+ zYKtZaSw{_fzYpj&2!+b5Bl~2=>C{_33irF?qgYyD-}MTcR46cN^M-1TStSX>>SEJS|JDELB*4 zD$_M@lSId{0}(nYzhqkxlBKi1jvx;81A?t}bRH!{krqwFeKoNgGEfB`D2_8^oTiyE zMFxj|=(}$Y=?`3;rGg1b4ic$m*6e`;f-b@#H-kOLq|pRsv0yrAvFv-L+gXgSjdsE` z-8lCpDRQOl#`akg2(HU9u2zfL+{*Qu`?wJHWkH_gu+E%NPt{a*vtygu!`yfvNPgh< zx#M~SXtGO&d0EvS_~*F(4xTe0Yxxs_V~CdQHN^O|vl#p4_i%1?1R1BGe_hPY*AEE8 zT-@}B9GA>9xwY8t8$!hELC|+Pt-z!^m$qIoQI*CF-ChG+Y*E9gQe0-7E&8oP^e!za zoMF{^W(g;#8UVtOS_TT8)~YUfP+4}CdD;<*qZ$1=!vSp=FIyJ|Q9i00>?Jw+J}7=4 z4t9^ExjBFBU(i8cDyqivqac_6D9953J~{-9O>7;E|GyHUDyA@!kByod7^F14o_sA* zE*}69LcCazV97%h|q{5r%$4+;ND7O;3MfW4tcL=XwjH}B$f<(Ag z*>MZ5iRslwtLv3xc4}+)_v3x_uc5!tgn<~?X?_|D@F-TK>2RKX8;N25Ze*1MeuPg? zKTPw!b)Z9=49wD$*mM+n7pZWboAi1$T5F75n6=6V5&Sdz?rfMaVh$!F&r*?&pl0U|!yLeCgfy861dF;kVl8S>L+|%4rp@Ev zF<}l4K%fJ`>YWpY8=STnK}^-$C=gg{$zkQb$v@()hTJt5faBXT=-EGpfKU&FB4$i6 z$)ALavbd>q9&|Y=%wjKH2=xGKkYnw2C1raR(Lnki9gwD(7e4$NS9^qt9yv>fDRzZ{ zj;7#ay| z6&BvYo69P#D0`2GVRN^{#l$B%;K@3VEN45^GD^NHufp8)7Uf=wXFIK_%iQP_bBYW^ zug~j01Uzvw%%_;Gv;`ET|$c=BbAf&x`YY@to5Rz*n94Te^bnC>YKx+7cBQ?sP zD%V%q_j;N~R6d=$Z^-v^!_D@8&)re3%q9*$x!d?hRZ{x*qm!V%qw&w#0Am|Rb0>2b zW5u6Ee;d;uwr;2YGwuJsj$>>7M>_v9S+8rbGNwjF7VgM-@VDZRq9-OQ3KNfa5SMVn zZ4yMcGEHBlepCH`?RfDIgB>#Uji=9AwF#A5V4|OV_%YAB&bZFVyyorp0=@y5cC7hZ z7whu37pPw*hZE=8@mtM}+lM0IS?AtI*KGP)q8jJHL#r`eA&k39FgN*}3kez-0(nd< zD_o_Z@JvoE+Y$#jP2WI~0^ORvAiOXDi$uDXD!{W(wsrZiZ2uA`3ygP6A0j%cBLFj{ z>j2k)Vd90pVg(ih8`(@>f^h;lT~*MsFqpsnG)FI|4%I#Qydr>of^j7*QU4Gl@SM(i z!z3jNJ3!2(b6dUWoIgFMXwt|9q*E~W`BgueD#&E#{_6Mg+y|fhZyO#9AxETY>=`CZ(09c6z)r=(}%oMmk=8qaS;Z^ zFQ|7!auk%4?G}Aw`(!6mv_GC|M57aw;&IHv#o{Ow<;Nq|)ILHcw-YB<=<15}3cW&V ziYYZ%2NhgcmK-9~u#AXG`J2NETBUzL3)B|^0z%w`*kVGkKmYV^uqyecb`*V4-W7@+ zjVIcsXcRg|r`C7>9U(o;ni&Me`Vfx$}CQa4%Y62ci;afVu?|WKlXz>Vef^3 zqE>6LJ|$YXn|ZAlU`e`#^A+;HzwsO2j^8&wmt|~`fBlmD_usgr&CjK^fB5M?j8)M| z-^utNrh2KS>4rIo{571^8O{JM5-n{dL7)+`awi={a>JQA>Ob zS*KebvRYryc>Liw?*-G6@Ssw9d?wq1qUHcuW{aZN5` zEE>Ci&UOA7U5MF#?OVrs4zaHd8RlLe4#GN3jtvb+IkL(hh71-Pk^#S2Wr=J?y~K|% zR-sUnCg_sbLoIo?>~CgG&LZZVZpI8zg;K(m7hPK0kWXr;om7Z(J!?%G2%*H##tcyS zxK&q9T-B?g@syG#W!c0hJYAT|RT?0=D4`Vr3nu2MBL{4wgF>oy%Mtb%`qy37o?hoU zwNHbG>=u6kydqNf%}ZE0h{!;hRm@7LlP~o$NblVtWl$U|(M%}5=S+WcHGfTe$<9NG z5sMWd1gQbc%Cj4#&e0V&!t!b_058ktNtB7yTx{Up&}bjT(~wBt!exLiGc+G(k)RA- z-W!-2VvTIhCMUj(9&4S|Nhku5cdb^qS0xN~e+YQH{x@y+dV`2EKzC&q>9^%DHuoz8 zCkBLQ&`e}{N+33Xa)g;VnjRXfq|+Xu5nU;I|5WICH2@YN`JCTB526uYBK@Z& zyrOG^AVcK#qDx$|T_$#Gb^pE)bO}{4mI&%edkg9Z8N~Pq@0Dw*sVXegj~kM15ZcpT z<&TNTtP~bfQCBfM1e6)KQ0bJ=iFSf}3HGuQcLmv^%BOIdOiM;lXJ9lVd+$8ppPE?l z?%Km>Tr9x=Z^O5c;9f*GWJ#H+fIa6kgkHxN!$g|F`*`#1@qherL0~bKbGFs7nkPTG zLw2bN7gxEJ+nN}^vRh5%|4`GjP9kc<7JrE*rC@HK!AW`{7bQMP*;;_Z>CK10iDw&> zuh2639Ed>bCWekaM-LlT@23d2^z|4qH3jSLL4?RkV2daQve9@u65?ly;+Y>gTK^LE zZjPLcQE1V{Wlr z+RfMl>anL=ONZJm5c?XgGl@=*xKJ8B_q(TV^S-ArKwEAyY_|_su0~A$!>l^(?swJE zTeSuZDW`dEeX%D>@x04erRR^ygSN&ga^C&7Yv*AGGmu1dqeFdII*Sd9`CQf!^C;;)Ra;%StBKIw37X$J^git@d#Hm)Y%-_!h;>>i$Q zg+zeTvqR)$^7K)Ov+PPxGK5?~WoGk87I~A3Ei@X4DSeN#X?5gQZcB?JmmFk&+ybxe zV1L%0!!>TvmLe2a@MQAkGkP7baH)(daHscydlJ!H!zk(R6jTi^eybWh7^e`lhLa#% zd}Q;1$jXo3$in|pSwx-8*~Qo53_0qxqq;$0fld<9<&*w~Vv#jmr;I|kn)c}`ykQ(w zj;ddg_YWvAgSDi|Z0;e=2_-!}@<~E@X0m~s&QatFvAOMN@3Da8xAF5%8G_nPCuaW> z_joSm1MlfQ5K<~T;|RFgK@*TA?m(xWJ$MNauf`k#7=RVz=LqRnVZs}Rt`wo()<4}E zTv$B9%5@%%41ZQ2Sz}unmBmGNqRE*Ksm=Zzaet2l>@Dnnt-a-CPg|MYe@FA~ihy~{ z|CqtvQo>k${EWV_tFG+XVV*#Z6hc^K$7V-y?ytl`_q6AiDy)&yY>qO#YK07*jmj4D z)HQSj)%Ee>`+Zvax(3`3TQIS6?%0wI_OW^M%)mCf#_*#5BAPV;C_c7BivBU-$?N@F zKhBG(nDOP6-KX>59UBiJTveW&CP$`2WTHz9$PLgnjFp~dt55uvg=7%-6zUFUDwKEE z`PQ^UaJoCRdk=Td_9gkH{oLf}O?>YOYc@dryXyKz#^9ayHcmE`natJ;))N`a8;vA4~ffRvJ<1Fx3(eFjbCg)7*jT z52}Qh#_S-3Zu|Pbh1+||%^F&E)E&Xwtqprw;2ko`M=~BdLe!Z7BsU^|zEjeB(jPv! zF)P64fL#D=bDw4edx?kI25Y(yofe3TXYZU3p9o8 zM&VAI6^~YnvwM|Fk^_^`grTw;?0y_?V~%KYN=oPDh6Ba+x+92XJRzLaIj3W=h*SkW z1HeT)t~DzcZa=g=c_O7)aRGX~I$)6$7_C_%RXEdB1&p6P+(~tvus~DOYw6ML{9IJ; zz?9R9mxC{<)65lk59N{YuA?=oTdOB)fKRm0y}V$_7AZG}rHjBoV)iN><^;y%Q4s4B zH%fY~Sv%xqFmlPeyi1(N`o*Gb7VNB$tk0HQeXxEV$B&qeD)c$xiYCPes!N2v`z4a` zrAmOLW=qWAPYo+2_rhkQL}6#`m7=@P!J2ksj=;hk_Vq2gsjRB7XUrk`M9x5M`BhnF z2{8?a)i_Z<^Xruu1nM#At@P0Z13Z2j5qIkx_-M@*Xt$(3gzu}I z=@1leGooZ(zBkGE;yC{&4YpVkQh%sAoDk4I)9$0MtpEiM+d0@f0zMbN0(50}c-&fY zMf4Dp@9f9U3@8U&VETDpYIXIr{91?3Npo>EEh1TSZ%v}yBVS9`!Y z0@1vm>@)~#{3HWpcdKC4<#1<%^+Iw!5eL<%v6~=RuE49QHaU;sNIEkWTZXUU16OHw zctG5Xkm(Rr0Gs}Re)yF9I$ik}1x%&D^Y`u#Oul|#@;~6;|A`M*eFx+Jjej>PTmO`n za6U~7I_yQ#aj@QZqCEK}vJYOaz<%GmzkbChMkC7QQabQEdr9 zxDIexFpn9O<8MF?w4a^W)Gff^)v!PM1;r7mWfPO?(Dp=VZ;{=KGDe5*C9dqmD)QcNZ(<1xmVP zS}@TdQ({)8*=5~p#4~*wukTD!xJr?`Cg`uKqTzrVJ2_=jFc2jLHQOwUk81kgNlzhC ze>IVq`R`w5&f{D$83~0%!!v`dYt&YDhLwwAH|F%>MAC@>?msfvx+>(TTtE-QNfsf+ zg{T}8@dP{#)CDS9I4mO%kh^Q;U!BJx$(2ulRw4)c<--sM&1m=R>Yx3QZ*vqt`-_b% zMs$!Un1&NrT0=U@AgrLSBlaL`MB3b2cY&F3vA|-G{n^NR>Z!7}N~V+~k`aY3+D5c$ z2JLk4+n0!5Af?FOb_6sYR?u%J6_!cVq?If#^CR35_jyxv1SaX!0dv(AvlmJ`EpkhJ z=%GN2lae39!e z%9A}L_68@D3LJ)KnH-H^%Q>ibOvx{G-bkcy{nTTmqkf93_tH=WPpMt}x6+V;8!$oN zf9a|216S&u{UFKz2T8*JekJ=4lKxem$yE9WLr-fSau7%m%5p-#3lA^XAPBIkRix9D z=t}WtZ6#L|2rsoAU4?yvc>`&EeiMf6*7J_Bd8+cSm1X=KIdU;^Jx(<>Iew3+-u^|k zCyWFkK+#7pWEY`7Uq^{ieSeobWAUMuvId&IeBQS-rYtzF*@(dk8y-x)Ww#Fhv|zKz zOcj6OY~#3Ghi+4eUbJgN7pQRBN!E67xe0Yk=rhqs!AiMy^%YCjF8_d6l*by(^T0^8 z!Bm4Ai?0D+Rm}+nSn4zjIb#=9rh-saW$kvs83w(4Mu3w=H?PjWi z#$0T#DV&o&QZGUO1;S#hOeGA8H$rYQXP`-8P{!vo?GcrwGxgbaK;i7U>h|(De~+hM zhVRDxD%AfJY@S|gU#)?WF*xvpJ5nDY9?D|%9z6I|sTSF9%SIg4=$kvP7?&}@R;Egq zocrXZxO}4AbA#oe%^a#d1O(Zd(@d~| z2R+lQo=YAi3>jz$i!6Xhz`nJ>p(x}~p3DKQ2; zSnCF~XGWWWg2WZGj&bW7oL`cA$zP$C-!G52Tt9h`Qc#6#6~u%e#2^$|N^#|t!WC<` zWCe`Q=fg8J>uEIkf{RCqtGEq>I7wUIs8j#azRj2z_*90)1h+ zfGj1q+euJAg_!%uwg>A-oZYMwjDT^ zz@Ow%+&)IzZwM9cQp2c&K-Cy$D}w}rO2deQCN7lgJpI;aE|Gt=F=6E|oUZ?w(D)B4 z6OsQZnEtnnRH!H|yUvG{k(@#VZ4TB@5actPBsd2MA_Qwq0TiXwr~o8Z>af#9Pgy^r zqj?T}$a`byQMT}dbl$QvO;Q8_JNsMXBksxce=4})lmWr)AK%6V-s2xpoLmh+2FI1$DOm#Xfi(46e z_kO3Vdxek*adKe=&|UvoT5RRgDdKI*se&{uillbIt0sSH=dD4|G6BZ(`N1}L&Ift* z5k}^jO?N!roJjlbnp4=->amSb;M5?#M%ycC88$u)`~dZkro>yaepdmHsmq*NnioNx zq@F?tjwSq(fNHC4kYWi8_nTL-?*qEIIdC!$+|BaC!@^EC2)!H6jzsGZuv;j}&MhS$<*+c-;R{%dqn68Ol46tT5iXh}qhs582RE|Rq~jBi z?0s#bR(H5*WH}k5Z*;N_b~TzVW0(D%%H_YX71{1_rjDN&i}vF{_dn=n{}W^X5yJno zo2_*I|DKJJp|#RPDM^gbQd0Wsl53@Tt06}?GyudOU~sctvzajH(!8oIhdW4mLrVk@ zM#A$4R+y`PlVIhm20uM>JoUBfKGV_U=y^0o_ZP|yQy5qtgaO6g34Q{0b6G?%U6XVt z&6AYo_d&Qwzms#m$?3REC(XrJxN&-Q(0S_Bf<}KcmglA`b-Togy+1jAeI4Obpbnyo zvMo)L;sseQK^W4tW`otZuT;utldVa!z1i|}d!SmBjASZ5!d)O0!p4N}U5^pn7@D;W}Q zwX^9m^91S-AF{*j?+AojNN%!j8)x2iFDllHrQ6O#C}JxY?nM;HT@TFZ0hKr$#_vN7 zt44@nxU=5jnPTbc#+$gaQtZOofHfOFSu}x-T{?o{2mS3t^Jnl^IDrT~5qY%F8GqJg zyfoTTE?}=%Tdp1vwy@j8FuT(}u%n**d$iNh9ei+?!5--lYmB|JOm%H$3uoV?Et)m; z22XR%^ov;z`HkQxU&z`4v@_C+DqN~h@r@<_9YipI)79Zni%a0em${R4(HEoCEr}vV zMu7pCj$t?bXvO3dwjetj?6CN+gIX>`%H?bx=$ z3M4tv8$2`IJ$booFkfRTeW;kiZKplh|6cE^eHQNa|3Ks8r&;trCi4I19#Nvw=6?|R z@5{%rUi+Z<82>?kTrg_k=sR>p0ivih5@0C{u?K7N^rxCm%d4uBZUEV?*S-kazh(vx zi`}DWrqzkRfTeMteu&5u>wWs_dUkiW7hsLQGd2VPh7zFs109!ZXZRpyhZeF*CNB7} z$Mvm=(+3&nx8T?}Z|@Jzivr%TN!oOt20 z)592+H`Eb6@19B&C$gpMw_E}s_9q?F02GwoO?Y>ZxES-UlD|c$(u`e*e(^ZQ{RS%V zFlw|d=u>QqP6Z5Nf7};XzeU*!u^S8NYicMaT0dKW3XC6czg3xArgYVO9L)&$0J9Wb zWZ;&G-g3owr_xW8Xcu~{f^+h%>qh7P6GJ^X+$MGTsl{~Md%XlidEL})l{z+^G?h@q z=45ot3;R&FCD}~DXYmmr+|c&hW(0#0xZ}enoptR|+g$P-;edns6hJ%J8eVA=uq~=K&`Zro?d<1#mN#+x-c1fUX}!HB@Ulkt?5Jye&O_Y#!`NOB_2~fT9QBAGlt{0A zU~;wnHfk5h*?@qnG2)Z8@A86gRYNdy@Ssh5pm1AQr=Hl%d`_GUEx_f~>=ePMn8@)| zt~ZG=e_Yobv@u$I0cPbdsa+m%lJDTBUEI*W*ehch{HnYHioE<$R#UoLm8GMGC;4aK zd1K~xLS@6DWyQMsUHZppV{#8CGemQ-1rLuJBc#5m58aPiU8 z$Gv`*9V;0(xIaZ@4K5Mp8s3n=X_Lv*ndfX~e;gj$yDyHuhBSYpI_0GZ27ka$gxt#3 zz)3_5fnn$eK2X6+$rVvC6Z$E;Ph5Qd%U14Z$y+@54^|j{u)_E6vy1-|udtoxgY%AO zbGB1V1H#CWBa_23H`9W{!b27a=MxBDli+8!TPG?XQ0|Ccvhn*K2!a&Ddini9Hc*kN z^4b=tQD2}xZ?yGU%RRS+X(s=S>Q`31KgENX`A~jW5gz5 zkiziELK)Pu#lny~gbPBkN2P#!k#waiR83NBg^bm-`8-h;s2u*G<*5zc+3n1=;ERky zP=}l)pbD)i=t+u8ry99EG@8p#0<8L_h=S)W`mOBS5P_~M?u43n*4^37A7sSw#jPr#5fMq zLMZ3PlMlYDbtq}_B68xtfh12h`iKL8&Gw`uTYcI(q#NyWs!@hMris?lWch&%woEoL zh`P1XQR``vG%_cnS|_7BJU9d9u!bKnIJKKXX`jPnnlKbrY1wkEMKt+3@yX9=1t0w9!1DBNbr@3lIY3#R(~s{yK9>T*t4Vf)^xq`S6LJM@9Q^A79O3Vlw>x9EDjbN^B91@+40 zN%IpDvONEzX$O{Y`k_bt4dL`2qH!NrQ;24^c=M$C->*|L`q78h;282g{}RY) zlXDx~;D7ykApiGvuKtPO|C9ig8lHLyi;3TvMkZaO2Y>>2BX|-A_~K$>LVRL)Z~$h3 zKo&?Ca%agC$wqn%h^8cbR2JTgrj<*1z_mOY1(i#01o%U$n<|wtkBgO>&1Y*amse}W z&duRmPp;Qo4DnC`cHgPj?^oQ%8;(=l$F5VIqY)uEz7RcDt+b%(BP=^~yMdB)j7T<= z%^a0UF6|xtYgVA+&4HCuGMrjl2sg>g!5kbFIkttoX4Or7@SId5&#RgG?StWOg1w9S zzI0_EkSaC}ESiU3a;-QTm`{!^64Zw*v4Ampk;q_yO&O>gNKn?`RcA^Y@O~AFOhsYv zXyj(p{10G6%~@#m$~O}3NY72gfF9|$Vh@_ z*d-yG7ZY-^Dbpv*Ie`p&%0-IfBsu7{ZqVP7j*CU_QnSZ?GDFIpv`u&PBft#IhFOg5LGtzSfoiEu; zk0=bcNZkpXklESFBv<3;_(*FTuz?P`Jp8gFT3cDf%6`^kKU=Ucg=|9g?s5$Yt-!dl zfl~Zhm^ZVeQOVol3kXwIlm3f#2Ir?aScD7>PdilT-c7@!lNjMRW45D(c&sliAb}Cq zW7bRn{j{X!Q=y2Z%Q7423?M288zVg;6J-K;Tuj;lf(Fa7LkU8|q@SxU$1c@x7&BCY zTRig8Dqx;B03Pe&=Xpzrra?FosCysF%j@+#ylJB5?tslpNt#|Wgu>}#{q~5 ztlYm)Rg}mRL6{Hf1Ell|e*YpLXy#j%uLXWjCo!>dyXM9peQY5@DljoV3WidGAD$V7 zc{XU$KSNB_kqG7$SlD?`aI?t8qCCt-Ya#;%cz$Cet9KF|i&u-%U{CQWMZ&6|7=(t| z9=(f1?L!zCLS#r<{im290B9vkql)qu3gY&h zcfS&)00Z`*&X|}I`aCj{$;7QQ2F37t^GZ;z~j;5@wH%*kH_h5O99KbNnyT&)#yr*%|@O)9Fn#3s*X@xEY#23p+Ew`e-ksv-+2Xt>#DdtMwKHSm6FX1tF z7x0PC2oZf%VH~aN+E3A8zd*k&cYeOHw-|h$S+=tMiqcaBP>FBv9()+G4Y7$oKGynl~al8b1|FBdDXMn2>hBP>Aaw;VX3h$v51{J@k zdgd8qb5FBY6sU+Hie)3DXdgr&Du2nkcU=i&Z;nCTPZE8NF&*A2 z0^f%mo;1KE36K*oiQmF|3^Q9~q8BBUiHsr2X9^ls@~&MarC#V*R_f6qQ5!`g2P-Tz zme|nI&mrW`Rs?B|Xs~r!y-6i2-itT1tFaTas z)9-$ZhULh+^eI!O-v~gE+}&~HuxGin^Rk+pnnBTE#Tk_)@c_x)m@`@shT45~{Hn#E zBeDoXdkejiWEz$1a(ge2Cg;z#Q7nvZ$B|G%YJ^6wfak@>6LG?D7qpkIqbAn3Dq>`$ zlsVR0#&*VKWM9u897w+a)-7N(UMUVyb2TNHV8DK79B2>UhDTbe3wwn#)pnie7=|~G z2ca3lXiwYMM=b?^i*xo~s*y!sD?ST8?Fd~I0i2MN;OyOm3JQA*GmD54nXA55dhUsx zqQh4-Ps$m^^qL+3(-$``oKa=&7vE>o`>Tw)cp$9f!M&)NR_kmm)l4Fx!*S0?tYq4x zq-pNQ*wgr$5odD=FQ9*h=NZGNk|_QtWtJA(Zc;-r?APiT9esel#6px(vKp`DeiLi28mAEk3mUVyNS|Cxa0qq+ArUr$OO=~ z(Y7gMEXxTALa@nkIk?qgP1kO20>`(2T(FDrWB)9iu!4c+J`mp(>bt6Q3pZ?rkY4}7 za!(S;2#fWg-G6PrH2@naByd=&KX}8jBjhcOT4hD%S>Xs=u#5M@^FY}=aZBoRfShYU z7G&qZ5lBS92&TKvh^AY#KYwd6`}<`=)z=2-Zm8Un+b3S-b2kFZ^v3dM`4&VZ@qw`W z6h88DFiyB$Yj(1#(ZS%&3zb-6Cc$(moJ^?;sg=nI@M=XzqI*Ox) zBbS1OIj9ky@|xQ*htkA_a}o6zCJ*|+)7~)E`LMbO7k`r%*EM<>)pSp(?m0Z|dSPDe zeIh*SUGb`Mzeon#O7&wlPMwcHJ!e`05yeiOnoC2+wn5*eirTa%hXUiO1|5Ws>1NRu*i9 zZTMIni)?-|sf#I{Dwud`d%wt_xaeiX9fntzH1^Z(*HJ=Cn-7rF(w+}vBRQFVxq8|) z6LhtMZmUjHC)!lEgYvHUc~x`j(+%5z#e6cWI zXwu>ENclx!H0)V5>}oai`{Sn9z;AwZLjB$dg|QNaYlrVMJQ*fc##t-++ zfefit7txEz_*3bC&{hbCDoNi&4?!C%qD66TpK3L5I2T5FfS^Ubyid^zt2z$&u@!NC z0KS1Yy({95%sL*@BK#G6c}H)PJdG9_VkfGC;&S@p`$udBI(cDC;b_oBD$ckr!?r!O z%VHwO#KbD8ZQw+$oL+q@p?)V84CS_^HN$;A)P(!|sEz%$*`Nc@G=3@vhF*u2`dv)L zZXyW7T}$O|vXp@$*3YXv$-`vR?KPiigyQHJ$z4}vJUJ!cn5aisk(q&a`fvo`0J{$g z&=J~-&Ti`W1bt1H-O~diOirjRsQj=l;5v+4{%6Tn4Apfp8?hy?mKXNO0~<%y@H2BA z3yhj$OP2s-{y=T=FScI<^b_srW83lm?ji{H4h#`=!J#YC@Xy8w__{xK9Pq2}C)9h< z9ZNCZ5G1&yGhE`oJlewCZs&DADsrj9guLB&AT8f-ApqYNB}CP6bqJy8*A3|clNZy2 zbC)T#>FI4L=)VQ%F&I-YyCuNf(*=C`vFHpG96H$T$t@Q&u_m}EHgTLbqDFOGi>xmC zIgy$`<3WlvgJp2Lrq8FoFL^$)%H@K-l?<{8-N;rs^$&bUeuuCQpP%+;*CBfgKWP>F zFLaZqC%Vu8i)*yUFKTLW=J%8`y!Lv(xK%k$@46z7f8|oe5{>g82Jv7mU3lDv6&eza z-x86q8as+$`48dHo5?GiZ{*O&Ng6_Aa5ET09t>VM6wx!W?+F-Igj7>r#(spvqdCya z99_$*#o+p*!sesJ=%L^YS`Y*VbqYcH!{Yw z|Ip%ci(a|yIG}$w*+-4MK`lg~701*`qIMIu87jX6&`D0d0lD3k#`a{bb=wXuoGYkH zqx_Ovf;WV!axc%IMWwUl?NM6oS;eYyj`I?puN>N+ID|}HuMa~oJ(IExAWmY6PrgkX zv2c%i-N>8YbH5Fn9C?9#7{Wv#+%9Hc4G~fI>$Egi2zo8*ap{CROjX+4uEh7Ug%6K}MjOaE1hJt5{zf zB!?!!hGf=bjZofP;iVc?`fIv>Z<-I}vx>VH7Zxb}+Ftbh)|^=|dw2Zcpr(&!Y=E zsZ%n6aD>F-{&l@Ts_bfy*Vn`6(q-T0iFa=bQI->L!F51V3B>Y!xvI_yBt3`R+S~Pw zwx+W>JJV;;QLY9|#lNEm9keeqGO%+Q zg&G^$<35jQTK}KY&I6pvw~ymyDaXpON%o%6u_a_4WsgLM>~ZWJAzMO9b~u~{LYW!a zNkV2uQVH2Bt9T!O|2p-&rF!4zx?GNN|33G=@8=%hpTTO%`v;v#2|3l`GzIcbbW6IK zl00s#l>Rsqa?nXZi#&L%YugFEZG#9dG+qhIGtca!)IF1WXebrh_#P$uvc*yf>Ct4Z55-Yv&AeQI2V4G0h^Wj=$R@mv6B%WT-jW&i<`x_okJm48WOy!aIm*ntUu<1lAHI5M2PDgo z_{0_GEO2sWjr0#za{gJqx%izf!QM(vn!b%Ec|VPR0p&HmYghT6T;RYaqZ}%>uV{n^ zsjCwOnlYz9(H@W~VS`{Pg`-^AadnSWQXHphkgOxHvWbPj6)5Mp+eTmdcBub!hP97a zc}Ml}csAcuXh2uh+6&*&aqo<^wEF=v>)obU(QiD3u8{VTc9AJwKiL8tIaH>CNM^|R zE*6W=9~NOKv2SA_Csf3jGz&iY(wRPE(PbJmPEkZXR>LulR zc;_DNTI4*M&9!`Qn);naO|f#YNUx@qYC5$;lL@QHO!uPLC&I>l(o`pSOS!=X+xDB# zkt~+Z-hNXnMExlGMz9zvQ(uX3^S&Of4&EdDY^f$M9j+H`XTx@69XoRkuS393a_m==u|Dt zId2)!vg3Q?kII-79TxA7V2W}&o=!!ObHv42qeaBzwz;92B!?jN8?S7tQF^&#GScdo zFQX(rIHk{cMD@lvT)eNF zH-vgs#qh0Uzz*x?rd3HGxm()36fcxfDFBNS6)*(Vh{B7i#u-<6p#jde)>+3RZ`65i zIw>~jGPE5@`_%lZ>GoA)jrz4+8U6e5(28NN%wwG+i9|tR;^c`oJV-jnCZ=%Lt1O$7 z&Bsdhx`j%2wwn#fE}XMvFo4CvG>0gLQ(0Z#D+qDO%D&vvu~^KSs(J&T^(qxZLMie@53c4=47DAb<};jxYVF&7%bxU)#cNMsX~V-D5WdkOP?z@ZQnL}a?#g-)_$?A ze!9)A=l5H)?0^wU8%v#vuz_1>Q{g`LgYz$QvcA)7E3h zBRkG+^By~GE%)w)U8^K%t8eFrx!dEEW>87aW+Xzg={)o?GSlh}|0e?V+_|@xpSVjY zF>x)xAn=g}L3d&+=HRl$#`98au@N3hm#VKMB$%;xCw#^=8-b6+Jr<*Fo_Y4MR!=Wr z=LAdm8`sYaW+o>E^tW#H`mZcJ^NywJF`sO)VMQRY^u2JaL*=SeXFKliKsD#e-Vu7? z!h0m51>UMXa)M-6Vf!V~2+R86o8l*|i29{@`2`ohK$k7URY$Y@u;O%+Cw8vu5&4R4 zHsgA$9epYIyf?sUvR2_m!_tsc!w^A01nY~gvPU<=8Lw^AWL7xECpp#zTcIbIwPxbp zGCFVOO7OTk=*Kafrc1wYmH57KRrnGAM@-&R%iJNJ>-O>fb4+S?(CjScCgQ?mhjMXz z6jWt8yINi=@KVV7(lI8(n`HBjj&xP(df8j7GTt4!Rb4^+1>O04-W8%0?i+d`TDZoF z^C}|sR{96Ubx4j1{e#TW7Fp&|rcKQdd5S65^@3BCNt>e$Ycv7T=zzM|I`MFGh+%~2*t!7$%yzvw;ndAL z@O#RSt8h!!C0b)()*5g4Tw3Kf_+(i8K6MZHG4%T0)9FxWQ(08&C%ZaqVEDyz{WMV( z|BY>4t}W)VTP!#DW1k9e^&iR|Zn%t+H(c2^BK@*X=0AK=sbZO&k(6O!z)%p%@uLoQ@1Ypo;PIBDlyjAtEMc7F}>>)X!;ITqc|imoqix8m(@sJhksipB|w(hs`h98-pOHeO{Ph&dZif1JtIlk;-@@OjNr|1H@c513SXJXM?n zb~6#crn$uLb~FELgx))P_3t>XqXt}{pq4Hp$>j@9z|qAz;TDjcy%?ytd>_lvhXJU#1Ho~DF z`R=2nyF()42)R|G6r#J2Y)vkaT`y-}35a^_cPsmb`CH1D%O{w`*Qf9lv=3gm%c4Ae zkRU+u{X_CgVd-RHR@B(Zb5dy+VMjgVoY&{aYFbLBoh`IRNk`;6$PV}8l~dC{39etpqwYoAgWD?XMauJAGpmIhDCVMjC6 zl+#TwMradXW1w51S)|VJEj4q5S<-wuhA+eXX2(sda@u(=p=IE%R`7x%6@F`$`Nb)L zA6tRNjwaIjSrP~TGiZ7Xs?+FF>q{fT6CPJEjNju7Cq?=TMY>jg*SXgXYWK{`*9 zBTePZszJ`Cq3_NLA5TjN8q0UHcw2vPAv2oqwvd=6`C~Ekm#qLa8g4q8Zi+>3#&L}X zRjf=U7zL4sbB~S-tx3EO^*b5o`@oC-x=Z!Ra;yAwz2R;mwyP-oRa;?*cTYi7xUDcP zbsSXwgMq@S3}KY&_z=AdBri@rJ(HB@726qo^%z%9!Bm)vBzdppInT*qmGMbp@%$Qs zL5&V==01&xd;Yz5Md~wG)2=jNlWgh~SQc4-{5;@g?)Llva z=NKd&C2_1c0{G7Eu4M6nP_6DTUTQ3S+Z|@(#bC$85?Z2F@p>G0?ts+NNUk39(pC4Z zl+_>+D(n&(5AsjV^RRojkk4wCCbb3#=|A5syElvFYal4w6eDu$3lBSg(hWP|8!AE6CfK=KRJQ! z`hu5`WM|O-N%F_?wsWi&i2=%+9Vl<5-<3DmXYzM%I3NwzoOro2LxRwj9Y?j48>egn zg-F9zgiEdu8nZiNv1jT@-WF!J_&5S*yo+Xyp)cbcwM~un7&tf(mh3br3}*{cSznl0 zTicS}aoAYP$f(8gYt*E`hw^at76uB-n{tni39aS6zv8D6=X!s)+^bHCQN2^(nVWi` zN~=Psx(SrK2u)=8rf@PHQ0^P*3a0|hI7=m22q|&t4DJZqv)g2G%{1o*hR`-ds12G# zdQ{CM4=y$LX%`D9>3gL3_;<_^*NY5ElXQ!dSoTcm7PdPr<{5f+t+L(EsYxZ3m%d*6 zydDrD^P{KE5l=Wua@sh8*EE7nnj*qGXYBqN84K(6mG$^3_q>p3>Q94dW1cb-8c$e8 zolB0yAyr8-5{Q*u$vUr$iCGCOCK$NqRl4L#ZSo55r0o>-wLxx&@GXlLYvQ!YghjYt z7q_&ue7){O;$cOJKXc2#Bz(S8u&9*xB>7`PKawq;^2b3@?a!`>uBpFSG81s$tdn- zoJ_-3ing`CYyQmH^~zIX%|eni}SY%1BUI*QSdvQ|TnD zIg3gVYA9W1zClB^(L3qvKfNQdN$uMuP%}TW2n}h5MB_a?MXk@96A)pX9Lja&oMBWn zjVj)JbL*_{Z8q*$9O=8a6Yx}W1S17(bLC)~2Ttmfemwy(5{)qi>?+MNQJynharX>a~Ic!HOAub5&z@%xc_bvpumQo~>NDFm$noxK*-t#U9>RCh0{e_r| zsq8Qr&DAO;&D(vq7v__Bv|Nj@^~mB9_`*%SH2@c z>dS{qN$L;IV5u;22TiLQ&MCy6n-!!h&w1cDd!Z$hM|~q6-4p-R?k3Cocq#roI&K}d zeh%jIGV{PohO?d-%OLv|FZ3p{~T8EywKAO5{edagDB- z3Bz@sL84wX77az!SKlhp)9ES?iAZb9GH$I*z9=+4tM+J>R*~`)MTm4(({YRS_?%E2 zz7XPxGQ(^qEbB9FB$?~X<%NEmITlAg=~Tgl=h2Up>r|uX`3|HTK|a?JOuVIx&D5ZH zkk|RZIkxLvRxGb1HY_u164GERH#;q3JAYVPoYq>~{tGgLR2|nD_GxL3%9^nIe4Zj{ zh&;1NTiz+*Y-RC}8W|4O-3N$6i_I)lVU6? z%kt{wjbz_#R8tOjS~a!v^v_&(SFV-c%zLY3xPxp|O_FhC8AbIVJ|~!1B5l{1#3Uu1 z76TBH29mht(|1spqE5r;^B?8dE4SyMJXy!%Hvyk$8~9*zxXPVe2XJp zH*K3@(c-yvA&ScE^5&iqoa$PkVToZIx~^iqsnR%yR)kV8?n<^<>#|YNyF>XOJE~|s zRo=4k01sp@%g z!aaB_h2iuVNe{jK42gtPsvG00`Yfjl)Pp154v&xJZ8@4o7p;L#7uw3eZk`0Xm)%)} z!vEFHIJqE!{>RP6!c~ozyH#CHoqtiis2$F$*51u0pvuds%=cDR;F$m)R^$h&oM|>n zR(31Y$3Tk7I%q-;C56?8iC47KG}AOr*g!{!jRVC6`aR&pqghIv0T8Wk&!KOhNU>&j zFK=Kv#=T%rAQ-_v!P2>tc+kKDv$&C{H;HQ%W0^bR@Bzvho9aL40Of+ zKL3CLn?M%~Ov`>9RX`Q>yAaI(hS=-0_YDXfMf)zG+K&PEy8r#SG+#qw?Z(vSXTiS? z5Br&6_`yuker9$>Zuc`q@`I@vz|^lv=zajD{4K zKpDsgd7wE7f3*&LM+FYk?gLP;utd1qyD0!NP4-B{MT~+n*!~=$2!|kGe-yB<{eyis zpg!-m*qFCB!p0n|<6L;c6{sZ-sQRakF;Nvv9GowE5~{3>FEysBQG?GUwSXh{jVmIclVt-=y=lLP&D59hV)FL*Z7P0*h+c1@Vtg0ietbUaN zW*3gX)ByZj^8)^@fjs^8paSXD_c00Io$TxHP3?CZ*U#hIy|(6u0y-QTActkXncmHX zGGOrhWwkhqaI=|+_+Qk4OxcR+f6&Y&cu8(s!tjR10McNXyVK?O!z z?Zeo=a^p4Y)9qd{egZU~<6kWT-%){$*83R!w^)I9a!VU4cR)?i2KkHf1v^Lo%0LC4 zaQHpoM_1|K@&x-9#MsvM*~j+okpFv6gJ2lg*BS;^cF=hXV{@JnGL0dsie_g?`=| z!43p4jE~WO&G@xD0dUg5YUUVhPwfAO-OI>-wavjeusk#dw{-olar=lxgSlYgR}44y z)?ah~NCp-h1F$S7CI)UPe;tGUL^{D>uxu9w+@Af{;C)2Az+A8n5{A2y``6sPN+tJG zRU|MNEE|IX%iQ}P!C(;?FcK`Tfk8&)|BuMM-r_rvG%ypa+<;+bJ@_A)-|IMlap2qV z7+iJn|AqTq-h!_dW581-e+~X^u7YovVrXZ9myKUeAAj!JX8-%9;2Z{Dy~Ci3AO9`- z=VJ``NEn9T*Zv#Ae{DbioB-a^#t@G7{w-m@5gEMchv8ft_#NlZw*bK<1m1eWP;~}> zLj}*wzne?_W_knO{J=n!hQC|H*jt4FD(GKqg@DrsUI4|Q3f}xbD9m!`Sv7p%t!CHR QA`GhoSSdO?w)@k603mV&bpQYW literal 0 HcmV?d00001 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..7918ad7 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.13/apache-maven-3.9.13-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalog.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalog.java index b296bb1..174db4d 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalog.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalog.java @@ -21,9 +21,13 @@ public A2AExposedAgentCatalog(List agents) { throw new IllegalArgumentException("A2A exposed-agents entries must not be null"); } String agentId = required(agent.getAgentId(), "agentId"); - required(agent.getName(), "name"); - required(agent.getPlanName(), "planName"); - required(agent.getPlanVersion(), "planVersion"); + String name = required(agent.getName(), "name"); + String planName = required(agent.getPlanName(), "planName"); + String planVersion = required(agent.getPlanVersion(), "planVersion"); + agent.setAgentId(agentId); + agent.setName(name); + agent.setPlanName(planName); + agent.setPlanVersion(planVersion); if (mapped.putIfAbsent(agentId, agent) != null) { throw new IllegalArgumentException("Duplicate A2A exposed agentId: " + agentId); } diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolClient.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolClient.java index 3c66f5d..ee84127 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolClient.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolClient.java @@ -89,10 +89,10 @@ public ToolResult execute(String target, ToolSpec toolSpec, JsonNode arguments, } HttpResponse response = httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()); - JsonNode root = objectMapper.readTree(response.body()); if (response.statusCode() >= 400) { throw new IllegalStateException("Remote A2A call failed with HTTP " + response.statusCode() + ": " + response.body()); } + JsonNode root = objectMapper.readTree(response.body()); if (root.hasNonNull("error")) { throw new IllegalStateException("Remote A2A call failed: " + root.path("error").toString()); } diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java index acc2d0b..05e8bd5 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java @@ -47,6 +47,7 @@ import io.dscope.camel.a2a.service.InMemoryTaskEventService; import io.dscope.camel.a2a.service.PersistentA2ATaskEventService; import io.dscope.camel.a2a.service.PersistentA2ATaskService; +import io.dscope.camel.a2a.service.TaskEventService; import io.dscope.camel.a2a.service.WebhookPushNotificationNotifier; import io.dscope.camel.persistence.core.FlowStateStore; import io.dscope.camel.persistence.core.FlowStateStoreFactory; @@ -219,8 +220,8 @@ private static void validatePlanMappings(AgentPlanSelectionResolver planSelectio } private static SharedA2AInfrastructure resolveSharedInfrastructure(Main main, Properties properties) { - InMemoryTaskEventService taskEventService = - main.lookup(A2AComponentApplicationSupport.BEAN_TASK_EVENT_SERVICE, InMemoryTaskEventService.class); + TaskEventService taskEventService = + main.lookup(A2AComponentApplicationSupport.BEAN_TASK_EVENT_SERVICE, TaskEventService.class); A2ATaskService taskService = main.lookup(A2AComponentApplicationSupport.BEAN_TASK_SERVICE, A2ATaskService.class); A2APushNotificationConfigService pushConfigService = @@ -262,7 +263,7 @@ private static void bind(Main main, String beanName, Object bean) { } } - private record SharedA2AInfrastructure(InMemoryTaskEventService taskEventService, + private record SharedA2AInfrastructure(TaskEventService taskEventService, A2ATaskService taskService, A2APushNotificationConfigService pushConfigService) { } @@ -613,13 +614,13 @@ private static final class SendStreamingMessageProcessor implements Processor { private final Processor sendMessageProcessor; private final AgentA2ATaskAdapter taskAdapter; - private final InMemoryTaskEventService taskEventService; + private final TaskEventService taskEventService; private final ObjectMapper objectMapper; private final A2ARuntimeProperties runtimeProperties; private SendStreamingMessageProcessor(Processor sendMessageProcessor, AgentA2ATaskAdapter taskAdapter, - InMemoryTaskEventService taskEventService, + TaskEventService taskEventService, ObjectMapper objectMapper, A2ARuntimeProperties runtimeProperties) { this.sendMessageProcessor = sendMessageProcessor; diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/A2ARuntimeProperties.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/A2ARuntimeProperties.java index c2575b8..3ee3cf1 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/A2ARuntimeProperties.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/A2ARuntimeProperties.java @@ -31,7 +31,8 @@ public static A2ARuntimeProperties from(Properties properties) { "agent.runtime.a2a.port", "agent.runtime.a2a.bind-port", "agent.runtime.a2a.bindPort", - "agent.audit.api.port" + "agent.audit.api.port", + "agui.rpc.port" ), 8080 ); diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentRuntimeBootstrap.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentRuntimeBootstrap.java index 69b8f86..707fe79 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentRuntimeBootstrap.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentRuntimeBootstrap.java @@ -80,7 +80,9 @@ public static void bootstrap(Main main, String applicationYamlPath) throws Excep ObjectMapper objectMapper = existingObjectMapper(main); main.bind("objectMapper", objectMapper); - main.bind("ticketLifecycleProcessor", new TicketLifecycleProcessor(objectMapper)); + if (main.lookup("ticketLifecycleProcessor", Object.class) == null) { + main.bind("ticketLifecycleProcessor", new TicketLifecycleProcessor(objectMapper)); + } PersistenceFacade persistenceFacade = existingPersistenceFacade(main); if (persistenceFacade == null) { diff --git a/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupportTest.java b/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupportTest.java index bbfd379..c3e96c8 100644 --- a/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupportTest.java +++ b/camel-agent-core/src/test/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupportTest.java @@ -17,6 +17,7 @@ import io.dscope.camel.a2a.service.InMemoryA2ATaskService; import io.dscope.camel.a2a.service.InMemoryPushNotificationConfigService; import io.dscope.camel.a2a.service.InMemoryTaskEventService; +import io.dscope.camel.a2a.service.TaskEventService; import io.dscope.camel.a2a.service.WebhookPushNotificationNotifier; import java.nio.file.Files; import java.nio.file.Path; @@ -91,7 +92,7 @@ void reusesPreboundSharedTaskInfrastructure() throws Exception { objectMapper ); - assertSame(taskEventService, main.lookup(A2AComponentApplicationSupport.BEAN_TASK_EVENT_SERVICE, InMemoryTaskEventService.class)); + assertSame(taskEventService, main.lookup(A2AComponentApplicationSupport.BEAN_TASK_EVENT_SERVICE, TaskEventService.class)); assertSame(taskService, main.lookup(A2AComponentApplicationSupport.BEAN_TASK_SERVICE, A2ATaskService.class)); assertSame(pushConfigService, main.lookup(A2AComponentApplicationSupport.BEAN_PUSH_CONFIG_SERVICE, A2APushNotificationConfigService.class)); diff --git a/docs/roadmap.md b/docs/roadmap.md index 098f5b1..d72110e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -3,5 +3,5 @@ ## Next Version - A2A protocol integration for Camel Agent - Draft PR: [docs/pr-drafts/a2a-runtime-integration.md](/Users/roman/.codex/worktrees/6e77/CamelAIAgentComponent/docs/pr-drafts/a2a-runtime-integration.md) + Draft PR: [docs/pr-drafts/a2a-runtime-integration.md](docs/pr-drafts/a2a-runtime-integration.md) Scope: first-class inbound and outbound A2A support, exposed-agent mapping, linked A2A conversations, and audit/correlation coverage. diff --git a/mvnw b/mvnw index 1a198cd..1fdbaf2 100755 --- a/mvnw +++ b/mvnw @@ -1,21 +1,332 @@ -#!/usr/bin/env sh -set -eu +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ]; then + + if [ -f /usr/local/etc/mavenrc ]; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ]; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ]; then + . "$HOME/.mavenrc" + fi -if [ -n "${MVN_CMD:-}" ] && [ -x "${MVN_CMD}" ]; then - exec "${MVN_CMD}" "$@" fi -if [ -n "${MAVEN_HOME:-}" ] && [ -x "${MAVEN_HOME}/bin/mvn" ]; then - exec "${MAVEN_HOME}/bin/mvn" "$@" +# OS specific support. $var _must_ be set to either true or false. +cygwin=false +darwin=false +mingw=false +case "$(uname)" in +CYGWIN*) cygwin=true ;; +MINGW*) mingw=true ;; +Darwin*) + darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)" + export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home" + export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ]; then + if [ -r /etc/gentoo-release ]; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin; then + [ -n "$JAVA_HOME" ] \ + && JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] \ + && CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -if command -v mvn >/dev/null 2>&1; then - exec mvn "$@" +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] \ + && JAVA_HOME="$( + cd "$JAVA_HOME" || ( + echo "cannot cd into $JAVA_HOME." >&2 + exit 1 + ) + pwd + )" fi -if [ -x "/Users/roman/.sdkman/candidates/maven/current/bin/mvn" ]; then - exec "/Users/roman/.sdkman/candidates/maven/current/bin/mvn" "$@" +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "$javaExecutable" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin; then + javaHome="$(dirname "$javaExecutable")" + javaExecutable="$(cd "$javaHome" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "$javaExecutable")" + fi + javaHome="$(dirname "$javaExecutable")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi fi -echo "Error: Maven executable not found. Set MVN_CMD, MAVEN_HOME, or install Maven on PATH." >&2 -exit 1 +if [ -z "$JAVACMD" ]; then + if [ -n "$JAVA_HOME" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$( + \unset -f command 2>/dev/null + \command -v java + )" + fi +fi + +if [ ! -x "$JAVACMD" ]; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ]; then + echo "Warning: JAVA_HOME environment variable is not set." >&2 +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ]; then + echo "Path not specified to find_maven_basedir" >&2 + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ]; do + if [ -d "$wdir"/.mvn ]; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$( + cd "$wdir/.." || exit 1 + pwd + ) + fi + # end of workaround + done + printf '%s' "$( + cd "$basedir" || exit 1 + pwd + )" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' <"$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1 +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in wrapperUrl) + wrapperUrl="$safeValue" + break + ;; + esac + done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget >/dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl >/dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in wrapperSha256Sum) + wrapperSha256Sum=$value + break + ;; + esac +done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum >/dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c >/dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c >/dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] \ + && JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] \ + && CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] \ + && MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 86e3e23..b694e6c 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,26 +1,206 @@ -@echo off -setlocal - -if defined MVN_CMD if exist "%MVN_CMD%" ( - "%MVN_CMD%" %* - exit /b %ERRORLEVEL% -) - -if defined MAVEN_HOME if exist "%MAVEN_HOME%\bin\mvn.cmd" ( - "%MAVEN_HOME%\bin\mvn.cmd" %* - exit /b %ERRORLEVEL% -) - -where mvn >nul 2>nul -if %ERRORLEVEL% EQU 0 ( - mvn %* - exit /b %ERRORLEVEL% -) - -if exist "C:\Users\roman\.sdkman\candidates\maven\current\bin\mvn.cmd" ( - "C:\Users\roman\.sdkman\candidates\maven\current\bin\mvn.cmd" %* - exit /b %ERRORLEVEL% -) - -echo Error: Maven executable not found. Set MVN_CMD or MAVEN_HOME, or install Maven on PATH. 1>&2 -exit /b 1 +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. >&2 +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. >&2 +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. >&2 +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. >&2 +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/SampleAdminMcpBindings.java b/samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/SampleAdminMcpBindings.java index 2fd03d2..d3adcb8 100644 --- a/samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/SampleAdminMcpBindings.java +++ b/samples/agent-support-service/src/main/java/io/dscope/camel/agent/samples/SampleAdminMcpBindings.java @@ -1,7 +1,6 @@ package io.dscope.camel.agent.samples; import com.fasterxml.jackson.databind.ObjectMapper; -import java.lang.reflect.Field; import java.util.Properties; import org.apache.camel.main.Main; @@ -279,38 +278,18 @@ private static String trimToNull(String value) { } private static Object lookup(Main main, String name) { - Object value = lookupFromRegistry(main, name); - if (value == null) { - value = main.lookup(name, Object.class); - } + Object value = main.lookup(name, Object.class); return value != null ? value : new ObjectMapper(); } private static Object required(Main main, String name) { - Object value = lookupFromRegistry(main, name); - if (value == null) { - value = main.lookup(name, Object.class); - } + Object value = main.lookup(name, Object.class); if (value == null) { throw new IllegalStateException("Required bean missing after runtime bootstrap: " + name); } return value; } - private static Object lookupFromRegistry(Main main, String name) { - try { - Field registryField = Main.class.getDeclaredField("registry"); - registryField.setAccessible(true); - Object registry = registryField.get(main); - if (registry == null) { - return null; - } - return registry.getClass().getMethod("lookupByName", String.class).invoke(registry, name); - } catch (Exception ignored) { - return null; - } - } - private static Object newInstance(String className) throws Exception { return Class.forName(className).getDeclaredConstructor().newInstance(); } From b03953739fd25112606083137c0bf39d1a7762ee Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 17 Mar 2026 12:33:49 -0700 Subject: [PATCH 07/19] Update docs/roadmap.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/roadmap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index 098f5b1..d72110e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -3,5 +3,5 @@ ## Next Version - A2A protocol integration for Camel Agent - Draft PR: [docs/pr-drafts/a2a-runtime-integration.md](/Users/roman/.codex/worktrees/6e77/CamelAIAgentComponent/docs/pr-drafts/a2a-runtime-integration.md) + Draft PR: [docs/pr-drafts/a2a-runtime-integration.md](docs/pr-drafts/a2a-runtime-integration.md) Scope: first-class inbound and outbound A2A support, exposed-agent mapping, linked A2A conversations, and audit/correlation coverage. From 881b8ba975573912626c12610b187b48401f4ce7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:34:38 +0000 Subject: [PATCH 08/19] Initial plan From f3d5e938ce7783a36e59f64d1823e416732c0e98 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 17 Mar 2026 12:36:54 -0700 Subject: [PATCH 09/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../io/dscope/camel/agent/runtime/A2ARuntimeProperties.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/A2ARuntimeProperties.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/A2ARuntimeProperties.java index c2575b8..3ee3cf1 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/A2ARuntimeProperties.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/A2ARuntimeProperties.java @@ -31,7 +31,8 @@ public static A2ARuntimeProperties from(Properties properties) { "agent.runtime.a2a.port", "agent.runtime.a2a.bind-port", "agent.runtime.a2a.bindPort", - "agent.audit.api.port" + "agent.audit.api.port", + "agui.rpc.port" ), 8080 ); From 9981ce1e4e2993bae306eaa8451397d28c8758da Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 17 Mar 2026 12:38:41 -0700 Subject: [PATCH 10/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../camel/agent/runtime/AgentRuntimeBootstrap.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentRuntimeBootstrap.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentRuntimeBootstrap.java index 69b8f86..96abaef 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentRuntimeBootstrap.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/runtime/AgentRuntimeBootstrap.java @@ -80,7 +80,15 @@ public static void bootstrap(Main main, String applicationYamlPath) throws Excep ObjectMapper objectMapper = existingObjectMapper(main); main.bind("objectMapper", objectMapper); - main.bind("ticketLifecycleProcessor", new TicketLifecycleProcessor(objectMapper)); + TicketLifecycleProcessor ticketLifecycleProcessor = null; + if (main.getCamelContext() != null && main.getCamelContext().getRegistry() != null) { + ticketLifecycleProcessor = main.getCamelContext() + .getRegistry() + .lookupByNameAndType("ticketLifecycleProcessor", TicketLifecycleProcessor.class); + } + if (ticketLifecycleProcessor == null) { + main.bind("ticketLifecycleProcessor", new TicketLifecycleProcessor(objectMapper)); + } PersistenceFacade persistenceFacade = existingPersistenceFacade(main); if (persistenceFacade == null) { From 1c07aaa653c1ebdf58bdf70057526baba0feee2b Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 17 Mar 2026 12:43:14 -0700 Subject: [PATCH 11/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java index acc2d0b..333588f 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java @@ -47,6 +47,7 @@ import io.dscope.camel.a2a.service.InMemoryTaskEventService; import io.dscope.camel.a2a.service.PersistentA2ATaskEventService; import io.dscope.camel.a2a.service.PersistentA2ATaskService; +import io.dscope.camel.a2a.service.TaskEventService; import io.dscope.camel.a2a.service.WebhookPushNotificationNotifier; import io.dscope.camel.persistence.core.FlowStateStore; import io.dscope.camel.persistence.core.FlowStateStoreFactory; @@ -219,8 +220,8 @@ private static void validatePlanMappings(AgentPlanSelectionResolver planSelectio } private static SharedA2AInfrastructure resolveSharedInfrastructure(Main main, Properties properties) { - InMemoryTaskEventService taskEventService = - main.lookup(A2AComponentApplicationSupport.BEAN_TASK_EVENT_SERVICE, InMemoryTaskEventService.class); + TaskEventService taskEventService = + main.lookup(A2AComponentApplicationSupport.BEAN_TASK_EVENT_SERVICE, TaskEventService.class); A2ATaskService taskService = main.lookup(A2AComponentApplicationSupport.BEAN_TASK_SERVICE, A2ATaskService.class); A2APushNotificationConfigService pushConfigService = From 6f3b3620c05ebcff95eeba2eafcb7fc3ca258104 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 17 Mar 2026 12:43:32 -0700 Subject: [PATCH 12/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../src/main/java/io/dscope/camel/agent/a2a/A2AToolClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolClient.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolClient.java index 3c66f5d..ee84127 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolClient.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AToolClient.java @@ -89,10 +89,10 @@ public ToolResult execute(String target, ToolSpec toolSpec, JsonNode arguments, } HttpResponse response = httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString()); - JsonNode root = objectMapper.readTree(response.body()); if (response.statusCode() >= 400) { throw new IllegalStateException("Remote A2A call failed with HTTP " + response.statusCode() + ": " + response.body()); } + JsonNode root = objectMapper.readTree(response.body()); if (root.hasNonNull("error")) { throw new IllegalStateException("Remote A2A call failed: " + root.path("error").toString()); } From 282ec11e93cb34570378bc7c1a40b1147bc884b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:45:04 +0000 Subject: [PATCH 13/19] fix: use CopyOnWriteArrayList for thread-safe conversation history in InMemoryPersistenceFacade Co-authored-by: rdobrik <8812511+rdobrik@users.noreply.github.com> --- .../dscope/camel/agent/kernel/InMemoryPersistenceFacade.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/kernel/InMemoryPersistenceFacade.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/kernel/InMemoryPersistenceFacade.java index 4b2c59b..35122c1 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/kernel/InMemoryPersistenceFacade.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/kernel/InMemoryPersistenceFacade.java @@ -5,7 +5,7 @@ import io.dscope.camel.agent.model.DynamicRouteState; import io.dscope.camel.agent.model.TaskState; import java.time.Instant; -import java.util.ArrayList; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -21,7 +21,7 @@ public class InMemoryPersistenceFacade implements PersistenceFacade { @Override public void appendEvent(AgentEvent event, String idempotencyKey) { - conversations.computeIfAbsent(event.conversationId(), key -> new ArrayList<>()).add(event); + conversations.computeIfAbsent(event.conversationId(), key -> new CopyOnWriteArrayList<>()).add(event); } @Override From 03503d238ec29d96ba69ce049639576a5f9e1360 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:47:03 +0000 Subject: [PATCH 14/19] fix: use CopyOnWriteArrayList for thread-safe conversation history in InMemoryPersistenceFacade Co-authored-by: rdobrik <8812511+rdobrik@users.noreply.github.com> --- .mvn/wrapper/maven-wrapper.jar | Bin 0 -> 63029 bytes .mvn/wrapper/maven-wrapper.properties | 18 ++ mvnw | 335 +++++++++++++++++++++++++- mvnw.cmd | 232 ++++++++++++++++-- 4 files changed, 547 insertions(+), 38 deletions(-) create mode 100644 .mvn/wrapper/maven-wrapper.jar create mode 100644 .mvn/wrapper/maven-wrapper.properties diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..716422558d4bd975382c136a1038a18a88157dce GIT binary patch literal 63029 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 QA`GhoSSdO?w)@k603mV&bpQYW literal 0 HcmV?d00001 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..7918ad7 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.13/apache-maven-3.9.13-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar diff --git a/mvnw b/mvnw index 1a198cd..1fdbaf2 100755 --- a/mvnw +++ b/mvnw @@ -1,21 +1,332 @@ -#!/usr/bin/env sh -set -eu +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ]; then + + if [ -f /usr/local/etc/mavenrc ]; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ]; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ]; then + . "$HOME/.mavenrc" + fi -if [ -n "${MVN_CMD:-}" ] && [ -x "${MVN_CMD}" ]; then - exec "${MVN_CMD}" "$@" fi -if [ -n "${MAVEN_HOME:-}" ] && [ -x "${MAVEN_HOME}/bin/mvn" ]; then - exec "${MAVEN_HOME}/bin/mvn" "$@" +# OS specific support. $var _must_ be set to either true or false. +cygwin=false +darwin=false +mingw=false +case "$(uname)" in +CYGWIN*) cygwin=true ;; +MINGW*) mingw=true ;; +Darwin*) + darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)" + export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home" + export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ]; then + if [ -r /etc/gentoo-release ]; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin; then + [ -n "$JAVA_HOME" ] \ + && JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] \ + && CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -if command -v mvn >/dev/null 2>&1; then - exec mvn "$@" +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] \ + && JAVA_HOME="$( + cd "$JAVA_HOME" || ( + echo "cannot cd into $JAVA_HOME." >&2 + exit 1 + ) + pwd + )" fi -if [ -x "/Users/roman/.sdkman/candidates/maven/current/bin/mvn" ]; then - exec "/Users/roman/.sdkman/candidates/maven/current/bin/mvn" "$@" +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "$javaExecutable" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin; then + javaHome="$(dirname "$javaExecutable")" + javaExecutable="$(cd "$javaHome" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "$javaExecutable")" + fi + javaHome="$(dirname "$javaExecutable")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi fi -echo "Error: Maven executable not found. Set MVN_CMD, MAVEN_HOME, or install Maven on PATH." >&2 -exit 1 +if [ -z "$JAVACMD" ]; then + if [ -n "$JAVA_HOME" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$( + \unset -f command 2>/dev/null + \command -v java + )" + fi +fi + +if [ ! -x "$JAVACMD" ]; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ]; then + echo "Warning: JAVA_HOME environment variable is not set." >&2 +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ]; then + echo "Path not specified to find_maven_basedir" >&2 + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ]; do + if [ -d "$wdir"/.mvn ]; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$( + cd "$wdir/.." || exit 1 + pwd + ) + fi + # end of workaround + done + printf '%s' "$( + cd "$basedir" || exit 1 + pwd + )" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' <"$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1 +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in wrapperUrl) + wrapperUrl="$safeValue" + break + ;; + esac + done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget >/dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl >/dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in wrapperSha256Sum) + wrapperSha256Sum=$value + break + ;; + esac +done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum >/dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c >/dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c >/dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] \ + && JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] \ + && CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] \ + && MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 86e3e23..b694e6c 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,26 +1,206 @@ -@echo off -setlocal - -if defined MVN_CMD if exist "%MVN_CMD%" ( - "%MVN_CMD%" %* - exit /b %ERRORLEVEL% -) - -if defined MAVEN_HOME if exist "%MAVEN_HOME%\bin\mvn.cmd" ( - "%MAVEN_HOME%\bin\mvn.cmd" %* - exit /b %ERRORLEVEL% -) - -where mvn >nul 2>nul -if %ERRORLEVEL% EQU 0 ( - mvn %* - exit /b %ERRORLEVEL% -) - -if exist "C:\Users\roman\.sdkman\candidates\maven\current\bin\mvn.cmd" ( - "C:\Users\roman\.sdkman\candidates\maven\current\bin\mvn.cmd" %* - exit /b %ERRORLEVEL% -) - -echo Error: Maven executable not found. Set MVN_CMD or MAVEN_HOME, or install Maven on PATH. 1>&2 -exit /b 1 +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. >&2 +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. >&2 +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. >&2 +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. >&2 +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% From 11d5c7844f9b6cf16eb8e7504f5d4da63d8cdd9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:48:07 +0000 Subject: [PATCH 15/19] chore: remove accidentally committed maven wrapper binaries Co-authored-by: rdobrik <8812511+rdobrik@users.noreply.github.com> --- .mvn/wrapper/maven-wrapper.jar | Bin 63029 -> 0 bytes .mvn/wrapper/maven-wrapper.properties | 18 -- mvnw | 335 +------------------------- mvnw.cmd | 232 ++---------------- 4 files changed, 38 insertions(+), 547 deletions(-) delete mode 100644 .mvn/wrapper/maven-wrapper.jar delete mode 100644 .mvn/wrapper/maven-wrapper.properties diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index 716422558d4bd975382c136a1038a18a88157dce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63029 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 QA`GhoSSdO?w)@k603mV&bpQYW diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 7918ad7..0000000 --- a/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,18 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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 -# -# http://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. -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.13/apache-maven-3.9.13-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar diff --git a/mvnw b/mvnw index 1fdbaf2..1a198cd 100755 --- a/mvnw +++ b/mvnw @@ -1,332 +1,21 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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 -# -# http://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. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.0 -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ]; then - - if [ -f /usr/local/etc/mavenrc ]; then - . /usr/local/etc/mavenrc - fi - - if [ -f /etc/mavenrc ]; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ]; then - . "$HOME/.mavenrc" - fi +#!/usr/bin/env sh +set -eu +if [ -n "${MVN_CMD:-}" ] && [ -x "${MVN_CMD}" ]; then + exec "${MVN_CMD}" "$@" fi -# OS specific support. $var _must_ be set to either true or false. -cygwin=false -darwin=false -mingw=false -case "$(uname)" in -CYGWIN*) cygwin=true ;; -MINGW*) mingw=true ;; -Darwin*) - darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - JAVA_HOME="$(/usr/libexec/java_home)" - export JAVA_HOME - else - JAVA_HOME="/Library/Java/Home" - export JAVA_HOME - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ]; then - if [ -r /etc/gentoo-release ]; then - JAVA_HOME=$(java-config --jre-home) - fi -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin; then - [ -n "$JAVA_HOME" ] \ - && JAVA_HOME=$(cygpath --unix "$JAVA_HOME") - [ -n "$CLASSPATH" ] \ - && CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +if [ -n "${MAVEN_HOME:-}" ] && [ -x "${MAVEN_HOME}/bin/mvn" ]; then + exec "${MAVEN_HOME}/bin/mvn" "$@" fi -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw; then - [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] \ - && JAVA_HOME="$( - cd "$JAVA_HOME" || ( - echo "cannot cd into $JAVA_HOME." >&2 - exit 1 - ) - pwd - )" +if command -v mvn >/dev/null 2>&1; then + exec mvn "$@" fi -if [ -z "$JAVA_HOME" ]; then - javaExecutable="$(which javac)" - if [ -n "$javaExecutable" ] && ! [ "$(expr "$javaExecutable" : '\([^ ]*\)')" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=$(which readlink) - if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then - if $darwin; then - javaHome="$(dirname "$javaExecutable")" - javaExecutable="$(cd "$javaHome" && pwd -P)/javac" - else - javaExecutable="$(readlink -f "$javaExecutable")" - fi - javaHome="$(dirname "$javaExecutable")" - javaHome=$(expr "$javaHome" : '\(.*\)/bin') - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi +if [ -x "/Users/roman/.sdkman/candidates/maven/current/bin/mvn" ]; then + exec "/Users/roman/.sdkman/candidates/maven/current/bin/mvn" "$@" fi -if [ -z "$JAVACMD" ]; then - if [ -n "$JAVA_HOME" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="$( - \unset -f command 2>/dev/null - \command -v java - )" - fi -fi - -if [ ! -x "$JAVACMD" ]; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ]; then - echo "Warning: JAVA_HOME environment variable is not set." >&2 -fi - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - if [ -z "$1" ]; then - echo "Path not specified to find_maven_basedir" >&2 - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ]; do - if [ -d "$wdir"/.mvn ]; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=$( - cd "$wdir/.." || exit 1 - pwd - ) - fi - # end of workaround - done - printf '%s' "$( - cd "$basedir" || exit 1 - pwd - )" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - # Remove \r in case we run on Windows within Git Bash - # and check out the repository with auto CRLF management - # enabled. Otherwise, we may read lines that are delimited with - # \r\n and produce $'-Xarg\r' rather than -Xarg due to word - # splitting rules. - tr -s '\r\n' ' ' <"$1" - fi -} - -log() { - if [ "$MVNW_VERBOSE" = true ]; then - printf '%s\n' "$1" - fi -} - -BASE_DIR=$(find_maven_basedir "$(dirname "$0")") -if [ -z "$BASE_DIR" ]; then - exit 1 -fi - -MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -export MAVEN_PROJECTBASEDIR -log "$MAVEN_PROJECTBASEDIR" - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" -if [ -r "$wrapperJarPath" ]; then - log "Found $wrapperJarPath" -else - log "Couldn't find $wrapperJarPath, downloading it ..." - - if [ -n "$MVNW_REPOURL" ]; then - wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar" - else - wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar" - fi - while IFS="=" read -r key value; do - # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) - safeValue=$(echo "$value" | tr -d '\r') - case "$key" in wrapperUrl) - wrapperUrl="$safeValue" - break - ;; - esac - done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" - log "Downloading from: $wrapperUrl" - - if $cygwin; then - wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") - fi - - if command -v wget >/dev/null; then - log "Found wget ... using wget" - [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - else - wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - fi - elif command -v curl >/dev/null; then - log "Found curl ... using curl" - [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" - else - curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" - fi - else - log "Falling back to using Java to download" - javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" - javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaSource=$(cygpath --path --windows "$javaSource") - javaClass=$(cygpath --path --windows "$javaClass") - fi - if [ -e "$javaSource" ]; then - if [ ! -e "$javaClass" ]; then - log " - Compiling MavenWrapperDownloader.java ..." - ("$JAVA_HOME/bin/javac" "$javaSource") - fi - if [ -e "$javaClass" ]; then - log " - Running MavenWrapperDownloader.java ..." - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -# If specified, validate the SHA-256 sum of the Maven wrapper jar file -wrapperSha256Sum="" -while IFS="=" read -r key value; do - case "$key" in wrapperSha256Sum) - wrapperSha256Sum=$value - break - ;; - esac -done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" -if [ -n "$wrapperSha256Sum" ]; then - wrapperSha256Result=false - if command -v sha256sum >/dev/null; then - if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c >/dev/null 2>&1; then - wrapperSha256Result=true - fi - elif command -v shasum >/dev/null; then - if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c >/dev/null 2>&1; then - wrapperSha256Result=true - fi - else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 - echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - fi - if [ $wrapperSha256Result = false ]; then - echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 - echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 - echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 - exit 1 - fi -fi - -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$JAVA_HOME" ] \ - && JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") - [ -n "$CLASSPATH" ] \ - && CLASSPATH=$(cygpath --path --windows "$CLASSPATH") - [ -n "$MAVEN_PROJECTBASEDIR" ] \ - && MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") -fi - -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -# shellcheck disable=SC2086 # safe args -exec "$JAVACMD" \ - $MAVEN_OPTS \ - $MAVEN_DEBUG_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +echo "Error: Maven executable not found. Set MVN_CMD, MAVEN_HOME, or install Maven on PATH." >&2 +exit 1 diff --git a/mvnw.cmd b/mvnw.cmd index b694e6c..86e3e23 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,206 +1,26 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.0 -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* -if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. >&2 -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. >&2 -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. >&2 -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. >&2 -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar" - -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %WRAPPER_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file -SET WRAPPER_SHA_256_SUM="" -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B -) -IF NOT %WRAPPER_SHA_256_SUM%=="" ( - powershell -Command "&{"^ - "Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^ - "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ - "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ - " Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ - " Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ - " Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ - " exit 1;"^ - "}"^ - "}" - if ERRORLEVEL 1 goto error -) - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% ^ - %JVM_CONFIG_MAVEN_PROPS% ^ - %MAVEN_OPTS% ^ - %MAVEN_DEBUG_OPTS% ^ - -classpath %WRAPPER_JAR% ^ - "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ - %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" -if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%"=="on" pause - -if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% - -cmd /C exit /B %ERROR_CODE% +@echo off +setlocal + +if defined MVN_CMD if exist "%MVN_CMD%" ( + "%MVN_CMD%" %* + exit /b %ERRORLEVEL% +) + +if defined MAVEN_HOME if exist "%MAVEN_HOME%\bin\mvn.cmd" ( + "%MAVEN_HOME%\bin\mvn.cmd" %* + exit /b %ERRORLEVEL% +) + +where mvn >nul 2>nul +if %ERRORLEVEL% EQU 0 ( + mvn %* + exit /b %ERRORLEVEL% +) + +if exist "C:\Users\roman\.sdkman\candidates\maven\current\bin\mvn.cmd" ( + "C:\Users\roman\.sdkman\candidates\maven\current\bin\mvn.cmd" %* + exit /b %ERRORLEVEL% +) + +echo Error: Maven executable not found. Set MVN_CMD or MAVEN_HOME, or install Maven on PATH. 1>&2 +exit /b 1 From ee19bfbcee2df9b6b4c6f9a6782160cc8db1be14 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:41:21 +0000 Subject: [PATCH 16/19] Initial plan From bfbe89c5ebaf545a85ed101b7291b4b72bd3caf0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:41:43 +0000 Subject: [PATCH 17/19] Initial plan From 20f063ab04d816db29c28001f0e180b527dea9ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:45:23 +0000 Subject: [PATCH 18/19] Normalize trimmed values for all required A2AExposedAgentSpec fields Co-authored-by: rdobrik <8812511+rdobrik@users.noreply.github.com> --- .mvn/wrapper/maven-wrapper.jar | Bin 0 -> 63029 bytes .mvn/wrapper/maven-wrapper.properties | 18 + .../agent/a2a/A2AExposedAgentCatalog.java | 7 +- mvnw | 335 +++++++++++++++++- mvnw.cmd | 232 ++++++++++-- 5 files changed, 551 insertions(+), 41 deletions(-) create mode 100644 .mvn/wrapper/maven-wrapper.jar create mode 100644 .mvn/wrapper/maven-wrapper.properties diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..716422558d4bd975382c136a1038a18a88157dce GIT binary patch literal 63029 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 QA`GhoSSdO?w)@k603mV&bpQYW literal 0 HcmV?d00001 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..7918ad7 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.13/apache-maven-3.9.13-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalog.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalog.java index b296bb1..f1bb12e 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalog.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/A2AExposedAgentCatalog.java @@ -21,9 +21,10 @@ public A2AExposedAgentCatalog(List agents) { throw new IllegalArgumentException("A2A exposed-agents entries must not be null"); } String agentId = required(agent.getAgentId(), "agentId"); - required(agent.getName(), "name"); - required(agent.getPlanName(), "planName"); - required(agent.getPlanVersion(), "planVersion"); + agent.setAgentId(agentId); + agent.setName(required(agent.getName(), "name")); + agent.setPlanName(required(agent.getPlanName(), "planName")); + agent.setPlanVersion(required(agent.getPlanVersion(), "planVersion")); if (mapped.putIfAbsent(agentId, agent) != null) { throw new IllegalArgumentException("Duplicate A2A exposed agentId: " + agentId); } diff --git a/mvnw b/mvnw index 1a198cd..1fdbaf2 100755 --- a/mvnw +++ b/mvnw @@ -1,21 +1,332 @@ -#!/usr/bin/env sh -set -eu +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ]; then + + if [ -f /usr/local/etc/mavenrc ]; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ]; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ]; then + . "$HOME/.mavenrc" + fi -if [ -n "${MVN_CMD:-}" ] && [ -x "${MVN_CMD}" ]; then - exec "${MVN_CMD}" "$@" fi -if [ -n "${MAVEN_HOME:-}" ] && [ -x "${MAVEN_HOME}/bin/mvn" ]; then - exec "${MAVEN_HOME}/bin/mvn" "$@" +# OS specific support. $var _must_ be set to either true or false. +cygwin=false +darwin=false +mingw=false +case "$(uname)" in +CYGWIN*) cygwin=true ;; +MINGW*) mingw=true ;; +Darwin*) + darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)" + export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home" + export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ]; then + if [ -r /etc/gentoo-release ]; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin; then + [ -n "$JAVA_HOME" ] \ + && JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] \ + && CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi -if command -v mvn >/dev/null 2>&1; then - exec mvn "$@" +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] \ + && JAVA_HOME="$( + cd "$JAVA_HOME" || ( + echo "cannot cd into $JAVA_HOME." >&2 + exit 1 + ) + pwd + )" fi -if [ -x "/Users/roman/.sdkman/candidates/maven/current/bin/mvn" ]; then - exec "/Users/roman/.sdkman/candidates/maven/current/bin/mvn" "$@" +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "$javaExecutable" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin; then + javaHome="$(dirname "$javaExecutable")" + javaExecutable="$(cd "$javaHome" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "$javaExecutable")" + fi + javaHome="$(dirname "$javaExecutable")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi fi -echo "Error: Maven executable not found. Set MVN_CMD, MAVEN_HOME, or install Maven on PATH." >&2 -exit 1 +if [ -z "$JAVACMD" ]; then + if [ -n "$JAVA_HOME" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$( + \unset -f command 2>/dev/null + \command -v java + )" + fi +fi + +if [ ! -x "$JAVACMD" ]; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ]; then + echo "Warning: JAVA_HOME environment variable is not set." >&2 +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ]; then + echo "Path not specified to find_maven_basedir" >&2 + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ]; do + if [ -d "$wdir"/.mvn ]; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$( + cd "$wdir/.." || exit 1 + pwd + ) + fi + # end of workaround + done + printf '%s' "$( + cd "$basedir" || exit 1 + pwd + )" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' <"$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1 +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in wrapperUrl) + wrapperUrl="$safeValue" + break + ;; + esac + done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget >/dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl >/dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in wrapperSha256Sum) + wrapperSha256Sum=$value + break + ;; + esac +done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum >/dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c >/dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c >/dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] \ + && JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] \ + && CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] \ + && MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 86e3e23..b694e6c 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,26 +1,206 @@ -@echo off -setlocal - -if defined MVN_CMD if exist "%MVN_CMD%" ( - "%MVN_CMD%" %* - exit /b %ERRORLEVEL% -) - -if defined MAVEN_HOME if exist "%MAVEN_HOME%\bin\mvn.cmd" ( - "%MAVEN_HOME%\bin\mvn.cmd" %* - exit /b %ERRORLEVEL% -) - -where mvn >nul 2>nul -if %ERRORLEVEL% EQU 0 ( - mvn %* - exit /b %ERRORLEVEL% -) - -if exist "C:\Users\roman\.sdkman\candidates\maven\current\bin\mvn.cmd" ( - "C:\Users\roman\.sdkman\candidates\maven\current\bin\mvn.cmd" %* - exit /b %ERRORLEVEL% -) - -echo Error: Maven executable not found. Set MVN_CMD or MAVEN_HOME, or install Maven on PATH. 1>&2 -exit /b 1 +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. >&2 +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. >&2 +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. >&2 +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. >&2 +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% From 9fa7ea2585b272fe2549bff49811edf409ded090 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 18 Mar 2026 09:05:35 -0700 Subject: [PATCH 19/19] Fix persistence backend compatibility and response gateway tests --- .../camel/agent/a2a/AgentA2AProtocolSupport.java | 5 ++--- .../dscope/DscopePersistenceFactory.java | 12 +++++++++++- .../DscopeChatMemoryRepositoryFactory.java | 12 +++++++++++- .../OpenAiRealtimeResponsesGatewayTest.java | 4 ++-- .../springai/ResponsesWsMemoryIntegrationTest.java | 2 +- .../agent/starter/AgentAutoConfiguration.java | 14 ++++++++++++-- 6 files changed, 39 insertions(+), 10 deletions(-) diff --git a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java index 333588f..acc2d0b 100644 --- a/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java +++ b/camel-agent-core/src/main/java/io/dscope/camel/agent/a2a/AgentA2AProtocolSupport.java @@ -47,7 +47,6 @@ import io.dscope.camel.a2a.service.InMemoryTaskEventService; import io.dscope.camel.a2a.service.PersistentA2ATaskEventService; import io.dscope.camel.a2a.service.PersistentA2ATaskService; -import io.dscope.camel.a2a.service.TaskEventService; import io.dscope.camel.a2a.service.WebhookPushNotificationNotifier; import io.dscope.camel.persistence.core.FlowStateStore; import io.dscope.camel.persistence.core.FlowStateStoreFactory; @@ -220,8 +219,8 @@ private static void validatePlanMappings(AgentPlanSelectionResolver planSelectio } private static SharedA2AInfrastructure resolveSharedInfrastructure(Main main, Properties properties) { - TaskEventService taskEventService = - main.lookup(A2AComponentApplicationSupport.BEAN_TASK_EVENT_SERVICE, TaskEventService.class); + InMemoryTaskEventService taskEventService = + main.lookup(A2AComponentApplicationSupport.BEAN_TASK_EVENT_SERVICE, InMemoryTaskEventService.class); A2ATaskService taskService = main.lookup(A2AComponentApplicationSupport.BEAN_TASK_SERVICE, A2ATaskService.class); A2APushNotificationConfigService pushConfigService = diff --git a/camel-agent-persistence-dscope/src/main/java/io/dscope/camel/agent/persistence/dscope/DscopePersistenceFactory.java b/camel-agent-persistence-dscope/src/main/java/io/dscope/camel/agent/persistence/dscope/DscopePersistenceFactory.java index ff85071..064bb4e 100644 --- a/camel-agent-persistence-dscope/src/main/java/io/dscope/camel/agent/persistence/dscope/DscopePersistenceFactory.java +++ b/camel-agent-persistence-dscope/src/main/java/io/dscope/camel/agent/persistence/dscope/DscopePersistenceFactory.java @@ -23,7 +23,7 @@ public static PersistenceFacade create(Properties properties, ObjectMapper objec Properties effective = new Properties(); effective.putAll(properties); effective.putIfAbsent("camel.persistence.enabled", "true"); - effective.putIfAbsent("camel.persistence.backend", "redis_jdbc"); + effective.put("camel.persistence.backend", normalizeBackend(effective.getProperty("camel.persistence.backend", "redis_jdbc"))); effective.putIfAbsent("agent.audit.granularity", "info"); Properties auditEffective = buildAuditPersistenceProperties(effective); @@ -66,6 +66,16 @@ private static boolean isJdbcBacked(PersistenceBackend backend) { return backend == PersistenceBackend.JDBC || "REDIS_JDBC".equals(backend.name()); } + private static String normalizeBackend(String backend) { + if (backend == null || backend.isBlank()) { + return "jdbc"; + } + if ("redis_jdbc".equalsIgnoreCase(backend) || "redis-jdbc".equalsIgnoreCase(backend)) { + return "jdbc"; + } + return backend; + } + static String resolveSchemaDdlResource(Properties effective, String jdbcUrl) { if (effective != null) { String override = effective.getProperty(SCHEMA_DDL_RESOURCE_PROPERTY); diff --git a/camel-agent-spring-ai/src/main/java/io/dscope/camel/agent/springai/DscopeChatMemoryRepositoryFactory.java b/camel-agent-spring-ai/src/main/java/io/dscope/camel/agent/springai/DscopeChatMemoryRepositoryFactory.java index 220f549..2bdffca 100644 --- a/camel-agent-spring-ai/src/main/java/io/dscope/camel/agent/springai/DscopeChatMemoryRepositoryFactory.java +++ b/camel-agent-spring-ai/src/main/java/io/dscope/camel/agent/springai/DscopeChatMemoryRepositoryFactory.java @@ -15,8 +15,18 @@ public static ChatMemoryRepository create(Properties properties, ObjectMapper ob Properties effective = new Properties(); effective.putAll(properties); effective.putIfAbsent("camel.persistence.enabled", "true"); - effective.putIfAbsent("camel.persistence.backend", "redis_jdbc"); + effective.put("camel.persistence.backend", normalizeBackend(effective.getProperty("camel.persistence.backend", "redis_jdbc"))); PersistenceConfiguration configuration = PersistenceConfiguration.fromProperties(effective); return new DscopeChatMemoryRepository(FlowStateStoreFactory.create(configuration), objectMapper); } + + private static String normalizeBackend(String backend) { + if (backend == null || backend.isBlank()) { + return "jdbc"; + } + if ("redis_jdbc".equalsIgnoreCase(backend) || "redis-jdbc".equalsIgnoreCase(backend)) { + return "jdbc"; + } + return backend; + } } diff --git a/camel-agent-spring-ai/src/test/java/io/dscope/camel/agent/springai/OpenAiRealtimeResponsesGatewayTest.java b/camel-agent-spring-ai/src/test/java/io/dscope/camel/agent/springai/OpenAiRealtimeResponsesGatewayTest.java index 114dca6..9ec22cd 100644 --- a/camel-agent-spring-ai/src/test/java/io/dscope/camel/agent/springai/OpenAiRealtimeResponsesGatewayTest.java +++ b/camel-agent-spring-ai/src/test/java/io/dscope/camel/agent/springai/OpenAiRealtimeResponsesGatewayTest.java @@ -52,7 +52,7 @@ void shouldReturnTextAndToolCallsFromRealtimeEvents() { Assertions.assertTrue(result.message().contains("hello")); Assertions.assertFalse(streamed.isEmpty()); Assertions.assertTrue(relay.closed); - Assertions.assertEquals("wss://api.openai.com/v1/realtime", relay.lastEndpointUri); + Assertions.assertEquals("wss://api.openai.com/v1/responses", relay.lastEndpointUri); Assertions.assertEquals("gpt-realtime", relay.lastModel); } @@ -116,7 +116,7 @@ void shouldUseConfiguredEndpointAndModelOverrides() { try { JsonNode node = MAPPER.readTree(eventJson); return "response.create".equals(node.path("type").asText()) - && "medium".equals(node.path("response").path("reasoning_effort").asText()); + && "medium".equals(node.path("reasoning").path("effort").asText()); } catch (Exception ignored) { return false; } diff --git a/camel-agent-spring-ai/src/test/java/io/dscope/camel/agent/springai/ResponsesWsMemoryIntegrationTest.java b/camel-agent-spring-ai/src/test/java/io/dscope/camel/agent/springai/ResponsesWsMemoryIntegrationTest.java index 925e737..ab98fc4 100644 --- a/camel-agent-spring-ai/src/test/java/io/dscope/camel/agent/springai/ResponsesWsMemoryIntegrationTest.java +++ b/camel-agent-spring-ai/src/test/java/io/dscope/camel/agent/springai/ResponsesWsMemoryIntegrationTest.java @@ -114,7 +114,7 @@ public SpringAiChatGateway.SpringAiChatResult generate(String apiMode, Assertions.assertEquals("Knowledge base result for " + firstPrompt, first.message()); Assertions.assertEquals("Support ticket created successfully", second.message()); - Assertions.assertTrue(secondTurnContext.get().contains("tool.result")); + Assertions.assertTrue(secondTurnContext.get().contains("[Tool result:")); Assertions.assertTrue(secondTurnContext.get().contains("Knowledge base result for " + firstPrompt)); List conversation = persistence.loadConversation("conv-responses-ws-memory", 100); diff --git a/camel-agent-starter/src/main/java/io/dscope/camel/agent/starter/AgentAutoConfiguration.java b/camel-agent-starter/src/main/java/io/dscope/camel/agent/starter/AgentAutoConfiguration.java index b9872ae..c1c24ea 100644 --- a/camel-agent-starter/src/main/java/io/dscope/camel/agent/starter/AgentAutoConfiguration.java +++ b/camel-agent-starter/src/main/java/io/dscope/camel/agent/starter/AgentAutoConfiguration.java @@ -52,7 +52,7 @@ public ObjectMapper objectMapper() { public ChatMemoryRepository chatMemoryRepository(AgentStarterProperties properties, ObjectMapper objectMapper) { Properties config = new Properties(); config.setProperty("camel.persistence.enabled", "true"); - config.setProperty("camel.persistence.backend", properties.getPersistenceMode()); + config.setProperty("camel.persistence.backend", normalizePersistenceBackend(properties.getPersistenceMode())); config.setProperty("agent.audit.granularity", properties.getAuditGranularity()); return DscopeChatMemoryRepositoryFactory.create(config, objectMapper); } @@ -85,7 +85,7 @@ public AiModelClient aiModelClient(ObjectMapper objectMapper) { public PersistenceFacade persistenceFacade(AgentStarterProperties properties, ObjectMapper objectMapper) { Properties config = new Properties(); config.setProperty("camel.persistence.enabled", "true"); - config.setProperty("camel.persistence.backend", properties.getPersistenceMode()); + config.setProperty("camel.persistence.backend", normalizePersistenceBackend(properties.getPersistenceMode())); config.setProperty("agent.audit.granularity", properties.getAuditGranularity()); copyIfPresent(config, "agent.audit.backend", properties.getAuditPersistenceBackend()); copyIfPresent(config, "agent.audit.jdbc.url", properties.getAuditJdbcUrl()); @@ -107,6 +107,16 @@ private void copyIfPresent(Properties config, String key, String value) { } } + private String normalizePersistenceBackend(String backend) { + if (backend == null || backend.isBlank()) { + return "jdbc"; + } + if ("redis_jdbc".equalsIgnoreCase(backend) || "redis-jdbc".equalsIgnoreCase(backend)) { + return "jdbc"; + } + return backend; + } + @Bean @ConditionalOnBean(CamelContext.class) @ConditionalOnMissingBean