mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-19 19:16:17 +00:00
* feat(gateway): accept conversation references in run context and report the capability LangGraph SDK clients build a fixed run body and drop unknown top-level fields, so they cannot send the conversation_references field from #5399. RunCreateRequest now lifts context.conversation_references into the top-level field before validation, so it keeps the same bounds and error locations, and drops it from context, so it never reaches the merged run context or the checkpointed configurable. Sending both is a 422. GET /api/features reports conversation_references {enabled, max_references} with the same "tool is configured" predicate as run admission, so a client can hide an entry point on deployments without the tool. Related to #5398. * fix(gateway): report the field type error for a malformed top-level reference list A malformed top-level conversation_references sent alongside a context list now fails with the field's own type error instead of the conflict message. The tool-configured predicate reads tool.use directly, and the features test doubles carry that attribute like every real ToolConfig. * fix(gateway): treat every list-like top-level reference value as a conflict Pydantic's lax mode coerces tuples, sets, frozensets and deques into the list[str] field, so a direct Python caller passing one of those together with context.conversation_references now reports the conflict instead of slipping both grants through. Unreachable over HTTP, where JSON has no such types. * fix(gateway): ask pydantic whether a top-level reference value is list-like Enumerating list-like types cannot track pydantic's lax acceptance set (generators, UserList, dict key views also coerce into list[str]). The conflict guard now validates the top-level value with a TypeAdapter for list[Any]: whatever pydantic would coerce reports the conflict when it is non-empty, and whatever it rejects still surfaces the field's own type error. Regression tests cover deque, UserList, dict keys and a generator, plus rejected scalars. * fix(gateway): probe top-level references with the field's own annotation The conflict guard now validates the top-level value with the exact item annotation the field uses, so its acceptance set is the field's rather than a superset: an item the field rejects (an empty string, a non-string, the ints of a range or dict view) surfaces the field's own item error instead of a conflict. The annotation is shared through one alias so the two cannot drift. * fix(gateway): materialise a one-shot iterator before probing top-level references The item-validating probe could consume a generator while collecting an item error, after which the field re-validated the exhausted iterator, coerced it to [] and let the request through with the key still in context. Iterators are now read once into a list that both the probe and the field validate, so a bad item is reported at its index and a valid generator is kept. * fix(gateway): materialise every once-walkable iterable before probing references Pydantic coerces any iterable into the list field, and an object whose __iter__ hands out a generator once is not an Iterator instance, so the previous gate let it reach the probe and be consumed. The lift now reads every iterable except lists, tuples and the shapes the field rejects as a whole (str, bytes, dict) into a list first, so the probe and the field always validate the same items. --------- Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
101 lines
4.6 KiB
Python
101 lines
4.6 KiB
Python
"""Read-only feature-flag endpoint for the frontend bootstrap.
|
|
|
|
Reports which optional features are exposed over HTTP so the frontend can gate
|
|
UI and avoid firing requests that the backend would reject. Config-only flags
|
|
read through ``get_config`` so edits to ``config.yaml`` take effect on the next
|
|
request, while startup-scoped capabilities report the runtime that actually
|
|
started.
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, Request
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app.gateway.browser_capability import browser_capability
|
|
from app.gateway.conversation_access import conversation_references_enabled
|
|
from app.gateway.deps import get_config
|
|
from app.gateway.run_models import MAX_CONVERSATION_REFERENCES
|
|
from deerflow.config.app_config import AppConfig
|
|
from deerflow.subagents.capacity import configured_subagent_max_running
|
|
|
|
router = APIRouter(prefix="/api", tags=["features"])
|
|
|
|
|
|
class AgentsApiFeature(BaseModel):
|
|
"""Availability of the custom-agent management API."""
|
|
|
|
enabled: bool = Field(..., description="Whether the agents_api routes are exposed over HTTP")
|
|
|
|
|
|
class BrowserControlFeature(BaseModel):
|
|
"""Availability of live agentic browser control."""
|
|
|
|
enabled: bool = Field(..., description="Whether the live browser routes and UI are available")
|
|
|
|
|
|
class McpTasksFeature(BaseModel):
|
|
"""Availability of the durable MCP task runtime."""
|
|
|
|
enabled: bool = Field(..., description="Whether durable MCP task APIs and UI are available")
|
|
|
|
|
|
class SubagentBatchesFeature(BaseModel):
|
|
"""Persistence, worker, and process capacity for native-subagent batches."""
|
|
|
|
enabled: bool = Field(..., description="Compatibility alias for worker_running")
|
|
repository_available: bool = Field(..., description="Whether durable batch history APIs are available")
|
|
worker_running: bool = Field(..., description="Whether this Gateway process is executing durable batch work")
|
|
max_running: int = Field(..., description="Native subagent execution slots in this Gateway process")
|
|
|
|
|
|
class ConversationReferencesFeature(BaseModel):
|
|
"""Availability of explicit conversation references on run requests."""
|
|
|
|
enabled: bool = Field(..., description="Whether the opt-in read_conversation tool is configured, so run requests may carry conversation_references")
|
|
max_references: int = Field(..., description="Maximum conversation references accepted on one run request")
|
|
|
|
|
|
class FeaturesResponse(BaseModel):
|
|
"""Frontend-facing feature availability flags."""
|
|
|
|
agents_api: AgentsApiFeature
|
|
browser_control: BrowserControlFeature
|
|
mcp_tasks: McpTasksFeature
|
|
subagent_batches: SubagentBatchesFeature
|
|
conversation_references: ConversationReferencesFeature
|
|
|
|
|
|
@router.get(
|
|
"/features",
|
|
response_model=FeaturesResponse,
|
|
summary="List Feature Flags",
|
|
description="Report which optional features are available, so the frontend can gate UI before issuing requests.",
|
|
)
|
|
async def list_features(request: Request, config: AppConfig = Depends(get_config)) -> FeaturesResponse:
|
|
"""Return availability of optional frontend features."""
|
|
browser = browser_capability(config)
|
|
subagent_batch_worker_running = bool(getattr(request.app.state, "subagent_batches_available", False))
|
|
return FeaturesResponse(
|
|
agents_api=AgentsApiFeature(enabled=config.agents_api.enabled),
|
|
browser_control=BrowserControlFeature(enabled=browser.available),
|
|
# MCP task bindings and the submitter are startup-scoped. Report the
|
|
# capability that actually started rather than a hot-reloaded config
|
|
# value that would require a Gateway restart to take effect.
|
|
mcp_tasks=McpTasksFeature(enabled=bool(getattr(request.app.state, "mcp_tasks_available", False))),
|
|
subagent_batches=SubagentBatchesFeature(
|
|
# Keep the historical `enabled` field as a compatibility alias
|
|
# while exposing read persistence independently from execution.
|
|
# A stopped/disabled worker must not hide durable history/export.
|
|
enabled=subagent_batch_worker_running,
|
|
repository_available=getattr(request.app.state, "subagent_batch_repo", None) is not None,
|
|
worker_running=subagent_batch_worker_running,
|
|
max_running=configured_subagent_max_running(),
|
|
),
|
|
# Same predicate as run admission (``prepare_conversation_reader``), read
|
|
# through ``get_config`` so enabling the tool in config.yaml shows up
|
|
# without a restart. A UI with no entry point still needs no change here.
|
|
conversation_references=ConversationReferencesFeature(
|
|
enabled=conversation_references_enabled(config),
|
|
max_references=MAX_CONVERSATION_REFERENCES,
|
|
),
|
|
)
|