Merge branch 'feat/agent-v2' of github.com:langgenius/dify into feat/agent-v2

This commit is contained in:
Yanli 盐粒 2026-06-25 22:17:43 +08:00
commit a84f70445a
27 changed files with 553 additions and 120 deletions

View File

@ -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))

View File

@ -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/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/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,
),
)

View File

@ -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)

View File

@ -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)

View File

@ -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(

View File

@ -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(

View File

@ -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:

View File

@ -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)

View File

@ -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)

View File

@ -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")

View File

@ -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):

View File

@ -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")

View File

@ -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)

View File

@ -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,
)
)

View File

@ -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 |

View File

@ -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,
}

View File

@ -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:
@ -548,11 +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.scope != AgentScope.ROSTER
or agent.source != AgentSource.AGENT_APP
):
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,
@ -627,6 +700,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 +820,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

View File

@ -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)

View File

@ -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)

View File

@ -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=".")
)

View File

@ -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"

View File

@ -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}

View File

@ -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"})

View File

@ -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<ComposerSaveStrategy>
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<string>
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

View File

@ -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(),

View File

@ -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<DeclaredOutputConfig>
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 = {

View File

@ -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,