aury-agent 0.0.4__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 (149) hide show
  1. aury/__init__.py +2 -0
  2. aury/agents/__init__.py +55 -0
  3. aury/agents/a2a/__init__.py +168 -0
  4. aury/agents/backends/__init__.py +196 -0
  5. aury/agents/backends/artifact/__init__.py +9 -0
  6. aury/agents/backends/artifact/memory.py +130 -0
  7. aury/agents/backends/artifact/types.py +133 -0
  8. aury/agents/backends/code/__init__.py +65 -0
  9. aury/agents/backends/file/__init__.py +11 -0
  10. aury/agents/backends/file/local.py +66 -0
  11. aury/agents/backends/file/types.py +40 -0
  12. aury/agents/backends/invocation/__init__.py +8 -0
  13. aury/agents/backends/invocation/memory.py +81 -0
  14. aury/agents/backends/invocation/types.py +110 -0
  15. aury/agents/backends/memory/__init__.py +8 -0
  16. aury/agents/backends/memory/memory.py +179 -0
  17. aury/agents/backends/memory/types.py +136 -0
  18. aury/agents/backends/message/__init__.py +9 -0
  19. aury/agents/backends/message/memory.py +122 -0
  20. aury/agents/backends/message/types.py +124 -0
  21. aury/agents/backends/sandbox.py +275 -0
  22. aury/agents/backends/session/__init__.py +8 -0
  23. aury/agents/backends/session/memory.py +93 -0
  24. aury/agents/backends/session/types.py +124 -0
  25. aury/agents/backends/shell/__init__.py +11 -0
  26. aury/agents/backends/shell/local.py +110 -0
  27. aury/agents/backends/shell/types.py +55 -0
  28. aury/agents/backends/shell.py +209 -0
  29. aury/agents/backends/snapshot/__init__.py +19 -0
  30. aury/agents/backends/snapshot/git.py +95 -0
  31. aury/agents/backends/snapshot/hybrid.py +125 -0
  32. aury/agents/backends/snapshot/memory.py +86 -0
  33. aury/agents/backends/snapshot/types.py +59 -0
  34. aury/agents/backends/state/__init__.py +29 -0
  35. aury/agents/backends/state/composite.py +49 -0
  36. aury/agents/backends/state/file.py +57 -0
  37. aury/agents/backends/state/memory.py +52 -0
  38. aury/agents/backends/state/sqlite.py +262 -0
  39. aury/agents/backends/state/types.py +178 -0
  40. aury/agents/backends/subagent/__init__.py +165 -0
  41. aury/agents/cli/__init__.py +41 -0
  42. aury/agents/cli/chat.py +239 -0
  43. aury/agents/cli/config.py +236 -0
  44. aury/agents/cli/extensions.py +460 -0
  45. aury/agents/cli/main.py +189 -0
  46. aury/agents/cli/session.py +337 -0
  47. aury/agents/cli/workflow.py +276 -0
  48. aury/agents/context_providers/__init__.py +66 -0
  49. aury/agents/context_providers/artifact.py +299 -0
  50. aury/agents/context_providers/base.py +177 -0
  51. aury/agents/context_providers/memory.py +70 -0
  52. aury/agents/context_providers/message.py +130 -0
  53. aury/agents/context_providers/skill.py +50 -0
  54. aury/agents/context_providers/subagent.py +46 -0
  55. aury/agents/context_providers/tool.py +68 -0
  56. aury/agents/core/__init__.py +83 -0
  57. aury/agents/core/base.py +573 -0
  58. aury/agents/core/context.py +797 -0
  59. aury/agents/core/context_builder.py +303 -0
  60. aury/agents/core/event_bus/__init__.py +15 -0
  61. aury/agents/core/event_bus/bus.py +203 -0
  62. aury/agents/core/factory.py +169 -0
  63. aury/agents/core/isolator.py +97 -0
  64. aury/agents/core/logging.py +95 -0
  65. aury/agents/core/parallel.py +194 -0
  66. aury/agents/core/runner.py +139 -0
  67. aury/agents/core/services/__init__.py +5 -0
  68. aury/agents/core/services/file_session.py +144 -0
  69. aury/agents/core/services/message.py +53 -0
  70. aury/agents/core/services/session.py +53 -0
  71. aury/agents/core/signals.py +109 -0
  72. aury/agents/core/state.py +363 -0
  73. aury/agents/core/types/__init__.py +107 -0
  74. aury/agents/core/types/action.py +176 -0
  75. aury/agents/core/types/artifact.py +135 -0
  76. aury/agents/core/types/block.py +736 -0
  77. aury/agents/core/types/message.py +350 -0
  78. aury/agents/core/types/recall.py +144 -0
  79. aury/agents/core/types/session.py +257 -0
  80. aury/agents/core/types/subagent.py +154 -0
  81. aury/agents/core/types/tool.py +205 -0
  82. aury/agents/eval/__init__.py +331 -0
  83. aury/agents/hitl/__init__.py +57 -0
  84. aury/agents/hitl/ask_user.py +242 -0
  85. aury/agents/hitl/compaction.py +230 -0
  86. aury/agents/hitl/exceptions.py +87 -0
  87. aury/agents/hitl/permission.py +617 -0
  88. aury/agents/hitl/revert.py +216 -0
  89. aury/agents/llm/__init__.py +31 -0
  90. aury/agents/llm/adapter.py +367 -0
  91. aury/agents/llm/openai.py +294 -0
  92. aury/agents/llm/provider.py +476 -0
  93. aury/agents/mcp/__init__.py +153 -0
  94. aury/agents/memory/__init__.py +46 -0
  95. aury/agents/memory/compaction.py +394 -0
  96. aury/agents/memory/manager.py +465 -0
  97. aury/agents/memory/processor.py +177 -0
  98. aury/agents/memory/store.py +187 -0
  99. aury/agents/memory/types.py +137 -0
  100. aury/agents/messages/__init__.py +40 -0
  101. aury/agents/messages/config.py +47 -0
  102. aury/agents/messages/raw_store.py +224 -0
  103. aury/agents/messages/store.py +118 -0
  104. aury/agents/messages/types.py +88 -0
  105. aury/agents/middleware/__init__.py +31 -0
  106. aury/agents/middleware/base.py +341 -0
  107. aury/agents/middleware/chain.py +342 -0
  108. aury/agents/middleware/message.py +129 -0
  109. aury/agents/middleware/message_container.py +126 -0
  110. aury/agents/middleware/raw_message.py +153 -0
  111. aury/agents/middleware/truncation.py +139 -0
  112. aury/agents/middleware/types.py +81 -0
  113. aury/agents/plugin.py +162 -0
  114. aury/agents/react/__init__.py +4 -0
  115. aury/agents/react/agent.py +1923 -0
  116. aury/agents/sandbox/__init__.py +23 -0
  117. aury/agents/sandbox/local.py +239 -0
  118. aury/agents/sandbox/remote.py +200 -0
  119. aury/agents/sandbox/types.py +115 -0
  120. aury/agents/skill/__init__.py +16 -0
  121. aury/agents/skill/loader.py +180 -0
  122. aury/agents/skill/types.py +83 -0
  123. aury/agents/tool/__init__.py +39 -0
  124. aury/agents/tool/builtin/__init__.py +23 -0
  125. aury/agents/tool/builtin/ask_user.py +155 -0
  126. aury/agents/tool/builtin/bash.py +107 -0
  127. aury/agents/tool/builtin/delegate.py +726 -0
  128. aury/agents/tool/builtin/edit.py +121 -0
  129. aury/agents/tool/builtin/plan.py +277 -0
  130. aury/agents/tool/builtin/read.py +91 -0
  131. aury/agents/tool/builtin/thinking.py +111 -0
  132. aury/agents/tool/builtin/yield_result.py +130 -0
  133. aury/agents/tool/decorator.py +252 -0
  134. aury/agents/tool/set.py +204 -0
  135. aury/agents/usage/__init__.py +12 -0
  136. aury/agents/usage/tracker.py +236 -0
  137. aury/agents/workflow/__init__.py +85 -0
  138. aury/agents/workflow/adapter.py +268 -0
  139. aury/agents/workflow/dag.py +116 -0
  140. aury/agents/workflow/dsl.py +575 -0
  141. aury/agents/workflow/executor.py +659 -0
  142. aury/agents/workflow/expression.py +136 -0
  143. aury/agents/workflow/parser.py +182 -0
  144. aury/agents/workflow/state.py +145 -0
  145. aury/agents/workflow/types.py +86 -0
  146. aury_agent-0.0.4.dist-info/METADATA +90 -0
  147. aury_agent-0.0.4.dist-info/RECORD +149 -0
  148. aury_agent-0.0.4.dist-info/WHEEL +4 -0
  149. aury_agent-0.0.4.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,93 @@
1
+ """In-memory session backend implementation."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime
5
+ from typing import Any
6
+
7
+
8
+ class InMemorySessionBackend:
9
+ """In-memory implementation of SessionBackend.
10
+
11
+ Suitable for testing and simple single-process use cases.
12
+ Data is lost when the process exits.
13
+ """
14
+
15
+ def __init__(self) -> None:
16
+ self._sessions: dict[str, dict[str, Any]] = {}
17
+ # Index: user_id -> list of session_ids
18
+ self._user_sessions: dict[str, list[str]] = {}
19
+
20
+ async def create(
21
+ self,
22
+ id: str,
23
+ data: dict[str, Any],
24
+ user_id: str | None = None,
25
+ ) -> None:
26
+ """Create a new session."""
27
+ session_data = {
28
+ "id": id,
29
+ "user_id": user_id,
30
+ "created_at": datetime.now().isoformat(),
31
+ "updated_at": datetime.now().isoformat(),
32
+ **data,
33
+ }
34
+ self._sessions[id] = session_data
35
+
36
+ # Update user index
37
+ if user_id:
38
+ if user_id not in self._user_sessions:
39
+ self._user_sessions[user_id] = []
40
+ self._user_sessions[user_id].append(id)
41
+
42
+ async def get(self, id: str) -> dict[str, Any] | None:
43
+ """Get session by ID."""
44
+ return self._sessions.get(id)
45
+
46
+ async def update(self, id: str, data: dict[str, Any]) -> None:
47
+ """Update session data."""
48
+ if id in self._sessions:
49
+ self._sessions[id].update(data)
50
+ self._sessions[id]["updated_at"] = datetime.now().isoformat()
51
+
52
+ async def delete(self, id: str) -> bool:
53
+ """Delete a session."""
54
+ if id not in self._sessions:
55
+ return False
56
+
57
+ session = self._sessions.pop(id)
58
+
59
+ # Update user index
60
+ user_id = session.get("user_id")
61
+ if user_id and user_id in self._user_sessions:
62
+ self._user_sessions[user_id] = [
63
+ sid for sid in self._user_sessions[user_id] if sid != id
64
+ ]
65
+
66
+ return True
67
+
68
+ async def list(
69
+ self,
70
+ user_id: str | None = None,
71
+ limit: int = 100,
72
+ offset: int = 0,
73
+ ) -> list[dict[str, Any]]:
74
+ """List sessions."""
75
+ if user_id:
76
+ # Filter by user
77
+ session_ids = self._user_sessions.get(user_id, [])
78
+ sessions = [
79
+ self._sessions[sid]
80
+ for sid in session_ids
81
+ if sid in self._sessions
82
+ ]
83
+ else:
84
+ sessions = list(self._sessions.values())
85
+
86
+ # Sort by created_at descending (newest first)
87
+ sessions.sort(key=lambda s: s.get("created_at", ""), reverse=True)
88
+
89
+ # Apply pagination
90
+ return sessions[offset:offset + limit]
91
+
92
+
93
+ __all__ = ["InMemorySessionBackend"]
@@ -0,0 +1,124 @@
1
+ """Session backend types and protocols."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Protocol, runtime_checkable, TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from ...core.context import InvocationContext
8
+
9
+
10
+ @runtime_checkable
11
+ class SessionBackend(Protocol):
12
+ """Protocol for session management.
13
+
14
+ Sessions represent a conversation scope. Each session can contain
15
+ multiple invocations (user turns).
16
+
17
+ All methods accept an optional `ctx` (InvocationContext) parameter.
18
+ When ctx is provided, session_id/user_id can be extracted from it.
19
+
20
+ Example usage:
21
+ # With explicit params
22
+ await backend.create("sess_123", {"root_agent_id": "agent"}, user_id="user_1")
23
+
24
+ # With ctx (auto-extract session_id)
25
+ await backend.get(ctx=ctx) # uses ctx.session_id
26
+
27
+ # List user's sessions
28
+ sessions = await backend.list(user_id="user_1")
29
+ """
30
+
31
+ async def create(
32
+ self,
33
+ data: dict[str, Any],
34
+ *,
35
+ id: str | None = None,
36
+ user_id: str | None = None,
37
+ ctx: "InvocationContext | None" = None,
38
+ ) -> str:
39
+ """Create a new session.
40
+
41
+ Args:
42
+ data: Session data (root_agent_id, metadata, etc.)
43
+ id: Session ID (auto-generated if None)
44
+ user_id: Optional user ID for multi-tenant isolation
45
+ ctx: Optional InvocationContext
46
+
47
+ Returns:
48
+ Created session ID
49
+ """
50
+ ...
51
+
52
+ async def get(
53
+ self,
54
+ id: str | None = None,
55
+ *,
56
+ ctx: "InvocationContext | None" = None,
57
+ ) -> dict[str, Any] | None:
58
+ """Get session by ID.
59
+
60
+ Args:
61
+ id: Session ID (or extracted from ctx.session_id)
62
+ ctx: Optional InvocationContext
63
+
64
+ Returns:
65
+ Session data dict or None if not found
66
+ """
67
+ ...
68
+
69
+ async def update(
70
+ self,
71
+ data: dict[str, Any],
72
+ *,
73
+ id: str | None = None,
74
+ ctx: "InvocationContext | None" = None,
75
+ ) -> None:
76
+ """Update session data.
77
+
78
+ Args:
79
+ data: Fields to update (partial update)
80
+ id: Session ID (or extracted from ctx.session_id)
81
+ ctx: Optional InvocationContext
82
+ """
83
+ ...
84
+
85
+ async def delete(
86
+ self,
87
+ id: str | None = None,
88
+ *,
89
+ ctx: "InvocationContext | None" = None,
90
+ ) -> bool:
91
+ """Delete a session.
92
+
93
+ Args:
94
+ id: Session ID (or extracted from ctx.session_id)
95
+ ctx: Optional InvocationContext
96
+
97
+ Returns:
98
+ True if deleted, False if not found
99
+ """
100
+ ...
101
+
102
+ async def list(
103
+ self,
104
+ *,
105
+ user_id: str | None = None,
106
+ limit: int = 100,
107
+ offset: int = 0,
108
+ ctx: "InvocationContext | None" = None,
109
+ ) -> list[dict[str, Any]]:
110
+ """List sessions.
111
+
112
+ Args:
113
+ user_id: Optional filter by user
114
+ limit: Max sessions to return
115
+ offset: Offset for pagination
116
+ ctx: Optional InvocationContext
117
+
118
+ Returns:
119
+ List of session data dicts
120
+ """
121
+ ...
122
+
123
+
124
+ __all__ = ["SessionBackend"]
@@ -0,0 +1,11 @@
1
+ """Shell backend for command execution.
2
+
3
+ Supports different execution environments:
4
+ - Local shell
5
+ - Docker/nsjail sandbox
6
+ - E2B cloud sandbox
7
+ """
8
+ from .types import ShellBackend, ShellResult
9
+ from .local import LocalShellBackend
10
+
11
+ __all__ = ["ShellBackend", "ShellResult", "LocalShellBackend"]
@@ -0,0 +1,110 @@
1
+ """Local shell execution backend."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import os
6
+ import time
7
+ from typing import AsyncIterator
8
+
9
+ from .types import ShellResult
10
+
11
+
12
+ class LocalShellBackend:
13
+ """Local shell execution backend."""
14
+
15
+ def __init__(self, default_cwd: str | None = None, shell: str = "/bin/bash"):
16
+ self.default_cwd = default_cwd
17
+ self.shell = shell
18
+
19
+ async def execute(
20
+ self,
21
+ command: str,
22
+ cwd: str | None = None,
23
+ env: dict[str, str] | None = None,
24
+ timeout: int = 120,
25
+ ) -> ShellResult:
26
+ """Execute command locally."""
27
+ work_dir = cwd or self.default_cwd or os.getcwd()
28
+ start_time = time.time()
29
+
30
+ full_env = os.environ.copy()
31
+ if env:
32
+ full_env.update(env)
33
+
34
+ try:
35
+ proc = await asyncio.create_subprocess_shell(
36
+ command,
37
+ cwd=work_dir,
38
+ env=full_env,
39
+ stdout=asyncio.subprocess.PIPE,
40
+ stderr=asyncio.subprocess.PIPE,
41
+ )
42
+
43
+ stdout, stderr = await asyncio.wait_for(
44
+ proc.communicate(),
45
+ timeout=timeout,
46
+ )
47
+
48
+ return ShellResult(
49
+ stdout=stdout.decode("utf-8", errors="replace"),
50
+ stderr=stderr.decode("utf-8", errors="replace"),
51
+ exit_code=proc.returncode or 0,
52
+ command=command,
53
+ cwd=work_dir,
54
+ duration_ms=int((time.time() - start_time) * 1000),
55
+ )
56
+
57
+ except asyncio.TimeoutError:
58
+ return ShellResult(
59
+ stdout="",
60
+ stderr=f"Command timed out after {timeout}s",
61
+ exit_code=-1,
62
+ command=command,
63
+ cwd=work_dir,
64
+ duration_ms=timeout * 1000,
65
+ )
66
+ except Exception as e:
67
+ return ShellResult(
68
+ stdout="",
69
+ stderr=str(e),
70
+ exit_code=-1,
71
+ command=command,
72
+ cwd=work_dir,
73
+ duration_ms=int((time.time() - start_time) * 1000),
74
+ )
75
+
76
+ async def execute_stream(
77
+ self,
78
+ command: str,
79
+ cwd: str | None = None,
80
+ env: dict[str, str] | None = None,
81
+ timeout: int = 120,
82
+ ) -> AsyncIterator[str]:
83
+ """Execute with streaming output."""
84
+ work_dir = cwd or self.default_cwd or os.getcwd()
85
+
86
+ full_env = os.environ.copy()
87
+ if env:
88
+ full_env.update(env)
89
+
90
+ proc = await asyncio.create_subprocess_shell(
91
+ command,
92
+ cwd=work_dir,
93
+ env=full_env,
94
+ stdout=asyncio.subprocess.PIPE,
95
+ stderr=asyncio.subprocess.STDOUT,
96
+ )
97
+
98
+ try:
99
+ async with asyncio.timeout(timeout):
100
+ while True:
101
+ line = await proc.stdout.readline()
102
+ if not line:
103
+ break
104
+ yield line.decode("utf-8", errors="replace")
105
+ except asyncio.TimeoutError:
106
+ proc.kill()
107
+ yield f"\n[Timeout after {timeout}s]\n"
108
+
109
+
110
+ __all__ = ["LocalShellBackend"]
@@ -0,0 +1,55 @@
1
+ """Shell backend types and protocols."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import AsyncIterator, Protocol, runtime_checkable
6
+
7
+
8
+ @dataclass
9
+ class ShellResult:
10
+ """Result of shell command execution."""
11
+ stdout: str
12
+ stderr: str
13
+ exit_code: int
14
+ command: str = ""
15
+ cwd: str = ""
16
+ duration_ms: int = 0
17
+
18
+ @property
19
+ def success(self) -> bool:
20
+ return self.exit_code == 0
21
+
22
+ @property
23
+ def output(self) -> str:
24
+ """Combined output."""
25
+ if self.success:
26
+ return self.stdout
27
+ return f"{self.stdout}\n{self.stderr}".strip()
28
+
29
+
30
+ @runtime_checkable
31
+ class ShellBackend(Protocol):
32
+ """Protocol for shell command execution."""
33
+
34
+ async def execute(
35
+ self,
36
+ command: str,
37
+ cwd: str | None = None,
38
+ env: dict[str, str] | None = None,
39
+ timeout: int = 120,
40
+ ) -> ShellResult:
41
+ """Execute a shell command."""
42
+ ...
43
+
44
+ async def execute_stream(
45
+ self,
46
+ command: str,
47
+ cwd: str | None = None,
48
+ env: dict[str, str] | None = None,
49
+ timeout: int = 120,
50
+ ) -> AsyncIterator[str]:
51
+ """Execute command with streaming output."""
52
+ ...
53
+
54
+
55
+ __all__ = ["ShellResult", "ShellBackend"]
@@ -0,0 +1,209 @@
1
+ """Shell backend protocol for command execution.
2
+
3
+ Supports different execution environments:
4
+ - Local shell
5
+ - Docker/nsjail sandbox
6
+ - E2B cloud sandbox
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import AsyncIterator, Literal, Protocol, runtime_checkable
12
+
13
+
14
+ @dataclass
15
+ class ShellResult:
16
+ """Result of shell command execution."""
17
+ stdout: str
18
+ stderr: str
19
+ exit_code: int
20
+
21
+ # Execution metadata
22
+ command: str = ""
23
+ cwd: str = ""
24
+ duration_ms: int = 0
25
+
26
+ @property
27
+ def success(self) -> bool:
28
+ return self.exit_code == 0
29
+
30
+ @property
31
+ def output(self) -> str:
32
+ """Combined output (stdout + stderr if error)."""
33
+ if self.success:
34
+ return self.stdout
35
+ return f"{self.stdout}\n{self.stderr}".strip()
36
+
37
+ def to_dict(self) -> dict:
38
+ return {
39
+ "stdout": self.stdout,
40
+ "stderr": self.stderr,
41
+ "exit_code": self.exit_code,
42
+ "command": self.command,
43
+ "cwd": self.cwd,
44
+ "duration_ms": self.duration_ms,
45
+ }
46
+
47
+
48
+ @runtime_checkable
49
+ class ShellBackend(Protocol):
50
+ """Protocol for shell command execution.
51
+
52
+ Implementations:
53
+ - LocalShellBackend - Execute on local machine
54
+ - SandboxShellBackend - Execute in Docker/nsjail sandbox
55
+ - E2BShellBackend - Execute in E2B cloud sandbox
56
+ """
57
+
58
+ async def execute(
59
+ self,
60
+ command: str,
61
+ cwd: str | None = None,
62
+ env: dict[str, str] | None = None,
63
+ timeout: int = 120,
64
+ ) -> ShellResult:
65
+ """Execute a shell command.
66
+
67
+ Args:
68
+ command: Command to execute
69
+ cwd: Working directory (optional)
70
+ env: Environment variables (optional)
71
+ timeout: Timeout in seconds (default: 120)
72
+
73
+ Returns:
74
+ ShellResult with stdout, stderr, exit_code
75
+ """
76
+ ...
77
+
78
+ async def execute_stream(
79
+ self,
80
+ command: str,
81
+ cwd: str | None = None,
82
+ env: dict[str, str] | None = None,
83
+ timeout: int = 120,
84
+ ) -> AsyncIterator[str]:
85
+ """Execute command with streaming output.
86
+
87
+ Args:
88
+ command: Command to execute
89
+ cwd: Working directory (optional)
90
+ env: Environment variables (optional)
91
+ timeout: Timeout in seconds (default: 120)
92
+
93
+ Yields:
94
+ Output chunks as they arrive
95
+ """
96
+ ...
97
+
98
+
99
+ class LocalShellBackend:
100
+ """Local shell execution backend."""
101
+
102
+ def __init__(self, default_cwd: str | None = None, default_shell: str = "/bin/bash"):
103
+ self.default_cwd = default_cwd
104
+ self.default_shell = default_shell
105
+
106
+ async def execute(
107
+ self,
108
+ command: str,
109
+ cwd: str | None = None,
110
+ env: dict[str, str] | None = None,
111
+ timeout: int = 120,
112
+ ) -> ShellResult:
113
+ """Execute command locally."""
114
+ import asyncio
115
+ import os
116
+ import time
117
+
118
+ work_dir = cwd or self.default_cwd or os.getcwd()
119
+ start_time = time.time()
120
+
121
+ # Merge environment
122
+ full_env = os.environ.copy()
123
+ if env:
124
+ full_env.update(env)
125
+
126
+ try:
127
+ proc = await asyncio.create_subprocess_shell(
128
+ command,
129
+ cwd=work_dir,
130
+ env=full_env,
131
+ stdout=asyncio.subprocess.PIPE,
132
+ stderr=asyncio.subprocess.PIPE,
133
+ )
134
+
135
+ stdout, stderr = await asyncio.wait_for(
136
+ proc.communicate(),
137
+ timeout=timeout,
138
+ )
139
+
140
+ return ShellResult(
141
+ stdout=stdout.decode("utf-8", errors="replace"),
142
+ stderr=stderr.decode("utf-8", errors="replace"),
143
+ exit_code=proc.returncode or 0,
144
+ command=command,
145
+ cwd=work_dir,
146
+ duration_ms=int((time.time() - start_time) * 1000),
147
+ )
148
+
149
+ except asyncio.TimeoutError:
150
+ return ShellResult(
151
+ stdout="",
152
+ stderr=f"Command timed out after {timeout} seconds",
153
+ exit_code=-1,
154
+ command=command,
155
+ cwd=work_dir,
156
+ duration_ms=timeout * 1000,
157
+ )
158
+ except Exception as e:
159
+ return ShellResult(
160
+ stdout="",
161
+ stderr=str(e),
162
+ exit_code=-1,
163
+ command=command,
164
+ cwd=work_dir,
165
+ duration_ms=int((time.time() - start_time) * 1000),
166
+ )
167
+
168
+ async def execute_stream(
169
+ self,
170
+ command: str,
171
+ cwd: str | None = None,
172
+ env: dict[str, str] | None = None,
173
+ timeout: int = 120,
174
+ ) -> AsyncIterator[str]:
175
+ """Execute with streaming output."""
176
+ import asyncio
177
+ import os
178
+
179
+ work_dir = cwd or self.default_cwd or os.getcwd()
180
+
181
+ full_env = os.environ.copy()
182
+ if env:
183
+ full_env.update(env)
184
+
185
+ proc = await asyncio.create_subprocess_shell(
186
+ command,
187
+ cwd=work_dir,
188
+ env=full_env,
189
+ stdout=asyncio.subprocess.PIPE,
190
+ stderr=asyncio.subprocess.STDOUT,
191
+ )
192
+
193
+ async def read_with_timeout():
194
+ try:
195
+ async with asyncio.timeout(timeout):
196
+ while True:
197
+ line = await proc.stdout.readline()
198
+ if not line:
199
+ break
200
+ yield line.decode("utf-8", errors="replace")
201
+ except asyncio.TimeoutError:
202
+ proc.kill()
203
+ yield f"\n[Timeout after {timeout} seconds]\n"
204
+
205
+ async for chunk in read_with_timeout():
206
+ yield chunk
207
+
208
+
209
+ __all__ = ["ShellBackend", "ShellResult", "LocalShellBackend"]
@@ -0,0 +1,19 @@
1
+ """Snapshot backend for file state tracking and revert.
2
+
3
+ Supports different snapshot strategies:
4
+ - Git-based (local)
5
+ - In-memory (testing)
6
+ - Git + S3 hybrid (cloud persistence)
7
+ """
8
+ from .types import Patch, SnapshotBackend
9
+ from .memory import InMemorySnapshotBackend
10
+ from .git import GitSnapshotBackend
11
+ from .hybrid import GitS3HybridBackend
12
+
13
+ __all__ = [
14
+ "SnapshotBackend",
15
+ "Patch",
16
+ "InMemorySnapshotBackend",
17
+ "GitSnapshotBackend",
18
+ "GitS3HybridBackend",
19
+ ]