agent-runtime-core 0.1.0__tar.gz

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.
Files changed (36) hide show
  1. agent_runtime_core-0.1.0/LICENSE +21 -0
  2. agent_runtime_core-0.1.0/PKG-INFO +75 -0
  3. agent_runtime_core-0.1.0/README.md +32 -0
  4. agent_runtime_core-0.1.0/agent_runtime/__init__.py +110 -0
  5. agent_runtime_core-0.1.0/agent_runtime/config.py +172 -0
  6. agent_runtime_core-0.1.0/agent_runtime/events/__init__.py +55 -0
  7. agent_runtime_core-0.1.0/agent_runtime/events/base.py +86 -0
  8. agent_runtime_core-0.1.0/agent_runtime/events/memory.py +89 -0
  9. agent_runtime_core-0.1.0/agent_runtime/events/redis.py +185 -0
  10. agent_runtime_core-0.1.0/agent_runtime/events/sqlite.py +168 -0
  11. agent_runtime_core-0.1.0/agent_runtime/interfaces.py +390 -0
  12. agent_runtime_core-0.1.0/agent_runtime/llm/__init__.py +83 -0
  13. agent_runtime_core-0.1.0/agent_runtime/llm/anthropic.py +237 -0
  14. agent_runtime_core-0.1.0/agent_runtime/llm/litellm_client.py +175 -0
  15. agent_runtime_core-0.1.0/agent_runtime/llm/openai.py +220 -0
  16. agent_runtime_core-0.1.0/agent_runtime/queue/__init__.py +55 -0
  17. agent_runtime_core-0.1.0/agent_runtime/queue/base.py +167 -0
  18. agent_runtime_core-0.1.0/agent_runtime/queue/memory.py +184 -0
  19. agent_runtime_core-0.1.0/agent_runtime/queue/redis.py +453 -0
  20. agent_runtime_core-0.1.0/agent_runtime/queue/sqlite.py +420 -0
  21. agent_runtime_core-0.1.0/agent_runtime/registry.py +74 -0
  22. agent_runtime_core-0.1.0/agent_runtime/runner.py +403 -0
  23. agent_runtime_core-0.1.0/agent_runtime/state/__init__.py +53 -0
  24. agent_runtime_core-0.1.0/agent_runtime/state/base.py +69 -0
  25. agent_runtime_core-0.1.0/agent_runtime/state/memory.py +51 -0
  26. agent_runtime_core-0.1.0/agent_runtime/state/redis.py +109 -0
  27. agent_runtime_core-0.1.0/agent_runtime/state/sqlite.py +158 -0
  28. agent_runtime_core-0.1.0/agent_runtime/tracing/__init__.py +47 -0
  29. agent_runtime_core-0.1.0/agent_runtime/tracing/langfuse.py +119 -0
  30. agent_runtime_core-0.1.0/agent_runtime/tracing/noop.py +34 -0
  31. agent_runtime_core-0.1.0/pyproject.toml +70 -0
  32. agent_runtime_core-0.1.0/tests/__init__.py +1 -0
  33. agent_runtime_core-0.1.0/tests/test_events.py +75 -0
  34. agent_runtime_core-0.1.0/tests/test_imports.py +80 -0
  35. agent_runtime_core-0.1.0/tests/test_queue.py +95 -0
  36. agent_runtime_core-0.1.0/tests/test_state.py +68 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chris Olstrom
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-runtime-core
3
+ Version: 0.1.0
4
+ Summary: Framework-agnostic Python library for executing AI agents with consistent patterns
5
+ Project-URL: Homepage, https://github.com/colstrom/agent_runtime
6
+ Project-URL: Repository, https://github.com/colstrom/agent_runtime
7
+ Author: Chris Olstrom
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,ai,async,llm,runtime
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.11
20
+ Provides-Extra: all
21
+ Requires-Dist: anthropic>=0.18.0; extra == 'all'
22
+ Requires-Dist: langfuse>=2.0.0; extra == 'all'
23
+ Requires-Dist: litellm>=1.0.0; extra == 'all'
24
+ Requires-Dist: openai>=1.0.0; extra == 'all'
25
+ Requires-Dist: redis>=5.0.0; extra == 'all'
26
+ Provides-Extra: anthropic
27
+ Requires-Dist: anthropic>=0.18.0; extra == 'anthropic'
28
+ Provides-Extra: dev
29
+ Requires-Dist: mypy>=1.0.0; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
31
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
32
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
33
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
34
+ Provides-Extra: langfuse
35
+ Requires-Dist: langfuse>=2.0.0; extra == 'langfuse'
36
+ Provides-Extra: litellm
37
+ Requires-Dist: litellm>=1.0.0; extra == 'litellm'
38
+ Provides-Extra: openai
39
+ Requires-Dist: openai>=1.0.0; extra == 'openai'
40
+ Provides-Extra: redis
41
+ Requires-Dist: redis>=5.0.0; extra == 'redis'
42
+ Description-Content-Type: text/markdown
43
+
44
+ # agent_runtime
45
+
46
+ Framework-agnostic agent runtime library for Python.
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install agent_runtime
52
+ ```
53
+
54
+ ## Quick Start
55
+
56
+ ```python
57
+ from agent_runtime import configure, get_llm_client
58
+
59
+ # Configure
60
+ configure(
61
+ model_provider="openai",
62
+ openai_api_key="sk-..."
63
+ )
64
+
65
+ # Get LLM client
66
+ llm = get_llm_client()
67
+ ```
68
+
69
+ ## Features
70
+
71
+ - Pluggable backends (memory, redis, sqlite)
72
+ - LLM client abstractions (OpenAI, Anthropic, LiteLLM)
73
+ - Event streaming
74
+ - State management
75
+ - Queue-based execution
@@ -0,0 +1,32 @@
1
+ # agent_runtime
2
+
3
+ Framework-agnostic agent runtime library for Python.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install agent_runtime
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from agent_runtime import configure, get_llm_client
15
+
16
+ # Configure
17
+ configure(
18
+ model_provider="openai",
19
+ openai_api_key="sk-..."
20
+ )
21
+
22
+ # Get LLM client
23
+ llm = get_llm_client()
24
+ ```
25
+
26
+ ## Features
27
+
28
+ - Pluggable backends (memory, redis, sqlite)
29
+ - LLM client abstractions (OpenAI, Anthropic, LiteLLM)
30
+ - Event streaming
31
+ - State management
32
+ - Queue-based execution
@@ -0,0 +1,110 @@
1
+ """
2
+ agent_runtime - A standalone Python package for building AI agent systems.
3
+
4
+ This package provides:
5
+ - Core interfaces for agent runtimes
6
+ - Queue, event bus, and state store implementations
7
+ - LLM client abstractions
8
+ - Tracing and observability
9
+ - A runner for executing agent runs
10
+
11
+ Example usage:
12
+ from agent_runtime import (
13
+ AgentRuntime,
14
+ RunContext,
15
+ RunResult,
16
+ Tool,
17
+ configure,
18
+ )
19
+
20
+ # Configure the runtime
21
+ configure(
22
+ model_provider="openai",
23
+ queue_backend="memory",
24
+ )
25
+
26
+ # Create a custom agent runtime
27
+ class MyAgent(AgentRuntime):
28
+ @property
29
+ def key(self) -> str:
30
+ return "my-agent"
31
+
32
+ async def run(self, ctx: RunContext) -> RunResult:
33
+ # Your agent logic here
34
+ return RunResult(final_output={"message": "Hello!"})
35
+ """
36
+
37
+ __version__ = "0.1.0"
38
+
39
+ # Core interfaces
40
+ from agent_runtime.interfaces import (
41
+ AgentRuntime,
42
+ EventType,
43
+ ErrorInfo,
44
+ LLMClient,
45
+ LLMResponse,
46
+ LLMStreamChunk,
47
+ Message,
48
+ RunContext,
49
+ RunResult,
50
+ Tool,
51
+ ToolDefinition,
52
+ ToolRegistry,
53
+ TraceSink,
54
+ )
55
+
56
+ # Configuration
57
+ from agent_runtime.config import (
58
+ RuntimeConfig,
59
+ configure,
60
+ get_config,
61
+ )
62
+
63
+ # Registry
64
+ from agent_runtime.registry import (
65
+ register_runtime,
66
+ get_runtime,
67
+ list_runtimes,
68
+ unregister_runtime,
69
+ clear_registry,
70
+ )
71
+
72
+ # Runner
73
+ from agent_runtime.runner import (
74
+ AgentRunner,
75
+ RunnerConfig,
76
+ RunContextImpl,
77
+ )
78
+
79
+ __all__ = [
80
+ # Version
81
+ "__version__",
82
+ # Interfaces
83
+ "AgentRuntime",
84
+ "LLMClient",
85
+ "LLMResponse",
86
+ "LLMStreamChunk",
87
+ "Message",
88
+ "RunContext",
89
+ "RunResult",
90
+ "ToolRegistry",
91
+ "Tool",
92
+ "ToolDefinition",
93
+ "TraceSink",
94
+ "EventType",
95
+ "ErrorInfo",
96
+ # Configuration
97
+ "RuntimeConfig",
98
+ "configure",
99
+ "get_config",
100
+ # Registry
101
+ "register_runtime",
102
+ "get_runtime",
103
+ "list_runtimes",
104
+ "unregister_runtime",
105
+ "clear_registry",
106
+ # Runner
107
+ "AgentRunner",
108
+ "RunnerConfig",
109
+ "RunContextImpl",
110
+ ]
@@ -0,0 +1,172 @@
1
+ """
2
+ Configuration system for agent_runtime.
3
+
4
+ Supports:
5
+ - Programmatic configuration via configure()
6
+ - Environment variables
7
+ - Default values
8
+ """
9
+
10
+ import os
11
+ from dataclasses import dataclass, field
12
+ from typing import Optional
13
+
14
+
15
+ @dataclass
16
+ class RuntimeConfig:
17
+ """
18
+ Configuration for the agent runtime.
19
+
20
+ All settings can be overridden via environment variables with
21
+ AGENT_RUNTIME_ prefix (e.g., AGENT_RUNTIME_MODEL_PROVIDER).
22
+ """
23
+
24
+ # LLM Provider
25
+ model_provider: str = "openai" # openai, anthropic, litellm
26
+ default_model: str = "gpt-4o"
27
+
28
+ # API Keys (loaded from env if not set)
29
+ openai_api_key: Optional[str] = None
30
+ anthropic_api_key: Optional[str] = None
31
+
32
+ # Queue backend
33
+ queue_backend: str = "memory" # memory, redis, sqlite
34
+
35
+ # Event bus backend
36
+ event_bus_backend: str = "memory" # memory, redis, sqlite
37
+
38
+ # State store backend
39
+ state_store_backend: str = "memory" # memory, redis, sqlite
40
+
41
+ # Tracing backend
42
+ tracing_backend: Optional[str] = None # noop, langfuse
43
+
44
+ # Redis settings
45
+ redis_url: Optional[str] = None
46
+
47
+ # SQLite settings
48
+ sqlite_path: Optional[str] = None
49
+
50
+ # Langfuse settings
51
+ langfuse_public_key: Optional[str] = None
52
+ langfuse_secret_key: Optional[str] = None
53
+ langfuse_host: Optional[str] = None
54
+
55
+ # Runner settings
56
+ run_timeout_seconds: int = 300
57
+ heartbeat_interval_seconds: int = 30
58
+ lease_ttl_seconds: int = 60
59
+ max_retries: int = 3
60
+ retry_backoff_base: int = 2
61
+ retry_backoff_max: int = 300
62
+
63
+ def get_openai_api_key(self) -> Optional[str]:
64
+ """Get OpenAI API key from config or environment."""
65
+ return self.openai_api_key or os.environ.get("OPENAI_API_KEY")
66
+
67
+ def get_anthropic_api_key(self) -> Optional[str]:
68
+ """Get Anthropic API key from config or environment."""
69
+ return self.anthropic_api_key or os.environ.get("ANTHROPIC_API_KEY")
70
+
71
+
72
+ # Global configuration instance
73
+ _config: Optional[RuntimeConfig] = None
74
+
75
+
76
+ def configure(**kwargs) -> RuntimeConfig:
77
+ """
78
+ Configure the agent runtime.
79
+
80
+ Args:
81
+ **kwargs: Configuration options (see RuntimeConfig)
82
+
83
+ Returns:
84
+ The configured RuntimeConfig instance
85
+
86
+ Example:
87
+ from agent_runtime import configure
88
+
89
+ configure(
90
+ model_provider="openai",
91
+ openai_api_key="sk-...",
92
+ queue_backend="redis",
93
+ redis_url="redis://localhost:6379",
94
+ )
95
+ """
96
+ global _config
97
+
98
+ # Start with defaults
99
+ config = RuntimeConfig()
100
+
101
+ # Apply environment variables
102
+ _apply_env_vars(config)
103
+
104
+ # Apply explicit kwargs
105
+ for key, value in kwargs.items():
106
+ if hasattr(config, key):
107
+ setattr(config, key, value)
108
+ else:
109
+ raise ValueError(f"Unknown configuration option: {key}")
110
+
111
+ _config = config
112
+ return config
113
+
114
+
115
+ def get_config() -> RuntimeConfig:
116
+ """
117
+ Get the current configuration.
118
+
119
+ If not configured, returns default configuration with env vars applied.
120
+
121
+ Returns:
122
+ RuntimeConfig instance
123
+ """
124
+ global _config
125
+
126
+ if _config is None:
127
+ _config = RuntimeConfig()
128
+ _apply_env_vars(_config)
129
+
130
+ return _config
131
+
132
+
133
+ def reset_config() -> None:
134
+ """Reset configuration to defaults. Useful for testing."""
135
+ global _config
136
+ _config = None
137
+
138
+
139
+ def _apply_env_vars(config: RuntimeConfig) -> None:
140
+ """Apply environment variables to config."""
141
+ env_mapping = {
142
+ "AGENT_RUNTIME_MODEL_PROVIDER": "model_provider",
143
+ "AGENT_RUNTIME_DEFAULT_MODEL": "default_model",
144
+ "AGENT_RUNTIME_QUEUE_BACKEND": "queue_backend",
145
+ "AGENT_RUNTIME_EVENT_BUS_BACKEND": "event_bus_backend",
146
+ "AGENT_RUNTIME_STATE_STORE_BACKEND": "state_store_backend",
147
+ "AGENT_RUNTIME_TRACING_BACKEND": "tracing_backend",
148
+ "AGENT_RUNTIME_REDIS_URL": "redis_url",
149
+ "AGENT_RUNTIME_SQLITE_PATH": "sqlite_path",
150
+ "AGENT_RUNTIME_LANGFUSE_PUBLIC_KEY": "langfuse_public_key",
151
+ "AGENT_RUNTIME_LANGFUSE_SECRET_KEY": "langfuse_secret_key",
152
+ "AGENT_RUNTIME_LANGFUSE_HOST": "langfuse_host",
153
+ }
154
+
155
+ int_fields = {
156
+ "AGENT_RUNTIME_RUN_TIMEOUT_SECONDS": "run_timeout_seconds",
157
+ "AGENT_RUNTIME_HEARTBEAT_INTERVAL_SECONDS": "heartbeat_interval_seconds",
158
+ "AGENT_RUNTIME_LEASE_TTL_SECONDS": "lease_ttl_seconds",
159
+ "AGENT_RUNTIME_MAX_RETRIES": "max_retries",
160
+ "AGENT_RUNTIME_RETRY_BACKOFF_BASE": "retry_backoff_base",
161
+ "AGENT_RUNTIME_RETRY_BACKOFF_MAX": "retry_backoff_max",
162
+ }
163
+
164
+ for env_var, attr in env_mapping.items():
165
+ value = os.environ.get(env_var)
166
+ if value is not None:
167
+ setattr(config, attr, value)
168
+
169
+ for env_var, attr in int_fields.items():
170
+ value = os.environ.get(env_var)
171
+ if value is not None:
172
+ setattr(config, attr, int(value))
@@ -0,0 +1,55 @@
1
+ """
2
+ Event bus implementations for agent communication.
3
+
4
+ Provides:
5
+ - EventBus: Abstract interface
6
+ - Event: Event data structure
7
+ - InMemoryEventBus: For testing and simple use cases
8
+ - RedisEventBus: For production with pub/sub
9
+ - SQLiteEventBus: For persistent local storage
10
+ """
11
+
12
+ from agent_runtime.events.base import EventBus, Event
13
+ from agent_runtime.events.memory import InMemoryEventBus
14
+
15
+ __all__ = [
16
+ "EventBus",
17
+ "Event",
18
+ "InMemoryEventBus",
19
+ "get_event_bus",
20
+ ]
21
+
22
+
23
+ def get_event_bus(backend: str = None, **kwargs) -> EventBus:
24
+ """
25
+ Factory function to get an event bus.
26
+
27
+ Args:
28
+ backend: "memory", "redis", or "sqlite"
29
+ **kwargs: Backend-specific configuration
30
+
31
+ Returns:
32
+ EventBus instance
33
+ """
34
+ from agent_runtime.config import get_config
35
+
36
+ config = get_config()
37
+ backend = backend or config.event_bus_backend
38
+
39
+ if backend == "memory":
40
+ return InMemoryEventBus()
41
+
42
+ elif backend == "redis":
43
+ from agent_runtime.events.redis import RedisEventBus
44
+ url = kwargs.get("url") or config.redis_url
45
+ if not url:
46
+ raise ValueError("redis_url is required for redis event bus backend")
47
+ return RedisEventBus(url=url, **kwargs)
48
+
49
+ elif backend == "sqlite":
50
+ from agent_runtime.events.sqlite import SQLiteEventBus
51
+ path = kwargs.get("path") or config.sqlite_path or "agent_runtime.db"
52
+ return SQLiteEventBus(path=path)
53
+
54
+ else:
55
+ raise ValueError(f"Unknown event bus backend: {backend}")
@@ -0,0 +1,86 @@
1
+ """
2
+ Abstract base class for event bus implementations.
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime, timezone
8
+ from typing import Any, AsyncIterator, Callable, Optional
9
+ from uuid import UUID
10
+
11
+
12
+ @dataclass
13
+ class Event:
14
+ """An event emitted by an agent run."""
15
+
16
+ run_id: UUID
17
+ event_type: str
18
+ payload: dict = field(default_factory=dict)
19
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
20
+ sequence: int = 0
21
+
22
+
23
+ class EventBus(ABC):
24
+ """
25
+ Abstract interface for event bus implementations.
26
+
27
+ Event buses handle:
28
+ - Publishing events from agent runs
29
+ - Subscribing to events for a run
30
+ - Event persistence (optional)
31
+ """
32
+
33
+ @abstractmethod
34
+ async def publish(
35
+ self,
36
+ run_id: UUID,
37
+ event_type: str,
38
+ payload: dict,
39
+ ) -> None:
40
+ """
41
+ Publish an event.
42
+
43
+ Args:
44
+ run_id: Run that emitted the event
45
+ event_type: Type of event
46
+ payload: Event data
47
+ """
48
+ ...
49
+
50
+ @abstractmethod
51
+ async def subscribe(
52
+ self,
53
+ run_id: UUID,
54
+ ) -> AsyncIterator[Event]:
55
+ """
56
+ Subscribe to events for a run.
57
+
58
+ Args:
59
+ run_id: Run to subscribe to
60
+
61
+ Yields:
62
+ Events as they are published
63
+ """
64
+ ...
65
+
66
+ @abstractmethod
67
+ async def get_events(
68
+ self,
69
+ run_id: UUID,
70
+ since_sequence: int = 0,
71
+ ) -> list[Event]:
72
+ """
73
+ Get historical events for a run.
74
+
75
+ Args:
76
+ run_id: Run to get events for
77
+ since_sequence: Only return events after this sequence
78
+
79
+ Returns:
80
+ List of events
81
+ """
82
+ ...
83
+
84
+ async def close(self) -> None:
85
+ """Close any connections. Override if needed."""
86
+ pass
@@ -0,0 +1,89 @@
1
+ """
2
+ In-memory event bus implementation.
3
+
4
+ Good for:
5
+ - Unit testing
6
+ - Local development
7
+ - Simple single-process scripts
8
+ """
9
+
10
+ import asyncio
11
+ from collections import defaultdict
12
+ from datetime import datetime, timezone
13
+ from typing import AsyncIterator, Optional
14
+ from uuid import UUID
15
+
16
+ from agent_runtime.events.base import EventBus, Event
17
+
18
+
19
+ class InMemoryEventBus(EventBus):
20
+ """
21
+ In-memory event bus implementation.
22
+
23
+ Stores events in memory. Data is lost when the process exits.
24
+ """
25
+
26
+ def __init__(self):
27
+ # run_id -> list of events
28
+ self._events: dict[UUID, list[Event]] = defaultdict(list)
29
+ # run_id -> list of subscriber queues
30
+ self._subscribers: dict[UUID, list[asyncio.Queue]] = defaultdict(list)
31
+ self._lock = asyncio.Lock()
32
+
33
+ async def publish(
34
+ self,
35
+ run_id: UUID,
36
+ event_type: str,
37
+ payload: dict,
38
+ ) -> None:
39
+ """Publish an event."""
40
+ async with self._lock:
41
+ events = self._events[run_id]
42
+ sequence = len(events)
43
+
44
+ event = Event(
45
+ run_id=run_id,
46
+ event_type=event_type,
47
+ payload=payload,
48
+ timestamp=datetime.now(timezone.utc),
49
+ sequence=sequence,
50
+ )
51
+
52
+ events.append(event)
53
+
54
+ # Notify subscribers
55
+ for queue in self._subscribers[run_id]:
56
+ await queue.put(event)
57
+
58
+ async def subscribe(
59
+ self,
60
+ run_id: UUID,
61
+ ) -> AsyncIterator[Event]:
62
+ """Subscribe to events for a run."""
63
+ queue: asyncio.Queue[Event] = asyncio.Queue()
64
+
65
+ async with self._lock:
66
+ self._subscribers[run_id].append(queue)
67
+
68
+ try:
69
+ while True:
70
+ event = await queue.get()
71
+ yield event
72
+ finally:
73
+ async with self._lock:
74
+ if queue in self._subscribers[run_id]:
75
+ self._subscribers[run_id].remove(queue)
76
+
77
+ async def get_events(
78
+ self,
79
+ run_id: UUID,
80
+ since_sequence: int = 0,
81
+ ) -> list[Event]:
82
+ """Get historical events for a run."""
83
+ events = self._events.get(run_id, [])
84
+ return [e for e in events if e.sequence >= since_sequence]
85
+
86
+ def clear(self) -> None:
87
+ """Clear all events. Useful for testing."""
88
+ self._events.clear()
89
+ self._subscribers.clear()