stravinsky 0.2.52__py3-none-any.whl → 0.4.18__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of stravinsky might be problematic. Click here for more details.

Files changed (58) hide show
  1. mcp_bridge/__init__.py +1 -1
  2. mcp_bridge/auth/token_store.py +113 -11
  3. mcp_bridge/cli/__init__.py +6 -0
  4. mcp_bridge/cli/install_hooks.py +1265 -0
  5. mcp_bridge/cli/session_report.py +585 -0
  6. mcp_bridge/config/MANIFEST_SCHEMA.md +305 -0
  7. mcp_bridge/config/README.md +276 -0
  8. mcp_bridge/config/hook_config.py +249 -0
  9. mcp_bridge/config/hooks_manifest.json +138 -0
  10. mcp_bridge/config/rate_limits.py +222 -0
  11. mcp_bridge/config/skills_manifest.json +128 -0
  12. mcp_bridge/hooks/HOOKS_SETTINGS.json +175 -0
  13. mcp_bridge/hooks/README.md +215 -0
  14. mcp_bridge/hooks/__init__.py +119 -60
  15. mcp_bridge/hooks/edit_recovery.py +42 -37
  16. mcp_bridge/hooks/git_noninteractive.py +89 -0
  17. mcp_bridge/hooks/keyword_detector.py +30 -0
  18. mcp_bridge/hooks/manager.py +8 -0
  19. mcp_bridge/hooks/notification_hook.py +103 -0
  20. mcp_bridge/hooks/parallel_execution.py +111 -0
  21. mcp_bridge/hooks/pre_compact.py +82 -183
  22. mcp_bridge/hooks/rules_injector.py +507 -0
  23. mcp_bridge/hooks/session_notifier.py +125 -0
  24. mcp_bridge/{native_hooks → hooks}/stravinsky_mode.py +51 -16
  25. mcp_bridge/hooks/subagent_stop.py +98 -0
  26. mcp_bridge/hooks/task_validator.py +73 -0
  27. mcp_bridge/hooks/tmux_manager.py +141 -0
  28. mcp_bridge/hooks/todo_continuation.py +90 -0
  29. mcp_bridge/hooks/todo_delegation.py +88 -0
  30. mcp_bridge/hooks/tool_messaging.py +267 -0
  31. mcp_bridge/hooks/truncator.py +21 -17
  32. mcp_bridge/notifications.py +151 -0
  33. mcp_bridge/prompts/multimodal.py +24 -3
  34. mcp_bridge/server.py +214 -49
  35. mcp_bridge/server_tools.py +445 -0
  36. mcp_bridge/tools/__init__.py +22 -18
  37. mcp_bridge/tools/agent_manager.py +220 -32
  38. mcp_bridge/tools/code_search.py +97 -11
  39. mcp_bridge/tools/lsp/__init__.py +7 -0
  40. mcp_bridge/tools/lsp/manager.py +448 -0
  41. mcp_bridge/tools/lsp/tools.py +637 -150
  42. mcp_bridge/tools/model_invoke.py +208 -106
  43. mcp_bridge/tools/query_classifier.py +323 -0
  44. mcp_bridge/tools/semantic_search.py +3042 -0
  45. mcp_bridge/tools/templates.py +32 -18
  46. mcp_bridge/update_manager.py +589 -0
  47. mcp_bridge/update_manager_pypi.py +299 -0
  48. stravinsky-0.4.18.dist-info/METADATA +468 -0
  49. stravinsky-0.4.18.dist-info/RECORD +88 -0
  50. stravinsky-0.4.18.dist-info/entry_points.txt +5 -0
  51. mcp_bridge/native_hooks/edit_recovery.py +0 -46
  52. mcp_bridge/native_hooks/todo_delegation.py +0 -54
  53. mcp_bridge/native_hooks/truncator.py +0 -23
  54. stravinsky-0.2.52.dist-info/METADATA +0 -204
  55. stravinsky-0.2.52.dist-info/RECORD +0 -63
  56. stravinsky-0.2.52.dist-info/entry_points.txt +0 -3
  57. /mcp_bridge/{native_hooks → hooks}/context.py +0 -0
  58. {stravinsky-0.2.52.dist-info → stravinsky-0.4.18.dist-info}/WHEEL +0 -0
@@ -5,6 +5,7 @@ These tools use OAuth tokens from the token store to authenticate
5
5
  API requests to external model providers.
6
6
  """
7
7
 
8
+ import asyncio
8
9
  import logging
9
10
  import os
10
11
  import time
@@ -134,6 +135,9 @@ _SESSION_CACHE: dict[str, str] = {}
134
135
  # Pooled HTTP client for connection reuse
135
136
  _HTTP_CLIENT: httpx.AsyncClient | None = None
136
137
 
138
+ # Rate limiting: Max 5 concurrent Gemini requests to prevent burst rate limits
139
+ _GEMINI_SEMAPHORE: asyncio.Semaphore | None = None
140
+
137
141
 
138
142
  def _get_session_id(conversation_key: str | None = None) -> str:
139
143
  """
@@ -174,6 +178,19 @@ async def _get_http_client() -> httpx.AsyncClient:
174
178
  return _HTTP_CLIENT
175
179
 
176
180
 
181
+ def _get_gemini_semaphore() -> asyncio.Semaphore:
182
+ """
183
+ Get or create semaphore for Gemini API rate limiting.
184
+
185
+ Limits concurrent Gemini requests to prevent burst rate limits (429 errors).
186
+ Max 5 concurrent requests balances throughput with API quota constraints.
187
+ """
188
+ global _GEMINI_SEMAPHORE
189
+ if _GEMINI_SEMAPHORE is None:
190
+ _GEMINI_SEMAPHORE = asyncio.Semaphore(5)
191
+ return _GEMINI_SEMAPHORE
192
+
193
+
177
194
  def _extract_gemini_response(data: dict) -> str:
178
195
  """
179
196
  Extract text from Gemini response, handling thinking blocks.
@@ -284,18 +301,25 @@ async def _ensure_valid_token(token_store: TokenStore, provider: str) -> str:
284
301
 
285
302
 
286
303
  def is_retryable_exception(e: Exception) -> bool:
287
- """Check if an exception is retryable (429 or 5xx)."""
304
+ """
305
+ Check if an exception is retryable (5xx only, NOT 429).
306
+
307
+ 429 (Rate Limit) errors should fail fast - retrying makes the problem worse
308
+ by adding more requests to an already exhausted quota. The semaphore prevents
309
+ these in the first place, but if one slips through, we shouldn't retry.
310
+ """
288
311
  if isinstance(e, httpx.HTTPStatusError):
289
- return e.response.status_code == 429 or 500 <= e.response.status_code < 600
312
+ # Only retry server errors (5xx), not rate limits (429)
313
+ return 500 <= e.response.status_code < 600
290
314
  return False
291
315
 
292
316
 
293
317
  @retry(
294
- stop=stop_after_attempt(5),
295
- wait=wait_exponential(multiplier=1, min=4, max=60),
318
+ stop=stop_after_attempt(2), # Reduced from 5 to 2 attempts
319
+ wait=wait_exponential(multiplier=2, min=10, max=120), # Longer waits: 10s → 20s → 40s
296
320
  retry=retry_if_exception(is_retryable_exception),
297
321
  before_sleep=lambda retry_state: logger.info(
298
- f"Rate limited or server error, retrying in {retry_state.next_action.sleep} seconds..."
322
+ f"Server error, retrying in {retry_state.next_action.sleep} seconds..."
299
323
  ),
300
324
  )
301
325
  async def invoke_gemini(
@@ -305,11 +329,13 @@ async def invoke_gemini(
305
329
  temperature: float = 0.7,
306
330
  max_tokens: int = 4096,
307
331
  thinking_budget: int = 0,
332
+ image_path: str | None = None,
308
333
  ) -> str:
309
334
  """
310
335
  Invoke a Gemini model with the given prompt.
311
336
 
312
337
  Uses OAuth authentication with Antigravity credentials.
338
+ Supports vision API for image/PDF analysis when image_path is provided.
313
339
 
314
340
  Args:
315
341
  token_store: Token store for OAuth credentials
@@ -317,6 +343,8 @@ async def invoke_gemini(
317
343
  model: Gemini model to use
318
344
  temperature: Sampling temperature (0.0-2.0)
319
345
  max_tokens: Maximum tokens in response
346
+ thinking_budget: Tokens reserved for internal reasoning
347
+ image_path: Optional path to image/PDF for vision analysis (token optimization)
320
348
 
321
349
  Returns:
322
350
  The model's response text.
@@ -349,132 +377,198 @@ async def invoke_gemini(
349
377
  # Extract agent context for logging (may be passed via params or original call)
350
378
  agent_context = params.get("agent_context", {})
351
379
  agent_type = agent_context.get("agent_type", "direct")
380
+ task_id = agent_context.get("task_id", "")
381
+ description = agent_context.get("description", "")
352
382
  prompt_summary = _summarize_prompt(prompt)
353
383
 
354
384
  # Log with agent context and prompt summary
355
385
  logger.info(f"[{agent_type}] → {model}: {prompt_summary}")
356
386
 
357
- access_token = await _ensure_valid_token(token_store, "gemini")
358
-
359
- # Resolve user-friendly model name to actual API model ID
360
- api_model = resolve_gemini_model(model)
387
+ # USER-VISIBLE NOTIFICATION (stderr) - Shows when Gemini is invoked
388
+ import sys
389
+ task_info = f" task={task_id}" if task_id else ""
390
+ desc_info = f" | {description}" if description else ""
391
+ print(f"🔮 GEMINI: {model} | agent={agent_type}{task_info}{desc_info}", file=sys.stderr)
361
392
 
362
- # Use persistent session ID for thinking signature caching
363
- session_id = _get_session_id()
364
- project_id = os.getenv("STRAVINSKY_ANTIGRAVITY_PROJECT_ID", ANTIGRAVITY_DEFAULT_PROJECT_ID)
393
+ # Acquire semaphore to limit concurrent Gemini requests (prevents 429 rate limits)
394
+ semaphore = _get_gemini_semaphore()
395
+ async with semaphore:
396
+ access_token = await _ensure_valid_token(token_store, "gemini")
365
397
 
366
- headers = {
367
- "Authorization": f"Bearer {access_token}",
368
- "Content-Type": "application/json",
369
- **ANTIGRAVITY_HEADERS, # Include Antigravity headers
370
- }
398
+ # Resolve user-friendly model name to actual API model ID
399
+ api_model = resolve_gemini_model(model)
371
400
 
372
- # Build inner request payload
373
- # Per API spec: contents must include role ("user" or "model")
374
- inner_payload = {
375
- "contents": [{"role": "user", "parts": [{"text": prompt}]}],
376
- "generationConfig": {
377
- "temperature": temperature,
378
- "maxOutputTokens": max_tokens,
379
- },
380
- "sessionId": session_id,
381
- }
401
+ # Use persistent session ID for thinking signature caching
402
+ session_id = _get_session_id()
403
+ project_id = os.getenv("STRAVINSKY_ANTIGRAVITY_PROJECT_ID", ANTIGRAVITY_DEFAULT_PROJECT_ID)
382
404
 
383
- # Add thinking budget if supported by model/API
384
- if thinking_budget > 0:
385
- # For Gemini 2.0+ Thinking models
386
- # Per Antigravity API: use "thinkingBudget", NOT "tokenLimit"
387
- inner_payload["generationConfig"]["thinkingConfig"] = {
388
- "includeThoughts": True,
389
- "thinkingBudget": thinking_budget,
405
+ headers = {
406
+ "Authorization": f"Bearer {access_token}",
407
+ "Content-Type": "application/json",
408
+ **ANTIGRAVITY_HEADERS, # Include Antigravity headers
390
409
  }
391
410
 
392
- # Wrap request body per reference implementation
393
- try:
394
- import uuid as uuid_module # Local import workaround for MCP context issue
411
+ # Build inner request payload
412
+ # Per API spec: contents must include role ("user" or "model")
413
+
414
+ # Build parts list - text prompt plus optional image
415
+ parts = [{"text": prompt}]
416
+
417
+ # Add image data for vision analysis (token optimization for multimodal)
418
+ if image_path:
419
+ import base64
420
+ from pathlib import Path
421
+
422
+ image_file = Path(image_path)
423
+ if image_file.exists():
424
+ # Determine MIME type
425
+ suffix = image_file.suffix.lower()
426
+ mime_types = {
427
+ ".png": "image/png",
428
+ ".jpg": "image/jpeg",
429
+ ".jpeg": "image/jpeg",
430
+ ".gif": "image/gif",
431
+ ".webp": "image/webp",
432
+ ".pdf": "application/pdf",
433
+ }
434
+ mime_type = mime_types.get(suffix, "image/png")
395
435
 
396
- request_id = f"invoke-{uuid_module.uuid4()}"
397
- except Exception as e:
398
- logger.error(f"UUID IMPORT FAILED: {e}")
399
- raise RuntimeError(f"CUSTOM ERROR: UUID import failed: {e}")
400
-
401
- wrapped_payload = {
402
- "project": project_id,
403
- "model": api_model,
404
- "userAgent": "antigravity",
405
- "requestId": request_id,
406
- "request": inner_payload,
407
- }
436
+ # Read and base64 encode
437
+ image_data = base64.b64encode(image_file.read_bytes()).decode("utf-8")
408
438
 
409
- # Get pooled HTTP client for connection reuse
410
- client = await _get_http_client()
439
+ # Add inline image data for Gemini Vision API
440
+ parts.append({
441
+ "inlineData": {
442
+ "mimeType": mime_type,
443
+ "data": image_data,
444
+ }
445
+ })
446
+ logger.info(f"[multimodal] Added vision data: {image_path} ({mime_type})")
411
447
 
412
- # Try endpoints in fallback order with thinking recovery
413
- response = None
414
- last_error = None
415
- max_retries = 2 # For thinking recovery
448
+ inner_payload = {
449
+ "contents": [{"role": "user", "parts": parts}],
450
+ "generationConfig": {
451
+ "temperature": temperature,
452
+ "maxOutputTokens": max_tokens,
453
+ },
454
+ "sessionId": session_id,
455
+ }
416
456
 
417
- for retry_attempt in range(max_retries):
418
- for endpoint in ANTIGRAVITY_ENDPOINTS:
419
- # Reference uses: {endpoint}/v1internal:generateContent (NOT /models/{model})
420
- api_url = f"{endpoint}/v1internal:generateContent"
457
+ # Add thinking budget if supported by model/API
458
+ if thinking_budget > 0:
459
+ # For Gemini 2.0+ Thinking models
460
+ # Per Antigravity API: use "thinkingBudget", NOT "tokenLimit"
461
+ inner_payload["generationConfig"]["thinkingConfig"] = {
462
+ "includeThoughts": True,
463
+ "thinkingBudget": thinking_budget,
464
+ }
421
465
 
422
- try:
423
- response = await client.post(
424
- api_url,
425
- headers=headers,
426
- json=wrapped_payload,
427
- timeout=120.0,
428
- )
466
+ # Wrap request body per reference implementation
467
+ try:
468
+ import uuid as uuid_module # Local import workaround for MCP context issue
429
469
 
430
- # 401/403 might be endpoint-specific, try next endpoint
431
- if response.status_code in (401, 403):
432
- logger.warning(
433
- f"[Gemini] Endpoint {endpoint} returned {response.status_code}, trying next"
470
+ request_id = f"invoke-{uuid_module.uuid4()}"
471
+ except Exception as e:
472
+ logger.error(f"UUID IMPORT FAILED: {e}")
473
+ raise RuntimeError(f"CUSTOM ERROR: UUID import failed: {e}")
474
+
475
+ wrapped_payload = {
476
+ "project": project_id,
477
+ "model": api_model,
478
+ "userAgent": "antigravity",
479
+ "requestId": request_id,
480
+ "request": inner_payload,
481
+ }
482
+
483
+ # Get pooled HTTP client for connection reuse
484
+ client = await _get_http_client()
485
+
486
+ # Try endpoints in fallback order with thinking recovery
487
+ response = None
488
+ last_error = None
489
+ max_retries = 2 # For thinking recovery
490
+
491
+ for retry_attempt in range(max_retries):
492
+ for endpoint in ANTIGRAVITY_ENDPOINTS:
493
+ # Reference uses: {endpoint}/v1internal:generateContent (NOT /models/{model})
494
+ api_url = f"{endpoint}/v1internal:generateContent"
495
+
496
+ try:
497
+ response = await client.post(
498
+ api_url,
499
+ headers=headers,
500
+ json=wrapped_payload,
501
+ timeout=120.0,
434
502
  )
435
- last_error = Exception(f"{response.status_code} from {endpoint}")
436
- continue
437
503
 
438
- # Check for thinking-related errors that need recovery
439
- if response.status_code in (400, 500):
440
- error_text = response.text.lower()
441
- if "thinking" in error_text or "signature" in error_text:
504
+ # 401/403 might be endpoint-specific, try next endpoint
505
+ if response.status_code in (401, 403):
442
506
  logger.warning(
443
- f"[Gemini] Thinking error detected, clearing session cache and retrying"
507
+ f"[Gemini] Endpoint {endpoint} returned {response.status_code}, trying next"
444
508
  )
445
- clear_session_cache()
446
- # Update session ID for retry
447
- wrapped_payload["request"]["sessionId"] = _get_session_id()
448
- last_error = Exception(f"Thinking error: {response.text[:200]}")
449
- break # Break inner loop to retry with new session
450
-
451
- # If we got a non-retryable response (success or 4xx client error), use it
452
- if response.status_code < 500 and response.status_code != 429:
453
- break
509
+ last_error = Exception(f"{response.status_code} from {endpoint}")
510
+ continue
511
+
512
+ # Check for thinking-related errors that need recovery
513
+ if response.status_code in (400, 500):
514
+ error_text = response.text.lower()
515
+ if "thinking" in error_text or "signature" in error_text:
516
+ logger.warning(
517
+ f"[Gemini] Thinking error detected, clearing session cache and retrying"
518
+ )
519
+ clear_session_cache()
520
+ # Update session ID for retry
521
+ wrapped_payload["request"]["sessionId"] = _get_session_id()
522
+ last_error = Exception(f"Thinking error: {response.text[:200]}")
523
+ break # Break inner loop to retry with new session
524
+
525
+ # If we got a non-retryable response (success or 4xx client error), use it
526
+ if response.status_code < 500 and response.status_code != 429:
527
+ break
528
+
529
+ except httpx.TimeoutException as e:
530
+ last_error = e
531
+ continue
532
+ except Exception as e:
533
+ last_error = e
534
+ continue
535
+ else:
536
+ # Inner loop completed without break - no thinking recovery needed
537
+ break
454
538
 
455
- except httpx.TimeoutException as e:
456
- last_error = e
539
+ # If we broke out of inner loop for thinking recovery, continue outer retry loop
540
+ if response and response.status_code in (400, 500):
457
541
  continue
458
- except Exception as e:
459
- last_error = e
460
- continue
461
- else:
462
- # Inner loop completed without break - no thinking recovery needed
463
542
  break
464
543
 
465
- # If we broke out of inner loop for thinking recovery, continue outer retry loop
466
- if response and response.status_code in (400, 500):
467
- continue
468
- break
544
+ if response is None:
545
+ # FALLBACK: Try Claude sonnet-4.5 for agents that support it
546
+ agent_context = params.get("agent_context", {})
547
+ agent_type = agent_context.get("agent_type", "unknown")
548
+
549
+ if agent_type in ("dewey", "explore", "document_writer", "multimodal"):
550
+ logger.warning(f"[{agent_type}] Gemini failed, falling back to Claude sonnet-4.5")
551
+ try:
552
+ import subprocess
553
+ fallback_result = subprocess.run(
554
+ ["claude", "-p", prompt, "--model", "sonnet", "--output-format", "text"],
555
+ capture_output=True,
556
+ text=True,
557
+ timeout=120,
558
+ cwd=os.getcwd(),
559
+ )
560
+ if fallback_result.returncode == 0 and fallback_result.stdout.strip():
561
+ return fallback_result.stdout.strip()
562
+ except Exception as fallback_error:
563
+ logger.error(f"Fallback to Claude also failed: {fallback_error}")
469
564
 
470
- if response is None:
471
- raise ValueError(f"All Antigravity endpoints failed: {last_error}")
565
+ raise ValueError(f"All Antigravity endpoints failed: {last_error}")
472
566
 
473
- response.raise_for_status()
474
- data = response.json()
567
+ response.raise_for_status()
568
+ data = response.json()
475
569
 
476
- # Extract text from response using thinking-aware parser
477
- return _extract_gemini_response(data)
570
+ # Extract text from response using thinking-aware parser
571
+ return _extract_gemini_response(data)
478
572
 
479
573
 
480
574
  # ========================
@@ -761,11 +855,11 @@ async def invoke_gemini_agentic(
761
855
 
762
856
 
763
857
  @retry(
764
- stop=stop_after_attempt(5),
765
- wait=wait_exponential(multiplier=1, min=4, max=60),
858
+ stop=stop_after_attempt(2), # Reduced from 5 to 2 attempts
859
+ wait=wait_exponential(multiplier=2, min=10, max=120), # Longer waits: 10s → 20s → 40s
766
860
  retry=retry_if_exception(is_retryable_exception),
767
861
  before_sleep=lambda retry_state: logger.info(
768
- f"Rate limited or server error, retrying in {retry_state.next_action.sleep} seconds..."
862
+ f"Server error, retrying in {retry_state.next_action.sleep} seconds..."
769
863
  ),
770
864
  )
771
865
  async def invoke_openai(
@@ -816,11 +910,19 @@ async def invoke_openai(
816
910
  # Extract agent context for logging (may be passed via params or original call)
817
911
  agent_context = params.get("agent_context", {})
818
912
  agent_type = agent_context.get("agent_type", "direct")
913
+ task_id = agent_context.get("task_id", "")
914
+ description = agent_context.get("description", "")
819
915
  prompt_summary = _summarize_prompt(prompt)
820
916
 
821
917
  # Log with agent context and prompt summary
822
918
  logger.info(f"[{agent_type}] → {model}: {prompt_summary}")
823
919
 
920
+ # USER-VISIBLE NOTIFICATION (stderr) - Shows when OpenAI is invoked
921
+ import sys
922
+ task_info = f" task={task_id}" if task_id else ""
923
+ desc_info = f" | {description}" if description else ""
924
+ print(f"🧠 OPENAI: {model} | agent={agent_type}{task_info}{desc_info}", file=sys.stderr)
925
+
824
926
  access_token = await _ensure_valid_token(token_store, "openai")
825
927
  logger.info(f"[invoke_openai] Got access token")
826
928