Skip to content

0043 — Plugin consumption SDK + workflows as the first opt-in core extraction

  • Status: Accepted (#791)
  • Date: 2026-06-09
  • Builds on: ADR 0001 (extensibility), 0002 (workflows), 0026/0027 (plugins), 0038 (two-mode plugin UI + src/ext seam), 0009 (control stack)

Context

Plugins so far only contribute to the host — PluginRegistry.register_* adds tools, routers, recipe dirs, goal verifiers, middleware. None had to consume deep core capability. Workflows (ADR 0002) is the first feature where that asymmetry bites: it's a real subsystem (engine + registry + tools + an operator API + the Studio surface) that we want out of the default build (lean core, like GitHub → plugin), but its engine runs each step as a subagent — so as a plugin it must call back into core's subagent executor. There was no stable way to do that without importing graph.agent internals, which would couple the plugin to core's refactors.

Two needs, then: (1) a stable consumption surface plugins call into core; (2) prove it by extracting workflows onto it and shipping workflows off-by-default.

Decision

1. A plugin SDK with two explicit halves.

  • ContributionPluginRegistry.register_* (unchanged): what a plugin adds.
  • Consumption — new graph/sdk.py: what a plugin calls back into core. Plugins import from graph.sdk import …, never graph.agent / runtime.state internals, so core can refactor underneath them.

v1 is deliberately tiny — only what workflows needs: run_subagent(subagent_type, prompt, …) (wraps the existing run_manual_subagent, pulls config/stores from runtime state), subagent_types(), and config(). Grow it deliberately as more plugins tap core; this is the seam we lean on going forward.

Grown since: complete(prompt, *, system=None, model_name=None) — a bare LLM completion (no tools, no agent loop, no persona), distinct from run_subagent's full tool-using subagent. The clean primitive for a plugin that just needs the model to answer a prompt — first consumer: the artifact plugin's interactive window.protoArtifact.ask() bridge (artifacts call back to the agent, the window.claude.complete analog).

spawn_background(prompt, *, subagent_type, origin_session, label=None) + background_status(task_id) (#1635) — the background channel: spawn a detached subagent job (ADR 0050) that rides the full ADR 0070 results pipeline (push-resume nudge, KB-indexed report, report card), and poll its jobs-store row in between. Closes the hole where a plugin with campaign-scale work had to reach into STATE.background_mgr directly. First consumer: spacetraders exploration campaigns.

react_on(topic, *, prompt, job_id, session=…, debounce_s=0) (#1633) — reactive-rule sugar over registry.on + run_in_session (ADR 0039): prompt-from-payload (None/empty skips the event), idempotent replace via job_id, thread-safe trailing-edge debounce (a burst coalesces into ONE turn; the last event's prompt wins). Returns an unsubscribe fn. First consumer: spacetraders engine-event → turn rules.

knowledge_search(query, *, k=5, domain=None, epoch=None) / knowledge_add(content, *, domain="general", heading=None, epoch=None) (#1089) + knowledge_purge(domain, *, before=None) (#1634) — the knowledge channel and its lifecycle half. Add-and-search alone let a long-running plugin's knowledge go actively wrong with no way to retire it (spacetraders: weekly universe wipes → recalled routes reference dead markets). knowledge_purge hard-deletes a domain (optionally only rows older than an ISO timestamp), consistently across every index (rows, FTS, vectors; layered = private tier only — the commons stays curated); the optional epoch tag scopes a chunk to the era it was learned in, and an epoch-filtered search excludes other eras and untagged chunks from both rankings — a wipe becomes a new tag, not a delete. First consumer: spacetraders route/lesson memory.

2. Workflows becomes an opt-in plugin (plugins/workflows, enabled: false).

  • The engine + registry move into the plugin (standalone — the engine was already decoupled from the runner via an injected run_step callback). It injects sdk.run_subagent as that callback — the first real SDK consumer.
  • Tools (run_workflow, save_workflow) register via register_tools; the operator API is the plugin's own /api/plugins/workflows router (validation errors → 400).
  • The plugin publishes STATE.workflow_registry + STATE.workflow_run, so the core chat /<recipe> slash-command keeps working by inversion — it reads those (both None when the plugin is off) instead of importing workflows.
  • Core is fully stripped: no run_manual_workflow, no _build_task_tools workflow branch, no _build_workflow_registry, no /api/workflows, no workflows_enabled config; graph/workflows/ and the bundled recipes are deleted (recipes now ship in the plugin).

3. The Studio surface stays native React, via the src/ext seam (NOT an iframe).

src/ext/workflows.tsx calls registerSurface(...); ExtSurface gains requiresPlugin, so the rail item is hidden and the surface unreachable unless the plugin is enabled. No iframe rewrite — src/ext (ADR 0038 D3) already hosts trusted in-process React. The surface JS is bundled but inert when off ("not active by default", not "zero JS").

Consequences

  • Lean core: the workflow engine/tools/recipes/API don't load unless the plugin is enabled. Default build is smaller; workflows is opt-in.
  • The SDK is the durable artifact. It establishes the contribution-vs-consumption contract; future "plugins tapping core" reuse graph/sdk.py rather than reaching in.
  • Studio = Workflows (ADR 0020) is now a plugin-contributed, gated surface — the "control stack" (ADR 0009) altitude for workflows lives in the plugin.
  • Tradeoff: the chat slash-command + the bundled-but-gated surface keep a thin core touch-point (STATE.workflow_run, requiresPlugin). Truly-zero-core would mean a plugin-contributed slash-command hook + an iframe surface — deferred; the inversion is clean enough and preserves the UX.
  • Cross-plugin recipes (ADR 0027) still work for plugins loaded before workflows; a recipe dir from a later-loading plugin is a known edge (rare) — revisit with lazy registry build if it bites.

Part of the protoLabs autonomous development studio.