Repository: langchain4j/langchain4j (2026-07-27T04:12:52Z)

Issue:
[BUG] Agentic proxies throw UnsupportedOperationException on equals()

**Describe the bug**

Agents returned by `AgenticServices` are JDK dynamic proxies. Both invocation handlers route `Object` methods through a switch that handles only `toString` and `hashCode`, so `equals` falls into the `default` branch and throws.

This breaks the `Object.equals` contract: `agent.equals(agent)` throws instead of returning `true`. Ordinary code that puts an agent into a collection and calls `contains`, `remove` or `indexOf` fails too.

Affected: `AgentInvocationHandler` (the `agentBuilder` path) and `PlannerBasedInvocationHandler` (workflow builders such as `sequenceBuilder`).

**Log and Stack trace**

```
java.lang.UnsupportedOperationException: Unknown method on Object class : equals
	at dev.langchain4j.agentic.agent.AgentInvocationHandler.invoke(AgentInvocationHandler.java:166)
	at jdk.proxy2/jdk.proxy2.$Proxy136.equals(Unknown Source)
	at java.base/java.util.ArrayList.indexOfRange(ArrayList.java:299)
	at java.base/java.util.ArrayList.contains(ArrayList.java:275)
```

**To Reproduce**

```java
Writer agent = AgenticServices.agentBuilder(Writer.class).chatModel(model).build();

agent.equals(agent);                                 // throws
new ArrayList<>(List.of(agent)).contains(agent);     // throws
```

No LLM call is needed to reproduce.

**Expected behavior**

`equals` returns a boolean. `DefaultAiServices` proxies already do this, comparing proxy identity.

**Please complete the following information:**
- LangChain4j version: 1.19.0-beta29-SNAPSHOT (`main`, 1da7e9cba)
- LLM(s) used: N/A (reproducible with any stub `ChatModel`)
- Java version: 17
- Spring Boot version (if applicable): N/A

**Additional context**

`hashCode` is already implemented on both handlers, so only `equals` is missing.




Discussion:

Patch:
diff --git a/langchain4j-agentic/src/main/java/dev/langchain4j/agentic/agent/AgentInvocationHandler.java b/langchain4j-agentic/src/main/java/dev/langchain4j/agentic/agent/AgentInvocationHandler.java
index c95a661e187..9cc4c0681d4 100644
--- a/langchain4j-agentic/src/main/java/dev/langchain4j/agentic/agent/AgentInvocationHandler.java
+++ b/langchain4j-agentic/src/main/java/dev/langchain4j/agentic/agent/AgentInvocationHandler.java
@@ -1,5 +1,13 @@
 package dev.langchain4j.agentic.agent;
 
+import static dev.langchain4j.agentic.observability.ComposedAgentListener.composeWithInherited;
+import static dev.langchain4j.agentic.observability.ComposedAgentListener.listenerOfType;
+import static dev.langchain4j.agentic.observability.ListenerNotifierUtil.afterAgentInvocation;
+import static dev.langchain4j.agentic.observability.ListenerNotifierUtil.agentError;
+import static dev.langchain4j.agentic.observability.ListenerNotifierUtil.beforeAgentInvocation;
+import static dev.langchain4j.agentic.scope.DefaultAgenticScope.ephemeralAgenticScope;
+
+import dev.langchain4j.agentic.internal.AgenticScopeOwner;
 import dev.langchain4j.agentic.internal.InternalAgent;
 import dev.langchain4j.agentic.observability.AfterAgentToolExecution;
 import dev.langchain4j.agentic.observability.AgentListener;
@@ -9,17 +17,16 @@
 import dev.langchain4j.agentic.observability.MonitoredAgent;
 import dev.langchain4j.agentic.planner.AgentArgument;
 import dev.langchain4j.agentic.planner.AgentInstance;
-import dev.langchain4j.agentic.internal.AgenticScopeOwner;
 import dev.langchain4j.agentic.planner.AgenticSystemConfigurationException;
 import dev.langchain4j.agentic.planner.AgenticSystemTopology;
 import dev.langchain4j.agentic.planner.Planner;
 import dev.langchain4j.agentic.scope.AgenticScope;
 import dev.langchain4j.agentic.scope.DefaultAgenticScope;
-import dev.langchain4j.model.chat.ChatModel;
-import dev.langchain4j.model.chat.StreamingChatModel;
 import dev.langchain4j.data.message.ChatMessage;
 import dev.langchain4j.data.message.UserMessage;
 import dev.langchain4j.invocation.LangChain4jManaged;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.StreamingChatModel;
 import dev.langchain4j.observability.api.event.AiServiceResponseReceivedEvent;
 import dev.langchain4j.observability.api.listener.AiServiceListener;
 import dev.langchain4j.observability.api.listener.AiServiceResponseReceivedListener;
@@ -27,11 +34,8 @@
 import dev.langchain4j.service.ParameterNameResolver;
 import dev.langchain4j.service.memory.ChatMemoryAccess;
 import dev.langchain4j.service.memory.ChatMemoryService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
-import java.lang.reflect.Proxy;
 import java.lang.reflect.Type;
 import java.util.Collection;
 import java.util.HashMap;
@@ -40,13 +44,8 @@
 import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
-
-import static dev.langchain4j.agentic.observability.ComposedAgentListener.composeWithInherited;
-import static dev.langchain4j.agentic.observability.ComposedAgentListener.listenerOfType;
-import static dev.langchain4j.agentic.observability.ListenerNotifierUtil.afterAgentInvocation;
-import static dev.langchain4j.agentic.observability.ListenerNotifierUtil.agentError;
-import static dev.langchain4j.agentic.observability.ListenerNotifierUtil.beforeAgentInvocation;
-import static dev.langchain4j.agentic.scope.DefaultAgenticScope.ephemeralAgenticScope;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class AgentInvocationHandler implements InvocationHandler, InternalAgent {
 
@@ -65,10 +64,7 @@ public class AgentInvocationHandler implements InvocationHandler, InternalAgent
     private final Map<Object, AiServiceResponseReceivedEvent> lastResponseEvents = new ConcurrentHashMap<>();
 
     public AgentInvocationHandler(
-            AiServiceContext context,
-            Object agent,
-            AgentBuilder<?, ?> builder,
-            boolean agenticScopeDependent) {
+            AiServiceContext context, Object agent, AgentBuilder<?, ?> builder, boolean agenticScopeDependent) {
         this.context = context;
         this.agent = agent;
         this.builder = builder;
@@ -81,18 +77,20 @@ public AgentInvocationHandler(
 
     @Override
     public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
-        if (method.getDeclaringClass() == AiServiceListener.class || method.getDeclaringClass() == AiServiceResponseReceivedListener.class) {
+        if (method.getDeclaringClass() == AiServiceListener.class
+                || method.getDeclaringClass() == AiServiceResponseReceivedListener.class) {
             return switch (method.getName()) {
                 case "getEventClass" -> AiServiceResponseReceivedEvent.class;
                 case "onEvent" -> {
                     AiServiceResponseReceivedEvent event = (AiServiceResponseReceivedEvent) args[0];
-                    AgenticScope agenticScope = (AgenticScope) event.invocationContext().managedParameters().get(AgenticScope.class);
+                    AgenticScope agenticScope = (AgenticScope)
+                            event.invocationContext().managedParameters().get(AgenticScope.class);
                     lastResponseEvents.put(agenticScope.memoryId(), event);
                     yield null;
                 }
                 default ->
-                        throw new UnsupportedOperationException(
-                                "Unknown method on AiServiceResponseReceivedListener class : " + method.getName());
+                    throw new UnsupportedOperationException(
+                            "Unknown method on AiServiceResponseReceivedListener class : " + method.getName());
             };
         }
 
@@ -106,7 +104,8 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Exceptio
                 return null;
             }
             return switch (method.getName()) {
-                case "lastUserMessage" -> lastUserMessage(lastResponseEvent.request().messages()).orElse(null);
+                case "lastUserMessage" ->
+                    lastUserMessage(lastResponseEvent.request().messages()).orElse(null);
                 case "lastChatRequest" -> lastResponseEvent.request();
                 case "lastChatResponse" -> lastResponseEvent.response();
                 default ->
@@ -138,9 +137,10 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Exceptio
         if (method.getDeclaringClass() == ChatMemoryAccess.class) {
             return switch (method.getName()) {
                 case "getChatMemory" ->
-                    context.hasChatMemory() && (ChatMemoryService.DEFAULT.equals(args[0]) || builder.hasNonDefaultChatMemory()) ?
-                            context.chatMemoryService.getChatMemory(args[0]) :
-                            null;
+                    context.hasChatMemory()
+                                    && (ChatMemoryService.DEFAULT.equals(args[0]) || builder.hasNonDefaultChatMemory())
+                            ? context.chatMemoryService.getChatMemory(args[0])
+                            : null;
                 case "evictChatMemory" ->
                     context.hasChatMemory() && context.chatMemoryService.evictChatMemory(args[0]) != null;
                 default ->
@@ -159,11 +159,11 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Exceptio
 
         if (method.getDeclaringClass() == Object.class) {
             return switch (method.getName()) {
+                case "equals" -> proxy == args[0];
                 case "toString" -> "Agent<" + builder.agentServiceClass.getSimpleName() + ">";
                 case "hashCode" -> System.identityHashCode(agent);
                 default ->
-                        throw new UnsupportedOperationException(
-                                "Unknown method on Object class : " + method.getName());
+                    throw new UnsupportedOperationException("Unknown method on Object class : " + method.getName());
             };
         }
 
@@ -180,7 +180,8 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Exceptio
     }
 
     private Object invokeStandaloneAgent(Method method, Object[] args) {
-        LOGGER.warn("Improper invocation of a standalone agent outside of an agentic system, consider using AiServices instead.");
+        LOGGER.warn(
+                "Improper invocation of a standalone agent outside of an agentic system, consider using AiServices instead.");
 
         AgenticScope standaloneAgenticScope = ephemeralAgenticScope();
         LangChain4jManaged.setCurrent(Map.of(AgenticScope.class, standaloneAgenticScope));
@@ -192,7 +193,8 @@ private Object invokeStandaloneAgent(Method method, Object[] args) {
         try {
             result = method.invoke(agent, args);
         } catch (Exception e) {
-            AgentInvocationException invocationException = new AgentInvocationException("Failed to invoke agent method: " + method, e);
+            AgentInvocationException invocationException =
+                    new AgentInvocationException("Failed to invoke agent method: " + method, e);
             agentError(agentListener, standaloneAgenticScope, this, namedArgs, invocationException);
             throw invocationException;
         } finally {
@@ -234,7 +236,8 @@ public String toString() {
     @Override
     public void setParent(InternalAgent parent) {
         if (builder.hasChatMemory() && parent != null && !parent.allowChatMemory()) {
-            throw new AgenticSystemConfigurationException("Agents with chat memory can't be a subagent of " + parent.type());
+            throw new AgenticSystemConfigurationException(
+                    "Agents with chat memory can't be a subagent of " + parent.type());
         }
         this.parent = parent;
         registerInheritedParentListener(parent.listener());
@@ -242,7 +245,9 @@ public void setParent(InternalAgent parent) {
 
     @Override
     public void registerInheritedParentListener(AgentListener parentListener) {
-        if (parentListener != null && parentListener.inheritedBySubagents() && isNewListener(agentListener, parentListener)) {
+        if (parentListener != null
+                && parentListener.inheritedBySubagents()
+                && isNewListener(agentListener, parentListener)) {
             agentListener = composeWithInherited(agentListener, parentListener);
             context.toolService.beforeToolExecution(beforeToolExecution ->
                     agentListener.beforeAgentToolExecution(new BeforeAgentToolExecution(this, beforeToolExecution)));
diff --git a/langchain4j-agentic/src/main/java/dev/langchain4j/agentic/internal/PlannerBasedInvocationHandler.java b/langchain4j-agentic/src/main/java/dev/langchain4j/agentic/internal/PlannerBasedInvocationHandler.java
index 0c62778ccc3..37d645727be 100644
--- a/langchain4j-agentic/src/main/java/dev/langchain4j/agentic/internal/PlannerBasedInvocationHandler.java
+++ b/langchain4j-agentic/src/main/java/dev/langchain4j/agentic/internal/PlannerBasedInvocationHandler.java
@@ -6,30 +6,11 @@
 import static dev.langchain4j.agentic.observability.ComposedAgentListener.composeWithInherited;
 import static dev.langchain4j.agentic.observability.ComposedAgentListener.listenerOfType;
 import static dev.langchain4j.agentic.observability.ListenerNotifierUtil.afterAgentInvocation;
+import static dev.langchain4j.agentic.observability.ListenerNotifierUtil.afterAgenticScopeCreated;
 import static dev.langchain4j.agentic.observability.ListenerNotifierUtil.agentError;
 import static dev.langchain4j.agentic.observability.ListenerNotifierUtil.beforeAgentInvocation;
-import static dev.langchain4j.agentic.observability.ListenerNotifierUtil.afterAgenticScopeCreated;
 import static dev.langchain4j.agentic.observability.ListenerNotifierUtil.onAgenticSystemSuspended;
 
-import java.lang.reflect.InvocationHandler;
-import java.lang.reflect.Method;
-import java.lang.reflect.Parameter;
-import java.lang.reflect.Proxy;
-import java.lang.reflect.Type;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.Set;
-import java.util.concurrent.locks.ReentrantLock;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import dev.langchain4j.invocation.InvocationParameters;
 import dev.langchain4j.agentic.UntypedAgent;
 import dev.langchain4j.agentic.agent.ErrorContext;
 import dev.langchain4j.agentic.agent.ErrorRecoveryResult;
@@ -40,11 +21,11 @@
 import dev.langchain4j.agentic.planner.AgentArgument;
 import dev.langchain4j.agentic.planner.AgentInstance;
 import dev.langchain4j.agentic.planner.AgenticSystemTopology;
+import dev.langchain4j.agentic.planner.ChatMemoryAccessProvider;
 import dev.langchain4j.agentic.planner.InitPlanningContext;
+import dev.langchain4j.agentic.planner.Planner;
 import dev.langchain4j.agentic.planner.PlanningContext;
 import dev.langchain4j.agentic.scope.AgentInvocation;
-import dev.langchain4j.agentic.planner.ChatMemoryAccessProvider;
-import dev.langchain4j.agentic.planner.Planner;
 import dev.langchain4j.agentic.scope.AgenticScope;
 import dev.langchain4j.agentic.scope.AgenticScopeAccess;
 import dev.langchain4j.agentic.scope.AgenticScopeRegistry;
@@ -53,10 +34,29 @@
 import dev.langchain4j.agentic.scope.ResultWithAgenticScope;
 import dev.langchain4j.agentic.workflow.impl.ParallelMapperServiceImpl;
 import dev.langchain4j.internal.DefaultExecutorProvider;
+import dev.langchain4j.invocation.InvocationParameters;
 import dev.langchain4j.service.MemoryId;
 import dev.langchain4j.service.ParameterNameResolver;
 import dev.langchain4j.service.TokenStream;
 import dev.langchain4j.service.memory.ChatMemoryAccess;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.lang.reflect.Proxy;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
 
 public class PlannerBasedInvocationHandler implements InvocationHandler, InternalAgent {
     private final Executor executor;
@@ -100,7 +100,12 @@ public PlannerBasedInvocationHandler(AbstractServiceBuilder<?, ?> service, Suppl
         agenticSystemDataTypes(this);
     }
 
-    private PlannerBasedInvocationHandler(AbstractServiceBuilder<?, ?> service, InternalAgent parent, String agentId, Supplier<Planner> plannerSupplier, DefaultAgenticScope agenticScope) {
+    private PlannerBasedInvocationHandler(
+            AbstractServiceBuilder<?, ?> service,
+            InternalAgent parent,
+            String agentId,
+            Supplier<Planner> plannerSupplier,
+            DefaultAgenticScope agenticScope) {
         this.service = service;
         this.agentId = agentId;
         this.output = service.output;
@@ -116,24 +121,23 @@ private PlannerBasedInvocationHandler(AbstractServiceBuilder<?, ?> service, Inte
         this.name = service.name;
         this.description = service.description;
         this.outputType = service.agentReturnType();
-        this.allowStreamingOutput = UntypedAgent.class.isAssignableFrom(this.type) ||
-                TokenStream.class.isAssignableFrom(rawType(this.outputType));
+        this.allowStreamingOutput = UntypedAgent.class.isAssignableFrom(this.type)
+                || TokenStream.class.isAssignableFrom(rawType(this.outputType));
         this.outputKey = service.outputKey;
         this.arguments = service.agenticMethod != null ? argumentsFromMethod(service.agenticMethod) : List.of();
-        this.subagents = service.subagents.stream().map(AgentInstance.class::cast).toList();
+        this.subagents =
+                service.subagents.stream().map(AgentInstance.class::cast).toList();
         setParent(parent);
     }
 
     public AgenticScopeOwner withAgenticScope(DefaultAgenticScope agenticScope) {
-        PlannerBasedInvocationHandler newHandler = new PlannerBasedInvocationHandler(
-                service, parent, agentId, plannerSupplier, agenticScope);
+        PlannerBasedInvocationHandler newHandler =
+                new PlannerBasedInvocationHandler(service, parent, agentId, plannerSupplier, agenticScope);
         if (service.agentInstanceFactory != null) {
             return (AgenticScopeOwner) service.agentInstanceFactory.apply(newHandler);
         }
         return (AgenticScopeOwner) Proxy.newProxyInstance(
-                type.getClassLoader(),
-                new Class<?>[] {type, InternalAgent.class, AgenticScopeOwner.class},
-                newHandler);
+                type.getClassLoader(), new Class<?>[] {type, InternalAgent.class, AgenticScopeOwner.class}, newHandler);
     }
 
     @Override
@@ -173,11 +177,11 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Exceptio
 
         if (method.getDeclaringClass() == Object.class) {
             return switch (method.getName()) {
+                case "equals" -> proxy == args[0];
                 case "toString" -> service.serviceType() + "<" + type.getSimpleName() + ">";
                 case "hashCode" -> System.identityHashCode(this);
                 default ->
-                        throw new UnsupportedOperationException(
-                                "Unknown method on Object class : " + method.getName());
+                    throw new UnsupportedOperationException("Unknown method on Object class : " + method.getName());
             };
         }
 
@@ -224,8 +228,8 @@ private Object executeAgentMethod(AgenticScopeRegistry registry, Method method,
         if (result instanceof Action action && action.isSuspended()) {
             onAgenticSystemSuspended(agentListener, currentScope);
             if (isRootCall() && method.getReturnType().equals(ResultWithAgenticScope.class)) {
-                return new ResultWithAgenticScope<>(currentScope, null, true,
-                        () -> (ResultWithAgenticScope) executeAgentMethod(registry, method, args));
+                return new ResultWithAgenticScope<>(currentScope, null, true, () ->
+                        (ResultWithAgenticScope) executeAgentMethod(registry, method, args));
             }
             throw new AgenticSystemSuspendedException(currentScope);
         }
@@ -327,7 +331,8 @@ public void setParent(InternalAgent parent) {
     public void registerInheritedParentListener(AgentListener parentListener) {
         if (parentListener != null && parentListener.inheritedBySubagents()) {
             agentListener = composeWithInherited(agentListener, parentListener);
-            subagents().stream().map(InternalAgent.class::cast)
+            subagents().stream()
+                    .map(InternalAgent.class::cast)
                     .forEach(agent -> agent.registerInheritedParentListener(parentListener));
         }
     }
@@ -430,7 +435,9 @@ public Object loop() {
         }
 
         private Action filterCompletedAgents(Action action) {
-            if (completedAgentIds.isEmpty() || !(action instanceof Action.AgentCallAction aca) || aca.agentsToCall().size() <= 1) {
+            if (completedAgentIds.isEmpty()
+                    || !(action instanceof Action.AgentCallAction aca)
+                    || aca.agentsToCall().size() <= 1) {
                 return action;
             }
             List<AgentExecutor> remaining = aca.agentsToCall().stream()
@@ -455,7 +462,8 @@ private String executionStateId() {
         private void parallelExecution(List<AgentExecutor> agents) {
             Executor exec = executor != null ? executor : DefaultExecutorProvider.getDefaultExecutorService();
             var tasks = agents.stream()
-                    .map(agentExecutor -> CompletableFuture.supplyAsync(() -> agentExecutor.execute(agenticScope, this), exec))
+                    .map(agentExecutor ->
+                            CompletableFuture.supplyAsync(() -> agentExecutor.execute(agenticScope, this), exec))
                     .toArray(CompletableFuture[]::new);
             try {
                 CompletableFuture.allOf(tasks).get();
@@ -511,7 +519,8 @@ public void onSubagentInvoked(AgentInvocation agentInvocation) {
             lock.lock();
             try {
                 completedAgentIds.add(agentInvocation.agentId());
-                this.nextAction = composeActions(this.nextAction, planner.nextAction(new PlanningContext(agenticScope, agentInvocation)));
+                this.nextAction = composeActions(
+                        this.nextAction, planner.nextAction(new PlanningContext(agenticScope, agentInvocation)));
                 saveExecutionState();
                 if (registry != null) {
                     agenticScope.checkpoint(registry);
@@ -541,7 +550,8 @@ private static Action composeActions(Action first, Action second) {
         }
 
         private static boolean isEmptyCall(Action action) {
-            return action instanceof Action.AgentCallAction aca && aca.agentsToCall().isEmpty();
+            return action instanceof Action.AgentCallAction aca
+                    && aca.agentsToCall().isEmpty();
         }
     }
 
@@ -579,7 +589,8 @@ private DefaultAgenticScope currentAgenticScope(AgenticScopeRegistry registry, M
         }
 
         Object memoryId = memoryId(method, args);
-        DefaultAgenticScope newAgenticScope = memoryId != null ? getOrCreateAgenticScope(registry, memoryId) : createEphemeralAgenticScope(registry);
+        DefaultAgenticScope newAgenticScope =
+                memoryId != null ? getOrCreateAgenticScope(registry, memoryId) : createEphemeralAgenticScope(registry);
         return newAgenticScope.withErrorHandler(errorHandler);
     }
 
@@ -609,11 +620,13 @@ private Object memoryId(Method method, Object[] args) {
     }
 
     private Object accessChatMemory(AgenticScope agenticScope, String methodName, Object memoryId) {
-        ChatMemoryAccess chatMemoryAccess = ((ChatMemoryAccessProvider) defaultPlannerInstance).chatMemoryAccess(agenticScope);
+        ChatMemoryAccess chatMemoryAccess =
+                ((ChatMemoryAccessProvider) defaultPlannerInstance).chatMemoryAccess(agenticScope);
         return switch (methodName) {
             case "getChatMemory" -> chatMemoryAccess.getChatMemory(memoryId);
             case "evictChatMemory" -> chatMemoryAccess.evictChatMemory(memoryId);
-            default -> throw new UnsupportedOperationException("Unknown method on ChatMemoryAccess class : " + methodName);
+            default ->
+                throw new UnsupportedOperationException("Unknown method on ChatMemoryAccess class : " + methodName);
         };
     }
 }


Test patch:
diff --git a/langchain4j-agentic/src/test/java/dev/langchain4j/agentic/AgentProxyEqualsTest.java b/langchain4j-agentic/src/test/java/dev/langchain4j/agentic/AgentProxyEqualsTest.java
new file mode 100644
index 00000000000..d3314105737
--- /dev/null
+++ b/langchain4j-agentic/src/test/java/dev/langchain4j/agentic/AgentProxyEqualsTest.java
@@ -0,0 +1,101 @@
+package dev.langchain4j.agentic;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.request.ChatRequest;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import dev.langchain4j.service.UserMessage;
+import dev.langchain4j.service.V;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class AgentProxyEqualsTest {
+
+    static final ChatModel DUMMY_MODEL = new ChatModel() {
+        @Override
+        public ChatResponse chat(ChatRequest chatRequest) {
+            return ChatResponse.builder().aiMessage(AiMessage.from("ok")).build();
+        }
+    };
+
+    public interface Writer {
+
+        @UserMessage("Write about {{topic}}")
+        @Agent(outputKey = "story")
+        String write(@V("topic") String topic);
+    }
+
+    private static Writer writerAgent() {
+        return AgenticServices.agentBuilder(Writer.class)
+                .chatModel(DUMMY_MODEL)
+                .outputKey("story")
+                .build();
+    }
+
+    private static UntypedAgent sequenceAgent() {
+        return AgenticServices.sequenceBuilder()
+                .subAgents(writerAgent())
+                .outputKey("story")
+                .build();
+    }
+
+    @Test
+    void agent_is_equal_to_itself() {
+        Writer agent = writerAgent();
+
+        assertThat(agent.equals(agent)).isTrue();
+    }
+
+    @Test
+    void distinct_agents_are_not_equal() {
+        Writer agent = writerAgent();
+        Writer otherAgent = writerAgent();
+
+        assertThat(agent.equals(otherAgent)).isFalse();
+        assertThat(agent.equals(null)).isFalse();
+        assertThat(agent.equals("not an agent")).isFalse();
+    }
+
+    @Test
+    void agent_can_be_looked_up_in_a_collection() {
+        Writer agent = writerAgent();
+        Writer otherAgent = writerAgent();
+        List<Writer> agents = new ArrayList<>(List.of(agent));
+
+        assertThat(agents.contains(agent)).isTrue();
+        assertThat(agents.contains(otherAgent)).isFalse();
+        assertThat(agents.remove(agent)).isTrue();
+        assertThat(agents).isEmpty();
+    }
+
+    @Test
+    void workflow_agent_is_equal_to_itself() {
+        UntypedAgent agent = sequenceAgent();
+
+        assertThat(agent.equals(agent)).isTrue();
+    }
+
+    @Test
+    void distinct_workflow_agents_are_not_equal() {
+        UntypedAgent agent = sequenceAgent();
+        UntypedAgent otherAgent = sequenceAgent();
+
+        assertThat(agent.equals(otherAgent)).isFalse();
+        assertThat(agent.equals(null)).isFalse();
+    }
+
+    @Test
+    void workflow_agent_can_be_looked_up_in_a_collection() {
+        UntypedAgent agent = sequenceAgent();
+        UntypedAgent otherAgent = sequenceAgent();
+        List<UntypedAgent> agents = new ArrayList<>(List.of(agent));
+
+        assertThat(agents.contains(agent)).isTrue();
+        assertThat(agents.contains(otherAgent)).isFalse();
+        assertThat(agents.remove(agent)).isTrue();
+        assertThat(agents).isEmpty();
+    }
+}