From cb86323e1941e18b48b15429f511ffa78b4bd6ec Mon Sep 17 00:00:00 2001 From: Aref Behboudi Date: Mon, 6 Apr 2026 12:30:08 +0300 Subject: [PATCH 1/2] Multi-Agent Runtime --- .../java/ai/javaclaw/JavaClawApplication.java | 9 +- .../ai/javaclaw/api/AgentsController.java | 54 +++++ .../java/ai/javaclaw/chat/ChatChannel.java | 30 ++- .../main/java/ai/javaclaw/chat/ChatHtml.java | 23 +- .../chat/ws/ChatWebSocketHandler.java | 51 ++++- .../onboarding/steps/S2_ProviderStep.java | 79 ++++++- .../onboarding/steps/S3_CredentialsStep.java | 101 +++++++-- .../onboarding/steps/S4_AgentMdStep.java | 56 ++++- app/src/main/resources/application.yaml | 14 +- .../main/resources/templates/agents.html.peb | 127 +++++++++++ .../main/resources/templates/chat.html.peb | 10 +- .../onboarding/steps/S2-provider.html.peb | 10 +- .../onboarding/steps/S3-credentials.html.peb | 12 +- .../ai/javaclaw/api/AgentsControllerTest.java | 44 ++++ .../api/OnboardingControllerTest.java | 6 +- .../ai/javaclaw/chat/ChatChannelTest.java | 62 +++-- .../chat/ws/ChatWebSocketHandlerTest.java | 25 +- .../onboarding/steps/S4_AgentMdStepTest.java | 87 +++++++ app/workspace/app.mv.db | Bin 53248 -> 0 bytes base/build.gradle | 1 + .../ai/javaclaw/JavaClawConfiguration.java | 80 +------ .../main/java/ai/javaclaw/agent/Agent.java | 12 +- .../java/ai/javaclaw/agent/DefaultAgent.java | 58 ++++- .../FileSystemChatMemoryRepository.java | 72 +++++- .../agents/AgentChatClientFactory.java | 214 ++++++++++++++++++ .../javaclaw/agents/AgentConversationId.java | 34 +++ .../ai/javaclaw/agents/AgentRegistry.java | 134 +++++++++++ .../agents/AgentWorkspaceResolver.java | 91 ++++++++ .../ai/javaclaw/agents/ConfiguredAgent.java | 14 ++ .../configuration/ConfigurationManager.java | 24 ++ .../onboarding/AgentOnboardingProvider.java | 8 + .../ai/javaclaw/providers/AgentProvider.java | 2 - .../FileSystemChatMemoryRepositoryTest.java | 10 +- .../models/AgentChatClientFactoryTest.java | 83 +++++++ .../ai/javaclaw/models/ModelRegistryTest.java | 81 +++++++ .../models/ModelWorkspaceResolverTest.java | 42 ++++ .../openai/OpenAIAgentOnboardingProvider.java | 5 + 37 files changed, 1569 insertions(+), 196 deletions(-) create mode 100644 app/src/main/java/ai/javaclaw/api/AgentsController.java create mode 100644 app/src/main/resources/templates/agents.html.peb create mode 100644 app/src/test/java/ai/javaclaw/api/AgentsControllerTest.java create mode 100644 app/src/test/java/ai/javaclaw/onboarding/steps/S4_AgentMdStepTest.java delete mode 100644 app/workspace/app.mv.db create mode 100644 base/src/main/java/ai/javaclaw/agents/AgentChatClientFactory.java create mode 100644 base/src/main/java/ai/javaclaw/agents/AgentConversationId.java create mode 100644 base/src/main/java/ai/javaclaw/agents/AgentRegistry.java create mode 100644 base/src/main/java/ai/javaclaw/agents/AgentWorkspaceResolver.java create mode 100644 base/src/main/java/ai/javaclaw/agents/ConfiguredAgent.java create mode 100644 base/src/test/java/ai/javaclaw/models/AgentChatClientFactoryTest.java create mode 100644 base/src/test/java/ai/javaclaw/models/ModelRegistryTest.java create mode 100644 base/src/test/java/ai/javaclaw/models/ModelWorkspaceResolverTest.java diff --git a/app/src/main/java/ai/javaclaw/JavaClawApplication.java b/app/src/main/java/ai/javaclaw/JavaClawApplication.java index e396ce6..0f80245 100644 --- a/app/src/main/java/ai/javaclaw/JavaClawApplication.java +++ b/app/src/main/java/ai/javaclaw/JavaClawApplication.java @@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.DefaultApplicationArguments; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; @@ -44,12 +45,16 @@ public void run(ApplicationArguments args) throws Exception { @EventListener public void on(ConfigurationChangedEvent configurationChangedEvent) { - ApplicationArguments args = applicationContext.getBean(ApplicationArguments.class); + Thread thread = new Thread(() -> { try { + ApplicationArguments args = new DefaultApplicationArguments(); + if(applicationContext != null){ + args = applicationContext.getBean(ApplicationArguments.class); + applicationContext.close(); + } Thread.sleep(2000); - applicationContext.close(); applicationContext = SpringApplication.run(JavaClawApplication.class, args.getSourceArgs()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); diff --git a/app/src/main/java/ai/javaclaw/api/AgentsController.java b/app/src/main/java/ai/javaclaw/api/AgentsController.java new file mode 100644 index 0000000..b4dc403 --- /dev/null +++ b/app/src/main/java/ai/javaclaw/api/AgentsController.java @@ -0,0 +1,54 @@ +package ai.javaclaw.api; + +import ai.javaclaw.agents.AgentRegistry; +import ai.javaclaw.agents.AgentWorkspaceResolver; +import ai.javaclaw.agents.ConfiguredAgent; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +import java.nio.file.Path; +import java.util.List; + +@Controller +public class AgentsController { + + private final AgentRegistry agentRegistry; + private final AgentWorkspaceResolver agentWorkspaceResolver; + + public AgentsController(AgentRegistry agentRegistry, AgentWorkspaceResolver agentWorkspaceResolver) { + this.agentRegistry = agentRegistry; + this.agentWorkspaceResolver = agentWorkspaceResolver; + } + + @GetMapping("/agents") + public String agents(Model model) { + List agents = agentRegistry.getAgents().stream() + .map(configuredAgent -> new AgentView( + configuredAgent.id(), + configuredAgent.provider(), + configuredAgent.model(), + resolveWorkspace(configuredAgent), + configuredAgent.id().equals(agentRegistry.getDefaultAgentId()) + )) + .toList(); + + model.addAttribute("agents", agents); + model.addAttribute("hasAgents", !agents.isEmpty()); + return "agents"; + } + + private String resolveWorkspace(ConfiguredAgent configuredAgent) { + Path workspacePath = agentWorkspaceResolver.resolveWorkspacePath(configuredAgent.workspacePath(), configuredAgent.id()); + return workspacePath.toString(); + } + + public record AgentView( + String id, + String provider, + String model, + String workspace, + boolean isDefault + ) { + } +} diff --git a/app/src/main/java/ai/javaclaw/chat/ChatChannel.java b/app/src/main/java/ai/javaclaw/chat/ChatChannel.java index 2050c31..2c2f539 100644 --- a/app/src/main/java/ai/javaclaw/chat/ChatChannel.java +++ b/app/src/main/java/ai/javaclaw/chat/ChatChannel.java @@ -1,6 +1,8 @@ package ai.javaclaw.chat; import ai.javaclaw.agent.Agent; +import ai.javaclaw.agents.AgentConversationId; +import ai.javaclaw.agents.AgentRegistry; import ai.javaclaw.channels.Channel; import ai.javaclaw.channels.ChannelMessageReceivedEvent; import ai.javaclaw.channels.ChannelRegistry; @@ -36,13 +38,15 @@ public class ChatChannel implements Channel { private static final Logger log = LoggerFactory.getLogger(ChatChannel.class); private final Agent agent; + private final AgentRegistry agentRegistry; private final ChannelRegistry channelRegistry; private final ChatMemoryRepository chatMemoryRepository; private final ConcurrentLinkedQueue pendingMessages = new ConcurrentLinkedQueue<>(); private final AtomicReference wsSession = new AtomicReference<>(); - public ChatChannel(Agent agent, ChannelRegistry channelRegistry, ChatMemoryRepository chatMemoryRepository) { + public ChatChannel(Agent agent, AgentRegistry agentRegistry, ChannelRegistry channelRegistry, ChatMemoryRepository chatMemoryRepository) { this.agent = agent; + this.agentRegistry = agentRegistry; this.channelRegistry = channelRegistry; this.chatMemoryRepository = chatMemoryRepository; channelRegistry.registerChannel(this); @@ -96,10 +100,24 @@ public void sendMessage(String message) { /** * Returns all known conversation IDs, always with "web" first. */ - public List conversationIds() { + public List agentIds() { + List ids = agentRegistry.getAgents().stream().map(ai.javaclaw.agents.ConfiguredAgent::id).toList(); + if (!ids.isEmpty()) { + return ids; + } + return List.of(agentRegistry.getDefaultAgentId()); + } + + public String defaultAgentId() { + return agentRegistry.getDefaultAgentId(); + } + + public List conversationIds(String agentId) { List result = new ArrayList<>(); result.add("web"); chatMemoryRepository.findConversationIds().stream() + .filter(id -> agentId.equals(AgentConversationId.agentId(id))) + .map(AgentConversationId::rawConversationId) .filter(id -> !id.equals("web")) .forEach(result::add); return result; @@ -109,8 +127,8 @@ public List conversationIds() { * Loads conversation history for the given conversationId as HTML bubbles. * Returns a single welcome bubble if no history exists yet. */ - public List loadHistoryAsHtml(String conversationId) { - List history = chatMemoryRepository.findByConversationId(conversationId); + public List loadHistoryAsHtml(String agentId, String conversationId) { + List history = chatMemoryRepository.findByConversationId(AgentConversationId.scoped(agentId, conversationId)); if (history.isEmpty()) { return List.of(ChatHtml.agentBubble("Hi! I'm your JavaClaw assistant. How can I help you today?")); } @@ -125,9 +143,9 @@ public List loadHistoryAsHtml(String conversationId) { /** * Handles a chat message from the web UI for the given conversationId. */ - public String chat(String conversationId, String message) { + public String chat(String agentId, String conversationId, String message) { channelRegistry.publishMessageReceivedEvent(new ChannelMessageReceivedEvent(getName(), message)); - return agent.respondTo(conversationId, message); + return agent.respondTo(agentId, conversationId, message); } private static String buildBackgroundMessageHtml(String text) { diff --git a/app/src/main/java/ai/javaclaw/chat/ChatHtml.java b/app/src/main/java/ai/javaclaw/chat/ChatHtml.java index c6416c8..b4a2121 100644 --- a/app/src/main/java/ai/javaclaw/chat/ChatHtml.java +++ b/app/src/main/java/ai/javaclaw/chat/ChatHtml.java @@ -34,11 +34,11 @@ public static String typingDots() { """; } - public static String chatInputArea(String conversationId) { + public static String chatInputArea(String agentId, String conversationId) { if ("web".equals(conversationId)) { return """
+ hx-vals='js:{"type": "userMessage", "agentId": document.getElementById("agent-select") ? document.getElementById("agent-select").value : "%s", "conversationId": document.getElementById("channel-select") ? document.getElementById("channel-select").value : "web"}'>