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.
Files changed (126) hide show
  1. harnessed-0.1.0/LICENSE +21 -0
  2. harnessed-0.1.0/PKG-INFO +123 -0
  3. harnessed-0.1.0/README.md +92 -0
  4. harnessed-0.1.0/harnessed/__init__.py +58 -0
  5. harnessed-0.1.0/harnessed/__init__.pyi +68 -0
  6. harnessed-0.1.0/harnessed/agent.py +132 -0
  7. harnessed-0.1.0/harnessed/core/__init__.py +0 -0
  8. harnessed-0.1.0/harnessed/core/approvals.py +291 -0
  9. harnessed-0.1.0/harnessed/core/attachments.py +86 -0
  10. harnessed-0.1.0/harnessed/core/budget.py +49 -0
  11. harnessed-0.1.0/harnessed/core/buffering.py +49 -0
  12. harnessed-0.1.0/harnessed/core/compaction.py +417 -0
  13. harnessed-0.1.0/harnessed/core/errors.py +105 -0
  14. harnessed-0.1.0/harnessed/core/event_kinds.py +50 -0
  15. harnessed-0.1.0/harnessed/core/event_registry.py +114 -0
  16. harnessed-0.1.0/harnessed/core/hooks.py +112 -0
  17. harnessed-0.1.0/harnessed/core/messages.py +368 -0
  18. harnessed-0.1.0/harnessed/core/model.py +248 -0
  19. harnessed-0.1.0/harnessed/core/outputs.py +170 -0
  20. harnessed-0.1.0/harnessed/core/plugin.py +118 -0
  21. harnessed-0.1.0/harnessed/core/prompt/__init__.py +0 -0
  22. harnessed-0.1.0/harnessed/core/prompt/context.py +84 -0
  23. harnessed-0.1.0/harnessed/core/prompt/instructions.py +19 -0
  24. harnessed-0.1.0/harnessed/core/prompt/system_prompt.py +42 -0
  25. harnessed-0.1.0/harnessed/core/prompt/values.py +131 -0
  26. harnessed-0.1.0/harnessed/core/runtime/__init__.py +0 -0
  27. harnessed-0.1.0/harnessed/core/runtime/agent_loop.py +71 -0
  28. harnessed-0.1.0/harnessed/core/runtime/approvals.py +231 -0
  29. harnessed-0.1.0/harnessed/core/runtime/control.py +278 -0
  30. harnessed-0.1.0/harnessed/core/runtime/executor.py +209 -0
  31. harnessed-0.1.0/harnessed/core/runtime/handles.py +47 -0
  32. harnessed-0.1.0/harnessed/core/runtime/inbox.py +132 -0
  33. harnessed-0.1.0/harnessed/core/runtime/opener.py +77 -0
  34. harnessed-0.1.0/harnessed/core/runtime/options.py +71 -0
  35. harnessed-0.1.0/harnessed/core/runtime/output.py +124 -0
  36. harnessed-0.1.0/harnessed/core/runtime/respond.py +69 -0
  37. harnessed-0.1.0/harnessed/core/runtime/result.py +168 -0
  38. harnessed-0.1.0/harnessed/core/runtime/services.py +38 -0
  39. harnessed-0.1.0/harnessed/core/runtime/session.py +583 -0
  40. harnessed-0.1.0/harnessed/core/runtime/stepper.py +162 -0
  41. harnessed-0.1.0/harnessed/core/runtime/streaming.py +174 -0
  42. harnessed-0.1.0/harnessed/core/runtime/turns.py +123 -0
  43. harnessed-0.1.0/harnessed/core/session/__init__.py +0 -0
  44. harnessed-0.1.0/harnessed/core/session/artifacts.py +81 -0
  45. harnessed-0.1.0/harnessed/core/session/codec.py +202 -0
  46. harnessed-0.1.0/harnessed/core/session/control.py +35 -0
  47. harnessed-0.1.0/harnessed/core/session/events.py +731 -0
  48. harnessed-0.1.0/harnessed/core/session/files.py +86 -0
  49. harnessed-0.1.0/harnessed/core/session/history.py +391 -0
  50. harnessed-0.1.0/harnessed/core/session/inbox.py +59 -0
  51. harnessed-0.1.0/harnessed/core/session/repository.py +45 -0
  52. harnessed-0.1.0/harnessed/core/session/session.py +537 -0
  53. harnessed-0.1.0/harnessed/core/session/store.py +138 -0
  54. harnessed-0.1.0/harnessed/core/session/turns.py +220 -0
  55. harnessed-0.1.0/harnessed/core/stream.py +161 -0
  56. harnessed-0.1.0/harnessed/core/tokens.py +78 -0
  57. harnessed-0.1.0/harnessed/core/tools/__init__.py +11 -0
  58. harnessed-0.1.0/harnessed/core/tools/arguments.py +98 -0
  59. harnessed-0.1.0/harnessed/core/tools/context.py +61 -0
  60. harnessed-0.1.0/harnessed/core/tools/decorator.py +53 -0
  61. harnessed-0.1.0/harnessed/core/tools/output_limit.py +46 -0
  62. harnessed-0.1.0/harnessed/core/tools/policy.py +18 -0
  63. harnessed-0.1.0/harnessed/core/tools/references.py +68 -0
  64. harnessed-0.1.0/harnessed/core/tools/registry.py +390 -0
  65. harnessed-0.1.0/harnessed/core/tools/scheduling.py +93 -0
  66. harnessed-0.1.0/harnessed/core/tracing.py +33 -0
  67. harnessed-0.1.0/harnessed/errors.py +45 -0
  68. harnessed-0.1.0/harnessed/events/__init__.py +53 -0
  69. harnessed-0.1.0/harnessed/events/approval.py +7 -0
  70. harnessed-0.1.0/harnessed/events/compaction.py +5 -0
  71. harnessed-0.1.0/harnessed/events/context.py +6 -0
  72. harnessed-0.1.0/harnessed/events/inbox.py +13 -0
  73. harnessed-0.1.0/harnessed/events/message.py +11 -0
  74. harnessed-0.1.0/harnessed/events/model.py +11 -0
  75. harnessed-0.1.0/harnessed/events/output.py +7 -0
  76. harnessed-0.1.0/harnessed/events/reasoning.py +5 -0
  77. harnessed-0.1.0/harnessed/events/rule.py +7 -0
  78. harnessed-0.1.0/harnessed/events/session.py +6 -0
  79. harnessed-0.1.0/harnessed/events/spill.py +5 -0
  80. harnessed-0.1.0/harnessed/events/step.py +5 -0
  81. harnessed-0.1.0/harnessed/events/text.py +5 -0
  82. harnessed-0.1.0/harnessed/events/tool.py +29 -0
  83. harnessed-0.1.0/harnessed/events/turn.py +6 -0
  84. harnessed-0.1.0/harnessed/hooks.py +29 -0
  85. harnessed-0.1.0/harnessed/kernel/__init__.py +55 -0
  86. harnessed-0.1.0/harnessed/kernel/context.py +344 -0
  87. harnessed-0.1.0/harnessed/kernel/declarations.py +208 -0
  88. harnessed-0.1.0/harnessed/kernel/di.py +121 -0
  89. harnessed-0.1.0/harnessed/kernel/effects.py +45 -0
  90. harnessed-0.1.0/harnessed/kernel/events.py +206 -0
  91. harnessed-0.1.0/harnessed/kernel/hooks.py +97 -0
  92. harnessed-0.1.0/harnessed/kernel/keys.py +20 -0
  93. harnessed-0.1.0/harnessed/kernel/plugin.py +343 -0
  94. harnessed-0.1.0/harnessed/kernel/spec.py +21 -0
  95. harnessed-0.1.0/harnessed/kernel/state.py +40 -0
  96. harnessed-0.1.0/harnessed/models/__init__.py +58 -0
  97. harnessed-0.1.0/harnessed/models/pydantic_ai.py +532 -0
  98. harnessed-0.1.0/harnessed/plugins/__init__.py +56 -0
  99. harnessed-0.1.0/harnessed/plugins/compaction.py +114 -0
  100. harnessed-0.1.0/harnessed/plugins/filesystem.py +283 -0
  101. harnessed-0.1.0/harnessed/plugins/observations.py +31 -0
  102. harnessed-0.1.0/harnessed/plugins/rules.py +210 -0
  103. harnessed-0.1.0/harnessed/plugins/sessions.py +449 -0
  104. harnessed-0.1.0/harnessed/plugins/shell/__init__.py +276 -0
  105. harnessed-0.1.0/harnessed/plugins/shell/windows.py +132 -0
  106. harnessed-0.1.0/harnessed/plugins/skills.py +408 -0
  107. harnessed-0.1.0/harnessed/plugins/spill.py +233 -0
  108. harnessed-0.1.0/harnessed/py.typed +0 -0
  109. harnessed-0.1.0/harnessed/testing/__init__.py +17 -0
  110. harnessed-0.1.0/harnessed/testing/assertions.py +20 -0
  111. harnessed-0.1.0/harnessed/testing/compaction.py +30 -0
  112. harnessed-0.1.0/harnessed/testing/fakes.py +184 -0
  113. harnessed-0.1.0/harnessed/testing/models.py +173 -0
  114. harnessed-0.1.0/harnessed/testing/pytest_plugin.py +26 -0
  115. harnessed-0.1.0/harnessed.egg-info/PKG-INFO +123 -0
  116. harnessed-0.1.0/harnessed.egg-info/SOURCES.txt +124 -0
  117. harnessed-0.1.0/harnessed.egg-info/dependency_links.txt +1 -0
  118. harnessed-0.1.0/harnessed.egg-info/requires.txt +13 -0
  119. harnessed-0.1.0/harnessed.egg-info/top_level.txt +1 -0
  120. harnessed-0.1.0/pyproject.toml +90 -0
  121. harnessed-0.1.0/setup.cfg +4 -0
  122. harnessed-0.1.0/tests/test_app.py +240 -0
  123. harnessed-0.1.0/tests/test_architecture.py +237 -0
  124. harnessed-0.1.0/tests/test_composition.py +59 -0
  125. harnessed-0.1.0/tests/test_default_plugins.py +145 -0
  126. harnessed-0.1.0/tests/test_examples.py +92 -0
@@ -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.
@@ -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