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.
Files changed (52) hide show
  1. loom/__init__.py +1 -0
  2. loom/adapters/converters.py +77 -0
  3. loom/adapters/registry.py +43 -0
  4. loom/api/factory.py +77 -0
  5. loom/api/main.py +201 -0
  6. loom/builtin/__init__.py +3 -0
  7. loom/builtin/memory/__init__.py +3 -0
  8. loom/builtin/memory/metabolic.py +96 -0
  9. loom/builtin/memory/pso.py +41 -0
  10. loom/builtin/memory/sanitizers.py +39 -0
  11. loom/builtin/memory/validators.py +55 -0
  12. loom/config/tool.py +63 -0
  13. loom/infra/__init__.py +0 -0
  14. loom/infra/llm.py +44 -0
  15. loom/infra/logging.py +42 -0
  16. loom/infra/store.py +39 -0
  17. loom/infra/transport/memory.py +112 -0
  18. loom/infra/transport/nats.py +170 -0
  19. loom/infra/transport/redis.py +161 -0
  20. loom/interfaces/llm.py +45 -0
  21. loom/interfaces/memory.py +50 -0
  22. loom/interfaces/store.py +29 -0
  23. loom/interfaces/transport.py +35 -0
  24. loom/kernel/__init__.py +0 -0
  25. loom/kernel/base_interceptor.py +97 -0
  26. loom/kernel/bus.py +85 -0
  27. loom/kernel/dispatcher.py +58 -0
  28. loom/kernel/interceptors/__init__.py +14 -0
  29. loom/kernel/interceptors/adaptive.py +567 -0
  30. loom/kernel/interceptors/budget.py +60 -0
  31. loom/kernel/interceptors/depth.py +45 -0
  32. loom/kernel/interceptors/hitl.py +51 -0
  33. loom/kernel/interceptors/studio.py +129 -0
  34. loom/kernel/interceptors/timeout.py +27 -0
  35. loom/kernel/state.py +71 -0
  36. loom/memory/hierarchical.py +124 -0
  37. loom/node/__init__.py +0 -0
  38. loom/node/agent.py +252 -0
  39. loom/node/base.py +121 -0
  40. loom/node/crew.py +105 -0
  41. loom/node/router.py +77 -0
  42. loom/node/tool.py +50 -0
  43. loom/protocol/__init__.py +0 -0
  44. loom/protocol/cloudevents.py +73 -0
  45. loom/protocol/interfaces.py +164 -0
  46. loom/protocol/mcp.py +97 -0
  47. loom/protocol/memory_operations.py +51 -0
  48. loom/protocol/patch.py +93 -0
  49. loom_agent-0.3.3.dist-info/LICENSE +204 -0
  50. loom_agent-0.3.3.dist-info/METADATA +139 -0
  51. loom_agent-0.3.3.dist-info/RECORD +52 -0
  52. 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
@@ -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
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
+