fix(core): log an error when a duplicate name silently drops a goal, action or tool - #1833
fix(core): log an error when a duplicate name silently drops a goal, action or tool#1833tuannx wants to merge 2 commits into
Conversation
AgentPlatform exposes goals, actions and conditions as flat, name-keyed views
over every deployed agent, aggregated with distinctBy { it.name }. Two agents
declaring different elements under one name therefore lost one of them: both
agents deployed, agents() reported both, nothing was logged, and the capability
simply stopped existing.
The reach went past planning. PerGoalToolFactory publishes one MCP tool per
goal, so a swallowed goal was a tool that vanished from the MCP server. Because
the survivor is decided by alphabetical agent name, deploying an unrelated agent
could also rebind an already published tool to different behaviour.
Removing the deduplication is not the fix: downstream identifies these elements
by name alone. Goals become MCP tools named after them — duplicates would be an
invalid tool list — and Autonomy asks an LLM to rank goals by name. The
ambiguity has to be prevented, not resolved later.
DefaultAgentPlatform.deploy now refuses an agent whose goal, action or condition
name is already taken by a different deployed agent, naming the offending
element. Elements are compared by value, so agents declaring the very same goal
or sharing one condition instance are unaffected — there is nothing to
disambiguate — and redeploying an agent still replaces it.
BREAKING CHANGE: deploying agents with conflicting element names now throws
IllegalArgumentException. Applications that ran with half their capability
silently missing will fail fast instead.
Signed-off-by: TuanNX <tuannx87@gmail.com>
|
@tuannx - thanks for reporting. Also - appears something got broken due to migration, or its existing issue; could you please try to dig into the history of the problem? |
|
Thanks for the review. Issue opened: #1834 On the history question — this is not from the migration. It goes back to Before: de-duplicate by value. Two identical goals collapse into one, which is That is the only commit that ever touched those lines. The fix restores that distinction rather than removing the de-duplication: Sorry about the dense write-up. Issue and PR description are rewritten in One decision I would rather you made than inherited from a diff: |
|
Thank you, @tuannx - will follow up. Best regards! |
@alexheifetz - could you please advise - conflicting goals:
Should probably align with logic: from PR #1801 |
@tuannx - could you please elaborate on "goal value"? Thank you. |
|
@tuannx @deleSerna @alexheifetz - since we are not sure whether it is an exception or a flagging error, it is a better option - may I suggest considering something like ErroneousAgentExitPolicy with default behavior ERROR. |
AchievesGoal without Action should definitely stop the Agent as it's spec violation and it can be easily fix while developing the Agent itself. Could these conflicting goals/action/conditions belongs to agents from third party libraries? If yes then I do not think there is a straight forward solution to this. But my knowledge here is limited. I always write Agent for a stand alone spring boot application. But, if Agent could also be in 3rd party library then just throwing an exception or just flagging an error also won't help as it's not actionable for the consumer of those conflicting libraries. |
==> That's the reason for suggesting having an error policy configurable. thanks |
|
@igordayen @deleSerna There's a third option, and the annotation path already does it. Annotated goals and the tool naming strategy expects that shape: So two annotated agents can both have a same goal and never collide. Only the That makes the collision impossible instead of reporting it better, and nobody If you agree with the direction I'll rework this PR — the deploy-time check goes |
@igordayen But that would not also fix the real issue when the conflicting actions/goal are coming from multiple agents right?
This could also still result in duplicate name as it's still @igordayen @alexheifetz IMO, we should go in the direction suggested by @tuannx but should use 'Name |
|
Three points raised:
@tuannx - concrete naming examples, please, to substantiate the idea and impact assessment. |
|
Before we choose between throwing, an error policy, or qualification, could we do a @igordayen asked for concrete naming examples and an impact assessment. We can generate Any naming change then shows up as a diff, so we can see what breaks on the wire before We have changed these names unnoticed before: #599 (Claude Desktop rejected a tool name) It would also show that two agents in different packages with the same state class name Happy to open it. This PR would then rebase on top. |
|
thanks @tuannx Was actually inquiring about all patterns on validation logic in the agent validation package - what behavior do they expose by default? Is it consistent? |
DefaultAgentStructureValidator currently report errors ( agent booting won’t stop) for the following cases:
AgentMetadataReader stop the agent for following cases:
AgentMetadataReader reports errors for following case
GoapPathToCompletionValidator reports error for cases where it can not to the goal I have not checked in other places @igordayen do you mean this report? |
|
@deleSerna - thanks for the analysis. So, Duplicate action names ==> already in place, but the algorithm requires refinements. What is the flow? metadata reader ==> validator ==> deployer. Maybe propagate errors up to the deployer, and at the deployer level apply a proper exit policy? Looking for an architecturally sound flow. thanks |
|
@tuannx - Is this PR valid then? As the analysis attached to the issues clearly states that deployments should not be rejected. |
Signed-off-by: TuanNX <tuannx87@gmail.com>
Hi @igordayen: Updated PR to log error only, please review whenever you're available. Thank you! |
| * safe to collapse. Defaults to equality, which is meaningful for the value types the | ||
| * platform aggregates; callers holding types without value semantics should pass identity. | ||
| * @param name the name to de-duplicate on | ||
| */ |
There was a problem hiding this comment.
Please add a doc for return too.
| * is reported once rather than on every pass. Bounded so a pathological caller cannot | ||
| * grow it without limit. | ||
| */ | ||
| private const val MAX_REPORTED = 1_000 |
There was a problem hiding this comment.
May be a configurable property?
| private fun report(kind: String, name: String) { | ||
| if (reported.size < MAX_REPORTED && reported.add("$kind/$name")) { | ||
| logger.error( | ||
| "🛑 Two different {}s are named '{}'. Only one of them is visible; the other has been dropped. " + |
There was a problem hiding this comment.
I am wondering whether printing the name enough here to identify which one is dropped.
Instead of just printing the name , may we print some kind of full name so that, it's easily identifiable which item is it. I have not done a thorough check but looks like all of them may be implementing HasInfoString, if that is the case then we can print infoString instead of just name.
There was a problem hiding this comment.
Please cross-check with Junits, thanks
Updated:
AgentPlatformexposes goals, actions and conditions as flat views keyed by name, andde-duplicates them with
distinctBy { it.name }. Two agents declaring different elementsunder one name therefore lose one of them: both agents deploy,
agents()reports both,and the capability simply stops existing with nothing in the log.
safelyGetToolsandsafelyGetToolsFromdo the same for tool names.This PR does not change any of that. It makes the loss visible.
A shared helper,
distinctByNameReportingCollisions, keeps the existingdistinctBysemantics — first declaration wins — and logs an ERROR when the element being dropped
differs from the one being kept. Two agents declaring the very same goal still collapse
silently, because nothing is lost.
Applied to:
AgentPlatform.goals/actions/conditions/domainTypes— compared by value,which is meaningful for those data classes
toolUtils.safelyGetToolsandsafelyGetToolsFrom— compared by identity, sinceToolis an interface with no value semantics
DefaultToolLoop.addToolsis deliberately left alone: re-injecting a tool group duringthe loop is idempotent by design, and the decorator immediately above it produces new
instances, so identity comparison would report on every iteration.
Reporting is once per collision.
safelyGetToolsruns per LLM operation and theplatform's aggregated views are recomputed on every read, so an unguarded log would repeat
indefinitely. The trade-off is that a collision which disappears and returns is not
reported a second time until restart.
What this does not do
ids are untouched
No breaking change.