harness-runtime 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.
- harness_runtime-0.1.0/PKG-INFO +136 -0
- harness_runtime-0.1.0/README.md +109 -0
- harness_runtime-0.1.0/cli.py +116 -0
- harness_runtime-0.1.0/core/__init__.py +47 -0
- harness_runtime-0.1.0/core/builder.py +353 -0
- harness_runtime-0.1.0/core/event_publisher.py +108 -0
- harness_runtime-0.1.0/core/executor.py +227 -0
- harness_runtime-0.1.0/core/model_factory.py +88 -0
- harness_runtime-0.1.0/core/model_identifier.py +77 -0
- harness_runtime-0.1.0/core/session.py +60 -0
- harness_runtime-0.1.0/core/state_schema_builder.py +109 -0
- harness_runtime-0.1.0/core/subagent_builder.py +264 -0
- harness_runtime-0.1.0/core/tool_loader.py +158 -0
- harness_runtime-0.1.0/harness_runtime.egg-info/PKG-INFO +136 -0
- harness_runtime-0.1.0/harness_runtime.egg-info/SOURCES.txt +21 -0
- harness_runtime-0.1.0/harness_runtime.egg-info/dependency_links.txt +1 -0
- harness_runtime-0.1.0/harness_runtime.egg-info/entry_points.txt +2 -0
- harness_runtime-0.1.0/harness_runtime.egg-info/requires.txt +20 -0
- harness_runtime-0.1.0/harness_runtime.egg-info/top_level.txt +3 -0
- harness_runtime-0.1.0/models/__init__.py +35 -0
- harness_runtime-0.1.0/models/frames.py +156 -0
- harness_runtime-0.1.0/pyproject.toml +88 -0
- harness_runtime-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: harness-runtime
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangGraph Agent Execution Engine — stdio subprocess pattern
|
|
5
|
+
Author: Bizmatters Team
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: deepagents>=0.2.7
|
|
9
|
+
Requires-Dist: langgraph>=1.0.0
|
|
10
|
+
Requires-Dist: langgraph-checkpoint-postgres>=3.0.0
|
|
11
|
+
Requires-Dist: langchain>=1.0.0
|
|
12
|
+
Requires-Dist: langchain-openai>=0.3.0
|
|
13
|
+
Requires-Dist: langchain-anthropic>=1.0.0
|
|
14
|
+
Requires-Dist: langsmith>=0.2.0
|
|
15
|
+
Requires-Dist: psycopg[binary,pool]>=3.2.0
|
|
16
|
+
Requires-Dist: structlog>=24.4.0
|
|
17
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-mock>=3.14.0; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-timeout>=2.3.0; extra == "dev"
|
|
24
|
+
Requires-Dist: black>=24.1.1; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff>=0.1.13; extra == "dev"
|
|
26
|
+
Requires-Dist: mypy>=1.8.0; extra == "dev"
|
|
27
|
+
|
|
28
|
+
# harness-runtime
|
|
29
|
+
|
|
30
|
+
LangGraph agent execution engine designed for the **stdio subprocess pattern** — spawned as a child process by the Waypoint SDK, communicating via NDJSON over stdin/stdout using the **LiteLLM frame protocol**.
|
|
31
|
+
|
|
32
|
+
## Overview
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
Waypoint SDK → spawn → harness-runtime → stdin NDJSON → turn frames → stdout NDJSON → exit
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- **Execution**: LangGraph DAG execution with PostgreSQL checkpointing
|
|
39
|
+
- **Protocol**: LiteLLM-compatible NDJSON frame protocol over stdio
|
|
40
|
+
- **Persistence**: LangGraph `PostgresSaver` for multi-turn conversations
|
|
41
|
+
- **No network, no K8s, no Redis**: Runs as a local subprocess
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Install
|
|
47
|
+
pip install .
|
|
48
|
+
|
|
49
|
+
# Run (expects NDJSON on stdin)
|
|
50
|
+
DATABASE_URL=postgresql://user:pass@localhost:5432/db harness-runtime
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Example NDJSON session
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
echo '{"type":"control_request","request_id":"req_1","request":{"subtype":"initialize","agent_definition":{...},"input_payload":{"messages":[]}}}' | \
|
|
57
|
+
DATABASE_URL=postgresql://user:pass@localhost:5432/db harness-runtime
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Protocol
|
|
61
|
+
|
|
62
|
+
See [ADR-003](../waypoint/docs/adr/003-harness-runtime-as-stdio-subprocess.md) for the full protocol specification.
|
|
63
|
+
|
|
64
|
+
### Frame Types (SDK → Server)
|
|
65
|
+
|
|
66
|
+
| Type | Purpose |
|
|
67
|
+
|------|---------|
|
|
68
|
+
| `control_request {initialize}` | Build LangGraph, init PostgresSaver |
|
|
69
|
+
| `control_request {interrupt}` | Cancel in-flight turn |
|
|
70
|
+
| `user {message}` | Each turn — runs graph with the input |
|
|
71
|
+
|
|
72
|
+
### Frame Types (Server → SDK)
|
|
73
|
+
|
|
74
|
+
| Type | Purpose |
|
|
75
|
+
|------|---------|
|
|
76
|
+
| `control_response {success}` | Ack for initialize/interrupt |
|
|
77
|
+
| `system {init}` | Start of turn — model, tools |
|
|
78
|
+
| `assistant {content}` | Complete assistant message with `text` / `tool_use` blocks |
|
|
79
|
+
| `user {content}` | Echo of internal tool results (`tool_result` blocks) |
|
|
80
|
+
| `stream_event {content_block_delta}` | Streaming LLM token deltas |
|
|
81
|
+
| `result` | **Terminates the turn** — `success`, `error_max_turns`, or `error_during_execution` |
|
|
82
|
+
|
|
83
|
+
## Architecture
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
harness-runtime/
|
|
87
|
+
├── cli.py # CLI entry point — stdin NDJSON loop
|
|
88
|
+
├── core/
|
|
89
|
+
│ ├── builder.py # GraphBuilder — builds LangGraph DAG
|
|
90
|
+
│ ├── event_publisher.py # StdioPublisher — LiteLLM frame emitter
|
|
91
|
+
│ ├── executor.py # ExecutionManager — runs graph, maps events → frames
|
|
92
|
+
│ ├── session.py # Session — multi-turn lifecycle
|
|
93
|
+
│ ├── model_factory.py # LLM model creation
|
|
94
|
+
│ ├── model_identifier.py # "provider:model" string builder
|
|
95
|
+
│ ├── subagent_builder.py # Specialist agent compilation
|
|
96
|
+
│ ├── tool_loader.py # Dynamic tool loading from script defs
|
|
97
|
+
│ └── state_schema_builder.py # Dynamic AgentState subclass creation
|
|
98
|
+
├── models/
|
|
99
|
+
│ ├── __init__.py
|
|
100
|
+
│ └── frames.py # LiteLLM frame dataclasses + content blocks
|
|
101
|
+
├── migrations/ # PostgreSQL schema migrations (checkpointer)
|
|
102
|
+
├── scripts/ci/run.sh # Docker entrypoint
|
|
103
|
+
├── Dockerfile
|
|
104
|
+
└── pyproject.toml
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Environment Variables
|
|
108
|
+
|
|
109
|
+
| Variable | Required | Description |
|
|
110
|
+
|----------|----------|-------------|
|
|
111
|
+
| `DATABASE_URL` | Yes | PostgreSQL connection string for LangGraph checkpointer |
|
|
112
|
+
| `USE_MOCK_LLM` | No | `true` to use mock LLM (default: `false`) |
|
|
113
|
+
| `LLM_MODEL_NAME` | No | Model name for real LLM calls (default: `gpt-4o-mini`) |
|
|
114
|
+
|
|
115
|
+
LLM provider API keys are read from environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc.) or from Agent Vault via proxy env vars.
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Install with dev dependencies
|
|
121
|
+
pip install -e ".[dev]"
|
|
122
|
+
|
|
123
|
+
# Code quality
|
|
124
|
+
black .
|
|
125
|
+
ruff check .
|
|
126
|
+
mypy .
|
|
127
|
+
|
|
128
|
+
# Testing
|
|
129
|
+
pytest
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## References
|
|
133
|
+
|
|
134
|
+
- [ADR-003](../waypoint/docs/adr/003-harness-runtime-as-stdio-subprocess.md)
|
|
135
|
+
- [LiteLLM Frame Protocol](../waypoint/archived/agents/lite-harness/src/sdk/PROTOCOL.md)
|
|
136
|
+
- [LangGraph Documentation](https://python.langchain.com/docs/langgraph)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# harness-runtime
|
|
2
|
+
|
|
3
|
+
LangGraph agent execution engine designed for the **stdio subprocess pattern** — spawned as a child process by the Waypoint SDK, communicating via NDJSON over stdin/stdout using the **LiteLLM frame protocol**.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Waypoint SDK → spawn → harness-runtime → stdin NDJSON → turn frames → stdout NDJSON → exit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- **Execution**: LangGraph DAG execution with PostgreSQL checkpointing
|
|
12
|
+
- **Protocol**: LiteLLM-compatible NDJSON frame protocol over stdio
|
|
13
|
+
- **Persistence**: LangGraph `PostgresSaver` for multi-turn conversations
|
|
14
|
+
- **No network, no K8s, no Redis**: Runs as a local subprocess
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Install
|
|
20
|
+
pip install .
|
|
21
|
+
|
|
22
|
+
# Run (expects NDJSON on stdin)
|
|
23
|
+
DATABASE_URL=postgresql://user:pass@localhost:5432/db harness-runtime
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Example NDJSON session
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
echo '{"type":"control_request","request_id":"req_1","request":{"subtype":"initialize","agent_definition":{...},"input_payload":{"messages":[]}}}' | \
|
|
30
|
+
DATABASE_URL=postgresql://user:pass@localhost:5432/db harness-runtime
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Protocol
|
|
34
|
+
|
|
35
|
+
See [ADR-003](../waypoint/docs/adr/003-harness-runtime-as-stdio-subprocess.md) for the full protocol specification.
|
|
36
|
+
|
|
37
|
+
### Frame Types (SDK → Server)
|
|
38
|
+
|
|
39
|
+
| Type | Purpose |
|
|
40
|
+
|------|---------|
|
|
41
|
+
| `control_request {initialize}` | Build LangGraph, init PostgresSaver |
|
|
42
|
+
| `control_request {interrupt}` | Cancel in-flight turn |
|
|
43
|
+
| `user {message}` | Each turn — runs graph with the input |
|
|
44
|
+
|
|
45
|
+
### Frame Types (Server → SDK)
|
|
46
|
+
|
|
47
|
+
| Type | Purpose |
|
|
48
|
+
|------|---------|
|
|
49
|
+
| `control_response {success}` | Ack for initialize/interrupt |
|
|
50
|
+
| `system {init}` | Start of turn — model, tools |
|
|
51
|
+
| `assistant {content}` | Complete assistant message with `text` / `tool_use` blocks |
|
|
52
|
+
| `user {content}` | Echo of internal tool results (`tool_result` blocks) |
|
|
53
|
+
| `stream_event {content_block_delta}` | Streaming LLM token deltas |
|
|
54
|
+
| `result` | **Terminates the turn** — `success`, `error_max_turns`, or `error_during_execution` |
|
|
55
|
+
|
|
56
|
+
## Architecture
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
harness-runtime/
|
|
60
|
+
├── cli.py # CLI entry point — stdin NDJSON loop
|
|
61
|
+
├── core/
|
|
62
|
+
│ ├── builder.py # GraphBuilder — builds LangGraph DAG
|
|
63
|
+
│ ├── event_publisher.py # StdioPublisher — LiteLLM frame emitter
|
|
64
|
+
│ ├── executor.py # ExecutionManager — runs graph, maps events → frames
|
|
65
|
+
│ ├── session.py # Session — multi-turn lifecycle
|
|
66
|
+
│ ├── model_factory.py # LLM model creation
|
|
67
|
+
│ ├── model_identifier.py # "provider:model" string builder
|
|
68
|
+
│ ├── subagent_builder.py # Specialist agent compilation
|
|
69
|
+
│ ├── tool_loader.py # Dynamic tool loading from script defs
|
|
70
|
+
│ └── state_schema_builder.py # Dynamic AgentState subclass creation
|
|
71
|
+
├── models/
|
|
72
|
+
│ ├── __init__.py
|
|
73
|
+
│ └── frames.py # LiteLLM frame dataclasses + content blocks
|
|
74
|
+
├── migrations/ # PostgreSQL schema migrations (checkpointer)
|
|
75
|
+
├── scripts/ci/run.sh # Docker entrypoint
|
|
76
|
+
├── Dockerfile
|
|
77
|
+
└── pyproject.toml
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Environment Variables
|
|
81
|
+
|
|
82
|
+
| Variable | Required | Description |
|
|
83
|
+
|----------|----------|-------------|
|
|
84
|
+
| `DATABASE_URL` | Yes | PostgreSQL connection string for LangGraph checkpointer |
|
|
85
|
+
| `USE_MOCK_LLM` | No | `true` to use mock LLM (default: `false`) |
|
|
86
|
+
| `LLM_MODEL_NAME` | No | Model name for real LLM calls (default: `gpt-4o-mini`) |
|
|
87
|
+
|
|
88
|
+
LLM provider API keys are read from environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc.) or from Agent Vault via proxy env vars.
|
|
89
|
+
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Install with dev dependencies
|
|
94
|
+
pip install -e ".[dev]"
|
|
95
|
+
|
|
96
|
+
# Code quality
|
|
97
|
+
black .
|
|
98
|
+
ruff check .
|
|
99
|
+
mypy .
|
|
100
|
+
|
|
101
|
+
# Testing
|
|
102
|
+
pytest
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## References
|
|
106
|
+
|
|
107
|
+
- [ADR-003](../waypoint/docs/adr/003-harness-runtime-as-stdio-subprocess.md)
|
|
108
|
+
- [LiteLLM Frame Protocol](../waypoint/archived/agents/lite-harness/src/sdk/PROTOCOL.md)
|
|
109
|
+
- [LangGraph Documentation](https://python.langchain.com/docs/langgraph)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import NoReturn
|
|
6
|
+
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
from core.event_publisher import StdioPublisher
|
|
10
|
+
from core.executor import ExecutionManager
|
|
11
|
+
from core.session import Session
|
|
12
|
+
|
|
13
|
+
env_path = Path(__file__).parent / ".env"
|
|
14
|
+
load_dotenv(dotenv_path=env_path)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> None:
|
|
18
|
+
database_url = os.getenv("DATABASE_URL")
|
|
19
|
+
if not database_url:
|
|
20
|
+
_error_exit("DATABASE_URL environment variable is required", 2)
|
|
21
|
+
|
|
22
|
+
session: Session | None = None
|
|
23
|
+
execution_manager: ExecutionManager | None = None
|
|
24
|
+
publisher = StdioPublisher()
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
execution_manager = ExecutionManager(
|
|
28
|
+
postgres_connection_string=database_url,
|
|
29
|
+
publisher=publisher,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
for line in sys.stdin:
|
|
33
|
+
line = line.strip()
|
|
34
|
+
if not line:
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
msg = json.loads(line)
|
|
39
|
+
except json.JSONDecodeError:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
msg_type = msg.get("type")
|
|
43
|
+
|
|
44
|
+
if msg_type == "control_request":
|
|
45
|
+
request = msg.get("request", {})
|
|
46
|
+
subtype = request.get("subtype")
|
|
47
|
+
request_id = msg.get("request_id", "")
|
|
48
|
+
|
|
49
|
+
if subtype == "initialize":
|
|
50
|
+
agent_definition = request.get("agent_definition", {})
|
|
51
|
+
input_payload = request.get("input_payload", {})
|
|
52
|
+
|
|
53
|
+
session = Session(
|
|
54
|
+
agent_definition=agent_definition,
|
|
55
|
+
input_payload=input_payload,
|
|
56
|
+
execution_manager=execution_manager,
|
|
57
|
+
publisher=publisher,
|
|
58
|
+
)
|
|
59
|
+
session.initialize()
|
|
60
|
+
|
|
61
|
+
publisher.publish_control_response(
|
|
62
|
+
request_id=request_id,
|
|
63
|
+
session_id=session.session_id,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
elif subtype == "interrupt":
|
|
67
|
+
publisher.publish_control_response(
|
|
68
|
+
request_id=request_id,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
elif msg_type == "user":
|
|
72
|
+
if session is None:
|
|
73
|
+
publisher.publish_control_response(
|
|
74
|
+
request_id="",
|
|
75
|
+
subtype="error",
|
|
76
|
+
error="Session not initialized",
|
|
77
|
+
)
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
user_content = ""
|
|
82
|
+
user_msg = msg.get("message", {})
|
|
83
|
+
raw_content = user_msg.get("content", "")
|
|
84
|
+
if isinstance(raw_content, str):
|
|
85
|
+
user_content = raw_content
|
|
86
|
+
elif isinstance(raw_content, list):
|
|
87
|
+
texts = [
|
|
88
|
+
b.get("text", "") for b in raw_content
|
|
89
|
+
if isinstance(b, dict) and b.get("type") == "text"
|
|
90
|
+
]
|
|
91
|
+
user_content = " ".join(texts)
|
|
92
|
+
|
|
93
|
+
session.run_turn(user_content=user_content)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
publisher.publish_result(
|
|
96
|
+
session_id=session.session_id,
|
|
97
|
+
subtype="error_during_execution",
|
|
98
|
+
is_error=True,
|
|
99
|
+
result=str(e),
|
|
100
|
+
)
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
except KeyboardInterrupt:
|
|
104
|
+
pass
|
|
105
|
+
finally:
|
|
106
|
+
if execution_manager:
|
|
107
|
+
execution_manager.close()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _error_exit(message: str, code: int = 1) -> NoReturn:
|
|
111
|
+
print(message, file=sys.stderr)
|
|
112
|
+
sys.exit(code)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
main()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Core business logic for graph building and execution.
|
|
2
|
+
|
|
3
|
+
This module provides the main entry point for the agent executor service.
|
|
4
|
+
The implementation follows a flat, modular structure:
|
|
5
|
+
|
|
6
|
+
Flat Structure:
|
|
7
|
+
- builder.py: Main GraphBuilder class (entry point)
|
|
8
|
+
- model_identifier.py: Model identifier creation
|
|
9
|
+
- subagent_builder.py: Subagent compilation logic
|
|
10
|
+
- tool_loader.py: Tool loading logic
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from core import GraphBuilder
|
|
14
|
+
|
|
15
|
+
builder = GraphBuilder()
|
|
16
|
+
agent = builder.build_from_definition(definition)
|
|
17
|
+
|
|
18
|
+
Or use the modular functions directly:
|
|
19
|
+
from core import (
|
|
20
|
+
load_tools_from_definition,
|
|
21
|
+
create_model_identifier,
|
|
22
|
+
build_subagent
|
|
23
|
+
)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Main API
|
|
27
|
+
from core.builder import GraphBuilder, GraphBuilderError
|
|
28
|
+
from core.model_identifier import create_model_identifier
|
|
29
|
+
from core.subagent_builder import SubAgentCompilationError, build_subagent
|
|
30
|
+
|
|
31
|
+
# Modular functions
|
|
32
|
+
from core.tool_loader import ToolLoadingError, load_tools_from_definition
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# Main API
|
|
36
|
+
"GraphBuilder",
|
|
37
|
+
"GraphBuilderError",
|
|
38
|
+
|
|
39
|
+
# Modular functions
|
|
40
|
+
"load_tools_from_definition",
|
|
41
|
+
"create_model_identifier",
|
|
42
|
+
"build_subagent",
|
|
43
|
+
|
|
44
|
+
# Exceptions
|
|
45
|
+
"ToolLoadingError",
|
|
46
|
+
"SubAgentCompilationError"
|
|
47
|
+
]
|