feat(telegram,core,backends,storage): telegram frontend with inbox, outbox, drafts and question buttons
This commit is contained in:
@@ -41,6 +41,7 @@ import sys
|
||||
import tempfile
|
||||
import time
|
||||
import uuid
|
||||
import warnings
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -49,9 +50,12 @@ from typing import TYPE_CHECKING, Any, Self, cast
|
||||
import claude_agent_sdk
|
||||
from claude_agent_sdk import (
|
||||
AssistantMessage,
|
||||
CanUseToolShadowedWarning,
|
||||
ClaudeAgentOptions,
|
||||
ClaudeSDKClient,
|
||||
MirrorErrorMessage,
|
||||
PermissionResultAllow,
|
||||
PermissionResultDeny,
|
||||
ResultMessage,
|
||||
StreamEvent,
|
||||
TextBlock,
|
||||
@@ -91,7 +95,12 @@ if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequence
|
||||
|
||||
from anthropic.types import MessageParam
|
||||
from claude_agent_sdk import McpSdkServerConfig, SessionStore
|
||||
from claude_agent_sdk import (
|
||||
McpSdkServerConfig,
|
||||
PermissionResult,
|
||||
SessionStore,
|
||||
ToolPermissionContext,
|
||||
)
|
||||
|
||||
from beaver_gateway.agents.base import BaseAgent
|
||||
from beaver_gateway.agents.claude import ClaudeAgent
|
||||
@@ -100,6 +109,12 @@ if TYPE_CHECKING:
|
||||
|
||||
_log = logging.getLogger("beaver_gateway.backends.claude_sdk")
|
||||
|
||||
# §3.7: in bypass the callback only ever sees AskUserQuestion, and that is
|
||||
# exactly the one we want - the SDK's warning about the rest is noise here.
|
||||
warnings.filterwarnings("ignore", category=CanUseToolShadowedWarning)
|
||||
|
||||
ASK_TOOL = "AskUserQuestion"
|
||||
|
||||
__all__ = [
|
||||
"ClaudeSdkBackend",
|
||||
"RunnerConfig",
|
||||
@@ -151,6 +166,11 @@ ClientFactory = "Callable[[ClaudeAgentOptions], SessionClient]"
|
||||
UsageSink = "Callable[[UsageEvent], Awaitable[None]]"
|
||||
ToolServerFactory = "Callable[[str, str], McpSdkServerConfig | None]"
|
||||
"""``(conversation_key, kind) -> in-process MCP server config`` or ``None``."""
|
||||
Asker = "Callable[[str, dict[str, Any]], Awaitable[str]]"
|
||||
"""``(conversation_key, AskUserQuestion input) -> text the model reads as the
|
||||
tool result``. The only channel an answer has in bypass mode is
|
||||
``PermissionResultDeny.message`` (spike S1, s05): ``updated_input`` never
|
||||
reaches the model."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -197,6 +217,7 @@ class ClaudeSdkBackend:
|
||||
work_dir: Path | None = None,
|
||||
pool: SessionPool | None = None,
|
||||
tool_server: Callable[[str, str], McpSdkServerConfig | None] | None = None,
|
||||
asker: Callable[[str, dict[str, Any]], Awaitable[str]] | None = None,
|
||||
) -> None:
|
||||
self._agent = agent
|
||||
self._store = session_store
|
||||
@@ -208,6 +229,7 @@ class ClaudeSdkBackend:
|
||||
self._mcp_disallowed = _mcp_disallowed(agent, mcp_tool_names or {})
|
||||
self._pool = pool if pool is not None else SessionPool()
|
||||
self._tool_server = tool_server
|
||||
self._asker = asker if ASK_TOOL not in agent.options.disallowed_tools else None
|
||||
self._uid, self._gid = _resolve_ids(self._runner.user)
|
||||
self._wrapper: Path | None = None
|
||||
|
||||
@@ -553,6 +575,7 @@ class ClaudeSdkBackend:
|
||||
env=env,
|
||||
cli_path=str(self._exec_wrapper(extra_keep=tuple(env))),
|
||||
include_partial_messages=opt.include_partial_messages,
|
||||
can_use_tool=self._can_use_tool(key) if self._asker else None,
|
||||
session_store=self._store,
|
||||
session_store_flush=cast("Any", opt.session_store_flush),
|
||||
resume=resume,
|
||||
@@ -564,6 +587,30 @@ class ClaudeSdkBackend:
|
||||
),
|
||||
)
|
||||
|
||||
def _can_use_tool(
|
||||
self, key: str
|
||||
) -> Callable[
|
||||
[str, dict[str, Any], ToolPermissionContext], Awaitable[PermissionResult]
|
||||
]:
|
||||
asker = self._asker
|
||||
|
||||
async def can_use_tool(
|
||||
name: str, tool_input: dict[str, Any], _ctx: ToolPermissionContext
|
||||
) -> PermissionResult:
|
||||
if name != ASK_TOOL or asker is None:
|
||||
return PermissionResultAllow()
|
||||
live = self._pool.get(key)
|
||||
if live is not None:
|
||||
live.pending_question = True
|
||||
try:
|
||||
message = await asker(key, tool_input)
|
||||
finally:
|
||||
if live is not None:
|
||||
live.pending_question = False
|
||||
return PermissionResultDeny(message=message)
|
||||
|
||||
return can_use_tool
|
||||
|
||||
def _plugins(self) -> list[dict[str, str]]:
|
||||
plugins: list[dict[str, str]] = []
|
||||
root = self._work_dir / "plugins" / self._agent.name
|
||||
|
||||
Reference in New Issue
Block a user