mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-08 03:30:34 +00:00
* feat(subagents): continue native batches and honor item dependencies * docs: link native batch follow-up changelog entries * test(migrations): pin isolated history-seed upgrade to 0050 * fix(subagents): preserve follow-up scope, blockers and fair scheduling * test(memory): freeze capacity evaluation time * fix(subagents): guard follow-up admission and retain run controls * fix(subagents): enforce runs:cancel on tool cancellation of follow-up batches * style(tests): format test_batch_followup_host with ruff * fix(subagents): drop the admission checkpoint check and keep SDK cancel compatible The checkpoint check inside run admission waited on the checkpoint saver's lock while holding the SQLite write lock, which could make another chat's checkpoint write fail. Remove it; dispatch still checks the conversation before admission. A follow-up waiting for an unanswered question now keeps its claim until the lease expires instead of rereading the conversation every poll. Cancel passes the caller's run authority only to submitters that take it, so existing SDK submitters keep the two-argument call. Document that retrying a prerequisite by hand does not requeue its blocked items, and list 0052 as the migration head. --------- Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com> Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
81 lines
3.3 KiB
Python
81 lines
3.3 KiB
Python
"""Server-owned origin of a run or a thread (``deerflow_origin``).
|
|
|
|
A run started by the server on a user's behalf (a schedule, an IM channel, a
|
|
GitHub agent, an extension, an MCP notification) carries
|
|
``metadata["deerflow_origin"] = {"kind": ..., "provider"?: ..., "namespace"?: ...}``.
|
|
Run admission derives ``runs.origin_kind`` from it, which drives the activity
|
|
feed and the per-user unread state. A thread the server creates for such a run
|
|
keeps the same marker in its metadata.
|
|
|
|
The value is never accepted from a browser, PAT or API client: the Gateway
|
|
drops client copies and only stamps what a server-side launcher set (see
|
|
``app.gateway.run_origin``). It is a different key from the message-level
|
|
``deerflow_scheduled_origin`` that marks the scheduled prompt message inside a
|
|
run; code must never read one for the other.
|
|
|
|
Pinned by ``contracts/thread_origin_contract.json``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from collections.abc import Mapping
|
|
|
|
DEERFLOW_ORIGIN_KEY = "deerflow_origin"
|
|
ORIGIN_KINDS: frozenset[str] = frozenset({"schedule", "im_channel", "github", "extension", "mcp_notification", "subagent_batch"})
|
|
|
|
_PROVIDER_RE = re.compile(r"[a-z0-9_-]{1,32}")
|
|
_MAX_NAMESPACE_CHARS = 96
|
|
_ALLOWED_KEYS = frozenset({"kind", "provider", "namespace"})
|
|
|
|
|
|
def admit_origin(value: object) -> dict[str, str] | None:
|
|
"""Return a clean copy of a well-formed origin, else ``None``.
|
|
|
|
Shape: a mapping with ``kind`` in :data:`ORIGIN_KINDS`, an optional
|
|
``provider`` (at most 32 chars of ``[a-z0-9_-]``) and an optional
|
|
``namespace`` (1-96 chars, no ``:``). Anything else, including extra keys,
|
|
is rejected as a whole rather than partially kept.
|
|
"""
|
|
if not isinstance(value, Mapping) or not set(value).issubset(_ALLOWED_KEYS):
|
|
return None
|
|
kind = value.get("kind")
|
|
if not isinstance(kind, str) or kind not in ORIGIN_KINDS:
|
|
return None
|
|
origin = {"kind": kind}
|
|
provider = value.get("provider")
|
|
if provider is not None:
|
|
if not isinstance(provider, str) or not _PROVIDER_RE.fullmatch(provider):
|
|
return None
|
|
origin["provider"] = provider
|
|
namespace = value.get("namespace")
|
|
if namespace is not None:
|
|
if not isinstance(namespace, str) or not namespace or len(namespace) > _MAX_NAMESPACE_CHARS or ":" in namespace:
|
|
return None
|
|
origin["namespace"] = namespace
|
|
return origin
|
|
|
|
|
|
def make_origin(kind: str, *, provider: str | None = None, namespace: str | None = None) -> dict[str, str]:
|
|
"""Build an origin for a server-side launcher; raises ``ValueError`` when malformed."""
|
|
value: dict[str, str] = {"kind": kind}
|
|
if provider is not None:
|
|
value["provider"] = provider
|
|
if namespace is not None:
|
|
value["namespace"] = namespace
|
|
origin = admit_origin(value)
|
|
if origin is None:
|
|
raise ValueError(f"invalid run origin: {value!r}")
|
|
return origin
|
|
|
|
|
|
def origin_kind_of(metadata: Mapping | None) -> str | None:
|
|
"""``metadata["deerflow_origin"]["kind"]`` when well-formed, else ``None`` (interactive or legacy)."""
|
|
if not isinstance(metadata, Mapping):
|
|
return None
|
|
origin = admit_origin(metadata.get(DEERFLOW_ORIGIN_KEY))
|
|
return origin["kind"] if origin is not None else None
|
|
|
|
|
|
__all__ = ["DEERFLOW_ORIGIN_KEY", "ORIGIN_KINDS", "admit_origin", "make_origin", "origin_kind_of"]
|