Skip to Content
DeerFlow

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

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

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)

No release has removed a public name.