alpineagents 0.1.0__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.
@@ -0,0 +1,81 @@
1
+ """alpineagents: an agent looks at the state, thinks, and uses tools when needed, until there is an answer."""
2
+
3
+ from .agent import Agent
4
+ from .blocks import CompactIfFull, acompact_if_full, compact_if_full
5
+ from .errors import (
6
+ AlpineAgentsError,
7
+ AuthError,
8
+ ContextTooLongError,
9
+ MCPConnectionError,
10
+ NoHumanError,
11
+ OutputError,
12
+ ProviderError,
13
+ RateLimitError,
14
+ )
15
+ from .human import Human
16
+ from .loop import Loop, adefault_loop, default_loop, loop
17
+ from .mcp_tools import MCP
18
+ from .models import Anthropic, Model, OpenAICompatible
19
+ from .reporter import Reporter
20
+ from .state import State
21
+ from .terminal import Terminal
22
+ from .tool import Tool, tool
23
+ from .types import (
24
+ ContextChange,
25
+ HistoryEntry,
26
+ Message,
27
+ ModelEvent,
28
+ Price,
29
+ Reply,
30
+ Request,
31
+ ToolCall,
32
+ ToolOutcome,
33
+ ToolSpec,
34
+ Usage,
35
+ )
36
+
37
+ __version__ = "0.1.0"
38
+
39
+ __all__ = [
40
+ # Objects
41
+ "Agent",
42
+ "State",
43
+ "loop",
44
+ "Loop",
45
+ "default_loop",
46
+ "adefault_loop",
47
+ "tool",
48
+ "Tool",
49
+ "compact_if_full",
50
+ "acompact_if_full",
51
+ "MCP",
52
+ "CompactIfFull",
53
+ # Roles and implementations
54
+ "Model",
55
+ "Anthropic",
56
+ "OpenAICompatible",
57
+ "Reporter",
58
+ "Human",
59
+ "Terminal",
60
+ # Data
61
+ "Price",
62
+ "Usage",
63
+ "ToolCall",
64
+ "Reply",
65
+ "Request",
66
+ "Message",
67
+ "ToolSpec",
68
+ "HistoryEntry",
69
+ "ContextChange",
70
+ "ModelEvent",
71
+ "ToolOutcome",
72
+ # Errors
73
+ "AlpineAgentsError",
74
+ "ProviderError",
75
+ "RateLimitError",
76
+ "ContextTooLongError",
77
+ "AuthError",
78
+ "OutputError",
79
+ "NoHumanError",
80
+ "MCPConnectionError",
81
+ ]
alpineagents/_async.py ADDED
@@ -0,0 +1,139 @@
1
+ """Helpers the async API shares (ARCHITECTURE.md "Async API").
2
+
3
+ - ``run_in_thread``: runs a blocking function on a daemon thread and returns an awaitable (not
4
+ ``asyncio.to_thread``: its pool is joined when ``asyncio.run`` ends, so a call that was cancelled and not waited
5
+ for would hold the program until it finishes).
6
+ - ``Relay``: calls callbacks that a worker thread invokes (``on_text``/``on_event``) on the event loop thread and
7
+ waits for them, so their exceptions reach the worker thread as is.
8
+ - ``ASYNC_RUN`` and ``check_sync_call``: a sync Agent method called on the event loop thread of an async run would
9
+ block every other task, so it is reported with the async name to use instead.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import contextvars
16
+ import inspect
17
+ import threading
18
+ from collections.abc import Awaitable, Callable
19
+ from concurrent.futures import Future, wait
20
+ from typing import Any, TypeVar
21
+
22
+ from .errors import fix_message
23
+
24
+ __all__ = ["ASYNC_RUN", "Relay", "check_sync_call", "is_async_callable", "run_in_thread"]
25
+
26
+ T = TypeVar("T")
27
+
28
+ #: The event loop running the current async run (set by ``Agent.arun`` and async loops). ``None`` otherwise.
29
+ ASYNC_RUN: contextvars.ContextVar[asyncio.AbstractEventLoop | None] = contextvars.ContextVar(
30
+ "alpineagents_async_run", default=None
31
+ )
32
+
33
+ #: How long (seconds) a worker thread waits at a time for a relayed callback, so it notices a cancel.
34
+ _POLL = 0.1
35
+
36
+
37
+ def run_in_thread(fn: Callable[..., T], /, *args: Any) -> Awaitable[T]:
38
+ """Runs ``fn(*args)`` on a new daemon thread, with a copy of the current context. Cancelling the awaitable does
39
+ not stop the thread; the result is dropped."""
40
+ future: Future[T] = Future()
41
+ context = contextvars.copy_context()
42
+
43
+ def target() -> None:
44
+ if not future.set_running_or_notify_cancel():
45
+ return
46
+ try:
47
+ result = context.run(fn, *args)
48
+ except BaseException as e:
49
+ future.set_exception(e)
50
+ else:
51
+ future.set_result(result)
52
+
53
+ threading.Thread(target=target, name="alpineagents-worker", daemon=True).start()
54
+ return asyncio.wrap_future(future)
55
+
56
+
57
+ class Relay:
58
+ """Wraps callbacks so a worker thread runs them on ``loop``'s thread and gets their result or exception.
59
+
60
+ After ``close()`` (the awaiting side finished or was cancelled), a wrapped callback raises
61
+ ``CancelledError`` in the worker thread instead of running, which also ends a stream early.
62
+ """
63
+
64
+ def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
65
+ self._loop = loop
66
+ self._closed = False
67
+ self._lock = threading.Lock()
68
+
69
+ def wrap(self, callback: Callable[..., Any] | None) -> Callable[..., Any] | None:
70
+ if callback is None:
71
+ return None
72
+ return lambda *args: self._call(callback, args)
73
+
74
+ def close(self) -> None:
75
+ with self._lock:
76
+ self._closed = True
77
+
78
+ def _call(self, callback: Callable[..., Any], args: tuple[Any, ...]) -> Any:
79
+ done: Future[Any] = Future()
80
+
81
+ def run() -> None:
82
+ # close() runs on this (the loop's) thread, so a callback queued before it is dropped here.
83
+ if self._closed:
84
+ done.cancel()
85
+ return
86
+ if not done.set_running_or_notify_cancel():
87
+ return
88
+ try:
89
+ done.set_result(callback(*args))
90
+ except BaseException as e:
91
+ done.set_exception(e)
92
+
93
+ with self._lock:
94
+ if self._closed:
95
+ raise asyncio.CancelledError()
96
+ try:
97
+ self._loop.call_soon_threadsafe(run)
98
+ except RuntimeError: # the loop is closed
99
+ raise asyncio.CancelledError() from None
100
+ # Poll with wait(), not result(timeout=): a TimeoutError raised by the callback must propagate as is.
101
+ while not wait([done], timeout=_POLL).done:
102
+ if self._closed:
103
+ done.cancel()
104
+ raise asyncio.CancelledError()
105
+ if done.cancelled():
106
+ raise asyncio.CancelledError()
107
+ return done.result()
108
+
109
+
110
+ def check_sync_call(method: str, hint: str = "") -> None:
111
+ """``TypeError`` if ``agent.{method}()`` (sync) is called on the event loop thread of an async run.
112
+
113
+ The sync API still works where a loop is running but no async run is (Jupyter), and on worker threads.
114
+ """
115
+ run_loop = ASYNC_RUN.get()
116
+ if run_loop is None:
117
+ return
118
+ try:
119
+ running = asyncio.get_running_loop()
120
+ except RuntimeError:
121
+ return
122
+ if running is run_loop:
123
+ raise TypeError(
124
+ fix_message(
125
+ f"agent.{method}() was called inside an async run, where it would block the event loop",
126
+ f"Use the async version, await agent.a{method}(...){hint}",
127
+ f"await agent.a{method}(state)" if method != "run" else "answer = await agent.arun(state)",
128
+ )
129
+ )
130
+
131
+
132
+ def is_async_callable(obj: Any) -> bool:
133
+ """Whether calling ``obj`` returns an awaitable: an ``async def`` function (or a partial of one), an async
134
+ ``@loop`` (``is_async``), or an object whose ``__call__`` is ``async def``."""
135
+ if inspect.iscoroutinefunction(obj):
136
+ return True
137
+ if getattr(obj, "is_async", False) is True:
138
+ return True
139
+ return inspect.iscoroutinefunction(getattr(type(obj), "__call__", None))