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.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
gobby/agents/context.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context resolver for subagent context injection.
|
|
3
|
+
|
|
4
|
+
Resolves various context sources for injecting into subagent prompts:
|
|
5
|
+
- summary_markdown: Parent session's summary
|
|
6
|
+
- compact_markdown: Parent session's handoff context
|
|
7
|
+
- session_id:<id>: Lookup specific session summary
|
|
8
|
+
- transcript:<n>: Last N messages from parent session
|
|
9
|
+
- file:<path>: Read file content with security checks
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import re
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from gobby.storage.session_messages import LocalSessionMessageManager
|
|
21
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ContextResolutionError(Exception):
|
|
27
|
+
"""Raised when context resolution fails."""
|
|
28
|
+
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ContextResolver:
|
|
33
|
+
"""
|
|
34
|
+
Resolves context from various sources for subagent injection.
|
|
35
|
+
|
|
36
|
+
Supports the following source formats:
|
|
37
|
+
- "summary_markdown": Parent session's summary_markdown field
|
|
38
|
+
- "compact_markdown": Parent session's compact_markdown (handoff context)
|
|
39
|
+
- "session_id:<id>": Summary from a specific session by ID
|
|
40
|
+
- "transcript:<n>": Last N messages from parent session
|
|
41
|
+
- "file:<path>": Read file content (project-scoped with security checks)
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
>>> resolver = ContextResolver(session_manager, message_manager, project_path)
|
|
45
|
+
>>> context = await resolver.resolve("summary_markdown", "sess-abc123")
|
|
46
|
+
>>> context = await resolver.resolve("transcript:10", "sess-abc123")
|
|
47
|
+
>>> context = await resolver.resolve("file:docs/context.md", "sess-abc123")
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
# Pattern matchers for source formats
|
|
51
|
+
SESSION_ID_PATTERN = re.compile(r"^session_id:(.+)$")
|
|
52
|
+
TRANSCRIPT_PATTERN = re.compile(r"^transcript:(\d+)$")
|
|
53
|
+
FILE_PATTERN = re.compile(r"^file:(.+)$")
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
session_manager: LocalSessionManager,
|
|
58
|
+
message_manager: LocalSessionMessageManager,
|
|
59
|
+
project_path: str | Path | None = None,
|
|
60
|
+
max_file_size: int = 51200, # 50KB default
|
|
61
|
+
max_content_size: int = 51200, # 50KB default for all content types
|
|
62
|
+
max_transcript_messages: int = 100,
|
|
63
|
+
truncation_suffix: str = "\n\n[truncated: {bytes} bytes remaining]",
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
Initialize the context resolver.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
session_manager: Session storage manager for session lookups.
|
|
70
|
+
message_manager: Message storage manager for transcript lookups.
|
|
71
|
+
project_path: Project root path for file security checks.
|
|
72
|
+
max_file_size: Maximum file size in bytes (default: 50KB).
|
|
73
|
+
max_content_size: Maximum content size for all sources (default: 50KB).
|
|
74
|
+
max_transcript_messages: Maximum transcript messages to fetch.
|
|
75
|
+
truncation_suffix: Suffix template when content is truncated.
|
|
76
|
+
"""
|
|
77
|
+
self._session_manager = session_manager
|
|
78
|
+
self._message_manager = message_manager
|
|
79
|
+
self._project_path = Path(project_path) if project_path else None
|
|
80
|
+
self._truncation_suffix = truncation_suffix
|
|
81
|
+
self._max_file_size = max_file_size
|
|
82
|
+
self._max_content_size = max_content_size
|
|
83
|
+
self._max_transcript_messages = max_transcript_messages
|
|
84
|
+
|
|
85
|
+
async def resolve(self, source: str, session_id: str) -> str:
|
|
86
|
+
"""
|
|
87
|
+
Resolve context from the specified source.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
source: Context source specification.
|
|
91
|
+
session_id: Parent session ID for context lookups.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Resolved context string (uncompressed), truncated if exceeding max_content_size.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ContextResolutionError: If resolution fails.
|
|
98
|
+
"""
|
|
99
|
+
content: str = ""
|
|
100
|
+
|
|
101
|
+
# Handle simple source types
|
|
102
|
+
if source == "summary_markdown":
|
|
103
|
+
content = self._resolve_summary_markdown(session_id)
|
|
104
|
+
|
|
105
|
+
elif source == "compact_markdown":
|
|
106
|
+
content = self._resolve_compact_markdown(session_id)
|
|
107
|
+
|
|
108
|
+
# Handle parameterized source types
|
|
109
|
+
elif match := self.SESSION_ID_PATTERN.match(source):
|
|
110
|
+
target_session_id = match.group(1)
|
|
111
|
+
content = self._resolve_session_id(target_session_id)
|
|
112
|
+
|
|
113
|
+
elif match := self.TRANSCRIPT_PATTERN.match(source):
|
|
114
|
+
count = int(match.group(1))
|
|
115
|
+
content = await self._resolve_transcript(session_id, count)
|
|
116
|
+
|
|
117
|
+
elif match := self.FILE_PATTERN.match(source):
|
|
118
|
+
file_path = match.group(1)
|
|
119
|
+
# File resolution has its own truncation logic
|
|
120
|
+
return self._resolve_file(file_path)
|
|
121
|
+
|
|
122
|
+
else:
|
|
123
|
+
# Unknown source format
|
|
124
|
+
raise ContextResolutionError(f"Unknown context source format: {source}")
|
|
125
|
+
|
|
126
|
+
# Apply truncation to all non-file sources
|
|
127
|
+
return self._truncate_content(content, self._max_content_size)
|
|
128
|
+
|
|
129
|
+
def _resolve_summary_markdown(self, session_id: str) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Resolve summary_markdown from parent session.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
session_id: Parent session ID.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Summary markdown content, or empty string if not available.
|
|
138
|
+
"""
|
|
139
|
+
session = self._session_manager.get(session_id)
|
|
140
|
+
if not session:
|
|
141
|
+
raise ContextResolutionError(f"Session not found: {session_id}")
|
|
142
|
+
|
|
143
|
+
return session.summary_markdown or ""
|
|
144
|
+
|
|
145
|
+
def _resolve_compact_markdown(self, session_id: str) -> str:
|
|
146
|
+
"""
|
|
147
|
+
Resolve compact_markdown (handoff context) from parent session.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
session_id: Parent session ID.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Compact markdown content, or empty string if not available.
|
|
154
|
+
"""
|
|
155
|
+
session = self._session_manager.get(session_id)
|
|
156
|
+
if not session:
|
|
157
|
+
raise ContextResolutionError(f"Session not found: {session_id}")
|
|
158
|
+
|
|
159
|
+
return session.compact_markdown or ""
|
|
160
|
+
|
|
161
|
+
def _resolve_session_id(self, target_session_id: str) -> str:
|
|
162
|
+
"""
|
|
163
|
+
Resolve summary from a specific session by ID.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
target_session_id: Target session ID to lookup.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Summary markdown from the target session.
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
ContextResolutionError: If session not found.
|
|
173
|
+
"""
|
|
174
|
+
session = self._session_manager.get(target_session_id)
|
|
175
|
+
if not session:
|
|
176
|
+
raise ContextResolutionError(f"Session not found: {target_session_id}")
|
|
177
|
+
|
|
178
|
+
return session.summary_markdown or ""
|
|
179
|
+
|
|
180
|
+
async def _resolve_transcript(self, session_id: str, count: int) -> str:
|
|
181
|
+
"""
|
|
182
|
+
Resolve last N messages from parent session as transcript.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
session_id: Parent session ID.
|
|
186
|
+
count: Number of recent messages to include.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Formatted transcript of recent messages, or empty string if none.
|
|
190
|
+
"""
|
|
191
|
+
# Clamp count to max
|
|
192
|
+
count = min(count, self._max_transcript_messages)
|
|
193
|
+
|
|
194
|
+
messages = await self._message_manager.get_messages(
|
|
195
|
+
session_id=session_id,
|
|
196
|
+
limit=count,
|
|
197
|
+
offset=0,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if not messages:
|
|
201
|
+
return ""
|
|
202
|
+
|
|
203
|
+
# Format messages into transcript
|
|
204
|
+
lines = []
|
|
205
|
+
for msg in messages:
|
|
206
|
+
role = msg.get("role", "unknown")
|
|
207
|
+
content = msg.get("content", "")
|
|
208
|
+
if content:
|
|
209
|
+
lines.append(f"**{role}**: {content}")
|
|
210
|
+
|
|
211
|
+
return "\n\n".join(lines)
|
|
212
|
+
|
|
213
|
+
def _resolve_file(self, file_path: str) -> str:
|
|
214
|
+
"""
|
|
215
|
+
Resolve content from a file with security checks.
|
|
216
|
+
|
|
217
|
+
Security checks:
|
|
218
|
+
- Path must be within project directory
|
|
219
|
+
- No path traversal (..)
|
|
220
|
+
- No absolute paths
|
|
221
|
+
- Symlinks must resolve within project
|
|
222
|
+
- File must be valid UTF-8 (no binary)
|
|
223
|
+
- File size must be within limit
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
file_path: Relative path to file within project.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
File content, possibly truncated.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
ContextResolutionError: If file not found, not readable, or fails security checks.
|
|
233
|
+
"""
|
|
234
|
+
if not self._project_path:
|
|
235
|
+
raise ContextResolutionError("No project path configured for file resolution")
|
|
236
|
+
|
|
237
|
+
# Parse the path and check for security issues
|
|
238
|
+
parsed_path = Path(file_path)
|
|
239
|
+
|
|
240
|
+
# Reject absolute paths
|
|
241
|
+
if parsed_path.is_absolute():
|
|
242
|
+
raise ContextResolutionError(f"Absolute paths not allowed: {file_path}")
|
|
243
|
+
|
|
244
|
+
# Reject path traversal attempts by checking path components
|
|
245
|
+
if ".." in parsed_path.parts:
|
|
246
|
+
raise ContextResolutionError(f"Path traversal not allowed: {file_path}")
|
|
247
|
+
|
|
248
|
+
# Resolve the full path
|
|
249
|
+
try:
|
|
250
|
+
full_path = (self._project_path / file_path).resolve()
|
|
251
|
+
except Exception as e:
|
|
252
|
+
raise ContextResolutionError(f"Invalid file path: {file_path}") from e
|
|
253
|
+
|
|
254
|
+
# Check path is within project
|
|
255
|
+
try:
|
|
256
|
+
full_path.relative_to(self._project_path.resolve())
|
|
257
|
+
except ValueError:
|
|
258
|
+
raise ContextResolutionError(
|
|
259
|
+
f"File path outside project directory: {file_path}"
|
|
260
|
+
) from None
|
|
261
|
+
|
|
262
|
+
# Check file exists
|
|
263
|
+
if not full_path.exists():
|
|
264
|
+
raise ContextResolutionError(f"File not found: {file_path}")
|
|
265
|
+
|
|
266
|
+
if not full_path.is_file():
|
|
267
|
+
raise ContextResolutionError(f"Path is not a file: {file_path}")
|
|
268
|
+
|
|
269
|
+
# Check file size
|
|
270
|
+
file_size = full_path.stat().st_size
|
|
271
|
+
if file_size > self._max_file_size:
|
|
272
|
+
# Read up to limit and truncate
|
|
273
|
+
try:
|
|
274
|
+
with open(full_path, encoding="utf-8") as f:
|
|
275
|
+
content = f.read(self._max_file_size)
|
|
276
|
+
remaining = file_size - self._max_file_size
|
|
277
|
+
return content + self._truncation_suffix.format(bytes=remaining)
|
|
278
|
+
except UnicodeDecodeError:
|
|
279
|
+
raise ContextResolutionError(
|
|
280
|
+
f"File is not valid UTF-8 (binary): {file_path}"
|
|
281
|
+
) from None
|
|
282
|
+
|
|
283
|
+
# Read file content
|
|
284
|
+
try:
|
|
285
|
+
with open(full_path, encoding="utf-8") as f:
|
|
286
|
+
return f.read()
|
|
287
|
+
except UnicodeDecodeError:
|
|
288
|
+
raise ContextResolutionError(f"File is not valid UTF-8 (binary): {file_path}") from None
|
|
289
|
+
except PermissionError:
|
|
290
|
+
raise ContextResolutionError(f"Permission denied: {file_path}") from None
|
|
291
|
+
except Exception as e:
|
|
292
|
+
raise ContextResolutionError(f"Failed to read file {file_path}: {e}") from e
|
|
293
|
+
|
|
294
|
+
def _truncate_content(self, content: str, max_bytes: int) -> str:
|
|
295
|
+
"""
|
|
296
|
+
Truncate content to max bytes with suffix.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
content: Content to potentially truncate.
|
|
300
|
+
max_bytes: Maximum bytes allowed.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Content, possibly truncated with suffix.
|
|
304
|
+
"""
|
|
305
|
+
encoded = content.encode("utf-8")
|
|
306
|
+
if len(encoded) <= max_bytes:
|
|
307
|
+
return content
|
|
308
|
+
|
|
309
|
+
# Truncate and add suffix
|
|
310
|
+
truncated = encoded[:max_bytes].decode("utf-8", errors="ignore")
|
|
311
|
+
remaining = len(encoded) - max_bytes
|
|
312
|
+
return truncated + self._truncation_suffix.format(bytes=remaining)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# Default template for context injection
|
|
316
|
+
DEFAULT_CONTEXT_TEMPLATE = """## Context from Parent Session
|
|
317
|
+
*Injected by Gobby subagent spawning*
|
|
318
|
+
|
|
319
|
+
{{ context }}
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Task
|
|
324
|
+
|
|
325
|
+
{{ prompt }}"""
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def format_injected_prompt(context: str, prompt: str, template: str | None = None) -> str:
|
|
329
|
+
"""
|
|
330
|
+
Format the injected prompt with context prepended.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
context: Resolved context to inject.
|
|
334
|
+
prompt: Original prompt for the agent.
|
|
335
|
+
template: Optional custom template with {{ context }} and {{ prompt }} placeholders.
|
|
336
|
+
If None, uses the default template.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Formatted prompt with context, or original prompt if context is empty.
|
|
340
|
+
"""
|
|
341
|
+
if not context or not context.strip():
|
|
342
|
+
return prompt
|
|
343
|
+
|
|
344
|
+
# Use default template if none provided
|
|
345
|
+
effective_template = template or DEFAULT_CONTEXT_TEMPLATE
|
|
346
|
+
|
|
347
|
+
# Simple string substitution for {{ context }} and {{ prompt }}
|
|
348
|
+
result = effective_template
|
|
349
|
+
result = result.replace("{{ context }}", context)
|
|
350
|
+
result = result.replace("{{ prompt }}", prompt)
|
|
351
|
+
|
|
352
|
+
# Also support {context} and {prompt} for Python format-style
|
|
353
|
+
# but only if {{ }} placeholders are not in the template
|
|
354
|
+
if "{{ context }}" not in effective_template and "{{ prompt }}" not in effective_template:
|
|
355
|
+
try:
|
|
356
|
+
result = effective_template.format(context=context, prompt=prompt)
|
|
357
|
+
except (KeyError, IndexError):
|
|
358
|
+
# If format fails due to missing placeholders or positional braces like {0},
|
|
359
|
+
# return as-is
|
|
360
|
+
pass
|
|
361
|
+
|
|
362
|
+
return result
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Named Agent Definitions.
|
|
3
|
+
|
|
4
|
+
This module defines the schema and loading logic for named agents (Agents V2).
|
|
5
|
+
Named agents are reusable configurations that allow child agents to have distinct
|
|
6
|
+
lifecycle behavior, solving recursion loops in delegation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
from gobby.utils.project_context import get_project_context
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentDefinition(BaseModel):
|
|
22
|
+
"""
|
|
23
|
+
Configuration for a named agent.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
name: str
|
|
27
|
+
description: str | None = None
|
|
28
|
+
|
|
29
|
+
# Execution parameters
|
|
30
|
+
model: str | None = None
|
|
31
|
+
mode: str = "headless" # Default to headless for stability
|
|
32
|
+
|
|
33
|
+
# Workflow configuration
|
|
34
|
+
workflow: str | None = None
|
|
35
|
+
|
|
36
|
+
# Lifecycle variables to override parent's lifecycle settings
|
|
37
|
+
lifecycle_variables: dict[str, Any] = Field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
# Default variables passed to the agent
|
|
40
|
+
default_variables: dict[str, Any] = Field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
# Execution limits
|
|
43
|
+
timeout: float = 120.0
|
|
44
|
+
max_turns: int = 10
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AgentDefinitionLoader:
|
|
48
|
+
"""
|
|
49
|
+
Loads agent definitions from YAML files.
|
|
50
|
+
|
|
51
|
+
Search priority (later overrides earlier):
|
|
52
|
+
1. Built-in: src/gobby/install/shared/agents/
|
|
53
|
+
2. User-level: ~/.gobby/agents/
|
|
54
|
+
3. Project-level: .gobby/agents/
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
# Determine paths
|
|
59
|
+
# Built-in path relative to this file
|
|
60
|
+
# src/gobby/agents/definitions.py -> src/gobby/install/shared/agents/
|
|
61
|
+
base_dir = Path(__file__).parent.parent
|
|
62
|
+
self._shared_path = base_dir / "install" / "shared" / "agents"
|
|
63
|
+
|
|
64
|
+
# User path
|
|
65
|
+
self._user_path = Path.home() / ".gobby" / "agents"
|
|
66
|
+
|
|
67
|
+
# Project path (tried dynamically based on current context)
|
|
68
|
+
self._project_path: Path | None = None
|
|
69
|
+
|
|
70
|
+
def _get_project_path(self) -> Path | None:
|
|
71
|
+
"""Get current project path from context."""
|
|
72
|
+
ctx = get_project_context()
|
|
73
|
+
if ctx and ctx.get("project_path"):
|
|
74
|
+
return Path(ctx["project_path"]) / ".gobby" / "agents"
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def _find_agent_file(self, name: str) -> Path | None:
|
|
78
|
+
"""Find the agent definition file in search paths."""
|
|
79
|
+
filename = f"{name}.yaml"
|
|
80
|
+
|
|
81
|
+
# Check project first (highest priority for finding logic, but technically
|
|
82
|
+
# we want to load from lowest to highest if we were merging, but we just
|
|
83
|
+
# want the "winner" here. Since we don't merge partial definitions,
|
|
84
|
+
# finding the first one in priority order is sufficient.)
|
|
85
|
+
|
|
86
|
+
# 1. Project
|
|
87
|
+
project_agents = self._get_project_path()
|
|
88
|
+
if project_agents and project_agents.exists():
|
|
89
|
+
f = project_agents / filename
|
|
90
|
+
if f.exists():
|
|
91
|
+
return f
|
|
92
|
+
|
|
93
|
+
# 2. User
|
|
94
|
+
if self._user_path.exists():
|
|
95
|
+
f = self._user_path / filename
|
|
96
|
+
if f.exists():
|
|
97
|
+
return f
|
|
98
|
+
|
|
99
|
+
# 3. Built-in (Shared)
|
|
100
|
+
if self._shared_path.exists():
|
|
101
|
+
f = self._shared_path / filename
|
|
102
|
+
if f.exists():
|
|
103
|
+
return f
|
|
104
|
+
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
def load(self, name: str) -> AgentDefinition | None:
|
|
108
|
+
"""
|
|
109
|
+
Load an agent definition by name.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
name: Name of the agent (e.g. "validation-runner")
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
AgentDefinition if found, None otherwise.
|
|
116
|
+
"""
|
|
117
|
+
path = self._find_agent_file(name)
|
|
118
|
+
if not path:
|
|
119
|
+
logger.debug(f"Agent definition '{name}' not found")
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
with open(path, encoding="utf-8") as f:
|
|
124
|
+
data = yaml.safe_load(f)
|
|
125
|
+
|
|
126
|
+
# Ensure name matches filename/request if not specified
|
|
127
|
+
if "name" not in data:
|
|
128
|
+
data["name"] = name
|
|
129
|
+
|
|
130
|
+
return AgentDefinition(**data)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.error(f"Failed to load agent definition '{name}' from {path}: {e}")
|
|
133
|
+
return None
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Gemini session ID capture utility.
|
|
2
|
+
|
|
3
|
+
Captures Gemini's session_id via stream-json output before launching interactive mode.
|
|
4
|
+
This is necessary because Gemini CLI in interactive mode cannot introspect its own
|
|
5
|
+
session_id, but we need it for:
|
|
6
|
+
1. Linking to Gobby sessions (external_id)
|
|
7
|
+
2. Resume functionality with `gemini -r {session_id}`
|
|
8
|
+
3. MCP tool calls that require session context
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class GeminiSessionInfo:
|
|
21
|
+
"""Captured Gemini session information."""
|
|
22
|
+
|
|
23
|
+
session_id: str
|
|
24
|
+
model: str | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def capture_gemini_session_id(
|
|
28
|
+
timeout: float = 10.0,
|
|
29
|
+
) -> GeminiSessionInfo:
|
|
30
|
+
"""Capture Gemini's session_id via preflight stream-json call.
|
|
31
|
+
|
|
32
|
+
Launches Gemini with minimal prompt in stream-json mode,
|
|
33
|
+
filters through token error noise to find init JSON,
|
|
34
|
+
extracts session_id, then terminates.
|
|
35
|
+
|
|
36
|
+
Note: Gemini CLI outputs token errors to stdout (not stderr),
|
|
37
|
+
so we must filter line-by-line for valid JSON.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
timeout: Max seconds to wait for init JSON (default 10s to account
|
|
41
|
+
for auth wait time which can take ~4s)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
GeminiSessionInfo with captured session_id and model
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
asyncio.TimeoutError: If init JSON not received within timeout
|
|
48
|
+
ValueError: If session_id not found in output
|
|
49
|
+
FileNotFoundError: If gemini CLI is not installed
|
|
50
|
+
"""
|
|
51
|
+
logger.debug("Starting Gemini preflight to capture session_id")
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
proc = await asyncio.create_subprocess_exec(
|
|
55
|
+
"gemini",
|
|
56
|
+
".",
|
|
57
|
+
"-o",
|
|
58
|
+
"stream-json",
|
|
59
|
+
"--allowed-mcp-server-names",
|
|
60
|
+
"",
|
|
61
|
+
stdout=asyncio.subprocess.PIPE,
|
|
62
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
63
|
+
)
|
|
64
|
+
except FileNotFoundError:
|
|
65
|
+
raise FileNotFoundError(
|
|
66
|
+
"Gemini CLI not found. Install with: npm install -g @google/gemini-cli"
|
|
67
|
+
) from None
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
|
|
71
|
+
async def read_init() -> GeminiSessionInfo:
|
|
72
|
+
"""Read lines until we find the init JSON."""
|
|
73
|
+
if proc.stdout is None:
|
|
74
|
+
raise RuntimeError("Process stdout is not available")
|
|
75
|
+
async for line in proc.stdout:
|
|
76
|
+
text = line.decode().strip()
|
|
77
|
+
|
|
78
|
+
# Skip non-JSON lines (token error noise)
|
|
79
|
+
if not text.startswith("{"):
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
data = json.loads(text)
|
|
84
|
+
if data.get("type") == "init":
|
|
85
|
+
session_id = data.get("session_id")
|
|
86
|
+
if not session_id:
|
|
87
|
+
raise ValueError("Init JSON missing session_id field")
|
|
88
|
+
|
|
89
|
+
logger.debug(
|
|
90
|
+
f"Captured Gemini session_id: {session_id}, model: {data.get('model')}"
|
|
91
|
+
)
|
|
92
|
+
return GeminiSessionInfo(
|
|
93
|
+
session_id=session_id,
|
|
94
|
+
model=data.get("model"),
|
|
95
|
+
)
|
|
96
|
+
except json.JSONDecodeError:
|
|
97
|
+
# Not valid JSON, skip
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
raise ValueError("No init JSON found in Gemini output")
|
|
101
|
+
|
|
102
|
+
return await asyncio.wait_for(read_init(), timeout=timeout)
|
|
103
|
+
|
|
104
|
+
finally:
|
|
105
|
+
# Terminate the preflight process
|
|
106
|
+
proc.terminate()
|
|
107
|
+
try:
|
|
108
|
+
await asyncio.wait_for(proc.wait(), timeout=5.0)
|
|
109
|
+
except TimeoutError:
|
|
110
|
+
proc.kill()
|
|
111
|
+
await proc.wait()
|