From 483d170194c8eac06efff3d635f5305ad36ba5e5 Mon Sep 17 00:00:00 2001 From: zyssyz123 <916125788@qq.com> Date: Thu, 25 Jun 2026 22:09:45 +0800 Subject: [PATCH 1/2] feat(agent): back inline agents with hidden apps (#37969) --- api/controllers/console/agent/app_helpers.py | 12 +- api/controllers/console/agent/composer.py | 18 +- api/controllers/console/agent/roster.py | 39 +++-- api/controllers/console/app/agent.py | 12 +- .../console/app/agent_app_feature.py | 4 +- .../console/app/agent_app_sandbox.py | 8 +- .../console/app/agent_drive_inspector.py | 12 +- api/controllers/console/app/completion.py | 6 +- api/controllers/console/app/message.py | 10 +- api/core/app/apps/agent_app/app_generator.py | 19 +- api/fields/agent_fields.py | 13 ++ ...0-a2b3c4d5e6f7_add_agent_backing_app_id.py | 30 ++++ api/models/agent.py | 9 + api/models/model.py | 12 +- api/services/agent/composer_service.py | 91 +++++++++- api/services/agent/roster_service.py | 165 ++++++++++++++++-- api/services/app_service.py | 11 ++ .../console/agent/test_agent_controllers.py | 24 ++- .../console/app/test_agent_app_sandbox.py | 4 +- .../console/app/test_agent_drive_inspector.py | 12 +- .../console/app/test_agent_skills.py | 10 +- .../services/agent/test_agent_services.py | 71 +++++++- .../generated/api/console/agent/types.gen.ts | 18 ++ .../generated/api/console/agent/zod.gen.ts | 18 ++ .../generated/api/console/apps/types.gen.ts | 9 + .../generated/api/console/apps/zod.gen.ts | 14 ++ 26 files changed, 534 insertions(+), 117 deletions(-) create mode 100644 api/migrations/versions/2026_06_25_1100-a2b3c4d5e6f7_add_agent_backing_app_id.py diff --git a/api/controllers/console/agent/app_helpers.py b/api/controllers/console/agent/app_helpers.py index 51adc1e136e..7af38b0164d 100644 --- a/api/controllers/console/agent/app_helpers.py +++ b/api/controllers/console/agent/app_helpers.py @@ -6,5 +6,15 @@ from services.agent.roster_service import AgentRosterService def resolve_agent_app_model(*, tenant_id: str, agent_id: UUID) -> App: - """Resolve the hidden Agent App backing an Agent Console resource.""" + """Resolve a roster Agent's public Agent App.""" return AgentRosterService(db.session).get_agent_app_model(tenant_id=tenant_id, agent_id=str(agent_id)) + + +def resolve_agent_runtime_app_model(*, tenant_id: str, agent_id: UUID) -> App: + """Resolve the App that backs an Agent runtime surface. + + This accepts both roster Agent Apps and workflow-only inline Agents with a + hidden backing App. + """ + + return AgentRosterService(db.session).get_agent_runtime_app_model(tenant_id=tenant_id, agent_id=str(agent_id)) diff --git a/api/controllers/console/agent/composer.py b/api/controllers/console/agent/composer.py index 5ae35e2138a..7413be95d75 100644 --- a/api/controllers/console/agent/composer.py +++ b/api/controllers/console/agent/composer.py @@ -5,7 +5,6 @@ from flask_restx import Resource from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns -from controllers.console.agent.app_helpers import resolve_agent_app_model from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( RBACPermission, @@ -48,10 +47,6 @@ register_response_schema_models( ) -def _resolve_agent_app_id(*, tenant_id: str, agent_id: UUID) -> str: - return resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id).id - - @console_ns.route("/apps//workflows/draft/nodes//agent-composer") class WorkflowAgentComposerApi(Resource): @console_ns.response( @@ -239,10 +234,9 @@ class AgentComposerApi(Resource): @account_initialization_required @with_current_tenant_id def get(self, tenant_id: str, agent_id: UUID): - app_id = _resolve_agent_app_id(tenant_id=tenant_id, agent_id=agent_id) return dump_response( AgentAppComposerResponse, - AgentComposerService.load_agent_app_composer(tenant_id=tenant_id, app_id=app_id), + AgentComposerService.load_agent_composer(tenant_id=tenant_id, agent_id=str(agent_id)), ) @console_ns.expect(console_ns.models[ComposerSavePayload.__name__]) @@ -255,13 +249,12 @@ class AgentComposerApi(Resource): @with_current_user_id @with_current_tenant_id def put(self, tenant_id: str, account_id: str, agent_id: UUID): - app_id = _resolve_agent_app_id(tenant_id=tenant_id, agent_id=agent_id) payload = ComposerSavePayload.model_validate(console_ns.payload or {}) return dump_response( AgentAppComposerResponse, - AgentComposerService.save_agent_app_composer( + AgentComposerService.save_agent_composer( tenant_id=tenant_id, - app_id=app_id, + agent_id=str(agent_id), account_id=account_id, payload=payload, ), @@ -279,7 +272,7 @@ class AgentComposerValidateApi(Resource): @account_initialization_required @with_current_tenant_id def post(self, tenant_id: str, agent_id: UUID): - _resolve_agent_app_id(tenant_id=tenant_id, agent_id=agent_id) + AgentComposerService.load_agent_composer(tenant_id=tenant_id, agent_id=str(agent_id)) payload = ComposerSavePayload.model_validate(console_ns.payload or {}) ComposerConfigValidator.validate_publish_payload(payload) AgentComposerService.validate_knowledge_datasets(tenant_id=tenant_id, agent_soul=payload.agent_soul) @@ -302,12 +295,11 @@ class AgentComposerCandidatesApi(Resource): @with_current_user_id @with_current_tenant_id def get(self, tenant_id: str, current_user_id: str, agent_id: UUID): - app_id = _resolve_agent_app_id(tenant_id=tenant_id, agent_id=agent_id) return dump_response( AgentComposerCandidatesResponse, AgentComposerService.get_agent_app_candidates( tenant_id=tenant_id, - app_id=app_id, + agent_id=str(agent_id), user_id=current_user_id, ), ) diff --git a/api/controllers/console/agent/roster.py b/api/controllers/console/agent/roster.py index 3a13842a5a5..3b33dc68c72 100644 --- a/api/controllers/console/agent/roster.py +++ b/api/controllers/console/agent/roster.py @@ -7,7 +7,7 @@ from sqlalchemy import func, select from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns -from controllers.console.agent.app_helpers import resolve_agent_app_model +from controllers.console.agent.app_helpers import resolve_agent_app_model, resolve_agent_runtime_app_model from controllers.console.apikey import ApiKeyItem, ApiKeyList, BaseApiKeyListResource, BaseApiKeyResource from controllers.console.app.app import ( AppDetailWithSite as GenericAppDetailWithSite, @@ -54,6 +54,7 @@ from libs.datetime_utils import parse_time_range from libs.helper import dump_response from libs.login import login_required from models import Account +from models.agent import Agent, AgentStatus from models.enums import ApiTokenType from models.model import ApiToken, App, IconType from services.agent.composer_service import AgentComposerService @@ -233,6 +234,8 @@ class AgentStatisticsQuery(BaseModel): class AgentAppPartial(GenericAppPartial): app_id: str | None = None + backing_app_id: str | None = None + hidden_app_backed: bool = False debug_conversation_id: str | None = None role: str | None = None active_config_is_published: bool = False @@ -242,6 +245,8 @@ class AgentAppPartial(GenericAppPartial): class AgentAppDetailWithSite(GenericAppDetailWithSite): app_id: str | None = None + backing_app_id: str | None = None + hidden_app_backed: bool = False debug_conversation_id: str | None = None role: str | None = None active_config_is_published: bool = False @@ -332,7 +337,7 @@ def _agent_roster_service() -> AgentRosterService: return AgentRosterService(db.session) -def _serialize_agent_app_detail(app_model, *, current_user: Account) -> dict: +def _serialize_agent_app_detail(app_model, *, current_user: Account, agent_id: str | None = None) -> dict: """Serialize an Agent App detail using roster-only DTOs. `/agent` responses are roster-shaped rather than raw app-shaped: `id` @@ -349,11 +354,23 @@ def _serialize_agent_app_detail(app_model, *, current_user: Account) -> dict: roster_service = _agent_roster_service() payload = AgentAppDetailWithSite.model_validate(app_model, from_attributes=True).model_dump(mode="json") - agent = roster_service.get_app_backing_agent(tenant_id=app_model.tenant_id, app_id=str(app_model.id)) + agent = ( + db.session.scalar( + select(Agent).where( + Agent.tenant_id == app_model.tenant_id, + Agent.id == agent_id, + Agent.status == AgentStatus.ACTIVE, + ) + ) + if agent_id + else roster_service.get_app_backing_agent(tenant_id=app_model.tenant_id, app_id=str(app_model.id)) + ) if not agent: raise AgentNotFoundError() payload.pop("bound_agent_id", None) - payload["app_id"] = str(app_model.id) + payload["app_id"] = agent.app_id + payload["backing_app_id"] = roster_service.runtime_backing_app_id(agent) + payload["hidden_app_backed"] = bool(agent.backing_app_id and agent.backing_app_id != agent.app_id) payload["id"] = agent.id payload["debug_conversation_id"] = roster_service.get_or_create_agent_app_debug_conversation_id( tenant_id=app_model.tenant_id, @@ -403,6 +420,8 @@ def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str, current_u agent = agents_by_app_id.get(app_id) if agent: item["app_id"] = app_id + item["backing_app_id"] = agent.backing_app_id or app_id + item["hidden_app_backed"] = False item["id"] = agent.id item["debug_conversation_id"] = debug_conversation_ids_by_agent_id.get(agent.id) item["role"] = agent.role or "" @@ -554,8 +573,8 @@ class AgentAppApi(Resource): @with_current_user @with_current_tenant_id def get(self, tenant_id: str, current_user: Account, agent_id: UUID): - app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) - return _serialize_agent_app_detail(app_model, current_user=current_user) + app_model = resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) + return _serialize_agent_app_detail(app_model, current_user=current_user, agent_id=str(agent_id)) @console_ns.expect(console_ns.models[AgentAppUpdatePayload.__name__]) @console_ns.response(200, "Agent app updated successfully", console_ns.models[AgentAppDetailWithSite.__name__]) @@ -856,7 +875,7 @@ class AgentLogsApi(Resource): @with_current_user @with_current_tenant_id def get(self, tenant_id: str, current_user: Account, agent_id: UUID): - app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) query_data: dict[str, object] = dict(request.args.to_dict(flat=True)) query_data["sources"] = _multi_query_values("sources", "source") query_data["statuses"] = _multi_query_values("statuses", "status") @@ -893,7 +912,7 @@ class AgentLogMessagesApi(Resource): @with_current_user @with_current_tenant_id def get(self, tenant_id: str, current_user: Account, agent_id: UUID, conversation_id: UUID): - app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) query_data: dict[str, object] = dict(request.args.to_dict(flat=True)) query_data["sources"] = _multi_query_values("sources", "source") query_data["statuses"] = _multi_query_values("statuses", "status") @@ -930,7 +949,7 @@ class AgentLogSourcesApi(Resource): @with_current_user @with_current_tenant_id def get(self, tenant_id: str, current_user: Account, agent_id: UUID): - app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) payload = _agent_observability_service().list_log_sources(app=app_model, agent_id=str(agent_id)) return dump_response(AgentLogSourceListResponse, payload) @@ -949,7 +968,7 @@ class AgentStatisticsSummaryApi(Resource): @with_current_user @with_current_tenant_id def get(self, tenant_id: str, current_user: Account, agent_id: UUID): - app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) query = AgentStatisticsQuery.model_validate(request.args.to_dict(flat=True)) timezone = current_user.timezone or "UTC" start, end = _parse_observability_time_range(query.start, query.end, current_user) diff --git a/api/controllers/console/app/agent.py b/api/controllers/console/app/agent.py index 86a3c473547..99164b4755a 100644 --- a/api/controllers/console/app/agent.py +++ b/api/controllers/console/app/agent.py @@ -13,7 +13,7 @@ from controllers.common.schema import ( register_schema_models, ) from controllers.console import console_ns -from controllers.console.agent.app_helpers import resolve_agent_app_model +from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( RBACPermission, @@ -351,7 +351,7 @@ class AgentSkillUploadByAgentApi(Resource): @with_current_user @with_current_tenant_id def post(self, tenant_id: str, current_user: Account, agent_id: UUID): - app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) return _upload_skill_for_app(current_user=current_user, app_model=app_model) @@ -394,7 +394,7 @@ class AgentDriveFilesByAgentApi(Resource): @with_current_user @with_current_tenant_id def post(self, tenant_id: str, current_user: Account, agent_id: UUID): - app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) return _commit_drive_file_for_app(current_user=current_user, app_model=app_model, allow_node_id=False) @console_ns.doc("delete_agent_drive_file_by_agent") @@ -407,7 +407,7 @@ class AgentDriveFilesByAgentApi(Resource): @with_current_user @with_current_tenant_id def delete(self, tenant_id: str, current_user: Account, agent_id: UUID): - app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) return _delete_drive_file_for_app(current_user=current_user, app_model=app_model, allow_node_id=False) @@ -454,7 +454,7 @@ class AgentSkillByAgentApi(Resource): @with_current_user @with_current_tenant_id def delete(self, tenant_id: str, current_user: Account, agent_id: UUID, slug: str): - app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) return _delete_skill_for_app(current_user=current_user, app_model=app_model, slug=slug, allow_node_id=False) @@ -494,7 +494,7 @@ class AgentSkillInferToolsByAgentApi(Resource): @account_initialization_required @with_current_tenant_id def post(self, tenant_id: str, agent_id: UUID, slug: str): - app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) return _infer_skill_tools_for_app(app_model=app_model, slug=slug) diff --git a/api/controllers/console/app/agent_app_feature.py b/api/controllers/console/app/agent_app_feature.py index 358e552beb0..be6b9b28543 100644 --- a/api/controllers/console/app/agent_app_feature.py +++ b/api/controllers/console/app/agent_app_feature.py @@ -17,7 +17,7 @@ from pydantic import BaseModel, Field from controllers.common.fields import SimpleResultResponse from controllers.common.schema import register_response_schema_models, register_schema_models from controllers.console import console_ns -from controllers.console.agent.app_helpers import resolve_agent_app_model +from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model from controllers.console.wraps import ( RBACPermission, RBACResourceScope, @@ -87,7 +87,7 @@ class AgentAppFeatureConfigResource(Resource): @with_current_user @with_current_tenant_id def post(self, tenant_id: str, current_user: Account, agent_id: UUID): - app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) args = AgentAppFeaturesPayload.model_validate(console_ns.payload or {}) new_app_model_config = AgentAppFeatureConfigService.update_features( diff --git a/api/controllers/console/app/agent_app_sandbox.py b/api/controllers/console/app/agent_app_sandbox.py index f9bda13c63a..9d92c078bd4 100644 --- a/api/controllers/console/app/agent_app_sandbox.py +++ b/api/controllers/console/app/agent_app_sandbox.py @@ -22,7 +22,7 @@ from controllers.common.schema import ( register_schema_models, ) from controllers.console import console_ns -from controllers.console.agent.app_helpers import resolve_agent_app_model +from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id from fields.base import ResponseModel @@ -144,7 +144,7 @@ class AgentAppSandboxListResource(Resource): @account_initialization_required @with_current_tenant_id def get(self, tenant_id: str, agent_id: UUID): - app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) query = query_params_from_request(AgentSandboxListQuery) try: result = AgentAppSandboxService().list_files( @@ -169,7 +169,7 @@ class AgentAppSandboxReadResource(Resource): @account_initialization_required @with_current_tenant_id def get(self, tenant_id: str, agent_id: UUID): - app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) query = query_params_from_request(AgentSandboxFileQuery) try: result = AgentAppSandboxService().read_file( @@ -194,7 +194,7 @@ class AgentAppSandboxUploadResource(Resource): @account_initialization_required @with_current_tenant_id def post(self, tenant_id: str, agent_id: UUID): - app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) payload = AgentSandboxUploadPayload.model_validate(request.get_json(silent=True) or {}) try: result = AgentAppSandboxService().upload_file( diff --git a/api/controllers/console/app/agent_drive_inspector.py b/api/controllers/console/app/agent_drive_inspector.py index bd639955d9c..473e7364b3e 100644 --- a/api/controllers/console/app/agent_drive_inspector.py +++ b/api/controllers/console/app/agent_drive_inspector.py @@ -25,7 +25,7 @@ from controllers.common.schema import ( register_response_schema_models, ) from controllers.console import console_ns -from controllers.console.agent.app_helpers import resolve_agent_app_model +from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id from fields.base import ResponseModel @@ -182,7 +182,7 @@ class AgentDriveListByAgentApi(Resource): @with_current_tenant_id def get(self, tenant_id: str, agent_id: UUID): query = query_params_from_request(AgentDriveListByAgentQuery) - resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) try: items = AgentDriveService().manifest(tenant_id=tenant_id, agent_id=str(agent_id), prefix=query.prefix) except AgentDriveError as exc: @@ -201,7 +201,7 @@ class AgentDriveSkillListByAgentApi(Resource): @account_initialization_required @with_current_tenant_id def get(self, tenant_id: str, agent_id: UUID): - resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) try: items = AgentDriveService().list_skills(tenant_id=tenant_id, agent_id=str(agent_id)) except AgentDriveError as exc: @@ -220,7 +220,7 @@ class AgentDriveSkillInspectByAgentApi(Resource): @account_initialization_required @with_current_tenant_id def get(self, tenant_id: str, agent_id: UUID, skill_path: str): - resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) try: return _json_response( AgentDriveService().inspect_skill( @@ -245,7 +245,7 @@ class AgentDrivePreviewByAgentApi(Resource): @with_current_tenant_id def get(self, tenant_id: str, agent_id: UUID): query = query_params_from_request(AgentDriveFileByAgentQuery) - resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) try: return AgentDriveService().preview(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key) except AgentDriveError as exc: @@ -264,7 +264,7 @@ class AgentDriveDownloadByAgentApi(Resource): @with_current_tenant_id def get(self, tenant_id: str, agent_id: UUID): query = query_params_from_request(AgentDriveFileByAgentQuery) - resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + resolve_agent_runtime_app_model(tenant_id=tenant_id, agent_id=agent_id) try: url = AgentDriveService().download_url(tenant_id=tenant_id, agent_id=str(agent_id), key=query.key) except AgentDriveError as exc: diff --git a/api/controllers/console/app/completion.py b/api/controllers/console/app/completion.py index d25d0a5c4d2..16c1a2f570b 100644 --- a/api/controllers/console/app/completion.py +++ b/api/controllers/console/app/completion.py @@ -11,7 +11,7 @@ import services from controllers.common.fields import GeneratedAppResponse, SimpleResultResponse from controllers.common.schema import register_response_schema_models, register_schema_models from controllers.console import console_ns -from controllers.console.agent.app_helpers import resolve_agent_app_model +from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model from controllers.console.app.error import ( AppUnavailableError, CompletionRequestError, @@ -222,7 +222,7 @@ class AgentChatMessageApi(Resource): @with_current_user @with_current_tenant_id def post(self, current_tenant_id: str, current_user: Account, agent_id: UUID): - app_model = resolve_agent_app_model(tenant_id=current_tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=current_tenant_id, agent_id=agent_id) return _create_chat_message( current_tenant_id=current_tenant_id, current_user=current_user, @@ -258,7 +258,7 @@ class AgentChatMessageStopApi(Resource): @with_current_user_id @with_current_tenant_id def post(self, current_tenant_id: str, current_user_id: str, agent_id: UUID, task_id: str): - app_model = resolve_agent_app_model(tenant_id=current_tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=current_tenant_id, agent_id=agent_id) return _stop_chat_message(current_user_id=current_user_id, app_model=app_model, task_id=task_id) diff --git a/api/controllers/console/app/message.py b/api/controllers/console/app/message.py index 195a41f2888..a9abb7ed22c 100644 --- a/api/controllers/console/app/message.py +++ b/api/controllers/console/app/message.py @@ -13,7 +13,7 @@ from controllers.common.controller_schemas import MessageFeedbackPayload as _Mes from controllers.common.fields import SimpleResultResponse, TextFileResponse from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns -from controllers.console.agent.app_helpers import resolve_agent_app_model +from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model from controllers.console.app.error import ( CompletionRequestError, ProviderModelCurrentlyNotSupportError, @@ -210,7 +210,7 @@ class AgentChatMessageListApi(Resource): @with_current_user @with_current_tenant_id def get(self, current_tenant_id: str, current_user: Account, agent_id: UUID): - app_model = resolve_agent_app_model(tenant_id=current_tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=current_tenant_id, agent_id=agent_id) return _list_chat_messages(app_model=app_model, current_user=current_user) @@ -246,7 +246,7 @@ class AgentMessageFeedbackApi(Resource): @with_current_user @with_current_tenant_id def post(self, current_tenant_id: str, current_user: Account, agent_id: UUID): - app_model = resolve_agent_app_model(tenant_id=current_tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=current_tenant_id, agent_id=agent_id) return _update_message_feedback(current_user=current_user, app_model=app_model) @@ -311,7 +311,7 @@ class AgentMessageSuggestedQuestionApi(Resource): @with_current_user @with_current_tenant_id def get(self, current_tenant_id: str, current_user: Account, agent_id: UUID, message_id: UUID): - app_model = resolve_agent_app_model(tenant_id=current_tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=current_tenant_id, agent_id=agent_id) return _get_message_suggested_questions(current_user=current_user, app_model=app_model, message_id=message_id) @@ -389,7 +389,7 @@ class AgentMessageApi(Resource): @account_initialization_required @with_current_tenant_id def get(self, current_tenant_id: str, agent_id: UUID, message_id: UUID): - app_model = resolve_agent_app_model(tenant_id=current_tenant_id, agent_id=agent_id) + app_model = resolve_agent_runtime_app_model(tenant_id=current_tenant_id, agent_id=agent_id) return _get_message_detail(app_model=app_model, message_id=message_id) diff --git a/api/core/app/apps/agent_app/app_generator.py b/api/core/app/apps/agent_app/app_generator.py index bd8012a0f24..4f2c546fb7b 100644 --- a/api/core/app/apps/agent_app/app_generator.py +++ b/api/core/app/apps/agent_app/app_generator.py @@ -16,7 +16,7 @@ from collections.abc import Generator, Mapping from typing import Any from flask import Flask, current_app -from sqlalchemy import select +from sqlalchemy import and_, or_, select from clients.agent_backend import AgentBackendRunEventAdapter from clients.agent_backend.factory import create_agent_backend_run_client @@ -448,12 +448,21 @@ class AgentAppGenerator(MessageBasedAppGenerator): user: Account | EndUser, ) -> tuple[Agent, str, AgentSoulConfig]: agent = db.session.scalar( - select(Agent).where( - Agent.app_id == app_model.id, - Agent.scope == AgentScope.ROSTER, - Agent.source == AgentSource.AGENT_APP, + select(Agent) + .where( + Agent.tenant_id == app_model.tenant_id, Agent.status == AgentStatus.ACTIVE, + or_( + and_( + Agent.app_id == app_model.id, + Agent.scope == AgentScope.ROSTER, + Agent.source == AgentSource.AGENT_APP, + ), + Agent.backing_app_id == app_model.id, + ), ) + .order_by(Agent.created_at.desc()) + .limit(1) ) if agent is None: raise AgentAppGeneratorError("Agent App has no bound Agent") diff --git a/api/fields/agent_fields.py b/api/fields/agent_fields.py index 783b99850c6..e045ee39afc 100644 --- a/api/fields/agent_fields.py +++ b/api/fields/agent_fields.py @@ -85,6 +85,8 @@ class AgentRosterResponse(ResponseModel): scope: AgentScope source: AgentSource app_id: str | None = None + backing_app_id: str | None = None + hidden_app_backed: bool = False workflow_id: str | None = None workflow_node_id: str | None = None active_config_snapshot_id: str | None = None @@ -318,7 +320,11 @@ class AgentComposerAgentResponse(ResponseModel): icon: str | None = None icon_background: str | None = None scope: AgentScope + source: AgentSource | None = None status: AgentStatus + app_id: str | None = None + backing_app_id: str | None = None + hidden_app_backed: bool = False active_config_snapshot_id: str | None = None @@ -362,6 +368,9 @@ class WorkflowAgentComposerResponse(ResponseModel): impact_summary: AgentComposerImpactResponse | None = None validation: "ComposerValidationFindingsResponse | None" = None app_id: str | None = None + backing_app_id: str | None = None + hidden_app_backed: bool = False + chat_endpoint: str | None = None workflow_id: str | None = None node_id: str | None = None @@ -374,6 +383,10 @@ class AgentAppComposerResponse(ResponseModel): agent_soul: AgentSoulConfig save_options: list[ComposerSaveStrategy] validation: "ComposerValidationFindingsResponse | None" = None + app_id: str | None = None + backing_app_id: str | None = None + hidden_app_backed: bool = False + chat_endpoint: str | None = None class ComposerValidationWarningResponse(ResponseModel): diff --git a/api/migrations/versions/2026_06_25_1100-a2b3c4d5e6f7_add_agent_backing_app_id.py b/api/migrations/versions/2026_06_25_1100-a2b3c4d5e6f7_add_agent_backing_app_id.py new file mode 100644 index 00000000000..be19ecf7ed9 --- /dev/null +++ b/api/migrations/versions/2026_06_25_1100-a2b3c4d5e6f7_add_agent_backing_app_id.py @@ -0,0 +1,30 @@ +"""add agent backing app id + +Revision ID: a2b3c4d5e6f7 +Revises: e4f5a6b7c8d9 +Create Date: 2026-06-25 11:00:00.000000 + +""" + +import sqlalchemy as sa +from alembic import op + +import models + +# revision identifiers, used by Alembic. +revision = "a2b3c4d5e6f7" +down_revision = "e4f5a6b7c8d9" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("agents", schema=None) as batch_op: + batch_op.add_column(sa.Column("backing_app_id", models.types.StringUUID(), nullable=True)) + op.create_index("agent_tenant_backing_app_id_idx", "agents", ["tenant_id", "backing_app_id"]) + + +def downgrade(): + op.drop_index("agent_tenant_backing_app_id_idx", table_name="agents") + with op.batch_alter_table("agents", schema=None) as batch_op: + batch_op.drop_column("backing_app_id") diff --git a/api/models/agent.py b/api/models/agent.py index e3d194a684b..43547d7a220 100644 --- a/api/models/agent.py +++ b/api/models/agent.py @@ -145,6 +145,7 @@ class Agent(DefaultFieldsMixin, Base): Index("agent_tenant_scope_idx", "tenant_id", "scope"), Index("agent_tenant_workflow_id_idx", "tenant_id", "workflow_id"), Index("agent_tenant_app_id_idx", "tenant_id", "app_id"), + Index("agent_tenant_backing_app_id_idx", "tenant_id", "backing_app_id"), Index("agent_active_config_snapshot_id_idx", "active_config_snapshot_id"), Index( "agent_tenant_invitable_idx", @@ -173,6 +174,14 @@ class Agent(DefaultFieldsMixin, Base): scope: Mapped[AgentScope] = mapped_column(EnumText(AgentScope, length=32), nullable=False) source: Mapped[AgentSource] = mapped_column(EnumText(AgentSource, length=32), nullable=False) app_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True) + backing_app_id: Mapped[str | None] = mapped_column( + StringUUID, + nullable=True, + comment=( + "Runtime Agent App used for chat/log/monitoring. For workflow-only agents, " + "app_id remains the parent workflow app id and this points to the hidden backing app." + ), + ) workflow_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True) workflow_node_id: Mapped[str | None] = mapped_column(String(255), nullable=True) active_config_snapshot_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True) diff --git a/api/models/model.py b/api/models/model.py index 38d67004de4..e3814ded257 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -487,9 +487,15 @@ class App(Base): agent = db.session.scalar( select(Agent).where( - Agent.app_id == self.id, - Agent.scope == AgentScope.ROSTER, - Agent.source == AgentSource.AGENT_APP, + Agent.tenant_id == self.tenant_id, + sa.or_( + sa.and_( + Agent.app_id == self.id, + Agent.scope == AgentScope.ROSTER, + Agent.source == AgentSource.AGENT_APP, + ), + Agent.backing_app_id == self.id, + ), Agent.status == AgentStatus.ACTIVE, ) ) diff --git a/api/services/agent/composer_service.py b/api/services/agent/composer_service.py index d2869c511ce..b8c1bdb5bc4 100644 --- a/api/services/agent/composer_service.py +++ b/api/services/agent/composer_service.py @@ -305,6 +305,15 @@ class AgentComposerService: @classmethod def load_agent_app_composer(cls, *, tenant_id: str, app_id: str) -> dict[str, Any]: agent = cls._require_agent_app_agent(tenant_id=tenant_id, app_id=app_id) + return cls._load_agent_composer_for_agent(tenant_id=tenant_id, agent=agent) + + @classmethod + def load_agent_composer(cls, *, tenant_id: str, agent_id: str) -> dict[str, Any]: + agent = cls._require_agent(tenant_id=tenant_id, agent_id=agent_id) + return cls._load_agent_composer_for_agent(tenant_id=tenant_id, agent=agent) + + @classmethod + def _load_agent_composer_for_agent(cls, *, tenant_id: str, agent: Agent) -> dict[str, Any]: draft = cls._get_or_create_agent_draft( tenant_id=tenant_id, agent=agent, @@ -322,6 +331,10 @@ class AgentComposerService: "draft": cls._serialize_draft(draft), "agent_soul": draft.config_snapshot_dict, "save_options": [ComposerSaveStrategy.SAVE_TO_CURRENT_VERSION.value], + "app_id": agent.app_id, + "backing_app_id": agent.backing_app_id or agent.app_id, + "hidden_app_backed": bool(agent.scope == AgentScope.WORKFLOW_ONLY and agent.backing_app_id), + "chat_endpoint": f"/console/api/agent/{agent.id}/chat-messages", } @classmethod @@ -334,11 +347,11 @@ class AgentComposerService: raise InvalidComposerConfigError( "Agent App composer only saves the normal draft. Use the publish endpoint to create a version." ) + if payload.agent_soul is None: + raise ValueError("agent_soul is required") _backfill_cli_tool_ids(payload.agent_soul) _validate_composer_payload_for_strategy(payload) cls.validate_knowledge_datasets(tenant_id=tenant_id, agent_soul=payload.agent_soul) - if payload.agent_soul is None: - raise ValueError("agent_soul is required") agent = cls._get_agent_app_agent(tenant_id=tenant_id, app_id=app_id) if not agent: @@ -350,6 +363,7 @@ class AgentComposerService: scope=AgentScope.ROSTER, source=AgentSource.AGENT_APP, app_id=app_id, + backing_app_id=app_id, status=AgentStatus.ACTIVE, created_by=account_id, updated_by=account_id, @@ -360,6 +374,47 @@ class AgentComposerService: except IntegrityError as exc: db.session.rollback() raise AgentNameConflictError() from exc + return cls._save_agent_composer_for_agent( + tenant_id=tenant_id, + agent=agent, + account_id=account_id, + payload=payload, + ) + + @classmethod + def save_agent_composer( + cls, *, tenant_id: str, agent_id: str, account_id: str, payload: ComposerSavePayload + ) -> dict[str, Any]: + if payload.variant != ComposerVariant.AGENT_APP: + raise ValueError("Agent composer endpoint only accepts agent_app variant") + if payload.save_strategy != ComposerSaveStrategy.SAVE_TO_CURRENT_VERSION: + raise InvalidComposerConfigError( + "Agent composer only saves the normal draft. Use the publish endpoint to create a version." + ) + if payload.agent_soul is None: + raise ValueError("agent_soul is required") + _backfill_cli_tool_ids(payload.agent_soul) + _validate_composer_payload_for_strategy(payload) + cls.validate_knowledge_datasets(tenant_id=tenant_id, agent_soul=payload.agent_soul) + agent = cls._require_agent(tenant_id=tenant_id, agent_id=agent_id) + return cls._save_agent_composer_for_agent( + tenant_id=tenant_id, + agent=agent, + account_id=account_id, + payload=payload, + ) + + @classmethod + def _save_agent_composer_for_agent( + cls, *, tenant_id: str, agent: Agent, account_id: str, payload: ComposerSavePayload + ) -> dict[str, Any]: + if payload.agent_soul is None: + raise ValueError("agent_soul is required") + payload.agent_soul = cls._preserve_active_soul_files( + tenant_id=tenant_id, + agent_id=agent.id, + agent_soul=payload.agent_soul, + ) cls._save_agent_draft( tenant_id=tenant_id, agent=agent, @@ -371,7 +426,7 @@ class AgentComposerService: agent.updated_by = account_id db.session.commit() - state = cls.load_agent_app_composer(tenant_id=tenant_id, app_id=app_id) + state = cls.load_agent_composer(tenant_id=tenant_id, agent_id=agent.id) state["validation"] = cls.collect_validation_findings( tenant_id=tenant_id, payload=payload, @@ -430,8 +485,6 @@ class AgentComposerService: cls, *, tenant_id: str, agent_id: str, account_id: str, force: bool = False ) -> dict[str, Any]: agent = cls._require_agent(tenant_id=tenant_id, agent_id=agent_id) - if agent.scope != AgentScope.ROSTER or agent.source != AgentSource.AGENT_APP: - raise AgentNotFoundError() normal_draft = cls._get_or_create_agent_draft( tenant_id=tenant_id, agent=agent, @@ -713,11 +766,11 @@ class AgentComposerService: return response.model_dump(mode="json") @classmethod - def get_agent_app_candidates(cls, *, tenant_id: str, app_id: str, user_id: str) -> dict[str, Any]: + def get_agent_app_candidates(cls, *, tenant_id: str, agent_id: str, user_id: str) -> dict[str, Any]: """Slash-menu data source for the Agent App (Console) composer (ENG-615).""" from services.agent.composer_candidates import soul_candidates - agent_soul = cls._load_agent_app_soul(tenant_id=tenant_id, app_id=app_id) + agent_soul = cls._load_agent_soul(tenant_id=tenant_id, agent_id=agent_id) soul_lists, truncated = soul_candidates( agent_soul=agent_soul, dataset_lookup=lambda ids: get_tenant_knowledge_dataset_rows(tenant_id=tenant_id, dataset_ids=ids), @@ -752,8 +805,8 @@ class AgentComposerService: return cls._parse_soul_snapshot(version) @classmethod - def _load_agent_app_soul(cls, *, tenant_id: str, app_id: str) -> AgentSoulConfig | None: - agent = cls._get_agent_app_agent(tenant_id=tenant_id, app_id=app_id) + def _load_agent_soul(cls, *, tenant_id: str, agent_id: str) -> AgentSoulConfig | None: + agent = cls._get_agent_if_present(tenant_id=tenant_id, agent_id=agent_id) if agent is None: return None draft = cls._get_or_create_agent_draft( @@ -1182,6 +1235,15 @@ class AgentComposerService: icon: str | None = None, icon_background: str | None = None, ) -> Agent: + backing_app = AgentRosterService(db.session).create_hidden_backing_app_for_workflow_agent( + tenant_id=tenant_id, + account_id=account_id, + name=name or f"Workflow Agent {node_id}", + description=description, + icon_type=icon_type, + icon=icon, + icon_background=icon_background, + ) agent = Agent( tenant_id=tenant_id, name=name or f"Workflow Agent {node_id}", @@ -1194,6 +1256,7 @@ class AgentComposerService: scope=AgentScope.WORKFLOW_ONLY, source=AgentSource.WORKFLOW, app_id=app_id, + backing_app_id=backing_app.id, workflow_id=workflow_id, workflow_node_id=node_id, status=AgentStatus.ACTIVE, @@ -1762,6 +1825,12 @@ class AgentComposerService: "impact_summary": cls.calculate_impact(tenant_id=binding.tenant_id, current_snapshot_id=version.id) if version else None, + "app_id": binding.app_id, + "backing_app_id": agent.backing_app_id if agent else None, + "hidden_app_backed": bool(agent and agent.scope == AgentScope.WORKFLOW_ONLY and agent.backing_app_id), + "chat_endpoint": f"/console/api/agent/{agent.id}/chat-messages" if agent else None, + "workflow_id": binding.workflow_id, + "node_id": binding.node_id, } @classmethod @@ -1775,6 +1844,10 @@ class AgentComposerService: "icon": agent.icon, "icon_background": agent.icon_background, "scope": agent.scope.value, + "source": agent.source.value, + "app_id": agent.app_id, + "backing_app_id": agent.backing_app_id or agent.app_id, + "hidden_app_backed": bool(agent.scope == AgentScope.WORKFLOW_ONLY and agent.backing_app_id), "status": agent.status.value, "active_config_snapshot_id": agent.active_config_snapshot_id, } diff --git a/api/services/agent/roster_service.py b/api/services/agent/roster_service.py index 646556e990f..d6531450f03 100644 --- a/api/services/agent/roster_service.py +++ b/api/services/agent/roster_service.py @@ -3,6 +3,7 @@ from typing import Any, TypedDict from sqlalchemy import and_, func, or_, select from sqlalchemy.exc import IntegrityError +from constants.model_template import default_app_templates from core.app.entities.app_invoke_entities import InvokeFrom from libs.datetime_utils import naive_utc_now from libs.helper import to_timestamp @@ -23,7 +24,7 @@ from models.agent import ( ) from models.agent_config_entities import AgentSoulConfig from models.enums import AppStatus, ConversationFromSource, ConversationStatus -from models.model import App, AppMode, Conversation, IconType +from models.model import App, AppMode, AppModelConfig, Conversation, IconType from models.workflow import Workflow from services.agent.agent_soul_state import agent_soul_has_model from services.agent.composer_validator import ComposerConfigValidator @@ -100,6 +101,8 @@ class AgentRosterService: "scope": agent.scope.value, "source": agent.source.value, "app_id": agent.app_id, + "backing_app_id": agent.backing_app_id, + "hidden_app_backed": bool(agent.scope == AgentScope.WORKFLOW_ONLY and agent.backing_app_id), "debug_conversation_id": None, "workflow_id": agent.workflow_id, "workflow_node_id": agent.workflow_node_id, @@ -365,6 +368,7 @@ class AgentRosterService: source=AgentSource.AGENT_APP, status=AgentStatus.ACTIVE, app_id=app_id, + backing_app_id=app_id, created_by=account_id, updated_by=account_id, ) @@ -400,6 +404,53 @@ class AgentRosterService: self._get_or_create_agent_app_debug_conversation(agent=agent, account_id=account_id) return agent + def create_hidden_backing_app_for_workflow_agent( + self, + *, + tenant_id: str, + account_id: str | None, + name: str, + description: str = "", + icon_type: Any = None, + icon: str | None = None, + icon_background: str | None = None, + ) -> App: + """Create an internal Agent App used only to back a workflow-only Agent. + + This deliberately bypasses AppService.create_app because that public + creation path also creates a roster Agent. Inline Agents need App runtime + infrastructure for chat/logs/monitoring, but must stay hidden from the + workspace Agent Roster until explicitly saved to roster. + """ + + app_template = dict(default_app_templates[AppMode.AGENT]["app"]) + app = App(**app_template) + app.name = name + app.description = description or "" + app.mode = AppMode.AGENT + normalized_icon_type = self._normalize_app_icon_type(icon_type) + app.icon_type = IconType(normalized_icon_type) if normalized_icon_type else IconType.EMOJI + app.icon = icon + app.icon_background = icon_background + app.tenant_id = tenant_id + app.enable_site = False + app.enable_api = False + app.api_rph = 0 + app.api_rpm = 0 + app.max_active_requests = None + app.created_by = account_id + app.maintainer = account_id + app.updated_by = account_id + self._session.add(app) + self._session.flush() + + app_model_config = AppModelConfig(app_id=app.id, created_by=account_id, updated_by=account_id) + self._session.add(app_model_config) + self._session.flush() + app.app_model_config_id = app_model_config.id + self._session.flush() + return app + def _create_agent_app_debug_conversation(self, *, app_id: str, account_id: str) -> str: """Create one console debug conversation for an Agent App editor.""" @@ -425,8 +476,31 @@ class AgentRosterService: self._session.flush() return conversation.id + @staticmethod + def runtime_backing_app_id(agent: Agent) -> str | None: + """Return the App id that backs Agent runtime chat/log/monitoring.""" + + return agent.backing_app_id or agent.app_id + + def _ensure_workflow_agent_backing_app(self, *, agent: Agent, account_id: str | None) -> str | None: + if agent.scope != AgentScope.WORKFLOW_ONLY or agent.backing_app_id: + return self.runtime_backing_app_id(agent) + backing_app = self.create_hidden_backing_app_for_workflow_agent( + tenant_id=agent.tenant_id, + account_id=account_id or agent.updated_by or agent.created_by, + name=agent.name, + description=agent.description, + icon_type=agent.icon_type, + icon=agent.icon, + icon_background=agent.icon_background, + ) + agent.backing_app_id = backing_app.id + self._session.flush() + return backing_app.id + def _get_or_create_agent_app_debug_conversation(self, *, agent: Agent, account_id: str) -> str: - if not agent.app_id: + backing_app_id = self._ensure_workflow_agent_backing_app(agent=agent, account_id=account_id) + if not backing_app_id: raise AgentNotFoundError() mapping = self._session.scalar( @@ -440,7 +514,7 @@ class AgentRosterService: conversation_id = self._session.scalar( select(Conversation.id).where( Conversation.id == mapping.conversation_id, - Conversation.app_id == agent.app_id, + Conversation.app_id == backing_app_id, Conversation.from_source == ConversationFromSource.CONSOLE, Conversation.from_account_id == account_id, Conversation.is_deleted.is_(False), @@ -450,21 +524,22 @@ class AgentRosterService: return conversation_id mapping.conversation_id = self._create_agent_app_debug_conversation( - app_id=agent.app_id, + app_id=backing_app_id, account_id=account_id, ) + mapping.app_id = backing_app_id self._session.flush() return mapping.conversation_id conversation_id = self._create_agent_app_debug_conversation( - app_id=agent.app_id, + app_id=backing_app_id, account_id=account_id, ) self._session.add( AgentDebugConversation( tenant_id=agent.tenant_id, agent_id=agent.id, - app_id=agent.app_id, + app_id=backing_app_id, account_id=account_id, conversation_id=conversation_id, ) @@ -481,8 +556,6 @@ class AgentRosterService: select(Agent).where( Agent.tenant_id == tenant_id, Agent.id == agent_id, - Agent.scope == AgentScope.ROSTER, - Agent.source == AgentSource.AGENT_APP, Agent.status == AgentStatus.ACTIVE, ) ) @@ -503,16 +576,20 @@ class AgentRosterService: select(Agent).where( Agent.tenant_id == tenant_id, Agent.id == agent_id, - Agent.scope == AgentScope.ROSTER, - Agent.source == AgentSource.AGENT_APP, Agent.status == AgentStatus.ACTIVE, ) ) - if agent is None or not agent.app_id: + if agent is None: + raise AgentNotFoundError() + backing_app_id = self._ensure_workflow_agent_backing_app( + agent=agent, + account_id=agent.updated_by or agent.created_by, + ) + if not backing_app_id: raise AgentNotFoundError() conversation_id = self._create_agent_app_debug_conversation( - app_id=agent.app_id, + app_id=backing_app_id, account_id=account_id, ) mapping = self._session.scalar( @@ -527,13 +604,13 @@ class AgentRosterService: AgentDebugConversation( tenant_id=tenant_id, agent_id=agent_id, - app_id=agent.app_id, + app_id=backing_app_id, account_id=account_id, conversation_id=conversation_id, ) ) else: - mapping.app_id = agent.app_id + mapping.app_id = backing_app_id mapping.conversation_id = conversation_id self._session.flush() if commit: @@ -550,8 +627,7 @@ class AgentRosterService: for agent in agents: if ( agent.tenant_id != tenant_id - or agent.scope != AgentScope.ROSTER - or agent.source != AgentSource.AGENT_APP + or agent.status != AgentStatus.ACTIVE ): continue conversation_ids_by_agent_id[agent.id] = self._get_or_create_agent_app_debug_conversation( @@ -627,6 +703,59 @@ class AgentRosterService: raise AgentNotFoundError() return app + def get_agent_runtime_app_model(self, *, tenant_id: str, agent_id: str) -> App: + """Resolve the App that backs an Agent runtime surface. + + Roster Agents use their public Agent App. Workflow-only Agents use a + hidden Agent App stored in ``backing_app_id`` so console chat/logs can + reuse the app runtime without exposing the resource in workspace app + lists. + """ + + agent = self._session.scalar( + select(Agent) + .where( + Agent.tenant_id == tenant_id, + Agent.id == agent_id, + Agent.status == AgentStatus.ACTIVE, + or_( + and_(Agent.scope == AgentScope.ROSTER, Agent.source == AgentSource.AGENT_APP), + and_( + Agent.scope == AgentScope.WORKFLOW_ONLY, + Agent.source == AgentSource.WORKFLOW, + Agent.workflow_id.is_not(None), + Agent.workflow_node_id.is_not(None), + ), + ), + ) + .limit(1) + ) + if agent is None: + raise AgentNotFoundError() + should_commit_backing_app = agent.scope == AgentScope.WORKFLOW_ONLY and not agent.backing_app_id + backing_app_id = self._ensure_workflow_agent_backing_app( + agent=agent, + account_id=agent.updated_by or agent.created_by, + ) + if not backing_app_id: + raise AgentNotFoundError() + if should_commit_backing_app: + self._session.commit() + + app = self._session.scalar( + select(App) + .where( + App.tenant_id == tenant_id, + App.id == backing_app_id, + App.mode == AppMode.AGENT, + App.status == AppStatus.NORMAL, + ) + .limit(1) + ) + if app is None: + raise AgentNotFoundError() + return app + def duplicate_agent_app( self, *, @@ -694,10 +823,10 @@ class AgentRosterService: return target_app @staticmethod - def _normalize_app_icon_type(icon_type: IconType | str | None) -> str | None: + def _normalize_app_icon_type(icon_type: Any | None) -> str | None: if icon_type is None: return None - if isinstance(icon_type, IconType): + if isinstance(icon_type, IconType) or hasattr(icon_type, "value"): return icon_type.value return icon_type diff --git a/api/services/app_service.py b/api/services/app_service.py index 941855b8321..bd0e3fd08e0 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -96,6 +96,17 @@ class AppService: filters.append(App.mode == AppMode.AGENT_CHAT) elif params.mode == "agent": filters.append(App.mode == AppMode.AGENT) + filters.append( + sa.exists() + .where( + Agent.tenant_id == tenant_id, + Agent.app_id == App.id, + Agent.scope == AgentScope.ROSTER, + Agent.source == AgentSource.AGENT_APP, + Agent.status == AgentStatus.ACTIVE, + ) + .correlate(App) + ) elif params.mode == "all": filters.append(App.mode != AppMode.AGENT) diff --git a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py index 5b854eb7410..371a3df7acc 100644 --- a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py +++ b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py @@ -1228,13 +1228,11 @@ def test_agent_composer_routes_resolve_app_from_agent_id( "agent_soul": {"prompt": {"system_prompt": "x"}}, } - monkeypatch.setattr(composer_controller, "resolve_agent_app_model", lambda **kwargs: SimpleNamespace(id="app-1")) - - def load_agent_app_composer(**kwargs: object) -> dict: + def load_agent_composer(**kwargs: object) -> dict: captured["load"] = kwargs return _agent_app_composer_response() - def save_agent_app_composer(**kwargs: object) -> dict: + def save_agent_composer(**kwargs: object) -> dict: captured["save"] = kwargs return _agent_app_composer_response() @@ -1248,13 +1246,13 @@ def test_agent_composer_routes_resolve_app_from_agent_id( monkeypatch.setattr( composer_controller.AgentComposerService, - "load_agent_app_composer", - load_agent_app_composer, + "load_agent_composer", + load_agent_composer, ) monkeypatch.setattr( composer_controller.AgentComposerService, - "save_agent_app_composer", - save_agent_app_composer, + "save_agent_composer", + save_agent_composer, ) monkeypatch.setattr(composer_controller.ComposerConfigValidator, "validate_publish_payload", lambda payload: None) monkeypatch.setattr( @@ -1269,13 +1267,13 @@ def test_agent_composer_routes_resolve_app_from_agent_id( ) assert unwrap(AgentComposerApi.get)(AgentComposerApi(), "tenant-1", agent_id)["variant"] == "agent_app" - assert cast(dict[str, object], captured["load"])["app_id"] == "app-1" + assert cast(dict[str, object], captured["load"])["agent_id"] == agent_id with app.test_request_context(json=payload): assert ( unwrap(AgentComposerApi.put)(AgentComposerApi(), "tenant-1", account_id, agent_id)["variant"] == "agent_app" ) - assert cast(dict[str, object], captured["save"])["app_id"] == "app-1" + assert cast(dict[str, object], captured["save"])["agent_id"] == agent_id assert unwrap(AgentComposerValidateApi.post)(AgentComposerValidateApi(), "tenant-1", agent_id) == { "result": "success", "errors": [], @@ -1286,7 +1284,7 @@ def test_agent_composer_routes_resolve_app_from_agent_id( candidates = unwrap(AgentComposerCandidatesApi.get)(AgentComposerCandidatesApi(), "tenant-1", account_id, agent_id) assert candidates["variant"] == "agent_app" - assert cast(dict[str, object], captured["candidates"])["app_id"] == "app-1" + assert cast(dict[str, object], captured["candidates"])["agent_id"] == agent_id def test_agent_chat_generate_and_stop_routes_resolve_app_from_agent_id( @@ -1308,7 +1306,7 @@ def test_agent_chat_generate_and_stop_routes_resolve_app_from_agent_id( captured["stop"] = kwargs return {"result": "success"}, 200 - monkeypatch.setattr(completion_controller, "resolve_agent_app_model", resolve_agent_app_model) + monkeypatch.setattr(completion_controller, "resolve_agent_runtime_app_model", resolve_agent_app_model) monkeypatch.setattr(completion_controller, "_create_chat_message", create_chat_message) monkeypatch.setattr(completion_controller, "_stop_chat_message", stop_chat_message) @@ -1511,7 +1509,7 @@ def test_agent_chat_message_routes_resolve_app_from_agent_id(app: Flask, monkeyp captured["detail"] = kwargs return {"id": message_id} - monkeypatch.setattr(message_controller, "resolve_agent_app_model", resolve_agent_app_model) + monkeypatch.setattr(message_controller, "resolve_agent_runtime_app_model", resolve_agent_app_model) monkeypatch.setattr(message_controller, "_list_chat_messages", list_chat_messages) monkeypatch.setattr(message_controller, "_update_message_feedback", update_message_feedback) monkeypatch.setattr(message_controller, "_get_message_suggested_questions", get_message_suggested_questions) diff --git a/api/tests/unit_tests/controllers/console/app/test_agent_app_sandbox.py b/api/tests/unit_tests/controllers/console/app/test_agent_app_sandbox.py index 3cda0a34332..9b0c530f4fd 100644 --- a/api/tests/unit_tests/controllers/console/app/test_agent_app_sandbox.py +++ b/api/tests/unit_tests/controllers/console/app/test_agent_app_sandbox.py @@ -119,7 +119,7 @@ def test_handle_maps_sandbox_and_agent_backend_errors() -> None: def test_agent_app_sandbox_resources_proxy_service(monkeypatch: pytest.MonkeyPatch) -> None: service = _AgentAppService() monkeypatch.setattr(module, "AgentAppSandboxService", lambda: service) - monkeypatch.setattr(module, "resolve_agent_app_model", lambda *, tenant_id, agent_id: _app_model()) + monkeypatch.setattr(module, "resolve_agent_runtime_app_model", lambda *, tenant_id, agent_id: _app_model()) monkeypatch.setattr( module, "query_params_from_request", @@ -151,7 +151,7 @@ def test_agent_app_sandbox_resource_returns_normalized_errors(monkeypatch: pytes raise AgentSandboxInspectorError("no_active_session", "no active session", status_code=404) monkeypatch.setattr(module, "AgentAppSandboxService", FailingService) - monkeypatch.setattr(module, "resolve_agent_app_model", lambda *, tenant_id, agent_id: _app_model()) + monkeypatch.setattr(module, "resolve_agent_runtime_app_model", lambda *, tenant_id, agent_id: _app_model()) monkeypatch.setattr( module, "query_params_from_request", lambda model: SimpleNamespace(conversation_id="conv-1", path=".") ) diff --git a/api/tests/unit_tests/controllers/console/app/test_agent_drive_inspector.py b/api/tests/unit_tests/controllers/console/app/test_agent_drive_inspector.py index 81f6fcf36bf..c8b28ef29f0 100644 --- a/api/tests/unit_tests/controllers/console/app/test_agent_drive_inspector.py +++ b/api/tests/unit_tests/controllers/console/app/test_agent_drive_inspector.py @@ -64,7 +64,7 @@ def test_list_by_agent_filters_value_pointers_out_of_console_payload(): raw = _raw(AgentDriveListByAgentApi.get) with app.test_request_context("/?prefix=pdf-toolkit/"): with ( - patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app, + patch(f"{_MOD}.resolve_agent_runtime_app_model", return_value=_APP) as resolve_app, patch(f"{_MOD}.AgentDriveService") as drive, ): drive.return_value.manifest.return_value = [ @@ -105,7 +105,7 @@ def test_skill_list_by_agent_calls_service(): raw = _raw(AgentDriveSkillListByAgentApi.get) with app.test_request_context("/"): with ( - patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app, + patch(f"{_MOD}.resolve_agent_runtime_app_model", return_value=_APP) as resolve_app, patch(f"{_MOD}.AgentDriveService") as drive, ): drive.return_value.list_skills.return_value = [ @@ -177,7 +177,7 @@ def test_skill_inspect_by_agent_returns_strict_json_response(): } with app.test_request_context("/"): with ( - patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP), + patch(f"{_MOD}.resolve_agent_runtime_app_model", return_value=_APP), patch(f"{_MOD}.AgentDriveService") as drive, ): drive.return_value.inspect_skill.return_value = payload @@ -256,7 +256,7 @@ def test_preview_by_agent_passes_through_and_maps_errors(): raw = _raw(AgentDrivePreviewByAgentApi.get) with app.test_request_context("/?key=pdf-toolkit/SKILL.md"): with ( - patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app, + patch(f"{_MOD}.resolve_agent_runtime_app_model", return_value=_APP) as resolve_app, patch(f"{_MOD}.AgentDriveService") as drive, ): drive.return_value.preview.return_value = { @@ -272,7 +272,7 @@ def test_preview_by_agent_passes_through_and_maps_errors(): with app.test_request_context("/?key=ghost/SKILL.md"): with ( - patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP), + patch(f"{_MOD}.resolve_agent_runtime_app_model", return_value=_APP), patch(f"{_MOD}.AgentDriveService") as drive, ): drive.return_value.preview.side_effect = AgentDriveError( @@ -296,7 +296,7 @@ def test_download_by_agent_returns_signed_url_json(): raw = _raw(AgentDriveDownloadByAgentApi.get) with app.test_request_context("/?key=pdf-toolkit/.DIFY-SKILL-FULL.zip"): with ( - patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app, + patch(f"{_MOD}.resolve_agent_runtime_app_model", return_value=_APP) as resolve_app, patch(f"{_MOD}.AgentDriveService") as drive, ): drive.return_value.download_url.return_value = "https://signed.example/zip" diff --git a/api/tests/unit_tests/controllers/console/app/test_agent_skills.py b/api/tests/unit_tests/controllers/console/app/test_agent_skills.py index bafdc3b46ac..79a363e74ca 100644 --- a/api/tests/unit_tests/controllers/console/app/test_agent_skills.py +++ b/api/tests/unit_tests/controllers/console/app/test_agent_skills.py @@ -67,7 +67,7 @@ def test_upload_by_agent_resolves_app_and_standardizes_into_drive(): with _file_ctx(files={"file": b"zip-bytes"}): with ( - patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app, + patch(f"{_MOD}.resolve_agent_runtime_app_model", return_value=_APP) as resolve_app, patch(f"{_MOD}.SkillStandardizeService") as svc, ): svc.return_value.standardize.return_value = {"skill": {"path": "skill-a"}, "manifest": {}} @@ -174,7 +174,7 @@ def test_files_by_agent_commit_uses_agent_route_and_ignores_node_id(): upload = SimpleNamespace(id="uf-1", name="sample.pdf") with _json_ctx({"upload_file_id": "0fa6f9bc-3416-4476-8857-a13129704dd9"}, query_string="node_id=ignored"): with ( - patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app, + patch(f"{_MOD}.resolve_agent_runtime_app_model", return_value=_APP) as resolve_app, patch(f"{_MOD}.console_ns") as ns, patch(f"{_MOD}.db") as db_mock, patch(f"{_MOD}.AgentDriveService") as drive, @@ -252,7 +252,7 @@ def test_files_by_agent_delete_uses_agent_route_and_ignores_node_id(): raw = _raw(AgentDriveFilesByAgentApi.delete) with _json_ctx(method="DELETE", query_string="key=files/sample.pdf&node_id=ignored"): with ( - patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app, + patch(f"{_MOD}.resolve_agent_runtime_app_model", return_value=_APP) as resolve_app, patch(f"{_MOD}.AgentDriveService") as drive, ): drive.return_value.commit.return_value = [{"key": "files/sample.pdf", "removed": True}] @@ -316,7 +316,7 @@ def test_skill_delete_by_agent_uses_agent_route(): raw = _raw(AgentSkillByAgentApi.delete) with _json_ctx(method="DELETE", query_string="node_id=ignored"): with ( - patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app, + patch(f"{_MOD}.resolve_agent_runtime_app_model", return_value=_APP) as resolve_app, patch(f"{_MOD}.AgentDriveService") as drive, ): drive.return_value.commit.return_value = [{"key": "tender-analyzer/SKILL.md", "removed": True}] @@ -360,7 +360,7 @@ def test_infer_tools_by_agent_uses_agent_route(): raw = _raw(AgentSkillInferToolsByAgentApi.post) with _json_ctx(query_string="node_id=ignored"): with ( - patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app, + patch(f"{_MOD}.resolve_agent_runtime_app_model", return_value=_APP) as resolve_app, patch(f"{_MOD}.SkillToolInferenceService") as svc, ): svc.return_value.infer.return_value = {"inferable": True, "cli_tools": [], "reason": None} diff --git a/api/tests/unit_tests/services/agent/test_agent_services.py b/api/tests/unit_tests/services/agent/test_agent_services.py index f26b74c5e91..165e60e17f5 100644 --- a/api/tests/unit_tests/services/agent/test_agent_services.py +++ b/api/tests/unit_tests/services/agent/test_agent_services.py @@ -28,8 +28,8 @@ from models.agent_config_entities import ( DeclaredOutputType, WorkflowNodeJobConfig, ) -from models.enums import ConversationFromSource, ConversationStatus -from models.model import Conversation, IconType +from models.enums import AppStatus, ConversationFromSource, ConversationStatus +from models.model import App, AppMode, Conversation, IconType from models.workflow import Workflow from services.agent import composer_service, roster_service from services.agent.agent_soul_state import agent_soul_has_model @@ -369,7 +369,7 @@ def test_save_agent_app_composer_creates_agent_when_missing(monkeypatch: pytest. monkeypatch.setattr(composer_service.db, "session", fake_session) monkeypatch.setattr(composer_service.ComposerConfigValidator, "validate_draft_save_payload", lambda payload: None) monkeypatch.setattr(AgentComposerService, "_save_agent_draft", lambda **kwargs: saved_draft) - monkeypatch.setattr(AgentComposerService, "load_agent_app_composer", lambda **kwargs: {"loaded": True}) + monkeypatch.setattr(AgentComposerService, "load_agent_composer", lambda **kwargs: {"loaded": True}) payload = ComposerSavePayload.model_validate( { "variant": ComposerVariant.AGENT_APP.value, @@ -396,6 +396,10 @@ def test_load_agent_app_composer_exposes_draft_save_only(monkeypatch: pytest.Mon active_config_snapshot_id="version-1", updated_by="account-1", created_by="account-1", + app_id="app-1", + backing_app_id="app-1", + scope=AgentScope.ROSTER, + status=AgentStatus.ACTIVE, ) draft = SimpleNamespace(config_snapshot_dict={"prompt": {"system_prompt": "x"}}) @@ -441,7 +445,7 @@ def test_save_agent_app_composer_updates_normal_draft(monkeypatch: pytest.Monkey "_save_agent_draft", lambda **kwargs: saved.update(kwargs) or SimpleNamespace(id="draft-1"), ) - monkeypatch.setattr(AgentComposerService, "load_agent_app_composer", lambda **kwargs: {"loaded": True}) + monkeypatch.setattr(AgentComposerService, "load_agent_composer", lambda **kwargs: {"loaded": True}) payload = ComposerSavePayload.model_validate( { "variant": ComposerVariant.AGENT_APP.value, @@ -581,14 +585,14 @@ def test_agent_app_composer_candidates_and_impact(monkeypatch: pytest.MonkeyPatc raise ValueError("draft workflow not found") monkeypatch.setattr(AgentComposerService, "_get_draft_workflow", _no_draft_workflow) - monkeypatch.setattr(AgentComposerService, "_load_agent_app_soul", lambda **kwargs: None) + monkeypatch.setattr(AgentComposerService, "_load_agent_soul", lambda **kwargs: None) monkeypatch.setattr(AgentComposerService, "_workspace_dify_tools", lambda **kwargs: []) workflow_candidates = AgentComposerService.get_workflow_candidates( tenant_id="tenant-1", app_id="app-1", node_id="node-1", user_id="account-1" ) agent_app_candidates = AgentComposerService.get_agent_app_candidates( - tenant_id="tenant-1", app_id="app-1", user_id="account-1" + tenant_id="tenant-1", agent_id="agent-1", user_id="account-1" ) impact = AgentComposerService.calculate_impact(tenant_id="tenant-1", current_snapshot_id="version-1") @@ -1446,6 +1450,7 @@ def test_composer_create_agents_syncs_active_config_has_model(monkeypatch: pytes fake_session = FakeSession() monkeypatch.setattr(composer_service.db, "session", fake_session) created_apps = [] + hidden_backing_apps = [] backing_agent = Agent( id="roster-agent-1", tenant_id="tenant-1", @@ -1465,6 +1470,10 @@ def test_composer_create_agents_syncs_active_config_has_model(monkeypatch: pytes def __init__(self, session): self.session = session + def create_hidden_backing_app_for_workflow_agent(self, **kwargs): + hidden_backing_apps.append(kwargs) + return SimpleNamespace(id="hidden-app-1") + def get_app_backing_agent(self, *, tenant_id, app_id): assert tenant_id == "tenant-1" assert app_id == "app-agent-1" @@ -1503,6 +1512,8 @@ def test_composer_create_agents_syncs_active_config_has_model(monkeypatch: pytes assert workflow_agent.active_config_snapshot_id == "version-with-model" assert workflow_agent.active_config_has_model is True + assert workflow_agent.backing_app_id == "hidden-app-1" + assert hidden_backing_apps[0]["name"] == "Workflow Agent node-1" assert roster_agent.active_config_snapshot_id == "version-with-model" assert roster_agent.active_config_has_model is True assert roster_agent.source == AgentSource.AGENT_APP @@ -2079,6 +2090,42 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch: pytest.MonkeyPatch assert loaded_versions["version-1"].agent_id == "agent-1" +def test_get_agent_runtime_app_model_creates_hidden_backing_app_for_existing_inline_agent(): + agent = Agent( + id="agent-1", + tenant_id="tenant-1", + app_id="workflow-app-1", + workflow_id="workflow-1", + workflow_node_id="node-1", + name="Inline Agent", + description="desc", + agent_kind=AgentKind.DIFY_AGENT, + scope=AgentScope.WORKFLOW_ONLY, + source=AgentSource.WORKFLOW, + status=AgentStatus.ACTIVE, + created_by="account-1", + updated_by="account-1", + ) + backing_app = App( + id="generated-1", + tenant_id="tenant-1", + name="Inline Agent", + mode=AppMode.AGENT, + status=AppStatus.NORMAL, + ) + session = FakeSession(scalar=[agent, backing_app]) + service = AgentRosterService(session) + + resolved_app = service.get_agent_runtime_app_model(tenant_id="tenant-1", agent_id="agent-1") + + assert resolved_app is backing_app + assert agent.backing_app_id == "generated-1" + assert session.commits == 1 + created_app = next(value for value in session.added if isinstance(value, App)) + assert created_app.enable_site is False + assert created_app.enable_api is False + + def test_agent_app_debug_conversation_create_reuse_and_recreate(): agent = Agent( id="agent-1", @@ -2316,6 +2363,18 @@ def test_app_list_all_excludes_agent_apps_by_default(): assert "apps.mode != :mode_1" in sql +def test_app_list_agent_mode_requires_visible_roster_backing_agent(): + filters = AppService._build_app_list_filters( + "account-1", "tenant-1", AppListParams(mode="agent"), FakeSession(scalar=None, scalars=None) + ) + sql = " ".join(str(filter_) for filter_ in filters) + + assert "EXISTS" in sql + assert "agents.app_id = apps.id" in sql + assert "agents.scope" in sql + assert "agents.source" in sql + + def test_validator_dict_helpers_wrap_validation_errors(): valid_soul = ComposerConfigValidator.validate_agent_soul_dict({"prompt": {"system_prompt": "x"}}) valid_node_job = ComposerConfigValidator.validate_node_job_dict({"workflow_prompt": "x"}) diff --git a/packages/contracts/generated/api/console/agent/types.gen.ts b/packages/contracts/generated/api/console/agent/types.gen.ts index 80cd58dd9fa..975d2215f63 100644 --- a/packages/contracts/generated/api/console/agent/types.gen.ts +++ b/packages/contracts/generated/api/console/agent/types.gen.ts @@ -26,6 +26,7 @@ export type AgentAppDetailWithSite = { active_config_is_published?: boolean api_base_url?: string | null app_id?: string | null + backing_app_id?: string | null bound_agent_id?: string | null created_at?: number | null created_by?: string | null @@ -34,6 +35,7 @@ export type AgentAppDetailWithSite = { description?: string | null enable_api: boolean enable_site: boolean + hidden_app_backed?: boolean icon?: string | null icon_background?: string | null icon_type?: string | null @@ -168,7 +170,11 @@ export type AgentAppComposerResponse = { active_config_snapshot?: AgentConfigSnapshotSummaryResponse | null agent: AgentComposerAgentResponse agent_soul: AgentSoulConfig + app_id?: string | null + backing_app_id?: string | null + chat_endpoint?: string | null draft?: AgentConfigDraftSummaryResponse | null + hidden_app_backed?: boolean save_options: Array validation?: ComposerValidationFindingsResponse | null variant: 'agent_app' @@ -410,6 +416,7 @@ export type AgentAppPartial = { active_config_is_published?: boolean app_id?: string | null author_name?: string | null + backing_app_id?: string | null bound_agent_id?: string | null create_user_name?: string | null created_at?: number | null @@ -417,6 +424,7 @@ export type AgentAppPartial = { debug_conversation_id?: string | null description?: string | null has_draft_trigger?: boolean | null + hidden_app_backed?: boolean icon?: string | null icon_background?: string | null icon_type?: string | null @@ -516,10 +524,12 @@ export type AgentInviteOptionResponse = { app_id?: string | null archived_at?: number | null archived_by?: string | null + backing_app_id?: string | null created_at?: number | null created_by?: string | null description: string existing_node_ids?: Array + hidden_app_backed?: boolean icon?: string | null icon_background?: string | null icon_type?: AgentIconType | null @@ -602,7 +612,10 @@ export type AgentConfigSnapshotSummaryResponse = { export type AgentComposerAgentResponse = { active_config_snapshot_id?: string | null + app_id?: string | null + backing_app_id?: string | null description: string + hidden_app_backed?: boolean icon?: string | null icon_background?: string | null icon_type?: string | null @@ -610,6 +623,7 @@ export type AgentComposerAgentResponse = { name: string role?: string | null scope: AgentScope + source?: AgentSource | null status: AgentStatus } @@ -1672,6 +1686,7 @@ export type AgentAppDetailWithSiteWritable = { active_config_is_published?: boolean api_base_url?: string | null app_id?: string | null + backing_app_id?: string | null bound_agent_id?: string | null created_at?: number | null created_by?: string | null @@ -1680,6 +1695,7 @@ export type AgentAppDetailWithSiteWritable = { description?: string | null enable_api: boolean enable_site: boolean + hidden_app_backed?: boolean icon?: string | null icon_background?: string | null icon_type?: string | null @@ -1705,6 +1721,7 @@ export type AgentAppPartialWritable = { active_config_is_published?: boolean app_id?: string | null author_name?: string | null + backing_app_id?: string | null bound_agent_id?: string | null create_user_name?: string | null created_at?: number | null @@ -1712,6 +1729,7 @@ export type AgentAppPartialWritable = { debug_conversation_id?: string | null description?: string | null has_draft_trigger?: boolean | null + hidden_app_backed?: boolean icon?: string | null icon_background?: string | null icon_type?: string | null diff --git a/packages/contracts/generated/api/console/agent/zod.gen.ts b/packages/contracts/generated/api/console/agent/zod.gen.ts index 34d5c92215a..0208a1ce361 100644 --- a/packages/contracts/generated/api/console/agent/zod.gen.ts +++ b/packages/contracts/generated/api/console/agent/zod.gen.ts @@ -793,6 +793,7 @@ export const zAgentAppPartial = z.object({ active_config_is_published: z.boolean().optional().default(false), app_id: z.string().nullish(), author_name: z.string().nullish(), + backing_app_id: z.string().nullish(), bound_agent_id: z.string().nullish(), create_user_name: z.string().nullish(), created_at: z.int().nullish(), @@ -800,6 +801,7 @@ export const zAgentAppPartial = z.object({ debug_conversation_id: z.string().nullish(), description: z.string().nullish(), has_draft_trigger: z.boolean().nullish(), + hidden_app_backed: z.boolean().optional().default(false), icon: z.string().nullish(), icon_background: z.string().nullish(), icon_type: z.string().nullish(), @@ -858,6 +860,7 @@ export const zAgentAppDetailWithSite = z.object({ active_config_is_published: z.boolean().optional().default(false), api_base_url: z.string().nullish(), app_id: z.string().nullish(), + backing_app_id: z.string().nullish(), bound_agent_id: z.string().nullish(), created_at: z.int().nullish(), created_by: z.string().nullish(), @@ -866,6 +869,7 @@ export const zAgentAppDetailWithSite = z.object({ description: z.string().nullish(), enable_api: z.boolean(), enable_site: z.boolean(), + hidden_app_backed: z.boolean().optional().default(false), icon: z.string().nullish(), icon_background: z.string().nullish(), icon_type: z.string().nullish(), @@ -945,10 +949,12 @@ export const zAgentInviteOptionResponse = z.object({ app_id: z.string().nullish(), archived_at: z.int().nullish(), archived_by: z.string().nullish(), + backing_app_id: z.string().nullish(), created_at: z.int().nullish(), created_by: z.string().nullish(), description: z.string(), existing_node_ids: z.array(z.string()).optional(), + hidden_app_backed: z.boolean().optional().default(false), icon: z.string().nullish(), icon_background: z.string().nullish(), icon_type: zAgentIconType.nullish(), @@ -985,7 +991,10 @@ export const zAgentInviteOptionsResponse = z.object({ */ export const zAgentComposerAgentResponse = z.object({ active_config_snapshot_id: z.string().nullish(), + app_id: z.string().nullish(), + backing_app_id: z.string().nullish(), description: z.string(), + hidden_app_backed: z.boolean().optional().default(false), icon: z.string().nullish(), icon_background: z.string().nullish(), icon_type: z.string().nullish(), @@ -993,6 +1002,7 @@ export const zAgentComposerAgentResponse = z.object({ name: z.string(), role: z.string().nullish(), scope: zAgentScope, + source: zAgentSource.nullish(), status: zAgentStatus, }) @@ -2186,7 +2196,11 @@ export const zAgentAppComposerResponse = z.object({ active_config_snapshot: zAgentConfigSnapshotSummaryResponse.nullish(), agent: zAgentComposerAgentResponse, agent_soul: zAgentSoulConfig, + app_id: z.string().nullish(), + backing_app_id: z.string().nullish(), + chat_endpoint: z.string().nullish(), draft: zAgentConfigDraftSummaryResponse.nullish(), + hidden_app_backed: z.boolean().optional().default(false), save_options: z.array(zComposerSaveStrategy), validation: zComposerValidationFindingsResponse.nullish(), variant: z.literal('agent_app'), @@ -2338,6 +2352,7 @@ export const zAgentAppPartialWritable = z.object({ active_config_is_published: z.boolean().optional().default(false), app_id: z.string().nullish(), author_name: z.string().nullish(), + backing_app_id: z.string().nullish(), bound_agent_id: z.string().nullish(), create_user_name: z.string().nullish(), created_at: z.int().nullish(), @@ -2345,6 +2360,7 @@ export const zAgentAppPartialWritable = z.object({ debug_conversation_id: z.string().nullish(), description: z.string().nullish(), has_draft_trigger: z.boolean().nullish(), + hidden_app_backed: z.boolean().optional().default(false), icon: z.string().nullish(), icon_background: z.string().nullish(), icon_type: z.string().nullish(), @@ -2414,6 +2430,7 @@ export const zAgentAppDetailWithSiteWritable = z.object({ active_config_is_published: z.boolean().optional().default(false), api_base_url: z.string().nullish(), app_id: z.string().nullish(), + backing_app_id: z.string().nullish(), bound_agent_id: z.string().nullish(), created_at: z.int().nullish(), created_by: z.string().nullish(), @@ -2422,6 +2439,7 @@ export const zAgentAppDetailWithSiteWritable = z.object({ description: z.string().nullish(), enable_api: z.boolean(), enable_site: z.boolean(), + hidden_app_backed: z.boolean().optional().default(false), icon: z.string().nullish(), icon_background: z.string().nullish(), icon_type: z.string().nullish(), diff --git a/packages/contracts/generated/api/console/apps/types.gen.ts b/packages/contracts/generated/api/console/apps/types.gen.ts index 51eb19e8a4c..d396351b2b8 100644 --- a/packages/contracts/generated/api/console/apps/types.gen.ts +++ b/packages/contracts/generated/api/console/apps/types.gen.ts @@ -970,8 +970,11 @@ export type WorkflowAgentComposerResponse = { agent?: AgentComposerAgentResponse | null agent_soul: AgentSoulConfig app_id?: string | null + backing_app_id?: string | null binding?: AgentComposerBindingResponse | null + chat_endpoint?: string | null effective_declared_outputs?: Array + hidden_app_backed?: boolean impact_summary?: AgentComposerImpactResponse | null node_id?: string | null node_job: WorkflowNodeJobConfig @@ -1797,7 +1800,10 @@ export type AgentConfigSnapshotSummaryResponse = { export type AgentComposerAgentResponse = { active_config_snapshot_id?: string | null + app_id?: string | null + backing_app_id?: string | null description: string + hidden_app_backed?: boolean icon?: string | null icon_background?: string | null icon_type?: string | null @@ -1805,6 +1811,7 @@ export type AgentComposerAgentResponse = { name: string role?: string | null scope: AgentScope + source?: AgentSource | null status: AgentStatus } @@ -2121,6 +2128,8 @@ export type WorkflowRunForArchivedLogResponse = { export type AgentScope = 'roster' | 'workflow_only' +export type AgentSource = 'agent_app' | 'imported' | 'roster' | 'system' | 'workflow' + export type AgentStatus = 'active' | 'archived' export type AgentSoulAppFeaturesConfig = { diff --git a/packages/contracts/generated/api/console/apps/zod.gen.ts b/packages/contracts/generated/api/console/apps/zod.gen.ts index d0bf1054790..ea9b9a6dc50 100644 --- a/packages/contracts/generated/api/console/apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/apps/zod.gen.ts @@ -2496,6 +2496,13 @@ export const zWorkflowArchivedLogPaginationResponse = z.object({ */ export const zAgentScope = z.enum(['roster', 'workflow_only']) +/** + * AgentSource + * + * Origin that created or imported the Agent. + */ +export const zAgentSource = z.enum(['agent_app', 'imported', 'roster', 'system', 'workflow']) + /** * AgentStatus * @@ -2508,7 +2515,10 @@ export const zAgentStatus = z.enum(['active', 'archived']) */ export const zAgentComposerAgentResponse = z.object({ active_config_snapshot_id: z.string().nullish(), + app_id: z.string().nullish(), + backing_app_id: z.string().nullish(), description: z.string(), + hidden_app_backed: z.boolean().optional().default(false), icon: z.string().nullish(), icon_background: z.string().nullish(), icon_type: z.string().nullish(), @@ -2516,6 +2526,7 @@ export const zAgentComposerAgentResponse = z.object({ name: z.string(), role: z.string().nullish(), scope: zAgentScope, + source: zAgentSource.nullish(), status: zAgentStatus, }) @@ -3643,8 +3654,11 @@ export const zWorkflowAgentComposerResponse = z.object({ agent: zAgentComposerAgentResponse.nullish(), agent_soul: zAgentSoulConfig, app_id: z.string().nullish(), + backing_app_id: z.string().nullish(), binding: zAgentComposerBindingResponse.nullish(), + chat_endpoint: z.string().nullish(), effective_declared_outputs: z.array(zDeclaredOutputConfig).optional(), + hidden_app_backed: z.boolean().optional().default(false), impact_summary: zAgentComposerImpactResponse.nullish(), node_id: z.string().nullish(), node_job: zWorkflowNodeJobConfig, From bbcc3a4c65eb531411479b7ae2acaa997288e2ce Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:15:08 +0000 Subject: [PATCH 2/2] [autofix.ci] apply automated fixes --- api/openapi/markdown/console-openapi.md | 19 +++++++++++++++++++ api/services/agent/roster_service.py | 5 +---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index e5dbed46d31..68dbfbd162e 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -12271,7 +12271,11 @@ Default namespace | active_config_snapshot | [AgentConfigSnapshotSummaryResponse](#agentconfigsnapshotsummaryresponse) | | No | | agent | [AgentComposerAgentResponse](#agentcomposeragentresponse) | | Yes | | agent_soul | [AgentSoulConfig](#agentsoulconfig) | | Yes | +| app_id | string | | No | +| backing_app_id | string | | No | +| chat_endpoint | string | | No | | draft | [AgentConfigDraftSummaryResponse](#agentconfigdraftsummaryresponse) | | No | +| hidden_app_backed | boolean | | No | | save_options | [ [ComposerSaveStrategy](#composersavestrategy) ] | | Yes | | validation | [ComposerValidationFindingsResponse](#composervalidationfindingsresponse) | | No | | variant | string | | Yes | @@ -12306,6 +12310,7 @@ Default namespace | active_config_is_published | boolean | | No | | api_base_url | string | | No | | app_id | string | | No | +| backing_app_id | string | | No | | bound_agent_id | string | | No | | created_at | integer | | No | | created_by | string | | No | @@ -12314,6 +12319,7 @@ Default namespace | description | string | | No | | enable_api | boolean | | Yes | | enable_site | boolean | | Yes | +| hidden_app_backed | boolean | | No | | icon | string | | No | | icon_background | string | | No | | icon_type | string | | No | @@ -12369,6 +12375,7 @@ default (the config form sends the full desired feature state on save). | active_config_is_published | boolean | | No | | app_id | string | | No | | author_name | string | | No | +| backing_app_id | string | | No | | bound_agent_id | string | | No | | create_user_name | string | | No | | created_at | integer | | No | @@ -12376,6 +12383,7 @@ default (the config form sends the full desired feature state on save). | debug_conversation_id | string | | No | | description | string | | No | | has_draft_trigger | boolean | | No | +| hidden_app_backed | boolean | | No | | icon | string | | No | | icon_background | string | | No | | icon_type | string | | No | @@ -12517,7 +12525,10 @@ Risk marker for CLI tool bootstrap commands. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | active_config_snapshot_id | string | | No | +| app_id | string | | No | +| backing_app_id | string | | No | | description | string | | Yes | +| hidden_app_backed | boolean | | No | | icon | string | | No | | icon_background | string | | No | | icon_type | string | | No | @@ -12525,6 +12536,7 @@ Risk marker for CLI tool bootstrap commands. | name | string | | Yes | | role | string | | No | | scope | [AgentScope](#agentscope) | | Yes | +| source | [AgentSource](#agentsource) | | No | | status | [AgentStatus](#agentstatus) | | Yes | #### AgentComposerBindingResponse @@ -12957,10 +12969,12 @@ Supported icon storage formats for Agent roster entries. | app_id | string | | No | | archived_at | integer | | No | | archived_by | string | | No | +| backing_app_id | string | | No | | created_at | integer | | No | | created_by | string | | No | | description | string | | Yes | | existing_node_ids | [ string ] | | No | +| hidden_app_backed | boolean | | No | | icon | string | | No | | icon_background | string | | No | | icon_type | [AgentIconType](#agenticontype) | | No | @@ -13395,9 +13409,11 @@ section may be empty, which is how callers express "no knowledge layer". | app_id | string | | No | | archived_at | integer | | No | | archived_by | string | | No | +| backing_app_id | string | | No | | created_at | integer | | No | | created_by | string | | No | | description | string | | Yes | +| hidden_app_backed | boolean | | No | | icon | string | | No | | icon_background | string | | No | | icon_type | [AgentIconType](#agenticontype) | | No | @@ -20706,8 +20722,11 @@ How a workflow node is bound to an Agent. | agent | [AgentComposerAgentResponse](#agentcomposeragentresponse) | | No | | agent_soul | [AgentSoulConfig](#agentsoulconfig) | | Yes | | app_id | string | | No | +| backing_app_id | string | | No | | binding | [AgentComposerBindingResponse](#agentcomposerbindingresponse) | | No | +| chat_endpoint | string | | No | | effective_declared_outputs | [ [DeclaredOutputConfig](#declaredoutputconfig) ] | | No | +| hidden_app_backed | boolean | | No | | impact_summary | [AgentComposerImpactResponse](#agentcomposerimpactresponse) | | No | | node_id | string | | No | | node_job | [WorkflowNodeJobConfig](#workflownodejobconfig) | | Yes | diff --git a/api/services/agent/roster_service.py b/api/services/agent/roster_service.py index d6531450f03..f16258f3c75 100644 --- a/api/services/agent/roster_service.py +++ b/api/services/agent/roster_service.py @@ -625,10 +625,7 @@ class AgentRosterService: conversation_ids_by_agent_id: dict[str, str] = {} changed = False for agent in agents: - if ( - agent.tenant_id != tenant_id - or agent.status != AgentStatus.ACTIVE - ): + if agent.tenant_id != tenant_id or agent.status != AgentStatus.ACTIVE: continue conversation_ids_by_agent_id[agent.id] = self._get_or_create_agent_app_debug_conversation( agent=agent,