From 03b77cd3d79976677cc1cd17dd088cc677338c47 Mon Sep 17 00:00:00 2001 From: Sung-jin Brian Hong Date: Mon, 13 Oct 2025 21:24:22 +0900 Subject: [PATCH 1/2] refactor: make context protocols for tools --- src/vibecore/context.py | 56 +++++++++++++++++++-- src/vibecore/flow.py | 4 +- src/vibecore/tools/file/executor.py | 18 +++++-- src/vibecore/tools/file/tools.py | 10 ++-- src/vibecore/tools/python/helpers.py | 4 +- src/vibecore/tools/python/tools.py | 4 +- src/vibecore/tools/shell/executor.py | 10 ++-- src/vibecore/tools/shell/tools.py | 10 ++-- src/vibecore/tools/todo/tools.py | 6 +-- src/vibecore/tools/webfetch/tools.py | 4 +- src/vibecore/tools/websearch/tools.py | 4 +- tests/tools/test_file_tools.py | 6 +-- tests/tools/test_python_tool_integration.py | 32 ++++++------ tests/tools/test_shell_tools.py | 6 +-- tests/tools/test_todo_tools.py | 4 +- 15 files changed, 119 insertions(+), 59 deletions(-) diff --git a/src/vibecore/context.py b/src/vibecore/context.py index 5bb6577..52970cb 100644 --- a/src/vibecore/context.py +++ b/src/vibecore/context.py @@ -1,6 +1,6 @@ from dataclasses import dataclass, field from pathlib import Path -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Protocol, runtime_checkable from vibecore.tools.python.manager import PythonExecutionManager from vibecore.tools.todo.manager import TodoManager @@ -10,8 +10,53 @@ from vibecore.tools.path_validator import PathValidator +@runtime_checkable +class BasicToolContext(Protocol): + """Context that does not impose any additional requirements.""" + + ... + + +@runtime_checkable +class TodoToolContext(Protocol): + """Context required by todo tools.""" + + todo_manager: TodoManager + + +@runtime_checkable +class PythonToolContext(Protocol): + """Context required by Python execution tools.""" + + python_manager: PythonExecutionManager + + +@runtime_checkable +class PathValidatedContext(Protocol): + """Context that provides a path validator for file-system tools.""" + + path_validator: "PathValidator" + + +@runtime_checkable +class AppAwareContext(Protocol): + """Context that exposes the optional Textual app for streaming updates.""" + + app: Optional["VibecoreApp"] + + +@runtime_checkable +class VibecoreContext(TodoToolContext, PythonToolContext, PathValidatedContext, AppAwareContext, Protocol): + """Protocol describing the full context required by Vibecore agents.""" + + allowed_directories: list[Path] + + def reset_state(self) -> None: + """Reset state between sessions.""" + + @dataclass -class VibecoreContext: +class DefaultVibecoreContext: todo_manager: TodoManager = field(default_factory=TodoManager) python_manager: PythonExecutionManager = field(default_factory=PythonExecutionManager) app: Optional["VibecoreApp"] = None @@ -20,7 +65,7 @@ class VibecoreContext: allowed_directories: list[Path] = field(default_factory=list) path_validator: "PathValidator" = field(init=False) # Always initialized, never None - def __post_init__(self): + def __post_init__(self) -> None: """Initialize path validator with allowed directories.""" from vibecore.tools.path_validator import PathValidator @@ -55,3 +100,8 @@ def reset_state(self) -> None: from vibecore.tools.path_validator import PathValidator self.path_validator = PathValidator(self.allowed_directories) + + +if TYPE_CHECKING: + # Ensure DefaultVibecoreContext conforms to the VibecoreContext protocol for static analyzers + _default_context: VibecoreContext = DefaultVibecoreContext() diff --git a/src/vibecore/flow.py b/src/vibecore/flow.py index c89bb19..7de97d7 100644 --- a/src/vibecore/flow.py +++ b/src/vibecore/flow.py @@ -20,7 +20,7 @@ from textual.pilot import Pilot from typing_extensions import TypeVar -from vibecore.context import VibecoreContext +from vibecore.context import DefaultVibecoreContext, VibecoreContext from vibecore.main import AppIsExiting, VibecoreApp from vibecore.session import JSONLSession from vibecore.settings import settings @@ -265,7 +265,7 @@ async def run(self, inputs: list[str] | None = None, shutdown: bool = False) -> class Vibecore(Generic[TWorkflowReturn]): def __init__(self, disable_user_input: bool = True) -> None: - self.context = VibecoreContext() + self.context = DefaultVibecoreContext() self.workflow_logic: DecoratedCallable[TWorkflowReturn] | None = None self.disable_user_input = disable_user_input self.runner: VibecoreRunnerBase[TWorkflowReturn] = VibecoreRunnerBase(self) diff --git a/src/vibecore/tools/file/executor.py b/src/vibecore/tools/file/executor.py index 443ca73..46040c7 100644 --- a/src/vibecore/tools/file/executor.py +++ b/src/vibecore/tools/file/executor.py @@ -4,7 +4,7 @@ from agents import RunContextWrapper -from vibecore.context import VibecoreContext +from vibecore.context import PathValidatedContext from vibecore.settings import settings from vibecore.tools.file.utils import PathValidationError @@ -12,7 +12,7 @@ async def read_file( - ctx: RunContextWrapper[VibecoreContext], file_path: str, offset: int | None = None, limit: int | None = None + ctx: RunContextWrapper[PathValidatedContext], file_path: str, offset: int | None = None, limit: int | None = None ) -> str: """Read a file and return its contents in cat -n format. @@ -100,7 +100,11 @@ async def read_file( async def edit_file( - ctx: RunContextWrapper[VibecoreContext], file_path: str, old_string: str, new_string: str, replace_all: bool = False + ctx: RunContextWrapper[PathValidatedContext], + file_path: str, + old_string: str, + new_string: str, + replace_all: bool = False, ) -> str: """Edit a file by replacing strings. @@ -182,7 +186,11 @@ async def edit_file( return f"Error: Unexpected error editing file: {e}" -async def multi_edit_file(ctx: RunContextWrapper[VibecoreContext], file_path: str, edits: list[dict[str, Any]]) -> str: +async def multi_edit_file( + ctx: RunContextWrapper[PathValidatedContext], + file_path: str, + edits: list[dict[str, Any]], +) -> str: """Edit a file by applying multiple replacements sequentially. Args: @@ -270,7 +278,7 @@ async def multi_edit_file(ctx: RunContextWrapper[VibecoreContext], file_path: st return f"Error: Unexpected error editing file: {e}" -async def write_file(ctx: RunContextWrapper[VibecoreContext], file_path: str, content: str) -> str: +async def write_file(ctx: RunContextWrapper[PathValidatedContext], file_path: str, content: str) -> str: """Write content to a file. Args: diff --git a/src/vibecore/tools/file/tools.py b/src/vibecore/tools/file/tools.py index fb75847..a3f6794 100644 --- a/src/vibecore/tools/file/tools.py +++ b/src/vibecore/tools/file/tools.py @@ -3,7 +3,7 @@ from agents import RunContextWrapper, function_tool from pydantic import BaseModel -from vibecore.context import VibecoreContext +from vibecore.context import PathValidatedContext from .executor import edit_file, multi_edit_file, read_file, write_file @@ -18,7 +18,7 @@ class EditOperation(BaseModel): @function_tool async def read( - ctx: RunContextWrapper[VibecoreContext], + ctx: RunContextWrapper[PathValidatedContext], file_path: str, offset: int | None = None, limit: int | None = None, @@ -54,7 +54,7 @@ async def read( @function_tool async def edit( - ctx: RunContextWrapper[VibecoreContext], + ctx: RunContextWrapper[PathValidatedContext], file_path: str, old_string: str, new_string: str, @@ -91,7 +91,7 @@ async def edit( @function_tool async def multi_edit( - ctx: RunContextWrapper[VibecoreContext], + ctx: RunContextWrapper[PathValidatedContext], file_path: str, edits: list[EditOperation], ) -> str: @@ -158,7 +158,7 @@ async def multi_edit( @function_tool async def write( - ctx: RunContextWrapper[VibecoreContext], + ctx: RunContextWrapper[PathValidatedContext], file_path: str, content: str, ) -> str: diff --git a/src/vibecore/tools/python/helpers.py b/src/vibecore/tools/python/helpers.py index e1c00d2..8f3dc5b 100644 --- a/src/vibecore/tools/python/helpers.py +++ b/src/vibecore/tools/python/helpers.py @@ -5,7 +5,7 @@ from agents import RunContextWrapper -from vibecore.context import VibecoreContext +from vibecore.context import PythonToolContext try: from PIL import Image # type: ignore[import-not-found] @@ -16,7 +16,7 @@ TERM_IMAGE_AVAILABLE = False -async def execute_python_helper(ctx: RunContextWrapper[VibecoreContext], code: str) -> str: +async def execute_python_helper(ctx: RunContextWrapper[PythonToolContext], code: str) -> str: """Helper function to execute Python code. This is the actual implementation extracted from the tool decorator. diff --git a/src/vibecore/tools/python/tools.py b/src/vibecore/tools/python/tools.py index d1144d3..3e43358 100644 --- a/src/vibecore/tools/python/tools.py +++ b/src/vibecore/tools/python/tools.py @@ -2,13 +2,13 @@ from agents import RunContextWrapper, function_tool -from vibecore.context import VibecoreContext +from vibecore.context import PythonToolContext from .helpers import execute_python_helper @function_tool -async def execute_python(ctx: RunContextWrapper[VibecoreContext], code: str) -> str: +async def execute_python(ctx: RunContextWrapper[PythonToolContext], code: str) -> str: """Execute Python code with persistent context across the session. The execution environment maintains state between calls, allowing you to: diff --git a/src/vibecore/tools/shell/executor.py b/src/vibecore/tools/shell/executor.py index 4ca68ae..dfd7397 100644 --- a/src/vibecore/tools/shell/executor.py +++ b/src/vibecore/tools/shell/executor.py @@ -8,13 +8,13 @@ from agents import RunContextWrapper -from vibecore.context import VibecoreContext +from vibecore.context import PathValidatedContext from vibecore.settings import settings from vibecore.tools.file.utils import PathValidationError, validate_file_path async def bash_executor( - ctx: RunContextWrapper[VibecoreContext], command: str, timeout: int | None = None + ctx: RunContextWrapper[PathValidatedContext], command: str, timeout: int | None = None ) -> tuple[str, int]: """Execute a bash command asynchronously. @@ -80,7 +80,7 @@ async def bash_executor( return f"Error executing command: {e}", 1 -async def glob_files(ctx: RunContextWrapper[VibecoreContext], pattern: str, path: str | None = None) -> list[str]: +async def glob_files(ctx: RunContextWrapper[PathValidatedContext], pattern: str, path: str | None = None) -> list[str]: """Find files matching a glob pattern. Args: @@ -142,7 +142,7 @@ async def glob_files(ctx: RunContextWrapper[VibecoreContext], pattern: str, path async def grep_files( - ctx: RunContextWrapper[VibecoreContext], pattern: str, path: str | None = None, include: str | None = None + ctx: RunContextWrapper[PathValidatedContext], pattern: str, path: str | None = None, include: str | None = None ) -> list[str]: """Search file contents using regular expressions. @@ -226,7 +226,7 @@ async def grep_files( async def list_directory( - ctx: RunContextWrapper[VibecoreContext], path: str, ignore: list[str] | None = None + ctx: RunContextWrapper[PathValidatedContext], path: str, ignore: list[str] | None = None ) -> list[str]: """List files and directories in a given path. diff --git a/src/vibecore/tools/shell/tools.py b/src/vibecore/tools/shell/tools.py index c304e80..18ca4c1 100644 --- a/src/vibecore/tools/shell/tools.py +++ b/src/vibecore/tools/shell/tools.py @@ -2,14 +2,14 @@ from agents import RunContextWrapper, function_tool -from vibecore.context import VibecoreContext +from vibecore.context import PathValidatedContext from .executor import bash_executor, glob_files, grep_files, list_directory @function_tool async def bash( - ctx: RunContextWrapper[VibecoreContext], + ctx: RunContextWrapper[PathValidatedContext], command: str, timeout: int | None = None, description: str | None = None, @@ -66,7 +66,7 @@ async def bash( @function_tool async def glob( - ctx: RunContextWrapper[VibecoreContext], + ctx: RunContextWrapper[PathValidatedContext], pattern: str, path: str | None = None, ) -> str: @@ -98,7 +98,7 @@ async def glob( @function_tool async def grep( - ctx: RunContextWrapper[VibecoreContext], + ctx: RunContextWrapper[PathValidatedContext], pattern: str, path: str | None = None, include: str | None = None, @@ -132,7 +132,7 @@ async def grep( @function_tool async def ls( - ctx: RunContextWrapper[VibecoreContext], + ctx: RunContextWrapper[PathValidatedContext], path: str, ignore: list[str] | None = None, ) -> str: diff --git a/src/vibecore/tools/todo/tools.py b/src/vibecore/tools/todo/tools.py index b6426de..9ebdd03 100644 --- a/src/vibecore/tools/todo/tools.py +++ b/src/vibecore/tools/todo/tools.py @@ -4,13 +4,13 @@ from agents import RunContextWrapper, function_tool -from vibecore.context import VibecoreContext +from vibecore.context import TodoToolContext from .models import TodoItem @function_tool -async def todo_read(ctx: RunContextWrapper[VibecoreContext]) -> list[dict[str, Any]]: +async def todo_read(ctx: RunContextWrapper[TodoToolContext]) -> list[dict[str, Any]]: """Use this tool to read the current to-do list for the session. This tool should be used proactively and frequently to ensure that you are aware of the status of the current task list. You should make use of this tool as often as possible, especially in the following situations: @@ -38,7 +38,7 @@ async def todo_read(ctx: RunContextWrapper[VibecoreContext]) -> list[dict[str, A @function_tool -async def todo_write(ctx: RunContextWrapper[VibecoreContext], todos: list[TodoItem]) -> str: +async def todo_write(ctx: RunContextWrapper[TodoToolContext], todos: list[TodoItem]) -> str: """Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user. It also helps the user understand the progress of the task and overall progress of their requests. diff --git a/src/vibecore/tools/webfetch/tools.py b/src/vibecore/tools/webfetch/tools.py index 90acfa3..c224655 100644 --- a/src/vibecore/tools/webfetch/tools.py +++ b/src/vibecore/tools/webfetch/tools.py @@ -2,7 +2,7 @@ from agents import RunContextWrapper, function_tool -from vibecore.context import VibecoreContext +from vibecore.context import BasicToolContext from .executor import fetch_url from .models import WebFetchParams @@ -10,7 +10,7 @@ @function_tool async def webfetch( - ctx: RunContextWrapper[VibecoreContext], + ctx: RunContextWrapper[BasicToolContext], url: str, timeout: int = 30, follow_redirects: bool = True, diff --git a/src/vibecore/tools/websearch/tools.py b/src/vibecore/tools/websearch/tools.py index 607a52a..01c25bc 100644 --- a/src/vibecore/tools/websearch/tools.py +++ b/src/vibecore/tools/websearch/tools.py @@ -2,7 +2,7 @@ from agents import RunContextWrapper, function_tool -from vibecore.context import VibecoreContext +from vibecore.context import BasicToolContext from .executor import perform_websearch from .models import SearchParams @@ -10,7 +10,7 @@ @function_tool async def websearch( - ctx: RunContextWrapper[VibecoreContext], + ctx: RunContextWrapper[BasicToolContext], query: str, max_results: int = 5, region: str | None = None, diff --git a/tests/tools/test_file_tools.py b/tests/tools/test_file_tools.py index ce5e9fd..2ebc0e6 100644 --- a/tests/tools/test_file_tools.py +++ b/tests/tools/test_file_tools.py @@ -6,7 +6,7 @@ import pytest from agents import FunctionTool, RunContextWrapper -from vibecore.context import VibecoreContext +from vibecore.context import DefaultVibecoreContext from vibecore.tools.file.executor import edit_file, multi_edit_file, read_file, write_file from vibecore.tools.file.tools import ( edit as edit_tool, @@ -31,7 +31,7 @@ def mock_context(): # Create a real VibecoreContext with current working directory only allowed_dirs = [Path.cwd()] - mock_ctx.context = VibecoreContext(allowed_directories=allowed_dirs) + mock_ctx.context = DefaultVibecoreContext(allowed_directories=allowed_dirs) return mock_ctx @@ -57,7 +57,7 @@ def context_with_temp_dir(temp_dir): # Create VibecoreContext with the temp directory as allowed allowed_dirs = [Path.cwd(), temp_dir] - mock_ctx.context = VibecoreContext(allowed_directories=allowed_dirs) + mock_ctx.context = DefaultVibecoreContext(allowed_directories=allowed_dirs) return mock_ctx diff --git a/tests/tools/test_python_tool_integration.py b/tests/tools/test_python_tool_integration.py index 40f7007..016fe95 100644 --- a/tests/tools/test_python_tool_integration.py +++ b/tests/tools/test_python_tool_integration.py @@ -1,9 +1,11 @@ """Integration tests for Python execution tool with agents.""" +from typing import cast + import pytest from agents import RunContextWrapper -from vibecore.context import VibecoreContext +from vibecore.context import DefaultVibecoreContext, PythonToolContext from vibecore.tools.python.helpers import execute_python_helper @@ -11,8 +13,8 @@ async def test_execute_python_helper_tool_basic(): """Test the execute_python_helper tool with basic code.""" # Create a mock context - context = VibecoreContext() - run_context = RunContextWrapper(context=context) + context = cast(PythonToolContext, DefaultVibecoreContext()) + run_context = RunContextWrapper[PythonToolContext](context=context) # Execute simple code result = await execute_python_helper(run_context, "print('Hello from tool!')") @@ -24,8 +26,8 @@ async def test_execute_python_helper_tool_basic(): @pytest.mark.asyncio async def test_execute_python_helper_tool_persistent_state(): """Test that state persists across tool calls.""" - context = VibecoreContext() - run_context = RunContextWrapper(context=context) + context = cast(PythonToolContext, DefaultVibecoreContext()) + run_context = RunContextWrapper[PythonToolContext](context=context) # Define a variable result1 = await execute_python_helper(run_context, "test_var = 123") @@ -39,8 +41,8 @@ async def test_execute_python_helper_tool_persistent_state(): @pytest.mark.asyncio async def test_execute_python_helper_tool_error_handling(): """Test error handling in the tool.""" - context = VibecoreContext() # type: ignore - run_context = RunContextWrapper(context=context) + context = cast(PythonToolContext, DefaultVibecoreContext()) + run_context = RunContextWrapper[PythonToolContext](context=context) # Syntax error result = await execute_python_helper(run_context, "print('unclosed") @@ -56,8 +58,8 @@ async def test_execute_python_helper_tool_error_handling(): @pytest.mark.asyncio async def test_execute_python_helper_tool_return_values(): """Test that return values are shown.""" - context = VibecoreContext() # type: ignore - run_context = RunContextWrapper(context=context) + context = cast(PythonToolContext, DefaultVibecoreContext()) + run_context = RunContextWrapper[PythonToolContext](context=context) # Expression that returns a value result = await execute_python_helper(run_context, "2 + 2") @@ -72,8 +74,8 @@ async def test_execute_python_helper_tool_return_values(): @pytest.mark.asyncio async def test_execute_python_helper_tool_complex_code(): """Test complex code execution.""" - context = VibecoreContext() # type: ignore - run_context = RunContextWrapper(context=context) + context = cast(PythonToolContext, DefaultVibecoreContext()) + run_context = RunContextWrapper[PythonToolContext](context=context) # Define and use a function code = """ @@ -93,8 +95,8 @@ def greet(name): @pytest.mark.asyncio async def test_execute_python_helper_tool_imports(): """Test that imports work correctly.""" - context = VibecoreContext() # type: ignore - run_context = RunContextWrapper(context=context) + context = cast(PythonToolContext, DefaultVibecoreContext()) + run_context = RunContextWrapper[PythonToolContext](context=context) # Import a module result = await execute_python_helper(run_context, "import json") @@ -108,8 +110,8 @@ async def test_execute_python_helper_tool_imports(): @pytest.mark.asyncio async def test_execute_python_helper_tool_multiline_output(): """Test multiline output formatting.""" - context = VibecoreContext() # type: ignore - run_context = RunContextWrapper(context=context) + context = cast(PythonToolContext, DefaultVibecoreContext()) + run_context = RunContextWrapper[PythonToolContext](context=context) code = """ for i in range(3): diff --git a/tests/tools/test_shell_tools.py b/tests/tools/test_shell_tools.py index b340cea..c871616 100644 --- a/tests/tools/test_shell_tools.py +++ b/tests/tools/test_shell_tools.py @@ -8,7 +8,7 @@ import pytest from agents import FunctionTool, RunContextWrapper -from vibecore.context import VibecoreContext +from vibecore.context import DefaultVibecoreContext from vibecore.tools.shell.executor import bash_executor, glob_files, grep_files, list_directory from vibecore.tools.shell.tools import bash as bash_tool from vibecore.tools.shell.tools import glob as glob_tool @@ -25,7 +25,7 @@ def mock_context(): # Create a real VibecoreContext with current working directory only allowed_dirs = [Path.cwd()] - mock_ctx.context = VibecoreContext(allowed_directories=allowed_dirs) + mock_ctx.context = DefaultVibecoreContext(allowed_directories=allowed_dirs) return mock_ctx @@ -44,7 +44,7 @@ def context_with_temp_dir(temp_dir): # Create VibecoreContext with the temp directory as allowed allowed_dirs = [Path.cwd(), temp_dir] - mock_ctx.context = VibecoreContext(allowed_directories=allowed_dirs) + mock_ctx.context = DefaultVibecoreContext(allowed_directories=allowed_dirs) return mock_ctx diff --git a/tests/tools/test_todo_tools.py b/tests/tools/test_todo_tools.py index 84b90fa..242bb02 100644 --- a/tests/tools/test_todo_tools.py +++ b/tests/tools/test_todo_tools.py @@ -5,7 +5,7 @@ import pytest from agents import RunContextWrapper -from vibecore.context import VibecoreContext +from vibecore.context import DefaultVibecoreContext from vibecore.tools.todo.manager import TodoManager from vibecore.tools.todo.models import TodoItem, TodoPriority, TodoStatus from vibecore.tools.todo.tools import todo_read as todo_read_tool @@ -30,7 +30,7 @@ def mock_context(): """Create a mock RunContextWrapper with VibecoreContext.""" mock_ctx = MagicMock(spec=RunContextWrapper) # Create a real VibecoreContext - mock_ctx.context = VibecoreContext() + mock_ctx.context = DefaultVibecoreContext() return mock_ctx From 28f3ee6ac9a19e495be2f7269d721ff77f493669 Mon Sep 17 00:00:00 2001 From: Sung-jin Brian Hong Date: Mon, 13 Oct 2025 21:28:31 +0900 Subject: [PATCH 2/2] fix: remove unused vibecore context import --- src/vibecore/flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vibecore/flow.py b/src/vibecore/flow.py index 7de97d7..677fe9d 100644 --- a/src/vibecore/flow.py +++ b/src/vibecore/flow.py @@ -20,7 +20,7 @@ from textual.pilot import Pilot from typing_extensions import TypeVar -from vibecore.context import DefaultVibecoreContext, VibecoreContext +from vibecore.context import DefaultVibecoreContext from vibecore.main import AppIsExiting, VibecoreApp from vibecore.session import JSONLSession from vibecore.settings import settings