diff --git "a/documents/zh/2.\345\274\200\345\217\221\346\214\207\345\215\227/API\346\226\207\346\241\243/com.openjiuwen.service/app/a2a/catalog.README.md" "b/documents/zh/2.\345\274\200\345\217\221\346\214\207\345\215\227/API\346\226\207\346\241\243/com.openjiuwen.service/app/a2a/catalog.README.md" new file mode 100644 index 00000000..237d570b --- /dev/null +++ "b/documents/zh/2.\345\274\200\345\217\221\346\214\207\345\215\227/API\346\226\207\346\241\243/com.openjiuwen.service/app/a2a/catalog.README.md" @@ -0,0 +1,16 @@ +# a2a.catalog + +`com.openjiuwen.service.app.a2a.catalog` 提供可供 Runtime 适配器复用的远端 A2A Agent 目录。 + +## 类型 + +| Type | Description | +| --- | --- | +| `A2ARemoteAgentCardRegistry` | 线程安全地保存远端 Agent Card、调用超时与调用模式,并发布目录更新事件。 | +| `RemoteAgentEntry` | 保存单个远端 Agent 的名称、Agent Card、调用超时与调用模式。 | +| `RemoteAgentCatalogSnapshot` | 保存版本号和全量远端 Agent 条目的不可变快照。 | +| `RemoteAgentCatalogChangedEvent` | 远端 Agent 目录更新后发布的全量快照事件。 | + +## 源码路径 + +`service/agent-service-app/src/main/java/com/openjiuwen/service/app/a2a/catalog/` diff --git "a/documents/zh/2.\345\274\200\345\217\221\346\214\207\345\215\227/API\346\226\207\346\241\243/com.openjiuwen.service/app/controller/a2a/client.README.md" "b/documents/zh/2.\345\274\200\345\217\221\346\214\207\345\215\227/API\346\226\207\346\241\243/com.openjiuwen.service/app/controller/a2a/client.README.md" index e0ec3651..939a2cb8 100644 --- "a/documents/zh/2.\345\274\200\345\217\221\346\214\207\345\215\227/API\346\226\207\346\241\243/com.openjiuwen.service/app/controller/a2a/client.README.md" +++ "b/documents/zh/2.\345\274\200\345\217\221\346\214\207\345\215\227/API\346\226\207\346\241\243/com.openjiuwen.service/app/controller/a2a/client.README.md" @@ -7,7 +7,6 @@ | Type | Description | | --- | --- | | `A2AAgentCardDiscovery` | 按 `openjiuwen.service.a2a.remote-agents` 拉取远端 Agent Card,失败后定时重试。 | -| `A2ARemoteAgentCardRegistry` | 保存远端 AgentCard、URL 和 timeout。 | | `A2ARemoteAgentClient` | 调用远端 Agent 的 sync / streaming client。 | ## 调用模式 diff --git a/service/agent-service-app/src/main/java/com/openjiuwen/service/app/a2a/catalog/A2ARemoteAgentCardRegistry.java b/service/agent-service-app/src/main/java/com/openjiuwen/service/app/a2a/catalog/A2ARemoteAgentCardRegistry.java new file mode 100644 index 00000000..d7442895 --- /dev/null +++ b/service/agent-service-app/src/main/java/com/openjiuwen/service/app/a2a/catalog/A2ARemoteAgentCardRegistry.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved. + */ + +package com.openjiuwen.service.app.a2a.catalog; + +import org.a2aproject.sdk.spec.AgentCard; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Thread-safe in-memory registry of discovered remote A2A AgentCards. + * + * @since 0.1.0 + */ +public class A2ARemoteAgentCardRegistry { + private static final Logger log = LoggerFactory.getLogger(A2ARemoteAgentCardRegistry.class); + + /** + * Default timeout in seconds for remote agent calls. + */ + static final int DEFAULT_TIMEOUT_SECONDS = 300; + + private final ApplicationEventPublisher eventPublisher; + private final Map entries = new ConcurrentHashMap<>(); + private final ReentrantLock updateLock = new ReentrantLock(); + + private long version; + + /** + * Creates a registry without event publication. + * + *

This constructor preserves direct, non-Spring usage. Runtime auto-configuration + * supplies an {@link ApplicationEventPublisher}.

+ */ + public A2ARemoteAgentCardRegistry() { + this(event -> { + }); + } + + /** + * Creates a registry that publishes complete catalog snapshots after updates. + * + * @param eventPublisher the Spring application event publisher + */ + public A2ARemoteAgentCardRegistry(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + /** + * Registers a remote agent card using the default timeout. + * + * @param name the agent name + * @param card the agent card + */ + public void register(String name, AgentCard card) { + register(name, card, DEFAULT_TIMEOUT_SECONDS, false); + } + + /** + * Returns all registered remote agent entries. + * + * @return an unmodifiable copy of all entries + */ + public List getAll() { + return snapshot().entries(); + } + + /** + * Looks up a remote agent entry by name. + * + * @param name the agent name + * @return the entry, or {@link Optional#empty()} if not found + */ + public Optional get(String name) { + return Optional.ofNullable(entries.get(name)); + } + + /** + * Resolves the JSON-RPC URL for a registered remote agent. + * + * @param name the agent name + * @return the JSON-RPC URL, or empty string if not found + */ + public String resolveUrl(String name) { + var entry = entries.get(name); + if (entry == null) { + return ""; + } + var ifaces = entry.card().supportedInterfaces(); + if (ifaces == null || ifaces.isEmpty()) { + return ""; + } + return ifaces.get(0).url(); + } + + /** + * Returns the current complete remote-agent catalog. + * + * @return an immutable, name-sorted catalog snapshot + */ + public RemoteAgentCatalogSnapshot snapshot() { + updateLock.lock(); + try { + return createSnapshot(); + } finally { + updateLock.unlock(); + } + } + + /** + * Registers a remote agent card with a specific timeout. + * + * @param name the agent name + * @param card the agent card + * @param timeoutSeconds the call timeout in seconds + */ + public void register(String name, AgentCard card, int timeoutSeconds) { + register(name, card, timeoutSeconds, false); + } + + /** + * Registers a remote agent card with timeout and invocation mode. + * + * @param name the agent name + * @param card the agent card + * @param timeoutSeconds the call timeout in seconds + * @param isStreaming whether Runtime should prefer a streaming remote invocation + */ + public void register(String name, AgentCard card, int timeoutSeconds, boolean isStreaming) { + RemoteAgentCatalogSnapshot updatedSnapshot; + updateLock.lock(); + try { + entries.put(name, new RemoteAgentEntry(name, card, timeoutSeconds, isStreaming)); + version++; + updatedSnapshot = createSnapshot(); + } finally { + updateLock.unlock(); + } + log.info("Registered remote A2A Agent Card agentName={} catalogVersion={} catalogSize={} streaming={}", name, + updatedSnapshot.version(), updatedSnapshot.entries().size(), isStreaming); + publishCatalogChanged(updatedSnapshot); + } + + private RemoteAgentCatalogSnapshot createSnapshot() { + List sortedEntries = entries.values().stream() + .sorted((left, right) -> left.name().compareTo(right.name())).toList(); + return new RemoteAgentCatalogSnapshot(version, sortedEntries); + } + + private void publishCatalogChanged(RemoteAgentCatalogSnapshot updatedSnapshot) { + try { + eventPublisher.publishEvent(new RemoteAgentCatalogChangedEvent(updatedSnapshot)); + log.info("Published remote A2A Agent catalog change catalogVersion={} catalogSize={}", + updatedSnapshot.version(), updatedSnapshot.entries().size()); + } catch (RuntimeException exception) { + log.error("Failed to publish remote A2A Agent catalog change catalogVersion={} catalogSize={}", + updatedSnapshot.version(), updatedSnapshot.entries().size(), exception); + } + } +} diff --git a/service/agent-service-app/src/main/java/com/openjiuwen/service/app/a2a/catalog/RemoteAgentCatalogChangedEvent.java b/service/agent-service-app/src/main/java/com/openjiuwen/service/app/a2a/catalog/RemoteAgentCatalogChangedEvent.java new file mode 100644 index 00000000..8616d3aa --- /dev/null +++ b/service/agent-service-app/src/main/java/com/openjiuwen/service/app/a2a/catalog/RemoteAgentCatalogChangedEvent.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved. + */ + +package com.openjiuwen.service.app.a2a.catalog; + +/** + * Event published after the remote A2A agent catalog changes. + * + * @param snapshot complete catalog snapshot produced by the registry update + * @since 0.1.1 + */ +public record RemoteAgentCatalogChangedEvent(RemoteAgentCatalogSnapshot snapshot) { +} diff --git a/service/agent-service-app/src/main/java/com/openjiuwen/service/app/a2a/catalog/RemoteAgentCatalogSnapshot.java b/service/agent-service-app/src/main/java/com/openjiuwen/service/app/a2a/catalog/RemoteAgentCatalogSnapshot.java new file mode 100644 index 00000000..63d1799e --- /dev/null +++ b/service/agent-service-app/src/main/java/com/openjiuwen/service/app/a2a/catalog/RemoteAgentCatalogSnapshot.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved. + */ + +package com.openjiuwen.service.app.a2a.catalog; + +import java.util.List; + +/** + * Immutable versioned snapshot of all discovered remote A2A agents. + * + * @param version monotonically increasing registry version + * @param entries complete remote-agent entries sorted by name + * @since 0.1.1 + */ +public record RemoteAgentCatalogSnapshot(long version, List entries) { + /** + * Creates an immutable snapshot. + */ + public RemoteAgentCatalogSnapshot { + entries = List.copyOf(entries); + } +} diff --git a/service/agent-service-app/src/main/java/com/openjiuwen/service/app/a2a/catalog/RemoteAgentEntry.java b/service/agent-service-app/src/main/java/com/openjiuwen/service/app/a2a/catalog/RemoteAgentEntry.java new file mode 100644 index 00000000..a325a8b5 --- /dev/null +++ b/service/agent-service-app/src/main/java/com/openjiuwen/service/app/a2a/catalog/RemoteAgentEntry.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved. + */ + +package com.openjiuwen.service.app.a2a.catalog; + +import org.a2aproject.sdk.spec.AgentCard; + +/** + * Immutable remote A2A Agent registration entry. + * + * @param name remote Agent name + * @param card discovered Agent Card + * @param timeoutSeconds remote call timeout in seconds + * @param isStreaming whether Runtime should prefer streaming invocation + * @since 0.1.1 + */ +public record RemoteAgentEntry(String name, AgentCard card, int timeoutSeconds, boolean isStreaming) { +} diff --git a/service/agent-service-app/src/main/java/com/openjiuwen/service/app/autoconfigure/A2AAutoConfiguration.java b/service/agent-service-app/src/main/java/com/openjiuwen/service/app/autoconfigure/A2AAutoConfiguration.java index bf8882d4..2ef92023 100644 --- a/service/agent-service-app/src/main/java/com/openjiuwen/service/app/autoconfigure/A2AAutoConfiguration.java +++ b/service/agent-service-app/src/main/java/com/openjiuwen/service/app/autoconfigure/A2AAutoConfiguration.java @@ -6,6 +6,7 @@ import com.openjiuwen.service.adapters.common.middleware.MiddlewareProperties; import com.openjiuwen.service.adapters.common.middleware.redis.RedisMiddlewareAutoConfiguration; +import com.openjiuwen.service.app.a2a.catalog.A2ARemoteAgentCardRegistry; import com.openjiuwen.service.app.config.A2AProperties; import com.openjiuwen.service.app.config.SpringEnvironmentConfigProvider; import com.openjiuwen.service.app.controller.a2a.A2AAgentExecutor; @@ -19,7 +20,6 @@ import com.openjiuwen.service.app.controller.a2a.RedisTaskStore; import com.openjiuwen.service.app.controller.a2a.WriteThrottlingTaskStore; import com.openjiuwen.service.app.controller.a2a.client.A2AAgentCardDiscovery; -import com.openjiuwen.service.app.controller.a2a.client.A2ARemoteAgentCardRegistry; import com.openjiuwen.service.app.controller.a2a.client.A2ARemoteAgentClient; import com.openjiuwen.service.app.controller.a2a.client.RemoteAgentCaller; import com.openjiuwen.service.app.controller.a2a.client.RemoteAgentCardResolver; @@ -50,6 +50,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; @@ -233,12 +234,13 @@ public A2AAgentExecutor a2aAgentExecutor(ServeOrchestrator orchestrator, A2AProt /** * Creates the remote agent card registry bean. * + * @param eventPublisher the Spring application event publisher * @return the remote agent card registry */ @Bean @ConditionalOnMissingBean - public A2ARemoteAgentCardRegistry a2aRemoteAgentCardRegistry() { - return new A2ARemoteAgentCardRegistry(); + public A2ARemoteAgentCardRegistry a2aRemoteAgentCardRegistry(ApplicationEventPublisher eventPublisher) { + return new A2ARemoteAgentCardRegistry(eventPublisher); } /** diff --git a/service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/client/A2AAgentCardDiscovery.java b/service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/client/A2AAgentCardDiscovery.java index 525d7d36..6e10b60e 100644 --- a/service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/client/A2AAgentCardDiscovery.java +++ b/service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/client/A2AAgentCardDiscovery.java @@ -4,6 +4,7 @@ package com.openjiuwen.service.app.controller.a2a.client; +import com.openjiuwen.service.app.a2a.catalog.A2ARemoteAgentCardRegistry; import com.openjiuwen.service.app.config.A2AProperties; import com.openjiuwen.service.app.config.A2AProperties.RemoteAgentProperties; import com.openjiuwen.service.spec.paths.A2AServicePaths; diff --git a/service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentCardRegistry.java b/service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentCardRegistry.java deleted file mode 100644 index 0acb76ad..00000000 --- a/service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentCardRegistry.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved. - */ - -package com.openjiuwen.service.app.controller.a2a.client; - -import org.a2aproject.sdk.spec.AgentCard; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Thread-safe in-memory registry of discovered remote A2A AgentCards. - * - * @since 0.1.0 - */ -@Component -public class A2ARemoteAgentCardRegistry { - /** - * Default timeout in seconds for remote agent calls. - */ - static final int DEFAULT_TIMEOUT_SECONDS = 300; - - private final Map entries = new ConcurrentHashMap<>(); - - /** - * Registers a remote agent card using the default timeout. - * - * @param name the agent name - * @param card the agent card - */ - public void register(String name, AgentCard card) { - register(name, card, DEFAULT_TIMEOUT_SECONDS, false); - } - - /** - * Returns all registered remote agent entries. - * - * @return an unmodifiable copy of all entries - */ - public List getAll() { - return List.copyOf(entries.values()); - } - - /** - * Looks up a remote agent entry by name. - * - * @param name the agent name - * @return the entry, or {@link Optional#empty()} if not found - */ - public Optional get(String name) { - return Optional.ofNullable(entries.get(name)); - } - - /** - * Resolves the JSON-RPC URL for a registered remote agent. - * - * @param name the agent name - * @return the JSON-RPC URL, or empty string if not found - */ - public String resolveUrl(String name) { - var entry = entries.get(name); - if (entry == null) { - return ""; - } - var ifaces = entry.card().supportedInterfaces(); - if (ifaces == null || ifaces.isEmpty()) { - return ""; - } - return ifaces.get(0).url(); - } - - /** - * A registered remote agent entry holding the card and timeout configuration. - */ - public record RemoteAgentEntry(String name, AgentCard card, int timeoutSeconds, boolean isStreaming) {} - - /** - * Registers a remote agent card with a specific timeout. - * - * @param name the agent name - * @param card the agent card - * @param timeoutSeconds the call timeout in seconds - */ - public void register(String name, AgentCard card, int timeoutSeconds) { - register(name, card, timeoutSeconds, false); - } - - /** - * Registers a remote agent card with timeout and invocation mode. - * - * @param name the agent name - * @param card the agent card - * @param timeoutSeconds the call timeout in seconds - * @param isStreaming whether Runtime should prefer a streaming remote invocation - */ - public void register(String name, AgentCard card, int timeoutSeconds, boolean isStreaming) { - entries.put(name, new RemoteAgentEntry(name, card, timeoutSeconds, isStreaming)); - } -} diff --git a/service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentClient.java b/service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentClient.java index b98c04b5..41c913ce 100644 --- a/service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentClient.java +++ b/service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentClient.java @@ -4,6 +4,8 @@ package com.openjiuwen.service.app.controller.a2a.client; +import com.openjiuwen.service.app.a2a.catalog.A2ARemoteAgentCardRegistry; +import com.openjiuwen.service.app.a2a.catalog.RemoteAgentEntry; import com.openjiuwen.service.app.controller.a2a.A2aPartContent; import jakarta.annotation.PreDestroy; @@ -123,8 +125,7 @@ public A2ARemoteAgentClient(A2ARemoteAgentCardRegistry registry, int ioConcurren * @param contextId * the context/conversation ID */ - private record RemoteCallSetup(A2ARemoteAgentCardRegistry.RemoteAgentEntry entry, MessageSendParams params, - String contextId) { + private record RemoteCallSetup(RemoteAgentEntry entry, MessageSendParams params, String contextId) { } private record TaskOutcome(String taskId, TaskState state, String statusText, Task task) { @@ -189,7 +190,7 @@ private static Optional callbackConfig(RemoteCall ca * whether the client should be in streaming mode * @return the SDK client */ - private Client createClient(A2ARemoteAgentCardRegistry.RemoteAgentEntry entry, boolean isStreaming) { + private Client createClient(RemoteAgentEntry entry, boolean isStreaming) { AgentCard card = entry.card(); ClientCacheKey key = new ClientCacheKey(entry.name(), endpoint(card), isStreaming); return withApplicationClassLoader(() -> clientCache.computeIfAbsent(key, @@ -234,7 +235,7 @@ private static T withApplicationClassLoader(Supplier action) { @Override public CompletableFuture callOutcome(RemoteCall call, RemoteAgentCaller.EventObserver eventObserver) { - A2ARemoteAgentCardRegistry.RemoteAgentEntry entry = registry.get(call.agentName()) + RemoteAgentEntry entry = registry.get(call.agentName()) .orElseThrow(() -> new IllegalStateException("Unknown remote agent: " + call.agentName())); boolean isStreaming = entry.isStreaming() && call.isCallerStreaming(); return callOutcome(call, eventObserver, isStreaming); diff --git a/service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/client/RemoteAgentCardResolver.java b/service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/client/RemoteAgentCardResolver.java index 2c4c8388..32936701 100644 --- a/service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/client/RemoteAgentCardResolver.java +++ b/service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/client/RemoteAgentCardResolver.java @@ -4,6 +4,8 @@ package com.openjiuwen.service.app.controller.a2a.client; +import com.openjiuwen.service.app.a2a.catalog.A2ARemoteAgentCardRegistry; + /** * SPI for resolving a remote agent's A2A URLs by {@code agentId}. * diff --git a/service/agent-service-app/src/test/java/com/openjiuwen/service/app/a2a/catalog/A2ARemoteAgentCardRegistryTest.java b/service/agent-service-app/src/test/java/com/openjiuwen/service/app/a2a/catalog/A2ARemoteAgentCardRegistryTest.java new file mode 100644 index 00000000..de6add7b --- /dev/null +++ b/service/agent-service-app/src/test/java/com/openjiuwen/service/app/a2a/catalog/A2ARemoteAgentCardRegistryTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved. + */ + +package com.openjiuwen.service.app.a2a.catalog; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +import org.a2aproject.sdk.spec.AgentCard; +import org.junit.jupiter.api.Test; +import org.springframework.context.ApplicationEventPublisher; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.IntStream; + +/** Tests remote Agent Card catalog snapshots and update publication. */ +class A2ARemoteAgentCardRegistryTest { + @Test + void initialSnapshotIsEmptyAndImmutable() { + A2ARemoteAgentCardRegistry registry = new A2ARemoteAgentCardRegistry(); + + RemoteAgentCatalogSnapshot snapshot = registry.snapshot(); + + assertThat(snapshot.version()).isZero(); + assertThat(snapshot.entries()).isEmpty(); + assertThatThrownBy(() -> snapshot.entries().add(entry("other"))) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void registrationPublishesCompleteSortedSnapshots() { + List events = new CopyOnWriteArrayList<>(); + A2ARemoteAgentCardRegistry registry = registryWithEvents(events); + + registry.register("weather", mock(AgentCard.class), 30, true); + registry.register("balance", mock(AgentCard.class), 60, false); + + assertThat(events).hasSize(2); + assertThat(events.get(0).snapshot().version()).isEqualTo(1L); + assertThat(events.get(0).snapshot().entries()).extracting(RemoteAgentEntry::name).containsExactly("weather"); + assertThat(events.get(1).snapshot().version()).isEqualTo(2L); + assertThat(events.get(1).snapshot().entries()).extracting(RemoteAgentEntry::name).containsExactly("balance", + "weather"); + assertThat(registry.getAll()).containsExactlyElementsOf(events.get(1).snapshot().entries()); + } + + @Test + void replacingSameNameCreatesNewVersion() { + List events = new CopyOnWriteArrayList<>(); + A2ARemoteAgentCardRegistry registry = registryWithEvents(events); + AgentCard firstCard = mock(AgentCard.class); + AgentCard secondCard = mock(AgentCard.class); + + registry.register("transfer", firstCard, 30, false); + registry.register("transfer", secondCard, 90, true); + + assertThat(registry.snapshot().version()).isEqualTo(2L); + assertThat(registry.snapshot().entries()).singleElement().satisfies(entry -> { + assertThat(entry.card()).isSameAs(secondCard); + assertThat(entry.timeoutSeconds()).isEqualTo(90); + assertThat(entry.isStreaming()).isTrue(); + }); + assertThat(events).extracting(event -> event.snapshot().version()).containsExactly(1L, 2L); + } + + @Test + void concurrentRegistrationProducesUniqueCompleteVersions() { + List events = new CopyOnWriteArrayList<>(); + A2ARemoteAgentCardRegistry registry = registryWithEvents(events); + + IntStream.range(0, 32).parallel() + .forEach(index -> registry.register("agent-" + index, mock(AgentCard.class), 30, false)); + + assertThat(registry.snapshot().version()).isEqualTo(32L); + assertThat(registry.snapshot().entries()).hasSize(32); + assertThat(events).hasSize(32); + assertThat(events).extracting(event -> event.snapshot().version()).doesNotHaveDuplicates() + .containsExactlyInAnyOrderElementsOf(IntStream.rangeClosed(1, 32).mapToObj(Long::valueOf).toList()); + assertThat(events) + .allSatisfy(event -> assertThat(event.snapshot().entries()).hasSize((int) event.snapshot().version())); + } + + @Test + void publicationFailureDoesNotFailCompletedRegistryUpdate() { + A2ARemoteAgentCardRegistry registry = new A2ARemoteAgentCardRegistry(event -> { + throw new IllegalStateException("listener failed"); + }); + + registry.register("balance", mock(AgentCard.class)); + + assertThat(registry.snapshot().version()).isEqualTo(1L); + assertThat(registry.get("balance")).isPresent(); + } + + private static A2ARemoteAgentCardRegistry registryWithEvents(List events) { + ApplicationEventPublisher publisher = event -> { + if (event instanceof RemoteAgentCatalogChangedEvent catalogEvent) { + events.add(catalogEvent); + return; + } + throw new IllegalArgumentException("Unexpected event type: " + event.getClass().getName()); + }; + return new A2ARemoteAgentCardRegistry(publisher); + } + + private static RemoteAgentEntry entry(String name) { + return new RemoteAgentEntry(name, mock(AgentCard.class), 30, false); + } +} diff --git a/service/agent-service-app/src/test/java/com/openjiuwen/service/app/autoconfigure/A2AAutoConfigurationTest.java b/service/agent-service-app/src/test/java/com/openjiuwen/service/app/autoconfigure/A2AAutoConfigurationTest.java index 7b275a70..5a1dc046 100644 --- a/service/agent-service-app/src/test/java/com/openjiuwen/service/app/autoconfigure/A2AAutoConfigurationTest.java +++ b/service/agent-service-app/src/test/java/com/openjiuwen/service/app/autoconfigure/A2AAutoConfigurationTest.java @@ -7,14 +7,22 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +import com.openjiuwen.service.app.a2a.catalog.A2ARemoteAgentCardRegistry; +import com.openjiuwen.service.app.a2a.catalog.RemoteAgentCatalogChangedEvent; +import com.openjiuwen.service.app.a2a.catalog.RemoteAgentEntry; import com.openjiuwen.service.app.config.SpringEnvironmentConfigProvider; import com.openjiuwen.service.spec.spi.ServeOrchestrator; import org.a2aproject.sdk.server.config.A2AConfigProvider; import org.a2aproject.sdk.server.requesthandlers.RequestHandler; +import org.a2aproject.sdk.spec.AgentCard; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.PayloadApplicationEvent; + +import java.util.ArrayList; +import java.util.List; /** * Auto-configuration tests for A2A SDK configuration. @@ -68,4 +76,25 @@ void a2aConfigProviderAllowsCustomProviderOverride() { assertThat(context.getBean(A2AConfigProvider.class)).isSameAs(customProvider); }); } + + @Test + void remoteAgentCardRegistryPublishesCatalogChanges() { + contextRunner.run(context -> { + List events = new ArrayList<>(); + context.getSourceApplicationContext().addApplicationListener(event -> { + if (event instanceof PayloadApplicationEvent payloadEvent + && payloadEvent.getPayload() instanceof RemoteAgentCatalogChangedEvent catalogChangedEvent) { + events.add(catalogChangedEvent); + } + }); + + A2ARemoteAgentCardRegistry registry = context.getBean(A2ARemoteAgentCardRegistry.class); + registry.register("balance", mock(AgentCard.class)); + + assertThat(events).singleElement().satisfies(event -> { + assertThat(event.snapshot().version()).isEqualTo(1L); + assertThat(event.snapshot().entries()).extracting(RemoteAgentEntry::name).containsExactly("balance"); + }); + }); + } } diff --git a/service/agent-service-app/src/test/java/com/openjiuwen/service/app/controller/a2a/client/A2AAgentCardDiscoveryTest.java b/service/agent-service-app/src/test/java/com/openjiuwen/service/app/controller/a2a/client/A2AAgentCardDiscoveryTest.java index 35326e60..634f750f 100644 --- a/service/agent-service-app/src/test/java/com/openjiuwen/service/app/controller/a2a/client/A2AAgentCardDiscoveryTest.java +++ b/service/agent-service-app/src/test/java/com/openjiuwen/service/app/controller/a2a/client/A2AAgentCardDiscoveryTest.java @@ -6,6 +6,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.openjiuwen.service.app.a2a.catalog.A2ARemoteAgentCardRegistry; import com.openjiuwen.service.app.config.A2AProperties; import com.openjiuwen.service.app.config.A2AProperties.RemoteAgentProperties; diff --git a/service/agent-service-app/src/test/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentClientClassLoaderTest.java b/service/agent-service-app/src/test/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentClientClassLoaderTest.java index 0ba0e19c..6d38cc49 100644 --- a/service/agent-service-app/src/test/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentClientClassLoaderTest.java +++ b/service/agent-service-app/src/test/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentClientClassLoaderTest.java @@ -20,6 +20,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.openjiuwen.service.app.a2a.catalog.A2ARemoteAgentCardRegistry; +import com.openjiuwen.service.app.a2a.catalog.RemoteAgentEntry; + import org.a2aproject.sdk.client.Client; import org.a2aproject.sdk.client.ClientBuilder; import org.a2aproject.sdk.client.MessageEvent; @@ -127,8 +130,7 @@ void timeoutAppliesWhileNonStreamingSdkCallIsBlocked() { var outcome = remoteClient.callOutcome(remoteCall("timeout-agent"), mock(RemoteAgentCaller.EventObserver.class)); - assertThatThrownBy(() -> outcome.get(2, TimeUnit.SECONDS)) - .hasCauseInstanceOf(TimeoutException.class); + assertThatThrownBy(() -> outcome.get(2, TimeUnit.SECONDS)).hasCauseInstanceOf(TimeoutException.class); } finally { release.countDown(); remoteClient.shutdown(); @@ -153,9 +155,8 @@ void synchronousSdkFailureCompletesOutcomeImmediately() { var outcome = remoteClient.callOutcome(remoteCall("failing-agent"), mock(RemoteAgentCaller.EventObserver.class)); - assertThatThrownBy(() -> outcome.get(1, TimeUnit.SECONDS)) - .hasCauseInstanceOf(A2AClientException.class) - .hasRootCauseMessage("SDK send failed"); + assertThatThrownBy(() -> outcome.get(1, TimeUnit.SECONDS)).hasCauseInstanceOf(A2AClientException.class) + .hasRootCauseMessage("SDK send failed"); } finally { remoteClient.shutdown(); } @@ -168,8 +169,8 @@ void synchronousSdkRuntimeFailureCompletesOutcomeImmediately() { registry.register("runtime-failing-agent", card, 30, false); ClientBuilder builder = mock(ClientBuilder.class); Client sdkClient = mock(Client.class); - doThrow(new IllegalArgumentException("invalid SDK event")) - .when(sdkClient).sendMessage(any(MessageSendParams.class), anyList(), any(), isNull()); + doThrow(new IllegalArgumentException("invalid SDK event")).when(sdkClient) + .sendMessage(any(MessageSendParams.class), anyList(), any(), isNull()); A2ARemoteAgentClient remoteClient = new A2ARemoteAgentClient(registry); try (MockedStatic clientFactory = mockStatic(Client.class)) { @@ -179,8 +180,7 @@ void synchronousSdkRuntimeFailureCompletesOutcomeImmediately() { mock(RemoteAgentCaller.EventObserver.class)); assertThatThrownBy(() -> outcome.get(1, TimeUnit.SECONDS)) - .hasCauseInstanceOf(IllegalArgumentException.class) - .hasRootCauseMessage("invalid SDK event"); + .hasCauseInstanceOf(IllegalArgumentException.class).hasRootCauseMessage("invalid SDK event"); } finally { remoteClient.shutdown(); } @@ -195,10 +195,11 @@ void directMessageEventCompletesCallWithAllTextParts() throws Exception { ClientBuilder builder = mock(ClientBuilder.class); Client sdkClient = mock(Client.class); Message message = Message.builder().role(Message.Role.ROLE_AGENT) - .parts(List.>of(new TextPart("hello "), new TextPart("world"))).build(); + .parts(List.>of(new TextPart("hello "), new TextPart("world"))).build(); doAnswer(invocation -> { - @SuppressWarnings("unchecked") List> consumers = invocation.getArgument(1); + @SuppressWarnings("unchecked") + List> consumers = invocation + .getArgument(1); consumers.get(0).accept(new MessageEvent(message), card); return null; }).when(sdkClient).sendMessage(any(MessageSendParams.class), anyList(), any(), isNull()); @@ -223,14 +224,17 @@ void completedTaskAggregatesAllArtifacts() throws Exception { ClientBuilder builder = mock(ClientBuilder.class); Client sdkClient = mock(Client.class); Task task = Task.builder().id("remote-task").contextId("remote-context") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .artifacts(List.of( - org.a2aproject.sdk.spec.Artifact.builder().artifactId("a").parts(new TextPart("hello ")).build(), - org.a2aproject.sdk.spec.Artifact.builder().artifactId("b").parts(new TextPart("world")).build())) - .build(); + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .artifacts(List.of( + org.a2aproject.sdk.spec.Artifact.builder().artifactId("a").parts(new TextPart("hello ")) + .build(), + org.a2aproject.sdk.spec.Artifact.builder().artifactId("b").parts(new TextPart("world")) + .build())) + .build(); doAnswer(invocation -> { - @SuppressWarnings("unchecked") List> consumers = invocation.getArgument(1); + @SuppressWarnings("unchecked") + List> consumers = invocation + .getArgument(1); consumers.get(0).accept(new TaskEvent(task), card); return null; }).when(sdkClient).sendMessage(any(MessageSendParams.class), anyList(), any(), isNull()); @@ -255,14 +259,14 @@ void completedTaskWithoutArtifactsUsesStatusMessage() throws Exception { ClientBuilder builder = mock(ClientBuilder.class); Client sdkClient = mock(Client.class); Message statusMessage = Message.builder().role(Message.Role.ROLE_AGENT) - .parts(List.>of(new TextPart("status result"))).build(); + .parts(List.>of(new TextPart("status result"))).build(); Task task = Task.builder().id("remote-task").contextId("remote-context") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED, statusMessage, null)) - .artifacts(List.of()) - .build(); + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED, statusMessage, null)).artifacts(List.of()) + .build(); doAnswer(invocation -> { - @SuppressWarnings("unchecked") List> consumers = invocation.getArgument(1); + @SuppressWarnings("unchecked") + List> consumers = invocation + .getArgument(1); consumers.get(0).accept(new TaskEvent(task), card); return null; }).when(sdkClient).sendMessage(any(MessageSendParams.class), anyList(), any(), isNull()); @@ -355,10 +359,9 @@ void createClientUsesApplicationClassLoaderForTransportDiscovery() throws Except Thread.currentThread().setContextClassLoader(new NoServicesClassLoader(original)); try { A2ARemoteAgentClient client = new A2ARemoteAgentClient(new A2ARemoteAgentCardRegistry()); - A2ARemoteAgentCardRegistry.RemoteAgentEntry entry = - new A2ARemoteAgentCardRegistry.RemoteAgentEntry("remote", testCard(), 30, true); - Method createClient = A2ARemoteAgentClient.class.getDeclaredMethod("createClient", - A2ARemoteAgentCardRegistry.RemoteAgentEntry.class, boolean.class); + RemoteAgentEntry entry = new RemoteAgentEntry("remote", testCard(), 30, true); + Method createClient = A2ARemoteAgentClient.class.getDeclaredMethod("createClient", RemoteAgentEntry.class, + boolean.class); createClient.setAccessible(true); assertThatCode(() -> createClient.invoke(client, entry, true)).doesNotThrowAnyException(); @@ -376,8 +379,8 @@ private static AgentCard testCard(String url) { .capabilities(new AgentCapabilities(true, false, false, List.of())).defaultInputModes(List.of("text")) .defaultOutputModes(List.of("text")).skills(List.of()).securitySchemes(Collections.emptyMap()) .securityRequirements(List.of()) - .supportedInterfaces(List.of(new AgentInterface("JSONRPC", url, null, "1.0"))) - .url(url).preferredTransport("JSONRPC").additionalInterfaces(List.of()).build(); + .supportedInterfaces(List.of(new AgentInterface("JSONRPC", url, null, "1.0"))).url(url) + .preferredTransport("JSONRPC").additionalInterfaces(List.of()).build(); } private static void stubClient(MockedStatic factory, AgentCard card, ClientBuilder builder, Client client) { @@ -392,8 +395,7 @@ private static RemoteCall remoteCall(String agentName) { } private static RemoteCall remoteCall(String agentName, boolean isCallerStreaming) { - return new RemoteCall(agentName, "hello", "context", null, Map.of(), Map.of(), - isCallerStreaming); + return new RemoteCall(agentName, "hello", "context", null, Map.of(), Map.of(), isCallerStreaming); } private static final class NoServicesClassLoader extends ClassLoader { diff --git a/service/agent-service-app/src/test/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentClientResultTest.java b/service/agent-service-app/src/test/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentClientResultTest.java index 8b461ce4..efaf78ca 100644 --- a/service/agent-service-app/src/test/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentClientResultTest.java +++ b/service/agent-service-app/src/test/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentClientResultTest.java @@ -14,6 +14,7 @@ import com.google.gson.Gson; import com.google.gson.JsonParser; +import com.openjiuwen.service.app.a2a.catalog.A2ARemoteAgentCardRegistry; import com.openjiuwen.service.app.controller.a2a.ChunkMapper; import com.openjiuwen.service.spec.dto.QueryChunk; diff --git a/service/agent-service-app/src/test/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentClientStreamingLifecycleTest.java b/service/agent-service-app/src/test/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentClientStreamingLifecycleTest.java index de985f81..d7ec8db0 100644 --- a/service/agent-service-app/src/test/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentClientStreamingLifecycleTest.java +++ b/service/agent-service-app/src/test/java/com/openjiuwen/service/app/controller/a2a/client/A2ARemoteAgentClientStreamingLifecycleTest.java @@ -8,6 +8,7 @@ import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.Mockito.mock; +import com.openjiuwen.service.app.a2a.catalog.A2ARemoteAgentCardRegistry; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; diff --git a/service/agent-service-app/src/test/java/com/openjiuwen/service/app/it/DualRuntimeCallbackIntegrationTest.java b/service/agent-service-app/src/test/java/com/openjiuwen/service/app/it/DualRuntimeCallbackIntegrationTest.java index 15f0bc64..dcd3883c 100644 --- a/service/agent-service-app/src/test/java/com/openjiuwen/service/app/it/DualRuntimeCallbackIntegrationTest.java +++ b/service/agent-service-app/src/test/java/com/openjiuwen/service/app/it/DualRuntimeCallbackIntegrationTest.java @@ -7,7 +7,8 @@ import static org.assertj.core.api.Assertions.assertThat; import com.fasterxml.jackson.databind.ObjectMapper; -import com.openjiuwen.service.app.controller.a2a.client.A2ARemoteAgentCardRegistry; +import com.openjiuwen.service.app.a2a.catalog.A2ARemoteAgentCardRegistry; +import com.openjiuwen.service.app.it.DualRuntimeCallbackIntegrationTest.CallerRuntimeApplication; import com.openjiuwen.service.spec.dto.QueryChunk; import com.openjiuwen.service.spec.dto.QueryResponse; import com.openjiuwen.service.spec.dto.ServeRequest; @@ -30,6 +31,7 @@ import org.springframework.boot.resttestclient.TestRestTemplate; import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; @@ -52,12 +54,8 @@ /** * Dual-runtime happy path for A2A callback-mode remote invocation. */ -@SpringBootTest(classes = DualRuntimeCallbackIntegrationTest.CallerRuntimeApplication.class, - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - properties = { - "spring.application.name=caller-it", - "openjiuwen.service.a2a.push-notifications=true" - }) +@SpringBootTest(classes = CallerRuntimeApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT, properties = { + "spring.application.name=caller-it", "openjiuwen.service.a2a.push-notifications=true"}) @AutoConfigureTestRestTemplate @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) class DualRuntimeCallbackIntegrationTest { @@ -83,12 +81,8 @@ class DualRuntimeCallbackIntegrationTest { @BeforeEach void startCallee() { - callee = new SpringApplicationBuilder(CalleeRuntimeApplication.class) - .properties( - "server.port=0", - "spring.application.name=callee-it", - "openjiuwen.service.a2a.push-notifications=true") - .run(); + callee = new SpringApplicationBuilder(CalleeRuntimeApplication.class).properties("server.port=0", + "spring.application.name=callee-it", "openjiuwen.service.a2a.push-notifications=true").run(); registry.register("callee", card(calleePort()), 5, false); } @@ -103,45 +97,34 @@ void stopCallee() { @SuppressWarnings("unchecked") void callerDelegatesToCalleeWaitsForCallbackThenResumesOriginalTask() throws Exception { String callbackUrl = "http://127.0.0.1:" + callerPort + "/a2a/push-notifications/callback"; - Map firstBody = json(postA2a(rpc("SendMessage", "dual-runtime-start", Map.of( - "metadata", Map.of( - CALLBACK_URL_METADATA, callbackUrl, - CALLBACK_ID_METADATA, "push-dual-runtime"), - "message", Map.of( - "role", "ROLE_USER", - "messageId", "msg-dual-runtime-start", - "contextId", "ctx-dual-runtime", - "parts", List.of(Map.of("kind", "text", "text", "start dual runtime"))))))); + Map firstBody = json(postA2a(rpc("SendMessage", "dual-runtime-start", Map.of("metadata", + Map.of(CALLBACK_URL_METADATA, callbackUrl, CALLBACK_ID_METADATA, "push-dual-runtime"), "message", + Map.of("role", "ROLE_USER", "messageId", "msg-dual-runtime-start", "contextId", "ctx-dual-runtime", + "parts", List.of(Map.of("kind", "text", "text", "start dual runtime"))))))); Map waitingTask = taskFrom(firstBody); String taskId = String.valueOf(waitingTask.get("id")); assertThat(((Map) waitingTask.get("status")).get("state")) - .isEqualTo("TASK_STATE_INPUT_REQUIRED"); + .isEqualTo("TASK_STATE_INPUT_REQUIRED"); Map readyBatch = awaitReadyRemoteBatch(taskId); List> members = (List>) readyBatch.get("members"); assertThat(readyBatch).containsEntry("state", "READY_TO_RESUME"); - assertThat(members).singleElement().satisfies(member -> assertThat(member) - .containsEntry("agentName", "callee") - .containsEntry("state", "COMPLETED") - .containsEntry("resultCategory", "COMPLETED")); + assertThat(members).singleElement().satisfies(member -> assertThat(member).containsEntry("agentName", "callee") + .containsEntry("state", "COMPLETED").containsEntry("resultCategory", "COMPLETED")); assertThat(String.valueOf(members.get(0).get("result"))).contains("callee result:delegate:start dual runtime"); - Map resumedBody = json(postA2a(rpc("SendMessage", "dual-runtime-resume", Map.of( - "message", Map.of( - "role", "ROLE_USER", - "messageId", "msg-dual-runtime-resume", - "taskId", taskId, - "contextId", "ctx-dual-runtime", - "parts", List.of(Map.of("kind", "text", "text", "continue"))))))); + Map resumedBody = json(postA2a(rpc("SendMessage", "dual-runtime-resume", + Map.of("message", + Map.of("role", "ROLE_USER", "messageId", "msg-dual-runtime-resume", "taskId", taskId, + "contextId", "ctx-dual-runtime", "parts", + List.of(Map.of("kind", "text", "text", "continue"))))))); Map completedTask = taskFrom(resumedBody); assertThat(completedTask.get("id")).isEqualTo(taskId); - assertThat(((Map) completedTask.get("status")).get("state")) - .isEqualTo("TASK_STATE_COMPLETED"); - assertThat(allArtifactText(completedTask)) - .contains("caller resumed") - .contains("callee result:delegate:start dual runtime"); + assertThat(((Map) completedTask.get("status")).get("state")).isEqualTo("TASK_STATE_COMPLETED"); + assertThat(allArtifactText(completedTask)).contains("caller resumed") + .contains("callee result:delegate:start dual runtime"); } private Map awaitReadyRemoteBatch(String taskId) throws Exception { @@ -151,8 +134,7 @@ private Map awaitReadyRemoteBatch(String taskId) throws Exceptio while (Instant.now().isBefore(deadline)) { Task shadow = taskStore.get(shadowTaskId); if (shadow != null && shadow.metadata() != null - && shadow.metadata().get("_remote_batch") instanceof Map batch - && isCompletedBatch(batch)) { + && shadow.metadata().get("_remote_batch") instanceof Map batch && isCompletedBatch(batch)) { Map result = new LinkedHashMap<>(); batch.forEach((key, value) -> result.put(String.valueOf(key), value)); return result; @@ -160,16 +142,16 @@ && isCompletedBatch(batch)) { lastObserved = shadow == null ? "" : String.valueOf(shadow.metadata()); Thread.sleep(100); } - throw new AssertionError("remote batch was not recovered for " + shadowTaskId - + ", lastObserved=" + lastObserved); + throw new AssertionError( + "remote batch was not recovered for " + shadowTaskId + ", lastObserved=" + lastObserved); } private static boolean isCompletedBatch(Map batch) { if (!"READY_TO_RESUME".equals(batch.get("state")) || !(batch.get("members") instanceof List members)) { return false; } - return members.stream().allMatch(member -> member instanceof Map item - && "COMPLETED".equals(item.get("state"))); + return members.stream() + .allMatch(member -> member instanceof Map item && "COMPLETED".equals(item.get("state"))); } private ResponseEntity postA2a(Map body) { @@ -224,30 +206,17 @@ private static String allArtifactText(Map task) { private static AgentCard card(int port) { String url = "http://127.0.0.1:" + port + "/a2a"; - return AgentCard.builder() - .name("callee") - .description("callee") - .provider(new AgentProvider("", "")) - .version("1.0") - .capabilities(new AgentCapabilities(false, true, false, List.of())) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .securitySchemes(Collections.emptyMap()) - .securityRequirements(List.of()) - .supportedInterfaces(List.of(new AgentInterface("JSONRPC", url, null, "1.0"))) - .url(url) - .preferredTransport("JSONRPC") - .additionalInterfaces(List.of()) - .build(); + return AgentCard.builder().name("callee").description("callee").provider(new AgentProvider("", "")) + .version("1.0").capabilities(new AgentCapabilities(false, true, false, List.of())) + .defaultInputModes(List.of("text")).defaultOutputModes(List.of("text")).skills(List.of()) + .securitySchemes(Collections.emptyMap()).securityRequirements(List.of()) + .supportedInterfaces(List.of(new AgentInterface("JSONRPC", url, null, "1.0"))).url(url) + .preferredTransport("JSONRPC").additionalInterfaces(List.of()).build(); } @SpringBootConfiguration @EnableAutoConfiguration - @ComponentScan(basePackages = { - "com.openjiuwen.service.app.controller", - "com.openjiuwen.service.app.lifecycle" - }) + @ComponentScan(basePackages = {"com.openjiuwen.service.app.controller", "com.openjiuwen.service.app.lifecycle"}) static class CallerRuntimeApplication { @Bean @Primary @@ -258,10 +227,7 @@ AgentHandler callerHandler() { @SpringBootConfiguration @EnableAutoConfiguration - @ComponentScan(basePackages = { - "com.openjiuwen.service.app.controller", - "com.openjiuwen.service.app.lifecycle" - }) + @ComponentScan(basePackages = {"com.openjiuwen.service.app.controller", "com.openjiuwen.service.app.lifecycle"}) static class CalleeRuntimeApplication { @Bean @Primary @@ -277,17 +243,13 @@ public QueryResponse query(ServeRequest request) { if (results instanceof Map remoteResults) { return response(request, "caller resumed:" + remoteResults.get("call-callee")); } - return new QueryResponse(Map.of( - "role", "assistant", - "_interrupt", Map.of( - "batchId", "dual-runtime-batch", - "items", List.of(Map.of( - "index", 0, - "toolCallId", "call-callee", - "toolName", "callee-tool", - "message", "delegate:" + request.lastUserQuery(), - "context", Map.of("_interrupt_kind", "a2a_delegate", "agentName", "callee"))))), - request.getConversationId()); + return new QueryResponse( + Map.of("role", "assistant", "_interrupt", + Map.of("batchId", "dual-runtime-batch", "items", + List.of(Map.of("index", 0, "toolCallId", "call-callee", "toolName", "callee-tool", + "message", "delegate:" + request.lastUserQuery(), "context", + Map.of("_interrupt_kind", "a2a_delegate", "agentName", "callee"))))), + request.getConversationId()); } @Override diff --git a/service/agent-service-app/src/test/java/com/openjiuwen/service/app/it/DualRuntimeFailureIntegrationTest.java b/service/agent-service-app/src/test/java/com/openjiuwen/service/app/it/DualRuntimeFailureIntegrationTest.java index c29dc7e0..58d21c1c 100644 --- a/service/agent-service-app/src/test/java/com/openjiuwen/service/app/it/DualRuntimeFailureIntegrationTest.java +++ b/service/agent-service-app/src/test/java/com/openjiuwen/service/app/it/DualRuntimeFailureIntegrationTest.java @@ -7,7 +7,7 @@ import static org.assertj.core.api.Assertions.assertThat; import com.fasterxml.jackson.databind.ObjectMapper; -import com.openjiuwen.service.app.controller.a2a.client.A2ARemoteAgentCardRegistry; +import com.openjiuwen.service.app.a2a.catalog.A2ARemoteAgentCardRegistry; import com.openjiuwen.service.spec.dto.QueryChunk; import com.openjiuwen.service.spec.dto.QueryResponse; import com.openjiuwen.service.spec.dto.ServeRequest;