harnessed 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- harnessed-0.1.0/LICENSE +21 -0
- harnessed-0.1.0/PKG-INFO +123 -0
- harnessed-0.1.0/README.md +92 -0
- harnessed-0.1.0/harnessed/__init__.py +58 -0
- harnessed-0.1.0/harnessed/__init__.pyi +68 -0
- harnessed-0.1.0/harnessed/agent.py +132 -0
- harnessed-0.1.0/harnessed/core/__init__.py +0 -0
- harnessed-0.1.0/harnessed/core/approvals.py +291 -0
- harnessed-0.1.0/harnessed/core/attachments.py +86 -0
- harnessed-0.1.0/harnessed/core/budget.py +49 -0
- harnessed-0.1.0/harnessed/core/buffering.py +49 -0
- harnessed-0.1.0/harnessed/core/compaction.py +417 -0
- harnessed-0.1.0/harnessed/core/errors.py +105 -0
- harnessed-0.1.0/harnessed/core/event_kinds.py +50 -0
- harnessed-0.1.0/harnessed/core/event_registry.py +114 -0
- harnessed-0.1.0/harnessed/core/hooks.py +112 -0
- harnessed-0.1.0/harnessed/core/messages.py +368 -0
- harnessed-0.1.0/harnessed/core/model.py +248 -0
- harnessed-0.1.0/harnessed/core/outputs.py +170 -0
- harnessed-0.1.0/harnessed/core/plugin.py +118 -0
- harnessed-0.1.0/harnessed/core/prompt/__init__.py +0 -0
- harnessed-0.1.0/harnessed/core/prompt/context.py +84 -0
- harnessed-0.1.0/harnessed/core/prompt/instructions.py +19 -0
- harnessed-0.1.0/harnessed/core/prompt/system_prompt.py +42 -0
- harnessed-0.1.0/harnessed/core/prompt/values.py +131 -0
- harnessed-0.1.0/harnessed/core/runtime/__init__.py +0 -0
- harnessed-0.1.0/harnessed/core/runtime/agent_loop.py +71 -0
- harnessed-0.1.0/harnessed/core/runtime/approvals.py +231 -0
- harnessed-0.1.0/harnessed/core/runtime/control.py +278 -0
- harnessed-0.1.0/harnessed/core/runtime/executor.py +209 -0
- harnessed-0.1.0/harnessed/core/runtime/handles.py +47 -0
- harnessed-0.1.0/harnessed/core/runtime/inbox.py +132 -0
- harnessed-0.1.0/harnessed/core/runtime/opener.py +77 -0
- harnessed-0.1.0/harnessed/core/runtime/options.py +71 -0
- harnessed-0.1.0/harnessed/core/runtime/output.py +124 -0
- harnessed-0.1.0/harnessed/core/runtime/respond.py +69 -0
- harnessed-0.1.0/harnessed/core/runtime/result.py +168 -0
- harnessed-0.1.0/harnessed/core/runtime/services.py +38 -0
- harnessed-0.1.0/harnessed/core/runtime/session.py +583 -0
- harnessed-0.1.0/harnessed/core/runtime/stepper.py +162 -0
- harnessed-0.1.0/harnessed/core/runtime/streaming.py +174 -0
- harnessed-0.1.0/harnessed/core/runtime/turns.py +123 -0
- harnessed-0.1.0/harnessed/core/session/__init__.py +0 -0
- harnessed-0.1.0/harnessed/core/session/artifacts.py +81 -0
- harnessed-0.1.0/harnessed/core/session/codec.py +202 -0
- harnessed-0.1.0/harnessed/core/session/control.py +35 -0
- harnessed-0.1.0/harnessed/core/session/events.py +731 -0
- harnessed-0.1.0/harnessed/core/session/files.py +86 -0
- harnessed-0.1.0/harnessed/core/session/history.py +391 -0
- harnessed-0.1.0/harnessed/core/session/inbox.py +59 -0
- harnessed-0.1.0/harnessed/core/session/repository.py +45 -0
- harnessed-0.1.0/harnessed/core/session/session.py +537 -0
- harnessed-0.1.0/harnessed/core/session/store.py +138 -0
- harnessed-0.1.0/harnessed/core/session/turns.py +220 -0
- harnessed-0.1.0/harnessed/core/stream.py +161 -0
- harnessed-0.1.0/harnessed/core/tokens.py +78 -0
- harnessed-0.1.0/harnessed/core/tools/__init__.py +11 -0
- harnessed-0.1.0/harnessed/core/tools/arguments.py +98 -0
- harnessed-0.1.0/harnessed/core/tools/context.py +61 -0
- harnessed-0.1.0/harnessed/core/tools/decorator.py +53 -0
- harnessed-0.1.0/harnessed/core/tools/output_limit.py +46 -0
- harnessed-0.1.0/harnessed/core/tools/policy.py +18 -0
- harnessed-0.1.0/harnessed/core/tools/references.py +68 -0
- harnessed-0.1.0/harnessed/core/tools/registry.py +390 -0
- harnessed-0.1.0/harnessed/core/tools/scheduling.py +93 -0
- harnessed-0.1.0/harnessed/core/tracing.py +33 -0
- harnessed-0.1.0/harnessed/errors.py +45 -0
- harnessed-0.1.0/harnessed/events/__init__.py +53 -0
- harnessed-0.1.0/harnessed/events/approval.py +7 -0
- harnessed-0.1.0/harnessed/events/compaction.py +5 -0
- harnessed-0.1.0/harnessed/events/context.py +6 -0
- harnessed-0.1.0/harnessed/events/inbox.py +13 -0
- harnessed-0.1.0/harnessed/events/message.py +11 -0
- harnessed-0.1.0/harnessed/events/model.py +11 -0
- harnessed-0.1.0/harnessed/events/output.py +7 -0
- harnessed-0.1.0/harnessed/events/reasoning.py +5 -0
- harnessed-0.1.0/harnessed/events/rule.py +7 -0
- harnessed-0.1.0/harnessed/events/session.py +6 -0
- harnessed-0.1.0/harnessed/events/spill.py +5 -0
- harnessed-0.1.0/harnessed/events/step.py +5 -0
- harnessed-0.1.0/harnessed/events/text.py +5 -0
- harnessed-0.1.0/harnessed/events/tool.py +29 -0
- harnessed-0.1.0/harnessed/events/turn.py +6 -0
- harnessed-0.1.0/harnessed/hooks.py +29 -0
- harnessed-0.1.0/harnessed/kernel/__init__.py +55 -0
- harnessed-0.1.0/harnessed/kernel/context.py +344 -0
- harnessed-0.1.0/harnessed/kernel/declarations.py +208 -0
- harnessed-0.1.0/harnessed/kernel/di.py +121 -0
- harnessed-0.1.0/harnessed/kernel/effects.py +45 -0
- harnessed-0.1.0/harnessed/kernel/events.py +206 -0
- harnessed-0.1.0/harnessed/kernel/hooks.py +97 -0
- harnessed-0.1.0/harnessed/kernel/keys.py +20 -0
- harnessed-0.1.0/harnessed/kernel/plugin.py +343 -0
- harnessed-0.1.0/harnessed/kernel/spec.py +21 -0
- harnessed-0.1.0/harnessed/kernel/state.py +40 -0
- harnessed-0.1.0/harnessed/models/__init__.py +58 -0
- harnessed-0.1.0/harnessed/models/pydantic_ai.py +532 -0
- harnessed-0.1.0/harnessed/plugins/__init__.py +56 -0
- harnessed-0.1.0/harnessed/plugins/compaction.py +114 -0
- harnessed-0.1.0/harnessed/plugins/filesystem.py +283 -0
- harnessed-0.1.0/harnessed/plugins/observations.py +31 -0
- harnessed-0.1.0/harnessed/plugins/rules.py +210 -0
- harnessed-0.1.0/harnessed/plugins/sessions.py +449 -0
- harnessed-0.1.0/harnessed/plugins/shell/__init__.py +276 -0
- harnessed-0.1.0/harnessed/plugins/shell/windows.py +132 -0
- harnessed-0.1.0/harnessed/plugins/skills.py +408 -0
- harnessed-0.1.0/harnessed/plugins/spill.py +233 -0
- harnessed-0.1.0/harnessed/py.typed +0 -0
- harnessed-0.1.0/harnessed/testing/__init__.py +17 -0
- harnessed-0.1.0/harnessed/testing/assertions.py +20 -0
- harnessed-0.1.0/harnessed/testing/compaction.py +30 -0
- harnessed-0.1.0/harnessed/testing/fakes.py +184 -0
- harnessed-0.1.0/harnessed/testing/models.py +173 -0
- harnessed-0.1.0/harnessed/testing/pytest_plugin.py +26 -0
- harnessed-0.1.0/harnessed.egg-info/PKG-INFO +123 -0
- harnessed-0.1.0/harnessed.egg-info/SOURCES.txt +124 -0
- harnessed-0.1.0/harnessed.egg-info/dependency_links.txt +1 -0
- harnessed-0.1.0/harnessed.egg-info/requires.txt +13 -0
- harnessed-0.1.0/harnessed.egg-info/top_level.txt +1 -0
- harnessed-0.1.0/pyproject.toml +90 -0
- harnessed-0.1.0/setup.cfg +4 -0
- harnessed-0.1.0/tests/test_app.py +240 -0
- harnessed-0.1.0/tests/test_architecture.py +237 -0
- harnessed-0.1.0/tests/test_composition.py +59 -0
- harnessed-0.1.0/tests/test_default_plugins.py +145 -0
- harnessed-0.1.0/tests/test_examples.py +92 -0
harnessed-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mathisarends
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
harnessed-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: harnessed
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A composable Python agent SDK with reversible plugins and a durable session log.
|
|
5
|
+
Author: mathisarends
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://github.com/mathisarends/harnessed/blob/main/docs/index.md
|
|
8
|
+
Project-URL: Repository, https://github.com/mathisarends/harnessed
|
|
9
|
+
Project-URL: Issues, https://github.com/mathisarends/harnessed/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/mathisarends/harnessed/blob/main/docs/releases.md
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: pydantic<3,>=2.12
|
|
22
|
+
Provides-Extra: openai
|
|
23
|
+
Requires-Dist: pydantic-ai-slim[openai]<3,>=2.63; extra == "openai"
|
|
24
|
+
Provides-Extra: anthropic
|
|
25
|
+
Requires-Dist: pydantic-ai-slim[anthropic]<3,>=2.63; extra == "anthropic"
|
|
26
|
+
Provides-Extra: google
|
|
27
|
+
Requires-Dist: pydantic-ai-slim[google]<3,>=2.63; extra == "google"
|
|
28
|
+
Provides-Extra: skills
|
|
29
|
+
Requires-Dist: pyyaml<7,>=6.0.3; extra == "skills"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# Harnessed
|
|
33
|
+
|
|
34
|
+
A Python agent SDK built from reversible plugins. Harnessed owns the loop,
|
|
35
|
+
tool policies and append-only session log; optional provider integrations use
|
|
36
|
+
PydanticAI's Direct API. Python **3.12-3.14**, Pydantic v2 and modern generics.
|
|
37
|
+
|
|
38
|
+
Harnessed 0.1.x is **alpha software**. APIs and persisted session formats may
|
|
39
|
+
change in minor releases before 1.0. See the
|
|
40
|
+
[release notes and compatibility policy](https://github.com/mathisarends/harnessed/blob/main/docs/releases.md).
|
|
41
|
+
|
|
42
|
+
## Core SDK
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv add "harnessed[openai]"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import asyncio
|
|
50
|
+
from harnessed import Agent, Plugin, tool
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Calculator(Plugin):
|
|
54
|
+
@tool(readonly=True)
|
|
55
|
+
async def add(self, a: int, b: int) -> int:
|
|
56
|
+
"""Add two integers."""
|
|
57
|
+
return a + b
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
agent = Agent(
|
|
61
|
+
model="deepseek:deepseek-chat", # DEEPSEEK_API_KEY
|
|
62
|
+
instructions="Keep answers concise.",
|
|
63
|
+
plugins=[Calculator()],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def main():
|
|
68
|
+
async with agent.session() as session:
|
|
69
|
+
result = await session.run("What is 19 + 23?")
|
|
70
|
+
print(result.text)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
asyncio.run(main())
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`Agent` has three configuration inputs: `model`, `instructions`, and `plugins`.
|
|
77
|
+
It is reusable; each session owns its runtime and plugin lifetimes.
|
|
78
|
+
|
|
79
|
+
- **Composition:** tools, policies and capabilities are plugins. Slots replace
|
|
80
|
+
defaults, and `agent.plugins` shows the resolved composition.
|
|
81
|
+
- **Ownership:** removing a plugin removes its registrations and effects.
|
|
82
|
+
Explicit `needs` suspend and rebuild dependent plugins as services change.
|
|
83
|
+
- **Context:** instructions and plugin guidance are fixed per session; named
|
|
84
|
+
context values and steering supply changes during a run.
|
|
85
|
+
- **History:** immutable events are the source of truth. Persistence, replay,
|
|
86
|
+
forks and control outcomes use the same append-only log.
|
|
87
|
+
|
|
88
|
+
## Test without credentials
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from harnessed.testing import ScriptedModel, assert_tool_called, call, reply
|
|
92
|
+
|
|
93
|
+
model = ScriptedModel([call(Calculator.add, a=19, b=23), reply("42")])
|
|
94
|
+
agent = Agent(model=model, plugins=[Calculator()])
|
|
95
|
+
|
|
96
|
+
# Inside an async function:
|
|
97
|
+
# result = await agent.run("Add 19 and 23")
|
|
98
|
+
# assert_tool_called(result, "add", a=19, b=23)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Read by topic
|
|
102
|
+
|
|
103
|
+
Start with the [documentation index](https://github.com/mathisarends/harnessed/blob/main/docs/index.md) or the
|
|
104
|
+
[SDK reference](https://github.com/mathisarends/harnessed/blob/main/docs/reference/sdk.md).
|
|
105
|
+
|
|
106
|
+
- [Models and provider settings](https://github.com/mathisarends/harnessed/blob/main/docs/guides/models.md)
|
|
107
|
+
- [Tools, hooks and cancellation](https://github.com/mathisarends/harnessed/blob/main/docs/guides/tools.md)
|
|
108
|
+
- [Plugin composition and lifecycle](https://github.com/mathisarends/harnessed/blob/main/docs/guides/plugins.md)
|
|
109
|
+
- [Instructions and changing context](https://github.com/mathisarends/harnessed/blob/main/docs/guides/context.md)
|
|
110
|
+
- [Sessions, controls, persistence and forks](https://github.com/mathisarends/harnessed/blob/main/docs/guides/sessions.md)
|
|
111
|
+
- [Namespaced events and custom extensions](https://github.com/mathisarends/harnessed/blob/main/docs/guides/events.md)
|
|
112
|
+
- [Approvals and headless operation](https://github.com/mathisarends/harnessed/blob/main/docs/guides/approvals.md)
|
|
113
|
+
- [Offline testing](https://github.com/mathisarends/harnessed/blob/main/docs/guides/testing.md)
|
|
114
|
+
- [Executable examples](https://github.com/mathisarends/harnessed/blob/main/docs/guides/examples.md)
|
|
115
|
+
- [Development and CI](https://github.com/mathisarends/harnessed/blob/main/docs/contributing/development.md)
|
|
116
|
+
|
|
117
|
+
The standalone `harnessed.kernel` uses only the standard library. Optional
|
|
118
|
+
features live under `harnessed.plugins`; providers live under `harnessed.models`.
|
|
119
|
+
|
|
120
|
+
Inspired by [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
|
|
121
|
+
and [Cordis](https://github.com/cordiverse/cordis).
|
|
122
|
+
|
|
123
|
+
Released under the [MIT License](https://github.com/mathisarends/harnessed/blob/main/LICENSE).
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Harnessed
|
|
2
|
+
|
|
3
|
+
A Python agent SDK built from reversible plugins. Harnessed owns the loop,
|
|
4
|
+
tool policies and append-only session log; optional provider integrations use
|
|
5
|
+
PydanticAI's Direct API. Python **3.12-3.14**, Pydantic v2 and modern generics.
|
|
6
|
+
|
|
7
|
+
Harnessed 0.1.x is **alpha software**. APIs and persisted session formats may
|
|
8
|
+
change in minor releases before 1.0. See the
|
|
9
|
+
[release notes and compatibility policy](https://github.com/mathisarends/harnessed/blob/main/docs/releases.md).
|
|
10
|
+
|
|
11
|
+
## Core SDK
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv add "harnessed[openai]"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import asyncio
|
|
19
|
+
from harnessed import Agent, Plugin, tool
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Calculator(Plugin):
|
|
23
|
+
@tool(readonly=True)
|
|
24
|
+
async def add(self, a: int, b: int) -> int:
|
|
25
|
+
"""Add two integers."""
|
|
26
|
+
return a + b
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
agent = Agent(
|
|
30
|
+
model="deepseek:deepseek-chat", # DEEPSEEK_API_KEY
|
|
31
|
+
instructions="Keep answers concise.",
|
|
32
|
+
plugins=[Calculator()],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def main():
|
|
37
|
+
async with agent.session() as session:
|
|
38
|
+
result = await session.run("What is 19 + 23?")
|
|
39
|
+
print(result.text)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
asyncio.run(main())
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`Agent` has three configuration inputs: `model`, `instructions`, and `plugins`.
|
|
46
|
+
It is reusable; each session owns its runtime and plugin lifetimes.
|
|
47
|
+
|
|
48
|
+
- **Composition:** tools, policies and capabilities are plugins. Slots replace
|
|
49
|
+
defaults, and `agent.plugins` shows the resolved composition.
|
|
50
|
+
- **Ownership:** removing a plugin removes its registrations and effects.
|
|
51
|
+
Explicit `needs` suspend and rebuild dependent plugins as services change.
|
|
52
|
+
- **Context:** instructions and plugin guidance are fixed per session; named
|
|
53
|
+
context values and steering supply changes during a run.
|
|
54
|
+
- **History:** immutable events are the source of truth. Persistence, replay,
|
|
55
|
+
forks and control outcomes use the same append-only log.
|
|
56
|
+
|
|
57
|
+
## Test without credentials
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from harnessed.testing import ScriptedModel, assert_tool_called, call, reply
|
|
61
|
+
|
|
62
|
+
model = ScriptedModel([call(Calculator.add, a=19, b=23), reply("42")])
|
|
63
|
+
agent = Agent(model=model, plugins=[Calculator()])
|
|
64
|
+
|
|
65
|
+
# Inside an async function:
|
|
66
|
+
# result = await agent.run("Add 19 and 23")
|
|
67
|
+
# assert_tool_called(result, "add", a=19, b=23)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Read by topic
|
|
71
|
+
|
|
72
|
+
Start with the [documentation index](https://github.com/mathisarends/harnessed/blob/main/docs/index.md) or the
|
|
73
|
+
[SDK reference](https://github.com/mathisarends/harnessed/blob/main/docs/reference/sdk.md).
|
|
74
|
+
|
|
75
|
+
- [Models and provider settings](https://github.com/mathisarends/harnessed/blob/main/docs/guides/models.md)
|
|
76
|
+
- [Tools, hooks and cancellation](https://github.com/mathisarends/harnessed/blob/main/docs/guides/tools.md)
|
|
77
|
+
- [Plugin composition and lifecycle](https://github.com/mathisarends/harnessed/blob/main/docs/guides/plugins.md)
|
|
78
|
+
- [Instructions and changing context](https://github.com/mathisarends/harnessed/blob/main/docs/guides/context.md)
|
|
79
|
+
- [Sessions, controls, persistence and forks](https://github.com/mathisarends/harnessed/blob/main/docs/guides/sessions.md)
|
|
80
|
+
- [Namespaced events and custom extensions](https://github.com/mathisarends/harnessed/blob/main/docs/guides/events.md)
|
|
81
|
+
- [Approvals and headless operation](https://github.com/mathisarends/harnessed/blob/main/docs/guides/approvals.md)
|
|
82
|
+
- [Offline testing](https://github.com/mathisarends/harnessed/blob/main/docs/guides/testing.md)
|
|
83
|
+
- [Executable examples](https://github.com/mathisarends/harnessed/blob/main/docs/guides/examples.md)
|
|
84
|
+
- [Development and CI](https://github.com/mathisarends/harnessed/blob/main/docs/contributing/development.md)
|
|
85
|
+
|
|
86
|
+
The standalone `harnessed.kernel` uses only the standard library. Optional
|
|
87
|
+
features live under `harnessed.plugins`; providers live under `harnessed.models`.
|
|
88
|
+
|
|
89
|
+
Inspired by [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
|
|
90
|
+
and [Cordis](https://github.com/cordiverse/cordis).
|
|
91
|
+
|
|
92
|
+
Released under the [MIT License](https://github.com/mathisarends/harnessed/blob/main/LICENSE).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from importlib import import_module
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
logger.addHandler(logging.NullHandler())
|
|
7
|
+
|
|
8
|
+
# Keep the standalone kernel independent of Pydantic and optional provider SDKs.
|
|
9
|
+
_EXPORTS = {
|
|
10
|
+
"Agent": ("harnessed.agent", "Agent"),
|
|
11
|
+
"ApprovalAnswer": ("harnessed.core.messages", "ApprovalAnswer"),
|
|
12
|
+
"ApprovalRequest": ("harnessed.core.session.events", "ApprovalRequested"),
|
|
13
|
+
"Context": ("harnessed.kernel", "Context"),
|
|
14
|
+
"File": ("harnessed.core.attachments", "File"),
|
|
15
|
+
"Image": ("harnessed.core.attachments", "Image"),
|
|
16
|
+
"ImageDetail": ("harnessed.core.messages", "ImageDetail"),
|
|
17
|
+
"Inject": ("harnessed.kernel", "Inject"),
|
|
18
|
+
"Observation": ("harnessed.core.messages", "Observation"),
|
|
19
|
+
"OutputContract": ("harnessed.core.outputs", "OutputContract"),
|
|
20
|
+
"Plugin": ("harnessed.core.plugin", "Plugin"),
|
|
21
|
+
"PluginConfig": ("harnessed.core.plugin", "PluginConfig"),
|
|
22
|
+
"PluginState": ("harnessed.kernel", "PluginState"),
|
|
23
|
+
"PluginConflict": ("harnessed.core.errors", "PluginConflict"),
|
|
24
|
+
"PolicyDecision": ("harnessed.core.messages", "PolicyDecision"),
|
|
25
|
+
"Role": ("harnessed.core.messages", "Role"),
|
|
26
|
+
"RunHandle": ("harnessed.core.runtime.handles", "RunHandle"),
|
|
27
|
+
"RunResult": ("harnessed.core.runtime.result", "RunResult"),
|
|
28
|
+
"Section": ("harnessed.core.prompt.values", "Section"),
|
|
29
|
+
"Session": ("harnessed.core.runtime.session", "Session"),
|
|
30
|
+
"SessionClosed": ("harnessed.core.errors", "SessionClosed"),
|
|
31
|
+
"SessionStatus": ("harnessed.core.runtime.control", "SessionStatus"),
|
|
32
|
+
"StopReason": ("harnessed.core.model", "StopReason"),
|
|
33
|
+
"StreamOverflow": ("harnessed.core.errors", "StreamOverflow"),
|
|
34
|
+
"ToolCall": ("harnessed.core.messages", "ToolCall"),
|
|
35
|
+
"ToolContext": ("harnessed.core.tools.context", "ToolContext"),
|
|
36
|
+
"ToolError": ("harnessed.core.tools.registry", "ToolError"),
|
|
37
|
+
"ToolResult": ("harnessed.core.messages", "ToolResult"),
|
|
38
|
+
"ToolStatus": ("harnessed.core.messages", "ToolStatus"),
|
|
39
|
+
"hook": ("harnessed.kernel", "hook"),
|
|
40
|
+
"plugin": ("harnessed.kernel", "plugin"),
|
|
41
|
+
"tool": ("harnessed.core.tools.decorator", "tool"),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def __getattr__(name: str) -> Any:
|
|
46
|
+
if name not in _EXPORTS:
|
|
47
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
48
|
+
module, attribute = _EXPORTS[name]
|
|
49
|
+
value = getattr(import_module(module), attribute)
|
|
50
|
+
globals()[name] = value
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def __dir__() -> list[str]:
|
|
55
|
+
return sorted(set(globals()) | set(_EXPORTS))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = sorted(_EXPORTS)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from harnessed.agent import Agent as Agent
|
|
2
|
+
from harnessed.core.attachments import File as File
|
|
3
|
+
from harnessed.core.attachments import Image as Image
|
|
4
|
+
from harnessed.core.errors import PluginConflict as PluginConflict
|
|
5
|
+
from harnessed.core.errors import SessionClosed as SessionClosed
|
|
6
|
+
from harnessed.core.errors import StreamOverflow as StreamOverflow
|
|
7
|
+
from harnessed.core.messages import ApprovalAnswer as ApprovalAnswer
|
|
8
|
+
from harnessed.core.messages import ImageDetail as ImageDetail
|
|
9
|
+
from harnessed.core.messages import Observation as Observation
|
|
10
|
+
from harnessed.core.messages import PolicyDecision as PolicyDecision
|
|
11
|
+
from harnessed.core.messages import Role as Role
|
|
12
|
+
from harnessed.core.messages import ToolCall as ToolCall
|
|
13
|
+
from harnessed.core.messages import ToolResult as ToolResult
|
|
14
|
+
from harnessed.core.messages import ToolStatus as ToolStatus
|
|
15
|
+
from harnessed.core.model import StopReason as StopReason
|
|
16
|
+
from harnessed.core.outputs import OutputContract as OutputContract
|
|
17
|
+
from harnessed.core.plugin import Plugin as Plugin
|
|
18
|
+
from harnessed.core.plugin import PluginConfig as PluginConfig
|
|
19
|
+
from harnessed.core.prompt.values import Section as Section
|
|
20
|
+
from harnessed.core.runtime.control import SessionStatus as SessionStatus
|
|
21
|
+
from harnessed.core.runtime.handles import RunHandle as RunHandle
|
|
22
|
+
from harnessed.core.runtime.result import RunResult as RunResult
|
|
23
|
+
from harnessed.core.runtime.session import Session as Session
|
|
24
|
+
from harnessed.core.session.events import ApprovalRequested as ApprovalRequest
|
|
25
|
+
from harnessed.core.tools.context import ToolContext as ToolContext
|
|
26
|
+
from harnessed.core.tools.decorator import tool as tool
|
|
27
|
+
from harnessed.core.tools.registry import ToolError as ToolError
|
|
28
|
+
from harnessed.kernel import Context as Context
|
|
29
|
+
from harnessed.kernel import Inject as Inject
|
|
30
|
+
from harnessed.kernel import PluginState as PluginState
|
|
31
|
+
from harnessed.kernel import hook as hook
|
|
32
|
+
from harnessed.kernel import plugin as plugin
|
|
33
|
+
|
|
34
|
+
from . import events as events
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"Agent",
|
|
38
|
+
"ApprovalAnswer",
|
|
39
|
+
"ApprovalRequest",
|
|
40
|
+
"Context",
|
|
41
|
+
"File",
|
|
42
|
+
"Image",
|
|
43
|
+
"ImageDetail",
|
|
44
|
+
"Inject",
|
|
45
|
+
"Observation",
|
|
46
|
+
"OutputContract",
|
|
47
|
+
"Plugin",
|
|
48
|
+
"PluginConfig",
|
|
49
|
+
"PluginConflict",
|
|
50
|
+
"PolicyDecision",
|
|
51
|
+
"Role",
|
|
52
|
+
"RunHandle",
|
|
53
|
+
"RunResult",
|
|
54
|
+
"Section",
|
|
55
|
+
"Session",
|
|
56
|
+
"SessionClosed",
|
|
57
|
+
"SessionStatus",
|
|
58
|
+
"StopReason",
|
|
59
|
+
"StreamOverflow",
|
|
60
|
+
"ToolCall",
|
|
61
|
+
"ToolContext",
|
|
62
|
+
"ToolError",
|
|
63
|
+
"ToolResult",
|
|
64
|
+
"ToolStatus",
|
|
65
|
+
"hook",
|
|
66
|
+
"plugin",
|
|
67
|
+
"tool",
|
|
68
|
+
]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncGenerator, Iterable
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Unpack, overload
|
|
6
|
+
|
|
7
|
+
from harnessed.core.approvals import Approvals
|
|
8
|
+
from harnessed.core.errors import PluginConflict
|
|
9
|
+
from harnessed.core.event_registry import EventRegistry
|
|
10
|
+
from harnessed.core.model import Model
|
|
11
|
+
from harnessed.core.outputs import OutputContract, OutputTypes
|
|
12
|
+
from harnessed.core.plugin import Service, plugin_contribution, plugin_events
|
|
13
|
+
from harnessed.core.prompt.instructions import Instructions
|
|
14
|
+
from harnessed.core.prompt.values import InstructionValues, PromptComposition
|
|
15
|
+
from harnessed.core.runtime.agent_loop import AgentLoop
|
|
16
|
+
from harnessed.core.runtime.opener import open_session
|
|
17
|
+
from harnessed.core.runtime.options import RunOptions
|
|
18
|
+
from harnessed.core.runtime.result import RunResult
|
|
19
|
+
from harnessed.core.runtime.services import RuntimeServices
|
|
20
|
+
from harnessed.core.runtime.session import Session
|
|
21
|
+
from harnessed.core.session.repository import SessionRepository
|
|
22
|
+
from harnessed.core.session.session import SessionLog
|
|
23
|
+
from harnessed.core.session.store import MemorySessions
|
|
24
|
+
from harnessed.core.tools.output_limit import ToolOutputLimit
|
|
25
|
+
from harnessed.core.tracing import NoopTracer
|
|
26
|
+
from harnessed.kernel import Context, PluginLike
|
|
27
|
+
from harnessed.kernel.plugin import as_plugin_spec
|
|
28
|
+
from harnessed.models.pydantic_ai import PydanticAIModel
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from harnessed.models.pydantic_ai import NativeModel
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Agent:
|
|
35
|
+
"""Resolve model, instructions and plugins into a reusable configuration.
|
|
36
|
+
|
|
37
|
+
Each opened session owns its runtime and plugin lifetimes. The configured
|
|
38
|
+
session-store plugin retains conversations and enforces a single writer.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
model: Model | str | NativeModel | None = None,
|
|
45
|
+
instructions: InstructionValues | None = None,
|
|
46
|
+
plugins: Iterable[PluginLike] = (),
|
|
47
|
+
) -> None:
|
|
48
|
+
supplied: list[PluginLike] = list(plugins)
|
|
49
|
+
if instructions is not None:
|
|
50
|
+
supplied.insert(0, Instructions(instructions))
|
|
51
|
+
if model is not None:
|
|
52
|
+
supplied.insert(0, Service(Model, model))
|
|
53
|
+
normalized: list[PluginLike] = []
|
|
54
|
+
for value in supplied:
|
|
55
|
+
if isinstance(value, Service) and value.key is Model:
|
|
56
|
+
implementation = value.value
|
|
57
|
+
if not isinstance(implementation, Model):
|
|
58
|
+
implementation = PydanticAIModel(implementation)
|
|
59
|
+
value = Service(Model, implementation)
|
|
60
|
+
normalized.append(value)
|
|
61
|
+
declared = next((value for value in normalized if isinstance(value, Instructions)), None)
|
|
62
|
+
self.prompt = PromptComposition(
|
|
63
|
+
declared.values if declared is not None else (),
|
|
64
|
+
(plugin_contribution(value) for value in normalized),
|
|
65
|
+
)
|
|
66
|
+
defaults: list[PluginLike] = [
|
|
67
|
+
AgentLoop(),
|
|
68
|
+
MemorySessions(),
|
|
69
|
+
ToolOutputLimit(),
|
|
70
|
+
Approvals(),
|
|
71
|
+
NoopTracer(),
|
|
72
|
+
Instructions(composition=self.prompt),
|
|
73
|
+
]
|
|
74
|
+
occupied = {as_plugin_spec(value).slot: index for index, value in enumerate(defaults)}
|
|
75
|
+
explicit: set[str] = set()
|
|
76
|
+
for value in normalized:
|
|
77
|
+
if isinstance(value, Instructions):
|
|
78
|
+
value = Instructions(value.values, composition=self.prompt)
|
|
79
|
+
spec = as_plugin_spec(value)
|
|
80
|
+
if spec.slot is not None:
|
|
81
|
+
if spec.slot in explicit:
|
|
82
|
+
raise PluginConflict(f"Multiple plugins in slot {spec.slot!r}")
|
|
83
|
+
explicit.add(spec.slot)
|
|
84
|
+
if spec.slot in occupied:
|
|
85
|
+
defaults[occupied[spec.slot]] = value
|
|
86
|
+
continue
|
|
87
|
+
occupied[spec.slot] = len(defaults)
|
|
88
|
+
defaults.append(value)
|
|
89
|
+
self.plugins = (RuntimeServices(), *defaults)
|
|
90
|
+
self._events = EventRegistry(
|
|
91
|
+
kind for plugin in self.plugins for kind in plugin_events(plugin)
|
|
92
|
+
)
|
|
93
|
+
self.sessions = SessionRepository(self.plugins, registry=self._events)
|
|
94
|
+
|
|
95
|
+
@asynccontextmanager
|
|
96
|
+
async def session(
|
|
97
|
+
self,
|
|
98
|
+
session_id: str | SessionLog | None = None,
|
|
99
|
+
*,
|
|
100
|
+
restore_outputs: Iterable[type[Any] | OutputContract[Any]] = (),
|
|
101
|
+
) -> AsyncGenerator[Session]:
|
|
102
|
+
"""Open or resume a conversation; omit its ID to create a new one."""
|
|
103
|
+
async with Context() as ctx:
|
|
104
|
+
ctx.provide(EventRegistry, self._events.copy())
|
|
105
|
+
# Validators are per session, so dynamic output types never leak between them.
|
|
106
|
+
outputs = OutputTypes()
|
|
107
|
+
for output in restore_outputs:
|
|
108
|
+
outputs.register(output)
|
|
109
|
+
ctx.provide(OutputTypes, outputs)
|
|
110
|
+
await ctx.install(self.plugins, settle=True)
|
|
111
|
+
yield await open_session(ctx, session_id)
|
|
112
|
+
|
|
113
|
+
@overload
|
|
114
|
+
async def run[T](
|
|
115
|
+
self, prompt: str, *, output: type[T] | OutputContract[T], **options: Unpack[RunOptions]
|
|
116
|
+
) -> RunResult[T]: ...
|
|
117
|
+
|
|
118
|
+
@overload
|
|
119
|
+
async def run(
|
|
120
|
+
self, prompt: str, *, output: None = None, **options: Unpack[RunOptions]
|
|
121
|
+
) -> RunResult[None]: ...
|
|
122
|
+
|
|
123
|
+
async def run(
|
|
124
|
+
self,
|
|
125
|
+
prompt: str,
|
|
126
|
+
*,
|
|
127
|
+
output: type[Any] | OutputContract[Any] | None = None,
|
|
128
|
+
**options: Unpack[RunOptions],
|
|
129
|
+
) -> RunResult[Any]:
|
|
130
|
+
"""Run one task in a new session."""
|
|
131
|
+
async with self.session() as session:
|
|
132
|
+
return await session.run(prompt, output=output, **options)
|
|
File without changes
|