loom-agent 0.0.1__py3-none-any.whl → 0.0.2__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.

Potentially problematic release.


This version of loom-agent might be problematic. Click here for more details.

Files changed (38) hide show
  1. loom/builtin/tools/calculator.py +4 -0
  2. loom/builtin/tools/document_search.py +5 -0
  3. loom/builtin/tools/glob.py +4 -0
  4. loom/builtin/tools/grep.py +4 -0
  5. loom/builtin/tools/http_request.py +5 -0
  6. loom/builtin/tools/python_repl.py +5 -0
  7. loom/builtin/tools/read_file.py +4 -0
  8. loom/builtin/tools/task.py +5 -0
  9. loom/builtin/tools/web_search.py +4 -0
  10. loom/builtin/tools/write_file.py +4 -0
  11. loom/components/agent.py +121 -5
  12. loom/core/agent_executor.py +505 -320
  13. loom/core/compression_manager.py +17 -10
  14. loom/core/context_assembly.py +329 -0
  15. loom/core/events.py +414 -0
  16. loom/core/execution_context.py +119 -0
  17. loom/core/tool_orchestrator.py +383 -0
  18. loom/core/turn_state.py +188 -0
  19. loom/core/types.py +15 -4
  20. loom/interfaces/event_producer.py +172 -0
  21. loom/interfaces/tool.py +22 -1
  22. loom/security/__init__.py +13 -0
  23. loom/security/models.py +85 -0
  24. loom/security/path_validator.py +128 -0
  25. loom/security/validator.py +346 -0
  26. loom/tasks/PHASE_1_FOUNDATION/task_1.1_agent_events.md +121 -0
  27. loom/tasks/PHASE_1_FOUNDATION/task_1.2_streaming_api.md +521 -0
  28. loom/tasks/PHASE_1_FOUNDATION/task_1.3_context_assembler.md +606 -0
  29. loom/tasks/PHASE_2_CORE_FEATURES/task_2.1_tool_orchestrator.md +743 -0
  30. loom/tasks/PHASE_2_CORE_FEATURES/task_2.2_security_validator.md +676 -0
  31. loom/tasks/README.md +109 -0
  32. loom/tasks/__init__.py +11 -0
  33. loom/tasks/sql_placeholder.py +100 -0
  34. loom_agent-0.0.2.dist-info/METADATA +295 -0
  35. {loom_agent-0.0.1.dist-info → loom_agent-0.0.2.dist-info}/RECORD +37 -19
  36. loom_agent-0.0.1.dist-info/METADATA +0 -457
  37. {loom_agent-0.0.1.dist-info → loom_agent-0.0.2.dist-info}/WHEEL +0 -0
  38. {loom_agent-0.0.1.dist-info → loom_agent-0.0.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,172 @@
1
+ """
2
+ Event Producer Protocol for Loom 2.0
3
+
4
+ Defines the interface that all event-producing components must implement.
5
+ This enables type-safe composition of streaming agents.
6
+ """
7
+
8
+ from typing import Protocol, AsyncGenerator, runtime_checkable
9
+ from loom.core.events import AgentEvent
10
+
11
+
12
+ @runtime_checkable
13
+ class EventProducer(Protocol):
14
+ """
15
+ Protocol for components that produce AgentEvent streams.
16
+
17
+ Any component that participates in the agent execution pipeline
18
+ and produces events should implement this protocol.
19
+
20
+ Example:
21
+ ```python
22
+ class MyCustomExecutor(EventProducer):
23
+ async def produce_events(self) -> AsyncGenerator[AgentEvent, None]:
24
+ yield AgentEvent.phase_start("custom_phase")
25
+ # ... do work
26
+ yield AgentEvent.phase_end("custom_phase")
27
+ ```
28
+ """
29
+
30
+ async def produce_events(self) -> AsyncGenerator[AgentEvent, None]:
31
+ """
32
+ Produce a stream of agent events.
33
+
34
+ Yields:
35
+ AgentEvent: Events representing execution progress
36
+
37
+ Example:
38
+ ```python
39
+ async for event in producer.produce_events():
40
+ if event.type == AgentEventType.LLM_DELTA:
41
+ print(event.content, end="")
42
+ ```
43
+ """
44
+ ...
45
+
46
+
47
+ @runtime_checkable
48
+ class ToolExecutor(Protocol):
49
+ """
50
+ Protocol for tool execution components that produce events.
51
+
52
+ This is a specialized EventProducer for tool execution.
53
+ """
54
+
55
+ async def execute_tool(
56
+ self,
57
+ tool_name: str,
58
+ arguments: dict
59
+ ) -> AsyncGenerator[AgentEvent, None]:
60
+ """
61
+ Execute a tool and yield progress events.
62
+
63
+ Args:
64
+ tool_name: Name of the tool to execute
65
+ arguments: Tool arguments
66
+
67
+ Yields:
68
+ AgentEvent: Tool execution events (TOOL_EXECUTION_START,
69
+ TOOL_PROGRESS, TOOL_RESULT, or TOOL_ERROR)
70
+ """
71
+ ...
72
+
73
+
74
+ @runtime_checkable
75
+ class LLMEventProducer(Protocol):
76
+ """
77
+ Protocol for LLM components that produce streaming events.
78
+
79
+ This enables streaming LLM calls with real-time token generation.
80
+ """
81
+
82
+ async def stream_with_events(
83
+ self,
84
+ messages: list,
85
+ tools: list = None
86
+ ) -> AsyncGenerator[AgentEvent, None]:
87
+ """
88
+ Stream LLM generation as AgentEvents.
89
+
90
+ Args:
91
+ messages: Conversation messages
92
+ tools: Optional tool definitions
93
+
94
+ Yields:
95
+ AgentEvent: LLM events (LLM_START, LLM_DELTA, LLM_COMPLETE,
96
+ LLM_TOOL_CALLS)
97
+ """
98
+ ...
99
+
100
+
101
+ # ===== Helper Functions =====
102
+
103
+ async def merge_event_streams(
104
+ *producers: EventProducer
105
+ ) -> AsyncGenerator[AgentEvent, None]:
106
+ """
107
+ Merge multiple event streams into a single stream.
108
+
109
+ This is useful for parallel execution where multiple components
110
+ produce events concurrently.
111
+
112
+ Args:
113
+ *producers: EventProducer instances to merge
114
+
115
+ Yields:
116
+ AgentEvent: Events from all producers in arrival order
117
+
118
+ Example:
119
+ ```python
120
+ async for event in merge_event_streams(executor1, executor2):
121
+ print(event)
122
+ ```
123
+ """
124
+ import asyncio
125
+
126
+ # Create tasks for all producers
127
+ tasks = [
128
+ asyncio.create_task(_consume_producer(producer))
129
+ for producer in producers
130
+ ]
131
+
132
+ # Yield events as they arrive
133
+ pending = set(tasks)
134
+ while pending:
135
+ done, pending = await asyncio.wait(
136
+ pending,
137
+ return_when=asyncio.FIRST_COMPLETED
138
+ )
139
+
140
+ for task in done:
141
+ events = task.result()
142
+ for event in events:
143
+ yield event
144
+
145
+
146
+ async def _consume_producer(producer: EventProducer) -> list:
147
+ """Helper to consume a producer into a list"""
148
+ events = []
149
+ async for event in producer.produce_events():
150
+ events.append(event)
151
+ return events
152
+
153
+
154
+ async def collect_events(
155
+ producer: EventProducer
156
+ ) -> list:
157
+ """
158
+ Collect all events from a producer into a list.
159
+
160
+ Args:
161
+ producer: EventProducer to consume
162
+
163
+ Returns:
164
+ List of all events produced
165
+
166
+ Example:
167
+ ```python
168
+ events = await collect_events(my_executor)
169
+ print(f"Generated {len(events)} events")
170
+ ```
171
+ """
172
+ return await _consume_producer(producer)
loom/interfaces/tool.py CHANGED
@@ -7,12 +7,32 @@ from pydantic import BaseModel
7
7
 
8
8
 
9
9
  class BaseTool(ABC):
10
- """工具基础接口。"""
10
+ """
11
+ 工具基础接口。
12
+
13
+ Attributes:
14
+ name: Tool name (unique identifier)
15
+ description: Tool description for LLM
16
+ args_schema: Pydantic model for argument validation
17
+ is_read_only: Whether tool only reads data (safe to parallelize) 🆕
18
+ category: Tool category (general/destructive/network) 🆕
19
+ requires_confirmation: Whether tool requires user confirmation 🆕
20
+ """
11
21
 
12
22
  name: str
13
23
  description: str
14
24
  args_schema: type[BaseModel]
15
25
 
26
+ # 🆕 Loom 2.0 - Orchestration attributes
27
+ is_read_only: bool = False
28
+ """Whether this tool only reads data (safe to parallelize with other read-only tools)."""
29
+
30
+ category: str = "general"
31
+ """Tool category: 'general', 'destructive', 'network'."""
32
+
33
+ requires_confirmation: bool = False
34
+ """Whether this tool requires explicit user confirmation before execution."""
35
+
16
36
  @abstractmethod
17
37
  async def run(self, **kwargs) -> Any:
18
38
  raise NotImplementedError
@@ -23,5 +43,6 @@ class BaseTool(ABC):
23
43
 
24
44
  @property
25
45
  def is_concurrency_safe(self) -> bool:
46
+ """Legacy attribute for backward compatibility."""
26
47
  return True
27
48
 
@@ -0,0 +1,13 @@
1
+ """Security module for Loom 2.0"""
2
+
3
+ from loom.security.models import RiskLevel, SecurityDecision, PathSecurityResult
4
+ from loom.security.path_validator import PathSecurityValidator
5
+ from loom.security.validator import SecurityValidator
6
+
7
+ __all__ = [
8
+ "RiskLevel",
9
+ "SecurityDecision",
10
+ "PathSecurityResult",
11
+ "PathSecurityValidator",
12
+ "SecurityValidator",
13
+ ]
@@ -0,0 +1,85 @@
1
+ """
2
+ Security Models
3
+
4
+ Data models for security validation results.
5
+ """
6
+
7
+ from enum import Enum
8
+ from dataclasses import dataclass, field
9
+ from typing import List, Optional
10
+
11
+
12
+ class RiskLevel(str, Enum):
13
+ """Security risk levels for tool execution."""
14
+ LOW = "low"
15
+ MEDIUM = "medium"
16
+ HIGH = "high"
17
+ CRITICAL = "critical"
18
+
19
+ def __lt__(self, other):
20
+ """Allow comparison of risk levels."""
21
+ order = {
22
+ RiskLevel.LOW: 0,
23
+ RiskLevel.MEDIUM: 1,
24
+ RiskLevel.HIGH: 2,
25
+ RiskLevel.CRITICAL: 3
26
+ }
27
+ return order[self] < order[other]
28
+
29
+ def __gt__(self, other):
30
+ order = {
31
+ RiskLevel.LOW: 0,
32
+ RiskLevel.MEDIUM: 1,
33
+ RiskLevel.HIGH: 2,
34
+ RiskLevel.CRITICAL: 3
35
+ }
36
+ return order[self] > order[other]
37
+
38
+
39
+ @dataclass
40
+ class SecurityDecision:
41
+ """
42
+ Result of security validation.
43
+
44
+ Attributes:
45
+ allow: Whether the operation is allowed
46
+ risk_level: Assessed risk level
47
+ reason: Human-readable reason for decision
48
+ failed_layers: List of security layers that failed
49
+ warnings: Non-blocking warnings
50
+ """
51
+ allow: bool
52
+ risk_level: RiskLevel
53
+ reason: str
54
+ failed_layers: List[str] = field(default_factory=list)
55
+ warnings: List[str] = field(default_factory=list)
56
+
57
+ @property
58
+ def is_safe(self) -> bool:
59
+ """Check if decision is safe to execute."""
60
+ return self.allow and self.risk_level in [RiskLevel.LOW, RiskLevel.MEDIUM]
61
+
62
+ def __repr__(self) -> str:
63
+ status = "ALLOWED" if self.allow else "BLOCKED"
64
+ return f"SecurityDecision({status}, risk={self.risk_level.value}, reason='{self.reason}')"
65
+
66
+
67
+ @dataclass
68
+ class PathSecurityResult:
69
+ """
70
+ Result of path security validation.
71
+
72
+ Attributes:
73
+ is_safe: Whether the path is safe to access
74
+ normalized_path: Resolved absolute path
75
+ warnings: Non-critical warnings
76
+ violations: Security violations found
77
+ """
78
+ is_safe: bool
79
+ normalized_path: str
80
+ warnings: List[str] = field(default_factory=list)
81
+ violations: List[str] = field(default_factory=list)
82
+
83
+ def __repr__(self) -> str:
84
+ status = "SAFE" if self.is_safe else "UNSAFE"
85
+ return f"PathSecurityResult({status}, violations={len(self.violations)})"
@@ -0,0 +1,128 @@
1
+ """
2
+ Path Security Validator
3
+
4
+ Validates file paths for security issues like path traversal and system path access.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import List, Optional
9
+ from loom.security.models import PathSecurityResult
10
+
11
+
12
+ # System paths that should never be accessed
13
+ SYSTEM_PATHS = [
14
+ "/etc",
15
+ "/sys",
16
+ "/proc",
17
+ "/dev",
18
+ "/boot",
19
+ "/root",
20
+ "/var/log",
21
+ "/bin",
22
+ "/sbin",
23
+ "/usr/bin",
24
+ "/usr/sbin",
25
+ ]
26
+
27
+
28
+ class PathSecurityValidator:
29
+ """
30
+ Validate file paths for security issues.
31
+
32
+ Checks for:
33
+ - Path traversal attacks (../)
34
+ - Absolute paths outside working directory
35
+ - System path access
36
+ - Invalid path constructions
37
+
38
+ Example:
39
+ ```python
40
+ validator = PathSecurityValidator(working_dir=Path("/Users/project"))
41
+
42
+ # Safe path
43
+ result = validator.validate_path("src/main.py")
44
+ assert result.is_safe
45
+
46
+ # Path traversal attempt
47
+ result = validator.validate_path("../../etc/passwd")
48
+ assert not result.is_safe
49
+ ```
50
+ """
51
+
52
+ def __init__(self, working_dir: Optional[Path] = None):
53
+ """
54
+ Initialize path validator.
55
+
56
+ Args:
57
+ working_dir: Working directory to enforce boundaries (defaults to cwd)
58
+ """
59
+ self.working_dir = (working_dir or Path.cwd()).resolve()
60
+
61
+ def validate_path(self, path: str) -> PathSecurityResult:
62
+ """
63
+ Validate a file path for security issues.
64
+
65
+ Args:
66
+ path: File path to validate
67
+
68
+ Returns:
69
+ PathSecurityResult with validation outcome
70
+ """
71
+ violations: List[str] = []
72
+ warnings: List[str] = []
73
+ normalized_path = path
74
+
75
+ # Check 1: Detect explicit path traversal
76
+ if ".." in path:
77
+ violations.append("Path traversal detected (..)")
78
+
79
+ # Check 2: Resolve and validate boundaries
80
+ try:
81
+ # Handle both relative and absolute paths
82
+ if Path(path).is_absolute():
83
+ resolved = Path(path).resolve()
84
+ else:
85
+ resolved = (self.working_dir / path).resolve()
86
+
87
+ normalized_path = str(resolved)
88
+
89
+ # Check if within working directory
90
+ try:
91
+ resolved.relative_to(self.working_dir)
92
+ except ValueError:
93
+ violations.append(
94
+ f"Path outside working directory: {resolved} "
95
+ f"(working dir: {self.working_dir})"
96
+ )
97
+
98
+ # Check 3: System path protection
99
+ for sys_path in SYSTEM_PATHS:
100
+ if str(resolved).startswith(sys_path):
101
+ violations.append(
102
+ f"System path access denied: {sys_path}"
103
+ )
104
+ break
105
+
106
+ except Exception as e:
107
+ violations.append(f"Path resolution failed: {e}")
108
+
109
+ is_safe = len(violations) == 0
110
+
111
+ return PathSecurityResult(
112
+ is_safe=is_safe,
113
+ normalized_path=normalized_path,
114
+ warnings=warnings,
115
+ violations=violations
116
+ )
117
+
118
+ def is_safe_path(self, path: str) -> bool:
119
+ """
120
+ Quick check if path is safe.
121
+
122
+ Args:
123
+ path: Path to check
124
+
125
+ Returns:
126
+ True if path is safe
127
+ """
128
+ return self.validate_path(path).is_safe