loopy-agent-fs 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.
Files changed (109) hide show
  1. loopy/__init__.py +66 -0
  2. loopy/_version.py +1 -0
  3. loopy/agent.py +277 -0
  4. loopy/application/__init__.py +15 -0
  5. loopy/application/application.py +348 -0
  6. loopy/application/client.py +117 -0
  7. loopy/application/config.py +16 -0
  8. loopy/application/context_projection.py +111 -0
  9. loopy/application/contracts.py +13 -0
  10. loopy/application/credentials.py +36 -0
  11. loopy/application/database.py +330 -0
  12. loopy/application/event_sink.py +162 -0
  13. loopy/application/mcp_config.py +237 -0
  14. loopy/application/mcp_projection.py +38 -0
  15. loopy/application/mcp_service.py +68 -0
  16. loopy/application/operational_logging.py +21 -0
  17. loopy/application/ownership.py +34 -0
  18. loopy/application/packaging.py +36 -0
  19. loopy/application/runtime.py +855 -0
  20. loopy/application/runtime_config.py +77 -0
  21. loopy/application/serialization.py +59 -0
  22. loopy/application/session_scope.py +27 -0
  23. loopy/application/tool_projection.py +547 -0
  24. loopy/application/wsl.py +357 -0
  25. loopy/cli/__init__.py +0 -0
  26. loopy/cli/backend.py +139 -0
  27. loopy/cli/frontend.py +56 -0
  28. loopy/client.py +4 -0
  29. loopy/context/__init__.py +16 -0
  30. loopy/context/models.py +122 -0
  31. loopy/context/planner.py +155 -0
  32. loopy/context/session.py +455 -0
  33. loopy/context/store.py +254 -0
  34. loopy/context/summarizer.py +81 -0
  35. loopy/context/tool.py +67 -0
  36. loopy/diagnostics/__init__.py +97 -0
  37. loopy/diagnostics/guides/agent-loop.md +3 -0
  38. loopy/diagnostics/guides/client.md +32 -0
  39. loopy/diagnostics/guides/context.md +17 -0
  40. loopy/diagnostics/guides/diagnostics.md +14 -0
  41. loopy/diagnostics/guides/lifecycle.md +7 -0
  42. loopy/diagnostics/guides/system1.md +115 -0
  43. loopy/diagnostics/guides/tool.md +25 -0
  44. loopy/entry/__init__.py +0 -0
  45. loopy/entry/__main__.py +3 -0
  46. loopy/entry/main.py +109 -0
  47. loopy/errors.py +44 -0
  48. loopy/events.py +22 -0
  49. loopy/execution/__init__.py +117 -0
  50. loopy/execution/cleanup.py +60 -0
  51. loopy/execution/python_entry.py +38 -0
  52. loopy/execution/worker.py +378 -0
  53. loopy/gateway/__init__.py +0 -0
  54. loopy/gateway/config.py +50 -0
  55. loopy/gateway/feishu.py +124 -0
  56. loopy/gateway/service.py +207 -0
  57. loopy/llm.py +364 -0
  58. loopy/logging/__init__.py +5 -0
  59. loopy/logging/jsonl.py +67 -0
  60. loopy/loop.py +222 -0
  61. loopy/mcp/__init__.py +6 -0
  62. loopy/mcp/config.py +149 -0
  63. loopy/mcp/connection.py +212 -0
  64. loopy/mcp/errors.py +18 -0
  65. loopy/mcp/manager.py +270 -0
  66. loopy/mcp/probe.py +37 -0
  67. loopy/mcp/reload.py +96 -0
  68. loopy/mcp/result.py +38 -0
  69. loopy/mcp/tool.py +54 -0
  70. loopy/py.typed +0 -0
  71. loopy/session.py +170 -0
  72. loopy/system1/__init__.py +6 -0
  73. loopy/system1/base.py +152 -0
  74. loopy/system1/display.py +62 -0
  75. loopy/system1/jev.py +162 -0
  76. loopy/system1/tool.py +469 -0
  77. loopy/system1/tracking.py +121 -0
  78. loopy/tools/__init__.py +1 -0
  79. loopy/tools/base.py +95 -0
  80. loopy/tools/builtins/__init__.py +12 -0
  81. loopy/tools/builtins/ask_user.py +53 -0
  82. loopy/tools/builtins/bash.py +50 -0
  83. loopy/tools/builtins/edit.py +70 -0
  84. loopy/tools/builtins/exec.py +44 -0
  85. loopy/tools/builtins/files.py +31 -0
  86. loopy/tools/builtins/grep.py +140 -0
  87. loopy/tools/builtins/load_tool.py +50 -0
  88. loopy/tools/builtins/read.py +54 -0
  89. loopy/tools/builtins/tool_control.py +105 -0
  90. loopy/tools/builtins/write.py +69 -0
  91. loopy/tools/command.py +125 -0
  92. loopy/tools/dispatcher.py +570 -0
  93. loopy/tools/loader.py +103 -0
  94. loopy/tools/process.py +160 -0
  95. loopy/tools/registry.py +214 -0
  96. loopy/tools/remote.py +197 -0
  97. loopy/tools/result_store.py +93 -0
  98. loopy/tools/runtime.py +315 -0
  99. loopy/tools/schema.py +58 -0
  100. loopy/tools/transport/__init__.py +1 -0
  101. loopy/tools/transport/client.py +144 -0
  102. loopy/tools/transport/protocol.py +25 -0
  103. loopy/tools/transport/server.py +126 -0
  104. loopy/tools/transport/worker.py +135 -0
  105. loopy/types.py +53 -0
  106. loopy_agent_fs-0.0.2.dist-info/METADATA +846 -0
  107. loopy_agent_fs-0.0.2.dist-info/RECORD +109 -0
  108. loopy_agent_fs-0.0.2.dist-info/WHEEL +4 -0
  109. loopy_agent_fs-0.0.2.dist-info/entry_points.txt +2 -0
loopy/__init__.py ADDED
@@ -0,0 +1,66 @@
1
+ """loopy — an extensible, evaluable agent SDK."""
2
+
3
+ from loopy._version import __version__
4
+ from loopy.agent import Agent
5
+ from loopy.errors import (
6
+ ConfigError,
7
+ LLMError,
8
+ LLMLoopError,
9
+ LoopyError,
10
+ SessionCancelledError,
11
+ SessionError,
12
+ ToolError,
13
+ ToolExecutionError,
14
+ ToolNotFoundError,
15
+ )
16
+ from loopy.events import CompositeEventSink, EventSink
17
+ from loopy.logging import JsonlSink
18
+ from loopy.session import Session
19
+ from loopy.tools.base import (
20
+ BatchTool,
21
+ StreamingTool,
22
+ Tool,
23
+ ToolContext,
24
+ ToolMethod,
25
+ ToolResult,
26
+ )
27
+ from loopy.tools.builtins import AskUserQuestion, Bash, Grep, Write, Read, Edit, Exec
28
+ from loopy.client import ToolClient
29
+ from loopy.tools.runtime import ToolRuntime
30
+ from loopy.types import FinalResponse, RedactedThinkingBlock, ThinkingBlock
31
+
32
+ __all__ = [
33
+ "Agent",
34
+ "AskUserQuestion",
35
+ "Bash",
36
+ "BatchTool",
37
+ "CompositeEventSink",
38
+ "ConfigError",
39
+ "EventSink",
40
+ "FinalResponse",
41
+ "Grep",
42
+ "JsonlSink",
43
+ "LLMError",
44
+ "LLMLoopError",
45
+ "LoopyError",
46
+ "RedactedThinkingBlock",
47
+ "Session",
48
+ "SessionCancelledError",
49
+ "SessionError",
50
+ "StreamingTool",
51
+ "ThinkingBlock",
52
+ "Tool",
53
+ "ToolContext",
54
+ "ToolMethod",
55
+ "ToolRuntime",
56
+ "ToolClient",
57
+ "ToolError",
58
+ "ToolExecutionError",
59
+ "ToolNotFoundError",
60
+ "ToolResult",
61
+ "Write",
62
+ "Read",
63
+ "Edit",
64
+ "Exec",
65
+ "__version__",
66
+ ]
loopy/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.0.2"
loopy/agent.py ADDED
@@ -0,0 +1,277 @@
1
+ """Agent configuration and session lifecycle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Callable
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from loopy.errors import ConfigError, SessionError
11
+ from loopy.events import CompositeEventSink, EventSink
12
+ from loopy.llm import LLMClient
13
+ from loopy.logging.jsonl import JsonlSink
14
+ from loopy.session import Session
15
+ from loopy.tools.builtins import AskUserQuestion
16
+ from loopy.tools.builtins.ask_user import Handler
17
+ from loopy.tools.dispatcher import ToolDispatcher
18
+ from loopy.tools.registry import ToolRegistry
19
+ from loopy.tools.runtime import ToolRuntime
20
+ from loopy.types import Message
21
+
22
+
23
+ class Agent:
24
+ def __init__(
25
+ self,
26
+ *,
27
+ name: str,
28
+ system_prompt: str,
29
+ model: str,
30
+ working_dir: Path,
31
+ tool_runtime: ToolRuntime,
32
+ api_key: str | None = None,
33
+ base_url: str | None = None,
34
+ max_tokens: int = 4096,
35
+ thinking: dict[str, Any] | None = None,
36
+ tagged_thinking_compat: bool = False,
37
+ event_sinks: list[EventSink] | None = None,
38
+ stream_responses: bool = False,
39
+ max_output_continuations: int = 3,
40
+ context_config=None,
41
+ context_summarizer=None,
42
+ ask_user_handler: Handler | None = None,
43
+ client_factory: Callable[..., Any] | None = None,
44
+ ) -> None:
45
+ if not name:
46
+ raise ConfigError("name must be non-empty")
47
+ if not system_prompt:
48
+ raise ConfigError("system_prompt must be non-empty")
49
+ if not model:
50
+ raise ConfigError("model must be non-empty")
51
+ if max_tokens <= 0:
52
+ raise ConfigError("max_tokens must be greater than zero")
53
+ if max_output_continuations < 0:
54
+ raise ConfigError("max_output_continuations must be non-negative")
55
+
56
+ self.name = name
57
+ self.system_prompt = system_prompt
58
+ self.model = model
59
+ self.max_output_continuations = max_output_continuations
60
+ self._event_sinks = tuple(event_sinks or ())
61
+ self.stream_responses = stream_responses
62
+ self.working_dir = Path(working_dir).resolve()
63
+ if not self.working_dir.is_dir():
64
+ raise ConfigError(
65
+ f"working_dir does not exist or is not a directory: {self.working_dir}"
66
+ )
67
+ (self.working_dir / "sessions").mkdir(parents=True, exist_ok=True)
68
+
69
+ if not isinstance(tool_runtime, ToolRuntime):
70
+ raise ConfigError("tool_runtime must be a ToolRuntime instance")
71
+ if tool_runtime.closed:
72
+ raise ConfigError("tool_runtime is closed")
73
+ if tool_runtime.working_dir != self.working_dir:
74
+ raise ConfigError("tool_runtime working_dir does not match Agent")
75
+ self.tool_runtime = tool_runtime
76
+
77
+ if ask_user_handler is not None:
78
+ if any(tool.name == "Ask_User_Question" for tool in self.registry.all()):
79
+ raise ConfigError(
80
+ "AskUserQuestion registered twice; pass ask_user_handler or the tool"
81
+ )
82
+ self.registry.register(AskUserQuestion(on_answer=ask_user_handler))
83
+
84
+ if client_factory is not None:
85
+ self.llm = client_factory(model=model, api_key=api_key)
86
+ else:
87
+ self.llm = LLMClient(
88
+ model=model,
89
+ api_key=api_key,
90
+ base_url=base_url,
91
+ max_tokens=max_tokens,
92
+ thinking=thinking,
93
+ tagged_thinking_compat=tagged_thinking_compat,
94
+ )
95
+
96
+ self.context_config = context_config
97
+ self.context_summarizer = context_summarizer
98
+ self._sessions: list[Session] = []
99
+ self._closed = False
100
+
101
+ @property
102
+ def registry(self) -> ToolRegistry:
103
+ """Access the registry owned by this Agent's runtime."""
104
+ return self.tool_runtime.registry
105
+
106
+ def new_session(self, session_id: str | None = None) -> Session:
107
+ if self._closed:
108
+ raise SessionError("agent is closed")
109
+ if session_id is not None and not session_id:
110
+ raise SessionError("session_id must be non-empty")
111
+ return self._create_live_session(
112
+ session_id if session_id is not None else self.tool_runtime.session_id,
113
+ messages=[],
114
+ )
115
+
116
+ def resume_session(self, session_id: str, messages: list[Message]) -> Session:
117
+ """Create a writable session from a caller-owned canonical history."""
118
+ if not session_id:
119
+ raise SessionError("session_id must be non-empty")
120
+ if not isinstance(messages, list):
121
+ raise SessionError("messages must be a list")
122
+ return self._create_live_session(session_id, messages=list(messages), resumed=True)
123
+
124
+ def resume_stored_session(self, session_id: str) -> Session:
125
+ if self.context_config is None:
126
+ raise SessionError('Stored context requires context_config')
127
+ return self._create_live_session(session_id, messages=[], resumed=True, stored=True)
128
+
129
+ def _create_live_session(
130
+ self,
131
+ session_id: str,
132
+ *,
133
+ messages: list[Message],
134
+ resumed: bool = False,
135
+ stored: bool = False,
136
+ ) -> Session:
137
+ if self._closed:
138
+ raise SessionError("agent is closed")
139
+ ToolDispatcher._check_id(session_id)
140
+ if self.tool_runtime.closed or session_id != self.tool_runtime.session_id:
141
+ raise SessionError("runtime is closed or session_id does not match")
142
+ if self.tool_runtime.model_active:
143
+ raise SessionError("runtime already has an active model run")
144
+ session_dir = self.working_dir / "sessions" / session_id
145
+ session_dir.mkdir(parents=True, exist_ok=True)
146
+ if self.context_config is not None:
147
+ bound = getattr(self.tool_runtime, 'context_session', None)
148
+ if bound is not None and not bound.closed:
149
+ raise SessionError('Runtime already has a bound context session')
150
+ if not stored and (session_dir / 'context' / 'HEAD.json').exists():
151
+ raise SessionError('Stored context exists; use resume_stored_session')
152
+ log_sink = JsonlSink(session_dir / "messages.jsonl")
153
+ sink: EventSink = (
154
+ CompositeEventSink(log_sink, *self._event_sinks) if self._event_sinks else log_sink
155
+ )
156
+ lifecycle_event = "session_resumed" if resumed else "session_created"
157
+ sink.emit(
158
+ lifecycle_event,
159
+ {
160
+ "session_id": session_id,
161
+ "system_prompt": self.system_prompt,
162
+ "model": self.model,
163
+ "working_dir": str(self.working_dir),
164
+ "resumed": resumed,
165
+ },
166
+ )
167
+ runtime = self.tool_runtime
168
+ dispatcher = runtime.dispatcher
169
+ session = Session(
170
+ agent=self,
171
+ session_id=session_id,
172
+ messages=messages,
173
+ sink=sink,
174
+ log_sink=log_sink,
175
+ working_dir=self.working_dir,
176
+ dispatcher=dispatcher,
177
+ tool_runtime=runtime,
178
+ _last_emitted_index=len(messages),
179
+ )
180
+ if self.context_config is not None:
181
+ from loopy.context import ContextConfig, ContextSession, ContextTool, LLMSummarizer
182
+
183
+ config = (
184
+ ContextConfig(**self.context_config)
185
+ if isinstance(self.context_config, dict)
186
+ else self.context_config
187
+ )
188
+ root = session_dir / "context"
189
+ if messages and (root / "HEAD.json").exists():
190
+ raise SessionError("Stored context exists; use resume_stored_session")
191
+ if stored and not (root / "HEAD.json").exists():
192
+ raise SessionError("Stored context not found")
193
+ context = ContextSession(
194
+ root,
195
+ config=config,
196
+ summarizer=self.context_summarizer
197
+ or LLMSummarizer(self.llm, config.summary_timeout),
198
+ sink=sink,
199
+ )
200
+ for message in messages:
201
+ context.append_message(message)
202
+ context.end_turn()
203
+ session.context = context
204
+ runtime.context_session = context
205
+ if not any(t.name == "Context" for t in runtime.registry.all()):
206
+ runtime.register(ContextTool(runtime))
207
+ self._sessions.append(session)
208
+ return session
209
+
210
+ def get_session(self, session_id: str) -> Session:
211
+ session_dir = self.working_dir / "sessions" / session_id
212
+ log_path = session_dir / "messages.jsonl"
213
+ if not log_path.is_file():
214
+ raise SessionError(f"session not found: {session_id}")
215
+
216
+ messages: list[dict] = []
217
+ for event in JsonlSink.read_all(log_path):
218
+ if event.get("event") != "message":
219
+ continue
220
+ payload = event.get("payload") or {}
221
+ messages.append(
222
+ {
223
+ "role": payload.get("role"),
224
+ "content": list(payload.get("content") or []),
225
+ }
226
+ )
227
+ return Session(
228
+ agent=self,
229
+ session_id=session_id,
230
+ messages=messages,
231
+ sink=None,
232
+ working_dir=self.working_dir,
233
+ dispatcher=None,
234
+ read_only=True,
235
+ _last_emitted_index=len(messages),
236
+ )
237
+
238
+ async def aclose(self) -> None:
239
+ if self._closed:
240
+ return
241
+ self._closed = True
242
+
243
+ for session in self._sessions:
244
+ session.cancel()
245
+ await asyncio.gather(
246
+ *(
247
+ s._active_run
248
+ for s in self._sessions
249
+ if s._active_run and s._active_run is not asyncio.current_task()
250
+ ),
251
+ return_exceptions=True,
252
+ )
253
+ errors = []
254
+
255
+ for session in self._sessions:
256
+ if session.context is not None:
257
+ await session.context.aclose()
258
+ if getattr(self.tool_runtime, "context_session", None) is session.context:
259
+ self.tool_runtime.context_session = None
260
+ if session.log_sink is not None:
261
+ session.log_sink.close()
262
+
263
+ client = getattr(self.llm, "_client", None)
264
+ aclose = getattr(client, "aclose", None)
265
+ if aclose is not None:
266
+ try:
267
+ await aclose()
268
+ except Exception as exc:
269
+ errors.append(exc)
270
+ if errors:
271
+ raise ExceptionGroup("Agent cleanup failed", errors)
272
+
273
+ def _resolve_dispatcher(self, session: Session) -> ToolDispatcher:
274
+ """Compatibility helper for callers that used the internal method."""
275
+ if session.dispatcher is None:
276
+ raise SessionError("Session has no dispatcher")
277
+ return session.dispatcher
@@ -0,0 +1,15 @@
1
+ """Application SDK. Importing the core SDK does not load this package."""
2
+ __all__ = ["LoopyApplication", "LoopyClient", "Settings"]
3
+
4
+
5
+ def __getattr__(name):
6
+ if name == "LoopyApplication":
7
+ from .application import LoopyApplication
8
+ return LoopyApplication
9
+ if name == "LoopyClient":
10
+ from .client import LoopyClient
11
+ return LoopyClient
12
+ if name == "Settings":
13
+ from .config import Settings
14
+ return Settings
15
+ raise AttributeError(name)