datagol-agent-harness 0.2.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 (58) hide show
  1. datagol_agent_harness-0.2.0/PKG-INFO +809 -0
  2. datagol_agent_harness-0.2.0/README.md +756 -0
  3. datagol_agent_harness-0.2.0/datagol_agent_harness/__init__.py +114 -0
  4. datagol_agent_harness-0.2.0/datagol_agent_harness/builtin/__init__.py +19 -0
  5. datagol_agent_harness-0.2.0/datagol_agent_harness/builtin/bash.py +58 -0
  6. datagol_agent_harness-0.2.0/datagol_agent_harness/builtin/file_output.py +31 -0
  7. datagol_agent_harness-0.2.0/datagol_agent_harness/builtin/filesystem.py +79 -0
  8. datagol_agent_harness-0.2.0/datagol_agent_harness/builtin/memory.py +104 -0
  9. datagol_agent_harness-0.2.0/datagol_agent_harness/builtin/web.py +32 -0
  10. datagol_agent_harness-0.2.0/datagol_agent_harness/core.py +392 -0
  11. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/__init__.py +83 -0
  12. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/cli.py +250 -0
  13. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/datasets/__init__.py +23 -0
  14. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/datasets/guardrails.py +34 -0
  15. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/datasets/loader.py +85 -0
  16. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/datasets/memory.py +41 -0
  17. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/datasets/multi_agent.py +40 -0
  18. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/datasets/registry.py +41 -0
  19. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/datasets/skills.py +63 -0
  20. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/datasets/tool_calling.py +57 -0
  21. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/evaluators/__init__.py +171 -0
  22. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/evaluators/correctness.py +170 -0
  23. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/evaluators/delegation.py +42 -0
  24. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/evaluators/llm_judge.py +116 -0
  25. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/evaluators/skills.py +58 -0
  26. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/evaluators/tools.py +194 -0
  27. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/evaluators/trajectory.py +106 -0
  28. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/runner.py +238 -0
  29. datagol_agent_harness-0.2.0/datagol_agent_harness/evals/target.py +220 -0
  30. datagol_agent_harness-0.2.0/datagol_agent_harness/extensions/__init__.py +37 -0
  31. datagol_agent_harness-0.2.0/datagol_agent_harness/extensions/base.py +157 -0
  32. datagol_agent_harness-0.2.0/datagol_agent_harness/extensions/langsmith.py +436 -0
  33. datagol_agent_harness-0.2.0/datagol_agent_harness/extensions/result_spill.py +548 -0
  34. datagol_agent_harness-0.2.0/datagol_agent_harness/hooks.py +135 -0
  35. datagol_agent_harness-0.2.0/datagol_agent_harness/mcp.py +437 -0
  36. datagol_agent_harness-0.2.0/datagol_agent_harness/memory.py +613 -0
  37. datagol_agent_harness-0.2.0/datagol_agent_harness/permissions.py +169 -0
  38. datagol_agent_harness-0.2.0/datagol_agent_harness/providers/__init__.py +14 -0
  39. datagol_agent_harness-0.2.0/datagol_agent_harness/providers/anthropic.py +138 -0
  40. datagol_agent_harness-0.2.0/datagol_agent_harness/providers/base.py +87 -0
  41. datagol_agent_harness-0.2.0/datagol_agent_harness/providers/openai.py +310 -0
  42. datagol_agent_harness-0.2.0/datagol_agent_harness/py.typed +0 -0
  43. datagol_agent_harness-0.2.0/datagol_agent_harness/runtime.py +415 -0
  44. datagol_agent_harness-0.2.0/datagol_agent_harness/sandbox.py +439 -0
  45. datagol_agent_harness-0.2.0/datagol_agent_harness/skills.py +221 -0
  46. datagol_agent_harness-0.2.0/datagol_agent_harness/streaming.py +307 -0
  47. datagol_agent_harness-0.2.0/datagol_agent_harness/tools.py +288 -0
  48. datagol_agent_harness-0.2.0/datagol_agent_harness/types.py +222 -0
  49. datagol_agent_harness-0.2.0/datagol_agent_harness.egg-info/PKG-INFO +809 -0
  50. datagol_agent_harness-0.2.0/datagol_agent_harness.egg-info/SOURCES.txt +56 -0
  51. datagol_agent_harness-0.2.0/datagol_agent_harness.egg-info/dependency_links.txt +1 -0
  52. datagol_agent_harness-0.2.0/datagol_agent_harness.egg-info/requires.txt +36 -0
  53. datagol_agent_harness-0.2.0/datagol_agent_harness.egg-info/top_level.txt +1 -0
  54. datagol_agent_harness-0.2.0/pyproject.toml +80 -0
  55. datagol_agent_harness-0.2.0/setup.cfg +4 -0
  56. datagol_agent_harness-0.2.0/tests/test_evals.py +410 -0
  57. datagol_agent_harness-0.2.0/tests/test_harness_enhancements.py +352 -0
  58. datagol_agent_harness-0.2.0/tests/test_langsmith_extension.py +343 -0
@@ -0,0 +1,809 @@
1
+ Metadata-Version: 2.4
2
+ Name: datagol-agent-harness
3
+ Version: 0.2.0
4
+ Summary: A lightweight, production-grade LLM agent toolkit in Python: streaming, MCP, lazy skills, memory, and sandboxing.
5
+ Author-email: Jyotish Bora <jyotish.bora@datagol.ai>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/inncretech/datagol-agent-harness
8
+ Project-URL: Repository, https://github.com/inncretech/datagol-agent-harness
9
+ Project-URL: Issues, https://github.com/inncretech/datagol-agent-harness/issues
10
+ Project-URL: Documentation, https://github.com/inncretech/datagol-agent-harness#readme
11
+ Keywords: agent,ai-agents,llm,anthropic,claude,openai,mcp,model-context-protocol,sandboxing,langsmith,multi-agent,streaming
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Operating System :: POSIX :: Linux
21
+ Classifier: Operating System :: MacOS
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: anthropic>=0.83.0
25
+ Requires-Dist: rich>=13.0.0
26
+ Requires-Dist: python-dotenv>=1.0.0
27
+ Requires-Dist: httpx>=0.27.0
28
+ Provides-Extra: langsmith
29
+ Requires-Dist: langsmith>=0.1.0; extra == "langsmith"
30
+ Provides-Extra: openai
31
+ Requires-Dist: openai>=1.50.0; extra == "openai"
32
+ Requires-Dist: tiktoken>=0.7.0; extra == "openai"
33
+ Provides-Extra: mcp
34
+ Requires-Dist: mcp[cli]>=1.0.0; extra == "mcp"
35
+ Provides-Extra: docker
36
+ Requires-Dist: docker>=7.0.0; extra == "docker"
37
+ Provides-Extra: server
38
+ Requires-Dist: fastapi>=0.110.0; extra == "server"
39
+ Requires-Dist: uvicorn[standard]>=0.27.0; extra == "server"
40
+ Requires-Dist: python-dotenv>=1.0.0; extra == "server"
41
+ Provides-Extra: dev
42
+ Requires-Dist: pytest>=8.0; extra == "dev"
43
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
44
+ Provides-Extra: all
45
+ Requires-Dist: langsmith>=0.1.0; extra == "all"
46
+ Requires-Dist: openai>=1.50.0; extra == "all"
47
+ Requires-Dist: tiktoken>=0.7.0; extra == "all"
48
+ Requires-Dist: mcp[cli]>=1.0.0; extra == "all"
49
+ Requires-Dist: docker>=7.0.0; extra == "all"
50
+ Requires-Dist: fastapi>=0.110.0; extra == "all"
51
+ Requires-Dist: uvicorn[standard]>=0.27.0; extra == "all"
52
+ Requires-Dist: python-dotenv>=1.0.0; extra == "all"
53
+
54
+ # DataGOL Agent Harness
55
+
56
+ [![PyPI version](https://img.shields.io/pypi/v/datagol-agent-harness.svg)](https://pypi.org/project/datagol-agent-harness/)
57
+ [![Python versions](https://img.shields.io/pypi/pyversions/datagol-agent-harness.svg)](https://pypi.org/project/datagol-agent-harness/)
58
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
59
+
60
+ **A provider-agnostic, library-first Python toolkit for building production-grade LLM agents.**
61
+
62
+ Rather than locking developers into rigid state-machine graphs or opaque persona prompts, `datagol-agent-harness` gives you a composable set of building blocks: real-time streaming, typed tool registration with schema inference, lazy-loaded skills, multi-agent delegation, 4-tier memory, sandboxed code execution, Model Context Protocol (MCP) tool bridges, native LangSmith tracing, and evaluation suites.
63
+
64
+ ### Why DataGOL Agent Harness?
65
+
66
+ - **No Graph Boilerplate:** Write simple async Python functions instead of complex state graphs.
67
+ - **Provider-Agnostic:** First-class support for Anthropic (Claude 3.5/3.7/Sonnet/Opus) and OpenAI (GPT-4o/GPT-5).
68
+ - **Streaming-First:** First-class typed event stream (`run_stream`) for WebSocket and SSE frontends.
69
+ - **Lazy Instruction Packs (Skills):** Load specialized guidelines only when needed via YAML-frontmatter `SKILL.md` packs.
70
+ - **Enterprise Guardrails & Sandboxing:** Execute untrusted code safely via Process, Docker, or macOS Seatbelt isolation with granular budget and iteration caps.
71
+ - **MCP Native:** Connect to any Model Context Protocol server (stdio subprocess or remote SSE) in 3 lines of code.
72
+ - **Full Observability & Evals:** Zero-overhead lifecycle hooks, middleware transforms, native LangSmith tracing, and benchmark evaluations.
73
+
74
+ ---
75
+
76
+ - [Setup](#setup)
77
+ - [1. Creating an agent](#1-creating-an-agent)
78
+ - [2. Registering tools](#2-registering-tools)
79
+ - [3. Streaming agents](#3-streaming-agents)
80
+ - [4. Multi-agent orchestration](#4-multi-agent-orchestration)
81
+ - [5. Skills](#5-skills)
82
+ - [6. Memory](#6-memory)
83
+ - [7. Permissions and guardrails](#7-permissions-and-guardrails)
84
+ - [8. Hooks and middleware](#8-hooks-and-middleware)
85
+ - [9. MCP servers](#9-mcp-servers)
86
+ - [10. Session persistence](#10-session-persistence)
87
+ - [11. Shipping a web app](#11-shipping-a-web-app)
88
+ - [12. Evaluations with LangSmith](#12-evaluations-with-langsmith)
89
+ - [API reference (quick)](#api-reference-quick)
90
+
91
+ ---
92
+
93
+ ## Setup
94
+
95
+ ```bash
96
+ # From PyPI
97
+ pip install datagol-agent-harness
98
+
99
+ # With optional extras (LangSmith, OpenAI, MCP, Docker, or everything)
100
+ pip install "datagol-agent-harness[langsmith]"
101
+ pip install "datagol-agent-harness[openai]"
102
+ pip install "datagol-agent-harness[all]"
103
+
104
+ # For local development (editable)
105
+ pip install -e ".[all]"
106
+ ```
107
+
108
+ Set your key (a root `.env` is auto-loaded by the `examples` package):
109
+
110
+ ```bash
111
+ ANTHROPIC_API_KEY=sk-ant-...
112
+ # or, for OpenAI-backed agents:
113
+ OPENAI_API_KEY=sk-...
114
+ ```
115
+
116
+ Run examples as modules from the repo root (not as plain scripts):
117
+
118
+ ```bash
119
+ python -m examples.simple_chat
120
+ ```
121
+
122
+ ---
123
+
124
+ ## 1. Creating an agent
125
+
126
+ `Agent` is the core agentic loop: LLM reasoning → tool execution → repeat,
127
+ until the model stops with `end_turn`.
128
+
129
+ ```python
130
+ import asyncio
131
+ from datagol_agent_harness import Agent, AgentConfig
132
+
133
+ agent = Agent(
134
+ config=AgentConfig(
135
+ model="claude-sonnet-4-6", # or "gpt-5"
136
+ provider="anthropic", # or "openai"
137
+ system_prompt="You are a helpful assistant.",
138
+ max_tokens=8192,
139
+ max_iterations=50, # loop guardrail
140
+ temperature=0.0,
141
+ )
142
+ )
143
+
144
+ print(asyncio.run(agent.run("What is 17 + 25?")))
145
+ ```
146
+
147
+ `AgentConfig` fields (all optional, these are the defaults):
148
+
149
+ | Field | Default | Purpose |
150
+ |---|---|---|
151
+ | `model` | `"claude-sonnet-4-6"` | Model id passed to the provider |
152
+ | `provider` | `"anthropic"` | `"anthropic"` or `"openai"` |
153
+ | `max_tokens` | `8192` | Per-response token cap |
154
+ | `max_iterations` | `50` | Max loop iterations before `MaxIterationsError` |
155
+ | `system_prompt` | `"You are a helpful assistant."` | System prompt |
156
+ | `temperature` | `0.0` | Sampling temperature |
157
+ | `max_result_chars` | `12000` | Tool-result eviction threshold in memory |
158
+
159
+ You can also inject your own pieces — everything is a constructor parameter:
160
+
161
+ ```python
162
+ from datagol_agent_harness import (
163
+ Agent, AgentConfig, ConversationMemory, HookManager,
164
+ MiddlewarePipeline, PermissionManager, ToolRegistry,
165
+ )
166
+
167
+ agent = Agent(
168
+ config=AgentConfig(system_prompt="..."),
169
+ tools=ToolRegistry(), # your own registry
170
+ memory=ConversationMemory(), # your own memory
171
+ permissions=PermissionManager(), # your own permission policy
172
+ hooks=HookManager(), # lifecycle hooks
173
+ middleware=MiddlewarePipeline(), # request/response transforms
174
+ )
175
+ ```
176
+
177
+ ### Conversation loop
178
+
179
+ `agent.run()` keeps the conversation in memory, so calling it repeatedly is a
180
+ multi-turn chat:
181
+
182
+ ```python
183
+ async def chat(agent):
184
+ while True:
185
+ text = input("You: ").strip()
186
+ if text in ("", "quit"):
187
+ break
188
+ print("Agent:", await agent.run(text))
189
+ ```
190
+
191
+ ---
192
+
193
+ ## 2. Registering tools
194
+
195
+ A tool is any Python function. The registry converts its signature, type
196
+ hints, and docstring into a JSON schema for the model, and dispatches calls
197
+ back to the function.
198
+
199
+ ### Decorator registration
200
+
201
+ ```python
202
+ from datagol_agent_harness import PermissionLevel
203
+
204
+ @agent.tools.register(permission=PermissionLevel.ALLOW)
205
+ def add(a: int, b: int) -> int:
206
+ """Add two numbers.""" # becomes the tool description
207
+ return a + b
208
+
209
+ @agent.tools.register(permission=PermissionLevel.ASK)
210
+ async def read_file(path: str) -> str:
211
+ """Read a file from disk.
212
+
213
+ Args:
214
+ path: Absolute path to the file. # becomes the param description
215
+ """
216
+ with open(path) as f:
217
+ return f.read()
218
+ ```
219
+
220
+ Rules of thumb:
221
+
222
+ - The **first line of the docstring** is the tool description the model sees.
223
+ - An `Args:` section documents parameters.
224
+ - Type hints drive the JSON schema: `str`, `int`, `float`, `bool`, `list[X]`,
225
+ `dict`. Optional params (with defaults) are not required.
226
+ - **Pitfall:** avoid `list[str] | None` style unions — the schema generator
227
+ can't express them and will fall back to `"type": "string"`. Give optional
228
+ params a plain default and type instead, or use `register_with_schema`
229
+ (below) for exact control.
230
+ - Sync and async handlers both work. Errors never crash the agent — they are
231
+ returned to the model as `is_error` tool results.
232
+
233
+ ### Explicit schema registration
234
+
235
+ When you need a schema the signature can't express (nested objects, arrays
236
+ with item types, `oneOf`-style choices):
237
+
238
+ ```python
239
+ async def propose_items(query: str = "", items=None, intro_text: str = "") -> str:
240
+ if (query and items) or (not query and not items):
241
+ return "Error: provide exactly one of `query` or `items`."
242
+ resolved = [query] if query else items
243
+ return f"Presented {len(resolved)} item(s)."
244
+
245
+ agent.tools.register_with_schema(
246
+ name="propose_items",
247
+ description="Propose items, via query (user's words) or items (your strings).",
248
+ input_schema={
249
+ "type": "object",
250
+ "properties": {
251
+ "query": {"type": "string", "description": "User's words, untouched."},
252
+ "items": {
253
+ "type": "array",
254
+ "items": {"type": "string"},
255
+ "description": "Item strings you wrote yourself.",
256
+ },
257
+ "intro_text": {"type": "string"},
258
+ },
259
+ },
260
+ handler=propose_items,
261
+ permission=PermissionLevel.ALLOW,
262
+ )
263
+ ```
264
+
265
+ ### Built-in tools
266
+
267
+ `datagol_agent_harness.builtin` ships ready-made tool sets:
268
+
269
+ ```python
270
+ from datagol_agent_harness.builtin import register_all_tools
271
+
272
+ register_all_tools(agent.tools)
273
+ # read_file, write_file, list_directory, run_bash, fetch_url, memory tools...
274
+ ```
275
+
276
+ Or individually: `register_filesystem_tools`, `register_bash_tools`,
277
+ `register_web_tools`, `register_memory_tools`.
278
+
279
+ ### Inspecting and executing
280
+
281
+ ```python
282
+ agent.tools.list_tools() # ['add', 'read_file', ...]
283
+ agent.tools.get_tool_params() # Anthropic-format tool schemas
284
+ result = await agent.tools.execute(call) # manual dispatch (ToolCall -> ToolResult)
285
+ ```
286
+
287
+ ---
288
+
289
+ ## 3. Streaming agents
290
+
291
+ `StreamingAgent` yields typed events as the model generates, instead of
292
+ returning one final string. It mirrors `Agent`'s constructor.
293
+
294
+ ```python
295
+ import asyncio
296
+ from datagol_agent_harness import AgentConfig, StreamingAgent, StreamEventType
297
+
298
+ agent = StreamingAgent(config=AgentConfig(system_prompt="You are helpful."))
299
+
300
+ async def main():
301
+ async for event in agent.run_stream("Tell me a story"):
302
+ if event.type == StreamEventType.TEXT_DELTA:
303
+ print(event.data, end="", flush=True)
304
+ elif event.type == StreamEventType.TOOL_CALL_START:
305
+ print(f"\n> calling {event.data.name}({event.data.input})")
306
+ elif event.type == StreamEventType.TOOL_RESULT:
307
+ if event.data.is_error:
308
+ print(f" tool error: {event.data.content}")
309
+ elif event.type == StreamEventType.TURN_COMPLETE:
310
+ print()
311
+
312
+ asyncio.run(main())
313
+ ```
314
+
315
+ Event types: `TEXT_DELTA`, `TEXT_COMPLETE`, `TOOL_CALL_START`,
316
+ `TOOL_CALL_COMPLETE`, `TOOL_RESULT`, `THINKING_DELTA`, `TURN_COMPLETE`,
317
+ `ERROR`.
318
+
319
+ Usage stats accumulate on the guardrails engine:
320
+
321
+ ```python
322
+ agent.guardrails.usage_summary
323
+ # {'iterations': 3, 'input_tokens': 5120, 'output_tokens': 640, 'estimated_cost': '$0.0250'}
324
+ ```
325
+
326
+ ---
327
+
328
+ ## 4. Multi-agent orchestration
329
+
330
+ There is no special "multi-agent" class — a sub-agent is just an `Agent`
331
+ wrapped in a tool. The orchestrator's LLM decides when to delegate; the tool
332
+ runs the specialist to completion and returns its text as the tool result.
333
+
334
+ ```python
335
+ import asyncio
336
+ from datagol_agent_harness import Agent, AgentConfig, PermissionLevel
337
+ from datagol_agent_harness.builtin.web import register_web_tools
338
+
339
+
340
+ async def run_research_agent(query: str) -> str:
341
+ """A specialist with its own prompt, tools, and iteration budget."""
342
+ researcher = Agent(
343
+ config=AgentConfig(
344
+ system_prompt="You are a research specialist. Cite sources.",
345
+ max_iterations=10,
346
+ )
347
+ )
348
+ register_web_tools(researcher.tools)
349
+ researcher.permissions.set_permission("fetch_url", PermissionLevel.ALLOW)
350
+ return await researcher.run(query)
351
+
352
+
353
+ orchestrator = Agent(
354
+ config=AgentConfig(
355
+ system_prompt=(
356
+ "You manage a team of specialists. Delegate research to "
357
+ "delegate_research; synthesize results for the user."
358
+ ),
359
+ max_iterations=20,
360
+ )
361
+ )
362
+
363
+ @orchestrator.tools.register(permission=PermissionLevel.ALLOW)
364
+ async def delegate_research(query: str) -> str:
365
+ """Delegate a research question to the research specialist.
366
+
367
+ Args:
368
+ query: The research question.
369
+ """
370
+ return await run_research_agent(query)
371
+
372
+
373
+ print(asyncio.run(orchestrator.run("Compare SQLite and DuckDB for analytics.")))
374
+ ```
375
+
376
+ Patterns that work well:
377
+
378
+ - **Give each specialist a tight system prompt and a small tool set.** The
379
+ orchestrator's prompt should list its delegation tools and when to use them.
380
+ - **Cap `max_iterations` per specialist** so a runaway sub-agent can't burn
381
+ the budget.
382
+ - Sub-agents are created per call (stateless). If a specialist needs
383
+ continuity, keep one instance and reuse it across calls.
384
+ - Sub-agents can themselves delegate — the pattern composes.
385
+
386
+ A full working version with three specialists lives in
387
+ `examples/multi_agent.py` (`python -m examples.multi_agent`).
388
+
389
+ ---
390
+
391
+ ## 5. Skills
392
+
393
+ A skill is a markdown file with YAML frontmatter — a way to ship reusable
394
+ instruction packs that load **lazily**: the agent only sees each skill's
395
+ name + description in its system prompt, and pulls the full body into context
396
+ by calling the `Skill` tool when a request matches.
397
+
398
+ ### Authoring a skill
399
+
400
+ `skills/commit-message/SKILL.md`:
401
+
402
+ ```markdown
403
+ ---
404
+ name: commit-message
405
+ description: Write a conventional commit message for a described change.
406
+ ---
407
+
408
+ You write commit messages. Rules:
409
+ - Conventional Commits format: type(scope): subject
410
+ - Subject in imperative mood, <= 72 chars, no trailing period
411
+ - Add a body only when the "why" isn't obvious from the subject
412
+ ```
413
+
414
+ A path can be a folder containing `SKILL.md`, or a markdown file directly.
415
+
416
+ ### Loading skills into an agent
417
+
418
+ ```python
419
+ from datagol_agent_harness import Agent, AgentConfig
420
+
421
+ agent = Agent(
422
+ config=AgentConfig(
423
+ system_prompt=(
424
+ "You are an engineering assistant. When a request matches a "
425
+ "skill in <available-skills>, call the Skill tool first to load "
426
+ "its instructions, then follow them."
427
+ ),
428
+ ),
429
+ skills=["./skills/commit-message", "./skills/code-review"],
430
+ )
431
+ ```
432
+
433
+ The `skills=` argument accepts a list of paths or a `SkillManager`:
434
+
435
+ ```python
436
+ from datagol_agent_harness import SkillManager
437
+
438
+ manager = SkillManager.from_paths(["./skills/commit-message"])
439
+ agent = Agent(config=AgentConfig(system_prompt="..."), skills=manager)
440
+
441
+ for s in agent.skills.list():
442
+ print(s.name, "-", s.description)
443
+ ```
444
+
445
+ ### Reacting to skill invocation
446
+
447
+ A `SKILL_INVOKED` hook fires when the model loads a skill:
448
+
449
+ ```python
450
+ from datagol_agent_harness import HookContext, HookEvent
451
+
452
+ async def on_skill(ctx: HookContext):
453
+ if ctx.data.get("found"):
454
+ print(f"loaded skill: {ctx.data['skill']} ({ctx.data['body_chars']} chars)")
455
+
456
+ agent.hooks.on(HookEvent.SKILL_INVOKED, on_skill)
457
+ ```
458
+
459
+ Full demo: `examples/skills_agent.py`, with sample skills under
460
+ `examples/skills/`.
461
+
462
+ ---
463
+
464
+ ## 6. Memory
465
+
466
+ Three layers, all optional:
467
+
468
+ ### Conversation memory (default)
469
+
470
+ Every agent has a `ConversationMemory` that stores the message list and trims
471
+ itself when approaching the context limit. You rarely touch it directly, but
472
+ you can:
473
+
474
+ ```python
475
+ agent.memory.get_messages() # current message list
476
+ agent.memory.set_messages(saved) # restore
477
+ ```
478
+
479
+ ### Agent memory — markdown notes on disk
480
+
481
+ `AgentMemory` persists titled, tagged notes as markdown files. You expose it
482
+ to the model through tools you write:
483
+
484
+ ```python
485
+ from datagol_agent_harness import AgentMemory, PermissionLevel
486
+
487
+ memory = AgentMemory(storage_dir=".agent_memory/agent")
488
+
489
+ @agent.tools.register(permission=PermissionLevel.ALLOW)
490
+ async def save_memory(title: str, content: str, tags: str = "") -> str:
491
+ """Save a note to persistent memory.
492
+
493
+ Args:
494
+ title: Short title for the memory.
495
+ content: The information to remember.
496
+ tags: Comma-separated tags.
497
+ """
498
+ tag_list = [t.strip() for t in tags.split(",") if t.strip()]
499
+ filename = memory.save(title, content, tags=tag_list)
500
+ return f"Saved '{title}' -> {filename}"
501
+
502
+ @agent.tools.register(permission=PermissionLevel.ALLOW)
503
+ async def search_memory(query: str = "") -> str:
504
+ """Search saved memories by keyword."""
505
+ results = memory.search(query=query)
506
+ return "\n".join(f"- {r['title']} [{r['filename']}]" for r in results) or "None found."
507
+ ```
508
+
509
+ ### Long-term memory — structured facts
510
+
511
+ `LongTermMemory` stores categorized facts with ids:
512
+
513
+ ```python
514
+ from datagol_agent_harness import LongTermMemory
515
+
516
+ long_term = LongTermMemory(storage_dir=".agent_memory/long_term")
517
+ fact_id = long_term.save("User prefers dark mode", category="preference")
518
+ long_term.search(query="dark mode")
519
+ long_term.update(fact_id, "User prefers light mode")
520
+ ```
521
+
522
+ Tell the agent about these tools in the system prompt and when to use them
523
+ ("proactively save useful information"). A complete two-layer example is in
524
+ `examples/memory_agent.py`.
525
+
526
+ ---
527
+
528
+ ## 7. Permissions and guardrails
529
+
530
+ ### Permission levels
531
+
532
+ Every tool has a level: `ALLOW` (run silently), `ASK` (prompt the user
533
+ y/n/always), or `DENY` (never run). The default at registration is `ASK`.
534
+
535
+ ```python
536
+ from datagol_agent_harness import PermissionLevel
537
+
538
+ # At registration
539
+ @agent.tools.register(permission=PermissionLevel.ALLOW)
540
+ def add(a: int, b: int) -> int: ...
541
+
542
+ # Or override later
543
+ agent.permissions.set_permission("run_bash", PermissionLevel.ASK)
544
+ agent.permissions.grant_session("read_file") # pre-approve for this session
545
+ ```
546
+
547
+ `ASK` prompts on stdin in terminal apps. In a server context, register tools
548
+ as `ALLOW` (or build your own `PermissionManager` subclass that asks over
549
+ your transport).
550
+
551
+ ### Guardrails
552
+
553
+ ```python
554
+ from datagol_agent_harness import GuardrailsEngine, MaxIterationsError, CostLimitError
555
+
556
+ agent.guardrails.max_cost_dollars = 0.50 # raises CostLimitError past this
557
+ agent.guardrails.usage_summary # tokens + estimated cost so far
558
+ agent.guardrails.reset() # reset counters
559
+ ```
560
+
561
+ `max_iterations` comes from `AgentConfig` and raises `MaxIterationsError`.
562
+
563
+ ---
564
+
565
+ ## 8. Hooks and middleware
566
+
567
+ ### Hooks — observe the lifecycle
568
+
569
+ ```python
570
+ from datagol_agent_harness import HookContext, HookEvent
571
+
572
+ @agent.hooks.before_tool
573
+ async def log_call(ctx: HookContext):
574
+ tc = ctx.data["tool_call"]
575
+ print(f"-> {tc.name}({tc.input})")
576
+
577
+ @agent.hooks.after_tool
578
+ async def log_result(ctx: HookContext):
579
+ result = ctx.data["result"]
580
+ if result.is_error:
581
+ print(f"tool failed: {result.content}")
582
+
583
+ agent.hooks.on(HookEvent.LLM_RESPONSE, lambda ctx: print(ctx.data["stop_reason"]))
584
+ ```
585
+
586
+ Events: `AGENT_START`, `AGENT_END`, `LOOP_ITERATION_START`,
587
+ `LOOP_ITERATION_END`, `LLM_REQUEST`, `LLM_RESPONSE`, `TOOL_CALL_START`,
588
+ `TOOL_CALL_END`, `SKILL_INVOKED`, `SANDBOX_EXEC`, `CHECKPOINT`, `ERROR`.
589
+
590
+ ### Middleware — transform requests and results
591
+
592
+ Subclass `Middleware` to mutate messages, tool calls, or results as they flow:
593
+
594
+ ```python
595
+ from datagol_agent_harness import Middleware
596
+
597
+ class RedactSecrets(Middleware):
598
+ async def before_tool_execution(self, tool_call):
599
+ if "api_key" in str(tool_call.input):
600
+ tool_call.input = {k: ("***" if "key" in k else v)
601
+ for k, v in tool_call.input.items()}
602
+ return tool_call
603
+
604
+ agent.middleware.add(RedactSecrets())
605
+ ```
606
+
607
+ Overridable stages: `before_llm_call`, `after_llm_call`,
608
+ `before_tool_execution`, `after_tool_execution`.
609
+
610
+ ---
611
+
612
+ ## 9. MCP servers
613
+
614
+ `MCPManager` connects to Model Context Protocol servers (stdio subprocess or
615
+ remote SSE) and registers their tools into your agent's registry, so MCP
616
+ tools look exactly like native tools to the model.
617
+
618
+ ```python
619
+ from datagol_agent_harness import Agent, AgentConfig, MCPManager
620
+
621
+ mcp = MCPManager()
622
+ await mcp.connect(
623
+ "filesystem",
624
+ command="npx",
625
+ args=["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
626
+ )
627
+ # or a remote server:
628
+ # await mcp.connect("remote", url="http://localhost:8000/sse")
629
+
630
+ agent = Agent(config=AgentConfig(system_prompt="..."), mcp=mcp)
631
+ mcp.register_tools(agent.tools)
632
+
633
+ print(await agent.run("What's in /tmp?"))
634
+ await mcp.disconnect_all()
635
+ ```
636
+
637
+ See `examples/mcp_agent.py` for the interactive version.
638
+
639
+ ---
640
+
641
+ ## 10. Session persistence
642
+
643
+ Save and restore an agent's conversation:
644
+
645
+ ```python
646
+ session_id = await agent.save_session() # -> writes .agent_sessions/
647
+
648
+ # later, or in another process:
649
+ from datagol_agent_harness import Agent
650
+ restored = await Agent.load_session(session_id, config=AgentConfig(system_prompt="..."))
651
+ await restored.run("Where were we?")
652
+ ```
653
+
654
+ For managed multi-session services (checkpointing, pause/resume, expiry),
655
+ see `AgentRuntime` in `datagol_agent_harness/runtime.py`.
656
+
657
+ ---
658
+
659
+ ## 11. Shipping a web app
660
+
661
+ The harness is transport-agnostic. The reference implementation in
662
+ `examples/web_app` demonstrates:
663
+
664
+ 1. **Streaming agent execution** translated to Server-Sent Events (SSE) or WebSockets:
665
+
666
+ ```python
667
+ from fastapi import FastAPI
668
+ from fastapi.responses import StreamingResponse
669
+ from datagol_agent_harness import AgentConfig, StreamingAgent, StreamEventType
670
+ import json
671
+
672
+ app = FastAPI()
673
+ agent = StreamingAgent(config=AgentConfig(system_prompt="You are helpful."))
674
+
675
+ @app.post("/api/stream")
676
+ async def stream(req: dict):
677
+ async def event_generator():
678
+ async for event in agent.run_stream(req["message"]):
679
+ if event.type == StreamEventType.TEXT_DELTA:
680
+ yield f"data: {json.dumps({'type': 'text_delta', 'content': event.data})}\n\n"
681
+ elif event.type == StreamEventType.TOOL_CALL_START:
682
+ yield f"data: {json.dumps({'type': 'tool_call', 'name': event.data.name})}\n\n"
683
+ yield "data: [DONE]\n\n"
684
+
685
+ return StreamingResponse(event_generator(), media_type="text/event-stream")
686
+ ```
687
+
688
+ 2. **Register tools as `PermissionLevel.ALLOW`** — in a server context, tools should be pre-approved or gated via an authorization middleware rather than interactive stdin prompts.
689
+ 3. **Built-in UI and MCP integrations** — the web app serves a complete browser UI that displays tool calls, token usage, MCP servers, and dynamic skills.
690
+
691
+ Run the full example from the repo root:
692
+
693
+ ```bash
694
+ uvicorn examples.web_app.server:app --port 8000 --reload
695
+ # open http://localhost:8000
696
+ ```
697
+
698
+ ---
699
+
700
+ ## 12. Evaluations with LangSmith
701
+
702
+ The harness includes first-class evaluation capabilities powered by the
703
+ **LangSmith evaluation framework** (`evaluate` / `aevaluate`). You can benchmark
704
+ tool selection, skill routing, multi-agent delegation, and guardrails either
705
+ locally (offline, zero-cost) or in the LangSmith Web UI.
706
+
707
+ ### Running an evaluation
708
+
709
+ ```python
710
+ from datagol_agent_harness import Agent, AgentConfig, PermissionLevel
711
+ from datagol_agent_harness.evals import (
712
+ build_example,
713
+ default_evaluators,
714
+ evaluate_agent,
715
+ tool_selection_evaluator,
716
+ contains_evaluator,
717
+ )
718
+
719
+ # 1. Define your agent factory or instance
720
+ def make_agent(inputs):
721
+ agent = Agent(config=AgentConfig(model="claude-sonnet-4-6"))
722
+ @agent.tools.register(permission=PermissionLevel.ALLOW)
723
+ def calculate(expression: str) -> str:
724
+ return str(eval(expression, {"__builtins__": None}, {}))
725
+ return agent
726
+
727
+ # 2. Define test cases or use built-in suites ("tool_calling", "skills", "all")
728
+ dataset = [
729
+ build_example(
730
+ inputs={"prompt": "What is 144 / 12?"},
731
+ outputs={
732
+ "expected_tools": ["calculate"],
733
+ "contains_all": ["12"],
734
+ "max_allowed_iterations": 3,
735
+ },
736
+ )
737
+ ]
738
+
739
+ # 3. Evaluate (offline for fast CI/CD tests, or live to sync to LangSmith)
740
+ summary = evaluate_agent(
741
+ agent=make_agent,
742
+ dataset=dataset,
743
+ evaluators=[tool_selection_evaluator, contains_evaluator],
744
+ experiment_prefix="math-agent-benchmark",
745
+ offline=True, # set False or omit when LANGSMITH_API_KEY is present
746
+ )
747
+
748
+ print(f"Pass rate: {summary.pass_rate * 100:.1f}%")
749
+ ```
750
+
751
+ ### Running evals from the CLI
752
+
753
+ ```bash
754
+ # Run tool calling benchmarks offline (no API keys required)
755
+ python -m datagol_agent_harness.evals.cli --suite tool_calling --offline
756
+
757
+ # Run against Anthropic and upload results + traces to LangSmith
758
+ export LANGSMITH_API_KEY="lsv2_pt_..."
759
+ python -m datagol_agent_harness.evals.cli --suite skills --model claude-sonnet-4-6 --upload
760
+
761
+ # Run all benchmark suites with concurrency
762
+ python -m datagol_agent_harness.evals.cli --suite all --concurrency 2
763
+ ```
764
+
765
+ When uploaded, the CLI prints a clickable LangSmith URL to inspect row-level scores,
766
+ side-by-side prompt diffs, and the complete nested execution tree for every turn.
767
+
768
+ ---
769
+
770
+ ## API reference (quick)
771
+
772
+ | Class / function | Module | Purpose |
773
+ |---|---|---|
774
+ | `Agent` | `datagol_agent_harness` | Core agentic loop (`await agent.run(msg)`) |
775
+ | `StreamingAgent` | `datagol_agent_harness` | Event-streaming variant (`run_stream`) |
776
+ | `AgentConfig` | `datagol_agent_harness` | Model, provider, limits, prompt |
777
+ | `ToolRegistry` | `datagol_agent_harness` | `register`, `register_with_schema`, `execute` |
778
+ | `PermissionLevel` | `datagol_agent_harness` | `ALLOW` / `ASK` / `DENY` |
779
+ | `PermissionManager` | `datagol_agent_harness` | Per-tool overrides, session grants |
780
+ | `GuardrailsEngine` | `datagol_agent_harness` | Iteration/cost limits, usage stats |
781
+ | `ConversationMemory` | `datagol_agent_harness` | Message list with auto-trimming |
782
+ | `AgentMemory` / `LongTermMemory` | `datagol_agent_harness` | Disk-backed notes / facts |
783
+ | `PersistentMemory` | `datagol_agent_harness` | Session save/load |
784
+ | `HookManager` / `HookEvent` | `datagol_agent_harness` | Lifecycle hooks |
785
+ | `Middleware` / `MiddlewarePipeline` | `datagol_agent_harness` | Request/result transforms |
786
+ | `SkillManager` | `datagol_agent_harness` | Lazy skill loading |
787
+ | `MCPManager` | `datagol_agent_harness` | MCP server connections |
788
+ | `Sandbox` | `datagol_agent_harness` | Sandboxed code execution |
789
+ | `AgentRuntime` | `datagol_agent_harness` | Managed sessions, checkpoints |
790
+ | `Extension` / `LangSmithExtension` | `datagol_agent_harness` | Pluggable runtime extensions / LangSmith tracing |
791
+ | `evaluate_agent` | `datagol_agent_harness.evals` | LangSmith evaluation runner |
792
+ | `AgentTarget` | `datagol_agent_harness.evals` | Target adapter with telemetry & trace linking |
793
+ | `default_evaluators` | `datagol_agent_harness.evals` | Standard suite of evaluators |
794
+ | `register_all_tools` | `datagol_agent_harness.builtin` | Filesystem, bash, web, memory tools |
795
+
796
+ ## Runnable examples
797
+
798
+ | Example | Shows |
799
+ |---|---|
800
+ | `python -m examples.simple_chat` | Streaming interactive chat + basic tools |
801
+ | `python -m examples.multi_agent` | Orchestrator + specialist agents |
802
+ | `python -m examples.langsmith_tracing` | LangSmith lifecycle tracing + multi-agent nesting |
803
+ | `python -m examples.run_evals` | Agent evaluation suite with LangSmith |
804
+ | `python -m examples.skills_agent` | Lazy skill loading |
805
+ | `python -m examples.memory_agent` | Two-layer persistent memory |
806
+ | `python -m examples.mcp_agent` | MCP tool integration |
807
+ | `python -m examples.coding_agent` | Full coding assistant |
808
+ | `python -m examples.sandboxed_coder` | Sandboxed execution |
809
+ | `uvicorn examples.web_app.server:app --port 8000` | Web app with streaming UI, MCP, and skills |