opencode-agent-hub 1.4.1__tar.gz → 1.4.2__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. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/.gitignore +2 -0
  2. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/PKG-INFO +5 -4
  3. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/pyproject.toml +5 -4
  4. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/src/opencode_agent_hub/__init__.py +1 -1
  5. opencode_agent_hub-1.4.2/src/opencode_agent_hub/config.py +330 -0
  6. opencode_agent_hub-1.4.2/src/opencode_agent_hub/coordinator.py +745 -0
  7. opencode_agent_hub-1.4.2/src/opencode_agent_hub/daemon.py +379 -0
  8. opencode_agent_hub-1.4.2/src/opencode_agent_hub/garbage_collector.py +269 -0
  9. opencode_agent_hub-1.4.2/src/opencode_agent_hub/hub_server.py +214 -0
  10. opencode_agent_hub-1.4.2/src/opencode_agent_hub/messaging.py +628 -0
  11. opencode_agent_hub-1.4.2/src/opencode_agent_hub/metrics.py +164 -0
  12. opencode_agent_hub-1.4.2/src/opencode_agent_hub/models.py +41 -0
  13. opencode_agent_hub-1.4.2/src/opencode_agent_hub/persistence.py +316 -0
  14. opencode_agent_hub-1.4.2/src/opencode_agent_hub/preflight.py +123 -0
  15. opencode_agent_hub-1.4.2/src/opencode_agent_hub/rate_limiting.py +63 -0
  16. opencode_agent_hub-1.4.2/src/opencode_agent_hub/service.py +159 -0
  17. opencode_agent_hub-1.4.2/src/opencode_agent_hub/sessions.py +742 -0
  18. opencode_agent_hub-1.4.2/src/opencode_agent_hub/utils.py +71 -0
  19. opencode_agent_hub-1.4.2/tests/test_agent_id_generation.py +49 -0
  20. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/tests/test_config.py +41 -39
  21. opencode_agent_hub-1.4.2/tests/test_coordinator.py +705 -0
  22. opencode_agent_hub-1.4.2/tests/test_coordinator_cost.py +72 -0
  23. opencode_agent_hub-1.4.2/tests/test_coordinator_model_logging.py +105 -0
  24. opencode_agent_hub-1.4.2/tests/test_daemon_integration.py +309 -0
  25. opencode_agent_hub-1.4.2/tests/test_orientation_retry.py +298 -0
  26. opencode_agent_hub-1.4.2/tests/test_rate_limiting.py +83 -0
  27. opencode_agent_hub-1.4.2/tests/test_session_agents.py +427 -0
  28. opencode_agent_hub-1.4.2/tests/test_sqlite_schema.py +84 -0
  29. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/tests/test_watch.py +1 -1
  30. opencode_agent_hub-1.4.1/src/opencode_agent_hub/daemon.py +0 -3373
  31. opencode_agent_hub-1.4.1/tests/test_agent_detection.py +0 -426
  32. opencode_agent_hub-1.4.1/tests/test_coordinator.py +0 -1282
  33. opencode_agent_hub-1.4.1/tests/test_coordinator_cost.py +0 -430
  34. opencode_agent_hub-1.4.1/tests/test_integration.py +0 -338
  35. opencode_agent_hub-1.4.1/tests/test_message_injection.py +0 -285
  36. opencode_agent_hub-1.4.1/tests/test_orientation_retry.py +0 -466
  37. opencode_agent_hub-1.4.1/tests/test_rate_limiting.py +0 -152
  38. opencode_agent_hub-1.4.1/tests/test_session_agents.py +0 -342
  39. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/LICENSE +0 -0
  40. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/README.md +0 -0
  41. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/contrib/coordinator/AGENTS.md +0 -0
  42. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/contrib/coordinator/opencode.json +0 -0
  43. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/contrib/launchd/com.xnoto.agent-hub-daemon.plist +0 -0
  44. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/contrib/packaging/aur/PKGBUILD +0 -0
  45. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/contrib/packaging/debian/changelog +0 -0
  46. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/contrib/packaging/debian/control +0 -0
  47. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/contrib/packaging/debian/copyright +0 -0
  48. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/contrib/packaging/debian/docs +0 -0
  49. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/contrib/packaging/debian/install +0 -0
  50. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/contrib/packaging/debian/postinst +0 -0
  51. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/contrib/packaging/debian/rules +0 -0
  52. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/contrib/packaging/debian/source/format +0 -0
  53. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/contrib/packaging/rpm/opencode-agent-hub.spec +0 -0
  54. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/contrib/systemd/agent-hub-daemon.service +0 -0
  55. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/src/opencode_agent_hub/py.typed +0 -0
  56. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/src/opencode_agent_hub/watch.py +0 -0
  57. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/tests/__init__.py +0 -0
  58. {opencode_agent_hub-1.4.1 → opencode_agent_hub-1.4.2}/tests/test_placeholder.py +0 -0
@@ -47,7 +47,9 @@ coverage.xml
47
47
  *.cover
48
48
  *.py,cover
49
49
  .hypothesis/
50
+ # pytest snapshot testing directories
50
51
  .pytest_cache/
52
+ pytest-of-user/
51
53
 
52
54
  # Translations
53
55
  *.mo
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencode-agent-hub
3
- Version: 1.4.1
3
+ Version: 1.4.2
4
4
  Summary: Multi-agent coordination daemon and tools for OpenCode
5
5
  Project-URL: Homepage, https://github.com/xnoto/opencode-agent-hub
6
6
  Project-URL: Repository, https://github.com/xnoto/opencode-agent-hub
@@ -22,15 +22,16 @@ Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
23
  Classifier: Topic :: System :: Distributed Computing
24
24
  Requires-Python: >=3.11
25
- Requires-Dist: requests>=2.28.0
25
+ Requires-Dist: requests>=2.31.0
26
26
  Requires-Dist: watchdog>=3.0.0
27
27
  Provides-Extra: dev
28
28
  Requires-Dist: mypy>=1.0.0; extra == 'dev'
29
29
  Requires-Dist: pre-commit>=3.0.0; extra == 'dev'
30
30
  Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
31
31
  Requires-Dist: pytest>=7.0.0; extra == 'dev'
32
- Requires-Dist: ruff>=0.1.0; extra == 'dev'
33
- Requires-Dist: types-requests>=2.28.0; extra == 'dev'
32
+ Requires-Dist: ruff>=0.15.0; extra == 'dev'
33
+ Requires-Dist: types-requests>=2.31.0; extra == 'dev'
34
+ Requires-Dist: vulture>=2.14; extra == 'dev'
34
35
  Description-Content-Type: text/markdown
35
36
 
36
37
  # opencode-agent-hub
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "opencode-agent-hub"
7
- version = "1.4.1"
7
+ version = "1.4.2"
8
8
  description = "Multi-agent coordination daemon and tools for OpenCode"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -35,7 +35,7 @@ classifiers = [
35
35
  "Topic :: System :: Distributed Computing",
36
36
  ]
37
37
  dependencies = [
38
- "requests>=2.28.0",
38
+ "requests>=2.31.0",
39
39
  "watchdog>=3.0.0",
40
40
  ]
41
41
 
@@ -44,9 +44,10 @@ dev = [
44
44
  "pre-commit>=3.0.0",
45
45
  "pytest>=7.0.0",
46
46
  "pytest-cov>=4.0.0",
47
- "ruff>=0.1.0",
47
+ "ruff>=0.15.0",
48
+ "vulture>=2.14",
48
49
  "mypy>=1.0.0",
49
- "types-requests>=2.28.0",
50
+ "types-requests>=2.31.0",
50
51
  ]
51
52
 
52
53
  [project.scripts]
@@ -8,4 +8,4 @@ from importlib.metadata import PackageNotFoundError, version
8
8
  try:
9
9
  __version__ = version("opencode-agent-hub")
10
10
  except PackageNotFoundError: # pragma: no cover - fallback for dev
11
- __version__ = "1.4.1"
11
+ __version__ = "1.4.2"
@@ -0,0 +1,330 @@
1
+ """Configuration management for the agent hub daemon.
2
+
3
+ This module handles all configuration loading with the precedence:
4
+ environment variables > config file > defaults
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import os
10
+ import threading
11
+ import time
12
+
13
+ # For accessing package data files reliably across install methods (pip, deb, rpm, aur)
14
+ from pathlib import Path
15
+ from typing import Any, cast
16
+
17
+ # =============================================================================
18
+ # Static Paths (not configurable)
19
+ # =============================================================================
20
+
21
+ AGENT_HUB_DIR = Path.home() / ".agent-hub"
22
+ MESSAGES_DIR = AGENT_HUB_DIR / "messages"
23
+ ARCHIVE_DIR = MESSAGES_DIR / "archive"
24
+ THREADS_DIR = AGENT_HUB_DIR / "threads"
25
+ AGENTS_DIR = AGENT_HUB_DIR / "agents"
26
+ ORIENTED_SESSIONS_FILE = AGENT_HUB_DIR / "oriented_sessions.json"
27
+ SESSION_AGENTS_FILE = AGENT_HUB_DIR / "session_agents.json"
28
+ OPENCODE_DATA_DIR = Path.home() / ".local/share/opencode"
29
+ OPENCODE_DB_PATH = OPENCODE_DATA_DIR / "opencode.db"
30
+ OPENCODE_STORAGE_DIR = OPENCODE_DATA_DIR / "storage" # Watch all project subdirs, not just global
31
+ CONFIG_DIR = Path.home() / ".config" / "agent-hub-daemon"
32
+ CONFIG_FILE = CONFIG_DIR / "config.json"
33
+
34
+ # Coordinator directory
35
+ COORDINATOR_DIR = AGENT_HUB_DIR / "coordinator"
36
+
37
+ # Metrics and logging
38
+ METRICS_FILE = AGENT_HUB_DIR / "metrics.prom"
39
+ HUB_SERVER_PID_FILE = AGENT_HUB_DIR / "hub-server.pid"
40
+ DAEMON_LOG_DIR = Path.home() / ".local/share/agent-hub-daemon"
41
+ HUB_STDERR_LOG_FILE = DAEMON_LOG_DIR / "hub-stderr.log"
42
+
43
+
44
+ # =============================================================================
45
+ # Configuration Loading
46
+ # =============================================================================
47
+
48
+
49
+ def _load_config_file() -> dict[str, Any]:
50
+ """Load configuration from JSON file if it exists."""
51
+ if not CONFIG_FILE.exists():
52
+ return {}
53
+ try:
54
+ return cast(dict[str, Any], json.loads(CONFIG_FILE.read_text()))
55
+ except (json.JSONDecodeError, OSError):
56
+ # Log warning later after logging is set up
57
+ return {}
58
+
59
+
60
+ def _get_config_value(
61
+ env_var: str,
62
+ config_path: list[str],
63
+ default: str | int | bool | float | None,
64
+ config: dict[str, Any],
65
+ type_: type = str,
66
+ ) -> Any:
67
+ """Get config value with precedence: env var > config file > default.
68
+
69
+ Args:
70
+ env_var: Environment variable name
71
+ config_path: Path in config dict (e.g., ["rate_limit", "enabled"])
72
+ default: Default value
73
+ config: Loaded config dict
74
+ type_: Expected type (str, int, or bool)
75
+ """
76
+ # Check environment variable first
77
+ env_value = os.environ.get(env_var)
78
+ if env_value is not None:
79
+ if type_ is bool:
80
+ return env_value.lower() in ("1", "true", "yes")
81
+ elif type_ is int:
82
+ return int(env_value)
83
+ return env_value
84
+
85
+ # Traverse config dict path
86
+ value = config
87
+ for key in config_path:
88
+ if isinstance(value, dict) and key in value:
89
+ value = value[key]
90
+ else:
91
+ return default
92
+
93
+ # Validate type
94
+ if type_ is bool and isinstance(value, bool):
95
+ return value
96
+ if type_ is int and isinstance(value, int):
97
+ return value
98
+ if type_ is str and isinstance(value, str):
99
+ return value
100
+
101
+ return default
102
+
103
+
104
+ # Load config file once at module load
105
+ _CONFIG = _load_config_file()
106
+
107
+ # =============================================================================
108
+ # Hub Server Configuration
109
+ # =============================================================================
110
+
111
+ OPENCODE_PORT = _get_config_value("OPENCODE_PORT", ["hub", "port"], 4096, _CONFIG, int)
112
+ OPENCODE_URL = f"http://127.0.0.1:{OPENCODE_PORT}"
113
+
114
+ # =============================================================================
115
+ # Coordinator Configuration
116
+ # =============================================================================
117
+
118
+ COORDINATOR_ENABLED = _get_config_value(
119
+ "AGENT_HUB_COORDINATOR", ["coordinator", "enabled"], True, _CONFIG, bool
120
+ )
121
+ COORDINATOR_AGENTS_MD = _get_config_value(
122
+ "AGENT_HUB_COORDINATOR_AGENTS_MD",
123
+ ["coordinator", "agents_md_path"],
124
+ None,
125
+ _CONFIG,
126
+ str,
127
+ )
128
+ COORDINATOR_PRESERVE_LOCAL_AGENTS_MD = _get_config_value(
129
+ "AGENT_HUB_COORDINATOR_PRESERVE_LOCAL_AGENTS_MD",
130
+ ["coordinator", "preserve_local_agents_md"],
131
+ False,
132
+ _CONFIG,
133
+ bool,
134
+ )
135
+
136
+ # Convert COORDINATOR_AGENTS_MD to Path if set
137
+ if COORDINATOR_AGENTS_MD:
138
+ COORDINATOR_AGENTS_MD = Path(COORDINATOR_AGENTS_MD)
139
+
140
+ # Coordinator settings
141
+ COORDINATOR_READY_TIMEOUT_SECONDS = _get_config_value(
142
+ "AGENT_HUB_COORDINATOR_READY_TIMEOUT",
143
+ ["coordinator", "ready_timeout_seconds"],
144
+ 60,
145
+ _CONFIG,
146
+ int,
147
+ )
148
+ COORDINATOR_BOOTSTRAP_REQUIRED = _get_config_value(
149
+ "AGENT_HUB_COORDINATOR_BOOTSTRAP_REQUIRED",
150
+ ["coordinator", "bootstrap_required"],
151
+ False, # Allow daemon to run without coordinator READY for headless operation
152
+ _CONFIG,
153
+ bool,
154
+ )
155
+ COORDINATOR_STRICT_READY = _get_config_value(
156
+ "AGENT_HUB_COORDINATOR_STRICT_READY",
157
+ ["coordinator", "strict_ready"],
158
+ False, # Allow any activity, not just exact "READY" text
159
+ _CONFIG,
160
+ bool,
161
+ )
162
+ COORDINATOR_BOOTSTRAP_PROMPT = _get_config_value(
163
+ "AGENT_HUB_COORDINATOR_BOOTSTRAP_PROMPT",
164
+ ["coordinator", "bootstrap_prompt"],
165
+ "READY",
166
+ _CONFIG,
167
+ str,
168
+ )
169
+
170
+ # Coordinator pricing (USD per token)
171
+ PRICING_INPUT = _get_config_value(
172
+ "AGENT_HUB_PRICING_INPUT", ["pricing", "input_per_1m"], 3.00, _CONFIG, float
173
+ )
174
+ PRICING_OUTPUT = _get_config_value(
175
+ "AGENT_HUB_PRICING_OUTPUT", ["pricing", "output_per_1m"], 15.00, _CONFIG, float
176
+ )
177
+ PRICING_CACHE_READ = _get_config_value(
178
+ "AGENT_HUB_PRICING_CACHE_READ", ["pricing", "cache_read_per_1m"], 0.30, _CONFIG, float
179
+ )
180
+ PRICING_CACHE_WRITE = _get_config_value(
181
+ "AGENT_HUB_PRICING_CACHE_WRITE", ["pricing", "cache_write_per_1m"], 3.75, _CONFIG, float
182
+ )
183
+
184
+ # Convert per-1M rates to per-token
185
+ PRICING_INPUT = PRICING_INPUT / 1_000_000
186
+ PRICING_OUTPUT = PRICING_OUTPUT / 1_000_000
187
+ PRICING_CACHE_READ = PRICING_CACHE_READ / 1_000_000
188
+ PRICING_CACHE_WRITE = PRICING_CACHE_WRITE / 1_000_000
189
+
190
+
191
+ # =============================================================================
192
+ # Rate Limiting Configuration
193
+ # =============================================================================
194
+
195
+ RATE_LIMIT_ENABLED = _get_config_value(
196
+ "AGENT_HUB_RATE_LIMIT_ENABLED", ["rate_limit", "enabled"], False, _CONFIG, bool
197
+ )
198
+ RATE_LIMIT_MAX_MESSAGES = _get_config_value(
199
+ "AGENT_HUB_RATE_LIMIT_MAX_MESSAGES",
200
+ ["rate_limit", "max_messages_per_window"],
201
+ 100,
202
+ _CONFIG,
203
+ int,
204
+ )
205
+ RATE_LIMIT_WINDOW_SECONDS = _get_config_value(
206
+ "AGENT_HUB_RATE_LIMIT_WINDOW_SECONDS", ["rate_limit", "window_seconds"], 3600, _CONFIG, int
207
+ )
208
+ RATE_LIMIT_COOLDOWN_SECONDS = _get_config_value(
209
+ "AGENT_HUB_RATE_LIMIT_COOLDOWN_SECONDS",
210
+ ["rate_limit", "cooldown_seconds"],
211
+ 5,
212
+ _CONFIG,
213
+ int,
214
+ )
215
+
216
+ # =============================================================================
217
+ # Agent & Session Configuration
218
+ # =============================================================================
219
+
220
+ AGENT_STALE_SECONDS = _get_config_value(
221
+ "AGENT_HUB_AGENT_STALE_SECONDS", ["agent", "stale_seconds"], 600, _CONFIG, int
222
+ )
223
+ MESSAGE_TTL_SECONDS = _get_config_value(
224
+ "AGENT_HUB_MESSAGE_TTL_SECONDS", ["message", "ttl_seconds"], 3600, _CONFIG, int
225
+ )
226
+
227
+ # =============================================================================
228
+ # Polling Intervals
229
+ # =============================================================================
230
+
231
+ SESSION_POLL_SECONDS = _get_config_value(
232
+ "AGENT_HUB_SESSION_POLL_SECONDS", ["poll", "session_seconds"], 5, _CONFIG, int
233
+ )
234
+ GC_INTERVAL_SECONDS = _get_config_value(
235
+ "AGENT_HUB_GC_INTERVAL_SECONDS", ["poll", "gc_seconds"], 60, _CONFIG, int
236
+ )
237
+ METRICS_INTERVAL = _get_config_value(
238
+ "AGENT_HUB_METRICS_INTERVAL", ["poll", "metrics_seconds"], 60, _CONFIG, int
239
+ )
240
+
241
+ # =============================================================================
242
+ # Injection Configuration
243
+ # =============================================================================
244
+
245
+ INJECTION_TIMEOUT = _get_config_value(
246
+ "AGENT_HUB_INJECTION_TIMEOUT", ["injection", "timeout_seconds"], 30, _CONFIG, int
247
+ )
248
+ INJECTION_RETRIES = _get_config_value(
249
+ "AGENT_HUB_INJECTION_RETRIES", ["injection", "retries"], 3, _CONFIG, int
250
+ )
251
+ INJECTION_WORKERS = _get_config_value(
252
+ "AGENT_HUB_INJECTION_WORKERS", ["injection", "workers"], 3, _CONFIG, int
253
+ )
254
+
255
+ # =============================================================================
256
+ # Caching Configuration
257
+ # =============================================================================
258
+
259
+ SESSION_CACHE_TTL = _get_config_value(
260
+ "AGENT_HUB_SESSION_CACHE_TTL", ["cache", "session_ttl_seconds"], 5, _CONFIG, int
261
+ )
262
+
263
+ # =============================================================================
264
+ # Orientation Configuration
265
+ # =============================================================================
266
+
267
+ ORIENTATION_RETRY_DELAY = _get_config_value(
268
+ "AGENT_HUB_ORIENTATION_RETRY_DELAY", ["orientation", "retry_delay_seconds"], 30, _CONFIG, int
269
+ )
270
+ ORIENTATION_RETRY_MAX = _get_config_value(
271
+ "AGENT_HUB_ORIENTATION_RETRY_MAX", ["orientation", "retry_max"], 5, _CONFIG, int
272
+ )
273
+
274
+ # =============================================================================
275
+ # Logging Configuration
276
+ # =============================================================================
277
+
278
+ LOG_LEVEL = _get_config_value(
279
+ "AGENT_HUB_DAEMON_LOG_LEVEL", ["logging", "level"], "INFO", _CONFIG, str
280
+ )
281
+
282
+ # =============================================================================
283
+ # Internal State (global mutable state)
284
+ # =============================================================================
285
+
286
+ # Track which sessions have been oriented
287
+ ORIENTED_SESSIONS: set[str] = set()
288
+
289
+ # Threading lock for ORIENTED_SESSIONS access
290
+ ORIENTED_SESSIONS_LOCK = threading.Lock()
291
+
292
+ # Track session-to-agent mappings
293
+ SESSION_AGENTS: dict[str, dict[str, Any]] = {}
294
+
295
+ # Track pending orientation retries
296
+ ORIENTATION_PENDING: dict[str, dict] = {}
297
+
298
+ # Session cache (avoids repeated API calls)
299
+ _sessions_cache: list[dict] = []
300
+ _sessions_cache_time: float = 0
301
+
302
+ # Coordinator session ID
303
+ COORDINATOR_SESSION_ID: str | None = None
304
+
305
+ # Daemon start time - only orient sessions created after this
306
+ DAEMON_START_TIME_MS: int = int(time.time() * 1000)
307
+
308
+
309
+ def _is_running_from_source() -> bool:
310
+ """Check if running from source (development) or installed package."""
311
+ this_file = Path(__file__)
312
+ # If we're in a site-packages or dist-packages, we're installed
313
+ if "site-packages" in str(this_file) or "dist-packages" in str(this_file):
314
+ return False
315
+ # If parent of parent has pyproject.toml, we're likely in dev
316
+ return (this_file.parent.parent.parent / "pyproject.toml").exists()
317
+
318
+
319
+ def _get_coordinator_title() -> str:
320
+ """Get the coordinator session title from environment or default."""
321
+ return os.environ.get("AGENT_HUB_COORDINATOR_TITLE", "Agent Hub Coordinator")
322
+
323
+
324
+ # Initialize logging
325
+ logging.basicConfig(
326
+ level=getattr(logging, LOG_LEVEL),
327
+ format="%(asctime)s [%(levelname)s] %(message)s",
328
+ datefmt="%Y-%m-%d %H:%M:%S",
329
+ )
330
+ log = logging.getLogger(__name__)