hud-python 0.4.1__py3-none-any.whl → 0.4.3__py3-none-any.whl

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.

Potentially problematic release.


This version of hud-python might be problematic. Click here for more details.

Files changed (130) hide show
  1. hud/__init__.py +22 -22
  2. hud/agents/__init__.py +13 -15
  3. hud/agents/base.py +599 -599
  4. hud/agents/claude.py +373 -373
  5. hud/agents/langchain.py +261 -250
  6. hud/agents/misc/__init__.py +7 -7
  7. hud/agents/misc/response_agent.py +82 -80
  8. hud/agents/openai.py +352 -352
  9. hud/agents/openai_chat_generic.py +154 -154
  10. hud/agents/tests/__init__.py +1 -1
  11. hud/agents/tests/test_base.py +742 -742
  12. hud/agents/tests/test_claude.py +324 -324
  13. hud/agents/tests/test_client.py +363 -363
  14. hud/agents/tests/test_openai.py +237 -237
  15. hud/cli/__init__.py +617 -617
  16. hud/cli/__main__.py +8 -8
  17. hud/cli/analyze.py +371 -371
  18. hud/cli/analyze_metadata.py +230 -230
  19. hud/cli/build.py +498 -427
  20. hud/cli/clone.py +185 -185
  21. hud/cli/cursor.py +92 -92
  22. hud/cli/debug.py +392 -392
  23. hud/cli/docker_utils.py +83 -83
  24. hud/cli/init.py +280 -281
  25. hud/cli/interactive.py +353 -353
  26. hud/cli/mcp_server.py +764 -756
  27. hud/cli/pull.py +330 -336
  28. hud/cli/push.py +404 -370
  29. hud/cli/remote_runner.py +311 -311
  30. hud/cli/runner.py +160 -160
  31. hud/cli/tests/__init__.py +3 -3
  32. hud/cli/tests/test_analyze.py +284 -284
  33. hud/cli/tests/test_cli_init.py +265 -265
  34. hud/cli/tests/test_cli_main.py +27 -27
  35. hud/cli/tests/test_clone.py +142 -142
  36. hud/cli/tests/test_cursor.py +253 -253
  37. hud/cli/tests/test_debug.py +453 -453
  38. hud/cli/tests/test_mcp_server.py +139 -139
  39. hud/cli/tests/test_utils.py +388 -388
  40. hud/cli/utils.py +263 -263
  41. hud/clients/README.md +143 -143
  42. hud/clients/__init__.py +16 -16
  43. hud/clients/base.py +378 -379
  44. hud/clients/fastmcp.py +222 -222
  45. hud/clients/mcp_use.py +298 -278
  46. hud/clients/tests/__init__.py +1 -1
  47. hud/clients/tests/test_client_integration.py +111 -111
  48. hud/clients/tests/test_fastmcp.py +342 -342
  49. hud/clients/tests/test_protocol.py +188 -188
  50. hud/clients/utils/__init__.py +1 -1
  51. hud/clients/utils/retry_transport.py +160 -160
  52. hud/datasets.py +327 -322
  53. hud/misc/__init__.py +1 -1
  54. hud/misc/claude_plays_pokemon.py +292 -292
  55. hud/otel/__init__.py +35 -35
  56. hud/otel/collector.py +142 -142
  57. hud/otel/config.py +164 -164
  58. hud/otel/context.py +536 -536
  59. hud/otel/exporters.py +366 -366
  60. hud/otel/instrumentation.py +97 -97
  61. hud/otel/processors.py +118 -118
  62. hud/otel/tests/__init__.py +1 -1
  63. hud/otel/tests/test_processors.py +197 -197
  64. hud/server/__init__.py +5 -5
  65. hud/server/context.py +114 -114
  66. hud/server/helper/__init__.py +5 -5
  67. hud/server/low_level.py +132 -132
  68. hud/server/server.py +170 -166
  69. hud/server/tests/__init__.py +3 -3
  70. hud/settings.py +73 -73
  71. hud/shared/__init__.py +5 -5
  72. hud/shared/exceptions.py +180 -180
  73. hud/shared/requests.py +264 -264
  74. hud/shared/tests/test_exceptions.py +157 -157
  75. hud/shared/tests/test_requests.py +275 -275
  76. hud/telemetry/__init__.py +25 -25
  77. hud/telemetry/instrument.py +379 -379
  78. hud/telemetry/job.py +309 -309
  79. hud/telemetry/replay.py +74 -74
  80. hud/telemetry/trace.py +83 -83
  81. hud/tools/__init__.py +33 -33
  82. hud/tools/base.py +365 -365
  83. hud/tools/bash.py +161 -161
  84. hud/tools/computer/__init__.py +15 -15
  85. hud/tools/computer/anthropic.py +437 -437
  86. hud/tools/computer/hud.py +376 -376
  87. hud/tools/computer/openai.py +295 -295
  88. hud/tools/computer/settings.py +82 -82
  89. hud/tools/edit.py +314 -314
  90. hud/tools/executors/__init__.py +30 -30
  91. hud/tools/executors/base.py +539 -539
  92. hud/tools/executors/pyautogui.py +621 -621
  93. hud/tools/executors/tests/__init__.py +1 -1
  94. hud/tools/executors/tests/test_base_executor.py +338 -338
  95. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  96. hud/tools/executors/xdo.py +511 -511
  97. hud/tools/playwright.py +412 -412
  98. hud/tools/tests/__init__.py +3 -3
  99. hud/tools/tests/test_base.py +282 -282
  100. hud/tools/tests/test_bash.py +158 -158
  101. hud/tools/tests/test_bash_extended.py +197 -197
  102. hud/tools/tests/test_computer.py +425 -425
  103. hud/tools/tests/test_computer_actions.py +34 -34
  104. hud/tools/tests/test_edit.py +259 -259
  105. hud/tools/tests/test_init.py +27 -27
  106. hud/tools/tests/test_playwright_tool.py +183 -183
  107. hud/tools/tests/test_tools.py +145 -145
  108. hud/tools/tests/test_utils.py +156 -156
  109. hud/tools/types.py +72 -72
  110. hud/tools/utils.py +50 -50
  111. hud/types.py +136 -136
  112. hud/utils/__init__.py +10 -10
  113. hud/utils/async_utils.py +65 -65
  114. hud/utils/design.py +236 -168
  115. hud/utils/mcp.py +55 -55
  116. hud/utils/progress.py +149 -149
  117. hud/utils/telemetry.py +66 -66
  118. hud/utils/tests/test_async_utils.py +173 -173
  119. hud/utils/tests/test_init.py +17 -17
  120. hud/utils/tests/test_progress.py +261 -261
  121. hud/utils/tests/test_telemetry.py +82 -82
  122. hud/utils/tests/test_version.py +8 -8
  123. hud/version.py +7 -7
  124. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
  125. hud_python-0.4.3.dist-info/RECORD +131 -0
  126. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
  127. hud/agents/art.py +0 -101
  128. hud_python-0.4.1.dist-info/RECORD +0 -132
  129. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
  130. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
@@ -1,197 +1,197 @@
1
- """Tests for OpenTelemetry processors."""
2
-
3
- from __future__ import annotations
4
-
5
- from unittest.mock import MagicMock, patch
6
-
7
- from hud.otel.processors import HudEnrichmentProcessor
8
-
9
-
10
- class TestHudEnrichmentProcessor:
11
- """Test HudEnrichmentProcessor."""
12
-
13
- def test_on_start_with_run_id(self):
14
- """Test on_start with current task run ID."""
15
-
16
- processor = HudEnrichmentProcessor()
17
-
18
- # Mock span
19
- span = MagicMock()
20
- span.set_attribute = MagicMock()
21
- span.is_recording.return_value = True
22
-
23
- # Mock baggage to return run ID
24
- parent_context = {}
25
- with patch("hud.otel.processors.baggage.get_baggage") as mock_get_baggage:
26
- # Return run ID for task_run_id, None for job_id
27
- mock_get_baggage.side_effect = (
28
- lambda key, context: "test-run-123" if key == "hud.task_run_id" else None
29
- )
30
- processor.on_start(span, parent_context)
31
-
32
- # Verify attribute was set
33
- span.set_attribute.assert_called_with("hud.task_run_id", "test-run-123")
34
-
35
- def test_on_start_no_run_id(self):
36
- """Test on_start without current task run ID."""
37
-
38
- processor = HudEnrichmentProcessor()
39
-
40
- # Mock span
41
- span = MagicMock()
42
- span.set_attribute = MagicMock()
43
- span.is_recording.return_value = True
44
- span.name = "test_span"
45
-
46
- # Set up attributes to return None (not matching any step type)
47
- span.attributes = {}
48
-
49
- # Mock baggage to return None
50
- parent_context = {}
51
- with patch("hud.otel.processors.baggage.get_baggage", return_value=None):
52
- processor.on_start(span, parent_context)
53
-
54
- # Verify only step count attributes were set (no run_id or job_id)
55
- calls = span.set_attribute.call_args_list
56
- set_attrs = {call[0][0] for call in calls}
57
-
58
- # Should have step counts but not run_id/job_id
59
- assert "hud.task_run_id" not in set_attrs
60
- assert "hud.job_id" not in set_attrs
61
- assert "hud.base_mcp_steps" in set_attrs
62
- assert "hud.mcp_tool_steps" in set_attrs
63
- assert "hud.agent_steps" in set_attrs
64
-
65
- def test_on_end(self):
66
- """Test on_end does nothing."""
67
-
68
- processor = HudEnrichmentProcessor()
69
- span = MagicMock()
70
-
71
- # Should not raise
72
- processor.on_end(span)
73
-
74
- def test_shutdown(self):
75
- """Test shutdown does nothing."""
76
-
77
- processor = HudEnrichmentProcessor()
78
-
79
- # Should not raise
80
- processor.shutdown()
81
-
82
- def test_force_flush(self):
83
- """Test force_flush returns True."""
84
-
85
- processor = HudEnrichmentProcessor()
86
-
87
- # Should return True
88
- result = processor.force_flush()
89
- assert result is True
90
-
91
- def test_on_start_with_job_id(self):
92
- """Test on_start with job ID in baggage."""
93
-
94
- processor = HudEnrichmentProcessor()
95
-
96
- # Mock span
97
- span = MagicMock()
98
- span.set_attribute = MagicMock()
99
- span.is_recording.return_value = True
100
-
101
- # Mock baggage with job ID
102
- parent_context = {}
103
- with patch("hud.otel.processors.baggage.get_baggage") as mock_get_baggage:
104
- # Return None for task_run_id, job-123 for job_id
105
- mock_get_baggage.side_effect = (
106
- lambda key, context: "job-123" if key == "hud.job_id" else None
107
- )
108
- processor.on_start(span, parent_context)
109
-
110
- # Verify job ID attribute was set
111
- span.set_attribute.assert_called_with("hud.job_id", "job-123")
112
-
113
- def test_on_start_exception_handling(self):
114
- """Test on_start handles exceptions gracefully."""
115
-
116
- processor = HudEnrichmentProcessor()
117
-
118
- # Mock span that raises exception
119
- span = MagicMock()
120
- span.is_recording.side_effect = Exception("Test error")
121
-
122
- # Should not raise
123
- processor.on_start(span, parent_context=None)
124
-
125
- def test_on_start_exception_handling_extended(self):
126
- """Test that exceptions in on_start are caught and logged."""
127
- from hud.otel.processors import HudEnrichmentProcessor
128
-
129
- processor = HudEnrichmentProcessor()
130
-
131
- # Create a mock span that raises when setting attributes
132
- mock_span = MagicMock()
133
- mock_span.is_recording.return_value = True
134
- mock_span.set_attribute.side_effect = RuntimeError("Attribute error")
135
-
136
- parent_context = {}
137
-
138
- # Patch logger and baggage to force an exception when setting attribute
139
- with (
140
- patch("hud.otel.processors.logger") as mock_logger,
141
- patch("hud.otel.processors.baggage.get_baggage", return_value="test-id"),
142
- ):
143
- # Should not raise, exception should be caught
144
- processor.on_start(mock_span, parent_context)
145
-
146
- # Verify logger.debug was called with the exception
147
- mock_logger.debug.assert_called_once()
148
- args = mock_logger.debug.call_args[0]
149
- assert "HudEnrichmentProcessor.on_start error" in args[0]
150
- assert "Attribute error" in str(args[1])
151
-
152
- def test_on_start_with_baggage_get_exception(self):
153
- """Test exception handling when baggage.get_baggage fails for task_run_id."""
154
- processor = HudEnrichmentProcessor()
155
-
156
- mock_span = MagicMock()
157
- mock_span.is_recording.return_value = True
158
-
159
- parent_context = {}
160
-
161
- # Make baggage.get_baggage raise an exception for task_run_id
162
- with (
163
- patch(
164
- "hud.otel.processors.baggage.get_baggage",
165
- side_effect=ValueError("Context error"),
166
- ),
167
- patch("hud.otel.processors.logger") as mock_logger,
168
- ):
169
- # Should not raise
170
- processor.on_start(mock_span, parent_context)
171
-
172
- # Verify logger.debug was called
173
- mock_logger.debug.assert_called_once()
174
- args = mock_logger.debug.call_args[0]
175
- assert "Context error" in str(args[1])
176
-
177
- def test_on_start_with_baggage_exception(self):
178
- """Test exception handling when baggage.get_baggage fails."""
179
- processor = HudEnrichmentProcessor()
180
-
181
- mock_span = MagicMock()
182
- mock_span.is_recording.return_value = True
183
-
184
- parent_context = {}
185
-
186
- # Make baggage.get_baggage raise an exception
187
- with (
188
- patch("hud.otel.processors.baggage.get_baggage", side_effect=KeyError("Baggage error")),
189
- patch("hud.otel.processors.logger") as mock_logger,
190
- ):
191
- # Should not raise
192
- processor.on_start(mock_span, parent_context)
193
-
194
- # Verify logger.debug was called
195
- mock_logger.debug.assert_called_once()
196
- args = mock_logger.debug.call_args[0]
197
- assert "Baggage error" in str(args[1])
1
+ """Tests for OpenTelemetry processors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import MagicMock, patch
6
+
7
+ from hud.otel.processors import HudEnrichmentProcessor
8
+
9
+
10
+ class TestHudEnrichmentProcessor:
11
+ """Test HudEnrichmentProcessor."""
12
+
13
+ def test_on_start_with_run_id(self):
14
+ """Test on_start with current task run ID."""
15
+
16
+ processor = HudEnrichmentProcessor()
17
+
18
+ # Mock span
19
+ span = MagicMock()
20
+ span.set_attribute = MagicMock()
21
+ span.is_recording.return_value = True
22
+
23
+ # Mock baggage to return run ID
24
+ parent_context = {}
25
+ with patch("hud.otel.processors.baggage.get_baggage") as mock_get_baggage:
26
+ # Return run ID for task_run_id, None for job_id
27
+ mock_get_baggage.side_effect = (
28
+ lambda key, context: "test-run-123" if key == "hud.task_run_id" else None
29
+ )
30
+ processor.on_start(span, parent_context)
31
+
32
+ # Verify attribute was set
33
+ span.set_attribute.assert_called_with("hud.task_run_id", "test-run-123")
34
+
35
+ def test_on_start_no_run_id(self):
36
+ """Test on_start without current task run ID."""
37
+
38
+ processor = HudEnrichmentProcessor()
39
+
40
+ # Mock span
41
+ span = MagicMock()
42
+ span.set_attribute = MagicMock()
43
+ span.is_recording.return_value = True
44
+ span.name = "test_span"
45
+
46
+ # Set up attributes to return None (not matching any step type)
47
+ span.attributes = {}
48
+
49
+ # Mock baggage to return None
50
+ parent_context = {}
51
+ with patch("hud.otel.processors.baggage.get_baggage", return_value=None):
52
+ processor.on_start(span, parent_context)
53
+
54
+ # Verify only step count attributes were set (no run_id or job_id)
55
+ calls = span.set_attribute.call_args_list
56
+ set_attrs = {call[0][0] for call in calls}
57
+
58
+ # Should have step counts but not run_id/job_id
59
+ assert "hud.task_run_id" not in set_attrs
60
+ assert "hud.job_id" not in set_attrs
61
+ assert "hud.base_mcp_steps" in set_attrs
62
+ assert "hud.mcp_tool_steps" in set_attrs
63
+ assert "hud.agent_steps" in set_attrs
64
+
65
+ def test_on_end(self):
66
+ """Test on_end does nothing."""
67
+
68
+ processor = HudEnrichmentProcessor()
69
+ span = MagicMock()
70
+
71
+ # Should not raise
72
+ processor.on_end(span)
73
+
74
+ def test_shutdown(self):
75
+ """Test shutdown does nothing."""
76
+
77
+ processor = HudEnrichmentProcessor()
78
+
79
+ # Should not raise
80
+ processor.shutdown()
81
+
82
+ def test_force_flush(self):
83
+ """Test force_flush returns True."""
84
+
85
+ processor = HudEnrichmentProcessor()
86
+
87
+ # Should return True
88
+ result = processor.force_flush()
89
+ assert result is True
90
+
91
+ def test_on_start_with_job_id(self):
92
+ """Test on_start with job ID in baggage."""
93
+
94
+ processor = HudEnrichmentProcessor()
95
+
96
+ # Mock span
97
+ span = MagicMock()
98
+ span.set_attribute = MagicMock()
99
+ span.is_recording.return_value = True
100
+
101
+ # Mock baggage with job ID
102
+ parent_context = {}
103
+ with patch("hud.otel.processors.baggage.get_baggage") as mock_get_baggage:
104
+ # Return None for task_run_id, job-123 for job_id
105
+ mock_get_baggage.side_effect = (
106
+ lambda key, context: "job-123" if key == "hud.job_id" else None
107
+ )
108
+ processor.on_start(span, parent_context)
109
+
110
+ # Verify job ID attribute was set
111
+ span.set_attribute.assert_called_with("hud.job_id", "job-123")
112
+
113
+ def test_on_start_exception_handling(self):
114
+ """Test on_start handles exceptions gracefully."""
115
+
116
+ processor = HudEnrichmentProcessor()
117
+
118
+ # Mock span that raises exception
119
+ span = MagicMock()
120
+ span.is_recording.side_effect = Exception("Test error")
121
+
122
+ # Should not raise
123
+ processor.on_start(span, parent_context=None)
124
+
125
+ def test_on_start_exception_handling_extended(self):
126
+ """Test that exceptions in on_start are caught and logged."""
127
+ from hud.otel.processors import HudEnrichmentProcessor
128
+
129
+ processor = HudEnrichmentProcessor()
130
+
131
+ # Create a mock span that raises when setting attributes
132
+ mock_span = MagicMock()
133
+ mock_span.is_recording.return_value = True
134
+ mock_span.set_attribute.side_effect = RuntimeError("Attribute error")
135
+
136
+ parent_context = {}
137
+
138
+ # Patch logger and baggage to force an exception when setting attribute
139
+ with (
140
+ patch("hud.otel.processors.logger") as mock_logger,
141
+ patch("hud.otel.processors.baggage.get_baggage", return_value="test-id"),
142
+ ):
143
+ # Should not raise, exception should be caught
144
+ processor.on_start(mock_span, parent_context)
145
+
146
+ # Verify logger.debug was called with the exception
147
+ mock_logger.debug.assert_called_once()
148
+ args = mock_logger.debug.call_args[0]
149
+ assert "HudEnrichmentProcessor.on_start error" in args[0]
150
+ assert "Attribute error" in str(args[1])
151
+
152
+ def test_on_start_with_baggage_get_exception(self):
153
+ """Test exception handling when baggage.get_baggage fails for task_run_id."""
154
+ processor = HudEnrichmentProcessor()
155
+
156
+ mock_span = MagicMock()
157
+ mock_span.is_recording.return_value = True
158
+
159
+ parent_context = {}
160
+
161
+ # Make baggage.get_baggage raise an exception for task_run_id
162
+ with (
163
+ patch(
164
+ "hud.otel.processors.baggage.get_baggage",
165
+ side_effect=ValueError("Context error"),
166
+ ),
167
+ patch("hud.otel.processors.logger") as mock_logger,
168
+ ):
169
+ # Should not raise
170
+ processor.on_start(mock_span, parent_context)
171
+
172
+ # Verify logger.debug was called
173
+ mock_logger.debug.assert_called_once()
174
+ args = mock_logger.debug.call_args[0]
175
+ assert "Context error" in str(args[1])
176
+
177
+ def test_on_start_with_baggage_exception(self):
178
+ """Test exception handling when baggage.get_baggage fails."""
179
+ processor = HudEnrichmentProcessor()
180
+
181
+ mock_span = MagicMock()
182
+ mock_span.is_recording.return_value = True
183
+
184
+ parent_context = {}
185
+
186
+ # Make baggage.get_baggage raise an exception
187
+ with (
188
+ patch("hud.otel.processors.baggage.get_baggage", side_effect=KeyError("Baggage error")),
189
+ patch("hud.otel.processors.logger") as mock_logger,
190
+ ):
191
+ # Should not raise
192
+ processor.on_start(mock_span, parent_context)
193
+
194
+ # Verify logger.debug was called
195
+ mock_logger.debug.assert_called_once()
196
+ args = mock_logger.debug.call_args[0]
197
+ assert "Baggage error" in str(args[1])
hud/server/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
- from __future__ import annotations
2
-
3
- from .server import MCPServer
4
-
5
- __all__ = ["MCPServer"]
1
+ from __future__ import annotations
2
+
3
+ from .server import MCPServer
4
+
5
+ __all__ = ["MCPServer"]
hud/server/context.py CHANGED
@@ -1,114 +1,114 @@
1
- """
2
- HUD context helpers for persistent state across hot-reloads.
3
-
4
- Provides utilities for creating shared context servers that survive
5
- code reloads during development.
6
-
7
- Usage in your environment:
8
- # In your context_server.py:
9
- from hud.server.context import serve_context
10
-
11
- class MyContext:
12
- def __init__(self):
13
- self.state = {}
14
- def startup(self):
15
- # Initialize resources
16
- pass
17
-
18
- if __name__ == "__main__":
19
- serve_context(MyContext())
20
-
21
- # In your MCP server:
22
- from hud.server.context import attach_context
23
- ctx = attach_context() # Gets the persistent context
24
- """
25
-
26
- from __future__ import annotations
27
-
28
- import asyncio
29
- import logging
30
- import os
31
- from multiprocessing.managers import BaseManager
32
- from typing import Any
33
-
34
- logger = logging.getLogger(__name__)
35
- # Default Unix socket path (can be overridden with HUD_CTX_SOCK)
36
- DEFAULT_SOCK_PATH = "/tmp/hud_ctx.sock" # noqa: S108
37
-
38
-
39
- def serve_context(
40
- context_instance: Any, sock_path: str | None = None, authkey: bytes = b"hud-context"
41
- ) -> BaseManager:
42
- """
43
- Serve a context object via multiprocessing Manager.
44
-
45
- Args:
46
- context_instance: The context object to serve
47
- sock_path: Unix socket path (defaults to HUD_CTX_SOCK env var or /tmp/hud_ctx.sock)
48
- authkey: Authentication key for the manager
49
-
50
- Returns:
51
- The manager instance (can be used to shutdown)
52
- """
53
- sock_path = sock_path or os.getenv("HUD_CTX_SOCK", DEFAULT_SOCK_PATH)
54
-
55
- class ContextManager(BaseManager):
56
- pass
57
-
58
- ContextManager.register("get_context", callable=lambda: context_instance)
59
-
60
- manager = ContextManager(address=sock_path, authkey=authkey)
61
- manager.start()
62
-
63
- return manager
64
-
65
-
66
- def attach_context(sock_path: str | None = None, authkey: bytes = b"hud-context") -> Any:
67
- """
68
- Attach to a running context server.
69
-
70
- Args:
71
- sock_path: Unix socket path (defaults to HUD_CTX_SOCK env var or /tmp/hud_ctx.sock)
72
- authkey: Authentication key for the manager
73
-
74
- Returns:
75
- The shared context object
76
- """
77
- sock_path = sock_path or os.getenv("HUD_CTX_SOCK", DEFAULT_SOCK_PATH)
78
-
79
- class ContextManager(BaseManager):
80
- pass
81
-
82
- ContextManager.register("get_context")
83
-
84
- manager = ContextManager(address=sock_path, authkey=authkey)
85
- manager.connect()
86
-
87
- return manager.get_context() # type: ignore
88
-
89
-
90
- async def run_context_server(
91
- context_instance: Any, sock_path: str | None = None, authkey: bytes = b"hud-context"
92
- ) -> None:
93
- """
94
- Run a context server until interrupted.
95
-
96
- Args:
97
- context_instance: The context object to serve
98
- sock_path: Unix socket path
99
- authkey: Authentication key
100
- """
101
- sock_path = sock_path or os.getenv("HUD_CTX_SOCK", DEFAULT_SOCK_PATH)
102
-
103
- logger.info("[Context Server] Starting on %s...", sock_path)
104
-
105
- # Start the manager
106
- manager = serve_context(context_instance, sock_path, authkey)
107
- logger.info("[Context Server] Ready on %s", sock_path)
108
-
109
- # Wait forever (until killed)
110
- try:
111
- await asyncio.Event().wait()
112
- except KeyboardInterrupt:
113
- logger.info("[Context Server] Shutting down...")
114
- manager.shutdown()
1
+ """
2
+ HUD context helpers for persistent state across hot-reloads.
3
+
4
+ Provides utilities for creating shared context servers that survive
5
+ code reloads during development.
6
+
7
+ Usage in your environment:
8
+ # In your context_server.py:
9
+ from hud.server.context import serve_context
10
+
11
+ class MyContext:
12
+ def __init__(self):
13
+ self.state = {}
14
+ def startup(self):
15
+ # Initialize resources
16
+ pass
17
+
18
+ if __name__ == "__main__":
19
+ serve_context(MyContext())
20
+
21
+ # In your MCP server:
22
+ from hud.server.context import attach_context
23
+ ctx = attach_context() # Gets the persistent context
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import asyncio
29
+ import logging
30
+ import os
31
+ from multiprocessing.managers import BaseManager
32
+ from typing import Any
33
+
34
+ logger = logging.getLogger(__name__)
35
+ # Default Unix socket path (can be overridden with HUD_CTX_SOCK)
36
+ DEFAULT_SOCK_PATH = "/tmp/hud_ctx.sock" # noqa: S108
37
+
38
+
39
+ def serve_context(
40
+ context_instance: Any, sock_path: str | None = None, authkey: bytes = b"hud-context"
41
+ ) -> BaseManager:
42
+ """
43
+ Serve a context object via multiprocessing Manager.
44
+
45
+ Args:
46
+ context_instance: The context object to serve
47
+ sock_path: Unix socket path (defaults to HUD_CTX_SOCK env var or /tmp/hud_ctx.sock)
48
+ authkey: Authentication key for the manager
49
+
50
+ Returns:
51
+ The manager instance (can be used to shutdown)
52
+ """
53
+ sock_path = sock_path or os.getenv("HUD_CTX_SOCK", DEFAULT_SOCK_PATH)
54
+
55
+ class ContextManager(BaseManager):
56
+ pass
57
+
58
+ ContextManager.register("get_context", callable=lambda: context_instance)
59
+
60
+ manager = ContextManager(address=sock_path, authkey=authkey)
61
+ manager.start()
62
+
63
+ return manager
64
+
65
+
66
+ def attach_context(sock_path: str | None = None, authkey: bytes = b"hud-context") -> Any:
67
+ """
68
+ Attach to a running context server.
69
+
70
+ Args:
71
+ sock_path: Unix socket path (defaults to HUD_CTX_SOCK env var or /tmp/hud_ctx.sock)
72
+ authkey: Authentication key for the manager
73
+
74
+ Returns:
75
+ The shared context object
76
+ """
77
+ sock_path = sock_path or os.getenv("HUD_CTX_SOCK", DEFAULT_SOCK_PATH)
78
+
79
+ class ContextManager(BaseManager):
80
+ pass
81
+
82
+ ContextManager.register("get_context")
83
+
84
+ manager = ContextManager(address=sock_path, authkey=authkey)
85
+ manager.connect()
86
+
87
+ return manager.get_context() # type: ignore
88
+
89
+
90
+ async def run_context_server(
91
+ context_instance: Any, sock_path: str | None = None, authkey: bytes = b"hud-context"
92
+ ) -> None:
93
+ """
94
+ Run a context server until interrupted.
95
+
96
+ Args:
97
+ context_instance: The context object to serve
98
+ sock_path: Unix socket path
99
+ authkey: Authentication key
100
+ """
101
+ sock_path = sock_path or os.getenv("HUD_CTX_SOCK", DEFAULT_SOCK_PATH)
102
+
103
+ logger.info("[Context Server] Starting on %s...", sock_path)
104
+
105
+ # Start the manager
106
+ manager = serve_context(context_instance, sock_path, authkey)
107
+ logger.info("[Context Server] Ready on %s", sock_path)
108
+
109
+ # Wait forever (until killed)
110
+ try:
111
+ await asyncio.Event().wait()
112
+ except KeyboardInterrupt:
113
+ logger.info("[Context Server] Shutting down...")
114
+ manager.shutdown()
@@ -1,5 +1,5 @@
1
- """Helper sub-package: utilities, registration helpers, shims."""
2
-
3
- from __future__ import annotations
4
-
5
- __all__ = []
1
+ """Helper sub-package: utilities, registration helpers, shims."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = []