diff --git a/api/controllers/console/agent/roster.py b/api/controllers/console/agent/roster.py index fc37845d63..faa97ada0d 100644 --- a/api/controllers/console/agent/roster.py +++ b/api/controllers/console/agent/roster.py @@ -95,7 +95,8 @@ def _serialize_agent_app_detail(app_model) -> dict: app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id)) app_model.access_mode = app_setting.access_mode # type: ignore[attr-defined] - agent = _agent_roster_service().get_app_backing_agent(tenant_id=app_model.tenant_id, app_id=app_model.id) + roster_service = _agent_roster_service() + agent = roster_service.get_app_backing_agent(tenant_id=app_model.tenant_id, app_id=app_model.id) if not agent: raise AgentNotFoundError() payload = AppDetailWithSite.model_validate(app_model, from_attributes=True).model_dump(mode="json") @@ -103,15 +104,24 @@ def _serialize_agent_app_detail(app_model) -> dict: payload["app_id"] = str(app_model.id) payload["id"] = agent.id payload["role"] = agent.role or "" + payload["active_config_is_published"] = roster_service.active_config_is_published( + tenant_id=app_model.tenant_id, + agent=agent, + ) return payload def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str) -> dict: app_ids = [str(app.id) for app in app_pagination.items] - agents_by_app_id = _agent_roster_service().load_app_backing_agents_by_app_id( + roster_service = _agent_roster_service() + agents_by_app_id = roster_service.load_app_backing_agents_by_app_id( tenant_id=tenant_id, app_ids=app_ids, ) + active_config_is_published_by_agent_id = roster_service.load_active_config_is_published_by_agent_id( + tenant_id=tenant_id, + agents=list(agents_by_app_id.values()), + ) payload = AppPagination.model_validate(app_pagination, from_attributes=True).model_dump(mode="json") for item in payload["data"]: app_id = item["id"] @@ -121,6 +131,7 @@ def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str) -> dict: item["app_id"] = app_id item["id"] = agent.id item["role"] = agent.role or "" + item["active_config_is_published"] = active_config_is_published_by_agent_id.get(agent.id, False) return payload diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 5cd674e4ea..2fb3a40296 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -403,6 +403,7 @@ class AppPartial(ResponseModel): # For Agent App responses exposed through /agent. app_id: str | None = None role: str | None = None + active_config_is_published: bool = False is_starred: bool = False @computed_field(return_type=str | None) # type: ignore @@ -457,6 +458,7 @@ class AppDetailWithSite(AppDetail): # For Agent App responses exposed through /agent. app_id: str | None = None role: str | None = None + active_config_is_published: bool = False @computed_field(return_type=str | None) # type: ignore @property diff --git a/api/core/workflow/nodes/agent_v2/runtime_request_builder.py b/api/core/workflow/nodes/agent_v2/runtime_request_builder.py index d4aa43898d..53c657e8ef 100644 --- a/api/core/workflow/nodes/agent_v2/runtime_request_builder.py +++ b/api/core/workflow/nodes/agent_v2/runtime_request_builder.py @@ -42,6 +42,7 @@ from models.agent import Agent, AgentConfigSnapshot, WorkflowAgentNodeBinding from models.agent_config_entities import ( AgentSoulConfig, DeclaredArrayItem, + DeclaredOutputChildConfig, DeclaredOutputConfig, DeclaredOutputType, WorkflowNodeJobConfig, @@ -395,7 +396,11 @@ class WorkflowAgentRuntimeRequestBuilder: @staticmethod def _schema_for_declared_output(output: DeclaredOutputConfig) -> dict[str, Any]: - schema = WorkflowAgentRuntimeRequestBuilder._schema_for_type(output.type, array_item=output.array_item) + schema = WorkflowAgentRuntimeRequestBuilder._schema_for_type( + output.type, + array_item=output.array_item, + children=output.children, + ) if output.description: schema["description"] = output.description return schema @@ -405,6 +410,7 @@ class WorkflowAgentRuntimeRequestBuilder: output_type: DeclaredOutputType, *, array_item: DeclaredArrayItem | None = None, + children: Sequence[DeclaredOutputChildConfig] | None = None, ) -> dict[str, Any]: match output_type: case DeclaredOutputType.STRING: @@ -414,18 +420,23 @@ class WorkflowAgentRuntimeRequestBuilder: case DeclaredOutputType.BOOLEAN: return {"type": "boolean"} case DeclaredOutputType.OBJECT: - return {"type": "object"} + object_schema: dict[str, Any] = {"type": "object"} + WorkflowAgentRuntimeRequestBuilder._apply_child_properties(object_schema, children or []) + return object_schema case DeclaredOutputType.ARRAY: # Stage 4 §4.2: items shape mirrors the declared array_item. # Validator guarantees array_item is set when type is array. item_type = array_item.type if array_item else DeclaredOutputType.OBJECT - schema: dict[str, Any] = { + array_schema: dict[str, Any] = { "type": "array", - "items": WorkflowAgentRuntimeRequestBuilder._schema_for_type(item_type), + "items": WorkflowAgentRuntimeRequestBuilder._schema_for_type( + item_type, + children=array_item.children if array_item else None, + ), } if array_item is not None and array_item.description: - schema["items"]["description"] = array_item.description - return schema + array_schema["items"]["description"] = array_item.description + return array_schema case DeclaredOutputType.FILE: return { "oneOf": [ @@ -469,6 +480,27 @@ class WorkflowAgentRuntimeRequestBuilder: } assert_never(output_type) + @staticmethod + def _apply_child_properties(schema: dict[str, Any], children: Sequence[DeclaredOutputChildConfig]) -> None: + if not children: + return + properties: dict[str, Any] = {} + required: list[str] = [] + for child in children: + child_schema = WorkflowAgentRuntimeRequestBuilder._schema_for_type( + child.type, + array_item=child.array_item, + children=child.children, + ) + if child.description: + child_schema["description"] = child.description + properties[child.name] = child_schema + if child.required: + required.append(child.name) + schema["properties"] = properties + if required: + schema["required"] = required + @staticmethod def _normalize_credentials(credentials: Mapping[str, Any]) -> dict[str, str | int | float | bool | None]: normalized: dict[str, str | int | float | bool | None] = {} @@ -669,7 +701,13 @@ def _shell_secret_ref(item: object) -> DifyShellSecretRefConfig | None: name = _name_from_mapping(data) if name is None: return None - ref = data.get("ref") or data.get("id") or data.get("credential_id") or data.get("provider_credential_id") + ref = ( + data.get("ref") + or data.get("value") + or data.get("id") + or data.get("credential_id") + or data.get("provider_credential_id") + ) return DifyShellSecretRefConfig(name=name, ref=str(ref) if ref is not None else None) diff --git a/api/fields/agent_fields.py b/api/fields/agent_fields.py index 0f22fd9a86..36d9623198 100644 --- a/api/fields/agent_fields.py +++ b/api/fields/agent_fields.py @@ -70,6 +70,7 @@ class AgentRosterResponse(ResponseModel): workflow_node_id: str | None = None active_config_snapshot_id: str | None = None active_config_snapshot: AgentConfigSnapshotSummaryResponse | None = None + active_config_is_published: bool = False status: AgentStatus created_by: str | None = None updated_by: str | None = None diff --git a/api/models/agent_config_entities.py b/api/models/agent_config_entities.py index 1f50924681..76108f271d 100644 --- a/api/models/agent_config_entities.py +++ b/api/models/agent_config_entities.py @@ -2,9 +2,9 @@ from __future__ import annotations import re from enum import StrEnum -from typing import Any, Final, Literal +from typing import Annotated, Any, Final, Literal -from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from pydantic import BaseModel, ConfigDict, Field, WithJsonSchema, field_validator, model_validator from core.workflow.file_reference import is_canonical_file_reference from graphon.file import FileTransferMethod @@ -29,6 +29,44 @@ class DeclaredOutputType(StrEnum): FILE = "file" +_DECLARED_OUTPUT_CHILDREN_JSON_SCHEMA = { + "type": "array", + "items": { + "type": "object", + "additionalProperties": False, + "properties": { + "name": {"type": "string"}, + "type": { + "type": "string", + "enum": [item.value for item in DeclaredOutputType], + }, + "description": {"anyOf": [{"type": "string"}, {"type": "null"}]}, + "required": {"type": "boolean"}, + "file": {"type": "object", "additionalProperties": True}, + "array_item": { + "type": "object", + "additionalProperties": True, + "properties": { + "type": { + "type": "string", + "enum": [item.value for item in DeclaredOutputType], + }, + "description": {"anyOf": [{"type": "string"}, {"type": "null"}]}, + "children": {"type": "array", "items": {"type": "object", "additionalProperties": True}}, + }, + }, + "children": {"type": "array", "items": {"type": "object", "additionalProperties": True}}, + }, + "required": ["name", "type"], + }, +} + +DeclaredOutputChildren = Annotated[ + list["DeclaredOutputChildConfig"], + WithJsonSchema(_DECLARED_OUTPUT_CHILDREN_JSON_SCHEMA), +] + + class AgentCliToolAuthorizationStatus(StrEnum): """Authorization state for Agent-scoped CLI tools. @@ -148,6 +186,9 @@ class AgentSecretRefConfig(AgentFlexibleConfig): env_name: str | None = Field(default=None, max_length=255) variable: str | None = Field(default=None, max_length=255) type: str | None = Field(default=None, max_length=64) + # UI-facing selected secret reference. This is a credential/ref id, not the + # plaintext secret value; runtime maps it to the shell-layer ``ref``. + value: str | None = Field(default=None, max_length=255) id: str | None = Field(default=None, max_length=255) ref: str | None = Field(default=None, max_length=255) credential_id: str | None = Field(default=None, max_length=255) @@ -507,11 +548,55 @@ class DeclaredArrayItem(BaseModel): type: DeclaredOutputType description: str | None = None + children: DeclaredOutputChildren = Field(default_factory=list) @model_validator(mode="after") def _reject_nested_array(self) -> DeclaredArrayItem: if self.type == DeclaredOutputType.ARRAY: raise ValueError("nested arrays are not supported as array_item.type") + if self.children and self.type != DeclaredOutputType.OBJECT: + raise ValueError("array_item.children is only allowed when array_item.type is object") + return self + + +class DeclaredOutputChildConfig(BaseModel): + """Nested field under an object-shaped declared output. + + The first backend version keeps child fields lightweight: they describe the + variable-picker/schema tree but do not own independent retry/check behavior. + """ + + model_config = ConfigDict(extra="forbid") + + name: str = Field(min_length=1, max_length=255) + type: DeclaredOutputType + description: str | None = None + required: bool = True + file: DeclaredOutputFileConfig | None = None + array_item: DeclaredArrayItem | None = None + children: DeclaredOutputChildren = Field(default_factory=list) + + @model_validator(mode="after") + def _validate_shape(self) -> DeclaredOutputChildConfig: + if not _OUTPUT_NAME_PATTERN.fullmatch(self.name): + raise ValueError( + f"output child name {self.name!r} must match {_OUTPUT_NAME_PATTERN.pattern} " + "(JSON-schema-friendly identifier)" + ) + if self.type == DeclaredOutputType.FILE: + if self.file is None: + self.file = DeclaredOutputFileConfig() + elif self.file is not None: + raise ValueError("file metadata is only allowed for file output children") + + if self.type == DeclaredOutputType.ARRAY: + if self.array_item is None: + self.array_item = DeclaredArrayItem(type=DeclaredOutputType.OBJECT) + elif self.array_item is not None: + raise ValueError("array_item is only allowed when child type is array") + + if self.children and self.type != DeclaredOutputType.OBJECT: + raise ValueError("children is only allowed for object output children") return self @@ -592,6 +677,7 @@ class DeclaredOutputConfig(BaseModel): required: bool = True file: DeclaredOutputFileConfig | None = None array_item: DeclaredArrayItem | None = None + children: DeclaredOutputChildren = Field(default_factory=list) check: DeclaredOutputCheckConfig | None = None failure_strategy: DeclaredOutputFailureStrategy = Field(default_factory=DeclaredOutputFailureStrategy) @@ -625,6 +711,9 @@ class DeclaredOutputConfig(BaseModel): elif self.array_item is not None: raise ValueError("array_item is only allowed when type is array") + if self.children and self.type != DeclaredOutputType.OBJECT: + raise ValueError("children is only allowed for object outputs") + # Per PRD §OUTPUT 配置框: output check is file-only. if self.check is not None and self.check.enabled and self.type != DeclaredOutputType.FILE: raise ValueError("output check is only allowed for file outputs") diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 688f22e0e8..2f2e8ae15b 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -11703,6 +11703,7 @@ Supported icon storage formats for Agent roster entries. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | +| active_config_is_published | boolean | | No | | active_config_snapshot | [AgentConfigSnapshotSummaryResponse](#agentconfigsnapshotsummaryresponse) | | No | | active_config_snapshot_id | string | | No | | agent_kind | [AgentKind](#agentkind) | | Yes | @@ -11924,6 +11925,7 @@ the current roster/workflow APIs scoped to Dify Agent. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | +| active_config_is_published | boolean | | No | | active_config_snapshot | [AgentConfigSnapshotSummaryResponse](#agentconfigsnapshotsummaryresponse) | | No | | active_config_snapshot_id | string | | No | | agent_kind | [AgentKind](#agentkind) | | Yes | @@ -11989,6 +11991,7 @@ Visibility and lifecycle scope of an Agent record. | provider_credential_id | string | | No | | ref | string | | No | | type | string | | No | +| value | string | | No | | variable | string | | No | #### AgentSensitiveWordAvoidanceFeatureConfig @@ -12515,6 +12518,7 @@ Enum class for api provider schema type. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | access_mode | string | | No | +| active_config_is_published | boolean | | No | | api_base_url | string | | No | | app_id | string | | No | | bound_agent_id | string | | No | @@ -12638,6 +12642,7 @@ AppMCPServer Status Enum | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | access_mode | string | | No | +| active_config_is_published | boolean | | No | | app_id | string | | No | | author_name | string | | No | | bound_agent_id | string | | No | @@ -14201,6 +14206,7 @@ about. Stage 4 §4.2. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | +| children | [ { **"array_item"**: { **"children"**: [ object ], **"description"**: , **"type"**: string,
**Available values:** "array", "boolean", "file", "number", "object", "string" }, **"children"**: [ object ], **"description"**: , **"file"**: object, **"name"**: string, **"required"**: boolean, **"type"**: string,
**Available values:** "array", "boolean", "file", "number", "object", "string" } ] | | No | | description | string | | No | | type | [DeclaredOutputType](#declaredoutputtype) | | Yes | @@ -14229,6 +14235,7 @@ code can call ``output.failure_strategy.on_failure`` without None-guards. | ---- | ---- | ----------- | -------- | | array_item | [DeclaredArrayItem](#declaredarrayitem) | | No | | check | [DeclaredOutputCheckConfig](#declaredoutputcheckconfig) | | No | +| children | [ { **"array_item"**: { **"children"**: [ object ], **"description"**: , **"type"**: string,
**Available values:** "array", "boolean", "file", "number", "object", "string" }, **"children"**: [ object ], **"description"**: , **"file"**: object, **"name"**: string, **"required"**: boolean, **"type"**: string,
**Available values:** "array", "boolean", "file", "number", "object", "string" } ] | | No | | description | string | | No | | failure_strategy | [DeclaredOutputFailureStrategy](#declaredoutputfailurestrategy) | | No | | file | [DeclaredOutputFileConfig](#declaredoutputfileconfig) | | No | diff --git a/api/services/agent/roster_service.py b/api/services/agent/roster_service.py index 21641b2965..69a2306cc8 100644 --- a/api/services/agent/roster_service.py +++ b/api/services/agent/roster_service.py @@ -1,6 +1,6 @@ from typing import Any, TypedDict -from sqlalchemy import func, select +from sqlalchemy import and_, func, or_, select from sqlalchemy.exc import IntegrityError from libs.datetime_utils import naive_utc_now @@ -56,6 +56,7 @@ class AgentRosterService: agent: Agent, active_version: AgentConfigSnapshot | None = None, published_references: list[AgentReferencingWorkflow] | None = None, + active_config_is_published: bool = False, ) -> dict[str, Any]: published_references = published_references or [] return { @@ -74,6 +75,7 @@ class AgentRosterService: "workflow_node_id": agent.workflow_node_id, "active_config_snapshot_id": agent.active_config_snapshot_id, "active_config_snapshot": AgentRosterService.serialize_version(active_version) if active_version else None, + "active_config_is_published": active_config_is_published, "status": agent.status.value, "created_by": agent.created_by, "updated_by": agent.updated_by, @@ -128,6 +130,10 @@ class AgentRosterService: tenant_id=tenant_id, agent_ids=[agent.id for agent in agents], ) + active_config_is_published_by_agent_id = self.load_active_config_is_published_by_agent_id( + tenant_id=tenant_id, + agents=agents, + ) data = [] for agent in agents: @@ -139,6 +145,7 @@ class AgentRosterService: agent, active_version, published_references_by_agent_id.get(agent.id, []), + active_config_is_published_by_agent_id.get(agent.id, False), ) ) @@ -165,11 +172,16 @@ class AgentRosterService: tenant_id=tenant_id, agent_ids=[agent.id for agent in agents], ) + active_config_is_published_by_agent_id = self.load_active_config_is_published_by_agent_id( + tenant_id=tenant_id, + agents=agents, + ) data = [ self.serialize_agent( agent, versions_by_id.get(agent.active_config_snapshot_id) if agent.active_config_snapshot_id else None, published_references_by_agent_id.get(agent.id, []), + active_config_is_published_by_agent_id.get(agent.id, False), ) for agent in agents ] @@ -429,7 +441,16 @@ class AgentRosterService: tenant_id=tenant_id, agent_ids=[agent.id], ) - return self.serialize_agent(agent, active_version, published_references_by_agent_id.get(agent.id, [])) + active_config_is_published_by_agent_id = self.load_active_config_is_published_by_agent_id( + tenant_id=tenant_id, + agents=[agent], + ) + return self.serialize_agent( + agent, + active_version, + published_references_by_agent_id.get(agent.id, []), + active_config_is_published_by_agent_id.get(agent.id, False), + ) def update_roster_agent( self, *, tenant_id: str, agent_id: str, account_id: str, payload: RosterAgentUpdatePayload @@ -471,6 +492,18 @@ class AgentRosterService: AgentConfigRevisionOperation.SAVE_TO_ROSTER, } + def active_config_is_published(self, *, tenant_id: str, agent: Agent) -> bool: + """Return whether the Agent's current active snapshot is a visible published version.""" + return self.load_active_config_is_published_by_agent_id(tenant_id=tenant_id, agents=[agent]).get( + agent.id, + False, + ) + + def load_active_config_is_published_by_agent_id(self, *, tenant_id: str, agents: list[Agent]) -> dict[str, bool]: + """Return publish-state flags for the active config snapshots of the given Agents.""" + published_agent_ids = self._load_published_active_snapshot_agent_ids(tenant_id=tenant_id, agents=agents) + return {agent.id: agent.id in published_agent_ids for agent in agents} + def list_agent_versions(self, *, tenant_id: str, agent_id: str) -> list[dict[str, Any]]: agent = self._get_agent(tenant_id=tenant_id, agent_id=agent_id, roster_only=True) visible_version_ids = ( @@ -568,6 +601,29 @@ class AgentRosterService: raise AgentVersionNotFoundError() return version + def _load_published_active_snapshot_agent_ids(self, *, tenant_id: str, agents: list[Agent]) -> set[str]: + predicates = [ + and_( + AgentConfigRevision.agent_id == agent.id, + AgentConfigRevision.current_snapshot_id == agent.active_config_snapshot_id, + AgentConfigRevision.operation.in_(self._visible_version_operations(agent)), + ) + for agent in agents + if agent.active_config_snapshot_id + ] + if not predicates: + return set() + + agent_ids = self._session.scalars( + select(AgentConfigRevision.agent_id) + .where( + AgentConfigRevision.tenant_id == tenant_id, + or_(*predicates), + ) + .distinct() + ).all() + return set(agent_ids) + def _load_published_references_by_agent_id( self, *, tenant_id: str, agent_ids: list[str] ) -> dict[str, list[AgentReferencingWorkflow]]: 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 d9bc08920b..91b644b1c7 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 @@ -199,12 +199,16 @@ def test_agent_app_list_and_create_use_agent_route( monkeypatch.setattr( roster_controller.AgentRosterService, "load_app_backing_agents_by_app_id", - lambda _self, **kwargs: {"app-list": SimpleNamespace(id="agent-list", role="List role")}, + lambda _self, **kwargs: { + "app-list": SimpleNamespace(id="agent-list", role="List role", active_config_snapshot_id=None) + }, ) monkeypatch.setattr( roster_controller.AgentRosterService, "get_app_backing_agent", - lambda _self, **kwargs: SimpleNamespace(id="agent-created", role="Created role"), + lambda _self, **kwargs: SimpleNamespace( + id="agent-created", role="Created role", active_config_snapshot_id=None + ), ) monkeypatch.setattr( roster_controller.FeatureService, @@ -221,6 +225,7 @@ def test_agent_app_list_and_create_use_agent_route( assert listed["data"][0]["id"] == "agent-list" assert listed["data"][0]["app_id"] == "app-list" assert listed["data"][0]["role"] == "List role" + assert listed["data"][0]["active_config_is_published"] is False assert "bound_agent_id" not in listed["data"][0] list_call = cast(dict[str, object], captured["list"]) list_params = cast(Any, list_call["params"]) @@ -237,6 +242,7 @@ def test_agent_app_list_and_create_use_agent_route( assert created["id"] == "agent-created" assert created["app_id"] == "app-created" assert created["role"] == "Created role" + assert created["active_config_is_published"] is False assert "bound_agent_id" not in created create_call = cast(dict[str, object], captured["create"]) create_params = cast(Any, create_call["params"]) @@ -258,7 +264,7 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id( monkeypatch.setattr( roster_controller.AgentRosterService, "get_app_backing_agent", - lambda _self, **kwargs: SimpleNamespace(id=agent_id, role="Resolved role"), + lambda _self, **kwargs: SimpleNamespace(id=agent_id, role="Resolved role", active_config_snapshot_id=None), ) monkeypatch.setattr( roster_controller.FeatureService, @@ -284,6 +290,7 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id( assert detail["id"] == agent_id assert detail["app_id"] == "app-1" assert detail["role"] == "Resolved role" + assert detail["active_config_is_published"] is False assert "bound_agent_id" not in detail with app.test_request_context( @@ -296,6 +303,7 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id( assert updated["id"] == agent_id assert updated["app_id"] == "app-1" assert updated["role"] == "Resolved role" + assert updated["active_config_is_published"] is False assert "bound_agent_id" not in updated update_call = cast(dict[str, object], captured["update"]) assert update_call["app"] is app_model diff --git a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py index 3cd77f7f2e..f402f851b8 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py @@ -21,6 +21,9 @@ from models.agent import Agent, AgentConfigSnapshot, WorkflowAgentNodeBinding from models.agent_config_entities import ( AgentSoulConfig, AgentSoulModelConfig, + DeclaredArrayItem, + DeclaredOutputChildConfig, + DeclaredOutputConfig, DeclaredOutputType, WorkflowNodeJobConfig, ) @@ -321,6 +324,7 @@ def test_build_shell_layer_config_accepts_legacy_fallback_keys(): "secret_refs": [ {"variable": "TOKEN", "credential_id": "credential-1"}, {"name": "API_KEY", "provider_credential_id": "credential-2"}, + {"name": "EDITABLE_TOKEN", "value": "credential-3"}, {"ref": "missing-name"}, ], }, @@ -341,6 +345,7 @@ def test_build_shell_layer_config_accepts_legacy_fallback_keys(): assert config["secret_refs"] == [ {"name": "TOKEN", "ref": "credential-1"}, {"name": "API_KEY", "ref": "credential-2"}, + {"name": "EDITABLE_TOKEN", "ref": "credential-3"}, ] assert config["sandbox"] is None @@ -630,6 +635,40 @@ def test_array_output_emits_typed_items_per_array_item(): assert output_schema["required"] == ["tags"] +def test_nested_declared_output_emits_object_and_array_child_schema(): + profile_output = DeclaredOutputConfig( + name="profile", + type=DeclaredOutputType.OBJECT, + children=[ + DeclaredOutputChildConfig(name="email", type=DeclaredOutputType.STRING), + DeclaredOutputChildConfig( + name="nickname", + type=DeclaredOutputType.STRING, + required=False, + description="Optional display name", + ), + DeclaredOutputChildConfig( + name="addresses", + type=DeclaredOutputType.ARRAY, + array_item=DeclaredArrayItem( + type=DeclaredOutputType.OBJECT, + description="Address item", + children=[DeclaredOutputChildConfig(name="city", type=DeclaredOutputType.STRING)], + ), + ), + ], + ) + + schema = WorkflowAgentRuntimeRequestBuilder._schema_for_declared_output(profile_output) + + assert schema["properties"]["email"] == {"type": "string"} + assert schema["properties"]["nickname"] == {"type": "string", "description": "Optional display name"} + assert schema["properties"]["addresses"]["items"]["properties"]["city"] == {"type": "string"} + assert schema["properties"]["addresses"]["items"]["description"] == "Address item" + assert schema["properties"]["addresses"]["items"]["required"] == ["city"] + assert schema["required"] == ["email", "addresses"] + + def test_effective_declared_outputs_passthrough_when_user_declared(): """effective_declared_outputs() must return user-provided outputs verbatim when non-empty; only empty input gets PRD defaults injected.""" diff --git a/api/tests/unit_tests/models/test_agent_config_entities.py b/api/tests/unit_tests/models/test_agent_config_entities.py index 51e51fb6d4..5538a1981d 100644 --- a/api/tests/unit_tests/models/test_agent_config_entities.py +++ b/api/tests/unit_tests/models/test_agent_config_entities.py @@ -1,7 +1,12 @@ import pytest from core.workflow.file_reference import build_file_reference -from models.agent_config_entities import DeclaredOutputConfig, DeclaredOutputType +from models.agent_config_entities import ( + DeclaredArrayItem, + DeclaredOutputChildConfig, + DeclaredOutputConfig, + DeclaredOutputType, +) def test_file_default_value_accepts_canonical_reference_mapping() -> None: @@ -92,3 +97,90 @@ def test_array_file_default_value_rejects_legacy_item_shape() -> None: }, } ) + + +def test_declared_array_item_rejects_nested_arrays_and_non_object_children() -> None: + with pytest.raises(ValueError, match="nested arrays"): + DeclaredArrayItem(type=DeclaredOutputType.ARRAY) + + with pytest.raises(ValueError, match="array_item.children"): + DeclaredArrayItem( + type=DeclaredOutputType.STRING, + children=[DeclaredOutputChildConfig(name="label", type=DeclaredOutputType.STRING)], + ) + + +def test_declared_output_child_validates_shape_and_defaults() -> None: + file_child = DeclaredOutputChildConfig(name="report", type=DeclaredOutputType.FILE) + assert file_child.file is not None + + array_child = DeclaredOutputChildConfig(name="items", type=DeclaredOutputType.ARRAY) + assert array_child.array_item is not None + assert array_child.array_item.type == DeclaredOutputType.OBJECT + + with pytest.raises(ValueError, match="output child name"): + DeclaredOutputChildConfig(name="bad-name", type=DeclaredOutputType.STRING) + + with pytest.raises(ValueError, match="file metadata"): + DeclaredOutputChildConfig(name="title", type=DeclaredOutputType.STRING, file={}) + + with pytest.raises(ValueError, match="array_item is only allowed"): + DeclaredOutputChildConfig( + name="title", + type=DeclaredOutputType.STRING, + array_item={"type": DeclaredOutputType.STRING}, + ) + + with pytest.raises(ValueError, match="children is only allowed"): + DeclaredOutputChildConfig( + name="title", + type=DeclaredOutputType.STRING, + children=[DeclaredOutputChildConfig(name="label", type=DeclaredOutputType.STRING)], + ) + + +def test_declared_output_validates_shape_and_defaults() -> None: + file_output = DeclaredOutputConfig(name="report", type=DeclaredOutputType.FILE) + assert file_output.file is not None + + array_output = DeclaredOutputConfig(name="items", type=DeclaredOutputType.ARRAY) + assert array_output.array_item is not None + assert array_output.array_item.type == DeclaredOutputType.OBJECT + + default_failure_strategy = DeclaredOutputConfig.model_validate( + {"name": "summary", "type": "string", "failure_strategy": None} + ) + assert default_failure_strategy.failure_strategy.on_failure == "stop" + + with pytest.raises(ValueError, match="output name"): + DeclaredOutputConfig(name="bad-name", type=DeclaredOutputType.STRING) + + with pytest.raises(ValueError, match="file metadata"): + DeclaredOutputConfig(name="summary", type=DeclaredOutputType.STRING, file={}) + + with pytest.raises(ValueError, match="array_item is only allowed"): + DeclaredOutputConfig( + name="summary", + type=DeclaredOutputType.STRING, + array_item={"type": DeclaredOutputType.STRING}, + ) + + with pytest.raises(ValueError, match="children is only allowed"): + DeclaredOutputConfig( + name="summary", + type=DeclaredOutputType.STRING, + children=[DeclaredOutputChildConfig(name="title", type=DeclaredOutputType.STRING)], + ) + + with pytest.raises(ValueError, match="output check is only allowed"): + DeclaredOutputConfig.model_validate( + { + "name": "summary", + "type": "string", + "check": { + "enabled": True, + "prompt": "Compare output", + "benchmark_file_ref": {"name": "expected.pdf"}, + }, + } + ) 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 bd2d2899e2..5d6ba1d0c9 100644 --- a/api/tests/unit_tests/services/agent/test_agent_services.py +++ b/api/tests/unit_tests/services/agent/test_agent_services.py @@ -17,6 +17,8 @@ from models.agent import ( ) from models.agent_config_entities import ( AgentFileRefConfig, + DeclaredArrayItem, + DeclaredOutputChildConfig, DeclaredOutputConfig, DeclaredOutputType, WorkflowNodeJobConfig, @@ -112,7 +114,7 @@ def test_load_workflow_composer_returns_empty_state(monkeypatch): effective = result["effective_declared_outputs"] assert [o["name"] for o in effective] == ["text", "files", "json"] files_output = next(o for o in effective if o["name"] == "files") - assert files_output["array_item"] == {"type": "file", "description": None} + assert files_output["array_item"] == {"type": "file", "description": None, "children": []} def test_load_workflow_composer_serializes_existing_binding(monkeypatch): @@ -649,6 +651,7 @@ def test_roster_list_and_invite_options(monkeypatch): lambda version_ids: {"version-1": version, "version-2": unconfigured_version}, ) monkeypatch.setattr(service, "_load_published_references_by_agent_id", lambda **kwargs: {}) + monkeypatch.setattr(service, "_load_published_active_snapshot_agent_ids", lambda **kwargs: {"agent-1"}) listed = service.list_roster_agents(tenant_id="tenant-1", page=1, limit=20) invited = service.list_invite_options(tenant_id="tenant-1", page=1, limit=20, app_id="app-1") @@ -661,6 +664,8 @@ def test_roster_list_and_invite_options(monkeypatch): assert listed["data"][0]["created_at"] == int(created_at.timestamp()) assert listed["data"][0]["updated_at"] == int(updated_at.timestamp()) assert listed["data"][0]["active_config_snapshot"]["created_at"] == int(version_created_at.timestamp()) + assert listed["data"][0]["active_config_is_published"] is True + assert listed["data"][1]["active_config_is_published"] is False assert invited["data"][0]["is_in_current_workflow"] is True assert invited["data"][0]["existing_node_ids"] == ["node-1"] @@ -690,6 +695,7 @@ def test_invite_options_uses_db_filtered_pagination(monkeypatch): }, ) monkeypatch.setattr(service, "_load_published_references_by_agent_id", lambda **kwargs: {}) + monkeypatch.setattr(service, "_load_published_active_snapshot_agent_ids", lambda **kwargs: set()) result = service.list_invite_options(tenant_id="tenant-1", page=1, limit=1) @@ -698,6 +704,41 @@ def test_invite_options_uses_db_filtered_pagination(monkeypatch): assert [item["id"] for item in result["data"]] == ["agent-2"] +def test_active_config_is_published_flags_handle_matching_and_empty_snapshots(): + agent = Agent( + id="agent-1", + tenant_id="tenant-1", + name="Published", + description="", + agent_kind=AgentKind.DIFY_AGENT, + scope=AgentScope.ROSTER, + source=AgentSource.AGENT_APP, + status=AgentStatus.ACTIVE, + active_config_snapshot_id="version-1", + ) + draft_agent = Agent( + id="agent-2", + tenant_id="tenant-1", + name="Draft", + description="", + agent_kind=AgentKind.DIFY_AGENT, + scope=AgentScope.ROSTER, + source=AgentSource.AGENT_APP, + status=AgentStatus.ACTIVE, + active_config_snapshot_id=None, + ) + service = AgentRosterService(FakeSession(scalars=[["agent-1"], ["agent-1"]])) + + flags = service.load_active_config_is_published_by_agent_id(tenant_id="tenant-1", agents=[agent, draft_agent]) + + assert flags == {"agent-1": True, "agent-2": False} + assert service.active_config_is_published(tenant_id="tenant-1", agent=agent) is True + assert AgentRosterService(FakeSession()).load_active_config_is_published_by_agent_id( + tenant_id="tenant-1", + agents=[draft_agent], + ) == {"agent-2": False} + + def test_published_references_include_app_display_fields_and_sort_by_updated_at(): recent_updated_at = datetime(2026, 1, 7, 3, 4, 5, tzinfo=UTC) stale_updated_at = datetime(2026, 1, 6, 3, 4, 5, tzinfo=UTC) @@ -1042,7 +1083,7 @@ def test_composer_validator_accepts_valid_shell_env_and_cli(): { "env": { "variables": [{"name": "MY_VAR", "value": "v"}], - "secret_refs": [{"name": "API_TOKEN", "id": "credential-1"}], + "secret_refs": [{"name": "API_TOKEN", "value": "credential-1"}], }, "tools": { "cli_tools": [ @@ -1051,7 +1092,7 @@ def test_composer_validator_accepts_valid_shell_env_and_cli(): "command": "apt-get install -y jq", "env": { "variables": [{"name": "JQ_COLOR", "value": "1"}], - "secret_refs": [{"name": "JQ_TOKEN", "id": "credential-2"}], + "secret_refs": [{"name": "JQ_TOKEN", "value": "credential-2"}], }, }, { @@ -1067,8 +1108,10 @@ def test_composer_validator_accepts_valid_shell_env_and_cli(): ) assert {variable.name for variable in config.env.variables} == {"MY_VAR"} assert {secret.name for secret in config.env.secret_refs} == {"API_TOKEN"} + assert config.env.secret_refs[0].value == "credential-1" assert config.tools.cli_tools[0].env.variables[0].name == "JQ_COLOR" assert config.tools.cli_tools[0].env.secret_refs[0].name == "JQ_TOKEN" + assert config.tools.cli_tools[0].env.secret_refs[0].value == "credential-2" class TestAgentAppBackingAgent: @@ -1263,7 +1306,22 @@ class TestWorkflowAgentDraftBindingSync: node_job_config=WorkflowNodeJobConfig( workflow_prompt="Summarize the upstream result.", declared_outputs=[ - DeclaredOutputConfig(name="summary", type=DeclaredOutputType.STRING, description="Short summary") + DeclaredOutputConfig(name="summary", type=DeclaredOutputType.STRING, description="Short summary"), + DeclaredOutputConfig( + name="profile", + type=DeclaredOutputType.OBJECT, + children=[ + DeclaredOutputChildConfig(name="email", type=DeclaredOutputType.STRING), + DeclaredOutputChildConfig( + name="addresses", + type=DeclaredOutputType.ARRAY, + array_item=DeclaredArrayItem( + type=DeclaredOutputType.OBJECT, + children=[DeclaredOutputChildConfig(name="city", type=DeclaredOutputType.STRING)], + ), + ), + ], + ), ], ), ) @@ -1279,6 +1337,9 @@ class TestWorkflowAgentDraftBindingSync: assert node_data["agent_declared_outputs"][0]["name"] == "summary" assert node_data["agent_declared_outputs"][0]["type"] == "string" assert node_data["agent_declared_outputs"][0]["description"] == "Short summary" + profile_output = node_data["agent_declared_outputs"][1] + assert profile_output["children"][0]["name"] == "email" + assert profile_output["children"][1]["array_item"]["children"][0]["name"] == "city" assert "agent_declared_outputs" not in workflow.graph_dict["nodes"][0]["data"] def test_creates_roster_binding_from_agent_node_graph(self): diff --git a/packages/contracts/generated/api/console/agent/types.gen.ts b/packages/contracts/generated/api/console/agent/types.gen.ts index a177953170..2f7d2ee0af 100644 --- a/packages/contracts/generated/api/console/agent/types.gen.ts +++ b/packages/contracts/generated/api/console/agent/types.gen.ts @@ -23,6 +23,7 @@ export type AgentAppCreatePayload = { export type AppDetailWithSite = { access_mode?: string | null + active_config_is_published?: boolean api_base_url?: string | null app_id?: string | null bound_agent_id?: string | null @@ -259,6 +260,7 @@ export type AgentConfigSnapshotDetailResponse = { export type AppPartial = { access_mode?: string | null + active_config_is_published?: boolean app_id?: string | null author_name?: string | null bound_agent_id?: string | null @@ -345,6 +347,7 @@ export type WorkflowPartial = { } export type AgentInviteOptionResponse = { + active_config_is_published?: boolean active_config_snapshot?: AgentConfigSnapshotSummaryResponse | null active_config_snapshot_id?: string | null agent_kind: AgentKind @@ -757,6 +760,26 @@ export type AgentSoulToolsConfig = { export type DeclaredOutputConfig = { array_item?: DeclaredArrayItem | null check?: DeclaredOutputCheckConfig | null + children?: Array<{ + array_item?: { + children?: Array<{ + [key: string]: unknown + }> + description?: string | null + type?: 'array' | 'boolean' | 'file' | 'number' | 'object' | 'string' + [key: string]: unknown + } + children?: Array<{ + [key: string]: unknown + }> + description?: string | null + file?: { + [key: string]: unknown + } + name: string + required?: boolean + type: 'array' | 'boolean' | 'file' | 'number' | 'object' | 'string' + }> description?: string | null failure_strategy?: DeclaredOutputFailureStrategy file?: DeclaredOutputFileConfig | null @@ -949,6 +972,7 @@ export type AgentSecretRefConfig = { provider_credential_id?: string | null ref?: string | null type?: string | null + value?: string | null variable?: string | null [key: string]: unknown } @@ -1073,6 +1097,26 @@ export type AgentSoulDifyToolConfig = { } export type DeclaredArrayItem = { + children?: Array<{ + array_item?: { + children?: Array<{ + [key: string]: unknown + }> + description?: string | null + type?: 'array' | 'boolean' | 'file' | 'number' | 'object' | 'string' + [key: string]: unknown + } + children?: Array<{ + [key: string]: unknown + }> + description?: string | null + file?: { + [key: string]: unknown + } + name: string + required?: boolean + type: 'array' | 'boolean' | 'file' | 'number' | 'object' | 'string' + }> description?: string | null type: DeclaredOutputType } @@ -1224,6 +1268,7 @@ export type AppPaginationWritable = { export type AppDetailWithSiteWritable = { access_mode?: string | null + active_config_is_published?: boolean api_base_url?: string | null app_id?: string | null bound_agent_id?: string | null @@ -1253,6 +1298,7 @@ export type AppDetailWithSiteWritable = { export type AppPartialWritable = { access_mode?: string | null + active_config_is_published?: boolean app_id?: string | null author_name?: string | null bound_agent_id?: 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 41e16de3b3..2e1ffadc4b 100644 --- a/packages/contracts/generated/api/console/agent/zod.gen.ts +++ b/packages/contracts/generated/api/console/agent/zod.gen.ts @@ -475,6 +475,7 @@ export const zModelConfigPartial = z.object({ */ export const zAppPartial = z.object({ access_mode: z.string().nullish(), + active_config_is_published: z.boolean().optional().default(false), app_id: z.string().nullish(), author_name: z.string().nullish(), bound_agent_id: z.string().nullish(), @@ -534,6 +535,7 @@ export const zModelConfig = z.object({ */ export const zAppDetailWithSite = z.object({ access_mode: z.string().nullish(), + active_config_is_published: z.boolean().optional().default(false), api_base_url: z.string().nullish(), app_id: z.string().nullish(), bound_agent_id: z.string().nullish(), @@ -620,6 +622,7 @@ export const zAgentStatus = z.enum(['active', 'archived']) * AgentInviteOptionResponse */ export const zAgentInviteOptionResponse = z.object({ + active_config_is_published: z.boolean().optional().default(false), active_config_snapshot: zAgentConfigSnapshotSummaryResponse.nullish(), active_config_snapshot_id: z.string().nullish(), agent_kind: zAgentKind, @@ -1078,6 +1081,25 @@ export const zWorkflowNodeJobMetadata = z.object({ * about. Stage 4 §4.2. */ export const zDeclaredArrayItem = z.object({ + children: z + .array( + z.object({ + array_item: z + .object({ + children: z.array(z.record(z.string(), z.unknown())).optional(), + description: z.string().nullish(), + type: z.enum(['array', 'boolean', 'file', 'number', 'object', 'string']).optional(), + }) + .optional(), + children: z.array(z.record(z.string(), z.unknown())).optional(), + description: z.string().nullish(), + file: z.record(z.string(), z.unknown()).optional(), + name: z.string(), + required: z.boolean().optional(), + type: z.enum(['array', 'boolean', 'file', 'number', 'object', 'string']), + }), + ) + .optional(), description: z.string().nullish(), type: zDeclaredOutputType, }) @@ -1136,6 +1158,7 @@ export const zAgentSecretRefConfig = z.object({ provider_credential_id: z.string().max(255).nullish(), ref: z.string().max(255).nullish(), type: z.string().max(64).nullish(), + value: z.string().max(255).nullish(), variable: z.string().max(255).nullish(), }) @@ -1515,6 +1538,25 @@ export const zDeclaredOutputFailureStrategy = z.object({ export const zDeclaredOutputConfig = z.object({ array_item: zDeclaredArrayItem.nullish(), check: zDeclaredOutputCheckConfig.nullish(), + children: z + .array( + z.object({ + array_item: z + .object({ + children: z.array(z.record(z.string(), z.unknown())).optional(), + description: z.string().nullish(), + type: z.enum(['array', 'boolean', 'file', 'number', 'object', 'string']).optional(), + }) + .optional(), + children: z.array(z.record(z.string(), z.unknown())).optional(), + description: z.string().nullish(), + file: z.record(z.string(), z.unknown()).optional(), + name: z.string(), + required: z.boolean().optional(), + type: z.enum(['array', 'boolean', 'file', 'number', 'object', 'string']), + }), + ) + .optional(), description: z.string().nullish(), failure_strategy: zDeclaredOutputFailureStrategy.optional(), file: zDeclaredOutputFileConfig.nullish(), @@ -1735,6 +1777,7 @@ export const zMessageInfiniteScrollPaginationResponse = z.object({ */ export const zAppPartialWritable = z.object({ access_mode: z.string().nullish(), + active_config_is_published: z.boolean().optional().default(false), app_id: z.string().nullish(), author_name: z.string().nullish(), bound_agent_id: z.string().nullish(), @@ -1795,6 +1838,7 @@ export const zSiteWritable = z.object({ */ export const zAppDetailWithSiteWritable = z.object({ access_mode: z.string().nullish(), + active_config_is_published: z.boolean().optional().default(false), api_base_url: z.string().nullish(), app_id: z.string().nullish(), bound_agent_id: 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 81842742f5..0c7633ba7e 100644 --- a/packages/contracts/generated/api/console/apps/types.gen.ts +++ b/packages/contracts/generated/api/console/apps/types.gen.ts @@ -23,6 +23,7 @@ export type CreateAppPayload = { export type AppDetailWithSite = { access_mode?: string | null + active_config_is_published?: boolean api_base_url?: string | null app_id?: string | null bound_agent_id?: string | null @@ -1155,6 +1156,7 @@ export type ApiKeyItem = { export type AppPartial = { access_mode?: string | null + active_config_is_published?: boolean app_id?: string | null author_name?: string | null bound_agent_id?: string | null @@ -1760,6 +1762,26 @@ export type AgentComposerBindingResponse = { export type DeclaredOutputConfig = { array_item?: DeclaredArrayItem | null check?: DeclaredOutputCheckConfig | null + children?: Array<{ + array_item?: { + children?: Array<{ + [key: string]: unknown + }> + description?: string | null + type?: 'array' | 'boolean' | 'file' | 'number' | 'object' | 'string' + [key: string]: unknown + } + children?: Array<{ + [key: string]: unknown + }> + description?: string | null + file?: { + [key: string]: unknown + } + name: string + required?: boolean + type: 'array' | 'boolean' | 'file' | 'number' | 'object' | 'string' + }> description?: string | null failure_strategy?: DeclaredOutputFailureStrategy file?: DeclaredOutputFileConfig | null @@ -2103,6 +2125,26 @@ export type AgentSoulToolsConfig = { export type WorkflowAgentBindingType = 'inline_agent' | 'roster_agent' export type DeclaredArrayItem = { + children?: Array<{ + array_item?: { + children?: Array<{ + [key: string]: unknown + }> + description?: string | null + type?: 'array' | 'boolean' | 'file' | 'number' | 'object' | 'string' + [key: string]: unknown + } + children?: Array<{ + [key: string]: unknown + }> + description?: string | null + file?: { + [key: string]: unknown + } + name: string + required?: boolean + type: 'array' | 'boolean' | 'file' | 'number' | 'object' | 'string' + }> description?: string | null type: DeclaredOutputType } @@ -2305,6 +2347,7 @@ export type AgentSecretRefConfig = { provider_credential_id?: string | null ref?: string | null type?: string | null + value?: string | null variable?: string | null [key: string]: unknown } @@ -2543,6 +2586,7 @@ export type AppPaginationWritable = { export type AppDetailWithSiteWritable = { access_mode?: string | null + active_config_is_published?: boolean api_base_url?: string | null app_id?: string | null bound_agent_id?: string | null @@ -2595,6 +2639,7 @@ export type WorkflowCommentDetailWritable = { export type AppPartialWritable = { access_mode?: string | null + active_config_is_published?: boolean app_id?: string | null author_name?: string | null bound_agent_id?: string | null diff --git a/packages/contracts/generated/api/console/apps/zod.gen.ts b/packages/contracts/generated/api/console/apps/zod.gen.ts index 94e4a8a3aa..475823246b 100644 --- a/packages/contracts/generated/api/console/apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/apps/zod.gen.ts @@ -1946,6 +1946,7 @@ export const zModelConfigPartial = z.object({ */ export const zAppPartial = z.object({ access_mode: z.string().nullish(), + active_config_is_published: z.boolean().optional().default(false), app_id: z.string().nullish(), author_name: z.string().nullish(), bound_agent_id: z.string().nullish(), @@ -2005,6 +2006,7 @@ export const zModelConfig = z.object({ */ export const zAppDetailWithSite = z.object({ access_mode: z.string().nullish(), + active_config_is_published: z.boolean().optional().default(false), api_base_url: z.string().nullish(), app_id: z.string().nullish(), bound_agent_id: z.string().nullish(), @@ -2475,6 +2477,25 @@ export const zAgentComposerBindingResponse = z.object({ * about. Stage 4 §4.2. */ export const zDeclaredArrayItem = z.object({ + children: z + .array( + z.object({ + array_item: z + .object({ + children: z.array(z.record(z.string(), z.unknown())).optional(), + description: z.string().nullish(), + type: z.enum(['array', 'boolean', 'file', 'number', 'object', 'string']).optional(), + }) + .optional(), + children: z.array(z.record(z.string(), z.unknown())).optional(), + description: z.string().nullish(), + file: z.record(z.string(), z.unknown()).optional(), + name: z.string(), + required: z.boolean().optional(), + type: z.enum(['array', 'boolean', 'file', 'number', 'object', 'string']), + }), + ) + .optional(), description: z.string().nullish(), type: zDeclaredOutputType, }) @@ -2908,6 +2929,7 @@ export const zAgentSecretRefConfig = z.object({ provider_credential_id: z.string().max(255).nullish(), ref: z.string().max(255).nullish(), type: z.string().max(64).nullish(), + value: z.string().max(255).nullish(), variable: z.string().max(255).nullish(), }) @@ -3079,6 +3101,25 @@ export const zDeclaredOutputCheckConfig = z.object({ export const zDeclaredOutputConfig = z.object({ array_item: zDeclaredArrayItem.nullish(), check: zDeclaredOutputCheckConfig.nullish(), + children: z + .array( + z.object({ + array_item: z + .object({ + children: z.array(z.record(z.string(), z.unknown())).optional(), + description: z.string().nullish(), + type: z.enum(['array', 'boolean', 'file', 'number', 'object', 'string']).optional(), + }) + .optional(), + children: z.array(z.record(z.string(), z.unknown())).optional(), + description: z.string().nullish(), + file: z.record(z.string(), z.unknown()).optional(), + name: z.string(), + required: z.boolean().optional(), + type: z.enum(['array', 'boolean', 'file', 'number', 'object', 'string']), + }), + ) + .optional(), description: z.string().nullish(), failure_strategy: zDeclaredOutputFailureStrategy.optional(), file: zDeclaredOutputFileConfig.nullish(), @@ -3437,6 +3478,7 @@ export const zGeneratedAppResponseWritable = zJsonValue */ export const zAppPartialWritable = z.object({ access_mode: z.string().nullish(), + active_config_is_published: z.boolean().optional().default(false), app_id: z.string().nullish(), author_name: z.string().nullish(), bound_agent_id: z.string().nullish(), @@ -3497,6 +3539,7 @@ export const zSiteWritable = z.object({ */ export const zAppDetailWithSiteWritable = z.object({ access_mode: z.string().nullish(), + active_config_is_published: z.boolean().optional().default(false), api_base_url: z.string().nullish(), app_id: z.string().nullish(), bound_agent_id: z.string().nullish(),