Harnessed Tutorial: Build a Tool-Calling Python Agent
Hands-on Harnessed tutorial: install 0.8.0, define plugin tools, run sessions, persist state, test without keys, and add live trend data.

What Harnessed is (and what "harnessed tutorial" actually means)
If you searched for "harnessed tutorial" and found course pages about DeepSeek Harness, STEP AP 242 wiring-harness XML, or dictionary definitions, you are on the wrong build. Harnessed, per its GitHub repository, is a composable Python agent SDK with reversible plugins, tool approvals, and durable session logs. That repository was updated on Oct 26, 2026. The package is published on PyPI as v0.8.0, uploaded Oct 30, 2026 with Trusted Publishing provenance. It is alpha (0.x) software.
So the tutorial you want is hands-on: install the package, define a tool inside a Plugin, hand the plugin to an Agent, run a session, carry the conversation forward with explicit state, and test tool calls without credentials. This walkthrough covers exactly that loop, and then shows how to feed a real-time trend feed into a tool call using TrendsAGI's REST API and Python SDK. Before you copy any snippet, pin the version and read the Harnessed README for your build, because names and session formats are still moving.
Scope check: what is verified about Harnessed and what to confirm yourself
Verified facts, attributable to github.com and the PyPI listing: Harnessed is a real, actively published Python agent SDK. Its description is composable, built from reversible plugins, and it owns the loop, tool policies and explicit conversation state. It ships optional provider integrations through PydanticAI's Direct API, and an [openai] extra appears in install guidance. The repository was updated Oct 26, 2026; the PyPI release is 0.8.0, dated Oct 30, 2026.
Things to confirm in Harnessed's own documentation rather than trust from any tutorial, including this one:
- Exact supported Python and Pydantic version ranges. A constrained range is reported in project material, but verify the classifiers on the version you install.
- Which model providers are enumerated beyond the
deepseek:deepseek-chatexample and the[openai]extra. No full provider list was verified. - Whether the GitHub repository's alpha version line (0.1.x) and PyPI's 0.8.0 are the same release line. That is not confirmed.
- Pricing, plans, usage limits, support channels, benchmarks and adoption data. None of these are shown on the pages reviewed. There is no pricing page.
- Release notes, compatibility policy and full session documentation. They are referenced in project material but were not included in what was read.
Treat everything below as a starting shape, not a frozen contract. The plugin and lifecycle names come from Harnessed's own descriptions; the exact signatures must be checked against your installed version.
Install and pin before you write a line of agent code
Because this is 0.x software and minor releases can break session formats, install with a pinned version and keep a lockfile. Create an isolated environment so a later upgrade is a deliberate act.
python -m venv .venv
source .venv/bin/activate
pip install "harnessed==0.8.0"
If your first provider is routed through the OpenAI integration, install the extra instead of the bare package:
pip install "harnessed[openai]==0.8.0"
Keep your provider credentials in environment variables, never in source. This is the same discipline you will use for the TrendsAGI key later:
export OPENAI_API_KEY="..."
Set the minimum supported Python interpreter reported for the release you installed. Project material indicates Python 3.12 through 3.14; confirm this against the classifiers on PyPI for 0.8.0 before committing to it in CI.
Step 1: define a tool inside a Plugin
The unit of reuse in Harnessed is the plugin, and plugins are described as reversible: they can be added and removed from an agent without rewriting your application. A tool is declared with a decorator inside that plugin. The structure below is representative of the pattern; confirm the import paths and decorator signature in the README for 0.8.0.
from harnessed import Agent, Plugin, tool
plugin = Plugin(name="trend_tools")
@plugin.tool
def top_trends(category: str, limit: int = 5) -> list[dict]:
"""Return the highest-velocity topics in a category."""
return fetch_trends(category=category, limit=limit)
The docstring is not decoration. When the agent is asked to choose a tool, that description is what distinguishes top_trends from any other function in the plugin. Write it as an instruction to the caller: what the tool returns, and what the arguments mean.
Step 2: create the Agent and attach the plugin
The Agent owns the loop and the tool policies; your code supplies tools and state. Attach the plugin and construct the agent:
agent = Agent(
instructions="You help marketing teams interpret live trend data.",
plugins=[plugin],
)
Provider configuration, where a model name such as deepseek:deepseek-chat is supplied, is part of agent construction in project examples. The precise keyword and the full list of provider strings are exactly the kind of detail to confirm in the Harnessed docs for the version you pinned, because it is the most likely thing to change between minor releases.
The lifecycle concepts you will meet if you read further into the project material are plugin_validation, needs=(...) suspension and rebuild, Inject[T], provides=True, and resolution through agent.plugins. These are described in reference material but rarely demonstrated. Do not design around them from a blog post; read the source for your version, then write one small test per lifecycle hook so an upgrade tells you immediately when behavior shifts.
Step 3: run a session and understand what comes back
A session is one execution of the agent loop. The return value is your handle on what happened: the assistant's response plus the record of tool calls the loop made. Log the full structure once during development rather than guessing at field names.
session = agent.session()
result = session.run("Which technology topics are accelerating right now?")
print(result)
The repository describes durable session logs and tool approvals, which matters for two reasons: tool calls are auditable after the fact, and a policy layer can intervene before a side-effecting tool executes. If your tools only read data, approvals are overhead; if any tool writes, decide the approval policy before you expose the agent to users.
Step 4: persist and resume with explicit conversation state
Harnessed makes conversation state explicit and portable. That means your application owns persistence, not the SDK. The benefit is that session state can live in the same database as the rest of your product, survive process restarts, and be inspected during a support incident. The cost is that you must implement storage, retrieval, and a retention policy.
# Pseudocode shape: export after a run, import before the next one.
state = session.export_state()
repository.save(session_id, state)
state = repository.load(session_id)
session = agent.session(state=state)
result = session.run("Compare that with last week.")
The export and import names above are placeholders: reference material shows state export and import as a fragment with a placeholder repository rather than a complete example. Confirm the actual method names in the docs, then wrap them in a repository class of your own. Session compaction, live controls and independent continuation branches are also referenced but not taught in the pages available, so plan to read the comments in the package source.
Step 5: test tool calls with no credentials
This is the strongest practical reason to adopt a plugin-based SDK: you can assert that the agent selected the right tool with the right arguments, without any provider API key and without network access. The project exposes a scripted model for this purpose. The test shape below follows the names in project material; verify the class and assertion names against 0.8.0.
import pytest
from harnessed.testing import ScriptedModel
@pytest.mark.asyncio
async def test_agent_selects_trend_tool():
scripted = ScriptedModel(
tool_calls=[{"name": "top_trends", "arguments": {"category": "Technology", "limit": 5}}]
)
agent = Agent(instructions="...", plugins=[plugin], model=scripted)
session = agent.session()
session.run("Show me technology trends.")
scripted.assert_tool_called("top_trends")
Even if the exact helper signature changes, the principle holds: replace the model with a script, run the loop, and assert on the tool call record. That kind of test belongs in CI and it is the difference between an agent you can refactor and one you can only hope works.
Feeding real-time trend data into a Harnessed tool
Harnessed supplies the loop, the plugin boundary and the state handling. It does not supply live topic data, so the useful demonstration is a tool whose body calls a real measured feed. TrendsAGI detects topics gaining attention on social platforms and measures velocity as posts per hour from stored snapshots. That is a measured snapshot rate, not a forecast and not a unique-user count. Recommendation confidence in the platform measures evidence completeness, not probability of success. State that plainly inside the tool docstring so the agent does not overclaim in its answers.
Call the REST API with Python's standard library, keeping the key in an environment variable:
import os, json, urllib.parse, urllib.request
API_KEY = os.environ["TRENDSAGI_API_KEY"]
BASE = "https://api.trendsagi.com"
def _get(path: str, params: dict | None = None) -> dict:
url = BASE + path
if params:
url += "?" + urllib.parse.urlencode(params)
request = urllib.request.Request(url, headers={"X-API-Key": API_KEY})
with urllib.request.urlopen(request, timeout=30) as response:
return json.loads(response.read().decode("utf-8"))
def fetch_trends(category: str, limit: int = 5) -> list[dict]:
"""Measured snapshot velocity, not a forecast."""
payload = _get("/api/trends", {
"category": category,
"sort_by": "velocity",
"limit": limit,
})
return payload.get("trends", [])
def fetch_insights(trend_id: int) -> dict:
return _get(f"/api/trends/{trend_id}/ai-insights")
Add a second tool that drills into one topic, and give it a docstring that tells the agent when to use it:
@plugin.tool
def explain_trend(trend_id: int) -> dict:
"""Return an AI summary, key themes and sentiment for one trend id."""
return fetch_insights(trend_id)
Two tools with distinct jobs are usually better than one tool with a mode flag, because the model can choose between them and your test suite can assert on each choice separately. Create the API key in your TrendsAGI account profile and store it as TRENDSAGI_API_KEY locally and as a deployment secret in CI.
The same tools via the TrendsAGI Python SDK and agent tool wrappers
If you would rather not hand-write HTTP, use the published Python SDK (0.12.0 or later) inside the tool body:
import os
from trendsagi import TrendsAGIClient
client = TrendsAGIClient(api_key=os.environ["TRENDSAGI_API_KEY"])
trends = client.get_trends(category="Technology", sort_by="velocity", limit=5)
trend_id = trends["trends"][0]["id"]
insights = client.get_ai_insights(trend_id)
analytics = client.get_trend_analytics(trend_id, period="7d")
recommendations = client.get_recommendations(limit=10, priority="high")
For an agent that already has its own tool registry, the SDK ships a read-only tool layer that maps to the same data. Wrap it behind a Harnessed plugin if Harnessed owns your loop, or expose it directly if you are only borrowing the data:
from trendsagi import TrendsAGIClient, TrendsAGITools
tools = TrendsAGITools(TrendsAGIClient(api_key=os.environ["TRENDSAGI_API_KEY"]))
outcome = tools.call("list_trends", {"category": "Technology"})
if "error" in outcome:
print(outcome["error"], outcome.get("detail"))
else:
first = outcome["result"]["trends"][0]
print(first["id"], first["volume"])
Note the failure contract: a tool call returns {"result": ...} on success and {"error": ..., "detail": ...} on failure rather than raising. Handle both branches, because inside a Harnessed tool an unhandled exception may be reported to the agent as a tool failure with little diagnostic detail. The same wrappers are available to Claude, OpenAI, LangChain and MCP clients, and there is a hosted read-only MCP server at https://api.trendsagi.com/mcp if you later want to swap the SDK call for a connection instead. Those are alternatives, not replacements; the tutorial above stands on its own without any of them.
Harnessed versus other Python agent approaches
The decision comes down to who owns the loop.
| Approach | Who owns the loop and tool policy | Best fit |
|---|---|---|
| Harnessed | The SDK, configured through plugins and policies | You want reversible plugins, tool approvals, and portable state your app stores |
| Hand-rolled loop | Your code, entirely | Unusual control flow, or you need full ownership from day one |
| Provider SDK only | The provider's conventions | Single-provider prototypes with no policy or persistence layer |
| TrendsAGI tools + a framework you already run | Your existing framework | You already have an agent stack and only need measured trend data |
Harnessed's honest trade-offs, per its own project material: it is alpha, breaking changes are plausible in minor releases, the supported interpreter range is narrow, and persistence is your job. In exchange you get a plugin boundary that makes tools testable without credentials and state you can move between systems.
A reproducible project layout
harnessed-trends/
pyproject.toml
.env.example
agent/
__init__.py
plugins.py
client.py
tests/
test_tool_selection.py
Put the HTTP client in client.py, the two tools in plugins.py, the scripted test in tests/, and pin harnessed==0.8.0 in pyproject.toml. Keep TRENDSAGI_API_KEY out of the repository and out of the test suite. When you upgrade Harnessed, run the scripted tests first; if tool names or session fields moved, they fail before any credential is spent.
Frequently asked questions
How do I install Harnessed and which Python and Pydantic versions does it support?
Install with pip install "harnessed==0.8.0", adding the [openai] extra if you route through the OpenAI integration. Project material indicates Python 3.12 through 3.14, but confirm both the Python and Pydantic ranges against the classifiers on the 0.8.0 PyPI page, since no compatibility policy was included in the available documentation.
How do I define a tool inside a Plugin and wire it into an Agent?
Build a Plugin, decorate a typed function with the tool decorator using a clear docstring, then pass plugins=[plugin] when you construct the Agent. Confirm the decorator and constructor signatures in the README for your pinned version, because those interfaces are subject to alpha churn.
How does the Agent own the loop, and what does session.run return?
The Agent owns the execution loop and the tool policies, so your code supplies tools and state while the SDK decides when to call them. session.run(...) returns the run's result including the record of tool calls; log the full object once on your installed version rather than assuming field names.
How do I persist and resume a conversation with SessionState, and who owns storage?
After a run, export the state and save it wherever your application keeps durable data; before the next run, load it and start a session from that state. Your application owns storage and retention, not the SDK. The export and import method names were not verified in complete form, so check them in the source for 0.8.0.
How do I test tools without credentials using ScriptedModel?
Replace the model with a scripted sequence of tool calls, run one session, and assert that the expected tool name was called with the expected arguments. No API key and no network connection are required. Verify the exact class and assertion names against your installed version.
What changes to expect in the 0.x alpha before 1.0?
Expect interface and session-format changes across minor releases, which is normal before 1.0 and the main reason to pin versions and keep credential-free tests. Release notes and a written compatibility policy were referenced but not available in the pages reviewed, so there is no published guarantee of stability.
Is Harnessed the same thing as DeepSeek Harness or a wiring-harness XML format?
No. Harnessed, per its GitHub repository, is a composable Python agent SDK with reversible plugins, tool approvals and durable session logs, published on PyPI as 0.8.0. Search results mixing it with a DeepSeek Harness course, STEP AP 242 harness XML or a dictionary definition are describing unrelated things.
How we researched this
Prepared by TrendsAGI Research. Facts checked on 30 October 2026 against the sources listed below.
We read the official Harnessed documentation listed below. The TrendsAGI steps and code use its documented REST API and Python SDK; confirm the Harnessed side against its current documentation, since third-party products change.
| Claim | Applies to | Source |
|---|---|---|
| The GitHub repository describes Harnessed as a composable Python agent SDK with reversible plugins, tool approvals, and durable session logs, and was updated Oct 26, 2026. | All plans | github.com |
Sources
- GitHub - mathisarends/harnessed: Composable Python agent SDK with reversible plugins, tool approvals, and durable session logs. — github.com
- dsh-lark-bot — Tools plugin for DeepSeek Harness (dshbase) — dshbase.com
- 12 节课从零搭出完整 Harness,附课程和模板 (Tencent Cloud) — cloud.tencent.com.cn