runlet 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.
runlet/__init__.py ADDED
@@ -0,0 +1,87 @@
1
+ """Runlet: a tiny observable runtime for Python agents."""
2
+
3
+ from runlet.core import (
4
+ Agent,
5
+ CancellationError,
6
+ CompositeEventSink,
7
+ ContextOverflowError,
8
+ InMemoryObserver,
9
+ HookError,
10
+ InternalRuntimeError,
11
+ Message,
12
+ ModelCapabilities,
13
+ ModelError,
14
+ ModelRequest,
15
+ ModelResponse,
16
+ ModelStreamEvent,
17
+ ProviderStreamEvent,
18
+ PolicyStop,
19
+ RunContext,
20
+ RunResult,
21
+ RunletError,
22
+ RuntimeEvent,
23
+ StateError,
24
+ ToolCall,
25
+ ToolError,
26
+ ToolResult,
27
+ Usage,
28
+ )
29
+ from runlet.integrations import BaseHook, HookRunner, ToolContext, ToolSpec, execute_tool_call, tool
30
+ from runlet.runtime import (
31
+ ContextManager,
32
+ ContextPolicy,
33
+ HookPolicy,
34
+ InMemoryStateStore,
35
+ RunPolicy,
36
+ Runtime,
37
+ SimpleTokenEstimator,
38
+ StateScope,
39
+ TokenEstimate,
40
+ ToolPolicy,
41
+ )
42
+
43
+ __version__ = "0.1.0"
44
+
45
+ __all__ = [
46
+ "Agent",
47
+ "CancellationError",
48
+ "CompositeEventSink",
49
+ "ContextManager",
50
+ "ContextPolicy",
51
+ "ContextOverflowError",
52
+ "BaseHook",
53
+ "HookError",
54
+ "HookRunner",
55
+ "HookPolicy",
56
+ "InMemoryObserver",
57
+ "InternalRuntimeError",
58
+ "Message",
59
+ "ModelCapabilities",
60
+ "ModelError",
61
+ "ModelRequest",
62
+ "ModelResponse",
63
+ "ModelStreamEvent",
64
+ "ProviderStreamEvent",
65
+ "PolicyStop",
66
+ "RunContext",
67
+ "RunPolicy",
68
+ "RunResult",
69
+ "RunletError",
70
+ "Runtime",
71
+ "RuntimeEvent",
72
+ "SimpleTokenEstimator",
73
+ "StateScope",
74
+ "StateError",
75
+ "InMemoryStateStore",
76
+ "TokenEstimate",
77
+ "ToolCall",
78
+ "ToolContext",
79
+ "ToolError",
80
+ "ToolResult",
81
+ "ToolPolicy",
82
+ "ToolSpec",
83
+ "Usage",
84
+ "__version__",
85
+ "execute_tool_call",
86
+ "tool",
87
+ ]
@@ -0,0 +1,52 @@
1
+ from runlet.core.agent import Agent
2
+ from runlet.core.errors import (
3
+ CancellationError,
4
+ ContextOverflowError,
5
+ HookError,
6
+ InternalRuntimeError,
7
+ ModelError,
8
+ PolicyStop,
9
+ RunletError,
10
+ StateError,
11
+ ToolError,
12
+ )
13
+ from runlet.core.events import CompositeEventSink, EventSink, InMemoryObserver, RuntimeEvent
14
+ from runlet.core.messages import Message, ToolCall, ToolResult
15
+ from runlet.core.models import (
16
+ ModelCapabilities,
17
+ ModelProvider,
18
+ ModelRequest,
19
+ ModelResponse,
20
+ ModelStreamEvent,
21
+ ProviderStreamEvent,
22
+ )
23
+ from runlet.core.runs import RunContext, RunResult, Usage
24
+
25
+ __all__ = [
26
+ "Agent",
27
+ "CancellationError",
28
+ "CompositeEventSink",
29
+ "ContextOverflowError",
30
+ "EventSink",
31
+ "HookError",
32
+ "InMemoryObserver",
33
+ "InternalRuntimeError",
34
+ "Message",
35
+ "ModelCapabilities",
36
+ "ModelError",
37
+ "ModelProvider",
38
+ "ModelRequest",
39
+ "ModelResponse",
40
+ "ModelStreamEvent",
41
+ "ProviderStreamEvent",
42
+ "PolicyStop",
43
+ "RunContext",
44
+ "RunResult",
45
+ "RunletError",
46
+ "RuntimeEvent",
47
+ "StateError",
48
+ "ToolCall",
49
+ "ToolError",
50
+ "ToolResult",
51
+ "Usage",
52
+ ]
runlet/core/agent.py ADDED
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ def _metadata_map() -> dict[str, Any]:
8
+ return {}
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Agent:
13
+ name: str
14
+ instructions: str
15
+ model: Any
16
+ tools: tuple[Any, ...] = ()
17
+ hooks: tuple[Any, ...] = ()
18
+ metadata: dict[str, Any] = field(default_factory=_metadata_map)
runlet/core/errors.py ADDED
@@ -0,0 +1,34 @@
1
+ class RunletError(Exception):
2
+ code = "runlet_error"
3
+
4
+
5
+ class ModelError(RunletError):
6
+ code = "model_error"
7
+
8
+
9
+ class ToolError(RunletError):
10
+ code = "tool_error"
11
+
12
+
13
+ class ContextOverflowError(RunletError):
14
+ code = "context_overflow"
15
+
16
+
17
+ class HookError(RunletError):
18
+ code = "hook_error"
19
+
20
+
21
+ class PolicyStop(RunletError):
22
+ code = "policy_stop"
23
+
24
+
25
+ class StateError(RunletError):
26
+ code = "state_error"
27
+
28
+
29
+ class CancellationError(RunletError):
30
+ code = "cancelled"
31
+
32
+
33
+ class InternalRuntimeError(RunletError):
34
+ code = "internal_runtime_error"
runlet/core/events.py ADDED
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime, timezone
5
+ from typing import Any, Protocol
6
+ from uuid import uuid4
7
+
8
+
9
+ def utc_now() -> datetime:
10
+ return datetime.now(timezone.utc)
11
+
12
+
13
+ def _event_map() -> dict[str, Any]:
14
+ return {}
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class RuntimeEvent:
19
+ type: str
20
+ run_id: str
21
+ id: str = field(default_factory=lambda: f"evt_{uuid4().hex}")
22
+ timestamp: datetime = field(default_factory=utc_now)
23
+ step_id: str | None = None
24
+ span_id: str | None = None
25
+ parent_span_id: str | None = None
26
+ agent_name: str | None = None
27
+ severity: str = "info"
28
+ payload: dict[str, Any] = field(default_factory=_event_map)
29
+ attributes: dict[str, Any] = field(default_factory=_event_map)
30
+
31
+
32
+ class EventSink(Protocol):
33
+ async def emit(self, event: RuntimeEvent) -> None:
34
+ ...
35
+
36
+
37
+ class InMemoryObserver:
38
+ def __init__(self) -> None:
39
+ self.events: list[RuntimeEvent] = []
40
+
41
+ async def emit(self, event: RuntimeEvent) -> None:
42
+ self.events.append(event)
43
+
44
+
45
+ class CompositeEventSink:
46
+ def __init__(self, sinks: list[EventSink] | tuple[EventSink, ...]) -> None:
47
+ self.sinks = tuple(sinks)
48
+
49
+ async def emit(self, event: RuntimeEvent) -> None:
50
+ for sink in self.sinks:
51
+ await sink.emit(event)
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ def _string_map() -> dict[str, Any]:
8
+ return {}
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Message:
13
+ role: str
14
+ text: str
15
+ metadata: dict[str, Any] = field(default_factory=_string_map)
16
+
17
+ @classmethod
18
+ def system(cls, text: str, metadata: dict[str, Any] | None = None) -> "Message":
19
+ return cls(role="system", text=text, metadata=metadata or {})
20
+
21
+ @classmethod
22
+ def user(cls, text: str, metadata: dict[str, Any] | None = None) -> "Message":
23
+ return cls(role="user", text=text, metadata=metadata or {})
24
+
25
+ @classmethod
26
+ def assistant(cls, text: str, metadata: dict[str, Any] | None = None) -> "Message":
27
+ return cls(role="assistant", text=text, metadata=metadata or {})
28
+
29
+ @classmethod
30
+ def tool(cls, text: str, metadata: dict[str, Any] | None = None) -> "Message":
31
+ return cls(role="tool", text=text, metadata=metadata or {})
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class ToolCall:
36
+ id: str
37
+ name: str
38
+ arguments: dict[str, Any] = field(default_factory=_string_map)
39
+ metadata: dict[str, Any] = field(default_factory=_string_map)
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class ToolResult:
44
+ call_id: str
45
+ name: str
46
+ content: str
47
+ metadata: dict[str, Any] = field(default_factory=_string_map)
runlet/core/models.py ADDED
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, Protocol
6
+
7
+ from runlet.core.messages import Message, ToolCall
8
+ from runlet.core.runs import Usage
9
+
10
+
11
+ def _tool_list() -> list[Any]:
12
+ return []
13
+
14
+
15
+ def _metadata_map() -> dict[str, Any]:
16
+ return {}
17
+
18
+
19
+ def _tool_call_list() -> list[ToolCall]:
20
+ return []
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ModelCapabilities:
25
+ model_name: str
26
+ context_window: int
27
+ supports_tools: bool = True
28
+ supports_parallel_tool_calls: bool = False
29
+ supports_streaming: bool = False
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class ModelRequest:
34
+ messages: list[Message]
35
+ tools: list[Any] = field(default_factory=_tool_list)
36
+ metadata: dict[str, Any] = field(default_factory=_metadata_map)
37
+ options: dict[str, Any] = field(default_factory=_metadata_map)
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class ModelResponse:
42
+ message: Message
43
+ tool_calls: list[ToolCall] = field(default_factory=_tool_call_list)
44
+ usage: Usage = field(default_factory=Usage)
45
+ final: bool = True
46
+ raw: Any = None
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class ModelStreamEvent:
51
+ delta: str = ""
52
+ tool_call: ToolCall | None = None
53
+ usage: Usage | None = None
54
+ final: bool = False
55
+ raw: Any = None
56
+
57
+
58
+ def _arguments_map() -> dict[str, Any]:
59
+ return {}
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class ProviderStreamEvent:
64
+ kind: str
65
+ delta: str = ""
66
+ call_id: str | None = None
67
+ name: str | None = None
68
+ arguments_delta: str = ""
69
+ arguments: dict[str, Any] = field(default_factory=_arguments_map)
70
+ usage: Usage | None = None
71
+ raw: Any = None
72
+
73
+ @classmethod
74
+ def text_delta(cls, delta: str, raw: Any = None) -> "ProviderStreamEvent":
75
+ return cls(kind="text_delta", delta=delta, raw=raw)
76
+
77
+ @classmethod
78
+ def tool_call_delta(
79
+ cls,
80
+ call_id: str,
81
+ name: str | None,
82
+ arguments_delta: str,
83
+ raw: Any = None,
84
+ ) -> "ProviderStreamEvent":
85
+ return cls(
86
+ kind="tool_call_delta",
87
+ call_id=call_id,
88
+ name=name,
89
+ arguments_delta=arguments_delta,
90
+ raw=raw,
91
+ )
92
+
93
+ @classmethod
94
+ def tool_call_completed(
95
+ cls,
96
+ call_id: str,
97
+ name: str,
98
+ arguments: dict[str, Any],
99
+ raw: Any = None,
100
+ ) -> "ProviderStreamEvent":
101
+ return cls(
102
+ kind="tool_call_completed",
103
+ call_id=call_id,
104
+ name=name,
105
+ arguments=dict(arguments),
106
+ raw=raw,
107
+ )
108
+
109
+ @classmethod
110
+ def message_completed(cls, raw: Any = None) -> "ProviderStreamEvent":
111
+ return cls(kind="message_completed", raw=raw)
112
+
113
+ @classmethod
114
+ def usage_event(cls, usage: Usage, raw: Any = None) -> "ProviderStreamEvent":
115
+ return cls(kind="usage", usage=usage, raw=raw)
116
+
117
+ @classmethod
118
+ def completed(cls, raw: Any = None) -> "ProviderStreamEvent":
119
+ return cls(kind="completed", raw=raw)
120
+
121
+
122
+ class ModelProvider(Protocol):
123
+ async def complete(self, request: ModelRequest) -> ModelResponse:
124
+ ...
125
+
126
+ async def stream(self, request: ModelRequest) -> AsyncIterator[ProviderStreamEvent | ModelStreamEvent]:
127
+ ...
128
+
129
+ async def capabilities(self) -> ModelCapabilities:
130
+ ...
runlet/core/runs.py ADDED
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ from runlet.core.agent import Agent
7
+ from runlet.core.messages import Message
8
+
9
+
10
+ def _message_list() -> list[Message]:
11
+ return []
12
+
13
+
14
+ def _metadata_map() -> dict[str, Any]:
15
+ return {}
16
+
17
+
18
+ def _state_map() -> dict[str, Any]:
19
+ return {}
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class Usage:
24
+ input_tokens: int = 0
25
+ output_tokens: int = 0
26
+ source: str = "actual"
27
+
28
+ @property
29
+ def total_tokens(self) -> int:
30
+ return self.input_tokens + self.output_tokens
31
+
32
+
33
+ @dataclass
34
+ class RunContext:
35
+ run_id: str
36
+ agent: Agent
37
+ messages: list[Message] = field(default_factory=_message_list)
38
+ metadata: dict[str, Any] = field(default_factory=_metadata_map)
39
+ state: dict[str, Any] = field(default_factory=_state_map)
40
+ usage: Usage = field(default_factory=Usage)
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class RunResult:
45
+ run_id: str
46
+ status: str
47
+ output: str | None = None
48
+ messages: tuple[Message, ...] = ()
49
+ usage: Usage = field(default_factory=Usage)
50
+ error: str | None = None
51
+
52
+ @classmethod
53
+ def completed(
54
+ cls,
55
+ run_id: str,
56
+ output: str,
57
+ usage: Usage | None = None,
58
+ messages: tuple[Message, ...] = (),
59
+ ) -> "RunResult":
60
+ return cls(
61
+ run_id=run_id,
62
+ status="completed",
63
+ output=output,
64
+ usage=usage or Usage(),
65
+ messages=messages,
66
+ )
67
+
68
+ @classmethod
69
+ def failed(
70
+ cls,
71
+ run_id: str,
72
+ error: str,
73
+ usage: Usage | None = None,
74
+ messages: tuple[Message, ...] = (),
75
+ ) -> "RunResult":
76
+ return cls(
77
+ run_id=run_id,
78
+ status="failed",
79
+ usage=usage or Usage(),
80
+ error=error,
81
+ messages=messages,
82
+ )
@@ -0,0 +1,13 @@
1
+ from runlet.integrations.hooks import BaseHook, HookRunner
2
+ from runlet.integrations.tools import ToolContext, ToolHandler, ToolSpec, execute_tool_call, tool, validate_arguments
3
+
4
+ __all__ = [
5
+ "BaseHook",
6
+ "HookRunner",
7
+ "ToolContext",
8
+ "ToolHandler",
9
+ "ToolSpec",
10
+ "execute_tool_call",
11
+ "tool",
12
+ "validate_arguments",
13
+ ]
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class BaseHook:
7
+ enabled = True
8
+
9
+ def is_enabled(self, context: Any) -> bool:
10
+ return bool(self.enabled)
11
+
12
+ async def before_model_request(self, request: Any, context: Any) -> Any:
13
+ return request
14
+
15
+ async def after_model_response(self, response: Any, context: Any) -> Any:
16
+ return response
17
+
18
+ async def before_tool_call(self, call: Any, context: Any) -> Any:
19
+ return call
20
+
21
+ async def after_tool_result(self, result: Any, context: Any) -> Any:
22
+ return result
23
+
24
+
25
+ class HookRunner:
26
+ def __init__(self, hooks: list[BaseHook] | tuple[BaseHook, ...]) -> None:
27
+ self.hooks = tuple(hooks)
28
+
29
+ def _enabled_hooks(self, context: Any) -> list[BaseHook]:
30
+ return [hook for hook in self.hooks if hook.is_enabled(context)]
31
+
32
+ async def before_model_request(self, request: Any, context: Any) -> Any:
33
+ for hook in self._enabled_hooks(context):
34
+ request = await hook.before_model_request(request, context)
35
+ return request
36
+
37
+ async def after_model_response(self, response: Any, context: Any) -> Any:
38
+ for hook in self._enabled_hooks(context):
39
+ response = await hook.after_model_response(response, context)
40
+ return response
41
+
42
+ async def before_tool_call(self, call: Any, context: Any) -> Any:
43
+ for hook in self._enabled_hooks(context):
44
+ call = await hook.before_tool_call(call, context)
45
+ return call
46
+
47
+ async def after_tool_result(self, result: Any, context: Any) -> Any:
48
+ for hook in self._enabled_hooks(context):
49
+ result = await hook.after_tool_result(result, context)
50
+ return result
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, Awaitable, Callable
6
+
7
+ from runlet.core.messages import ToolCall, ToolResult
8
+
9
+
10
+ ToolHandler = Callable[[dict[str, Any], "ToolContext"], Awaitable[str]]
11
+
12
+
13
+ def _metadata_map() -> dict[str, Any]:
14
+ return {}
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ToolContext:
19
+ run_id: str
20
+ metadata: dict[str, Any] = field(default_factory=_metadata_map)
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ToolSpec:
25
+ name: str
26
+ description: str
27
+ input_schema: dict[str, Any]
28
+ handler: ToolHandler
29
+
30
+
31
+ def _annotation_to_schema(annotation: Any) -> dict[str, str]:
32
+ if annotation is int:
33
+ return {"type": "integer"}
34
+ if annotation is float:
35
+ return {"type": "number"}
36
+ if annotation is bool:
37
+ return {"type": "boolean"}
38
+ return {"type": "string"}
39
+
40
+
41
+ def tool(func: Callable[..., Awaitable[str]]) -> ToolSpec:
42
+ signature = inspect.signature(func)
43
+ properties: dict[str, dict[str, str]] = {}
44
+ required: list[str] = []
45
+ for name, parameter in signature.parameters.items():
46
+ required.append(name)
47
+ properties[name] = _annotation_to_schema(parameter.annotation)
48
+
49
+ async def handler(arguments: dict[str, Any], context: ToolContext) -> str:
50
+ del context
51
+ return await func(**arguments)
52
+
53
+ return ToolSpec(
54
+ name=func.__name__,
55
+ description=(func.__doc__ or "").strip(),
56
+ input_schema={"type": "object", "required": required, "properties": properties},
57
+ handler=handler,
58
+ )
59
+
60
+
61
+ def validate_arguments(schema: dict[str, Any], arguments: dict[str, Any]) -> None:
62
+ for name in schema.get("required", []):
63
+ if name not in arguments:
64
+ raise ValueError(f"Missing required tool argument: {name}")
65
+
66
+
67
+ async def execute_tool_call(
68
+ call: ToolCall,
69
+ tools: dict[str, ToolSpec],
70
+ context: ToolContext,
71
+ ) -> ToolResult:
72
+ if call.name not in tools:
73
+ raise ValueError(f"Tool not found: {call.name}")
74
+ spec = tools[call.name]
75
+ validate_arguments(spec.input_schema, call.arguments)
76
+ content = await spec.handler(call.arguments, context)
77
+ return ToolResult(call_id=call.id, name=call.name, content=str(content))
@@ -0,0 +1,5 @@
1
+ from runlet.providers.openai import OpenAIResponsesProvider
2
+
3
+ __all__ = [
4
+ "OpenAIResponsesProvider",
5
+ ]