Skip to main content

Harnessed

PyPI version Python versions CI status License: MIT Status: Alpha

A composable Python agent SDK. Keep tools, instructions and event hooks together in plugins, combine them into reusable kits, and stream typed events from a live session. Harnessed owns the agent loop and plugin lifetimes.

Python 3.12-3.14, Pydantic v2. Optional provider integrations use PydanticAI's Direct API.

Harnessed 0.x is alpha software. APIs and persisted session formats may change in minor releases before 1.0. See the release notes and compatibility policy. For the breaking prompt changes in 0.10.0, follow the upgrade from 0.9.0.

Build an agent

uv add "harnessed[openai]"
import asyncio

from harnessed import Agent, Instructions, Plugin, PluginOrder, SystemPrompt, tool
from harnessed.events import ToolCallFinishedEvent
from harnessed.kernel import hook


class Calculator(Plugin):
    def instructions(self) -> SystemPrompt:
        return SystemPrompt("Use add for arithmetic instead of calculating mentally.")

    @tool(readonly=True)
    async def add(self, a: int, b: int) -> int:
        """Add two integers."""
        return a + b

    @hook(ToolCallFinishedEvent)
    def on_result(self, event: ToolCallFinishedEvent) -> None:
        print(f"{event.call.name}: {event.result.status}")


calculator = Calculator()
agent = Agent(
    model="deepseek:deepseek-chat",  # DEEPSEEK_API_KEY
    plugins=[calculator],
    instructions="You are a helpful assistant. Keep answers concise.",
    prompt=SystemPrompt.from_plugins(order=PluginOrder.AS_LISTED),
)


async def main() -> None:
    result = await agent.run("What is 19 + 23?")
    print(result.text)


asyncio.run(main())

One plugin colocates the tool, its model-facing instructions and its event hook. @tool derives the input schema from Python types; the method docstring describes the tool. @hook observes the selected event type across the session. SystemPrompt.from_plugins(order=PluginOrder.AS_LISTED) explicitly collects complete instruction blocks in resolved plugin-list order.

Agent is reusable configuration. agent.run() opens a session for one turn; use agent.session() when several turns should share a conversation.

Stream typed events

Using the agent above, handle the events your application needs with ordinary Python class patterns:

from harnessed.events import (
    TextDeltaEvent,
    ToolCallFinishedEvent,
    ToolCallRequestedEvent,
    ToolProgressEvent,
    TurnFinishedEvent,
)


async def conversation() -> None:
    async with agent.session() as session:
        async for event in session.stream("What is 19 + 23?"):
            match event:
                case TextDeltaEvent(text=text):
                    print(text, end="", flush=True)
                case ToolCallRequestedEvent(call=call):
                    print(f"\nCalling {call.name}")
                case ToolProgressEvent(text=text):
                    print(f"\n{text}")
                case ToolCallFinishedEvent(call=call, result=result):
                    print(f"\n{call.name}: {result.status}")
                case TurnFinishedEvent(result=result):
                    print(f"\nFinished: {result.stop_reason}")

        follow_up = await session.run("Add 8 to that result.")
        print(follow_up.text)

Events are immutable typed values with stable tags such as text.delta and tool.call.finished. The stream includes:

Activity Events
Generated text and reasoning TextDeltaEvent, ReasoningDeltaEvent
Tool call previews ToolCallGenerationStartedEvent, ToolArgumentsDeltaEvent
Tool calls and progress ToolCallRequestedEvent, ToolProgressEvent, ToolCallFinishedEvent
Approval questions ApprovalNeededEvent
Model requests and retries ModelRequestedEvent, ModelRequestFailedEvent, ModelRetryScheduledEvent
Context compaction CompactionStartedEvent, CompactionAppliedEvent, CompactionFinishedEvent
Large tool-output artifacts SpillCreatedEvent, SpillExpiredEvent
Session status and turn result SessionStatusChangedEvent, TurnFinishedEvent

ToolCallFinishedEvent carries the final tool result, including denied or interrupted calls. TurnFinishedEvent carries the assembled run result on normal stream completion. Plugins can also hook audit events such as StepEndedEvent and TurnEndedEvent, or declare their own event types. See the event reference for all events and delivery surfaces, and streaming for approval handling and stream cleanup.

Compose capabilities with Kit

Bundle library defaults, nest kits, and replace individual policies at the application boundary:

from harnessed import Agent, Kit, PluginOrder, SystemPrompt
from harnessed.plugins import AgentLoop, Compaction, Filesystem, Shell


def workspace_kit(root: str) -> Kit:
    return Kit(
        "workspace",
        [
            Filesystem(root),
            Shell(root),
            AgentLoop(max_steps=30),
            Compaction(keep_steps=4),
        ],
        prompt=SystemPrompt.from_plugins(order=PluginOrder.AS_LISTED),
    )


workspace = workspace_kit(".").without(Shell)
agent = Agent(
    model="deepseek:deepseek-chat",
    plugins=[workspace, AgentLoop(max_steps=10)],
    instructions="Help the user understand this codebase. Cite file paths in your answer.",
)

print(agent.describe())

without(Shell) removes that capability from the kit. The direct AgentLoop replaces the kit's loop through its loop slot; custom plugins can declare slots with class MyPolicy(Plugin, slot="my.policy"). Unslotted plugins are additive. SystemPrompt.from_plugins(order=PluginOrder.AS_LISTED) follows the selected plugins, and agent.describe() shows their origins, replacements and exclusions before any session starts.

Tools, approvals, output limits, compaction and tracing all compose through plugins. See plugin composition for nested-kit precedence and lifecycle rules.

Inject services into tools

Separate a backend from the capabilities that use it. Service publishes an implementation; Inject[T] supplies it without exposing it as a model argument:

from pathlib import PurePath

from harnessed import Agent, Plugin, ToolContext, tool
from harnessed.kernel import Inject
from harnessed.plugins import Service
from harnessed.plugins.filesystem import FileSystemBackend, LocalFileSystem


class ProjectDocs(Plugin, needs=(FileSystemBackend,)):
    @tool(readonly=True)
    async def read_document(
        self, path: str, *, files: Inject[FileSystemBackend], ctx: ToolContext
    ) -> str:
        """Read up to 4,000 characters from a workspace document."""
        await ctx.progress(f"Reading {path}")
        return await files.read(PurePath(path), limit=4_000)


agent = Agent(
    model="deepseek:deepseek-chat",
    plugins=[Service(FileSystemBackend, LocalFileSystem(".")), ProjectDocs()],
)

The model supplies only path. ToolContext provides per-call progress and cancellation; needs=(FileSystemBackend,) ties the plugin's lifetime to that service. Removing the provider suspends its dependent tools; restoring it creates a fresh activation. Backends implement ordinary Python protocols, so the same tools can use a local, remote or sandboxed implementation.

Add capabilities for part of a session

Using Calculator from the first example, mount its tools, instructions and hooks for a bounded scope:

from harnessed import Agent, SectionDelivery


async def temporary_tools() -> None:
    agent = Agent(model="deepseek:deepseek-chat")
    async with agent.session() as session:
        async with await session.use(Calculator(), section_delivery=SectionDelivery.CONTEXT):
            result = await session.run("What is 19 + 23?")
            print(result.text)

Leaving the inner scope removes the tool, hook and mount-owned guidance. SectionDelivery.CONTEXT delivers late instructions as active context; the session's system prompt stays fixed. Each activation has its own lifetime and can keep mutable data in PluginState.

Sessions also expose named context, steering, pause/resume and immutable SessionState snapshots for application-owned persistence. See sessions and context for those controls.

Read by topic

Start with the documentation index or the SDK reference.

The standalone harnessed.kernel uses only the standard library. Optional features live under harnessed.plugins; providers live under harnessed.models.

Inspired by DeepSeek Harness and Cordis.

Released under the MIT License.

Metadata

Release files for harnessed 0.10.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for harnessed 0.10.0
File Size Uploaded Provenance Actions
harnessed-0.10.0.tar.gz 187.2 kB With provenance Details for harnessed-0.10.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for harnessed 0.10.0
File Interpreter ABI Platform Actions
harnessed-0.10.0-py3-none-any.whl Python 3 none any Details for harnessed-0.10.0-py3-none-any.whl

Total release size: 412.0 kB

Release files / harnessed-0.10.0.tar.gz

Download URL harnessed-0.10.0.tar.gz
Size 187.2 kB
Tags Source
SHA-256 checksum
How to use checksums
8a0a9798bde1bb76550266ee487072962279c33fd9b372b0ff719988e25beba2
BLAKE2b-256 checksum
How to use checksums
5c5673874f5013debd7eb2a35bedbbbd92388e0904ea3ef6d752691474ab3776
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

Release files / harnessed-0.10.0-py3-none-any.whl

Download URL harnessed-0.10.0-py3-none-any.whl
Size 224.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2671d72e1b663813557005fb21b5f6ad14006c7ce55e90440850618b9da7f439
BLAKE2b-256 checksum
How to use checksums
680b31b0dc1e417998343ae48123763f20dc1ae5eb3999ce5e5f68b00b71ceeb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

Security for harnessed 0.10.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for harnessed-0.10.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page