agent-runtime-core 0.2.1__py3-none-any.whl → 0.4.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.
Files changed (38) hide show
  1. {agent_runtime → agent_runtime_core}/__init__.py +8 -8
  2. {agent_runtime → agent_runtime_core}/config.py +1 -1
  3. {agent_runtime → agent_runtime_core}/events/__init__.py +5 -5
  4. {agent_runtime → agent_runtime_core}/events/memory.py +1 -1
  5. {agent_runtime → agent_runtime_core}/events/redis.py +1 -1
  6. {agent_runtime → agent_runtime_core}/events/sqlite.py +1 -1
  7. {agent_runtime → agent_runtime_core}/llm/__init__.py +6 -6
  8. {agent_runtime → agent_runtime_core}/llm/anthropic.py +4 -4
  9. {agent_runtime → agent_runtime_core}/llm/litellm_client.py +2 -2
  10. {agent_runtime → agent_runtime_core}/llm/openai.py +4 -4
  11. {agent_runtime → agent_runtime_core}/persistence/__init__.py +48 -12
  12. agent_runtime_core/persistence/base.py +737 -0
  13. {agent_runtime → agent_runtime_core}/persistence/file.py +1 -1
  14. {agent_runtime → agent_runtime_core}/persistence/manager.py +122 -14
  15. {agent_runtime → agent_runtime_core}/queue/__init__.py +5 -5
  16. {agent_runtime → agent_runtime_core}/queue/memory.py +1 -1
  17. {agent_runtime → agent_runtime_core}/queue/redis.py +1 -1
  18. {agent_runtime → agent_runtime_core}/queue/sqlite.py +1 -1
  19. {agent_runtime → agent_runtime_core}/registry.py +1 -1
  20. {agent_runtime → agent_runtime_core}/runner.py +6 -6
  21. {agent_runtime → agent_runtime_core}/state/__init__.py +5 -5
  22. {agent_runtime → agent_runtime_core}/state/memory.py +1 -1
  23. {agent_runtime → agent_runtime_core}/state/redis.py +1 -1
  24. {agent_runtime → agent_runtime_core}/state/sqlite.py +1 -1
  25. {agent_runtime → agent_runtime_core}/testing.py +1 -1
  26. {agent_runtime → agent_runtime_core}/tracing/__init__.py +4 -4
  27. {agent_runtime → agent_runtime_core}/tracing/langfuse.py +1 -1
  28. {agent_runtime → agent_runtime_core}/tracing/noop.py +1 -1
  29. {agent_runtime_core-0.2.1.dist-info → agent_runtime_core-0.4.0.dist-info}/METADATA +352 -42
  30. agent_runtime_core-0.4.0.dist-info/RECORD +36 -0
  31. agent_runtime/persistence/base.py +0 -332
  32. agent_runtime_core-0.2.1.dist-info/RECORD +0 -36
  33. {agent_runtime → agent_runtime_core}/events/base.py +0 -0
  34. {agent_runtime → agent_runtime_core}/interfaces.py +0 -0
  35. {agent_runtime → agent_runtime_core}/queue/base.py +0 -0
  36. {agent_runtime → agent_runtime_core}/state/base.py +0 -0
  37. {agent_runtime_core-0.2.1.dist-info → agent_runtime_core-0.4.0.dist-info}/WHEEL +0 -0
  38. {agent_runtime_core-0.2.1.dist-info → agent_runtime_core-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,737 @@
1
+ """
2
+ Abstract base classes for persistence backends.
3
+
4
+ These interfaces define the contract that all storage backends must implement.
5
+ Projects depending on agent-runtime-core can provide their own implementations
6
+ (e.g., database-backed, cloud storage, etc.).
7
+
8
+ For Django/database implementations:
9
+ - The `scope` parameter can be ignored if you use user/tenant context instead
10
+ - Store implementations receive context through their constructor (e.g., user, org)
11
+ - The abstract methods still accept scope for interface compatibility, but
12
+ implementations can choose to ignore it
13
+
14
+ Example Django implementation:
15
+ class DjangoMemoryStore(MemoryStore):
16
+ def __init__(self, user):
17
+ self.user = user
18
+
19
+ async def get(self, key: str, scope: Scope = Scope.PROJECT) -> Optional[Any]:
20
+ # Ignore scope, use self.user instead
21
+ try:
22
+ entry = await Memory.objects.aget(user=self.user, key=key)
23
+ return entry.value
24
+ except Memory.DoesNotExist:
25
+ return None
26
+ """
27
+
28
+ from abc import ABC, abstractmethod
29
+ from dataclasses import dataclass, field
30
+ from datetime import datetime
31
+ from enum import Enum
32
+ from typing import Any, Optional, AsyncIterator
33
+ from uuid import UUID
34
+
35
+
36
+ class Scope(str, Enum):
37
+ """
38
+ Storage scope for memory and other persistent data.
39
+
40
+ For file-based storage:
41
+ - GLOBAL: User's home directory (~/.agent_runtime/)
42
+ - PROJECT: Current working directory (./.agent_runtime/)
43
+ - SESSION: In-memory only, not persisted
44
+
45
+ For database-backed storage, implementations may ignore this
46
+ and use user/tenant context from the store constructor instead.
47
+ """
48
+
49
+ GLOBAL = "global"
50
+ PROJECT = "project"
51
+ SESSION = "session"
52
+
53
+
54
+ class TaskState(str, Enum):
55
+ """State of a task."""
56
+
57
+ NOT_STARTED = "not_started"
58
+ IN_PROGRESS = "in_progress"
59
+ COMPLETE = "complete"
60
+ CANCELLED = "cancelled"
61
+
62
+
63
+ @dataclass
64
+ class ToolCall:
65
+ """A tool call made during a conversation."""
66
+
67
+ id: str
68
+ name: str
69
+ arguments: dict
70
+ timestamp: datetime = field(default_factory=datetime.utcnow)
71
+
72
+
73
+ @dataclass
74
+ class ToolResult:
75
+ """Result of a tool call."""
76
+
77
+ tool_call_id: str
78
+ result: Any
79
+ error: Optional[str] = None
80
+ timestamp: datetime = field(default_factory=datetime.utcnow)
81
+
82
+
83
+ @dataclass
84
+ class ConversationMessage:
85
+ """A message in a conversation with full state."""
86
+
87
+ id: UUID
88
+ role: str # system, user, assistant, tool
89
+ content: str | dict | list
90
+ timestamp: datetime = field(default_factory=datetime.utcnow)
91
+
92
+ # For assistant messages with tool calls
93
+ tool_calls: list[ToolCall] = field(default_factory=list)
94
+
95
+ # For tool result messages
96
+ tool_call_id: Optional[str] = None
97
+
98
+ # Metadata
99
+ model: Optional[str] = None
100
+ usage: dict = field(default_factory=dict) # token counts: {prompt_tokens, completion_tokens, total_tokens}
101
+ metadata: dict = field(default_factory=dict)
102
+
103
+ # Branching support
104
+ parent_message_id: Optional[UUID] = None # For branched/edited messages
105
+ branch_id: Optional[UUID] = None # Groups messages in same branch
106
+
107
+
108
+ @dataclass
109
+ class Conversation:
110
+ """A complete conversation with all state."""
111
+
112
+ id: UUID
113
+ title: Optional[str] = None
114
+ messages: list[ConversationMessage] = field(default_factory=list)
115
+
116
+ # Metadata
117
+ created_at: datetime = field(default_factory=datetime.utcnow)
118
+ updated_at: datetime = field(default_factory=datetime.utcnow)
119
+ metadata: dict = field(default_factory=dict)
120
+
121
+ # Associated agent
122
+ agent_key: Optional[str] = None
123
+
124
+ # Summary for long conversations
125
+ summary: Optional[str] = None
126
+
127
+ # Branching support
128
+ parent_conversation_id: Optional[UUID] = None # For forked conversations
129
+ active_branch_id: Optional[UUID] = None # Currently active branch
130
+
131
+
132
+ @dataclass
133
+ class Task:
134
+ """A task in a task list."""
135
+
136
+ id: UUID
137
+ name: str
138
+ description: str = ""
139
+ state: TaskState = TaskState.NOT_STARTED
140
+ parent_id: Optional[UUID] = None
141
+ created_at: datetime = field(default_factory=datetime.utcnow)
142
+ updated_at: datetime = field(default_factory=datetime.utcnow)
143
+ metadata: dict = field(default_factory=dict)
144
+
145
+ # Dependencies and scheduling
146
+ dependencies: list[UUID] = field(default_factory=list) # Task IDs this depends on
147
+ priority: int = 0 # Higher = more important
148
+ due_at: Optional[datetime] = None
149
+ completed_at: Optional[datetime] = None
150
+
151
+ # Checkpoint for resumable long-running operations
152
+ checkpoint_data: dict = field(default_factory=dict)
153
+ checkpoint_at: Optional[datetime] = None
154
+
155
+ # Execution tracking
156
+ attempts: int = 0
157
+ last_error: Optional[str] = None
158
+
159
+
160
+ @dataclass
161
+ class TaskList:
162
+ """A list of tasks."""
163
+
164
+ id: UUID
165
+ name: str
166
+ tasks: list[Task] = field(default_factory=list)
167
+ created_at: datetime = field(default_factory=datetime.utcnow)
168
+ updated_at: datetime = field(default_factory=datetime.utcnow)
169
+
170
+ # Associated conversation/run
171
+ conversation_id: Optional[UUID] = None
172
+ run_id: Optional[UUID] = None
173
+
174
+
175
+ class MemoryStore(ABC):
176
+ """
177
+ Abstract interface for key-value memory storage.
178
+
179
+ Memory stores handle persistent key-value data that agents can
180
+ use to remember information across sessions.
181
+ """
182
+
183
+ @abstractmethod
184
+ async def get(self, key: str, scope: Scope = Scope.PROJECT) -> Optional[Any]:
185
+ """Get a value by key."""
186
+ ...
187
+
188
+ @abstractmethod
189
+ async def set(self, key: str, value: Any, scope: Scope = Scope.PROJECT) -> None:
190
+ """Set a value by key."""
191
+ ...
192
+
193
+ @abstractmethod
194
+ async def delete(self, key: str, scope: Scope = Scope.PROJECT) -> bool:
195
+ """Delete a key. Returns True if key existed."""
196
+ ...
197
+
198
+ @abstractmethod
199
+ async def list_keys(self, scope: Scope = Scope.PROJECT, prefix: Optional[str] = None) -> list[str]:
200
+ """List all keys, optionally filtered by prefix."""
201
+ ...
202
+
203
+ @abstractmethod
204
+ async def clear(self, scope: Scope = Scope.PROJECT) -> None:
205
+ """Clear all keys in the given scope."""
206
+ ...
207
+
208
+ async def close(self) -> None:
209
+ """Close any connections. Override if needed."""
210
+ pass
211
+
212
+
213
+ class ConversationStore(ABC):
214
+ """
215
+ Abstract interface for conversation history storage.
216
+
217
+ Conversation stores handle full conversation state including
218
+ messages, tool calls, and metadata.
219
+ """
220
+
221
+ @abstractmethod
222
+ async def save(self, conversation: Conversation, scope: Scope = Scope.PROJECT) -> None:
223
+ """Save or update a conversation."""
224
+ ...
225
+
226
+ @abstractmethod
227
+ async def get(self, conversation_id: UUID, scope: Scope = Scope.PROJECT) -> Optional[Conversation]:
228
+ """Get a conversation by ID."""
229
+ ...
230
+
231
+ @abstractmethod
232
+ async def delete(self, conversation_id: UUID, scope: Scope = Scope.PROJECT) -> bool:
233
+ """Delete a conversation. Returns True if it existed."""
234
+ ...
235
+
236
+ @abstractmethod
237
+ async def list_conversations(
238
+ self,
239
+ scope: Scope = Scope.PROJECT,
240
+ limit: int = 100,
241
+ offset: int = 0,
242
+ agent_key: Optional[str] = None,
243
+ ) -> list[Conversation]:
244
+ """List conversations, optionally filtered by agent."""
245
+ ...
246
+
247
+ @abstractmethod
248
+ async def add_message(
249
+ self,
250
+ conversation_id: UUID,
251
+ message: ConversationMessage,
252
+ scope: Scope = Scope.PROJECT,
253
+ ) -> None:
254
+ """Add a message to an existing conversation."""
255
+ ...
256
+
257
+ @abstractmethod
258
+ async def get_messages(
259
+ self,
260
+ conversation_id: UUID,
261
+ scope: Scope = Scope.PROJECT,
262
+ limit: Optional[int] = None,
263
+ before: Optional[datetime] = None,
264
+ ) -> list[ConversationMessage]:
265
+ """Get messages from a conversation."""
266
+ ...
267
+
268
+ async def close(self) -> None:
269
+ """Close any connections. Override if needed."""
270
+ pass
271
+
272
+
273
+ class TaskStore(ABC):
274
+ """
275
+ Abstract interface for task list storage.
276
+
277
+ Task stores handle task lists and their state for tracking
278
+ agent progress on complex work.
279
+ """
280
+
281
+ @abstractmethod
282
+ async def save(self, task_list: TaskList, scope: Scope = Scope.PROJECT) -> None:
283
+ """Save or update a task list."""
284
+ ...
285
+
286
+ @abstractmethod
287
+ async def get(self, task_list_id: UUID, scope: Scope = Scope.PROJECT) -> Optional[TaskList]:
288
+ """Get a task list by ID."""
289
+ ...
290
+
291
+ @abstractmethod
292
+ async def delete(self, task_list_id: UUID, scope: Scope = Scope.PROJECT) -> bool:
293
+ """Delete a task list. Returns True if it existed."""
294
+ ...
295
+
296
+ @abstractmethod
297
+ async def get_by_conversation(
298
+ self,
299
+ conversation_id: UUID,
300
+ scope: Scope = Scope.PROJECT,
301
+ ) -> Optional[TaskList]:
302
+ """Get the task list associated with a conversation."""
303
+ ...
304
+
305
+ @abstractmethod
306
+ async def update_task(
307
+ self,
308
+ task_list_id: UUID,
309
+ task_id: UUID,
310
+ state: Optional[TaskState] = None,
311
+ name: Optional[str] = None,
312
+ description: Optional[str] = None,
313
+ scope: Scope = Scope.PROJECT,
314
+ ) -> None:
315
+ """Update a specific task in a task list."""
316
+ ...
317
+
318
+ async def close(self) -> None:
319
+ """Close any connections. Override if needed."""
320
+ pass
321
+
322
+
323
+ class PreferencesStore(ABC):
324
+ """
325
+ Abstract interface for preferences storage.
326
+
327
+ Preferences stores handle user and agent configuration
328
+ that persists across sessions.
329
+ """
330
+
331
+ @abstractmethod
332
+ async def get(self, key: str, scope: Scope = Scope.GLOBAL) -> Optional[Any]:
333
+ """Get a preference value."""
334
+ ...
335
+
336
+ @abstractmethod
337
+ async def set(self, key: str, value: Any, scope: Scope = Scope.GLOBAL) -> None:
338
+ """Set a preference value."""
339
+ ...
340
+
341
+ @abstractmethod
342
+ async def delete(self, key: str, scope: Scope = Scope.GLOBAL) -> bool:
343
+ """Delete a preference. Returns True if it existed."""
344
+ ...
345
+
346
+ @abstractmethod
347
+ async def get_all(self, scope: Scope = Scope.GLOBAL) -> dict[str, Any]:
348
+ """Get all preferences in the given scope."""
349
+ ...
350
+
351
+ async def close(self) -> None:
352
+ """Close any connections. Override if needed."""
353
+ pass
354
+
355
+
356
+ # =============================================================================
357
+ # Knowledge Base Models and Store
358
+ # =============================================================================
359
+
360
+
361
+ class FactType(str, Enum):
362
+ """Type of fact stored in knowledge base."""
363
+
364
+ USER = "user" # Facts about the user
365
+ PROJECT = "project" # Facts about the project
366
+ PREFERENCE = "preference" # Learned preferences
367
+ CONTEXT = "context" # Contextual information
368
+ CUSTOM = "custom" # Custom fact type
369
+
370
+
371
+ @dataclass
372
+ class Fact:
373
+ """A learned fact about user, project, or context."""
374
+
375
+ id: UUID
376
+ key: str # Unique identifier for the fact
377
+ value: Any # The fact content
378
+ fact_type: FactType = FactType.CUSTOM
379
+ confidence: float = 1.0 # 0.0 to 1.0
380
+ source: Optional[str] = None # Where this fact came from
381
+
382
+ created_at: datetime = field(default_factory=datetime.utcnow)
383
+ updated_at: datetime = field(default_factory=datetime.utcnow)
384
+ expires_at: Optional[datetime] = None # Optional expiration
385
+
386
+ metadata: dict = field(default_factory=dict)
387
+
388
+
389
+ @dataclass
390
+ class Summary:
391
+ """A summary of a conversation or set of interactions."""
392
+
393
+ id: UUID
394
+ content: str # The summary text
395
+
396
+ # What this summarizes
397
+ conversation_id: Optional[UUID] = None
398
+ conversation_ids: list[UUID] = field(default_factory=list) # For multi-conversation summaries
399
+
400
+ # Time range covered
401
+ start_time: Optional[datetime] = None
402
+ end_time: Optional[datetime] = None
403
+
404
+ created_at: datetime = field(default_factory=datetime.utcnow)
405
+ metadata: dict = field(default_factory=dict)
406
+
407
+
408
+ @dataclass
409
+ class Embedding:
410
+ """
411
+ A vector embedding for semantic search.
412
+
413
+ Note: This is optional and requires additional dependencies
414
+ for vector operations (e.g., numpy, faiss, pgvector).
415
+ """
416
+
417
+ id: UUID
418
+ vector: list[float] # The embedding vector
419
+
420
+ # What this embedding represents
421
+ content: str # Original text
422
+ content_type: str = "text" # text, summary, fact, etc.
423
+ source_id: Optional[UUID] = None # ID of source object
424
+
425
+ model: Optional[str] = None # Embedding model used
426
+ dimensions: int = 0 # Vector dimensions
427
+
428
+ created_at: datetime = field(default_factory=datetime.utcnow)
429
+ metadata: dict = field(default_factory=dict)
430
+
431
+
432
+ class KnowledgeStore(ABC):
433
+ """
434
+ Abstract interface for knowledge base storage.
435
+
436
+ Knowledge stores handle facts, summaries, and optionally
437
+ embeddings for semantic search. This is optional - agents
438
+ can function without a knowledge store.
439
+ """
440
+
441
+ # Fact operations
442
+ @abstractmethod
443
+ async def save_fact(self, fact: Fact, scope: Scope = Scope.PROJECT) -> None:
444
+ """Save or update a fact."""
445
+ ...
446
+
447
+ @abstractmethod
448
+ async def get_fact(self, fact_id: UUID, scope: Scope = Scope.PROJECT) -> Optional[Fact]:
449
+ """Get a fact by ID."""
450
+ ...
451
+
452
+ @abstractmethod
453
+ async def get_fact_by_key(self, key: str, scope: Scope = Scope.PROJECT) -> Optional[Fact]:
454
+ """Get a fact by its key."""
455
+ ...
456
+
457
+ @abstractmethod
458
+ async def list_facts(
459
+ self,
460
+ scope: Scope = Scope.PROJECT,
461
+ fact_type: Optional[FactType] = None,
462
+ limit: int = 100,
463
+ ) -> list[Fact]:
464
+ """List facts, optionally filtered by type."""
465
+ ...
466
+
467
+ @abstractmethod
468
+ async def delete_fact(self, fact_id: UUID, scope: Scope = Scope.PROJECT) -> bool:
469
+ """Delete a fact. Returns True if it existed."""
470
+ ...
471
+
472
+ # Summary operations
473
+ @abstractmethod
474
+ async def save_summary(self, summary: Summary, scope: Scope = Scope.PROJECT) -> None:
475
+ """Save or update a summary."""
476
+ ...
477
+
478
+ @abstractmethod
479
+ async def get_summary(self, summary_id: UUID, scope: Scope = Scope.PROJECT) -> Optional[Summary]:
480
+ """Get a summary by ID."""
481
+ ...
482
+
483
+ @abstractmethod
484
+ async def get_summaries_for_conversation(
485
+ self,
486
+ conversation_id: UUID,
487
+ scope: Scope = Scope.PROJECT,
488
+ ) -> list[Summary]:
489
+ """Get all summaries for a conversation."""
490
+ ...
491
+
492
+ @abstractmethod
493
+ async def delete_summary(self, summary_id: UUID, scope: Scope = Scope.PROJECT) -> bool:
494
+ """Delete a summary. Returns True if it existed."""
495
+ ...
496
+
497
+ # Embedding operations (optional - can raise NotImplementedError)
498
+ async def save_embedding(self, embedding: Embedding, scope: Scope = Scope.PROJECT) -> None:
499
+ """Save an embedding. Optional - may raise NotImplementedError."""
500
+ raise NotImplementedError("Embeddings not supported by this store")
501
+
502
+ async def search_similar(
503
+ self,
504
+ query_vector: list[float],
505
+ limit: int = 10,
506
+ scope: Scope = Scope.PROJECT,
507
+ content_type: Optional[str] = None,
508
+ ) -> list[tuple[Embedding, float]]:
509
+ """
510
+ Search for similar embeddings. Returns (embedding, score) tuples.
511
+ Optional - may raise NotImplementedError.
512
+ """
513
+ raise NotImplementedError("Embeddings not supported by this store")
514
+
515
+ async def delete_embedding(self, embedding_id: UUID, scope: Scope = Scope.PROJECT) -> bool:
516
+ """Delete an embedding. Optional - may raise NotImplementedError."""
517
+ raise NotImplementedError("Embeddings not supported by this store")
518
+
519
+ async def close(self) -> None:
520
+ """Close any connections. Override if needed."""
521
+ pass
522
+
523
+
524
+ # =============================================================================
525
+ # Audit/History Models and Store
526
+ # =============================================================================
527
+
528
+
529
+ class AuditEventType(str, Enum):
530
+ """Type of audit event."""
531
+
532
+ # Conversation events
533
+ CONVERSATION_START = "conversation_start"
534
+ CONVERSATION_END = "conversation_end"
535
+ MESSAGE_SENT = "message_sent"
536
+ MESSAGE_RECEIVED = "message_received"
537
+
538
+ # Tool events
539
+ TOOL_CALL = "tool_call"
540
+ TOOL_RESULT = "tool_result"
541
+ TOOL_ERROR = "tool_error"
542
+
543
+ # Agent events
544
+ AGENT_START = "agent_start"
545
+ AGENT_END = "agent_end"
546
+ AGENT_ERROR = "agent_error"
547
+
548
+ # System events
549
+ CHECKPOINT_SAVED = "checkpoint_saved"
550
+ CHECKPOINT_RESTORED = "checkpoint_restored"
551
+
552
+ CUSTOM = "custom"
553
+
554
+
555
+ class ErrorSeverity(str, Enum):
556
+ """Severity level for errors."""
557
+
558
+ DEBUG = "debug"
559
+ INFO = "info"
560
+ WARNING = "warning"
561
+ ERROR = "error"
562
+ CRITICAL = "critical"
563
+
564
+
565
+ @dataclass
566
+ class AuditEntry:
567
+ """An audit log entry for tracking interactions."""
568
+
569
+ id: UUID
570
+ event_type: AuditEventType
571
+ timestamp: datetime = field(default_factory=datetime.utcnow)
572
+
573
+ # Context
574
+ conversation_id: Optional[UUID] = None
575
+ run_id: Optional[UUID] = None
576
+ agent_key: Optional[str] = None
577
+
578
+ # Event details
579
+ action: str = "" # Human-readable action description
580
+ details: dict = field(default_factory=dict) # Event-specific data
581
+
582
+ # Actor information
583
+ actor_type: str = "agent" # agent, user, system
584
+ actor_id: Optional[str] = None
585
+
586
+ # Request/response tracking
587
+ request_id: Optional[str] = None
588
+ parent_event_id: Optional[UUID] = None # For nested events
589
+
590
+ metadata: dict = field(default_factory=dict)
591
+
592
+
593
+ @dataclass
594
+ class ErrorRecord:
595
+ """A record of an error for debugging."""
596
+
597
+ id: UUID
598
+ timestamp: datetime = field(default_factory=datetime.utcnow)
599
+ severity: ErrorSeverity = ErrorSeverity.ERROR
600
+
601
+ # Error details
602
+ error_type: str = "" # Exception class name
603
+ message: str = ""
604
+ stack_trace: Optional[str] = None
605
+
606
+ # Context
607
+ conversation_id: Optional[UUID] = None
608
+ run_id: Optional[UUID] = None
609
+ agent_key: Optional[str] = None
610
+
611
+ # What was happening when error occurred
612
+ context: dict = field(default_factory=dict)
613
+
614
+ # Resolution tracking
615
+ resolved: bool = False
616
+ resolved_at: Optional[datetime] = None
617
+ resolution_notes: Optional[str] = None
618
+
619
+ metadata: dict = field(default_factory=dict)
620
+
621
+
622
+ @dataclass
623
+ class PerformanceMetric:
624
+ """A performance metric for monitoring."""
625
+
626
+ id: UUID
627
+ name: str # Metric name (e.g., "llm_latency", "tool_execution_time")
628
+ value: float # Metric value
629
+ unit: str = "" # Unit of measurement (ms, tokens, etc.)
630
+
631
+ timestamp: datetime = field(default_factory=datetime.utcnow)
632
+
633
+ # Context
634
+ conversation_id: Optional[UUID] = None
635
+ run_id: Optional[UUID] = None
636
+ agent_key: Optional[str] = None
637
+
638
+ # Additional dimensions for grouping/filtering
639
+ tags: dict = field(default_factory=dict)
640
+
641
+ metadata: dict = field(default_factory=dict)
642
+
643
+
644
+ class AuditStore(ABC):
645
+ """
646
+ Abstract interface for audit and history storage.
647
+
648
+ Audit stores handle interaction logs, error history, and
649
+ performance metrics. This is optional - agents can function
650
+ without an audit store.
651
+ """
652
+
653
+ # Audit entry operations
654
+ @abstractmethod
655
+ async def log_event(self, entry: AuditEntry, scope: Scope = Scope.PROJECT) -> None:
656
+ """Log an audit event."""
657
+ ...
658
+
659
+ @abstractmethod
660
+ async def get_events(
661
+ self,
662
+ scope: Scope = Scope.PROJECT,
663
+ conversation_id: Optional[UUID] = None,
664
+ run_id: Optional[UUID] = None,
665
+ event_types: Optional[list[AuditEventType]] = None,
666
+ start_time: Optional[datetime] = None,
667
+ end_time: Optional[datetime] = None,
668
+ limit: int = 100,
669
+ ) -> list[AuditEntry]:
670
+ """Get audit events with optional filters."""
671
+ ...
672
+
673
+ # Error operations
674
+ @abstractmethod
675
+ async def log_error(self, error: ErrorRecord, scope: Scope = Scope.PROJECT) -> None:
676
+ """Log an error."""
677
+ ...
678
+
679
+ @abstractmethod
680
+ async def get_errors(
681
+ self,
682
+ scope: Scope = Scope.PROJECT,
683
+ severity: Optional[ErrorSeverity] = None,
684
+ resolved: Optional[bool] = None,
685
+ start_time: Optional[datetime] = None,
686
+ end_time: Optional[datetime] = None,
687
+ limit: int = 100,
688
+ ) -> list[ErrorRecord]:
689
+ """Get errors with optional filters."""
690
+ ...
691
+
692
+ @abstractmethod
693
+ async def resolve_error(
694
+ self,
695
+ error_id: UUID,
696
+ resolution_notes: Optional[str] = None,
697
+ scope: Scope = Scope.PROJECT,
698
+ ) -> bool:
699
+ """Mark an error as resolved. Returns True if error existed."""
700
+ ...
701
+
702
+ # Performance metric operations
703
+ @abstractmethod
704
+ async def record_metric(self, metric: PerformanceMetric, scope: Scope = Scope.PROJECT) -> None:
705
+ """Record a performance metric."""
706
+ ...
707
+
708
+ @abstractmethod
709
+ async def get_metrics(
710
+ self,
711
+ name: str,
712
+ scope: Scope = Scope.PROJECT,
713
+ start_time: Optional[datetime] = None,
714
+ end_time: Optional[datetime] = None,
715
+ tags: Optional[dict] = None,
716
+ limit: int = 1000,
717
+ ) -> list[PerformanceMetric]:
718
+ """Get metrics by name with optional filters."""
719
+ ...
720
+
721
+ @abstractmethod
722
+ async def get_metric_summary(
723
+ self,
724
+ name: str,
725
+ scope: Scope = Scope.PROJECT,
726
+ start_time: Optional[datetime] = None,
727
+ end_time: Optional[datetime] = None,
728
+ ) -> dict:
729
+ """
730
+ Get summary statistics for a metric.
731
+ Returns: {count, min, max, avg, sum, p50, p95, p99}
732
+ """
733
+ ...
734
+
735
+ async def close(self) -> None:
736
+ """Close any connections. Override if needed."""
737
+ pass