data-harness 0.1.3__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 (69) hide show
  1. data_harness-0.1.3/.github/workflows/ci.yml +18 -0
  2. data_harness-0.1.3/.github/workflows/release-testpypi.yml +15 -0
  3. data_harness-0.1.3/.github/workflows/release.yml +26 -0
  4. data_harness-0.1.3/.gitignore +25 -0
  5. data_harness-0.1.3/LICENSE +21 -0
  6. data_harness-0.1.3/PKG-INFO +238 -0
  7. data_harness-0.1.3/README.md +213 -0
  8. data_harness-0.1.3/data_harness/__init__.py +48 -0
  9. data_harness-0.1.3/data_harness/agent.py +550 -0
  10. data_harness-0.1.3/data_harness/cache.py +319 -0
  11. data_harness-0.1.3/data_harness/exceptions.py +21 -0
  12. data_harness-0.1.3/data_harness/format.py +108 -0
  13. data_harness-0.1.3/data_harness/logger.py +115 -0
  14. data_harness-0.1.3/data_harness/loop.py +640 -0
  15. data_harness-0.1.3/data_harness/observe.py +31 -0
  16. data_harness-0.1.3/data_harness/providers/__init__.py +0 -0
  17. data_harness-0.1.3/data_harness/providers/anthropic.py +182 -0
  18. data_harness-0.1.3/data_harness/providers/base.py +67 -0
  19. data_harness-0.1.3/data_harness/providers/openai.py +174 -0
  20. data_harness-0.1.3/data_harness/result.py +51 -0
  21. data_harness-0.1.3/data_harness/schema.py +79 -0
  22. data_harness-0.1.3/data_harness/serialize.py +111 -0
  23. data_harness-0.1.3/data_harness/testing.py +134 -0
  24. data_harness-0.1.3/data_harness/tools/__init__.py +0 -0
  25. data_harness-0.1.3/data_harness/tools/connectors.py +130 -0
  26. data_harness-0.1.3/data_harness/tools/interpreter.py +198 -0
  27. data_harness-0.1.3/data_harness/tools/planner.py +107 -0
  28. data_harness-0.1.3/data_harness/tools/subagent.py +222 -0
  29. data_harness-0.1.3/data_harness/tools/variables.py +33 -0
  30. data_harness-0.1.3/data_harness/types.py +64 -0
  31. data_harness-0.1.3/examples/advanced_wiring.py +152 -0
  32. data_harness-0.1.3/examples/data/README.md +6 -0
  33. data_harness-0.1.3/examples/data/fred_unrate_2024.csv +13 -0
  34. data_harness-0.1.3/examples/inspect_run.py +24 -0
  35. data_harness-0.1.3/examples/quickstart.py +37 -0
  36. data_harness-0.1.3/pyproject.toml +68 -0
  37. data_harness-0.1.3/tests/__init__.py +0 -0
  38. data_harness-0.1.3/tests/_smoke_dashboard.py +420 -0
  39. data_harness-0.1.3/tests/conftest.py +223 -0
  40. data_harness-0.1.3/tests/smoke_tests.py +452 -0
  41. data_harness-0.1.3/tests/test_agent.py +612 -0
  42. data_harness-0.1.3/tests/test_async_loop.py +285 -0
  43. data_harness-0.1.3/tests/test_cache.py +282 -0
  44. data_harness-0.1.3/tests/test_docs.py +63 -0
  45. data_harness-0.1.3/tests/test_docs_phase5.py +53 -0
  46. data_harness-0.1.3/tests/test_format.py +100 -0
  47. data_harness-0.1.3/tests/test_gaps.py +374 -0
  48. data_harness-0.1.3/tests/test_integration.py +304 -0
  49. data_harness-0.1.3/tests/test_logger.py +134 -0
  50. data_harness-0.1.3/tests/test_loop.py +362 -0
  51. data_harness-0.1.3/tests/test_loop_reminders.py +204 -0
  52. data_harness-0.1.3/tests/test_observe.py +33 -0
  53. data_harness-0.1.3/tests/test_providers.py +201 -0
  54. data_harness-0.1.3/tests/test_providers_openai.py +323 -0
  55. data_harness-0.1.3/tests/test_result.py +474 -0
  56. data_harness-0.1.3/tests/test_review_fixes.py +302 -0
  57. data_harness-0.1.3/tests/test_schema.py +97 -0
  58. data_harness-0.1.3/tests/test_serialize.py +105 -0
  59. data_harness-0.1.3/tests/test_session_inspection.py +251 -0
  60. data_harness-0.1.3/tests/test_testing.py +70 -0
  61. data_harness-0.1.3/tests/test_tool_annotations.py +209 -0
  62. data_harness-0.1.3/tests/test_tool_connectors.py +106 -0
  63. data_harness-0.1.3/tests/test_tool_interpreter.py +139 -0
  64. data_harness-0.1.3/tests/test_tool_planner.py +94 -0
  65. data_harness-0.1.3/tests/test_tool_subagent.py +461 -0
  66. data_harness-0.1.3/tests/test_tool_variables.py +40 -0
  67. data_harness-0.1.3/tests/test_turn_summary.py +289 -0
  68. data_harness-0.1.3/tests/test_types.py +78 -0
  69. data_harness-0.1.3/uv.lock +1025 -0
@@ -0,0 +1,18 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ matrix:
10
+ python: ["3.10", "3.11", "3.12"]
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v3
14
+ - run: uv python install ${{ matrix.python }}
15
+ - run: uv sync --python ${{ matrix.python }}
16
+ - run: uv run --python ${{ matrix.python }} ruff check data_harness tests
17
+ - run: uv run --python ${{ matrix.python }} ruff format --check data_harness tests
18
+ - run: uv run --python ${{ matrix.python }} pytest tests/ -m "not live"
@@ -0,0 +1,15 @@
1
+ name: Release to TestPyPI
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ jobs:
7
+ publish:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: astral-sh/setup-uv@v3
12
+ - run: uv build
13
+ - run: uv publish --publish-url https://test.pypi.org/legacy/
14
+ env:
15
+ UV_PUBLISH_TOKEN: ${{ secrets.TEST_PYPI_TOKEN }}
@@ -0,0 +1,26 @@
1
+ name: Release to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*.*.*"
7
+ release:
8
+ types: [published]
9
+ workflow_dispatch:
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ environment: pypi
15
+ permissions:
16
+ id-token: write
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: astral-sh/setup-uv@v3
20
+ - run: uv python install 3.12
21
+ - run: uv sync --python 3.12 --frozen
22
+ - run: uv run --python 3.12 ruff check data_harness tests
23
+ - run: uv run --python 3.12 ruff format --check data_harness tests
24
+ - run: uv run --python 3.12 pytest tests/ -m "not live"
25
+ - run: uv build --no-sources
26
+ - run: uv publish
@@ -0,0 +1,25 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ venv/
9
+ env/
10
+ .env
11
+ *.egg
12
+ .pytest_cache/
13
+ .mypy_cache/
14
+ .ruff_cache/
15
+ runs/
16
+ *.jsonl
17
+ .DS_Store
18
+ plan/
19
+ .claude/
20
+ AGENTS.md
21
+ CLAUDE.md
22
+ blogs/
23
+ .playwright-mcp/
24
+ artifact-*.html
25
+ smoke_results/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Max Khor
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,238 @@
1
+ Metadata-Version: 2.4
2
+ Name: data-harness
3
+ Version: 0.1.3
4
+ Summary: Data agent with Python-native tools (no bash)
5
+ Project-URL: Homepage, https://github.com/maxkskhor/data-harness
6
+ Project-URL: Repository, https://github.com/maxkskhor/data-harness
7
+ Project-URL: Issues, https://github.com/maxkskhor/data-harness/issues
8
+ Author: Max Khor
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: anthropic
13
+ Requires-Dist: loguru
14
+ Requires-Dist: numpy
15
+ Requires-Dist: pandas
16
+ Provides-Extra: dev
17
+ Requires-Dist: openai; extra == 'dev'
18
+ Requires-Dist: pytest; extra == 'dev'
19
+ Requires-Dist: pytest-asyncio; extra == 'dev'
20
+ Requires-Dist: pytest-mock; extra == 'dev'
21
+ Requires-Dist: python-dotenv; extra == 'dev'
22
+ Provides-Extra: openai
23
+ Requires-Dist: openai; extra == 'openai'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # data-harness
27
+
28
+ *(data + ReAct — a controlled data-agent SDK for Python workflows)*
29
+
30
+ A data-native agent SDK for Python — built around controlled execution, handle-based state, provider adapters, sessions, subagents, and reconstructable runs.
31
+
32
+ Most agent frameworks hand the model a shell and call it a day. `data-harness` takes a different approach: the model operates through a constrained Python interpreter, with data stored in a session cache and exposed as named handles. No bash. Explicit state. Logs that can reconstruct what happened.
33
+
34
+ `data-harness` began as an installable reference implementation for harness design. It is now developing into the full SDK/framework track. A separate `learn-data-harness` repository will be created after the SDK stabilises to extract the basic principles without async, production sandboxing, or SDK-heavy features.
35
+
36
+ The design is covered in a three-part series:
37
+
38
+ - [Designing a ReAct Harness for Data Workflows Without Bash](https://maxkskhor.substack.com/p/designing-a-react-harness-for-data)
39
+ - [How a Bash-Free Data Agent Remembers Its Work](https://maxkskhor.substack.com/p/how-a-bash-free-data-agent-remembers)
40
+ - [The Bugs Hidden Inside a Data Agent Harness](https://maxkskhor.substack.com/p/the-engineering-invariants-behind)
41
+
42
+ ---
43
+
44
+ ## Why no bash?
45
+
46
+ Giving an agent shell access is the path of least resistance, but it creates real problems in production: unpredictable side effects, security exposure, and behaviour that's hard to reproduce. `data-harness` deliberately constrains the model to Python only — which turns out to be enough for most data workloads and forces cleaner tool design.
47
+
48
+ ---
49
+
50
+ ## Core design decisions
51
+
52
+ Each decision here is intentional. Understanding them is the point.
53
+
54
+ **Handle/snapshot pattern**
55
+ Large objects (DataFrames, arrays, query results) live in a `SessionCache`, not in message history. The model only sees a compact snapshot — shape, columns, a few sample rows. It accesses the data by writing Python against the handle name. This keeps context lean without hiding data from the model.
56
+
57
+ **Prefix-stable system prompt**
58
+ The system prompt never changes between turns. Reminders, state, and nags are appended to the conversation suffix. This is a KV-cache discipline: a stable prefix means the provider can cache it, which reduces latency and cost on long runs.
59
+
60
+ **Progressive connector disclosure**
61
+ Data connectors (databases, APIs, warehouses) are registered but hidden from the tool list until explicitly loaded. A shorter tool list means the model makes better routing decisions. Connectors are only visible when relevant.
62
+
63
+ **Subagent isolation**
64
+ Spawned subagents get a fresh adapter and a fresh cache. State is transferred explicitly via `input_handles`. No implicit shared state. This makes subagent behaviour reproducible and debuggable.
65
+
66
+ **Suffix-only nag reminders**
67
+ The planner escalates reminders at 4 / 8 / 12 turns without progress. These are always appended to the suffix, never inserted into the prefix, so the KV cache is never busted by reminder text.
68
+
69
+ **JSONL turn logging**
70
+ Every turn is logged to a `.jsonl` file from the start. Not bolted on later. Each line is a complete turn record including latency, token counts, and cache hit/miss. Reproducibility is a first-class concern.
71
+
72
+ ---
73
+
74
+ ## Install
75
+
76
+ ```bash
77
+ # requires Python 3.10+ and uv
78
+ uv sync
79
+ ```
80
+
81
+ ## Quick start
82
+
83
+ `Agent` needs a provider adapter. The adapter is the boundary between the
84
+ provider SDK and the harness: it turns Anthropic/OpenAI responses into
85
+ `data-harness`'s normalised `Message`, `ToolUseBlock`, and token-count types. It is
86
+ explicit on purpose so the harness is not tied to one model provider, and tests
87
+ can swap in `FakeAdapter` without touching the loop.
88
+
89
+ For Anthropic:
90
+
91
+ ```python
92
+ from data_harness import Agent
93
+ from data_harness.providers.anthropic import AnthropicAdapter
94
+
95
+ adapter = AnthropicAdapter(model="claude-sonnet-4-6")
96
+ agent = Agent(adapter=adapter, system="You are a data analyst.")
97
+
98
+ result = agent.run("Compute the mean of [1, 2, 3, 4, 5] and print it.")
99
+ print(result)
100
+ ```
101
+
102
+ For OpenAI, install the optional extra and change only the adapter:
103
+
104
+ ```bash
105
+ pip install "data-harness[openai]"
106
+ ```
107
+
108
+ ```python
109
+ from data_harness.providers.openai import OpenAIAdapter
110
+
111
+ adapter = OpenAIAdapter(model="gpt-4o-mini")
112
+ ```
113
+
114
+ Run the minimal Anthropic example:
115
+
116
+ ```bash
117
+ uv run python examples/quickstart.py
118
+ ```
119
+
120
+ `examples/quickstart.py` requires `ANTHROPIC_API_KEY` when run as a script. Tests import `build_agent()` and drive it with `FakeAdapter`, so the example stays covered without token spend.
121
+
122
+ ## Chat sessions
123
+
124
+ `Agent.run()` is still the simple one-shot path: it starts a fresh message
125
+ history each time. For chatbot or workbench applications, create a session and
126
+ ask follow-up questions on it:
127
+
128
+ ```python
129
+ from data_harness import Agent
130
+ from data_harness.providers.openai import OpenAIAdapter
131
+
132
+ adapter = OpenAIAdapter(model="gpt-4o-mini")
133
+ agent = Agent(adapter=adapter, system="You are a data analyst.")
134
+
135
+ session = agent.session()
136
+ session.put("uploaded_data", df)
137
+
138
+ print(session.ask("What columns are in the uploaded data?"))
139
+ print(session.ask("Which numeric column has the highest average?"))
140
+ ```
141
+
142
+ The session keeps one `Harness`, one message history, and one `SessionCache`.
143
+ This is the path to use when a UI needs uploaded artefacts and conversation
144
+ follow-up to stay in scope.
145
+
146
+ ## Connector example
147
+
148
+ Connector helpers keep the quick path small while preserving progressive disclosure. Connector tools start hidden; the model must call `load_connectors` before it can use them.
149
+
150
+ ```python
151
+ from data_harness import Agent
152
+ from data_harness.providers.anthropic import AnthropicAdapter
153
+
154
+ adapter = AnthropicAdapter(model="claude-sonnet-4-6")
155
+ agent = Agent(adapter=adapter, system="You are a data analyst.")
156
+
157
+ market_data = agent.connector(
158
+ "market_data",
159
+ description="Market data tools.",
160
+ )
161
+
162
+
163
+ def fetch_ohlcv(symbol: str) -> list[dict]:
164
+ return [{"symbol": symbol, "close": 101.2}]
165
+
166
+
167
+ market_data.tool(
168
+ fetch_ohlcv,
169
+ description="Fetch OHLCV data for a ticker.",
170
+ )
171
+
172
+ result = agent.run("Load market_data and inspect AAPL.")
173
+ print(result)
174
+ ```
175
+
176
+ ## What `Agent` composes
177
+
178
+ `Agent` is a thin composition layer over the lower-level primitives:
179
+
180
+ - A provider adapter translates model-provider SDK objects into the harness's normalised response types.
181
+ - `Harness` owns the ReAct loop, messages, dispatch, reminders, and JSONL logging.
182
+ - `SessionCache` stores large values as handles plus compact snapshots.
183
+ - `AgentSession` keeps a chat-style harness and cache alive across follow-up questions.
184
+ - `python_interpreter` is the controlled execution surface; there is no bash tool.
185
+ - `list_variables` exposes cache handles without dumping raw payloads.
186
+ - `ConnectorRegistry` keeps connector tools hidden until loaded.
187
+ - `Planner` reminders and subagents are opt-in helpers, not a second runtime.
188
+
189
+ For explicit wiring, read [examples/advanced_wiring.py](examples/advanced_wiring.py). The future `learn-data-harness` repository will provide the smaller, linear teaching guide once this SDK surface has stabilised.
190
+
191
+ Run the advanced example - it loads a checked-in FRED unemployment-rate sample, runs analysis, uses subagents and the planner (requires `ANTHROPIC_API_KEY`):
192
+
193
+ ```bash
194
+ uv run python examples/advanced_wiring.py
195
+ ```
196
+
197
+ Run tests:
198
+
199
+ ```bash
200
+ uv run pytest tests/ -v
201
+ uv run pytest tests/smoke_tests.py -m live -v # requires OPENAI_API_KEY
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Project structure
207
+
208
+ ```
209
+ data_harness/
210
+ loop.py # Harness: the core ReAct loop
211
+ cache.py # SessionCache: handle/snapshot storage
212
+ providers/ # Normalised adapter interface (Anthropic and OpenAI)
213
+ tools/
214
+ interpreter.py # Sandboxed Python executor
215
+ connectors.py # Progressive connector registry
216
+ planner.py # Plan/nag tool
217
+ subagent.py # Isolated subagent spawning
218
+ variables.py # list_variables tool
219
+ types.py # Shared types: Message, ToolSpec, ContentBlock
220
+ logger.py # JSONL turn logging
221
+ observe.py # Latency measurement
222
+ examples/
223
+ quickstart.py # Minimal Agent path
224
+ advanced_wiring.py # Explicit Harness wiring
225
+ data/ # Small public sample data for the advanced demo
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Sandbox disclaimer
231
+
232
+ The Python interpreter uses AST checks and restricted globals to reduce accidental misuse. It is not a container sandbox and should not be treated as safe for untrusted input.
233
+
234
+ ---
235
+
236
+ ## License
237
+
238
+ MIT
@@ -0,0 +1,213 @@
1
+ # data-harness
2
+
3
+ *(data + ReAct — a controlled data-agent SDK for Python workflows)*
4
+
5
+ A data-native agent SDK for Python — built around controlled execution, handle-based state, provider adapters, sessions, subagents, and reconstructable runs.
6
+
7
+ Most agent frameworks hand the model a shell and call it a day. `data-harness` takes a different approach: the model operates through a constrained Python interpreter, with data stored in a session cache and exposed as named handles. No bash. Explicit state. Logs that can reconstruct what happened.
8
+
9
+ `data-harness` began as an installable reference implementation for harness design. It is now developing into the full SDK/framework track. A separate `learn-data-harness` repository will be created after the SDK stabilises to extract the basic principles without async, production sandboxing, or SDK-heavy features.
10
+
11
+ The design is covered in a three-part series:
12
+
13
+ - [Designing a ReAct Harness for Data Workflows Without Bash](https://maxkskhor.substack.com/p/designing-a-react-harness-for-data)
14
+ - [How a Bash-Free Data Agent Remembers Its Work](https://maxkskhor.substack.com/p/how-a-bash-free-data-agent-remembers)
15
+ - [The Bugs Hidden Inside a Data Agent Harness](https://maxkskhor.substack.com/p/the-engineering-invariants-behind)
16
+
17
+ ---
18
+
19
+ ## Why no bash?
20
+
21
+ Giving an agent shell access is the path of least resistance, but it creates real problems in production: unpredictable side effects, security exposure, and behaviour that's hard to reproduce. `data-harness` deliberately constrains the model to Python only — which turns out to be enough for most data workloads and forces cleaner tool design.
22
+
23
+ ---
24
+
25
+ ## Core design decisions
26
+
27
+ Each decision here is intentional. Understanding them is the point.
28
+
29
+ **Handle/snapshot pattern**
30
+ Large objects (DataFrames, arrays, query results) live in a `SessionCache`, not in message history. The model only sees a compact snapshot — shape, columns, a few sample rows. It accesses the data by writing Python against the handle name. This keeps context lean without hiding data from the model.
31
+
32
+ **Prefix-stable system prompt**
33
+ The system prompt never changes between turns. Reminders, state, and nags are appended to the conversation suffix. This is a KV-cache discipline: a stable prefix means the provider can cache it, which reduces latency and cost on long runs.
34
+
35
+ **Progressive connector disclosure**
36
+ Data connectors (databases, APIs, warehouses) are registered but hidden from the tool list until explicitly loaded. A shorter tool list means the model makes better routing decisions. Connectors are only visible when relevant.
37
+
38
+ **Subagent isolation**
39
+ Spawned subagents get a fresh adapter and a fresh cache. State is transferred explicitly via `input_handles`. No implicit shared state. This makes subagent behaviour reproducible and debuggable.
40
+
41
+ **Suffix-only nag reminders**
42
+ The planner escalates reminders at 4 / 8 / 12 turns without progress. These are always appended to the suffix, never inserted into the prefix, so the KV cache is never busted by reminder text.
43
+
44
+ **JSONL turn logging**
45
+ Every turn is logged to a `.jsonl` file from the start. Not bolted on later. Each line is a complete turn record including latency, token counts, and cache hit/miss. Reproducibility is a first-class concern.
46
+
47
+ ---
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ # requires Python 3.10+ and uv
53
+ uv sync
54
+ ```
55
+
56
+ ## Quick start
57
+
58
+ `Agent` needs a provider adapter. The adapter is the boundary between the
59
+ provider SDK and the harness: it turns Anthropic/OpenAI responses into
60
+ `data-harness`'s normalised `Message`, `ToolUseBlock`, and token-count types. It is
61
+ explicit on purpose so the harness is not tied to one model provider, and tests
62
+ can swap in `FakeAdapter` without touching the loop.
63
+
64
+ For Anthropic:
65
+
66
+ ```python
67
+ from data_harness import Agent
68
+ from data_harness.providers.anthropic import AnthropicAdapter
69
+
70
+ adapter = AnthropicAdapter(model="claude-sonnet-4-6")
71
+ agent = Agent(adapter=adapter, system="You are a data analyst.")
72
+
73
+ result = agent.run("Compute the mean of [1, 2, 3, 4, 5] and print it.")
74
+ print(result)
75
+ ```
76
+
77
+ For OpenAI, install the optional extra and change only the adapter:
78
+
79
+ ```bash
80
+ pip install "data-harness[openai]"
81
+ ```
82
+
83
+ ```python
84
+ from data_harness.providers.openai import OpenAIAdapter
85
+
86
+ adapter = OpenAIAdapter(model="gpt-4o-mini")
87
+ ```
88
+
89
+ Run the minimal Anthropic example:
90
+
91
+ ```bash
92
+ uv run python examples/quickstart.py
93
+ ```
94
+
95
+ `examples/quickstart.py` requires `ANTHROPIC_API_KEY` when run as a script. Tests import `build_agent()` and drive it with `FakeAdapter`, so the example stays covered without token spend.
96
+
97
+ ## Chat sessions
98
+
99
+ `Agent.run()` is still the simple one-shot path: it starts a fresh message
100
+ history each time. For chatbot or workbench applications, create a session and
101
+ ask follow-up questions on it:
102
+
103
+ ```python
104
+ from data_harness import Agent
105
+ from data_harness.providers.openai import OpenAIAdapter
106
+
107
+ adapter = OpenAIAdapter(model="gpt-4o-mini")
108
+ agent = Agent(adapter=adapter, system="You are a data analyst.")
109
+
110
+ session = agent.session()
111
+ session.put("uploaded_data", df)
112
+
113
+ print(session.ask("What columns are in the uploaded data?"))
114
+ print(session.ask("Which numeric column has the highest average?"))
115
+ ```
116
+
117
+ The session keeps one `Harness`, one message history, and one `SessionCache`.
118
+ This is the path to use when a UI needs uploaded artefacts and conversation
119
+ follow-up to stay in scope.
120
+
121
+ ## Connector example
122
+
123
+ Connector helpers keep the quick path small while preserving progressive disclosure. Connector tools start hidden; the model must call `load_connectors` before it can use them.
124
+
125
+ ```python
126
+ from data_harness import Agent
127
+ from data_harness.providers.anthropic import AnthropicAdapter
128
+
129
+ adapter = AnthropicAdapter(model="claude-sonnet-4-6")
130
+ agent = Agent(adapter=adapter, system="You are a data analyst.")
131
+
132
+ market_data = agent.connector(
133
+ "market_data",
134
+ description="Market data tools.",
135
+ )
136
+
137
+
138
+ def fetch_ohlcv(symbol: str) -> list[dict]:
139
+ return [{"symbol": symbol, "close": 101.2}]
140
+
141
+
142
+ market_data.tool(
143
+ fetch_ohlcv,
144
+ description="Fetch OHLCV data for a ticker.",
145
+ )
146
+
147
+ result = agent.run("Load market_data and inspect AAPL.")
148
+ print(result)
149
+ ```
150
+
151
+ ## What `Agent` composes
152
+
153
+ `Agent` is a thin composition layer over the lower-level primitives:
154
+
155
+ - A provider adapter translates model-provider SDK objects into the harness's normalised response types.
156
+ - `Harness` owns the ReAct loop, messages, dispatch, reminders, and JSONL logging.
157
+ - `SessionCache` stores large values as handles plus compact snapshots.
158
+ - `AgentSession` keeps a chat-style harness and cache alive across follow-up questions.
159
+ - `python_interpreter` is the controlled execution surface; there is no bash tool.
160
+ - `list_variables` exposes cache handles without dumping raw payloads.
161
+ - `ConnectorRegistry` keeps connector tools hidden until loaded.
162
+ - `Planner` reminders and subagents are opt-in helpers, not a second runtime.
163
+
164
+ For explicit wiring, read [examples/advanced_wiring.py](examples/advanced_wiring.py). The future `learn-data-harness` repository will provide the smaller, linear teaching guide once this SDK surface has stabilised.
165
+
166
+ Run the advanced example - it loads a checked-in FRED unemployment-rate sample, runs analysis, uses subagents and the planner (requires `ANTHROPIC_API_KEY`):
167
+
168
+ ```bash
169
+ uv run python examples/advanced_wiring.py
170
+ ```
171
+
172
+ Run tests:
173
+
174
+ ```bash
175
+ uv run pytest tests/ -v
176
+ uv run pytest tests/smoke_tests.py -m live -v # requires OPENAI_API_KEY
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Project structure
182
+
183
+ ```
184
+ data_harness/
185
+ loop.py # Harness: the core ReAct loop
186
+ cache.py # SessionCache: handle/snapshot storage
187
+ providers/ # Normalised adapter interface (Anthropic and OpenAI)
188
+ tools/
189
+ interpreter.py # Sandboxed Python executor
190
+ connectors.py # Progressive connector registry
191
+ planner.py # Plan/nag tool
192
+ subagent.py # Isolated subagent spawning
193
+ variables.py # list_variables tool
194
+ types.py # Shared types: Message, ToolSpec, ContentBlock
195
+ logger.py # JSONL turn logging
196
+ observe.py # Latency measurement
197
+ examples/
198
+ quickstart.py # Minimal Agent path
199
+ advanced_wiring.py # Explicit Harness wiring
200
+ data/ # Small public sample data for the advanced demo
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Sandbox disclaimer
206
+
207
+ The Python interpreter uses AST checks and restricted globals to reduce accidental misuse. It is not a container sandbox and should not be treated as safe for untrusted input.
208
+
209
+ ---
210
+
211
+ ## License
212
+
213
+ MIT
@@ -0,0 +1,48 @@
1
+ from data_harness.agent import Agent, AgentSession, AsyncAgent, AsyncAgentSession
2
+ from data_harness.exceptions import (
3
+ MaxTurnsExceeded,
4
+ SubagentRecursionError,
5
+ ToolNotFoundError,
6
+ )
7
+ from data_harness.loop import AsyncHarness
8
+ from data_harness.providers.base import (
9
+ AsyncProviderAdapter,
10
+ NormalizedResponse,
11
+ ProviderAdapter,
12
+ StopReason,
13
+ )
14
+ from data_harness.result import CacheStorageInfo, RunResult, Usage
15
+ from data_harness.types import (
16
+ ContentBlock,
17
+ Message,
18
+ TextBlock,
19
+ ToolAnnotations,
20
+ ToolResultBlock,
21
+ ToolSpec,
22
+ ToolUseBlock,
23
+ )
24
+
25
+ __all__ = [
26
+ "Agent",
27
+ "AgentSession",
28
+ "AsyncAgent",
29
+ "AsyncAgentSession",
30
+ "AsyncHarness",
31
+ "AsyncProviderAdapter",
32
+ "CacheStorageInfo",
33
+ "ContentBlock",
34
+ "MaxTurnsExceeded",
35
+ "Message",
36
+ "NormalizedResponse",
37
+ "ProviderAdapter",
38
+ "RunResult",
39
+ "StopReason",
40
+ "SubagentRecursionError",
41
+ "TextBlock",
42
+ "ToolAnnotations",
43
+ "ToolNotFoundError",
44
+ "ToolResultBlock",
45
+ "ToolSpec",
46
+ "ToolUseBlock",
47
+ "Usage",
48
+ ]