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