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.
@@ -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
+ ]