deer-flow/docs/skill-evolution-host.md
Ryker_Feng d1d0958bea
feat(skills): add guarded evolution host APIs (#5645)
* feat(skills): add guarded evolution host APIs

* docs(agents): avoid growing saturated guidance chains

* fix(skills): address mutation host review findings

* fix(skills): order name fence before projection lock

* fix(skills): preserve composite toggle lock order

* fix(skills): address catalog and pagination review feedback

* docs: keep subagent guidance within merged instruction budget

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-18 21:17:02 +08:00

12 KiB

Skill evolution host API (P0)

DeerFlow supplies evidence, versioned candidate staging, checking, publication, and recovery. The learning strategy, candidate generation, quality evaluation, and product UI belong to the extension and are not included in this feature.

Enable deliberately

Use deerflow-extension-api==0.2.7. Existing extensions need no changes: both ExtensionRuntimeDeps.completed_run_evidence and .skill_mutations default to None. The older cross-user run_evidence_reader is unchanged.

Automatic publication requires:

  • A durable application database (sqlite or postgres) and run_events.backend: db.
  • One POSIX host and shared local skill storage. NFS, CSI, and multi-node deployments are unsupported, including when the database is PostgreSQL.
  • All managed writers upgraded together; no concurrent direct filesystem edits.
  • One Gateway process for SQLite, enforced by a process-lifetime file lock.
  • Enabled static scanning and a working configured security-scanning model. Unavailable or incomplete scans never allow automatic publication.

Grant only the intended owners, trigger agents, operations, and existing skill names in operator-controlled config.yaml, then restart the Gateway:

plugins:
  - name: skill-evolution
    use: your_package:install
    host_access:
      evidence:
        owners: ["exact-user-id"]
      skill_mutations:
        owners: ["exact-user-id"]
        operations: [stage, check, commit, revert]
        trigger_agents: [lead_agent]
        target_skills: [your-existing-custom-skill]
        topology: single_host_local

This is an example grant, not a bundled plugin. Owner lists are exact and mutation owners must also have evidence access. Trigger agents are canonical AgentConfig.name values, not display names. Both plugin name and installation entry point must be unique and stable; changing either changes its durable identity. Grants bind to host-recorded contribution sources, never plugin-supplied IDs. They take effect at restart, not live config reload.

These controls prevent mistakes through the supported API. Trusted Python extensions still have Gateway process privileges and legacy database access; this is not an untrusted-code sandbox or proof of learning quality. Ordinary host logs omit candidate contents and raw model errors. Security scans still send candidate text to the configured model, and operator-enabled tracing may retain its prompts; apply your provider and tracing data-retention policies.

Integrate a plugin

Acquire the two optional dependencies in the extension service's start(deps). All methods are asynchronous; DTOs come from deerflow_extension_api.

  1. Discover changed runs and obtain a completed-run snapshot. Terminal status alone does not mean complete evidence: inspect seal_state, coverage limits, origin, and canonical agent identity. Pages stay within the snapshot's fixed upper sequence; deletion/retention invalidates old references.
  2. read_skill(source_ref=..., name=...) obtains the opaque target and revision.
  3. stage(source_refs=..., target_ref=..., expected_revision=..., content=..., idempotency_key=...) persists an immutable candidate.
  4. Inspect read_proposal_bundle(proposal_id=...) to evaluate the exact full baseline and candidate. These detached bytes are never activated as skills.
  5. check(proposal_id=...) runs fail-closed host checks; require allow.
  6. commit(proposal_id=..., idempotency_key=..., assessment_ref=...) returns a durable operation. An optional assessment reference records the plugin's evaluation; the host neither fetches its URL nor endorses its conclusion.
  7. After response loss, use find_operation(idempotency_key=..., method="commit") or retry with the same arguments and key. A different key is not a safe way to retry a logically identical request.

P0 updates only existing, enabled, user-custom SKILL.md body and description. Every other parsed frontmatter value, including unknown fields, stays unchanged. It cannot create, rename, delete, toggle, or modify support files. Symlinks and special files anywhere in the automatic-update package are unsupported.

Revisions include incarnation, mutation sequence, and whole-package digest. Manual edits, support-file edits, delete/recreate, and enable toggles invalidate stale proposals, even if content later returns to its original bytes. Unrelated skills do not share a compare-and-swap revision.

Limits and retention

The service advertises limits through .capabilities:

Resource P0 limit
Main file 128 KiB
Complete package 256 files / 16 MiB
Evidence sources 20, all for the same owner
Pending proposals 20 per owner/plugin
Model scan 32 text files / 1 MiB total / 128 KiB each / 120 seconds
Live scans 4 globally; 2 per owner/plugin
Scan attempts 20 per owner/plugin per hour, including failed checks and reverts
Proposal / check lifetime 24 hours / 15 minutes
Rollback snapshot lifetime 30 days

Oversized bundles fail as a whole. A scan never approves only the first part of a package. Model-based checks can incur model cost, even though the host does not generate evolved skills. Metadata and idempotency records survive blob expiry; expired rollback bytes are no longer available. Recovery references for unresolved operations are not garbage-collected. The legacy JSONL history receives only an idempotent, compact metadata mirror, not retained skill bodies. Repair replays at most the latest 100 applied operations per owner; files above 8 MiB, lines above 64 KiB, or malformed history are left untouched. The operation database remains authoritative even if the display mirror is unavailable.

A cancelled scan or expired scan deadline propagates to the caller and releases its proposal/scan leases without caching an unavailable verdict for 15 minutes. The attempt still counts against scan-cost quotas. Retrying check() starts a fresh scan; cancelling one waiter alone leaves a shared in-flight scan running for its other waiters. Completed unavailable/rejected verdicts retain the normal cache policy.

Publication, recovery, and rollback

Publication and derived-view readiness are separate. APPLIED means canonical bytes are committed; views=ERROR does not mean they were rolled back. Keep the operation ID and inspect its current state rather than blindly resubmitting.

Before touching canonical bytes, the host durably records PREPARED, before/after snapshots, and reserved versions. It then atomically replaces and fsyncs the main file. Recovery compares the exact package with those snapshots:

  • Before snapshot: ABORTED; an unapplied candidate is never replayed.
  • After snapshot: finalize APPLIED with the reserved version exactly once.
  • Neither: NEEDS_REPAIR; do not overwrite possible human work.

Affected owner reads remain gated while publication is unresolved. Other owners remain usable. Recovery belongs to DeerFlow, so disabling or uninstalling the originating plugin does not disable coordination or recovery.

Administrators using session authentication can discover unresolved publication or view operations through paginated GET /api/skill-mutations/operations (limit up to 100, after_id from next_cursor). Operation pagination ends with has_more: false and next_cursor: null, including an empty page after a supplied cursor. They can query GET /api/skill-mutations/operations/{operation_id}, reconcile through POST /api/skill-mutations/operations/{operation_id}/recover, or reconcile an owner's interrupted managed writes through POST /api/skill-mutations/owners/{owner_id}/recover. Ordinary users and personal access tokens cannot use these endpoints. If reconciliation cannot clear the owner's readiness barrier, either recovery endpoint returns HTTP 409 with detail: "NEEDS_REPAIR"; query the operation to inspect its durable state and retry after repairing the underlying storage problem. A successful response still requires inspecting views, since a projection rebuild can record views=ERROR independently of publication. There is no force-overwrite endpoint.

revert(operation_id=..., expected_current_revision=..., idempotency_key=...) creates a new forward revision only if that operation is still the asset's current head. It rescans the original package under current policy. A later manual edit or toggle causes a conflict, not a destructive rollback. Revert and operation queries can survive source-run deletion; a new commit cannot.

Deleting a conversation does not erase information already distilled into a skill. Skill removal and deletion of derived information are separate operations; P0 does not implement automatic derived-content forgetting.

Local measurements on macOS arm64 (five captures, warm local disk): canonical capture plus digest took a median 4.43 ms for 4 files / 3,120 bytes and 94.03 ms for 256 files / 16,711,473 bytes. These are package-processing measurements, not model latency or a deployment throughput guarantee. Reproduce with uv run pytest tests/test_skill_mutation_package_cost.py -q -s from backend/.

Live PostgreSQL validation on 2026-09-23 used PostgreSQL 17.11 (aarch64, READ COMMITTED), one pytest host process and two concurrent actors sharing a disposable database schema: a publication-fence worker thread holding the source run's SELECT ... FOR SHARE lock, and an async RunRepository.delete() worker. The opt-in regression passed six times (five repetitions plus the related suite); deletion waited for the share lock, then removed both the run and its completed snapshot. The related mutation/evidence/host suite passed 209 tests. See the recorded validation for the command, environment and cleanup.

This verifies the source-fence/deletion interleaving, not a multi-process PostgreSQL soak. Idempotency, recovery, scan-budget and lease behavior were covered by deterministic suites, not by concurrent PostgreSQL Gateway workers. tests/test_skill_mutation_postgres.py remains opt-in via DEERFLOW_TEST_POSTGRES_URL and creates/removes its own uniquely named schema. Validate the intended same-host worker deployment before enabling it; SQLite results alone do not establish PostgreSQL lock behavior.

Upgrade and operational rollback

Back up the application database and user skill directories together. Stop all writers, upgrade all Gateway/embedded writer processes and migrate through 0046_skill_mutations, then restart before enabling mutation grants. Do not mix old and new writers on enrolled assets.

Standalone SDK processes without a Gateway mutation runtime check persisted owner/asset enrollment on every managed storage access. Enrolled owners are rejected with MUTATION_RUNTIME_REQUIRED; use the Gateway for their skill reads and writes. Global enable-state writes are also rejected while any owner is enrolled, because they affect same-named custom skills. Removing the plugin does not remove this barrier. Unavailable enrollment checks fail closed with UNAVAILABLE; missing/legacy databases and unenrolled owners retain ordinary SDK access. Owned custom-skill YAML diagnostics follow the same guard: Gateway reads first recover pending publications, and standalone SDK reads refuse enrolled owners without a mutation runtime. Processes sharing a skill home must use the same application DB.

To stop automation, disable the plugin or remove mutation grants and restart; keep the upgraded host running so it can reconcile outstanding operations. Before downgrading host code, quiesce writers, inspect unresolved operations, and resolve readiness barriers. Do not downgrade while a PREPARED or NEEDS_REPAIR record exists. Restore database and files as a coordinated backup only under an explicit operator recovery procedure.

This host slice follows up the Skill Self-Evolution RFC and the bilingual Plugin Host APIs RFC. Neither RFC implies that this PR ships a learning plugin.

Journal writes with unknown outcomes (including lost acknowledgements or cancelled owned writes) prevent evidence sealing and are not replayed. Only a store-proven not-committed batch can be retried; a cancelled waiter does not cancel the owned write. Normal close stops evidence producers before draining accepted callbacks.