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 additive context input and parent-owned subagents in 0.12.0, follow the upgrade from 0.11.0. Applications on 0.10.0 should also read the 0.11.0 persistence guide. The earlier 0.10.0 prompt migration remains available for applications upgrading 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.

Select a model with a provider:model string, such as Agent(model="openai-responses:gpt-6.1-terra") (OPENAI_API_KEY). Harnessed configures the PydanticAI adapter automatically. Use an explicit model object when you need generation defaults or native provider configuration; see model configuration.

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.

Use Subagents from harnessed.plugins to delegate explicit tasks to configured specialists in isolated child sessions. It supports both delegate until completion and background tasks with spawn, status, send, wait and cancel. The parent owns child cleanup and routes approvals. See subagents for configuration, limits and recovery.

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. Opt into automatic persistence with Agent(store=JsonSessionStore(root=...)), importing the JSON store from harnessed.integrations.json, or implement SessionStore in your conversation repository. Stored conversations reopen with agent.session(id=...); the default remains in memory. 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.12.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.12.0
File Size Uploaded Provenance Actions
harnessed-0.12.0.tar.gz 210.4 kB With provenance Details for harnessed-0.12.0.tar.gz

Built distribution (wheel)

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

Total release size: 462.4 kB

Release files / harnessed-0.12.0.tar.gz

Download URL harnessed-0.12.0.tar.gz
Size 210.4 kB
Tags Source
SHA-256 checksum
How to use checksums
c8d95339ed39e90bfd59aa55cd1f0d171c60653e23be559e7bc8813e88f51226
BLAKE2b-256 checksum
How to use checksums
272c8767b4a8c8597b7a4d86ae1926f4c4fa4728e106d6668932ebd91d3cfc47
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 4, 2026.

Transparency log

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

Download URL harnessed-0.12.0-py3-none-any.whl
Size 251.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9f634530fa8bf47db555c53d13000ff6c38da95ff4af913112a33f566f316475
BLAKE2b-256 checksum
How to use checksums
65afabc464de60b7cf57af514da25e869286cd4bb40ec02d598c5c7d2b66bd5c
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 4, 2026.

Transparency log

Security for harnessed 0.12.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.12.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 4, 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