loom-agent 0.3.3__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.
- loom/__init__.py +1 -0
- loom/adapters/converters.py +77 -0
- loom/adapters/registry.py +43 -0
- loom/api/factory.py +77 -0
- loom/api/main.py +201 -0
- loom/builtin/__init__.py +3 -0
- loom/builtin/memory/__init__.py +3 -0
- loom/builtin/memory/metabolic.py +96 -0
- loom/builtin/memory/pso.py +41 -0
- loom/builtin/memory/sanitizers.py +39 -0
- loom/builtin/memory/validators.py +55 -0
- loom/config/tool.py +63 -0
- loom/infra/__init__.py +0 -0
- loom/infra/llm.py +44 -0
- loom/infra/logging.py +42 -0
- loom/infra/store.py +39 -0
- loom/infra/transport/memory.py +112 -0
- loom/infra/transport/nats.py +170 -0
- loom/infra/transport/redis.py +161 -0
- loom/interfaces/llm.py +45 -0
- loom/interfaces/memory.py +50 -0
- loom/interfaces/store.py +29 -0
- loom/interfaces/transport.py +35 -0
- loom/kernel/__init__.py +0 -0
- loom/kernel/base_interceptor.py +97 -0
- loom/kernel/bus.py +85 -0
- loom/kernel/dispatcher.py +58 -0
- loom/kernel/interceptors/__init__.py +14 -0
- loom/kernel/interceptors/adaptive.py +567 -0
- loom/kernel/interceptors/budget.py +60 -0
- loom/kernel/interceptors/depth.py +45 -0
- loom/kernel/interceptors/hitl.py +51 -0
- loom/kernel/interceptors/studio.py +129 -0
- loom/kernel/interceptors/timeout.py +27 -0
- loom/kernel/state.py +71 -0
- loom/memory/hierarchical.py +124 -0
- loom/node/__init__.py +0 -0
- loom/node/agent.py +252 -0
- loom/node/base.py +121 -0
- loom/node/crew.py +105 -0
- loom/node/router.py +77 -0
- loom/node/tool.py +50 -0
- loom/protocol/__init__.py +0 -0
- loom/protocol/cloudevents.py +73 -0
- loom/protocol/interfaces.py +164 -0
- loom/protocol/mcp.py +97 -0
- loom/protocol/memory_operations.py +51 -0
- loom/protocol/patch.py +93 -0
- loom_agent-0.3.3.dist-info/LICENSE +204 -0
- loom_agent-0.3.3.dist-info/METADATA +139 -0
- loom_agent-0.3.3.dist-info/RECORD +52 -0
- loom_agent-0.3.3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory Interface
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
class MemoryEntry(BaseModel):
|
|
10
|
+
"""
|
|
11
|
+
A single unit of memory.
|
|
12
|
+
"""
|
|
13
|
+
role: str
|
|
14
|
+
content: str
|
|
15
|
+
timestamp: float = Field(default_factory=lambda: __import__("time").time())
|
|
16
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
17
|
+
tier: str = "session" # ephemeral, working, session, longterm
|
|
18
|
+
|
|
19
|
+
from loom.protocol.interfaces import MemoryStrategy
|
|
20
|
+
|
|
21
|
+
class MemoryInterface(MemoryStrategy, ABC):
|
|
22
|
+
"""
|
|
23
|
+
Abstract Base Class for Agent Memory.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
async def add(self, role: str, content: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
|
28
|
+
"""Add a memory entry."""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
async def get_context(self, task: str = "") -> str:
|
|
33
|
+
"""
|
|
34
|
+
Get full context formatted for the LLM.
|
|
35
|
+
May involve retrieval relevant to the 'task'.
|
|
36
|
+
"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
async def get_recent(self, limit: int = 10) -> List[Dict[str, Any]]:
|
|
41
|
+
"""
|
|
42
|
+
Get recent memory entries as a list of dicts (role/content).
|
|
43
|
+
Useful for Chat History.
|
|
44
|
+
"""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
async def clear(self) -> None:
|
|
49
|
+
"""Clear short-term memory."""
|
|
50
|
+
pass
|
loom/interfaces/store.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Event Store Interface
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import List, Optional, Dict, Any
|
|
7
|
+
|
|
8
|
+
from loom.protocol.cloudevents import CloudEvent
|
|
9
|
+
|
|
10
|
+
class EventStore(ABC):
|
|
11
|
+
"""
|
|
12
|
+
Abstract Interface for Event Persistence.
|
|
13
|
+
Decouples the Event Bus from the storage mechanism (Memory, Redis, SQL).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
async def append(self, event: CloudEvent) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Persist a single event.
|
|
20
|
+
"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
async def get_events(self, limit: int = 100, offset: int = 0, **filters) -> List[CloudEvent]:
|
|
25
|
+
"""
|
|
26
|
+
Retrieve events with optional filtering.
|
|
27
|
+
Filters can match on standard CloudEvent attributes (source, type, etc.)
|
|
28
|
+
"""
|
|
29
|
+
pass
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transport Interface (Connectivity Layer)
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Callable, Awaitable
|
|
7
|
+
from loom.protocol.cloudevents import CloudEvent
|
|
8
|
+
|
|
9
|
+
EventHandler = Callable[[CloudEvent], Awaitable[None]]
|
|
10
|
+
|
|
11
|
+
class Transport(ABC):
|
|
12
|
+
"""
|
|
13
|
+
Abstract Base Class for Event Transport.
|
|
14
|
+
Responsible for delivering events between components (local or remote).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
async def connect(self) -> None:
|
|
19
|
+
"""Establish connection to the transport layer."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
async def disconnect(self) -> None:
|
|
24
|
+
"""Close connection."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
async def publish(self, topic: str, event: CloudEvent) -> None:
|
|
29
|
+
"""Publish an event to a specific topic."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
async def subscribe(self, topic: str, handler: EventHandler) -> None:
|
|
34
|
+
"""Subscribe to a topic."""
|
|
35
|
+
pass
|
loom/kernel/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Middleware Interceptors (Kernel)
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Optional, Set
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
from loom.protocol.cloudevents import CloudEvent
|
|
10
|
+
|
|
11
|
+
class Interceptor(ABC):
|
|
12
|
+
"""
|
|
13
|
+
Abstract Base Class for Interceptors.
|
|
14
|
+
Allows AOP-style cross-cutting concerns (Auth, Logging, Budget).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
async def pre_invoke(self, event: CloudEvent) -> Optional[CloudEvent]:
|
|
19
|
+
"""
|
|
20
|
+
Called before the event is dispatched to a handler.
|
|
21
|
+
Return the event (modified or not) to proceed.
|
|
22
|
+
Return None to halt execution (block/filter).
|
|
23
|
+
"""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
async def post_invoke(self, event: CloudEvent) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Called after the event has been processed.
|
|
30
|
+
"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
class BudgetInterceptor(Interceptor):
|
|
34
|
+
"""
|
|
35
|
+
Simulated Token Budget Interceptor.
|
|
36
|
+
"""
|
|
37
|
+
def __init__(self, max_tokens: int = 100000):
|
|
38
|
+
self.max_tokens = max_tokens
|
|
39
|
+
self.used_tokens = 0
|
|
40
|
+
|
|
41
|
+
async def pre_invoke(self, event: CloudEvent) -> Optional[CloudEvent]:
|
|
42
|
+
# Check if event carries token cost estimation
|
|
43
|
+
# This is a simplification.
|
|
44
|
+
if "token_usage" in event.data:
|
|
45
|
+
cost = event.data["token_usage"].get("estimated", 0)
|
|
46
|
+
if self.used_tokens + cost > self.max_tokens:
|
|
47
|
+
print(f"⚠️ Budget exceeded: {self.used_tokens}/{self.max_tokens}")
|
|
48
|
+
return None
|
|
49
|
+
return event
|
|
50
|
+
|
|
51
|
+
async def post_invoke(self, event: CloudEvent) -> None:
|
|
52
|
+
if "token_usage" in event.data:
|
|
53
|
+
actual = event.data["token_usage"].get("actual", 0)
|
|
54
|
+
self.used_tokens += actual
|
|
55
|
+
|
|
56
|
+
class TracingInterceptor(Interceptor):
|
|
57
|
+
"""
|
|
58
|
+
Injects Distributed Tracing Context (W3C Trace Parent).
|
|
59
|
+
"""
|
|
60
|
+
async def pre_invoke(self, event: CloudEvent) -> Optional[CloudEvent]:
|
|
61
|
+
if not event.traceparent:
|
|
62
|
+
# Generate new trace
|
|
63
|
+
trace_id = uuid.uuid4().hex
|
|
64
|
+
span_id = uuid.uuid4().hex[:16]
|
|
65
|
+
event.traceparent = f"00-{trace_id}-{span_id}-01"
|
|
66
|
+
return event
|
|
67
|
+
|
|
68
|
+
async def post_invoke(self, event: CloudEvent) -> None:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
class AuthInterceptor(Interceptor):
|
|
72
|
+
"""
|
|
73
|
+
Basic Source Verification.
|
|
74
|
+
"""
|
|
75
|
+
def __init__(self, allowed_prefixes: Set[str]):
|
|
76
|
+
self.allowed_prefixes = allowed_prefixes
|
|
77
|
+
|
|
78
|
+
async def pre_invoke(self, event: CloudEvent) -> Optional[CloudEvent]:
|
|
79
|
+
if not event.source:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
# Check simplified prefix
|
|
83
|
+
# e.g. source="/agent/foo", prefix="agent"
|
|
84
|
+
# source="agent", prefix="agent"
|
|
85
|
+
parts = event.source.strip("/").split("/")
|
|
86
|
+
if not parts:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
prefix = parts[0]
|
|
90
|
+
if prefix not in self.allowed_prefixes:
|
|
91
|
+
print(f"🚫 Unauthorized source: {event.source}")
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
return event
|
|
95
|
+
|
|
96
|
+
async def post_invoke(self, event: CloudEvent) -> None:
|
|
97
|
+
pass
|
loom/kernel/bus.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Universal Event Bus (Kernel)
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Dict, List, Callable, Awaitable, Optional, Any
|
|
7
|
+
|
|
8
|
+
from loom.protocol.cloudevents import CloudEvent
|
|
9
|
+
from loom.interfaces.store import EventStore
|
|
10
|
+
from loom.infra.store import InMemoryEventStore
|
|
11
|
+
from loom.interfaces.transport import Transport, EventHandler
|
|
12
|
+
from loom.infra.transport.memory import InMemoryTransport
|
|
13
|
+
|
|
14
|
+
class UniversalEventBus:
|
|
15
|
+
"""
|
|
16
|
+
Universal Event Bus based on Event Sourcing.
|
|
17
|
+
Delegates routing to a Transport layer.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, store: EventStore = None, transport: Transport = None):
|
|
21
|
+
self.store = store or InMemoryEventStore()
|
|
22
|
+
self.transport = transport or InMemoryTransport()
|
|
23
|
+
|
|
24
|
+
async def connect(self):
|
|
25
|
+
"""Connect the underlying transport."""
|
|
26
|
+
await self.transport.connect()
|
|
27
|
+
|
|
28
|
+
async def disconnect(self):
|
|
29
|
+
"""Disconnect the underlying transport."""
|
|
30
|
+
await self.transport.disconnect()
|
|
31
|
+
|
|
32
|
+
async def publish(self, event: CloudEvent) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Publish an event to the bus.
|
|
35
|
+
1. Persist to store.
|
|
36
|
+
2. Route to subscribers via Transport.
|
|
37
|
+
"""
|
|
38
|
+
# 1. Persist
|
|
39
|
+
await self.store.append(event)
|
|
40
|
+
|
|
41
|
+
# 2. Route via Transport
|
|
42
|
+
topic = self._get_topic(event)
|
|
43
|
+
|
|
44
|
+
# Ensure connected
|
|
45
|
+
# (Optimistically connect. In prod, connect() called at startup app.start())
|
|
46
|
+
await self.transport.connect()
|
|
47
|
+
|
|
48
|
+
await self.transport.publish(topic, event)
|
|
49
|
+
|
|
50
|
+
async def subscribe(self, topic: str, handler: Callable[[CloudEvent], Awaitable[None]]):
|
|
51
|
+
"""Register a handler for a topic."""
|
|
52
|
+
# optimistic connect
|
|
53
|
+
await self.transport.connect()
|
|
54
|
+
await self.transport.subscribe(topic, handler)
|
|
55
|
+
|
|
56
|
+
async def unsubscribe(self, topic: str, handler: Callable[[CloudEvent], Awaitable[None]]):
|
|
57
|
+
"""
|
|
58
|
+
Unregister a handler from a topic.
|
|
59
|
+
|
|
60
|
+
FIXED: Added to prevent memory leaks from accumulated handlers.
|
|
61
|
+
Delegates to transport layer.
|
|
62
|
+
"""
|
|
63
|
+
await self.transport.unsubscribe(topic, handler)
|
|
64
|
+
|
|
65
|
+
def _get_topic(self, event: CloudEvent) -> str:
|
|
66
|
+
"""Construct topic string from event."""
|
|
67
|
+
# Special routing for requests: use subject (target) if present
|
|
68
|
+
if event.subject and (event.type == "node.request" or event.type == "node.call"):
|
|
69
|
+
safe_subject = event.subject.strip("/")
|
|
70
|
+
return f"{event.type}/{safe_subject}"
|
|
71
|
+
|
|
72
|
+
# Default: route by source (Origin)
|
|
73
|
+
safe_source = event.source.strip("/")
|
|
74
|
+
return f"{event.type}/{safe_source}"
|
|
75
|
+
|
|
76
|
+
async def get_events(self) -> List[CloudEvent]:
|
|
77
|
+
"""Return all events in the store."""
|
|
78
|
+
return await self.store.get_events(limit=1000)
|
|
79
|
+
|
|
80
|
+
async def clear(self):
|
|
81
|
+
"""Clear state (for testing)."""
|
|
82
|
+
if hasattr(self.store, "clear"):
|
|
83
|
+
self.store.clear()
|
|
84
|
+
|
|
85
|
+
await self.transport.disconnect()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Event Dispatcher (Kernel)
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Any
|
|
6
|
+
|
|
7
|
+
from loom.protocol.cloudevents import CloudEvent
|
|
8
|
+
from loom.kernel.bus import UniversalEventBus
|
|
9
|
+
from loom.kernel.base_interceptor import Interceptor
|
|
10
|
+
|
|
11
|
+
class Dispatcher:
|
|
12
|
+
"""
|
|
13
|
+
Central dispatch mechanism.
|
|
14
|
+
1. Runs Interceptor Chain (Pre-invoke).
|
|
15
|
+
2. Publishes to Bus.
|
|
16
|
+
3. Runs Interceptor Chain (Post-invoke).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, bus: UniversalEventBus):
|
|
20
|
+
self.bus = bus
|
|
21
|
+
self.interceptors: List[Interceptor] = []
|
|
22
|
+
|
|
23
|
+
def add_interceptor(self, interceptor: Interceptor) -> None:
|
|
24
|
+
"""Add an interceptor to the chain."""
|
|
25
|
+
self.interceptors.append(interceptor)
|
|
26
|
+
|
|
27
|
+
async def dispatch(self, event: CloudEvent) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Dispatch an event through the system.
|
|
30
|
+
"""
|
|
31
|
+
# 1. Pre-invoke Interceptors
|
|
32
|
+
current_event = event
|
|
33
|
+
for interceptor in self.interceptors:
|
|
34
|
+
current_event = await interceptor.pre_invoke(current_event)
|
|
35
|
+
if current_event is None:
|
|
36
|
+
# Blocked by interceptor
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
# 2. Publish to Bus (Routing & Persistence)
|
|
40
|
+
import asyncio
|
|
41
|
+
timeout = 30.0 # Default fallback
|
|
42
|
+
if current_event.extensions and "timeout" in current_event.extensions:
|
|
43
|
+
try:
|
|
44
|
+
timeout = float(current_event.extensions["timeout"])
|
|
45
|
+
except:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
await asyncio.wait_for(self.bus.publish(current_event), timeout=timeout)
|
|
50
|
+
except asyncio.TimeoutError:
|
|
51
|
+
print(f"timeout dispatching event {current_event.id}")
|
|
52
|
+
# We might want to raise or handle graceful failure
|
|
53
|
+
# Raising allows the caller (e.g. app.run) to catch it
|
|
54
|
+
raise
|
|
55
|
+
|
|
56
|
+
# 3. Post-invoke Interceptors (in reverse order)
|
|
57
|
+
for interceptor in reversed(self.interceptors):
|
|
58
|
+
await interceptor.post_invoke(current_event)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .timeout import TimeoutInterceptor
|
|
2
|
+
from .budget import BudgetInterceptor
|
|
3
|
+
from .depth import DepthInterceptor
|
|
4
|
+
from .hitl import HITLInterceptor
|
|
5
|
+
from loom.kernel.base_interceptor import TracingInterceptor
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"TimeoutInterceptor",
|
|
9
|
+
"BudgetInterceptor",
|
|
10
|
+
"DepthInterceptor",
|
|
11
|
+
"HITLInterceptor",
|
|
12
|
+
"TracingInterceptor"
|
|
13
|
+
]
|
|
14
|
+
|