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
@@ -0,0 +1,195 @@
1
+ """
2
+ Gemini transcript parser.
3
+
4
+ Parses JSONL transcript files generated by Gemini CLI.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+ from datetime import UTC, datetime
12
+ from typing import Any
13
+
14
+ from gobby.sessions.transcripts.base import ParsedMessage, TokenUsage
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class GeminiTranscriptParser:
20
+ """
21
+ Parses JSONL transcript files from Gemini.
22
+
23
+ Implements the TranscriptParser protocol for Gemini's transcript format.
24
+ Assumes a standard JSONL structure where each line is a message or event.
25
+ """
26
+
27
+ def __init__(self, logger_instance: logging.Logger | None = None):
28
+ """
29
+ Initialize GeminiTranscriptParser.
30
+
31
+ Args:
32
+ logger_instance: Optional logger instance.
33
+ """
34
+ self.logger = logger_instance or logger
35
+
36
+ def extract_last_messages(
37
+ self, turns: list[dict[str, Any]], num_pairs: int = 2
38
+ ) -> list[dict[str, Any]]:
39
+ """
40
+ Extract last N user<>agent message pairs.
41
+ """
42
+ messages: list[dict[str, str]] = []
43
+ for turn in reversed(turns):
44
+ # Adapt to generic turn structure
45
+ # Assumes generic schema: {"role": "...", "content": "..."} or nested in "message"
46
+ role = turn.get("role") or turn.get("message", {}).get("role")
47
+ content = turn.get("content") or turn.get("message", {}).get("content")
48
+
49
+ if role in ["user", "model", "assistant"]:
50
+ norm_role = "assistant" if role == "model" else role
51
+
52
+ # Handle complex content types if necessary
53
+ if isinstance(content, list):
54
+ content = " ".join(str(part) for part in content)
55
+
56
+ messages.insert(0, {"role": norm_role, "content": str(content)})
57
+ if len(messages) >= num_pairs * 2:
58
+ break
59
+ return messages
60
+
61
+ def extract_turns_since_clear(
62
+ self, turns: list[dict[str, Any]], max_turns: int = 50
63
+ ) -> list[dict[str, Any]]:
64
+ """
65
+ Extract turns since the most recent session boundary.
66
+ For Gemini, we might look for specific clear events or just return the tail.
67
+ """
68
+ # Placeholder: just return last N turns for now until we know the clear signal
69
+ return turns[-max_turns:] if len(turns) > max_turns else turns
70
+
71
+ def is_session_boundary(self, turn: dict[str, Any]) -> bool:
72
+ """
73
+ Check if a turn is a session boundary.
74
+ """
75
+ # Placeholder for Gemini specific boundary
76
+ return False
77
+
78
+ def parse_line(self, line: str, index: int) -> ParsedMessage | None:
79
+ """
80
+ Parse a single line from the transcript JSONL.
81
+ """
82
+ if not line.strip():
83
+ return None
84
+
85
+ try:
86
+ data = json.loads(line)
87
+ except json.JSONDecodeError:
88
+ self.logger.warning(f"Invalid JSON at line {index}")
89
+ return None
90
+
91
+ # Extract timestamp
92
+ timestamp_str = data.get("timestamp") or datetime.now(UTC).isoformat()
93
+ try:
94
+ timestamp = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
95
+ except ValueError:
96
+ timestamp = datetime.now(UTC)
97
+
98
+ # Determine role and content
99
+ # Check top-level or nested 'message'
100
+ role = data.get("role")
101
+ content = data.get("content")
102
+
103
+ if not role and "message" in data:
104
+ msg = data["message"]
105
+ role = msg.get("role")
106
+ content = msg.get("content")
107
+
108
+ if not role:
109
+ # Try type field common in other schemas
110
+ msg_type = data.get("type")
111
+ if msg_type == "user":
112
+ role = "user"
113
+ elif msg_type == "model":
114
+ role = "assistant"
115
+
116
+ # Normalize role
117
+ if role == "model":
118
+ role = "assistant"
119
+
120
+ if not role:
121
+ # Maybe a tool result or system event
122
+ if "tool_result" in data:
123
+ role = "tool"
124
+ content = str(data["tool_result"])
125
+ else:
126
+ # Unknown or uninteresting line
127
+ return None
128
+
129
+ # Normalize content
130
+ content_type = "text"
131
+ tool_name = None
132
+ tool_input = None
133
+ tool_result = None
134
+
135
+ if isinstance(content, list):
136
+ # Handle potential rich content
137
+ text_parts: list[str] = []
138
+ for part in content:
139
+ if isinstance(part, str):
140
+ text_parts.append(part)
141
+ elif isinstance(part, dict):
142
+ if "text" in part:
143
+ text_parts.append(str(part["text"]))
144
+ # Check for tool calls
145
+ if "functionCall" in part:
146
+ content_type = "tool_use"
147
+ tool_name = part["functionCall"].get("name")
148
+ tool_input = part["functionCall"].get("args")
149
+ content = " ".join(text_parts)
150
+ else:
151
+ content = str(content or "")
152
+
153
+ return ParsedMessage(
154
+ index=index,
155
+ role=role,
156
+ content=content,
157
+ content_type=content_type,
158
+ tool_name=tool_name,
159
+ tool_input=tool_input,
160
+ tool_result=tool_result,
161
+ timestamp=timestamp,
162
+ raw_json=data,
163
+ usage=self._extract_usage(data),
164
+ )
165
+
166
+ def _extract_usage(self, data: dict[str, Any]) -> TokenUsage | None:
167
+ """Extract token usage from Gemini message data."""
168
+ # Gemini API standard is usageMetadata
169
+ usage_data = data.get("usageMetadata")
170
+
171
+ if not usage_data:
172
+ return None
173
+
174
+ return TokenUsage(
175
+ input_tokens=usage_data.get("promptTokenCount", 0),
176
+ output_tokens=usage_data.get("candidatesTokenCount", 0),
177
+ # Gemini doesn't always split cache tokens in this view, strictly speaking
178
+ # but usually total is prompt + candidates
179
+ total_cost_usd=None, # Cost calculation not standard in CLI output
180
+ )
181
+
182
+ def parse_lines(self, lines: list[str], start_index: int = 0) -> list[ParsedMessage]:
183
+ """
184
+ Parse a list of transcript lines.
185
+ """
186
+ parsed_messages = []
187
+ current_index = start_index
188
+
189
+ for line in lines:
190
+ message = self.parse_line(line, current_index)
191
+ if message:
192
+ parsed_messages.append(message)
193
+ current_index += 1
194
+
195
+ return parsed_messages
@@ -0,0 +1,21 @@
1
+ """Local storage layer for Gobby daemon."""
2
+
3
+ from gobby.storage.database import LocalDatabase
4
+ from gobby.storage.inter_session_messages import InterSessionMessageManager
5
+ from gobby.storage.mcp import LocalMCPManager
6
+ from gobby.storage.migrations import run_migrations
7
+ from gobby.storage.projects import LocalProjectManager
8
+ from gobby.storage.sessions import LocalSessionManager
9
+ from gobby.storage.task_dependencies import TaskDependencyManager
10
+ from gobby.storage.tasks import LocalTaskManager
11
+
12
+ __all__ = [
13
+ "InterSessionMessageManager",
14
+ "LocalDatabase",
15
+ "LocalMCPManager",
16
+ "LocalProjectManager",
17
+ "LocalSessionManager",
18
+ "LocalTaskManager",
19
+ "TaskDependencyManager",
20
+ "run_migrations",
21
+ ]
@@ -0,0 +1,409 @@
1
+ """Storage manager for agent runs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import uuid
7
+ from dataclasses import dataclass
8
+ from datetime import UTC, datetime
9
+ from typing import Any, Literal
10
+
11
+ from gobby.storage.database import DatabaseProtocol
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ AgentRunStatus = Literal["pending", "running", "success", "error", "timeout", "cancelled"]
16
+
17
+
18
+ @dataclass
19
+ class AgentRun:
20
+ """Agent run data model."""
21
+
22
+ id: str
23
+ parent_session_id: str
24
+ provider: str
25
+ prompt: str
26
+ status: AgentRunStatus
27
+ created_at: str
28
+ updated_at: str
29
+ # Optional fields
30
+ child_session_id: str | None = None
31
+ workflow_name: str | None = None
32
+ model: str | None = None
33
+ result: str | None = None
34
+ error: str | None = None
35
+ tool_calls_count: int = 0
36
+ turns_used: int = 0
37
+ started_at: str | None = None
38
+ completed_at: str | None = None
39
+
40
+ @classmethod
41
+ def from_row(cls, row: Any) -> AgentRun:
42
+ """Create AgentRun from database row."""
43
+ return cls(
44
+ id=row["id"],
45
+ parent_session_id=row["parent_session_id"],
46
+ child_session_id=row["child_session_id"],
47
+ workflow_name=row["workflow_name"],
48
+ provider=row["provider"],
49
+ model=row["model"],
50
+ status=row["status"],
51
+ prompt=row["prompt"],
52
+ result=row["result"],
53
+ error=row["error"],
54
+ tool_calls_count=row["tool_calls_count"] or 0,
55
+ turns_used=row["turns_used"] or 0,
56
+ started_at=row["started_at"],
57
+ completed_at=row["completed_at"],
58
+ created_at=row["created_at"],
59
+ updated_at=row["updated_at"],
60
+ )
61
+
62
+ def to_dict(self) -> dict[str, Any]:
63
+ """Convert to dictionary."""
64
+ return {
65
+ "id": self.id,
66
+ "parent_session_id": self.parent_session_id,
67
+ "child_session_id": self.child_session_id,
68
+ "workflow_name": self.workflow_name,
69
+ "provider": self.provider,
70
+ "model": self.model,
71
+ "status": self.status,
72
+ "prompt": self.prompt,
73
+ "result": self.result,
74
+ "error": self.error,
75
+ "tool_calls_count": self.tool_calls_count,
76
+ "turns_used": self.turns_used,
77
+ "started_at": self.started_at,
78
+ "completed_at": self.completed_at,
79
+ "created_at": self.created_at,
80
+ "updated_at": self.updated_at,
81
+ }
82
+
83
+
84
+ class LocalAgentRunManager:
85
+ """Manager for agent run storage operations."""
86
+
87
+ def __init__(self, db: DatabaseProtocol):
88
+ """Initialize with database connection."""
89
+ self.db = db
90
+
91
+ def create(
92
+ self,
93
+ parent_session_id: str,
94
+ provider: str,
95
+ prompt: str,
96
+ workflow_name: str | None = None,
97
+ model: str | None = None,
98
+ child_session_id: str | None = None,
99
+ ) -> AgentRun:
100
+ """
101
+ Create a new agent run.
102
+
103
+ Args:
104
+ parent_session_id: Session that spawned this agent.
105
+ provider: LLM provider (claude, gemini, etc.)
106
+ prompt: The prompt given to the agent.
107
+ workflow_name: Optional workflow being executed.
108
+ model: Optional model override.
109
+ child_session_id: Optional child session for the agent.
110
+
111
+ Returns:
112
+ Created AgentRun.
113
+ """
114
+ run_id = f"ar-{uuid.uuid4().hex[:12]}"
115
+ now = datetime.now(UTC).isoformat()
116
+
117
+ self.db.execute(
118
+ """
119
+ INSERT INTO agent_runs (
120
+ id, parent_session_id, child_session_id, workflow_name,
121
+ provider, model, status, prompt, created_at, updated_at
122
+ )
123
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?)
124
+ """,
125
+ (
126
+ run_id,
127
+ parent_session_id,
128
+ child_session_id,
129
+ workflow_name,
130
+ provider,
131
+ model,
132
+ prompt,
133
+ now,
134
+ now,
135
+ ),
136
+ )
137
+
138
+ logger.debug(f"Created agent run {run_id} for session {parent_session_id}")
139
+ agent_run = self.get(run_id)
140
+ if agent_run is None:
141
+ raise RuntimeError(f"Failed to retrieve newly created agent run: {run_id}")
142
+ return agent_run
143
+
144
+ def get(self, run_id: str) -> AgentRun | None:
145
+ """Get agent run by ID."""
146
+ row = self.db.fetchone("SELECT * FROM agent_runs WHERE id = ?", (run_id,))
147
+ return AgentRun.from_row(row) if row else None
148
+
149
+ def start(self, run_id: str) -> AgentRun | None:
150
+ """Mark agent run as started."""
151
+ now = datetime.now(UTC).isoformat()
152
+ self.db.execute(
153
+ """
154
+ UPDATE agent_runs
155
+ SET status = 'running', started_at = ?, updated_at = ?
156
+ WHERE id = ?
157
+ """,
158
+ (now, now, run_id),
159
+ )
160
+ return self.get(run_id)
161
+
162
+ def complete(
163
+ self,
164
+ run_id: str,
165
+ result: str,
166
+ tool_calls_count: int = 0,
167
+ turns_used: int = 0,
168
+ ) -> AgentRun | None:
169
+ """
170
+ Mark agent run as completed successfully.
171
+
172
+ Args:
173
+ run_id: The agent run ID.
174
+ result: The agent's output/result.
175
+ tool_calls_count: Number of tool calls made.
176
+ turns_used: Number of turns used.
177
+
178
+ Returns:
179
+ Updated AgentRun.
180
+ """
181
+ now = datetime.now(UTC).isoformat()
182
+ self.db.execute(
183
+ """
184
+ UPDATE agent_runs
185
+ SET status = 'success',
186
+ result = ?,
187
+ tool_calls_count = ?,
188
+ turns_used = ?,
189
+ completed_at = ?,
190
+ updated_at = ?
191
+ WHERE id = ?
192
+ """,
193
+ (result, tool_calls_count, turns_used, now, now, run_id),
194
+ )
195
+ return self.get(run_id)
196
+
197
+ def fail(
198
+ self,
199
+ run_id: str,
200
+ error: str,
201
+ tool_calls_count: int = 0,
202
+ turns_used: int = 0,
203
+ ) -> AgentRun | None:
204
+ """
205
+ Mark agent run as failed.
206
+
207
+ Args:
208
+ run_id: The agent run ID.
209
+ error: Error message.
210
+ tool_calls_count: Number of tool calls made before failure.
211
+ turns_used: Number of turns used before failure.
212
+
213
+ Returns:
214
+ Updated AgentRun.
215
+ """
216
+ now = datetime.now(UTC).isoformat()
217
+ self.db.execute(
218
+ """
219
+ UPDATE agent_runs
220
+ SET status = 'error',
221
+ error = ?,
222
+ tool_calls_count = ?,
223
+ turns_used = ?,
224
+ completed_at = ?,
225
+ updated_at = ?
226
+ WHERE id = ?
227
+ """,
228
+ (error, tool_calls_count, turns_used, now, now, run_id),
229
+ )
230
+ return self.get(run_id)
231
+
232
+ def timeout(self, run_id: str, turns_used: int = 0) -> AgentRun | None:
233
+ """Mark agent run as timed out."""
234
+ now = datetime.now(UTC).isoformat()
235
+ self.db.execute(
236
+ """
237
+ UPDATE agent_runs
238
+ SET status = 'timeout',
239
+ error = 'Execution timed out',
240
+ turns_used = ?,
241
+ completed_at = ?,
242
+ updated_at = ?
243
+ WHERE id = ?
244
+ """,
245
+ (turns_used, now, now, run_id),
246
+ )
247
+ return self.get(run_id)
248
+
249
+ def cancel(self, run_id: str) -> AgentRun | None:
250
+ """Mark agent run as cancelled."""
251
+ now = datetime.now(UTC).isoformat()
252
+ self.db.execute(
253
+ """
254
+ UPDATE agent_runs
255
+ SET status = 'cancelled', completed_at = ?, updated_at = ?
256
+ WHERE id = ?
257
+ """,
258
+ (now, now, run_id),
259
+ )
260
+ return self.get(run_id)
261
+
262
+ def update_child_session(self, run_id: str, child_session_id: str) -> AgentRun | None:
263
+ """Update the child session ID for an agent run."""
264
+ now = datetime.now(UTC).isoformat()
265
+ self.db.execute(
266
+ """
267
+ UPDATE agent_runs
268
+ SET child_session_id = ?, updated_at = ?
269
+ WHERE id = ?
270
+ """,
271
+ (child_session_id, now, run_id),
272
+ )
273
+ return self.get(run_id)
274
+
275
+ def list_by_session(
276
+ self,
277
+ parent_session_id: str,
278
+ status: AgentRunStatus | None = None,
279
+ limit: int = 100,
280
+ ) -> list[AgentRun]:
281
+ """
282
+ List agent runs for a session.
283
+
284
+ Args:
285
+ parent_session_id: The parent session ID.
286
+ status: Optional status filter.
287
+ limit: Maximum number of results.
288
+
289
+ Returns:
290
+ List of AgentRun objects.
291
+ """
292
+ if status:
293
+ rows = self.db.fetchall(
294
+ """
295
+ SELECT * FROM agent_runs
296
+ WHERE parent_session_id = ? AND status = ?
297
+ ORDER BY created_at DESC
298
+ LIMIT ?
299
+ """,
300
+ (parent_session_id, status, limit),
301
+ )
302
+ else:
303
+ rows = self.db.fetchall(
304
+ """
305
+ SELECT * FROM agent_runs
306
+ WHERE parent_session_id = ?
307
+ ORDER BY created_at DESC
308
+ LIMIT ?
309
+ """,
310
+ (parent_session_id, limit),
311
+ )
312
+ return [AgentRun.from_row(row) for row in rows]
313
+
314
+ def list_running(self, limit: int = 100) -> list[AgentRun]:
315
+ """List all currently running agent runs."""
316
+ rows = self.db.fetchall(
317
+ """
318
+ SELECT * FROM agent_runs
319
+ WHERE status = 'running'
320
+ ORDER BY started_at ASC
321
+ LIMIT ?
322
+ """,
323
+ (limit,),
324
+ )
325
+ return [AgentRun.from_row(row) for row in rows]
326
+
327
+ def count_by_session(self, parent_session_id: str) -> dict[str, int]:
328
+ """
329
+ Count agent runs by status for a session.
330
+
331
+ Args:
332
+ parent_session_id: The parent session ID.
333
+
334
+ Returns:
335
+ Dict mapping status to count.
336
+ """
337
+ rows = self.db.fetchall(
338
+ """
339
+ SELECT status, COUNT(*) as count
340
+ FROM agent_runs
341
+ WHERE parent_session_id = ?
342
+ GROUP BY status
343
+ """,
344
+ (parent_session_id,),
345
+ )
346
+ return {row["status"]: row["count"] for row in rows}
347
+
348
+ def delete(self, run_id: str) -> bool:
349
+ """Delete an agent run."""
350
+ cursor = self.db.execute("DELETE FROM agent_runs WHERE id = ?", (run_id,))
351
+ return bool(cursor.rowcount and cursor.rowcount > 0)
352
+
353
+ def cleanup_stale_runs(self, timeout_minutes: int = 30) -> int:
354
+ """
355
+ Mark stale running agent runs as timed out.
356
+
357
+ Args:
358
+ timeout_minutes: Minutes of inactivity before timeout.
359
+
360
+ Returns:
361
+ Number of runs timed out.
362
+ """
363
+ now = datetime.now(UTC).isoformat()
364
+ cursor = self.db.execute(
365
+ """
366
+ UPDATE agent_runs
367
+ SET status = 'timeout',
368
+ error = 'Stale run timed out',
369
+ completed_at = ?,
370
+ updated_at = ?
371
+ WHERE status = 'running'
372
+ AND datetime(started_at) < datetime('now', 'utc', ? || ' minutes')
373
+ """,
374
+ (now, now, f"-{timeout_minutes}"),
375
+ )
376
+ count = cursor.rowcount or 0
377
+ if count > 0:
378
+ logger.info(f"Timed out {count} stale agent runs (>{timeout_minutes}m)")
379
+ return count
380
+
381
+ def cleanup_stale_pending_runs(self, timeout_minutes: int = 60) -> int:
382
+ """
383
+ Mark stale pending agent runs as failed.
384
+
385
+ Pending runs that never started within the timeout period are marked as errors.
386
+
387
+ Args:
388
+ timeout_minutes: Minutes since creation before marking as failed.
389
+
390
+ Returns:
391
+ Number of runs failed.
392
+ """
393
+ now = datetime.now(UTC).isoformat()
394
+ cursor = self.db.execute(
395
+ """
396
+ UPDATE agent_runs
397
+ SET status = 'error',
398
+ error = 'Pending run never started',
399
+ completed_at = ?,
400
+ updated_at = ?
401
+ WHERE status = 'pending'
402
+ AND datetime(created_at) < datetime('now', 'utc', ? || ' minutes')
403
+ """,
404
+ (now, now, f"-{timeout_minutes}"),
405
+ )
406
+ count = cursor.rowcount or 0
407
+ if count > 0:
408
+ logger.info(f"Failed {count} stale pending agent runs (>{timeout_minutes}m)")
409
+ return count