gobby 0.2.5__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.
Files changed (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. gobby-0.2.5.dist-info/top_level.txt +1 -0
gobby/utils/metrics.py ADDED
@@ -0,0 +1,589 @@
1
+ """
2
+ Metrics collection service for Gobby Client Runtime.
3
+
4
+ Provides in-memory metrics collection for monitoring daemon and HTTP
5
+ server performance with Prometheus-compatible export format.
6
+ """
7
+
8
+ import logging
9
+ import time
10
+ from dataclasses import dataclass, field
11
+ from threading import Lock
12
+ from typing import Any
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class Counter:
19
+ """Simple counter metric."""
20
+
21
+ name: str
22
+ help_text: str
23
+ value: int = 0
24
+ labels: dict[str, str] = field(default_factory=dict)
25
+
26
+ def inc(self, amount: int = 1) -> None:
27
+ """Increment counter by amount."""
28
+ self.value += amount
29
+
30
+
31
+ @dataclass
32
+ class Gauge:
33
+ """Gauge metric that can go up or down."""
34
+
35
+ name: str
36
+ help_text: str
37
+ value: float = 0.0
38
+ labels: dict[str, str] = field(default_factory=dict)
39
+
40
+ def set(self, value: float) -> None:
41
+ """Set gauge to value."""
42
+ self.value = value
43
+
44
+ def inc(self, amount: float = 1.0) -> None:
45
+ """Increment gauge by amount."""
46
+ self.value += amount
47
+
48
+ def dec(self, amount: float = 1.0) -> None:
49
+ """Decrement gauge by amount."""
50
+ self.value -= amount
51
+
52
+
53
+ @dataclass
54
+ class Histogram:
55
+ """Histogram metric for tracking distributions."""
56
+
57
+ name: str
58
+ help_text: str
59
+ buckets: list[float] = field(
60
+ default_factory=lambda: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
61
+ )
62
+ bucket_counts: dict[float, int] = field(default_factory=dict)
63
+ sum: float = 0.0
64
+ count: int = 0
65
+ labels: dict[str, str] = field(default_factory=dict)
66
+
67
+ def __post_init__(self) -> None:
68
+ """Initialize bucket counts."""
69
+ for bucket in self.buckets:
70
+ self.bucket_counts[bucket] = 0
71
+
72
+ def observe(self, value: float) -> None:
73
+ """Record an observation."""
74
+ self.sum += value
75
+ self.count += 1
76
+
77
+ # Update bucket counts
78
+ for bucket in self.buckets:
79
+ if value <= bucket:
80
+ self.bucket_counts[bucket] += 1
81
+
82
+
83
+ class MetricsCollector:
84
+ """
85
+ In-memory metrics collector with thread-safe operations.
86
+
87
+ Collects counters, gauges, and histograms for monitoring
88
+ daemon health, HTTP performance, and memory operations.
89
+ """
90
+
91
+ def __init__(self) -> None:
92
+ """Initialize metrics collector."""
93
+ self._lock = Lock()
94
+ self._counters: dict[str, Counter] = {}
95
+ self._gauges: dict[str, Gauge] = {}
96
+ self._histograms: dict[str, Histogram] = {}
97
+ self._start_time = time.time()
98
+
99
+ # Initialize core metrics
100
+ self._initialize_metrics()
101
+
102
+ def _initialize_metrics(self) -> None:
103
+ """Initialize standard metrics."""
104
+ # HTTP request metrics
105
+ self.register_counter(
106
+ "http_requests_total",
107
+ "Total number of HTTP requests received",
108
+ )
109
+ self.register_counter(
110
+ "http_requests_errors_total",
111
+ "Total number of HTTP requests that resulted in errors",
112
+ )
113
+ self.register_histogram(
114
+ "http_request_duration_seconds",
115
+ "HTTP request duration in seconds",
116
+ )
117
+ self.register_counter(
118
+ "session_registrations_total",
119
+ "Total number of session registration requests",
120
+ )
121
+
122
+ # Memory operation metrics
123
+ self.register_counter(
124
+ "memory_saves_total",
125
+ "Total number of memory save requests",
126
+ )
127
+ self.register_counter(
128
+ "memory_saves_succeeded_total",
129
+ "Total number of successful memory saves",
130
+ )
131
+ self.register_counter(
132
+ "memory_saves_failed_total",
133
+ "Total number of failed memory saves",
134
+ )
135
+ self.register_histogram(
136
+ "memory_save_duration_seconds",
137
+ "Memory save operation duration in seconds",
138
+ )
139
+
140
+ # Context restore metrics
141
+ self.register_counter(
142
+ "context_restores_total",
143
+ "Total number of context restore requests",
144
+ )
145
+ self.register_counter(
146
+ "context_restores_succeeded_total",
147
+ "Total number of successful context restores",
148
+ )
149
+ self.register_counter(
150
+ "context_restores_failed_total",
151
+ "Total number of failed context restores",
152
+ )
153
+ self.register_histogram(
154
+ "context_restore_duration_seconds",
155
+ "Context restore operation duration in seconds",
156
+ )
157
+
158
+ # MCP call metrics
159
+ self.register_counter(
160
+ "mcp_calls_total",
161
+ "Total number of MCP calls made",
162
+ )
163
+ self.register_counter(
164
+ "mcp_calls_succeeded_total",
165
+ "Total number of successful MCP calls",
166
+ )
167
+ self.register_counter(
168
+ "mcp_calls_failed_total",
169
+ "Total number of failed MCP calls",
170
+ )
171
+ self.register_histogram(
172
+ "mcp_call_duration_seconds",
173
+ "MCP call duration in seconds",
174
+ )
175
+ self.register_gauge(
176
+ "mcp_active_connections",
177
+ "Number of active MCP connections",
178
+ )
179
+
180
+ # MCP tool call metrics (specific to tool invocations)
181
+ self.register_counter(
182
+ "mcp_tool_calls_total",
183
+ "Total number of MCP tool calls made",
184
+ )
185
+ self.register_counter(
186
+ "mcp_tool_calls_succeeded_total",
187
+ "Total number of successful MCP tool calls",
188
+ )
189
+ self.register_counter(
190
+ "mcp_tool_calls_failed_total",
191
+ "Total number of failed MCP tool calls",
192
+ )
193
+
194
+ # Background task metrics
195
+ self.register_gauge(
196
+ "background_tasks_active",
197
+ "Number of currently active background tasks",
198
+ )
199
+ self.register_counter(
200
+ "background_tasks_total",
201
+ "Total number of background tasks created",
202
+ )
203
+ self.register_counter(
204
+ "background_tasks_completed_total",
205
+ "Total number of background tasks completed",
206
+ )
207
+ self.register_counter(
208
+ "background_tasks_failed_total",
209
+ "Total number of background tasks that failed",
210
+ )
211
+
212
+ # Daemon health metrics
213
+ self.register_gauge(
214
+ "daemon_uptime_seconds",
215
+ "Daemon uptime in seconds",
216
+ )
217
+ self.register_gauge(
218
+ "daemon_memory_usage_bytes",
219
+ "Daemon memory usage in bytes",
220
+ )
221
+ self.register_gauge(
222
+ "daemon_cpu_percent",
223
+ "Daemon CPU usage percentage",
224
+ )
225
+
226
+ # Hook execution metrics
227
+ self.register_counter(
228
+ "hooks_total",
229
+ "Total number of hook executions",
230
+ )
231
+ self.register_counter(
232
+ "hooks_succeeded_total",
233
+ "Total number of successful hook executions",
234
+ )
235
+ self.register_counter(
236
+ "hooks_failed_total",
237
+ "Total number of failed hook executions",
238
+ )
239
+
240
+ def register_counter(
241
+ self, name: str, help_text: str, labels: dict[str, str] | None = None
242
+ ) -> Counter:
243
+ """
244
+ Register a new counter metric.
245
+
246
+ Args:
247
+ name: Metric name
248
+ help_text: Description of what this metric measures
249
+ labels: Optional labels for this metric
250
+
251
+ Returns:
252
+ Counter instance
253
+ """
254
+ with self._lock:
255
+ if name in self._counters:
256
+ return self._counters[name]
257
+
258
+ counter = Counter(name=name, help_text=help_text, labels=labels or {})
259
+ self._counters[name] = counter
260
+ return counter
261
+
262
+ def register_gauge(
263
+ self, name: str, help_text: str, labels: dict[str, str] | None = None
264
+ ) -> Gauge:
265
+ """
266
+ Register a new gauge metric.
267
+
268
+ Args:
269
+ name: Metric name
270
+ help_text: Description of what this metric measures
271
+ labels: Optional labels for this metric
272
+
273
+ Returns:
274
+ Gauge instance
275
+ """
276
+ with self._lock:
277
+ if name in self._gauges:
278
+ return self._gauges[name]
279
+
280
+ gauge = Gauge(name=name, help_text=help_text, labels=labels or {})
281
+ self._gauges[name] = gauge
282
+ return gauge
283
+
284
+ def register_histogram(
285
+ self,
286
+ name: str,
287
+ help_text: str,
288
+ buckets: list[float] | None = None,
289
+ labels: dict[str, str] | None = None,
290
+ ) -> Histogram:
291
+ """
292
+ Register a new histogram metric.
293
+
294
+ Args:
295
+ name: Metric name
296
+ help_text: Description of what this metric measures
297
+ buckets: Histogram buckets (defaults to standard durations)
298
+ labels: Optional labels for this metric
299
+
300
+ Returns:
301
+ Histogram instance
302
+ """
303
+ with self._lock:
304
+ if name in self._histograms:
305
+ return self._histograms[name]
306
+
307
+ histogram = Histogram(
308
+ name=name,
309
+ help_text=help_text,
310
+ buckets=buckets or [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0],
311
+ labels=labels or {},
312
+ )
313
+ self._histograms[name] = histogram
314
+ return histogram
315
+
316
+ def inc_counter(self, name: str, amount: int = 1) -> None:
317
+ """
318
+ Increment a counter by amount.
319
+
320
+ Args:
321
+ name: Counter name
322
+ amount: Amount to increment by (default: 1)
323
+ """
324
+ with self._lock:
325
+ if name in self._counters:
326
+ self._counters[name].inc(amount)
327
+ else:
328
+ logger.warning(f"Counter {name} not registered")
329
+
330
+ def set_gauge(self, name: str, value: float) -> None:
331
+ """
332
+ Set a gauge to value.
333
+
334
+ Args:
335
+ name: Gauge name
336
+ value: Value to set
337
+ """
338
+ with self._lock:
339
+ if name in self._gauges:
340
+ self._gauges[name].set(value)
341
+ else:
342
+ logger.warning(f"Gauge {name} not registered")
343
+
344
+ def inc_gauge(self, name: str, amount: float = 1.0) -> None:
345
+ """
346
+ Increment a gauge by amount.
347
+
348
+ Args:
349
+ name: Gauge name
350
+ amount: Amount to increment by
351
+ """
352
+ with self._lock:
353
+ if name in self._gauges:
354
+ self._gauges[name].inc(amount)
355
+ else:
356
+ logger.warning(f"Gauge {name} not registered")
357
+
358
+ def dec_gauge(self, name: str, amount: float = 1.0) -> None:
359
+ """
360
+ Decrement a gauge by amount.
361
+
362
+ Args:
363
+ name: Gauge name
364
+ amount: Amount to decrement by
365
+ """
366
+ with self._lock:
367
+ if name in self._gauges:
368
+ self._gauges[name].dec(amount)
369
+ else:
370
+ logger.warning(f"Gauge {name} not registered")
371
+
372
+ def observe_histogram(self, name: str, value: float) -> None:
373
+ """
374
+ Record an observation in a histogram.
375
+
376
+ Args:
377
+ name: Histogram name
378
+ value: Value to observe
379
+ """
380
+ with self._lock:
381
+ if name in self._histograms:
382
+ self._histograms[name].observe(value)
383
+ else:
384
+ logger.warning(f"Histogram {name} not registered")
385
+
386
+ def get_uptime(self) -> float:
387
+ """
388
+ Get collector uptime in seconds.
389
+
390
+ Returns:
391
+ Uptime in seconds
392
+ """
393
+ return time.time() - self._start_time
394
+
395
+ def update_daemon_metrics(self, pid: int | None = None) -> None:
396
+ """
397
+ Update daemon health metrics (uptime, memory, CPU).
398
+
399
+ Args:
400
+ pid: Process ID to monitor. If None, uses current process.
401
+ """
402
+ import os
403
+
404
+ import psutil
405
+
406
+ try:
407
+ # Get process
408
+ process = psutil.Process(pid) if pid else psutil.Process(os.getpid())
409
+
410
+ # Update uptime
411
+ self.set_gauge("daemon_uptime_seconds", self.get_uptime())
412
+
413
+ # Update memory usage
414
+ mem_info = process.memory_info()
415
+ self.set_gauge("daemon_memory_usage_bytes", float(mem_info.rss))
416
+
417
+ # Update CPU usage
418
+ cpu_percent = process.cpu_percent(interval=0.1)
419
+ self.set_gauge("daemon_cpu_percent", cpu_percent)
420
+
421
+ except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
422
+ logger.warning(f"Failed to update daemon metrics: {e}")
423
+
424
+ def record_mcp_call(self, duration: float, success: bool = True) -> None:
425
+ """
426
+ Record an MCP call with duration and success status.
427
+
428
+ Args:
429
+ duration: Call duration in seconds
430
+ success: Whether the call succeeded
431
+ """
432
+ self.inc_counter("mcp_calls_total")
433
+ if success:
434
+ self.inc_counter("mcp_calls_succeeded_total")
435
+ else:
436
+ self.inc_counter("mcp_calls_failed_total")
437
+ self.observe_histogram("mcp_call_duration_seconds", duration)
438
+
439
+ def record_http_request(self, duration: float, error: bool = False) -> None:
440
+ """
441
+ Record an HTTP request with duration and error status.
442
+
443
+ Args:
444
+ duration: Request duration in seconds
445
+ error: Whether the request resulted in an error
446
+ """
447
+ self.inc_counter("http_requests_total")
448
+ if error:
449
+ self.inc_counter("http_requests_errors_total")
450
+ self.observe_histogram("http_request_duration_seconds", duration)
451
+
452
+ def record_memory_save(self, duration: float, success: bool = True) -> None:
453
+ """
454
+ Record a memory save operation.
455
+
456
+ Args:
457
+ duration: Operation duration in seconds
458
+ success: Whether the save succeeded
459
+ """
460
+ self.inc_counter("memory_saves_total")
461
+ if success:
462
+ self.inc_counter("memory_saves_succeeded_total")
463
+ else:
464
+ self.inc_counter("memory_saves_failed_total")
465
+ self.observe_histogram("memory_save_duration_seconds", duration)
466
+
467
+ def record_context_restore(self, duration: float, success: bool = True) -> None:
468
+ """
469
+ Record a context restore operation.
470
+
471
+ Args:
472
+ duration: Operation duration in seconds
473
+ success: Whether the restore succeeded
474
+ """
475
+ self.inc_counter("context_restores_total")
476
+ if success:
477
+ self.inc_counter("context_restores_succeeded_total")
478
+ else:
479
+ self.inc_counter("context_restores_failed_total")
480
+ self.observe_histogram("context_restore_duration_seconds", duration)
481
+
482
+ def get_all_metrics(self) -> dict[str, Any]:
483
+ """
484
+ Get all metrics as a dictionary.
485
+
486
+ Returns:
487
+ Dictionary containing all metrics
488
+ """
489
+ with self._lock:
490
+ return {
491
+ "counters": {
492
+ name: {"value": counter.value, "labels": counter.labels}
493
+ for name, counter in self._counters.items()
494
+ },
495
+ "gauges": {
496
+ name: {"value": gauge.value, "labels": gauge.labels}
497
+ for name, gauge in self._gauges.items()
498
+ },
499
+ "histograms": {
500
+ name: {
501
+ "count": hist.count,
502
+ "sum": hist.sum,
503
+ "buckets": hist.bucket_counts,
504
+ "labels": hist.labels,
505
+ }
506
+ for name, hist in self._histograms.items()
507
+ },
508
+ "uptime_seconds": self.get_uptime(),
509
+ }
510
+
511
+ def export_prometheus(self) -> str:
512
+ """
513
+ Export metrics in Prometheus text format.
514
+
515
+ Returns:
516
+ Metrics in Prometheus exposition format
517
+ """
518
+ lines = []
519
+
520
+ with self._lock:
521
+ # Export counters
522
+ for name, counter in self._counters.items():
523
+ lines.append(f"# HELP {name} {counter.help_text}")
524
+ lines.append(f"# TYPE {name} counter")
525
+ labels_str = self._format_labels(counter.labels)
526
+ lines.append(f"{name}{labels_str} {counter.value}")
527
+
528
+ # Export gauges
529
+ for name, gauge in self._gauges.items():
530
+ lines.append(f"# HELP {name} {gauge.help_text}")
531
+ lines.append(f"# TYPE {name} gauge")
532
+ labels_str = self._format_labels(gauge.labels)
533
+ lines.append(f"{name}{labels_str} {gauge.value}")
534
+
535
+ # Export histograms
536
+ for name, hist in self._histograms.items():
537
+ lines.append(f"# HELP {name} {hist.help_text}")
538
+ lines.append(f"# TYPE {name} histogram")
539
+ labels_str = self._format_labels(hist.labels)
540
+
541
+ # Export bucket counts
542
+ for bucket, count in sorted(hist.bucket_counts.items()):
543
+ bucket_labels = {**hist.labels, "le": str(bucket)}
544
+ bucket_labels_str = self._format_labels(bucket_labels)
545
+ lines.append(f"{name}_bucket{bucket_labels_str} {count}")
546
+
547
+ # Export +Inf bucket
548
+ inf_labels = {**hist.labels, "le": "+Inf"}
549
+ inf_labels_str = self._format_labels(inf_labels)
550
+ lines.append(f"{name}_bucket{inf_labels_str} {hist.count}")
551
+
552
+ # Export sum and count
553
+ lines.append(f"{name}_sum{labels_str} {hist.sum}")
554
+ lines.append(f"{name}_count{labels_str} {hist.count}")
555
+
556
+ return "\n".join(lines) + "\n"
557
+
558
+ def _format_labels(self, labels: dict[str, str]) -> str:
559
+ """
560
+ Format labels for Prometheus exposition format.
561
+
562
+ Args:
563
+ labels: Label dictionary
564
+
565
+ Returns:
566
+ Formatted labels string (e.g., '{method="GET",status="200"}')
567
+ """
568
+ if not labels:
569
+ return ""
570
+
571
+ label_pairs = [f'{k}="{v}"' for k, v in sorted(labels.items())]
572
+ return "{" + ",".join(label_pairs) + "}"
573
+
574
+
575
+ # Global metrics collector instance
576
+ _metrics_collector: MetricsCollector | None = None
577
+
578
+
579
+ def get_metrics_collector() -> MetricsCollector:
580
+ """
581
+ Get global metrics collector instance.
582
+
583
+ Returns:
584
+ MetricsCollector instance
585
+ """
586
+ global _metrics_collector
587
+ if _metrics_collector is None:
588
+ _metrics_collector = MetricsCollector()
589
+ return _metrics_collector