Skip to Content
DeerFlow

Reference

This page lists every name in deerflow_extension_api.__all__ for contract version 0.2.4. Import them from the package root:

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

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.

MethodReturnsRegisters
middlewares(contributor: MiddlewareContributor)NoneA middleware contributor
task_lifecycle(contributor: TaskLifecycleContributor)NoneStart and stop hooks for lead runs and subagents
system_model_observer(observer: SystemModelCallObserver)NoneAn observer of DeerFlow-owned model calls
agent_assembly_observer(observer: AgentAssemblyObserver)NoneAn observer of assembled agents
context_compaction_observer(observer: ContextCompactionObserver)NoneAn observer of summarization
service(service: ExtensionService)NoneA Gateway-lifetime service
routers(routers: Sequence[Any])NoneFastAPI routers, built during install()
plugin(contribution: PluginContribution)boolA 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.

FieldTypeDefault
middlewareAny (must be a LangChain AgentMiddleware)required
placementPlacementrequired
scopeAgentScopeAgentScope.BOTH
orderint0

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

FieldTypeDefault
scopeAgentScoperequired
agent_namestr | NoneNone
model_namestr | NoneNone
policyHostPolicySnapshotHostPolicySnapshot()

HostPolicySnapshot

Frozen dataclass: the limits the host enforces, projected so extensions do not depend on DeerFlow’s config types. Every field has a default.

FieldTypeDefault
token_budget_enabledboolFalse
max_input_tokensint | NoneNone
max_output_tokensint | NoneNone
max_total_tokensint | NoneNone
budget_warn_fractionfloat | NoneNone
budget_hard_fractionfloat | NoneNone
max_subagents_per_runint | NoneNone

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.

MemberDescription
ExtensionData(scope_id: str)Constructor. The host creates stores; construct one yourself only in tests
scope_id: strHost identity of the scope
get(typ: type[T]) -> T | NoneThe stored instance of typ, or None
get_or_init(typ: type[T], init: Callable[[], T]) -> TThe stored instance, created by init() when absent. init runs under the store’s lock
set(value: T) -> NoneStore value under type(value), replacing any previous one
remove(typ: type[T]) -> T | NoneRemove 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.

FieldTypeDefault
task_idstrrequired
run_idstrrequired
thread_idstrrequired
kindLiteral["lead", "subagent"]required
parent_task_idstr | NoneNone
agent_namestr | NoneNone
resumedboolFalse

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.

FieldTypeDefault
messagesSequence[Any]()
model_namestr | NoneNone
invoke_configMapping[str, Any] | NoneNone

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.

FieldTypeDefault
namespacestrrequired
agent_namestrrequired
requested_modelstr | Nonerequired
effective_modelstrrequired
model_parametersdict[str, Any]required
thinking_enabledboolrequired
reasoning_effortAnyrequired
base_prompt_hashstrrequired
toolstuple[ToolDescriptor, ...]required
middlewarestuple[MiddlewareDescriptor, ...]required
deferred_tool_namestuple[str, ...]required
enabled_skillstuple[str, ...]required
effective_policiesdict[str, Any]required
builddict[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.

FieldType
transform_kindstr
transform_versionstr
source_content_hashestuple[str, ...]
output_content_hashstr
compacted_message_countint
kept_message_countint

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

FieldTypeDefault
app_storeExtensionData | NoneNone
policyHostPolicySnapshotHostPolicySnapshot()
session_factoryAny | NoneNone
run_evidence_readerRunEvidenceReader | NoneNone

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.

MethodReturns
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.

require_plugin_management(request: object, namespace: str, *, scope: Literal["read", "write"] = "read") -> ExtensionPrincipal

Return the caller when it holds plugin_management scope for an installed plugin namespace; otherwise raise PermissionError. Use it in a contributed management route before reading (scope="read") or changing (scope="write") the enterprise’s own policy store. The host asks its configured authorization provider with the caller’s trusted identity, so read and write are separate decisions and neither is implied by page access or by a tool grant.

It fails closed: an unknown namespace, an anonymous caller, a missing or failing host resolver, and a non-allowed decision all raise PermissionError. A host that is running on a configuration and can no longer read it follows the configured failure policy — denied under the default fail_closed: true. A host with authorization.enabled: false, or one with no configuration at all, answers “allow”, so the helper is a no-op and does not start rejecting routes; an enterprise that needs an unconditional floor keeps calling require_admin as well. A scope other than "read"/"write", or a namespace outside [a-z][a-z0-9_.-]{0,95}, raises ValueError before any decision.

This form is synchronous. Call it from a FastAPI def endpoint (executed in the thread pool), not from an async endpoint.

arequire_plugin_management(request: object, namespace: str, *, scope: Literal["read", "write"] = "read") -> ExtensionPrincipal

The async form of require_plugin_management, for async def endpoints. It awaits the host’s async resolver and never falls back to the synchronous one, so the host’s configuration read stays off the event loop.

EXTENSION_PLUGIN_AUTHZ_RESOLVER_KEY

"deerflow_extension_plugin_authz_resolver". The app.state attribute the host installs the synchronous plugin_management resolver under, answering (request, namespace, scope) -> bool | None. Host-owned.

EXTENSION_PLUGIN_AUTHZ_RESOLVER_ASYNC_KEY

"deerflow_extension_plugin_authz_resolver_async". The async counterpart, answering async (request, namespace, scope) -> bool | None. 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.

ConstantValue
MESSAGE_CONTENT_KIND_KEY"deerflow_content_kind"
MESSAGE_PRODUCER_KIND_KEY"deerflow_producer_kind"
MESSAGE_PRODUCER_ENTITY_ID_KEY"deerflow_producer_entity_id"
PROVENANCE_KEYSfrozenset 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.4. Always check the return value of registry.plugin(...). See Plugins.

PluginContribution

Frozen dataclass.

FieldTypeDefault
namespacestrrequired
titlestrrequired
descriptionstr""
enabledboolFalse
fieldstuple[SettingsField, ...]()
frontendBrowserModule | BrowserAssets | NoneNone
backendtuple[BackendAction, ...]()
api_versionint1
toolstuple[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.

FieldTypeDefault
modulestrrequired
rootstr | Pathrequired
manifeststr"ui_manifest.json"
public_fieldstuple[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.

FieldTypeDefault
keystrrequired
titlestrrequired
kindLiteral["boolean", "integer", "string"]required
defaultbool | int | strrequired
descriptionstr""
minimumint | NoneNone
maximumint | NoneNone
max_lengthint256

Version constant

API_VERSION

The host’s contract version as a dotted string, "0.2.4" 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.4 host accepts 0.2.0 through 0.2.4, and rejects 0.2.5, 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

VersionPRAdded
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)
0.2.4#5842 Plugin resource authorization: require_plugin_management, arequire_plugin_management, EXTENSION_PLUGIN_AUTHZ_RESOLVER_KEY, EXTENSION_PLUGIN_AUTHZ_RESOLVER_ASYNC_KEY

No release has removed a public name.