agent-runtime-core 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.
- agent_runtime/__init__.py +110 -0
- agent_runtime/config.py +172 -0
- agent_runtime/events/__init__.py +55 -0
- agent_runtime/events/base.py +86 -0
- agent_runtime/events/memory.py +89 -0
- agent_runtime/events/redis.py +185 -0
- agent_runtime/events/sqlite.py +168 -0
- agent_runtime/interfaces.py +390 -0
- agent_runtime/llm/__init__.py +83 -0
- agent_runtime/llm/anthropic.py +237 -0
- agent_runtime/llm/litellm_client.py +175 -0
- agent_runtime/llm/openai.py +220 -0
- agent_runtime/queue/__init__.py +55 -0
- agent_runtime/queue/base.py +167 -0
- agent_runtime/queue/memory.py +184 -0
- agent_runtime/queue/redis.py +453 -0
- agent_runtime/queue/sqlite.py +420 -0
- agent_runtime/registry.py +74 -0
- agent_runtime/runner.py +403 -0
- agent_runtime/state/__init__.py +53 -0
- agent_runtime/state/base.py +69 -0
- agent_runtime/state/memory.py +51 -0
- agent_runtime/state/redis.py +109 -0
- agent_runtime/state/sqlite.py +158 -0
- agent_runtime/tracing/__init__.py +47 -0
- agent_runtime/tracing/langfuse.py +119 -0
- agent_runtime/tracing/noop.py +34 -0
- agent_runtime_core-0.1.0.dist-info/METADATA +75 -0
- agent_runtime_core-0.1.0.dist-info/RECORD +31 -0
- agent_runtime_core-0.1.0.dist-info/WHEEL +4 -0
- agent_runtime_core-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Abstract base class for run queue 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 Optional
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class QueuedRun:
|
|
14
|
+
"""A run waiting in the queue."""
|
|
15
|
+
|
|
16
|
+
run_id: UUID
|
|
17
|
+
agent_key: str
|
|
18
|
+
input: dict
|
|
19
|
+
metadata: dict = field(default_factory=dict)
|
|
20
|
+
priority: int = 0
|
|
21
|
+
attempt: int = 1
|
|
22
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
23
|
+
scheduled_at: Optional[datetime] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RunQueue(ABC):
|
|
27
|
+
"""
|
|
28
|
+
Abstract interface for run queue implementations.
|
|
29
|
+
|
|
30
|
+
Queues handle:
|
|
31
|
+
- Enqueueing new runs
|
|
32
|
+
- Claiming runs for processing
|
|
33
|
+
- Lease management
|
|
34
|
+
- Retries
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
async def enqueue(
|
|
39
|
+
self,
|
|
40
|
+
run_id: UUID,
|
|
41
|
+
agent_key: str,
|
|
42
|
+
input: dict,
|
|
43
|
+
metadata: Optional[dict] = None,
|
|
44
|
+
priority: int = 0,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Add a run to the queue.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
run_id: Unique run identifier
|
|
51
|
+
agent_key: Agent to handle the run
|
|
52
|
+
input: Input data for the run
|
|
53
|
+
metadata: Optional metadata
|
|
54
|
+
priority: Priority (higher = more urgent)
|
|
55
|
+
"""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def claim(
|
|
60
|
+
self,
|
|
61
|
+
worker_id: str,
|
|
62
|
+
lease_seconds: int = 60,
|
|
63
|
+
) -> Optional[QueuedRun]:
|
|
64
|
+
"""
|
|
65
|
+
Claim the next available run.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
worker_id: ID of the claiming worker
|
|
69
|
+
lease_seconds: How long to hold the lease
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
QueuedRun if one is available, None otherwise
|
|
73
|
+
"""
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
async def release(
|
|
78
|
+
self,
|
|
79
|
+
run_id: UUID,
|
|
80
|
+
worker_id: str,
|
|
81
|
+
success: bool,
|
|
82
|
+
output: Optional[dict] = None,
|
|
83
|
+
error: Optional[dict] = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Release a claimed run.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
run_id: Run to release
|
|
90
|
+
worker_id: Worker releasing the run
|
|
91
|
+
success: Whether the run succeeded
|
|
92
|
+
output: Output data (if success)
|
|
93
|
+
error: Error info (if failure)
|
|
94
|
+
"""
|
|
95
|
+
...
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
async def extend_lease(
|
|
99
|
+
self,
|
|
100
|
+
run_id: UUID,
|
|
101
|
+
worker_id: str,
|
|
102
|
+
lease_seconds: int,
|
|
103
|
+
) -> bool:
|
|
104
|
+
"""
|
|
105
|
+
Extend the lease on a run.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
run_id: Run to extend
|
|
109
|
+
worker_id: Worker holding the lease
|
|
110
|
+
lease_seconds: New lease duration
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
True if extended, False if lease was lost
|
|
114
|
+
"""
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
@abstractmethod
|
|
118
|
+
async def is_cancelled(self, run_id: UUID) -> bool:
|
|
119
|
+
"""
|
|
120
|
+
Check if a run has been cancelled.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
run_id: Run to check
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if cancelled
|
|
127
|
+
"""
|
|
128
|
+
...
|
|
129
|
+
|
|
130
|
+
@abstractmethod
|
|
131
|
+
async def cancel(self, run_id: UUID) -> bool:
|
|
132
|
+
"""
|
|
133
|
+
Cancel a run.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
run_id: Run to cancel
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
True if cancelled, False if not found or already complete
|
|
140
|
+
"""
|
|
141
|
+
...
|
|
142
|
+
|
|
143
|
+
@abstractmethod
|
|
144
|
+
async def requeue_for_retry(
|
|
145
|
+
self,
|
|
146
|
+
run_id: UUID,
|
|
147
|
+
worker_id: str,
|
|
148
|
+
error: dict,
|
|
149
|
+
delay_seconds: int = 0,
|
|
150
|
+
) -> bool:
|
|
151
|
+
"""
|
|
152
|
+
Requeue a failed run for retry.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
run_id: Run to retry
|
|
156
|
+
worker_id: Worker releasing the run
|
|
157
|
+
error: Error information
|
|
158
|
+
delay_seconds: Delay before retry
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
True if requeued, False if max retries exceeded
|
|
162
|
+
"""
|
|
163
|
+
...
|
|
164
|
+
|
|
165
|
+
async def close(self) -> None:
|
|
166
|
+
"""Close any connections. Override if needed."""
|
|
167
|
+
pass
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
In-memory queue 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 deque
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import datetime, timezone, timedelta
|
|
14
|
+
from typing import Optional
|
|
15
|
+
from uuid import UUID
|
|
16
|
+
|
|
17
|
+
from agent_runtime.queue.base import RunQueue, QueuedRun
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class QueueEntry:
|
|
22
|
+
"""Internal queue entry with lease info."""
|
|
23
|
+
run: QueuedRun
|
|
24
|
+
lease_owner: Optional[str] = None
|
|
25
|
+
lease_expires_at: Optional[datetime] = None
|
|
26
|
+
cancelled: bool = False
|
|
27
|
+
completed: bool = False
|
|
28
|
+
output: Optional[dict] = None
|
|
29
|
+
error: Optional[dict] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class InMemoryQueue(RunQueue):
|
|
33
|
+
"""
|
|
34
|
+
In-memory queue implementation.
|
|
35
|
+
|
|
36
|
+
Stores runs in memory. Data is lost when the process exits.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, max_retries: int = 3):
|
|
40
|
+
self._entries: dict[UUID, QueueEntry] = {}
|
|
41
|
+
self._queue: deque[UUID] = deque()
|
|
42
|
+
self._lock = asyncio.Lock()
|
|
43
|
+
self._max_retries = max_retries
|
|
44
|
+
|
|
45
|
+
async def enqueue(
|
|
46
|
+
self,
|
|
47
|
+
run_id: UUID,
|
|
48
|
+
agent_key: str,
|
|
49
|
+
input: dict,
|
|
50
|
+
metadata: Optional[dict] = None,
|
|
51
|
+
priority: int = 0,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Add a run to the queue."""
|
|
54
|
+
async with self._lock:
|
|
55
|
+
run = QueuedRun(
|
|
56
|
+
run_id=run_id,
|
|
57
|
+
agent_key=agent_key,
|
|
58
|
+
input=input,
|
|
59
|
+
metadata=metadata or {},
|
|
60
|
+
priority=priority,
|
|
61
|
+
)
|
|
62
|
+
self._entries[run_id] = QueueEntry(run=run)
|
|
63
|
+
self._queue.append(run_id)
|
|
64
|
+
|
|
65
|
+
async def claim(
|
|
66
|
+
self,
|
|
67
|
+
worker_id: str,
|
|
68
|
+
lease_seconds: int = 60,
|
|
69
|
+
) -> Optional[QueuedRun]:
|
|
70
|
+
"""Claim the next available run."""
|
|
71
|
+
async with self._lock:
|
|
72
|
+
now = datetime.now(timezone.utc)
|
|
73
|
+
|
|
74
|
+
# Find an available run
|
|
75
|
+
for _ in range(len(self._queue)):
|
|
76
|
+
run_id = self._queue.popleft()
|
|
77
|
+
entry = self._entries.get(run_id)
|
|
78
|
+
|
|
79
|
+
if entry is None or entry.completed or entry.cancelled:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# Check if lease expired
|
|
83
|
+
if entry.lease_owner and entry.lease_expires_at:
|
|
84
|
+
if entry.lease_expires_at > now:
|
|
85
|
+
# Still leased, put back
|
|
86
|
+
self._queue.append(run_id)
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
# Claim it
|
|
90
|
+
entry.lease_owner = worker_id
|
|
91
|
+
entry.lease_expires_at = now + timedelta(seconds=lease_seconds)
|
|
92
|
+
return entry.run
|
|
93
|
+
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
async def release(
|
|
97
|
+
self,
|
|
98
|
+
run_id: UUID,
|
|
99
|
+
worker_id: str,
|
|
100
|
+
success: bool,
|
|
101
|
+
output: Optional[dict] = None,
|
|
102
|
+
error: Optional[dict] = None,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Release a claimed run."""
|
|
105
|
+
async with self._lock:
|
|
106
|
+
entry = self._entries.get(run_id)
|
|
107
|
+
if entry is None:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
if entry.lease_owner != worker_id:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
entry.completed = True
|
|
114
|
+
entry.lease_owner = None
|
|
115
|
+
entry.lease_expires_at = None
|
|
116
|
+
entry.output = output
|
|
117
|
+
entry.error = error
|
|
118
|
+
|
|
119
|
+
async def extend_lease(
|
|
120
|
+
self,
|
|
121
|
+
run_id: UUID,
|
|
122
|
+
worker_id: str,
|
|
123
|
+
lease_seconds: int,
|
|
124
|
+
) -> bool:
|
|
125
|
+
"""Extend the lease on a run."""
|
|
126
|
+
async with self._lock:
|
|
127
|
+
entry = self._entries.get(run_id)
|
|
128
|
+
if entry is None:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
if entry.lease_owner != worker_id:
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
entry.lease_expires_at = datetime.now(timezone.utc) + timedelta(seconds=lease_seconds)
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
async def is_cancelled(self, run_id: UUID) -> bool:
|
|
138
|
+
"""Check if a run has been cancelled."""
|
|
139
|
+
entry = self._entries.get(run_id)
|
|
140
|
+
return entry.cancelled if entry else False
|
|
141
|
+
|
|
142
|
+
async def cancel(self, run_id: UUID) -> bool:
|
|
143
|
+
"""Cancel a run."""
|
|
144
|
+
async with self._lock:
|
|
145
|
+
entry = self._entries.get(run_id)
|
|
146
|
+
if entry is None or entry.completed:
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
entry.cancelled = True
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
async def requeue_for_retry(
|
|
153
|
+
self,
|
|
154
|
+
run_id: UUID,
|
|
155
|
+
worker_id: str,
|
|
156
|
+
error: dict,
|
|
157
|
+
delay_seconds: int = 0,
|
|
158
|
+
) -> bool:
|
|
159
|
+
"""Requeue a failed run for retry."""
|
|
160
|
+
async with self._lock:
|
|
161
|
+
entry = self._entries.get(run_id)
|
|
162
|
+
if entry is None:
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
if entry.lease_owner != worker_id:
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
if entry.run.attempt >= self._max_retries:
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
# Increment attempt and requeue
|
|
172
|
+
entry.run.attempt += 1
|
|
173
|
+
entry.lease_owner = None
|
|
174
|
+
entry.lease_expires_at = None
|
|
175
|
+
entry.error = error
|
|
176
|
+
|
|
177
|
+
# Add back to queue
|
|
178
|
+
self._queue.append(run_id)
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
def clear(self) -> None:
|
|
182
|
+
"""Clear all entries. Useful for testing."""
|
|
183
|
+
self._entries.clear()
|
|
184
|
+
self._queue.clear()
|