fix: resolve reserved id "default" to the default agent instead of falling back on unknown ids

Co-authored-by: cowagent <cow@cowagent.ai>
This commit is contained in:
zhayujieandcowagent committed 2026-09-15 17:52:41 +08:00
1 parent 74cd0feac9
commit a12295d0b6
3 files changed
+57 -16

No files matched your search

+19 -1
View File
@@ -18,6 +18,10 @@ from common.utils import expand_path
_AGENT_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$")
# Id of the built-in single agent, and the reserved alias by which the default
# agent can always be addressed (see AgentRegistry.get_addressed).
DEFAULT_AGENT_ALIAS = "default"
class AgentRegistryError(ValueError):
"""Raised when agent configuration is invalid."""
@@ -201,7 +205,7 @@ class AgentRegistry:
# all-digit id parsed as int), so coerce before validating.
builtin_id = str(builtin_id).strip() if builtin_id is not None else ""
if not builtin_id or not _AGENT_ID_RE.match(builtin_id):
builtin_id = "default"
builtin_id = DEFAULT_AGENT_ALIAS
builtin_name = settings.get("default_agent_name")
builtin_name = str(builtin_name).strip() if builtin_name is not None else ""
profile = AgentProfile(
@@ -277,6 +281,20 @@ class AgentRegistry:
raise AgentRegistryError(f"agent '{resolved_id}' is disabled")
return profile
def get_addressed(self, agent_id: Optional[str], require_enabled: bool = True) -> AgentProfile:
"""The agent an inbound request addresses by id.
Like ``get``, except that the reserved id ``"default"`` always means
the default agent, whatever id it was actually given. Remote callers
can therefore address the default agent without knowing its real id.
Any other unknown id still raises, so a request is never silently
answered by a different agent.
"""
requested = (agent_id or "").strip()
if requested == DEFAULT_AGENT_ALIAS:
requested = ""
return self.get(requested or None, require_enabled=require_enabled)
def get_or_default(self, agent_id: Optional[str]) -> AgentProfile:
with self._lock:
profile = self._profiles.get(agent_id) if agent_id else None
+1 -1
View File
@@ -42,7 +42,7 @@ class AgentRouter:
and nothing in the conversation would reveal the substitution.
"""
try:
return self.registry.get(agent_id).id
return self.registry.get_addressed(agent_id).id
except Exception as exc:
raise AgentUnavailableError(
f"{source} selected agent {agent_id!r}, which is missing or disabled"
+37 -14
View File
@@ -742,7 +742,11 @@ class CloudClient(LinkAIClient):
payload = data.get("payload") or {}
logger.info(f"[CloudClient] on_memory: action={action}")
svc = self._memory_service_for(payload.get("agent_id") or payload.get("agentId"))
agent_id = payload.get("agent_id") or payload.get("agentId")
try:
svc = self._memory_service_for(agent_id)
except KeyError:
return self._agent_not_found(action, agent_id)
if svc is None:
return {"action": action, "code": 500, "message": "MemoryService not available", "payload": None}
@@ -750,8 +754,8 @@ class CloudClient(LinkAIClient):
def _memory_service_for(self, agent_id):
"""A MemoryService bound to the requested agent's workspace. Falls back
to the process-wide default-agent service when no agent is requested or
it cannot be resolved, so single-agent installs are unaffected."""
to the process-wide default-agent service when no agent is requested,
so single-agent installs are unaffected."""
workspace = self._agent_workspace(agent_id)
if workspace is None:
return self.memory_service
@@ -777,7 +781,11 @@ class CloudClient(LinkAIClient):
payload = data.get("payload") or {}
logger.info(f"[CloudClient] on_knowledge: action={action}")
svc = self._knowledge_service_for(payload.get("agent_id") or payload.get("agentId"))
agent_id = payload.get("agent_id") or payload.get("agentId")
try:
svc = self._knowledge_service_for(agent_id)
except KeyError:
return self._agent_not_found(action, agent_id)
if svc is None:
return {"action": action, "code": 500, "message": "KnowledgeService not available", "payload": None}
@@ -786,9 +794,8 @@ class CloudClient(LinkAIClient):
def _knowledge_service_for(self, agent_id):
"""A KnowledgeService bound to the requested agent's workspace. Falls
back to the process-wide default-agent service when no agent is
requested or it cannot be resolved, so single-agent installs are
unaffected. An agent with its own knowledge/ reads that; otherwise it
transparently reads the shared one."""
requested, so single-agent installs are unaffected. An agent with its
own knowledge/ reads that; otherwise it transparently reads the shared one."""
workspace = self._agent_workspace(agent_id)
if workspace is None:
return self.knowledge_service
@@ -892,10 +899,13 @@ class CloudClient(LinkAIClient):
send_chunk_fn=send_chunk_fn, agent_id=agent_id)
def _resolve_chat_agent_id(self, agent_id):
"""Validate a requested agent id, or fall back to the default agent.
"""Validate a requested agent id, or fall back to the default agent
when none is requested.
Returns None when no bridge is available yet so callers keep their
existing default-agent behaviour.
existing default-agent behaviour. A requested agent that is unknown or
disabled is an error: answering as the default agent instead would
silently mix up two agents' sessions and memory.
"""
agent_id = str(agent_id).strip() if agent_id is not None else ""
bridge = self._agent_bridge()
@@ -904,23 +914,31 @@ class CloudClient(LinkAIClient):
try:
return bridge.agent_router.resolve(explicit_agent_id=agent_id or None)
except Exception as e:
if agent_id:
raise RuntimeError(f"agent unavailable: {agent_id}") from e
logger.warning(f"[CloudClient] agent route fallback to default: {e}")
return None
def _agent_workspace(self, agent_id):
"""Resolve a requested agent id to its workspace root, or None to keep
the default-agent behaviour. Read-only queries (memory/history) use this
to scope results to the requested agent instead of the default one."""
the default-agent behaviour when no agent is requested. An agent that is
requested but unknown raises KeyError rather than silently serving the
default agent's data (see AgentRegistry.get_addressed for the ids that
are tolerated)."""
agent_id = str(agent_id).strip() if agent_id is not None else ""
if not agent_id:
return None
try:
from agent.registry import get_agent_registry
profile = get_agent_registry().get(agent_id, require_enabled=False)
return profile.workspace
registry = get_agent_registry()
except Exception as e:
logger.warning(f"[CloudClient] workspace resolve fallback to default: {e}")
logger.warning(f"[CloudClient] agent registry unavailable, using default: {e}")
return None
return registry.get_addressed(agent_id, require_enabled=False).workspace
@staticmethod
def _agent_not_found(action, agent_id):
return {"action": action, "code": 404, "message": f"agent not found: {agent_id}", "payload": None}
@contextmanager
def _chat_identity(self, agent_id, user_id, session_id):
@@ -1059,6 +1077,11 @@ class CloudClient(LinkAIClient):
"action": "query",
"payload": {"status": "success", **result},
}
except KeyError as e:
return {
"action": "query",
"payload": {"status": "error", "message": f"agent not found: {e.args[0] if e.args else ''}"},
}
except Exception as e:
logger.error(f"[CloudClient] History query error: {e}")
return {