Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ jobs:

- run: uv sync

- run: uv run ruff format --check src tests
- run: uv run ruff check src tests
- run: uv run mypy src tests

- run: uv run pytest
26 changes: 13 additions & 13 deletions src/vercel_ai_sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
from . import anthropic, mcp, openai, ai_sdk_ui
from . import ai_sdk_ui, anthropic, mcp, openai
from .core.checkpoint import Checkpoint
from .core.hooks import Hook, hook
from .core.llm import LanguageModel

# Re-export core types
from .core.messages import (
HookPart,
Message,
Part,
PartState,
ReasoningPart,
TextPart,
ToolPart,
ToolDelta,
ReasoningPart,
HookPart,
ToolPart,
make_messages,
)
from .core.tools import ToolLike, ToolSchema, Tool, tool
from .core.llm import LanguageModel
from .core.streams import StreamResult, stream
from .core.runtime import (
Runtime,
RunResult,
HookInfo,
stream_step,
stream_loop,
RunResult,
Runtime,
execute_tool,
get_checkpoint,
run,
stream_loop,
stream_step,
)
from .core.hooks import Hook, hook
from .core.checkpoint import Checkpoint
from .core.streams import StreamResult, stream
from .core.tools import Tool, ToolLike, ToolSchema, tool

__all__ = [
# Types
Expand Down
2 changes: 1 addition & 1 deletion src/vercel_ai_sdk/agent/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from . import proto, tools, local, vercel
from . import local, proto, tools, vercel
from .agent import Agent, ToolApproval

__all__ = ["Agent", "ToolApproval", "proto", "tools", "local", "vercel"]
4 changes: 2 additions & 2 deletions src/vercel_ai_sdk/agent/local/fs.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,13 @@ async def bash(self, command: str, *, timeout: int | None = None) -> str:
)
try:
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout)
except asyncio.TimeoutError:
except TimeoutError as exc:
proc.kill()
await proc.communicate()
raise TimeoutError(
f"Command timed out after {timeout}s. "
"Try increasing the timeout or breaking the command into smaller steps."
)
) from exc

output = stdout.decode() if stdout else ""
if proc.returncode != 0:
Expand Down
31 changes: 25 additions & 6 deletions src/vercel_ai_sdk/agent/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any

import vercel_ai_sdk as ai

from . import proto

_filesystem: contextvars.ContextVar[proto.Filesystem] = contextvars.ContextVar(
Expand All @@ -20,20 +21,29 @@ def _fs() -> proto.Filesystem:

@ai.tool
async def read(path: str, offset: int | None = None, limit: int | None = None) -> str:
"""Read a file and return its contents with line numbers. For large files, use offset/limit to paginate."""
"""Read file contents with line numbers.

For large files, use offset/limit to paginate.
"""
return await _fs().read(path, offset=offset, limit=limit)


@ai.tool
async def write(path: str, content: str) -> str:
"""Write content to a file. Creates parent directories automatically. Overwrites existing files."""
"""Write content to a file.

Creates parent directories automatically. Overwrites existing files.
"""
await _fs().write(path, content)
return f"Wrote {len(content)} bytes to {path}"


@ai.tool
async def edit(path: str, old_string: str, new_string: str) -> str:
"""Edit a file by replacing an exact string match. Fails if old_string is not found or appears multiple times."""
"""Edit a file by replacing an exact string match.

Fails if old_string is not found or appears multiple times.
"""
await _fs().edit(path, old_string, new_string)
return f"Edited {path}"

Expand All @@ -45,7 +55,10 @@ async def ls(
pattern: str | None = None,
include_hidden: bool = False,
) -> str:
"""List directory contents recursively. Control depth to balance detail vs overview."""
"""List directory contents recursively.

Control depth to balance detail vs overview.
"""
return await _fs().ls(
path, depth=depth, pattern=pattern, include_hidden=include_hidden
)
Expand All @@ -69,7 +82,10 @@ async def grep(
max_count: int | None = None,
case_sensitive: bool = True,
) -> str:
"""Search file contents using regex (ripgrep syntax). Use include to filter by file pattern (e.g. '*.py')."""
"""Search file contents using regex (ripgrep syntax).

Use include to filter by file pattern (e.g. '*.py').
"""
return await _fs().grep(
pattern,
path=path,
Expand All @@ -82,7 +98,10 @@ async def grep(

@ai.tool
async def bash(command: str, timeout: int | None = None) -> str:
"""Execute a bash command in the workspace. Use timeout (seconds) to limit long-running commands."""
"""Execute a bash command in the workspace.

Use timeout (seconds) to limit long-running commands.
"""
return await _fs().bash(command, timeout=timeout)


Expand Down
2 changes: 1 addition & 1 deletion src/vercel_ai_sdk/agent/vercel/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .filesystem import VercelSandbox, SandboxError, SandboxGoneError
from .filesystem import SandboxError, SandboxGoneError, VercelSandbox

__all__ = ["VercelSandbox", "SandboxError", "SandboxGoneError"]
29 changes: 12 additions & 17 deletions src/vercel_ai_sdk/agent/vercel/filesystem.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
from __future__ import annotations

import asyncio
import contextlib
import logging
import time
from dataclasses import dataclass, field
from typing import Any

import httpx

from vercel.sandbox import AsyncSandbox
from vercel.sandbox.models import WriteFile

from .. import proto

logger = logging.getLogger(__name__)

HOME_DIR = "/home/vercel-sandbox"
Expand All @@ -34,17 +31,16 @@ def _is_gone_error(exc: BaseException) -> bool:
Detect stale/terminated sandbox errors.
Mirrors isSandboxGoneError from agent-sdk.
"""
if isinstance(exc, httpx.HTTPStatusError):
if exc.response.status_code in (410, 422):
return True
if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code in (
410,
422,
):
return True

msg = str(exc)
if "Expected a stream of command data" in msg:
return True
if "Expected a stream of logs" in msg:
return True

return False
return "Expected a stream of logs" in msg


@dataclass
Expand Down Expand Up @@ -151,10 +147,8 @@ async def stop(self) -> None:
"""Stop the sandbox VM."""
if self._sandbox is None:
return
try:
with contextlib.suppress(Exception):
await self._sandbox.stop()
except Exception:
pass
self._sandbox = None
self._sandbox_task = None

Expand Down Expand Up @@ -233,11 +227,11 @@ async def _do_run_command(
sb.run_command(cmd, args or [], cwd=effective_cwd),
timeout=timeout,
)
except asyncio.TimeoutError:
except TimeoutError as exc:
raise TimeoutError(
f"Command timed out after {timeout}s. "
"Try increasing the timeout or breaking the command into smaller steps."
)
) from exc
except Exception as exc:
if _is_gone_error(exc):
raise SandboxGoneError(str(exc)) from exc
Expand Down Expand Up @@ -338,7 +332,8 @@ async def ls(

async def glob(self, pattern: str, *, path: str | None = None) -> list[str]:
root = self._resolve_path(path or ".")
cmd = f"cd {_shell_quote(root)} && find . -path {_shell_quote(f'./{pattern}')} 2>/dev/null | sort"
find_expr = _shell_quote(f"./{pattern}")
cmd = f"cd {_shell_quote(root)} && find . -path {find_expr} 2>/dev/null | sort"
stdout, _, _ = await self._run_command("bash", ["-c", cmd])
if not stdout.strip():
return []
Expand Down
4 changes: 2 additions & 2 deletions src/vercel_ai_sdk/ai_sdk_ui/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .adapter import to_ui_message_stream, filter_by_label, to_sse_stream, to_messages
from .ui_message import UIMessage
from .adapter import filter_by_label, to_messages, to_sse_stream, to_ui_message_stream
from .protocol import UI_MESSAGE_STREAM_HEADERS
from .ui_message import UIMessage

__all__ = [
"to_ui_message_stream",
Expand Down
1 change: 0 additions & 1 deletion src/vercel_ai_sdk/ai_sdk_ui/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import dataclasses
from typing import Any, Literal


# necessary headers for the streaming integration to work
UI_MESSAGE_STREAM_HEADERS = {
"Content-Type": "text/event-stream",
Expand Down
5 changes: 4 additions & 1 deletion src/vercel_ai_sdk/ai_sdk_ui/ui_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ class UIToolPart(pydantic.BaseModel):

@property
def tool_name(self) -> str:
"""Extract tool name from the type string (e.g., 'tool-get_weather' -> 'get_weather')."""
"""Extract tool name from the type string.

E.g., 'tool-get_weather' -> 'get_weather'.
"""
if self.type.startswith("tool-"):
return self.type[5:]
return self.type
Expand Down
4 changes: 2 additions & 2 deletions src/vercel_ai_sdk/anthropic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ async def stream_events(
self,
messages: list[core.messages.Message],
tools: Sequence[core.tools.ToolLike] | None = None,
) -> AsyncGenerator[core.llm.StreamEvent, None]:
) -> AsyncGenerator[core.llm.StreamEvent]:
"""Yield raw stream events from Anthropic API."""
system_prompt, anthropic_messages = _messages_to_anthropic(messages)
anthropic_tools = _tools_to_anthropic(tools) if tools else None
Expand Down Expand Up @@ -208,7 +208,7 @@ async def stream(
self,
messages: list[core.messages.Message],
tools: Sequence[core.tools.ToolLike] | None = None,
) -> AsyncGenerator[core.messages.Message, None]:
) -> AsyncGenerator[core.messages.Message]:
"""Stream Messages (uses StreamProcessor internally)."""
handler = core.llm.StreamHandler()
async for event in self.stream_events(messages, tools):
Expand Down
2 changes: 1 addition & 1 deletion src/vercel_ai_sdk/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from . import messages, tools, runtime, hooks, llm
from . import hooks, llm, messages, runtime, tools

__all__ = ["messages", "tools", "runtime", "hooks", "llm"]
2 changes: 1 addition & 1 deletion src/vercel_ai_sdk/core/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ async def stream(
self,
messages: list[messages_.Message],
tools: Sequence[tools_.ToolLike] | None = None,
) -> AsyncGenerator[messages_.Message, None]:
) -> AsyncGenerator[messages_.Message]:
raise NotImplementedError
yield

Expand Down
21 changes: 13 additions & 8 deletions src/vercel_ai_sdk/openai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ def _messages_to_openai(messages: list[core.messages.Message]) -> list[dict[str,
},
}
)
# If tool has completed (success or error), collect for tool messages
# If tool has completed (success or error),
# collect for tool messages
if part.status in ("result", "error") and part.result is not None:
tool_results.append(
{
Expand Down Expand Up @@ -114,13 +115,17 @@ def __init__(
"""Initialize OpenAI model adapter.

Args:
model: Model identifier (e.g., 'openai/gpt-5.2', 'anthropic/claude-sonnet-4.5')
base_url: API base URL (e.g., 'https://ai-gateway.vercel.sh/v1')
model: Model identifier
(e.g., 'openai/gpt-5.2', 'anthropic/claude-sonnet-4.5')
base_url: API base URL
(e.g., 'https://ai-gateway.vercel.sh/v1')
api_key: API key for authentication
thinking: Enable reasoning/thinking output
budget_tokens: Max tokens for reasoning (mutually exclusive with reasoning_effort)
reasoning_effort: Effort level - 'none', 'minimal', 'low', 'medium', 'high', 'xhigh'
(mutually exclusive with budget_tokens)
budget_tokens: Max tokens for reasoning
(mutually exclusive with reasoning_effort)
reasoning_effort: Effort level — 'none', 'minimal',
'low', 'medium', 'high', 'xhigh'
(mutually exclusive with budget_tokens)
"""
self._model = model
self._thinking = thinking
Expand All @@ -133,7 +138,7 @@ async def stream_events(
self,
messages: list[core.messages.Message],
tools: Sequence[core.tools.ToolLike] | None = None,
) -> AsyncGenerator[core.llm.StreamEvent, None]:
) -> AsyncGenerator[core.llm.StreamEvent]:
"""Yield raw stream events from OpenAI API."""
openai_messages = _messages_to_openai(messages)
openai_tools = _tools_to_openai(tools) if tools else None
Expand Down Expand Up @@ -244,7 +249,7 @@ async def stream(
self,
messages: list[core.messages.Message],
tools: Sequence[core.tools.ToolLike] | None = None,
) -> AsyncGenerator[core.messages.Message, None]:
) -> AsyncGenerator[core.messages.Message]:
"""Stream Messages (uses StreamHandler internally)."""
handler = core.llm.StreamHandler()
async for event in self.stream_events(messages, tools):
Expand Down
Loading