Reference
This page lists every name in deerflow_extension_api.__all__ for contract version 0.2.3. Import them from the package root:
from deerflow_extension_api import ExtensionRegistry, MiddlewarePlacement, PlacementThe 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
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.
@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
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.
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
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
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
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
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
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.
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
@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)
The plugin contract is experimental in 0.2.3. Always check the return value of
registry.plugin(...). See Plugins.
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:
{
"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.3host accepts0.2.0through0.2.3, and rejects0.2.4,0.1.x, and0.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.
- before 1.0: the same major and minor, and the host’s version is at least the declared one (a
-
Package metadata is the primary mechanism: declare
deerflow-extension-api>=0.2,<0.3so 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 | 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 | task_lifecycle and system_model_observer registrations with TaskLifecycleContributor, TaskInfo, TaskOutcome, SystemModelCallObserver, SystemModelRequest, SystemModelResult, SystemOperationKind |
| 0.1.2 | #4780 | service and routers registrations with ExtensionService and ExtensionRuntimeDeps; the packaged-extension manager |
| 0.2.0 | #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 | ExtensionRuntimeDeps.run_evidence_reader with RunEvidenceReader, RunPage, RunEventPage, RunStatusView, RunEventView, InvalidRunEvidenceCursor |
| 0.2.2 | #5647 | Experimental registry.plugin() with PluginContribution, BrowserModule, BackendAction, ModelTool, ActionContext, ToolContext, SettingsField |
| 0.2.3 | #5727 , #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.