DeepSeek Harness: Everything-is-a-Plugin Developer Preview

SitePoint TeamPublished inAI·Programming·
August 15, 2026
The AI briefing for Developers
Stay up to date with AI tools, model releases, and developer workflows that matter.
Weekly. Free. One click to leave.
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
- Premium Results
- Publish articles on SitePoint
- Daily curated jobs
- Learning Paths
- Discounts to dev tools
7 Day Free Trial. Cancel Anytime.
The AI agent framework space has grown dense. LangChain, CrewAI, AutoGen, and several other alternatives compete to define how developers build, compose, and deploy AI-powered workflows. DeepSeek Harness joins these frameworks with a distinct architectural bet: treat every component as a swappable plugin.
Table of Contents
Available as an MIT-licensed developer preview at https://github.com/<org>/deepseek-harness (confirm the current release tag before installing), DeepSeek Harness ships an orchestration runtime that handles only lifecycle management, plugin resolution, and context propagation. Model providers, memory backends, tool integrations, routing strategies, output parsers, and even the orchestration logic itself are all plugins conforming to a universal contract. This tutorial walks through installation, building a first agent with plugins, creating a custom plugin from scratch, and deploying a working pipeline.
What Is DeepSeek Harness?
Architecture Overview: Everything Is a Plugin
The core runtime in DeepSeek Harness is deliberately minimal. It acts as an orchestration shell responsible for three things: lifecycle management, plugin resolution, and context propagation. Every capability of any agent built on Harness comes from its plugins.
Every component is a plugin. Model providers, memory backends, web search tools, routing strategies, output parsers, control flow gates: the framework draws no distinction between them. A plugin registry handles discovery, dependency resolution, and lifecycle transitions for every registered component.
Every plugin implements the same interface, registers through the same API, and communicates through the same shared context object. Swapping a vector store for a different provider follows the same registration and configuration process as swapping a model provider or a routing strategy.
This differs from how existing frameworks structure their abstractions. LangChain (as of its pre-LCEL architecture, prior to the Runnable-based LangChain Expression Language introduced in 2023) distinguishes between tools, chains, agents, and memory as separate abstractions with different interfaces. Harness collapses these into a single plugin contract. Every plugin implements the same interface, registers through the same API, and communicates through the same shared context object. Swapping a vector store for a different provider follows the same registration and configuration process as swapping a model provider or a routing strategy.
Key Design Principles
Four principles guide the architecture. Agents are assembled from discrete plugins rather than subclassed from base agent types, favoring composability over inheritance. Plugins can be replaced without redefining the agent, though the runtime swap API is not yet demonstrated in this developer preview (see Roadmap). Every plugin declares typed interfaces for its inputs, outputs, and capabilities through schema-driven contracts. No plugin is privileged, including the DeepSeek model provider itself, enforcing zero vendor lock-in by design.
project:name: my-first-agentversion: 0.1.0plugins:model:provider: deepseek-v3api_key: ${DEEPSEEK_API_KEY}memory:provider: conversation-buffermax_turns:50tools:-provider: web-searchengine: duckduckgo-provider: file-readerallowed_extensions:[".txt",".md",".pdf"]router:provider: intent-classifierfallback: defaultoutput:provider: markdown-formatterThis configuration file illustrates the plugin-centric architecture. Every section under plugins declares a component by provider name, and the runtime resolves each to a registered plugin. Model, memory, tools, router, and output formatter all occupy the same structural level because they all share the same underlying plugin contract.
Getting Started: Installation and Project Setup
Prerequisites
Python 3.10 through 3.12 is required for the developer preview. Python 3.13 compatibility is unverified. Pinned dependency versions are available in the repository’s requirements.txt. You need an API key for at least one supported model provider. DeepSeek’s own API, OpenAI, and Ollama for local inference all work at launch through their respective provider plugins. Familiarity with virtual environments and pip is assumed.
You will also need python-dotenv to load environment variables from the scaffold’s .env file:
pip install python-dotenvInstall and Scaffold
Note: The deepseek-harness package may not yet be published on PyPI. If pip install deepseek-harness fails, install directly from the repository:
pip install git+https://github.com/<org>/deepseek-harness.git@<release-tag>Replace <org> and <release-tag> with values from the repository’s Releases page.
python -m venv harness-envsource harness-env/bin/activatepip install deepseek-harnessharness init my-agent-projectcd my-agent-projectAfter installation, verify the CLI is available with harness --version. If the command is not found, try python -m deepseek_harness init my-agent-project.
harness init generates the following directory structure:
my-agent-project/├── harness.config.yaml├── plugins/│ ├── custom/│ └── __init__.py├── agents/│ └── default_agent.py├── tests/│ └── test_agent.py├── .env└── requirements.txtCustom plugin implementations live in plugins/custom/. Agent definitions in agents/ compose plugins into working pipelines. The separation is intentional: plugins are reusable components, agents are specific compositions of those components.
Building Your First Agent with Plugins
Registering Built-in Plugins
You register every plugin explicitly through the same harness.register() method, regardless of whether the plugin provides model inference, memory, or tooling. Custom (user-defined) plugins require a fourth argument, plugin_class=, passing the class object so the runtime can instantiate it. Built-in plugins omit this argument because they are already registered internally.
import osfrom dotenv import load_dotenvload_dotenv()from deepseek_harness import Harnessapi_key = os.environ.get("DEEPSEEK_API_KEY")ifnot api_key:raise EnvironmentError("DEEPSEEK_API_KEY is not set. ""Add it to your .env file or export it before running.")harness = Harness()harness.register("model","deepseek-v3",{"api_key": api_key,"temperature":0.7,"max_tokens":2048})harness.register("memory","conversation-buffer",{"max_turns":50,"persist":False})harness.register("tool","web-search",{"engine":"duckduckgo","max_results":5})The first argument to register() is the plugin category, the second is the provider name, and the third is a configuration dictionary. The uniformity here is the point: there is no separate add_tool() or set_memory() method. Everything flows through the same registration mechanism.
Composing an Agent Pipeline
Once plugins are registered, you compose an agent by declaring which plugins participate in its pipeline and in what order prompts flow through them.
import osfrom dotenv import load_dotenvload_dotenv()from deepseek_harness import Harness, Agentapi_key = os.environ.get("DEEPSEEK_API_KEY")ifnot api_key:raise EnvironmentError("DEEPSEEK_API_KEY is not set. ""Add it to your .env file or export it before running.")harness = Harness()harness.register("model","deepseek-v3",{"api_key": api_key,"temperature":0.7,"max_tokens":2048})harness.register("memory","conversation-buffer",{"max_turns":50})harness.register("tool","web-search",{"engine":"duckduckgo","max_results":5})harness.register("router","intent-classifier",{"fallback":"default"})harness.register("output","markdown-formatter",{})agent = Agent(harness=harness,pipeline=["router","memory","model","output"],tools=["web-search"])result = agent.run("Summarize the latest Python 3.13 release notes")print(result.content)The pipeline parameter defines the sequence of plugin invocations. When the agent runs, the prompt first hits the router plugin, which classifies intent and sets a routing key in the shared context. This routing key persists through subsequent pipeline steps and is read by the runtime when the model plugin is invoked. Memory injection between routing and model invocation is intentional: conversation history is appended after the routing decision is made, so the model receives the full conversational context at generation time. Finally the output plugin formats the result. Tools like web search are available to the model plugin during invocation but do not occupy a fixed position in the pipeline.
Understanding the Plugin Lifecycle
Every plugin supports three lifecycle hooks: on_register, on_invoke, and on_teardown. on_register fires when the plugin is added to the registry, allowing initialization of connections, caches, or state. on_invoke executes when the plugin is called during pipeline execution. on_teardown runs during cleanup, closing database connections or flushing buffers.
A shared context object carries data between plugins as the pipeline executes. Each plugin can read from and write to this context, which carries the prompt, conversation history, intermediate results, metadata, and any plugin-specific data. This shared context replaces the tightly coupled method calls found in monolithic frameworks.
For error handling, Harness supports fallback plugin patterns. A plugin can declare a fallback provider that the runtime invokes if the primary plugin raises an exception during on_invoke. This allows, for example, a pipeline to fall back from a cloud model provider to a local Ollama instance if the API is unreachable.
Creating a Custom Plugin
The Plugin Contract (Interface)
Every custom plugin must subclass the HarnessPlugin base class, which defines the required methods and metadata declarations.
from deepseek_harness.plugin import HarnessPlugin, PluginMetadata, PluginContextfrom typing import Any, DictclassMyExamplePlugin(HarnessPlugin):"""Illustrative plugin showing the required interface."""@staticmethoddefmetadata()-> PluginMetadata:"""Return plugin metadata: name, version, capabilities, dependencies."""return PluginMetadata(name="my-example",version="0.1.0",capabilities=["example"],dependencies=[])defon_register(self, config: Dict[str, Any])->None:"""Called when the plugin is registered. Initialize resources here."""...defon_invoke(self, context: PluginContext)-> PluginContext:"""Called during pipeline execution. Process and return updated context.context.get(key) retrieves a value from the shared pipeline context.context.set(key, value) writes a value into the shared pipeline context."""...defon_teardown(self)->None:"""Called during cleanup. Release resources here."""...metadata() returns a PluginMetadata object containing the plugin’s name, version string, a list of capabilities it provides, and any dependencies on other plugins. on_invoke receives and returns a PluginContext object, which enforces typed input/output contracts. Because this contract is schema-driven, the runtime validates plugin compatibility at registration time rather than failing at runtime.
Hands-On: Building a Database Query Plugin
Consider a real-world scenario: a plugin that accepts natural language questions, converts them to SQL using the model plugin’s output, queries a SQLite database, and returns structured results.
First, create and seed the database that the plugin will query:
import sqlite3try:conn = sqlite3.connect("company_data.db")conn.execute("""CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY,date TEXT NOT NULL,amount REAL NOT NULL)""")conn.execute("DELETE FROM orders")conn.execute("INSERT INTO orders VALUES (1, '2025-07-15', 250.00)")conn.execute("INSERT INTO orders VALUES (2, '2025-07-20', 175.50)")conn.commit()finally:conn.close()import osimport reimport sqlite3import threadingimport loggingfrom typing import Any, Dictfrom dotenv import load_dotenvload_dotenv()from deepseek_harness.plugin import HarnessPlugin, PluginMetadata, PluginContextlogger = logging.getLogger(__name__)classDatabaseQueryPlugin(HarnessPlugin):"""Plugin that converts natural language to SQL and queries SQLite.WARNING: This plugin executes LLM-generated SQL against a live database.The database is opened in read-only mode and only single SELECT statementsare permitted. For additional production hardening:- Implement a query allowlist beyond the SELECT check shown here.- Never run against a database containing sensitive data withoutadditional safeguards."""def__init__(self):super().__init__()self.connection =Noneself.db_path =Noneself._lock = threading.Lock()self.max_rows =100@staticmethoddefmetadata()-> PluginMetadata:return PluginMetadata(name="sqlite-query",version="0.1.0",capabilities=["database-query","structured-output"],dependencies=["model"])defon_register(self, config: Dict[str, Any])->None:self.db_path = config.get("db_path","data.db")self.max_rows = config.get("max_rows",100)uri =f"file:{self.db_path}?mode=ro"self.connection = sqlite3.connect(uri, uri=True, check_same_thread=False)self.connection.row_factory = sqlite3.Rowself.connection.execute("PRAGMA busy_timeout = 5000")logger.info("sqlite-query: registered with db_path=%s (read-only)", self.db_path)defon_invoke(self, context: PluginContext)-> PluginContext:sql_query = context.get("generated_sql")ifnot sql_query:schema_info = self._get_schema_summary()context.set("system_prompt_append",f"Convert to SQL for this schema:{schema_info}")context.set("sqlite_query.error","No SQL provided; schema hint injected")logger.warning("No generated_sql in context; schema hint appended")return contextstripped = sql_query.strip()ifnot stripped.upper().startswith("SELECT")or";"in stripped:raise ValueError("Only single SELECT statements are permitted. "f"Rejected query:{stripped[:80]}")try:with self._lock:cursor = self.connection.cursor()cursor.execute(stripped)rows =[dict(row)for row in cursor.fetchall()]iflen(rows)> self.max_rows:logger.warning("Result truncated from %d to %d rows",len(rows), self.max_rows)rows = rows[:self.max_rows]context.set("query_results", rows)context.set("result_count",len(rows))context.set("sqlite_query.content",f"Query returned {len(rows)} results:{rows}")logger.info("sqlite-query: executed successfully, %d rows returned",len(rows))except sqlite3.Error as e:logger.error("sqlite-query: database error: %s", e)context.set("sqlite_query.error",f"Database error:{e}")context.set("sqlite_query.content",f"Failed to execute query:{e}")return contextdefon_teardown(self)->None:if self.connection:self.connection.close()logger.info("sqlite-query: connection closed")def_get_schema_summary(self)->str:outer = self.connection.cursor()inner = self.connection.cursor()outer.execute("SELECT name FROM sqlite_master WHERE type='table'")tables =[row[0]for row in outer.fetchall()]schema_parts =[]for table in tables:ifnot re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', table):logger.warning("Skipping table with non-standard name: %r", table)continueinner.execute(f"PRAGMA table_info({table})")columns =[(row[1], row[2])for row in inner.fetchall()]schema_parts.append(f"{table}:{columns}")return "".join(schema_parts)from deepseek_harness import Harness, Agentapi_key = os.environ.get("DEEPSEEK_API_KEY")ifnot api_key:raise EnvironmentError("DEEPSEEK_API_KEY is not set. ""Add it to your .env file or export it before running.")harness = Harness()harness.register("model","deepseek-v3-sql-gen",{"api_key": api_key})harness.register("model","deepseek-v3-synthesizer",{"api_key": api_key})harness.register("memory","conversation-buffer",{"max_turns":20})harness.register("tool","sqlite-query",{"db_path":"company_data.db"}, plugin_class=DatabaseQueryPlugin)agent = Agent(harness=harness,pipeline=["memory","deepseek-v3-sql-gen","sqlite-query","deepseek-v3-synthesizer"],tools=[])result = agent.run("How many orders were placed last month?")print(result.content)The pipeline here routes through the model plugin twice: first to generate SQL from the natural language prompt, then to synthesize the query results into a human-readable response. The database query plugin sits between these two model invocations, executing the generated SQL and placing results into the shared context.
Packaging and Sharing Plugins
Custom plugins can be distributed through a plugin manifest file that declares metadata, dependencies, and compatibility constraints. The manifest supports versioning with semantic version ranges, allowing plugin consumers to pin compatible versions. You can share plugins via Git repositories, publish them as standard pip packages, or submit them to the Harness plugin registry. At developer preview, the registry lists only the built-in plugins. The manifest format is subject to the same instability caveats as the rest of the API; pin the harness-schema-version field in your manifest and re-validate on each preview release.
Plugin Composition Patterns for Real-World Use Cases
Pattern 1: Multi-Model Routing
Why would you send every prompt to the same model? The router plugin enables prompt-level model selection. By configuring an intent classifier as the router, the runtime dispatches different prompt types to different model plugins. Code generation tasks can route to DeepSeek-Coder while general knowledge queries go to DeepSeek-V3. The router plugin inspects the shared context, classifies the prompt, and sets a routing key that the runtime uses to select the appropriate model plugin.
Pattern 2: RAG with Swappable Vector Stores
A retrieval-augmented generation pipeline composes an embedding plugin, a vector store plugin, and a retriever plugin. Because Harness treats all three as plugins under the same contract, switching vector store providers means changing one YAML block (3-5 lines):
plugins:embedding:provider: sentence-transformersmodel: all-MiniLM-L6-v2vector_store:provider: chromacollection: documentspersist_directory: ./chroma_dbretriever:provider: similarity-searchtop_k:5plugins:embedding:provider: sentence-transformersmodel: all-MiniLM-L6-v2vector_store:provider: pineconeindex_name: documentsapi_key: ${PINECONE_API_KEY}host: <your-index-host>.svc.pinecone.ioretriever:provider: similarity-searchtop_k:5The embedding plugin and retriever plugin remain identical. Only the
vector_storeblock changes. Because all three conform to the same plugin contract, the runtime needs no modification. This is the practical payoff of the universal plugin architecture.
Pattern 3: Human-in-the-Loop Approval Gate
An approval plugin can be inserted at any point in the pipeline. It reads the current context, presents it through a configured interface (CLI prompt, webhook, or UI callback), and either allows the pipeline to continue or halts it with a rejection message. The pipeline blocks until the gate receives a response or hits a configurable timeout. Control flow itself is a plugin here. Treating it as such rather than as a framework-level concept means you can add, remove, or reposition approval gates without altering agent code.
Implementation Checklist: Your DeepSeek Harness Starter Kit
- ☐ Install DeepSeek Harness, then verify CLI with
harness --version - ☐ Configure at least one model provider plugin with API credentials loaded from environment variables
- ☐ Register memory and tool plugins (web search, file I/O, or API connector)
- ☐ Compose and test a basic agent pipeline with
agent.run() - ☐ Create a custom plugin subclassing
HarnessPlugin; implement all three lifecycle hooks (register, invoke, teardown) - ☐ Test hot-swapping: replace one plugin without changing agent code (see Roadmap; the runtime swap API is not yet available in this preview)
- ☐ Add error handling with a fallback plugin configuration
- ☐ Package a custom plugin with manifest and version metadata
- ☐ Run end-to-end with logging enabled (
harness = Harness(log_level="DEBUG")orHARNESS_LOG_LEVEL=DEBUG) and review the Harness plugin registry for available community plugins
Current Limitations and What’s Coming Next
Developer Preview Caveats
The maintainers mark the API surface as unstable. Expect breaking changes between preview releases. At launch, only the built-in plugins ship with the framework; community contributions are just beginning. Specific documentation gaps include circular plugin dependencies and multi-agent context sharing, which remain undocumented. For clarification, use the project’s GitHub Issues and Discussions. The team has not published performance benchmarks, so run your own latency and throughput tests for any workload-specific evaluation.
Roadmap Signals
The repository references planned features: a multi-agent orchestration plugin for coordinating multiple agents, streaming and async-first execution modes, and a visual pipeline builder for graphical agent composition. The repository also hints at a plugin marketplace or curated registry, but neither is available yet. These are directional signals, not commitments; consult the repository’s GitHub Issues and Discussions for current status.
Should You Adopt DeepSeek Harness Today?
DeepSeek Harness is best suited for developers building modular AI agents who want to avoid framework lock-in and value the ability to swap any component without restructuring their codebase. It is not ready for production workloads. The API will change, the plugin ecosystem is small, and documentation is incomplete.
The single-contract design breaks from the multi-abstraction pattern that LangChain and CrewAI use (one plugin interface vs. separate Tool, Chain, Memory, and Agent interfaces). Evaluating it now provides a head start on a shift toward single-contract composition in agent development.
Clone the repository at https://github.com/<org>/deepseek-harness (verify the URL in the project README), work through the checklist above, build a custom plugin, and file feedback on GitHub while the design is still malleable.
Sharing our passion for building incredible internet things.


