v1.1.3
Co-Authored-By: Quentin Torroba <quentin.torroba@mistral.ai> Co-Authored-By: Michel Thomazo <michel.thomazo@mistral.ai> Co-Authored-By: Vincent Guilloux <vincent.guilloux@mistral.ai>
This commit is contained in:
parent
a340a721ea
commit
661588de0c
48 changed files with 1679 additions and 467 deletions
|
|
@ -57,7 +57,9 @@ from vibe.core import __version__
|
|||
from vibe.core.agent import Agent as VibeAgent
|
||||
from vibe.core.autocompletion.path_prompt_adapter import render_path_prompt
|
||||
from vibe.core.config import MissingAPIKeyError, VibeConfig, load_api_keys_from_env
|
||||
from vibe.core.tools.base import BaseToolConfig, ToolPermission
|
||||
from vibe.core.types import (
|
||||
ApprovalResponse,
|
||||
AssistantEvent,
|
||||
AsyncApprovalCallback,
|
||||
ToolCallEvent,
|
||||
|
|
@ -211,10 +213,32 @@ class VibeAcpAgent(AcpAgent):
|
|||
return disabled
|
||||
|
||||
def _create_approval_callback(self, session_id: str) -> AsyncApprovalCallback:
|
||||
session = self._get_session(session_id)
|
||||
|
||||
def _handle_permission_selection(
|
||||
option_id: str, tool_name: str
|
||||
) -> tuple[ApprovalResponse, str | None]:
|
||||
match option_id:
|
||||
case ToolOption.ALLOW_ONCE:
|
||||
return (ApprovalResponse.YES, None)
|
||||
case ToolOption.ALLOW_ALWAYS:
|
||||
if tool_name not in session.agent.config.tools:
|
||||
session.agent.config.tools[tool_name] = BaseToolConfig()
|
||||
session.agent.config.tools[
|
||||
tool_name
|
||||
].permission = ToolPermission.ALWAYS
|
||||
return (ApprovalResponse.YES, None)
|
||||
case ToolOption.REJECT_ONCE:
|
||||
return (
|
||||
ApprovalResponse.NO,
|
||||
"User rejected the tool call, provide an alternative plan",
|
||||
)
|
||||
case _:
|
||||
return (ApprovalResponse.NO, f"Unknown option: {option_id}")
|
||||
|
||||
async def approval_callback(
|
||||
tool_name: str, args: dict[str, Any], tool_call_id: str
|
||||
) -> tuple[str, str | None]:
|
||||
) -> tuple[ApprovalResponse, str | None]:
|
||||
# Create the tool call update
|
||||
tool_call = ToolCall(toolCallId=tool_call_id)
|
||||
|
||||
|
|
@ -228,10 +252,10 @@ class VibeAcpAgent(AcpAgent):
|
|||
# Parse the response using isinstance for proper type narrowing
|
||||
if response.outcome.outcome == "selected":
|
||||
outcome = cast(AllowedOutcome, response.outcome)
|
||||
return self._handle_permission_selection(outcome.optionId)
|
||||
return _handle_permission_selection(outcome.optionId, tool_name)
|
||||
else:
|
||||
return (
|
||||
"n",
|
||||
ApprovalResponse.NO,
|
||||
str(
|
||||
get_user_cancellation_message(
|
||||
CancellationReason.OPERATION_CANCELLED
|
||||
|
|
@ -241,18 +265,6 @@ class VibeAcpAgent(AcpAgent):
|
|||
|
||||
return approval_callback
|
||||
|
||||
@staticmethod
|
||||
def _handle_permission_selection(option_id: str) -> tuple[str, str | None]:
|
||||
match option_id:
|
||||
case ToolOption.ALLOW_ONCE:
|
||||
return ("y", None)
|
||||
case ToolOption.ALLOW_ALWAYS:
|
||||
return ("a", None)
|
||||
case ToolOption.REJECT_ONCE:
|
||||
return ("n", "User rejected the tool call, provide an alternative plan")
|
||||
case _:
|
||||
return ("n", f"Unknown option: {option_id}")
|
||||
|
||||
def _get_session(self, session_id: str) -> AcpSession:
|
||||
if session_id not in self.sessions:
|
||||
raise RequestError.invalid_params({"session": "Not found"})
|
||||
|
|
|
|||
|
|
@ -1,8 +1,31 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import os
|
||||
|
||||
import pyperclip
|
||||
from textual.app import App
|
||||
|
||||
_PREVIEW_MAX_LENGTH = 40
|
||||
|
||||
|
||||
def _copy_osc52(text: str) -> None:
|
||||
encoded = base64.b64encode(text.encode("utf-8")).decode("ascii")
|
||||
osc52_seq = f"\033]52;c;{encoded}\a"
|
||||
if os.environ.get("TMUX"):
|
||||
osc52_seq = f"\033Ptmux;\033{osc52_seq}\033\\"
|
||||
|
||||
with open("/dev/tty", "w") as tty:
|
||||
tty.write(osc52_seq)
|
||||
tty.flush()
|
||||
|
||||
|
||||
def _shorten_preview(texts: list[str]) -> str:
|
||||
dense_text = "⏎".join(texts).replace("\n", "⏎")
|
||||
if len(dense_text) > _PREVIEW_MAX_LENGTH:
|
||||
return f"{dense_text[: _PREVIEW_MAX_LENGTH - 1]}…"
|
||||
return dense_text
|
||||
|
||||
|
||||
def copy_selection_to_clipboard(app: App) -> None:
|
||||
selected_texts = []
|
||||
|
|
@ -30,10 +53,21 @@ def copy_selection_to_clipboard(app: App) -> None:
|
|||
|
||||
combined_text = "\n".join(selected_texts)
|
||||
|
||||
try:
|
||||
pyperclip.copy(combined_text)
|
||||
app.notify("Selection added to clipboard", severity="information", timeout=2)
|
||||
except Exception:
|
||||
for copy_fn in [_copy_osc52, pyperclip.copy, app.copy_to_clipboard]:
|
||||
try:
|
||||
copy_fn(combined_text)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
app.notify(
|
||||
f'"{_shorten_preview(selected_texts)}" copied to clipboard',
|
||||
severity="information",
|
||||
timeout=2,
|
||||
)
|
||||
break
|
||||
else:
|
||||
app.notify(
|
||||
"Use Ctrl+c to copy selections in Vibe", severity="warning", timeout=3
|
||||
"Failed to copy - no clipboard method available",
|
||||
severity="warning",
|
||||
timeout=3,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,14 +7,12 @@ from rich import print as rprint
|
|||
|
||||
from vibe.cli.textual_ui.app import run_textual_ui
|
||||
from vibe.core.config import (
|
||||
CONFIG_FILE,
|
||||
HISTORY_FILE,
|
||||
INSTRUCTIONS_FILE,
|
||||
MissingAPIKeyError,
|
||||
MissingPromptFileError,
|
||||
VibeConfig,
|
||||
load_api_keys_from_env,
|
||||
)
|
||||
from vibe.core.config_path import CONFIG_FILE, HISTORY_FILE, INSTRUCTIONS_FILE
|
||||
from vibe.core.interaction_logger import InteractionLogger
|
||||
from vibe.core.programmatic import run_programmatic
|
||||
from vibe.core.types import OutputFormat, ResumeSessionInfo
|
||||
|
|
@ -138,23 +136,23 @@ def main() -> None: # noqa: PLR0912, PLR0915
|
|||
run_onboarding()
|
||||
sys.exit(0)
|
||||
try:
|
||||
if not CONFIG_FILE.exists():
|
||||
if not CONFIG_FILE.path.exists():
|
||||
try:
|
||||
VibeConfig.save_updates(VibeConfig.create_default())
|
||||
except Exception as e:
|
||||
rprint(f"[yellow]Could not create default config file: {e}[/]")
|
||||
|
||||
if not INSTRUCTIONS_FILE.exists():
|
||||
if not INSTRUCTIONS_FILE.path.exists():
|
||||
try:
|
||||
INSTRUCTIONS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
INSTRUCTIONS_FILE.touch()
|
||||
INSTRUCTIONS_FILE.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
INSTRUCTIONS_FILE.path.touch()
|
||||
except Exception as e:
|
||||
rprint(f"[yellow]Could not create instructions file: {e}[/]")
|
||||
|
||||
if not HISTORY_FILE.exists():
|
||||
if not HISTORY_FILE.path.exists():
|
||||
try:
|
||||
HISTORY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
HISTORY_FILE.write_text("Hello Vibe!\n", "utf-8")
|
||||
HISTORY_FILE.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
HISTORY_FILE.path.write_text("Hello Vibe!\n", "utf-8")
|
||||
except Exception as e:
|
||||
rprint(f"[yellow]Could not create history file: {e}[/]")
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
from enum import StrEnum, auto
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Any, ClassVar, assert_never
|
||||
|
||||
|
|
@ -35,20 +34,22 @@ from vibe.cli.textual_ui.widgets.path_display import PathDisplay
|
|||
from vibe.cli.textual_ui.widgets.tools import ToolCallMessage, ToolResultMessage
|
||||
from vibe.cli.textual_ui.widgets.welcome import WelcomeBanner
|
||||
from vibe.cli.update_notifier import (
|
||||
GitHubVersionUpdateGateway,
|
||||
VersionUpdate,
|
||||
FileSystemUpdateCacheRepository,
|
||||
PyPIVersionUpdateGateway,
|
||||
UpdateCacheRepository,
|
||||
VersionUpdateAvailability,
|
||||
VersionUpdateError,
|
||||
is_version_update_available,
|
||||
VersionUpdateGateway,
|
||||
get_update_if_available,
|
||||
)
|
||||
from vibe.cli.update_notifier.version_update_gateway import VersionUpdateGateway
|
||||
from vibe.core import __version__ as CORE_VERSION
|
||||
from vibe.core.agent import Agent
|
||||
from vibe.core.autocompletion.path_prompt_adapter import render_path_prompt
|
||||
from vibe.core.config import HISTORY_FILE, VibeConfig
|
||||
from vibe.core.config import VibeConfig
|
||||
from vibe.core.config_path import HISTORY_FILE
|
||||
from vibe.core.tools.base import BaseToolConfig, ToolPermission
|
||||
from vibe.core.types import LLMMessage, ResumeSessionInfo, Role
|
||||
from vibe.core.types import ApprovalResponse, LLMMessage, ResumeSessionInfo, Role
|
||||
from vibe.core.utils import (
|
||||
ApprovalResponse,
|
||||
CancellationReason,
|
||||
get_user_cancellation_message,
|
||||
is_dangerous_directory,
|
||||
|
|
@ -72,6 +73,10 @@ class VibeApp(App):
|
|||
Binding("ctrl+o", "toggle_tool", "Toggle Tool", show=False),
|
||||
Binding("ctrl+t", "toggle_todo", "Toggle Todo", show=False),
|
||||
Binding("shift+tab", "cycle_mode", "Cycle Mode", show=False, priority=True),
|
||||
Binding("shift+up", "scroll_chat_up", "Scroll Up", show=False, priority=True),
|
||||
Binding(
|
||||
"shift+down", "scroll_chat_down", "Scroll Down", show=False, priority=True
|
||||
),
|
||||
]
|
||||
|
||||
def __init__(
|
||||
|
|
@ -83,6 +88,7 @@ class VibeApp(App):
|
|||
loaded_messages: list[LLMMessage] | None = None,
|
||||
session_info: ResumeSessionInfo | None = None,
|
||||
version_update_notifier: VersionUpdateGateway | None = None,
|
||||
update_cache_repository: UpdateCacheRepository | None = None,
|
||||
current_version: str = CORE_VERSION,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
|
|
@ -108,12 +114,13 @@ class VibeApp(App):
|
|||
self._current_bottom_app: BottomApp = BottomApp.Input
|
||||
self.theme = config.textual_theme
|
||||
|
||||
self.history_file = HISTORY_FILE
|
||||
self.history_file = HISTORY_FILE.path
|
||||
|
||||
self._tools_collapsed = True
|
||||
self._todos_collapsed = False
|
||||
self._current_streaming_message: AssistantMessage | None = None
|
||||
self._version_update_notifier = version_update_notifier
|
||||
self._update_cache_repository = update_cache_repository
|
||||
self._is_update_check_enabled = config.enable_update_checks
|
||||
self._current_version = current_version
|
||||
self._update_notification_task: asyncio.Task | None = None
|
||||
|
|
@ -126,6 +133,7 @@ class VibeApp(App):
|
|||
# prevent a race condition where the agent initialization
|
||||
# completes exactly at the moment the user interrupts
|
||||
self._agent_init_interrupted = False
|
||||
self._auto_scroll = True
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with VerticalScroll(id="chat"):
|
||||
|
|
@ -460,7 +468,7 @@ class VibeApp(App):
|
|||
|
||||
async def _approval_callback(
|
||||
self, tool: str, args: dict, tool_call_id: str
|
||||
) -> tuple[str, str | None]:
|
||||
) -> tuple[ApprovalResponse, str | None]:
|
||||
self._pending_approval = asyncio.Future()
|
||||
await self._switch_to_approval_app(tool, args)
|
||||
result = await self._pending_approval
|
||||
|
|
@ -955,6 +963,23 @@ class VibeApp(App):
|
|||
|
||||
self.exit()
|
||||
|
||||
def action_scroll_chat_up(self) -> None:
|
||||
try:
|
||||
chat = self.query_one("#chat", VerticalScroll)
|
||||
chat.scroll_relative(y=-5, animate=False)
|
||||
self._auto_scroll = False
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def action_scroll_chat_down(self) -> None:
|
||||
try:
|
||||
chat = self.query_one("#chat", VerticalScroll)
|
||||
chat.scroll_relative(y=5, animate=False)
|
||||
if self._is_scrolled_to_bottom(chat):
|
||||
self._auto_scroll = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def _show_dangerous_directory_warning(self) -> None:
|
||||
is_dangerous, reason = is_dangerous_directory()
|
||||
if is_dangerous:
|
||||
|
|
@ -975,6 +1000,9 @@ class VibeApp(App):
|
|||
chat = self.query_one("#chat", VerticalScroll)
|
||||
was_at_bottom = self._is_scrolled_to_bottom(chat)
|
||||
|
||||
if was_at_bottom:
|
||||
self._auto_scroll = True
|
||||
|
||||
if isinstance(widget, AssistantMessage):
|
||||
if self._current_streaming_message is not None:
|
||||
content = widget._content or ""
|
||||
|
|
@ -1014,6 +1042,8 @@ class VibeApp(App):
|
|||
self.call_after_refresh(self._scroll_to_bottom)
|
||||
|
||||
def _anchor_if_scrollable(self) -> None:
|
||||
if not self._auto_scroll:
|
||||
return
|
||||
try:
|
||||
chat = self.query_one("#chat", VerticalScroll)
|
||||
if chat.max_scroll_y == 0:
|
||||
|
|
@ -1036,11 +1066,16 @@ class VibeApp(App):
|
|||
|
||||
async def _check_version_update(self) -> None:
|
||||
try:
|
||||
if self._version_update_notifier is None:
|
||||
if (
|
||||
self._version_update_notifier is None
|
||||
or self._update_cache_repository is None
|
||||
):
|
||||
return
|
||||
|
||||
update = await is_version_update_available(
|
||||
self._version_update_notifier, current_version=self._current_version
|
||||
update = await get_update_if_available(
|
||||
version_update_notifier=self._version_update_notifier,
|
||||
current_version=self._current_version,
|
||||
update_cache_repository=self._update_cache_repository,
|
||||
)
|
||||
except VersionUpdateError as error:
|
||||
self.notify(
|
||||
|
|
@ -1056,12 +1091,12 @@ class VibeApp(App):
|
|||
finally:
|
||||
self._update_notification_task = None
|
||||
|
||||
if update is None:
|
||||
if update is None or not update.should_notify:
|
||||
return
|
||||
|
||||
self._display_update_notification(update)
|
||||
|
||||
def _display_update_notification(self, update: VersionUpdate) -> None:
|
||||
def _display_update_notification(self, update: VersionUpdateAvailability) -> None:
|
||||
if self._update_notification_shown:
|
||||
return
|
||||
|
||||
|
|
@ -1084,9 +1119,8 @@ def run_textual_ui(
|
|||
loaded_messages: list[LLMMessage] | None = None,
|
||||
session_info: ResumeSessionInfo | None = None,
|
||||
) -> None:
|
||||
update_notifier = GitHubVersionUpdateGateway(
|
||||
owner="mistralai", repository="mistral-vibe", token=os.getenv("GITHUB_TOKEN")
|
||||
)
|
||||
update_notifier = PyPIVersionUpdateGateway(project_name="mistral-vibe")
|
||||
update_cache_repository = FileSystemUpdateCacheRepository()
|
||||
app = VibeApp(
|
||||
config=config,
|
||||
auto_approve=auto_approve,
|
||||
|
|
@ -1095,5 +1129,6 @@ def run_textual_ui(
|
|||
loaded_messages=loaded_messages,
|
||||
session_info=session_info,
|
||||
version_update_notifier=update_notifier,
|
||||
update_cache_repository=update_cache_repository,
|
||||
)
|
||||
app.run()
|
||||
|
|
|
|||
|
|
@ -1,31 +1,43 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.cli.update_notifier.fake_version_update_gateway import (
|
||||
FakeVersionUpdateGateway,
|
||||
from vibe.cli.update_notifier.adapters.filesystem_update_cache_repository import (
|
||||
FileSystemUpdateCacheRepository,
|
||||
)
|
||||
from vibe.cli.update_notifier.github_version_update_gateway import (
|
||||
from vibe.cli.update_notifier.adapters.github_version_update_gateway import (
|
||||
GitHubVersionUpdateGateway,
|
||||
)
|
||||
from vibe.cli.update_notifier.version_update import (
|
||||
VersionUpdateError,
|
||||
is_version_update_available,
|
||||
from vibe.cli.update_notifier.adapters.pypi_version_update_gateway import (
|
||||
PyPIVersionUpdateGateway,
|
||||
)
|
||||
from vibe.cli.update_notifier.version_update_gateway import (
|
||||
from vibe.cli.update_notifier.ports.update_cache_repository import (
|
||||
UpdateCache,
|
||||
UpdateCacheRepository,
|
||||
)
|
||||
from vibe.cli.update_notifier.ports.version_update_gateway import (
|
||||
DEFAULT_GATEWAY_MESSAGES,
|
||||
VersionUpdate,
|
||||
VersionUpdateGateway,
|
||||
VersionUpdateGatewayCause,
|
||||
VersionUpdateGatewayError,
|
||||
)
|
||||
from vibe.cli.update_notifier.version_update import (
|
||||
VersionUpdateAvailability,
|
||||
VersionUpdateError,
|
||||
get_update_if_available,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_GATEWAY_MESSAGES",
|
||||
"FakeVersionUpdateGateway",
|
||||
"FileSystemUpdateCacheRepository",
|
||||
"GitHubVersionUpdateGateway",
|
||||
"PyPIVersionUpdateGateway",
|
||||
"UpdateCache",
|
||||
"UpdateCacheRepository",
|
||||
"VersionUpdate",
|
||||
"VersionUpdateAvailability",
|
||||
"VersionUpdateError",
|
||||
"VersionUpdateGateway",
|
||||
"VersionUpdateGatewayCause",
|
||||
"VersionUpdateGatewayError",
|
||||
"is_version_update_available",
|
||||
"get_update_if_available",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from vibe.cli.update_notifier.ports.update_cache_repository import (
|
||||
UpdateCache,
|
||||
UpdateCacheRepository,
|
||||
)
|
||||
from vibe.core.config_path import VIBE_HOME
|
||||
|
||||
|
||||
class FileSystemUpdateCacheRepository(UpdateCacheRepository):
|
||||
def __init__(self, base_path: Path | str | None = None) -> None:
|
||||
self._base_path = Path(base_path) if base_path is not None else VIBE_HOME.path
|
||||
self._cache_file = self._base_path / "update_cache.json"
|
||||
|
||||
async def get(self) -> UpdateCache | None:
|
||||
try:
|
||||
content = await asyncio.to_thread(self._cache_file.read_text)
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
try:
|
||||
data = json.loads(content)
|
||||
latest_version = data.get("latest_version")
|
||||
stored_at_timestamp = data.get("stored_at_timestamp")
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
return None
|
||||
|
||||
if not isinstance(latest_version, str) or not isinstance(
|
||||
stored_at_timestamp, int
|
||||
):
|
||||
return None
|
||||
|
||||
return UpdateCache(
|
||||
latest_version=latest_version, stored_at_timestamp=stored_at_timestamp
|
||||
)
|
||||
|
||||
async def set(self, update_cache: UpdateCache) -> None:
|
||||
try:
|
||||
payload = json.dumps({
|
||||
"latest_version": update_cache.latest_version,
|
||||
"stored_at_timestamp": update_cache.stored_at_timestamp,
|
||||
})
|
||||
await asyncio.to_thread(self._cache_file.write_text, payload)
|
||||
except OSError:
|
||||
return None
|
||||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import httpx
|
||||
|
||||
from vibe.cli.update_notifier.version_update_gateway import (
|
||||
from vibe.cli.update_notifier.ports.version_update_gateway import (
|
||||
VersionUpdate,
|
||||
VersionUpdateGateway,
|
||||
VersionUpdateGatewayCause,
|
||||
113
vibe/cli/update_notifier/adapters/pypi_version_update_gateway.py
Normal file
113
vibe/cli/update_notifier/adapters/pypi_version_update_gateway.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
from packaging.utils import parse_sdist_filename, parse_wheel_filename
|
||||
from packaging.version import InvalidVersion, Version
|
||||
|
||||
from vibe.cli.update_notifier.ports.version_update_gateway import (
|
||||
VersionUpdate,
|
||||
VersionUpdateGateway,
|
||||
VersionUpdateGatewayCause,
|
||||
VersionUpdateGatewayError,
|
||||
)
|
||||
|
||||
_STATUS_CAUSES: dict[int, VersionUpdateGatewayCause] = {
|
||||
httpx.codes.NOT_FOUND: VersionUpdateGatewayCause.NOT_FOUND,
|
||||
httpx.codes.FORBIDDEN: VersionUpdateGatewayCause.FORBIDDEN,
|
||||
httpx.codes.TOO_MANY_REQUESTS: VersionUpdateGatewayCause.TOO_MANY_REQUESTS,
|
||||
}
|
||||
|
||||
|
||||
class PyPIVersionUpdateGateway(VersionUpdateGateway):
|
||||
def __init__(
|
||||
self,
|
||||
project_name: str,
|
||||
*,
|
||||
client: httpx.AsyncClient | None = None,
|
||||
timeout: float = 5.0,
|
||||
base_url: str = "https://pypi.org",
|
||||
) -> None:
|
||||
self._project_name = project_name
|
||||
self._client = client
|
||||
self._timeout = timeout
|
||||
self._base_url = base_url.rstrip("/")
|
||||
|
||||
async def fetch_update(self) -> VersionUpdate | None:
|
||||
response = await self._fetch()
|
||||
self._raise_gateway_error_if_any(response)
|
||||
|
||||
try:
|
||||
data = response.json()
|
||||
except ValueError as exc:
|
||||
raise VersionUpdateGatewayError(
|
||||
cause=VersionUpdateGatewayCause.INVALID_RESPONSE
|
||||
) from exc
|
||||
|
||||
versions = data.get("versions") or []
|
||||
files = data.get("files") or []
|
||||
|
||||
non_yanked_versions: set[Version] = set()
|
||||
for file in files:
|
||||
if not isinstance(file, dict) or file.get("yanked") is True:
|
||||
continue
|
||||
filename = file.get("filename")
|
||||
if not isinstance(filename, str):
|
||||
continue
|
||||
parsed_version = _parse_filename_version(filename)
|
||||
if parsed_version is not None:
|
||||
non_yanked_versions.add(parsed_version)
|
||||
|
||||
valid_versions: list[Version] = []
|
||||
for raw_version in versions:
|
||||
try:
|
||||
valid_versions.append(Version(str(raw_version)))
|
||||
except InvalidVersion:
|
||||
continue
|
||||
|
||||
for version in sorted(valid_versions, reverse=True):
|
||||
if version in non_yanked_versions:
|
||||
return VersionUpdate(latest_version=str(version))
|
||||
|
||||
return None
|
||||
|
||||
async def _fetch(self) -> httpx.Response:
|
||||
headers = {"Accept": "application/vnd.pypi.simple.v1+json"}
|
||||
request_path = f"/simple/{self._project_name}/"
|
||||
|
||||
try:
|
||||
if self._client is not None:
|
||||
return await self._client.get(
|
||||
f"{self._base_url}{request_path}",
|
||||
headers=headers,
|
||||
timeout=self._timeout,
|
||||
)
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
base_url=self._base_url, timeout=self._timeout
|
||||
) as client:
|
||||
return await client.get(request_path, headers=headers)
|
||||
except httpx.RequestError as exc:
|
||||
raise VersionUpdateGatewayError(
|
||||
cause=VersionUpdateGatewayCause.REQUEST_FAILED
|
||||
) from exc
|
||||
|
||||
def _raise_gateway_error_if_any(self, response: httpx.Response) -> None:
|
||||
if response.status_code in _STATUS_CAUSES:
|
||||
raise VersionUpdateGatewayError(cause=_STATUS_CAUSES[response.status_code])
|
||||
|
||||
if response.is_error:
|
||||
raise VersionUpdateGatewayError(
|
||||
cause=VersionUpdateGatewayCause.ERROR_RESPONSE
|
||||
)
|
||||
|
||||
|
||||
def _parse_filename_version(filename: str) -> Version | None:
|
||||
try:
|
||||
_, version, *_ = parse_wheel_filename(filename)
|
||||
return Version(str(version))
|
||||
except Exception:
|
||||
try:
|
||||
_, sdist_version = parse_sdist_filename(filename)
|
||||
return Version(str(sdist_version))
|
||||
except Exception:
|
||||
return None
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.cli.update_notifier.version_update_gateway import (
|
||||
VersionUpdate,
|
||||
VersionUpdateGateway,
|
||||
VersionUpdateGatewayError,
|
||||
)
|
||||
|
||||
|
||||
class FakeVersionUpdateGateway(VersionUpdateGateway):
|
||||
def __init__(
|
||||
self,
|
||||
update: VersionUpdate | None = None,
|
||||
error: VersionUpdateGatewayError | None = None,
|
||||
) -> None:
|
||||
self._update: VersionUpdate | None = update
|
||||
self._error = error
|
||||
self.fetch_update_calls = 0
|
||||
|
||||
async def fetch_update(self) -> VersionUpdate | None:
|
||||
self.fetch_update_calls += 1
|
||||
if self._error is not None:
|
||||
raise self._error
|
||||
return self._update
|
||||
15
vibe/cli/update_notifier/ports/update_cache_repository.py
Normal file
15
vibe/cli/update_notifier/ports/update_cache_repository.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class UpdateCache:
|
||||
latest_version: str
|
||||
stored_at_timestamp: int
|
||||
|
||||
|
||||
class UpdateCacheRepository(Protocol):
|
||||
async def get(self) -> UpdateCache | None: ...
|
||||
async def set(self, update_cache: UpdateCache) -> None: ...
|
||||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum, auto
|
||||
from typing import Protocol, runtime_checkable
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
@ -49,6 +49,5 @@ class VersionUpdateGatewayError(Exception):
|
|||
super().__init__(detail)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class VersionUpdateGateway(Protocol):
|
||||
async def fetch_update(self) -> VersionUpdate | None: ...
|
||||
|
|
@ -1,15 +1,28 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import time
|
||||
|
||||
from packaging.version import InvalidVersion, Version
|
||||
|
||||
from vibe.cli.update_notifier.version_update_gateway import (
|
||||
from vibe.cli.update_notifier import (
|
||||
DEFAULT_GATEWAY_MESSAGES,
|
||||
VersionUpdate,
|
||||
UpdateCache,
|
||||
UpdateCacheRepository,
|
||||
VersionUpdateGateway,
|
||||
VersionUpdateGatewayCause,
|
||||
VersionUpdateGatewayError,
|
||||
)
|
||||
|
||||
UPDATE_CACHE_TTL_SECONDS = 24 * 60 * 60
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VersionUpdateAvailability:
|
||||
latest_version: str
|
||||
should_notify: bool
|
||||
|
||||
|
||||
class VersionUpdateError(Exception):
|
||||
def __init__(self, message: str) -> None:
|
||||
|
|
@ -37,21 +50,76 @@ def _describe_gateway_error(error: VersionUpdateGatewayError) -> str:
|
|||
return DEFAULT_GATEWAY_MESSAGES[VersionUpdateGatewayCause.UNKNOWN]
|
||||
|
||||
|
||||
async def is_version_update_available(
|
||||
version_update_notifier: VersionUpdateGateway, current_version: str
|
||||
) -> VersionUpdate | None:
|
||||
def _is_cache_fresh(
|
||||
cache: UpdateCache, get_current_timestamp: Callable[[], int]
|
||||
) -> bool:
|
||||
return (
|
||||
cache.stored_at_timestamp > get_current_timestamp() - UPDATE_CACHE_TTL_SECONDS
|
||||
)
|
||||
|
||||
|
||||
def _get_cached_update_if_any(
|
||||
cache: UpdateCache, current: Version
|
||||
) -> VersionUpdateAvailability | None:
|
||||
latest_version_in_cache = _parse_version(cache.latest_version)
|
||||
if latest_version_in_cache is None or latest_version_in_cache <= current:
|
||||
return None
|
||||
|
||||
return VersionUpdateAvailability(
|
||||
latest_version=cache.latest_version, should_notify=False
|
||||
)
|
||||
|
||||
|
||||
async def _write_update_cache(
|
||||
repository: UpdateCacheRepository,
|
||||
version: str,
|
||||
get_current_timestamp: Callable[[], int],
|
||||
) -> None:
|
||||
await repository.set(
|
||||
UpdateCache(latest_version=version, stored_at_timestamp=get_current_timestamp())
|
||||
)
|
||||
|
||||
|
||||
async def get_update_if_available(
|
||||
version_update_notifier: VersionUpdateGateway,
|
||||
current_version: str,
|
||||
update_cache_repository: UpdateCacheRepository,
|
||||
get_current_timestamp: Callable[[], int] = lambda: int(time.time()),
|
||||
) -> VersionUpdateAvailability | None:
|
||||
if not (current := _parse_version(current_version)):
|
||||
return None
|
||||
|
||||
if update_cache := await update_cache_repository.get():
|
||||
if _is_cache_fresh(update_cache, get_current_timestamp):
|
||||
return _get_cached_update_if_any(update_cache, current)
|
||||
|
||||
try:
|
||||
update = await version_update_notifier.fetch_update()
|
||||
except VersionUpdateGatewayError as error:
|
||||
await _write_update_cache(
|
||||
update_cache_repository, current_version, get_current_timestamp
|
||||
)
|
||||
raise VersionUpdateError(_describe_gateway_error(error)) from error
|
||||
|
||||
if not update:
|
||||
await _write_update_cache(
|
||||
update_cache_repository, current_version, get_current_timestamp
|
||||
)
|
||||
return None
|
||||
|
||||
latest_version = _parse_version(update.latest_version)
|
||||
current = _parse_version(current_version)
|
||||
|
||||
if latest_version is None or current is None:
|
||||
if not (latest_version := _parse_version(update.latest_version)):
|
||||
return None
|
||||
|
||||
return update if latest_version > current else None
|
||||
if latest_version <= current:
|
||||
await _write_update_cache(
|
||||
update_cache_repository, current_version, get_current_timestamp
|
||||
)
|
||||
return None
|
||||
|
||||
await _write_update_cache(
|
||||
update_cache_repository, update.latest_version, get_current_timestamp
|
||||
)
|
||||
|
||||
return VersionUpdateAvailability(
|
||||
latest_version=update.latest_version, should_notify=True
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
__all__ = ["__version__", "run_programmatic"]
|
||||
__version__ = "1.1.2"
|
||||
__version__ = "1.1.3"
|
||||
|
||||
from vibe.core.programmatic import run_programmatic
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@ from vibe.core.tools.manager import ToolManager
|
|||
from vibe.core.types import (
|
||||
AgentStats,
|
||||
ApprovalCallback,
|
||||
ApprovalResponse,
|
||||
AssistantEvent,
|
||||
AsyncApprovalCallback,
|
||||
BaseEvent,
|
||||
CompactEndEvent,
|
||||
CompactStartEvent,
|
||||
|
|
@ -53,7 +55,6 @@ from vibe.core.types import (
|
|||
from vibe.core.utils import (
|
||||
TOOL_ERROR_TAG,
|
||||
VIBE_STOP_EVENT_TAG,
|
||||
ApprovalResponse,
|
||||
CancellationReason,
|
||||
get_user_agent,
|
||||
get_user_cancellation_message,
|
||||
|
|
@ -188,11 +189,6 @@ class Agent:
|
|||
case MiddlewareAction.STOP:
|
||||
yield AssistantEvent(
|
||||
content=f"<{VIBE_STOP_EVENT_TAG}>{result.reason}</{VIBE_STOP_EVENT_TAG}>",
|
||||
prompt_tokens=0,
|
||||
completion_tokens=0,
|
||||
session_total_tokens=self.stats.session_total_llm_tokens,
|
||||
last_turn_duration=0,
|
||||
tokens_per_second=0,
|
||||
stopped_by_middleware=True,
|
||||
)
|
||||
await self.interaction_logger.save_interaction(
|
||||
|
|
@ -337,16 +333,7 @@ class Agent:
|
|||
def _create_assistant_event(
|
||||
self, content: str, chunk: LLMChunk | None
|
||||
) -> AssistantEvent:
|
||||
return AssistantEvent(
|
||||
content=content,
|
||||
prompt_tokens=chunk.usage.prompt_tokens if chunk and chunk.usage else 0,
|
||||
completion_tokens=chunk.usage.completion_tokens
|
||||
if chunk and chunk.usage
|
||||
else 0,
|
||||
session_total_tokens=self.stats.session_total_llm_tokens,
|
||||
last_turn_duration=self.stats.last_turn_duration,
|
||||
tokens_per_second=self.stats.tokens_per_second,
|
||||
)
|
||||
return AssistantEvent(content=content)
|
||||
|
||||
async def _stream_assistant_events(self) -> AsyncGenerator[AssistantEvent]:
|
||||
chunks: list[LLMChunk] = []
|
||||
|
|
@ -421,14 +408,7 @@ class Agent:
|
|||
assistant_msg = llm_result.message
|
||||
self.messages.append(assistant_msg)
|
||||
|
||||
return AssistantEvent(
|
||||
content=assistant_msg.content or "",
|
||||
prompt_tokens=llm_result.usage.prompt_tokens,
|
||||
completion_tokens=llm_result.usage.completion_tokens,
|
||||
session_total_tokens=self.stats.session_total_llm_tokens,
|
||||
last_turn_duration=self.stats.last_turn_duration,
|
||||
tokens_per_second=self.stats.tokens_per_second,
|
||||
)
|
||||
return AssistantEvent(content=assistant_msg.content or "")
|
||||
|
||||
async def _handle_tool_calls( # noqa: PLR0915
|
||||
self, resolved: ResolvedMessage
|
||||
|
|
@ -761,24 +741,18 @@ class Agent:
|
|||
feedback="Tool execution not permitted.",
|
||||
)
|
||||
if asyncio.iscoroutinefunction(self.approval_callback):
|
||||
response, feedback = await self.approval_callback(
|
||||
tool_name, args, tool_call_id
|
||||
)
|
||||
async_callback = cast(AsyncApprovalCallback, self.approval_callback)
|
||||
response, feedback = await async_callback(tool_name, args, tool_call_id)
|
||||
else:
|
||||
sync_callback = cast(SyncApprovalCallback, self.approval_callback)
|
||||
response, feedback = sync_callback(tool_name, args, tool_call_id)
|
||||
|
||||
match response:
|
||||
case ApprovalResponse.ALWAYS:
|
||||
self.auto_approve = True
|
||||
return ToolDecision(
|
||||
verdict=ToolExecutionResponse.EXECUTE, feedback=feedback
|
||||
)
|
||||
case ApprovalResponse.YES:
|
||||
return ToolDecision(
|
||||
verdict=ToolExecutionResponse.EXECUTE, feedback=feedback
|
||||
)
|
||||
case _:
|
||||
case ApprovalResponse.NO:
|
||||
return ToolDecision(
|
||||
verdict=ToolExecutionResponse.SKIP, feedback=feedback
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,45 +19,28 @@ from pydantic_settings import (
|
|||
)
|
||||
import tomli_w
|
||||
|
||||
from vibe.core.config_path import (
|
||||
AGENT_DIR,
|
||||
CONFIG_DIR,
|
||||
CONFIG_FILE,
|
||||
GLOBAL_ENV_FILE,
|
||||
PROMPT_DIR,
|
||||
SESSION_LOG_DIR,
|
||||
)
|
||||
from vibe.core.prompts import SystemPrompt
|
||||
from vibe.core.tools.base import BaseToolConfig
|
||||
|
||||
|
||||
def get_vibe_home() -> Path:
|
||||
if vibe_home := os.getenv("VIBE_HOME"):
|
||||
return Path(vibe_home).expanduser().resolve()
|
||||
return Path.home() / ".vibe"
|
||||
|
||||
|
||||
GLOBAL_CONFIG_DIR = get_vibe_home()
|
||||
GLOBAL_CONFIG_FILE = GLOBAL_CONFIG_DIR / "config.toml"
|
||||
GLOBAL_ENV_FILE = GLOBAL_CONFIG_DIR / ".env"
|
||||
|
||||
|
||||
def resolve_config_file() -> Path:
|
||||
for directory in (cwd := Path.cwd(), *cwd.parents):
|
||||
if (candidate := directory / ".vibe" / "config.toml").is_file():
|
||||
return candidate
|
||||
return GLOBAL_CONFIG_FILE
|
||||
PROJECT_DOC_FILENAMES = ["AGENTS.md", "VIBE.md", ".vibe.md"]
|
||||
|
||||
|
||||
def load_api_keys_from_env() -> None:
|
||||
if GLOBAL_ENV_FILE.is_file():
|
||||
env_vars = dotenv_values(GLOBAL_ENV_FILE)
|
||||
if GLOBAL_ENV_FILE.path.is_file():
|
||||
env_vars = dotenv_values(GLOBAL_ENV_FILE.path)
|
||||
for key, value in env_vars.items():
|
||||
if value:
|
||||
os.environ.setdefault(key, value)
|
||||
|
||||
|
||||
CONFIG_FILE = resolve_config_file()
|
||||
CONFIG_DIR = CONFIG_FILE.parent
|
||||
AGENT_DIR = CONFIG_DIR / "agents"
|
||||
PROMPT_DIR = CONFIG_DIR / "prompts"
|
||||
INSTRUCTIONS_FILE = CONFIG_DIR / "instructions.md"
|
||||
HISTORY_FILE = CONFIG_DIR / "vibehistory"
|
||||
PROJECT_DOC_FILENAMES = ["AGENTS.md", "VIBE.md", ".vibe.md"]
|
||||
|
||||
|
||||
class MissingAPIKeyError(RuntimeError):
|
||||
def __init__(self, env_key: str, provider_name: str) -> None:
|
||||
super().__init__(
|
||||
|
|
@ -94,7 +77,7 @@ class TomlFileSettingsSource(PydanticBaseSettingsSource):
|
|||
self.toml_data = self._load_toml()
|
||||
|
||||
def _load_toml(self) -> dict[str, Any]:
|
||||
file = CONFIG_FILE
|
||||
file = CONFIG_FILE.path
|
||||
try:
|
||||
with file.open("rb") as f:
|
||||
return tomllib.load(f)
|
||||
|
|
@ -134,7 +117,7 @@ class SessionLoggingConfig(BaseSettings):
|
|||
@classmethod
|
||||
def set_default_save_dir(cls, v: str) -> str:
|
||||
if not v:
|
||||
return str(get_vibe_home() / "logs" / "session")
|
||||
return str(SESSION_LOG_DIR.path)
|
||||
return v
|
||||
|
||||
@field_validator("save_dir", mode="after")
|
||||
|
|
@ -350,7 +333,7 @@ class VibeConfig(BaseSettings):
|
|||
)
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="VIBE_", case_sensitive=False, extra="forbid"
|
||||
env_prefix="VIBE_", case_sensitive=False, extra="ignore"
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
@ -364,9 +347,9 @@ class VibeConfig(BaseSettings):
|
|||
except KeyError:
|
||||
pass
|
||||
|
||||
custom_sp_path = (PROMPT_DIR / self.system_prompt_id).with_suffix(".md")
|
||||
custom_sp_path = (PROMPT_DIR.path / self.system_prompt_id).with_suffix(".md")
|
||||
if not custom_sp_path.is_file():
|
||||
raise MissingPromptFileError(self.system_prompt_id, str(PROMPT_DIR))
|
||||
raise MissingPromptFileError(self.system_prompt_id, str(PROMPT_DIR.path))
|
||||
return custom_sp_path.read_text()
|
||||
|
||||
def get_active_model(self) -> ModelConfig:
|
||||
|
|
@ -492,7 +475,7 @@ class VibeConfig(BaseSettings):
|
|||
|
||||
@classmethod
|
||||
def save_updates(cls, updates: dict[str, Any]) -> None:
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
CONFIG_DIR.path.mkdir(parents=True, exist_ok=True)
|
||||
current_config = TomlFileSettingsSource(cls).toml_data
|
||||
|
||||
def deep_merge(target: dict, source: dict) -> None:
|
||||
|
|
@ -522,7 +505,7 @@ class VibeConfig(BaseSettings):
|
|||
|
||||
@classmethod
|
||||
def dump_config(cls, config: dict[str, Any]) -> None:
|
||||
with CONFIG_FILE.open("wb") as f:
|
||||
with CONFIG_FILE.path.open("wb") as f:
|
||||
tomli_w.dump(config, f)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -530,21 +513,21 @@ class VibeConfig(BaseSettings):
|
|||
if agent is None:
|
||||
return None
|
||||
|
||||
agent_config_path = (AGENT_DIR / agent).with_suffix(".toml")
|
||||
agent_config_path = (AGENT_DIR.path / agent).with_suffix(".toml")
|
||||
try:
|
||||
return tomllib.load(agent_config_path.open("rb"))
|
||||
except FileNotFoundError:
|
||||
raise ValueError(
|
||||
f"Config '{agent}.toml' for agent not found in {AGENT_DIR}"
|
||||
f"Config '{agent}.toml' for agent not found in {AGENT_DIR.path}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _migrate(cls) -> None:
|
||||
if not CONFIG_FILE.exists():
|
||||
if not CONFIG_FILE.path.is_file():
|
||||
return
|
||||
|
||||
try:
|
||||
with CONFIG_FILE.open("rb") as f:
|
||||
with CONFIG_FILE.path.open("rb") as f:
|
||||
config = tomllib.load(f)
|
||||
except (OSError, tomllib.TOMLDecodeError):
|
||||
return
|
||||
|
|
|
|||
51
vibe/core/config_path.py
Normal file
51
vibe/core/config_path.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ConfigPath:
|
||||
def __init__(self, path_resolver: Callable[[], Path]) -> None:
|
||||
self._path_resolver = path_resolver
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
return self._path_resolver()
|
||||
|
||||
|
||||
_DEFAULT_VIBE_HOME = Path.home() / ".vibe"
|
||||
|
||||
|
||||
def _get_vibe_home() -> Path:
|
||||
if vibe_home := os.getenv("VIBE_HOME"):
|
||||
return Path(vibe_home).expanduser().resolve()
|
||||
return _DEFAULT_VIBE_HOME
|
||||
|
||||
|
||||
def _resolve_config_file() -> Path:
|
||||
if (candidate := Path.cwd() / ".vibe" / "config.toml").is_file():
|
||||
return candidate
|
||||
return _get_vibe_home() / "config.toml"
|
||||
|
||||
|
||||
def resolve_local_tools_dir(dir: Path) -> Path | None:
|
||||
if (candidate := dir / ".vibe" / "tools").is_dir():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
VIBE_HOME = ConfigPath(_get_vibe_home)
|
||||
GLOBAL_CONFIG_FILE = ConfigPath(lambda: VIBE_HOME.path / "config.toml")
|
||||
GLOBAL_ENV_FILE = ConfigPath(lambda: VIBE_HOME.path / ".env")
|
||||
GLOBAL_TOOLS_DIR = ConfigPath(lambda: VIBE_HOME.path / "tools")
|
||||
SESSION_LOG_DIR = ConfigPath(lambda: VIBE_HOME.path / "logs" / "session")
|
||||
|
||||
CONFIG_FILE = ConfigPath(_resolve_config_file)
|
||||
CONFIG_DIR = ConfigPath(lambda: CONFIG_FILE.path.parent)
|
||||
LOG_DIR = ConfigPath(lambda: CONFIG_FILE.path.parent / "logs")
|
||||
AGENT_DIR = ConfigPath(lambda: CONFIG_FILE.path.parent / "agents")
|
||||
PROMPT_DIR = ConfigPath(lambda: CONFIG_FILE.path.parent / "prompts")
|
||||
INSTRUCTIONS_FILE = ConfigPath(lambda: CONFIG_FILE.path.parent / "instructions.md")
|
||||
HISTORY_FILE = ConfigPath(lambda: CONFIG_FILE.path.parent / "vibehistory")
|
||||
LOG_FILE = ConfigPath(lambda: CONFIG_FILE.path.parent / "vibe.log")
|
||||
|
|
@ -124,8 +124,10 @@ class OpenAIAdapter(APIAdapter):
|
|||
|
||||
if enable_streaming:
|
||||
payload["stream"] = True
|
||||
stream_options = {"include_usage": True}
|
||||
if provider.name == "mistral":
|
||||
payload["stream_options"] = {"stream_tool_calls": True}
|
||||
stream_options["stream_tool_calls"] = True
|
||||
payload["stream_options"] = stream_options
|
||||
|
||||
headers = self.build_headers(api_key)
|
||||
|
||||
|
|
@ -141,11 +143,11 @@ class OpenAIAdapter(APIAdapter):
|
|||
message = LLMMessage.model_validate(data["choices"][0]["delta"])
|
||||
else:
|
||||
raise ValueError("Invalid response data")
|
||||
finish_reason = data["choices"][0]["finish_reason"]
|
||||
finish_reason = data["choices"][0].get("finish_reason", None)
|
||||
|
||||
elif "message" in data:
|
||||
message = LLMMessage.model_validate(data["message"])
|
||||
finish_reason = data["finish_reason"]
|
||||
finish_reason = data["choices"][0].get("finish_reason", None)
|
||||
elif "delta" in data:
|
||||
message = LLMMessage.model_validate(data["delta"])
|
||||
finish_reason = None
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import sys
|
|||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from vibe.core.config import INSTRUCTIONS_FILE, PROJECT_DOC_FILENAMES
|
||||
from vibe.core.config import PROJECT_DOC_FILENAMES
|
||||
from vibe.core.config_path import INSTRUCTIONS_FILE
|
||||
from vibe.core.llm.format import get_active_tool_classes
|
||||
from vibe.core.prompts import UtilityPrompt
|
||||
from vibe.core.utils import is_dangerous_directory, is_windows
|
||||
|
|
@ -21,7 +22,7 @@ if TYPE_CHECKING:
|
|||
|
||||
def _load_user_instructions() -> str:
|
||||
try:
|
||||
return INSTRUCTIONS_FILE.read_text("utf-8", errors="ignore")
|
||||
return INSTRUCTIONS_FILE.path.read_text("utf-8", errors="ignore")
|
||||
except (FileNotFoundError, OSError):
|
||||
return ""
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import sys
|
|||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from vibe import VIBE_ROOT
|
||||
from vibe.core.config import get_vibe_home
|
||||
from vibe.core.config_path import GLOBAL_TOOLS_DIR, resolve_local_tools_dir
|
||||
from vibe.core.tools.base import BaseTool, BaseToolConfig
|
||||
from vibe.core.tools.mcp import (
|
||||
RemoteTool,
|
||||
|
|
@ -60,16 +60,11 @@ class ToolManager:
|
|||
if path.is_dir():
|
||||
paths.append(path)
|
||||
|
||||
cwd = config.effective_workdir
|
||||
for directory in (cwd, *cwd.parents):
|
||||
tools_dir = directory / ".vibe" / "tools"
|
||||
if tools_dir.is_dir():
|
||||
paths.append(tools_dir)
|
||||
break
|
||||
if (tools_dir := resolve_local_tools_dir(config.effective_workdir)) is not None:
|
||||
paths.append(tools_dir)
|
||||
|
||||
global_tools = get_vibe_home() / "tools"
|
||||
if global_tools.is_dir():
|
||||
paths.append(global_tools)
|
||||
if GLOBAL_TOOLS_DIR.path.is_dir():
|
||||
paths.append(GLOBAL_TOOLS_DIR.path)
|
||||
|
||||
unique: list[Path] = []
|
||||
seen: set[Path] = set()
|
||||
|
|
|
|||
|
|
@ -173,6 +173,11 @@ class Role(StrEnum):
|
|||
tool = auto()
|
||||
|
||||
|
||||
class ApprovalResponse(StrEnum):
|
||||
YES = "y"
|
||||
NO = "n"
|
||||
|
||||
|
||||
class LLMMessage(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
|
|
@ -219,11 +224,6 @@ class BaseEvent(BaseModel, ABC):
|
|||
|
||||
class AssistantEvent(BaseEvent):
|
||||
content: str
|
||||
prompt_tokens: int = 0
|
||||
completion_tokens: int = 0
|
||||
session_total_tokens: int = 0
|
||||
last_turn_duration: float = 0.0
|
||||
tokens_per_second: float = 0.0
|
||||
stopped_by_middleware: bool = False
|
||||
|
||||
|
||||
|
|
@ -263,9 +263,11 @@ class OutputFormat(StrEnum):
|
|||
|
||||
|
||||
type AsyncApprovalCallback = Callable[
|
||||
[str, dict[str, Any], str], Awaitable[tuple[str, str | None]]
|
||||
[str, dict[str, Any], str], Awaitable[tuple[ApprovalResponse, str | None]]
|
||||
]
|
||||
|
||||
type SyncApprovalCallback = Callable[[str, dict[str, Any], str], tuple[str, str | None]]
|
||||
type SyncApprovalCallback = Callable[
|
||||
[str, dict[str, Any], str], tuple[ApprovalResponse, str | None]
|
||||
]
|
||||
|
||||
type ApprovalCallback = AsyncApprovalCallback | SyncApprovalCallback
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
import asyncio
|
||||
from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine
|
||||
import concurrent.futures
|
||||
from enum import Enum, StrEnum, auto
|
||||
from enum import Enum, auto
|
||||
import functools
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
|
@ -14,16 +14,10 @@ from typing import Any
|
|||
import httpx
|
||||
|
||||
from vibe.core import __version__
|
||||
from vibe.core.config import CONFIG_DIR, CONFIG_FILE, GLOBAL_CONFIG_FILE, Backend
|
||||
from vibe.core.config import Backend
|
||||
from vibe.core.config_path import CONFIG_FILE, GLOBAL_CONFIG_FILE, LOG_DIR, LOG_FILE
|
||||
from vibe.core.types import BaseEvent, ToolResultEvent
|
||||
|
||||
|
||||
class ApprovalResponse(StrEnum):
|
||||
YES = "y"
|
||||
NO = "n"
|
||||
ALWAYS = "a"
|
||||
|
||||
|
||||
CANCELLATION_TAG = "user_cancellation"
|
||||
TOOL_ERROR_TAG = "tool_error"
|
||||
VIBE_STOP_EVENT_TAG = "vibe_stop_event"
|
||||
|
|
@ -141,22 +135,21 @@ def is_dangerous_directory(path: Path | str = ".") -> tuple[bool, str]:
|
|||
return False, ""
|
||||
|
||||
|
||||
LOG_DIR = CONFIG_DIR
|
||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
LOG_FILE = LOG_DIR / "vibe.log"
|
||||
LOG_DIR.path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s %(message)s",
|
||||
handlers=[logging.FileHandler(LOG_FILE, "a", "utf-8")],
|
||||
handlers=[logging.FileHandler(LOG_FILE.path, "a", "utf-8")],
|
||||
)
|
||||
|
||||
logger = logging.getLogger("vibe")
|
||||
logger.info("Using config: %s", CONFIG_FILE)
|
||||
if CONFIG_FILE != GLOBAL_CONFIG_FILE and GLOBAL_CONFIG_FILE.is_file():
|
||||
logger.info("Using config: %s", CONFIG_FILE.path)
|
||||
if CONFIG_FILE.path != GLOBAL_CONFIG_FILE.path and GLOBAL_CONFIG_FILE.path.is_file():
|
||||
logger.warning(
|
||||
"Project config active (%s); ignoring global config (%s)",
|
||||
CONFIG_FILE,
|
||||
GLOBAL_CONFIG_FILE,
|
||||
CONFIG_FILE.path,
|
||||
GLOBAL_CONFIG_FILE.path,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import sys
|
|||
from rich import print as rprint
|
||||
from textual.app import App
|
||||
|
||||
from vibe.core.config import GLOBAL_ENV_FILE
|
||||
from vibe.core.config_path import GLOBAL_ENV_FILE
|
||||
from vibe.setup.onboarding.screens import (
|
||||
ApiKeyScreen,
|
||||
ThemeSelectionScreen,
|
||||
|
|
@ -34,7 +34,7 @@ def run_onboarding(app: App | None = None) -> None:
|
|||
rprint(
|
||||
f"\n[yellow]Warning: Could not save API key to .env file: {err}[/]"
|
||||
"\n[dim]The API key is set for this session only. "
|
||||
f"You may need to set it manually in {GLOBAL_ENV_FILE}[/]\n"
|
||||
f"You may need to set it manually in {GLOBAL_ENV_FILE.path}[/]\n"
|
||||
)
|
||||
case "completed":
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ from textual.validation import Length
|
|||
from textual.widgets import Input, Link, Static
|
||||
|
||||
from vibe.cli.clipboard import copy_selection_to_clipboard
|
||||
from vibe.core.config import GLOBAL_ENV_FILE, VibeConfig
|
||||
from vibe.core.config import VibeConfig
|
||||
from vibe.core.config_path import GLOBAL_ENV_FILE
|
||||
from vibe.setup.onboarding.base import OnboardingScreen
|
||||
|
||||
PROVIDER_HELP = {
|
||||
|
|
@ -24,8 +25,8 @@ CONFIG_DOCS_URL = (
|
|||
|
||||
|
||||
def _save_api_key_to_env_file(env_key: str, api_key: str) -> None:
|
||||
GLOBAL_ENV_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
set_key(GLOBAL_ENV_FILE, env_key, api_key)
|
||||
GLOBAL_ENV_FILE.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
set_key(GLOBAL_ENV_FILE.path, env_key, api_key)
|
||||
|
||||
|
||||
class ApiKeyScreen(OnboardingScreen):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue