mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-25 14:06:18 +00:00
* docs(extensions): add an eleven-page extension developer manual
Add a harness/extensions/ section in English and Chinese documenting the
deerflow-extension-api 0.2.3 contract:
index when to write an extension vs a tool, MCP server or
skill; contribution kinds; loading; failure and
trust model; versioning
quick-start build, test, install and remove a working extension
runtime load sequence, API version rules, scopes and
ExtensionData, fail-open and cancellation, diagnostics
middleware placements and where each lands, scope, ordering,
observe-only wrap hooks, failure isolation
observers task lifecycle, system model calls, agent assembly
and fingerprint, context compaction
services-and-routes service lifecycle, router rejection rules, auth,
principals
run-evidence service and request-scoped readers (#5727), cursor
semantics, deletions, redaction, store differences
plugins experimental full-stack plugins, inline modules and
manifest-based browser assets (#5685)
operations extension manager CLI, sources, upgrade, rollback,
Docker, Helm, recovery
troubleshooting indexed by the exact log and error strings
reference every public name and the contract version history
Also fix stale descriptions: the contribution kinds listed in AGENTS.md
and deerflow/extensions/AGENTS.md (now eight, including plugins), the
run-evidence redaction claim (only auth_token is removed), and the plugin
mount return value in docs/full-stack-plugins.md ({ dispose }, not a
function). Changelog entries added in both languages.
* docs(agents): trim root AGENTS.md to keep inherited chains under the size limit
* docs(extensions): scope the manual to the 0.2.1 contract
Plugins and request-scoped run evidence are not in 2.1.x-dev yet; they
move to a stacked follow-up so this change can be cherry-picked.
* docs(extensions): cover full-stack plugins and request-scoped run evidence
Brings the extension manual up to deerflow-extension-api 0.2.3. Stacked on
the 0.2.1 manual so that part can be cherry-picked to 2.1.x-dev alone.
* docs(extensions): clarify PAT rejection on contributed routes
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
512 lines
28 KiB
Plaintext
512 lines
28 KiB
Plaintext
---
|
|
title: Reference
|
|
description: Every public name in deerflow-extension-api 0.2.3, grouped by topic, with signatures, fields, and defaults. Also covers the compatibility rules and the contract's version history.
|
|
---
|
|
|
|
import { Callout } from "nextra/components";
|
|
|
|
# Reference
|
|
|
|
This page lists every name in `deerflow_extension_api.__all__` for contract version **0.2.3**. Import them from the package root:
|
|
|
|
```python
|
|
from deerflow_extension_api import ExtensionRegistry, MiddlewarePlacement, Placement
|
|
```
|
|
|
|
The package has no dependencies and never imports DeerFlow. Types written as `Any` below, such as a middleware or a router, are validated by the host at runtime rather than by the contract.
|
|
|
|
## Entry point and registry
|
|
|
|
### `ExtensionInstall`
|
|
|
|
```python
|
|
ExtensionInstall = Callable[[ExtensionRegistry, Mapping[str, Any]], None]
|
|
```
|
|
|
|
The signature of the function named by a `plugins:` record's `use`. The second argument is a shallow copy of the record's `config`.
|
|
|
|
### `extension(*, api, name=None)`
|
|
|
|
Decorator that stamps an install function with the contract version it was written against (`__deerflow_api__`) and an optional name (`__deerflow_name__`). Optional; see [Compatibility rules](#compatibility-rules).
|
|
|
|
```python
|
|
@extension(api="0.2.0", name="hello")
|
|
def install(registry: ExtensionRegistry, config: Mapping[str, Any]) -> None: ...
|
|
```
|
|
|
|
### `ExtensionRegistry`
|
|
|
|
A `runtime_checkable` Protocol: the write-only surface passed to `install()`. Every method has a default implementation that registers nothing. A host whose registry predates a method inherits that default, so the call succeeds but the contribution is not registered. `plugin()` is the one method that reports this, by returning `False`; the version marker and package metadata are how you prevent it for the others.
|
|
|
|
| Method | Returns | Registers |
|
|
| ---------------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------- |
|
|
| `middlewares(contributor: MiddlewareContributor)` | `None` | A middleware contributor |
|
|
| `task_lifecycle(contributor: TaskLifecycleContributor)` | `None` | Start and stop hooks for lead runs and subagents |
|
|
| `system_model_observer(observer: SystemModelCallObserver)` | `None` | An observer of DeerFlow-owned model calls |
|
|
| `agent_assembly_observer(observer: AgentAssemblyObserver)` | `None` | An observer of assembled agents |
|
|
| `context_compaction_observer(observer: ContextCompactionObserver)` | `None` | An observer of summarization |
|
|
| `service(service: ExtensionService)` | `None` | A Gateway-lifetime service |
|
|
| `routers(routers: Sequence[Any])` | `None` | FastAPI routers, built during `install()` |
|
|
| `plugin(contribution: PluginContribution)` | `bool` | A full-stack plugin. `True` when accepted, `False` on a host without plugin support. Experimental |
|
|
|
|
## Middleware
|
|
|
|
### `MiddlewareContributor`
|
|
|
|
```python
|
|
class MiddlewareContributor(Protocol):
|
|
def contribute_middlewares(
|
|
self, app_store: ExtensionData, ctx: AgentBuildContext
|
|
) -> Sequence[MiddlewarePlacement]: ...
|
|
```
|
|
|
|
Called on every agent assembly. Default returns `()`.
|
|
|
|
### `MiddlewarePlacement`
|
|
|
|
Frozen dataclass.
|
|
|
|
| Field | Type | Default |
|
|
| ------------ | ------------ | ----------------- |
|
|
| `middleware` | `Any` (must be a LangChain `AgentMiddleware`) | required |
|
|
| `placement` | `Placement` | required |
|
|
| `scope` | `AgentScope` | `AgentScope.BOTH` |
|
|
| `order` | `int` | `0` |
|
|
|
|
### `Placement`
|
|
|
|
`StrEnum`: `MODEL_LOGICAL = "model_logical"`, `MODEL_PHYSICAL = "model_physical"`, `TOOL_VISIBLE = "tool_visible"`, `TOOL_RAW = "tool_raw"`, `STANDARD = "standard"`. The guarantee each one makes is in [Middleware Contributions](/docs/harness/extensions/middleware).
|
|
|
|
### `AgentScope`
|
|
|
|
`Flag`: `LEAD`, `SUBAGENT`, `BOTH = LEAD | SUBAGENT`.
|
|
|
|
### `AgentBuildContext`
|
|
|
|
Frozen dataclass passed to `contribute_middlewares()`.
|
|
|
|
| Field | Type | Default |
|
|
| ------------ | -------------------- | ---------------------- |
|
|
| `scope` | `AgentScope` | required |
|
|
| `agent_name` | `str \| None` | `None` |
|
|
| `model_name` | `str \| None` | `None` |
|
|
| `policy` | `HostPolicySnapshot` | `HostPolicySnapshot()` |
|
|
|
|
### `HostPolicySnapshot`
|
|
|
|
Frozen dataclass: the limits the host enforces, projected so extensions do not depend on DeerFlow's config types. Every field has a default.
|
|
|
|
| Field | Type | Default |
|
|
| ----------------------- | --------------- | ------- |
|
|
| `token_budget_enabled` | `bool` | `False` |
|
|
| `max_input_tokens` | `int \| None` | `None` |
|
|
| `max_output_tokens` | `int \| None` | `None` |
|
|
| `max_total_tokens` | `int \| None` | `None` |
|
|
| `budget_warn_fraction` | `float \| None` | `None` |
|
|
| `budget_hard_fraction` | `float \| None` | `None` |
|
|
| `max_subagents_per_run` | `int \| None` | `None` |
|
|
|
|
## State
|
|
|
|
### `ExtensionData`
|
|
|
|
Typed, thread-safe store attached to one host scope (the app, or one task). Keyed by Python type, so two extensions cannot collide.
|
|
|
|
| Member | Description |
|
|
| ---------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
| `ExtensionData(scope_id: str)` | Constructor. The host creates stores; construct one yourself only in tests |
|
|
| `scope_id: str` | Host identity of the scope |
|
|
| `get(typ: type[T]) -> T \| None` | The stored instance of `typ`, or `None` |
|
|
| `get_or_init(typ: type[T], init: Callable[[], T]) -> T` | The stored instance, created by `init()` when absent. `init` runs under the store's lock |
|
|
| `set(value: T) -> None` | Store `value` under `type(value)`, replacing any previous one |
|
|
| `remove(typ: type[T]) -> T \| None` | Remove and return the stored instance |
|
|
|
|
### `task_store_from_runtime(runtime: object) -> ExtensionData | None`
|
|
|
|
Return the task-scoped store from a LangGraph runtime (`request.runtime` in a wrap hook, the `runtime` argument of a lifecycle hook), or `None` when there is no live task.
|
|
|
|
### `EXTENSION_TASK_STORE_KEY`
|
|
|
|
`"__deerflow_extension_task_store"`. The host-owned runtime-context key behind `task_store_from_runtime()`. Read it only through that helper and never write it.
|
|
|
|
## Task lifecycle
|
|
|
|
### `TaskLifecycleContributor`
|
|
|
|
```python
|
|
class TaskLifecycleContributor(Protocol):
|
|
async def on_task_start(self, app_store: ExtensionData, task_store: ExtensionData, info: TaskInfo) -> None: ...
|
|
async def on_task_stop(
|
|
self, app_store: ExtensionData, task_store: ExtensionData, info: TaskInfo, outcome: TaskOutcome
|
|
) -> None: ...
|
|
```
|
|
|
|
### `TaskInfo`
|
|
|
|
Frozen dataclass.
|
|
|
|
| Field | Type | Default |
|
|
| ---------------- | ----------------------------- | ------- |
|
|
| `task_id` | `str` | required |
|
|
| `run_id` | `str` | required |
|
|
| `thread_id` | `str` | required |
|
|
| `kind` | `Literal["lead", "subagent"]` | required |
|
|
| `parent_task_id` | `str \| None` | `None` |
|
|
| `agent_name` | `str \| None` | `None` |
|
|
| `resumed` | `bool` | `False` |
|
|
|
|
### `TaskOutcome`
|
|
|
|
`StrEnum`: `COMPLETED = "completed"`, `ABORTED = "aborted"`, `FAILED = "failed"`.
|
|
|
|
## System model calls
|
|
|
|
### `SystemModelCallObserver`
|
|
|
|
```python
|
|
class SystemModelCallObserver(Protocol):
|
|
async def on_system_model_call(
|
|
self,
|
|
app_store: ExtensionData,
|
|
task_store: ExtensionData,
|
|
kind: SystemOperationKind,
|
|
request: SystemModelRequest,
|
|
result: SystemModelResult,
|
|
) -> None: ...
|
|
```
|
|
|
|
### `SystemOperationKind`
|
|
|
|
`StrEnum`: `GOAL = "goal"`, `MEMORY = "memory"`, `TITLE = "title"`, `SUMMARIZATION = "summarization"`.
|
|
|
|
### `SystemModelRequest`
|
|
|
|
Frozen dataclass, a read-only snapshot taken before the call.
|
|
|
|
| Field | Type | Default |
|
|
| --------------- | ----------------------------- | ------- |
|
|
| `messages` | `Sequence[Any]` | `()` |
|
|
| `model_name` | `str \| None` | `None` |
|
|
| `invoke_config` | `Mapping[str, Any] \| None` | `None` |
|
|
|
|
`messages` is normalized to a tuple on construction. A single prompt string becomes a one-element tuple rather than a sequence of characters.
|
|
|
|
### `SystemModelResult`
|
|
|
|
Frozen dataclass: `response: Any | None = None`, `error: BaseException | None = None`, `duration_ms: float | None = None`.
|
|
|
|
## Agent assembly
|
|
|
|
### `AgentAssemblyObserver`
|
|
|
|
```python
|
|
class AgentAssemblyObserver(Protocol):
|
|
def on_agent_assembled(self, app_store: ExtensionData, descriptor: AgentAssemblyDescriptor) -> None: ...
|
|
```
|
|
|
|
Synchronous, called at the end of agent construction. Must be cheap and must not raise.
|
|
|
|
### `AgentAssemblyDescriptor`
|
|
|
|
Frozen dataclass.
|
|
|
|
| Field | Type | Default |
|
|
| --------------------- | --------------------------------- | ------- |
|
|
| `namespace` | `str` | required |
|
|
| `agent_name` | `str` | required |
|
|
| `requested_model` | `str \| None` | required |
|
|
| `effective_model` | `str` | required |
|
|
| `model_parameters` | `dict[str, Any]` | required |
|
|
| `thinking_enabled` | `bool` | required |
|
|
| `reasoning_effort` | `Any` | required |
|
|
| `base_prompt_hash` | `str` | required |
|
|
| `tools` | `tuple[ToolDescriptor, ...]` | required |
|
|
| `middlewares` | `tuple[MiddlewareDescriptor, ...]` | required |
|
|
| `deferred_tool_names` | `tuple[str, ...]` | required |
|
|
| `enabled_skills` | `tuple[str, ...]` | required |
|
|
| `effective_policies` | `dict[str, Any]` | required |
|
|
| `build` | `dict[str, Any]` | `{}` |
|
|
|
|
`fingerprint: str` (cached property) is a `canonical_hash` of everything that changes behavior. Tools, deferred tool names, and skills are sorted; middleware order is preserved because it decides what wraps what. `build` and `requested_model` are excluded, so a redeploy of the same assembly keeps the same fingerprint.
|
|
|
|
### `ToolDescriptor`
|
|
|
|
Frozen dataclass: `name: str`, `description_hash: str`, `schema_hash: str`, `source: str`, `mcp_server: str | None = None`, `mcp_transport: str | None = None`.
|
|
|
|
### `MiddlewareDescriptor`
|
|
|
|
Frozen dataclass: `name: str`, `module: str`, `policy_parameters: dict[str, Any] = {}`, `extension: str | None = None`. `extension` names the contributing extension; it is `None` for host middleware.
|
|
|
|
## Context compaction
|
|
|
|
### `ContextCompactionObserver`
|
|
|
|
```python
|
|
class ContextCompactionObserver(Protocol):
|
|
async def on_context_compacted(
|
|
self, app_store: ExtensionData, task_store: ExtensionData, event: CompactionEvent
|
|
) -> None: ...
|
|
```
|
|
|
|
### `CompactionEvent`
|
|
|
|
Frozen dataclass, captured while both sides of the transform still exist.
|
|
|
|
| Field | Type |
|
|
| ------------------------- | ----------------- |
|
|
| `transform_kind` | `str` |
|
|
| `transform_version` | `str` |
|
|
| `source_content_hashes` | `tuple[str, ...]` |
|
|
| `output_content_hash` | `str` |
|
|
| `compacted_message_count` | `int` |
|
|
| `kept_message_count` | `int` |
|
|
|
|
Hashes are `canonical_hash(message.content)` of the content passed directly, never a stringified copy. To match a message to an event, hash its `content` the same way.
|
|
|
|
## Services
|
|
|
|
### `ExtensionService`
|
|
|
|
```python
|
|
class ExtensionService(Protocol):
|
|
async def start(self, deps: ExtensionRuntimeDeps) -> None: ...
|
|
async def stop(self) -> None: ...
|
|
```
|
|
|
|
### `ExtensionRuntimeDeps`
|
|
|
|
Frozen dataclass passed to `start()`.
|
|
|
|
| Field | Type | Default |
|
|
| --------------------- | --------------------------- | ---------------------- |
|
|
| `app_store` | `ExtensionData \| None` | `None` |
|
|
| `policy` | `HostPolicySnapshot` | `HostPolicySnapshot()` |
|
|
| `session_factory` | `Any \| None` | `None` |
|
|
| `run_evidence_reader` | `RunEvidenceReader \| None` | `None` |
|
|
|
|
`run_evidence_reader` is `None` on a host that does not provide one.
|
|
|
|
## Run evidence
|
|
|
|
### `RunEvidenceReader`
|
|
|
|
A read-only Protocol. Its default methods raise `NotImplementedError`.
|
|
|
|
| Method | Returns |
|
|
| ------------------------------------------------------------------------------------------------ | ------------------------- |
|
|
| `async list_changed_runs(*, cursor: str \| None, limit: int)` | `RunPage` |
|
|
| `async list_run_events(*, thread_id: str, run_id: str, after_seq: int \| None, limit: int)` | `RunEventPage` |
|
|
| `async get_run_status(*, thread_id: str, run_id: str)` | `RunStatusView \| None` |
|
|
|
|
The Gateway's implementation accepts `limit` from 1 to 2000 and a non-negative `after_seq`, raising `ValueError` otherwise. Deletions produce no tombstone: `get_run_status()` returning `None` means the run is absent or not visible. See [Run Evidence](/docs/harness/extensions/run-evidence).
|
|
|
|
### `RunStatusView`
|
|
|
|
Frozen dataclass: `thread_id`, `run_id`, `status`, `created_at`, `updated_at` (all `str = ""`), `error: str | None = None`, `stop_reason: str | None = None`.
|
|
|
|
### `RunEventView`
|
|
|
|
Frozen dataclass: `thread_id: str = ""`, `run_id: str = ""`, `seq: int = 0` (monotonic within a thread), `event_type: str = ""`, `category: str = ""`, `content: Any = None`, `metadata: dict[str, Any] = {}`, `created_at: str = ""`. Content and metadata are detached copies. Content is returned unchanged; metadata has only the legacy `auth_token` key removed, with no other redaction.
|
|
|
|
### `RunPage` / `RunEventPage`
|
|
|
|
Frozen dataclasses. `RunPage`: `items: tuple[RunStatusView, ...] = ()`, `next_cursor: str | None = None`, `has_more: bool = False`. `RunEventPage`: `items: tuple[RunEventView, ...] = ()`, `next_after_seq: int | None = None`, `has_more: bool = False`.
|
|
|
|
### `resolve_run_evidence_reader(request: object) -> RunEvidenceReader | None`
|
|
|
|
Return a reader bound to the authenticated caller of a contributed route. Use it in user-facing routes instead of the global `ExtensionRuntimeDeps.run_evidence_reader`, which sees every user's runs. `request` is duck-typed. Returns `None` on a host without request-scoped evidence. The Gateway raises `PermissionError("run evidence requires an authenticated user with runs:read")` when the caller is unauthenticated or lacks the `runs:read` permission; it never widens an admin or internal caller to global visibility. Any other resolver error propagates.
|
|
|
|
### `require_run_evidence_reader(request: object) -> RunEvidenceReader`
|
|
|
|
Like `resolve_run_evidence_reader()`, but raises `NotImplementedError("request-scoped run evidence is unavailable")` instead of returning `None`. Map `NotImplementedError` to HTTP 503 and `PermissionError` to 403. It never falls back to the global reader.
|
|
|
|
### `RUN_EVIDENCE_READER_RESOLVER_KEY`
|
|
|
|
`"deerflow_extension_run_evidence_reader_resolver"`. The `app.state` attribute the host installs its request-scoped resolver under. Host-owned.
|
|
|
|
### `InvalidRunEvidenceCursor`
|
|
|
|
Subclass of `ValueError`, raised for a malformed or unsupported cursor, or a cursor from another scope.
|
|
|
|
## Identity
|
|
|
|
### `ExtensionPrincipal`
|
|
|
|
Frozen dataclass: `user_id: str`, `is_admin: bool = False`, `is_internal: bool = False`, `roles: tuple[str, ...] = ()`.
|
|
|
|
### `resolve_principal(request: object) -> ExtensionPrincipal | None`
|
|
|
|
The authenticated caller of a contributed route, or `None` when it cannot be determined. `request` is duck-typed, so a Starlette `Request` works without the contract depending on Starlette.
|
|
|
|
### `require_admin(request: object) -> ExtensionPrincipal`
|
|
|
|
Return the principal if it is an administrator, otherwise raise `PermissionError("this endpoint requires an administrator account")`. It fails closed when identity cannot be determined.
|
|
|
|
### `EXTENSION_PRINCIPAL_RESOLVER_KEY`
|
|
|
|
`"deerflow_extension_principal_resolver"`. The `app.state` attribute the host installs its resolver under. Host-owned.
|
|
|
|
## Message provenance
|
|
|
|
Middleware that injects messages stamps them, so an observer can tell an injected message from the user's own without matching on wording.
|
|
|
|
| Constant | Value |
|
|
| -------------------------------- | ------------------------------- |
|
|
| `MESSAGE_CONTENT_KIND_KEY` | `"deerflow_content_kind"` |
|
|
| `MESSAGE_PRODUCER_KIND_KEY` | `"deerflow_producer_kind"` |
|
|
| `MESSAGE_PRODUCER_ENTITY_ID_KEY` | `"deerflow_producer_entity_id"` |
|
|
| `PROVENANCE_KEYS` | `frozenset` of the three keys. The host treats them as server-owned and strips caller-supplied values from untrusted input |
|
|
|
|
### `ContentKind`
|
|
|
|
`StrEnum`: `MIDDLEWARE_INJECTION = "middleware_injection"`, `MEMORY = "memory"`, `DURABLE_CONTEXT = "durable_context"`, `SKILL_BODY = "skill_body"`, `IMAGE_PAYLOAD = "image_payload"`. Stamped values are plain strings, so a kind added by a newer host arrives as an unrecognized string rather than an error.
|
|
|
|
### `MessageProvenance`
|
|
|
|
Frozen dataclass: `content_kind: str`, `producer_kind: str`, `producer_entity_id: str | None = None`.
|
|
|
|
### `provenance_kwargs(content_kind, producer_kind, *, producer_entity_id=None) -> dict[str, str]`
|
|
|
|
The `additional_kwargs` fragment to merge into a message you produce. `producer_entity_id` is omitted when `None`.
|
|
|
|
### `read_provenance(message: object) -> MessageProvenance | None`
|
|
|
|
Read a stamp from `message.additional_kwargs`. Returns `None` when either required key is missing or not a string.
|
|
|
|
## Release policies and hashing
|
|
|
|
### `ReleasePolicyProvider`
|
|
|
|
```python
|
|
@runtime_checkable
|
|
class ReleasePolicyProvider(Protocol):
|
|
def release_policy_parameters(self) -> dict[str, object]: ...
|
|
```
|
|
|
|
Implement it on a middleware to declare its behavior-affecting parameters. They appear in `MiddlewareDescriptor.policy_parameters` and in the assembly fingerprint. Values must be JSON-serializable; hash long text instead of embedding it.
|
|
|
|
### `collect_release_policies(middlewares: Sequence[object]) -> dict[str, dict[str, object]]`
|
|
|
|
Gather declarations from a stack, keyed by class name (`Name`, `Name#2`, ... for repeats), unwrapping isolation wrappers. A declaration that raises is recorded as `{"error": "<ExceptionType>"}`, and one that returns a non-mapping as `{"error": "NonMappingDeclaration"}`.
|
|
|
|
### `canonical_json(value: object) -> str`
|
|
|
|
Deterministic JSON: sorted keys, `(",", ":")` separators, `ensure_ascii=False`. Raises `TypeError` for values that are not JSON-serializable.
|
|
|
|
### `canonical_hash(value: object) -> str`
|
|
|
|
SHA-256 hex digest of `canonical_json(value)` encoded as UTF-8.
|
|
|
|
## Full-stack plugins (experimental)
|
|
|
|
<Callout type="warning">
|
|
The plugin contract is experimental in 0.2.3. Always check the return value of
|
|
`registry.plugin(...)`. See [Plugins](/docs/harness/extensions/plugins).
|
|
</Callout>
|
|
|
|
### `PluginContribution`
|
|
|
|
Frozen dataclass.
|
|
|
|
| Field | Type | Default |
|
|
| ------------- | ------------------------------ | ------- |
|
|
| `namespace` | `str` | required |
|
|
| `title` | `str` | required |
|
|
| `description` | `str` | `""` |
|
|
| `enabled` | `bool` | `False` |
|
|
| `fields` | `tuple[SettingsField, ...]` | `()` |
|
|
| `frontend` | `BrowserModule \| BrowserAssets \| None` | `None` |
|
|
| `backend` | `tuple[BackendAction, ...]` | `()` |
|
|
| `api_version` | `int` | `1` |
|
|
| `tools` | `tuple[ModelTool, ...]` | `()` |
|
|
|
|
The host accepts only `api_version == 1` and requires at least one of `frontend`, `backend`, or `tools`. Namespaces must be unique.
|
|
|
|
### `BrowserModule`
|
|
|
|
Frozen dataclass: `module: str`, `code: str` (a self-contained ES module, non-empty and at most 512 KiB), `public_fields: tuple[str, ...] = ()`. Discovery labels this transport `inline-v1`. Use `BrowserAssets` when the module needs relative imports, stylesheets, or images.
|
|
|
|
### `BrowserAssets`
|
|
|
|
Frozen dataclass declaring a manifest and static files inside the installed package. Discovery labels this transport `assets-v1`.
|
|
|
|
| Field | Type | Default |
|
|
| --------------- | ----------------- | -------------------- |
|
|
| `module` | `str` | required |
|
|
| `root` | `str \| Path` | required |
|
|
| `manifest` | `str` | `"ui_manifest.json"` |
|
|
| `public_fields` | `tuple[str, ...]` | `()` |
|
|
|
|
`root` is usually `Path(__file__).parent`. The manifest, relative to `root`, must be a JSON object with exactly the keys `schema_version` (the integer `1`), `entry`, and `files`:
|
|
|
|
```json
|
|
{
|
|
"schema_version": 1,
|
|
"entry": "static/dist/index.mjs",
|
|
"files": ["static/dist/index.mjs", "static/dist/styles.css"]
|
|
}
|
|
```
|
|
|
|
`files` lists 1 to 256 unique paths and `entry` must be one of them, ending in `.js` or `.mjs`. Paths use ASCII letters, digits, `_`, `-`, and `.`, separated by `/`, and each segment must start with a letter, digit, `_`, or `-`. The root must not be a symlink, and no listed path may pass through one. Limits: 64 KiB for the manifest, 4 MiB per file, 16 MiB in total. Accepted file types: `.js`, `.mjs`, `.css`, `.json`, `.map`, `.wasm`, `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.svg`, `.ico`, `.woff`, `.woff2`, `.ttf`, `.otf`.
|
|
|
|
`registry.plugin()` validates the manifest and reads every listed file into an in-memory snapshot. A validation error raises from `registry.plugin()`, so `install()` fails. The snapshot's revision is a SHA-256 over the manifest, the paths, and the file contents, and the Gateway serves the files from that snapshot at `GET /api/plugins/{namespace}/assets/{revision}/{path}`. Any authenticated user can download listed files, including source maps, even while the plugin is disabled, so never list secrets.
|
|
|
|
### `BackendAction`
|
|
|
|
Frozen dataclass: `name: str`, `handler: Callable[[Mapping[str, Any], ActionContext], Awaitable[Any]]`. Names must be unique and handlers async.
|
|
|
|
### `ModelTool`
|
|
|
|
Frozen dataclass: `name: str`, `description: str`, `input_schema: Mapping[str, Any]` (an inline object schema), `handler: Callable[[Mapping[str, Any], ToolContext], Awaitable[Any]]`, `group: str = "extensions"`.
|
|
|
|
### `ActionContext` / `ToolContext`
|
|
|
|
Frozen dataclasses. `ActionContext`: `principal: ExtensionPrincipal`, `settings: Mapping[str, bool | int | str]`. `ToolContext` extends it with `thread_id: str | None`.
|
|
|
|
### `SettingsField`
|
|
|
|
Frozen dataclass describing one non-secret deployment setting.
|
|
|
|
| Field | Type | Default |
|
|
| ------------- | -------------------------------------------- | ------- |
|
|
| `key` | `str` | required |
|
|
| `title` | `str` | required |
|
|
| `kind` | `Literal["boolean", "integer", "string"]` | required |
|
|
| `default` | `bool \| int \| str` | required |
|
|
| `description` | `str` | `""` |
|
|
| `minimum` | `int \| None` | `None` |
|
|
| `maximum` | `int \| None` | `None` |
|
|
| `max_length` | `int` | `256` |
|
|
|
|
## Version constant
|
|
|
|
### `API_VERSION`
|
|
|
|
The host's contract version as a dotted string, `"0.2.3"` for the contract this page describes. Always equal to the package version in `backend/packages/extension-api/pyproject.toml`.
|
|
|
|
## Compatibility rules
|
|
|
|
- **Additive growth.** Every Protocol method has a default implementation and every optional dataclass field has a default. A contract release that adds a method or field does not break extensions built against an earlier one.
|
|
- **Before 1.0**, a minor release may break extensions and a patch release is additive. **From 1.0 on**, breaking changes bump the major.
|
|
- **The `@extension(api=...)` check.** When an install function carries a marker, the host refuses it unless:
|
|
- before 1.0: the same major and minor, and the host's version is at least the declared one (a `0.2.3` host accepts `0.2.0` through `0.2.3`, and rejects `0.2.4`, `0.1.x`, and `0.3.x`);
|
|
- from 1.0: the same major, and the host's version is at least the declared one.
|
|
|
|
A marker that is not a dotted numeric string is refused. An install function without a marker is not checked.
|
|
- **Package metadata** is the primary mechanism: declare `deerflow-extension-api>=0.2,<0.3` so the resolver rejects a mismatched host before anything loads. The marker covers installs that bypass resolution.
|
|
- **Declare frameworks yourself.** The contract depends on nothing. An extension that imports LangChain, LangGraph, or FastAPI declares them.
|
|
|
|
## Version history
|
|
|
|
| Version | PR | Added |
|
|
| ------- | -- | ----- |
|
|
| 0.1.0 | [#4636](https://github.com/bytedance/deer-flow/pull/4636) | The foundation: `install()` and `@extension`, `ExtensionRegistry.middlewares`, `MiddlewareContributor`, `MiddlewarePlacement`, `Placement`, `AgentScope`, `AgentBuildContext`, `HostPolicySnapshot`, `ExtensionData`, `task_store_from_runtime`, `EXTENSION_TASK_STORE_KEY`, `API_VERSION` |
|
|
| 0.1.1 | [#4684](https://github.com/bytedance/deer-flow/pull/4684) | `task_lifecycle` and `system_model_observer` registrations with `TaskLifecycleContributor`, `TaskInfo`, `TaskOutcome`, `SystemModelCallObserver`, `SystemModelRequest`, `SystemModelResult`, `SystemOperationKind` |
|
|
| 0.1.2 | [#4780](https://github.com/bytedance/deer-flow/pull/4780) | `service` and `routers` registrations with `ExtensionService` and `ExtensionRuntimeDeps`; the packaged-extension manager |
|
|
| 0.2.0 | [#4863](https://github.com/bytedance/deer-flow/pull/4863) | `agent_assembly_observer` and `context_compaction_observer` with `AgentAssemblyDescriptor`, `ToolDescriptor`, `MiddlewareDescriptor`, `CompactionEvent`; message provenance; release policies and canonical hashing; `ExtensionPrincipal`, `resolve_principal`, `require_admin` |
|
|
| 0.2.1 | [#5405](https://github.com/bytedance/deer-flow/pull/5405) | `ExtensionRuntimeDeps.run_evidence_reader` with `RunEvidenceReader`, `RunPage`, `RunEventPage`, `RunStatusView`, `RunEventView`, `InvalidRunEvidenceCursor` |
|
|
| 0.2.2 | [#5647](https://github.com/bytedance/deer-flow/pull/5647) | Experimental `registry.plugin()` with `PluginContribution`, `BrowserModule`, `BackendAction`, `ModelTool`, `ActionContext`, `ToolContext`, `SettingsField` |
|
|
| 0.2.3 | [#5727](https://github.com/bytedance/deer-flow/pull/5727), [#5685](https://github.com/bytedance/deer-flow/pull/5685) | Request-scoped run evidence: `resolve_run_evidence_reader`, `require_run_evidence_reader`, `RUN_EVIDENCE_READER_RESOLVER_KEY` (#5727). Packaged browser resources: `BrowserAssets`, and `PluginContribution.frontend` also accepts it (#5685) |
|
|
|
|
No release has removed a public name.
|