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.
- loopy/__init__.py +66 -0
- loopy/_version.py +1 -0
- loopy/agent.py +277 -0
- loopy/application/__init__.py +15 -0
- loopy/application/application.py +348 -0
- loopy/application/client.py +117 -0
- loopy/application/config.py +16 -0
- loopy/application/context_projection.py +111 -0
- loopy/application/contracts.py +13 -0
- loopy/application/credentials.py +36 -0
- loopy/application/database.py +330 -0
- loopy/application/event_sink.py +162 -0
- loopy/application/mcp_config.py +237 -0
- loopy/application/mcp_projection.py +38 -0
- loopy/application/mcp_service.py +68 -0
- loopy/application/operational_logging.py +21 -0
- loopy/application/ownership.py +34 -0
- loopy/application/packaging.py +36 -0
- loopy/application/runtime.py +855 -0
- loopy/application/runtime_config.py +77 -0
- loopy/application/serialization.py +59 -0
- loopy/application/session_scope.py +27 -0
- loopy/application/tool_projection.py +547 -0
- loopy/application/wsl.py +357 -0
- loopy/cli/__init__.py +0 -0
- loopy/cli/backend.py +139 -0
- loopy/cli/frontend.py +56 -0
- loopy/client.py +4 -0
- loopy/context/__init__.py +16 -0
- loopy/context/models.py +122 -0
- loopy/context/planner.py +155 -0
- loopy/context/session.py +455 -0
- loopy/context/store.py +254 -0
- loopy/context/summarizer.py +81 -0
- loopy/context/tool.py +67 -0
- loopy/diagnostics/__init__.py +97 -0
- loopy/diagnostics/guides/agent-loop.md +3 -0
- loopy/diagnostics/guides/client.md +32 -0
- loopy/diagnostics/guides/context.md +17 -0
- loopy/diagnostics/guides/diagnostics.md +14 -0
- loopy/diagnostics/guides/lifecycle.md +7 -0
- loopy/diagnostics/guides/system1.md +115 -0
- loopy/diagnostics/guides/tool.md +25 -0
- loopy/entry/__init__.py +0 -0
- loopy/entry/__main__.py +3 -0
- loopy/entry/main.py +109 -0
- loopy/errors.py +44 -0
- loopy/events.py +22 -0
- loopy/execution/__init__.py +117 -0
- loopy/execution/cleanup.py +60 -0
- loopy/execution/python_entry.py +38 -0
- loopy/execution/worker.py +378 -0
- loopy/gateway/__init__.py +0 -0
- loopy/gateway/config.py +50 -0
- loopy/gateway/feishu.py +124 -0
- loopy/gateway/service.py +207 -0
- loopy/llm.py +364 -0
- loopy/logging/__init__.py +5 -0
- loopy/logging/jsonl.py +67 -0
- loopy/loop.py +222 -0
- loopy/mcp/__init__.py +6 -0
- loopy/mcp/config.py +149 -0
- loopy/mcp/connection.py +212 -0
- loopy/mcp/errors.py +18 -0
- loopy/mcp/manager.py +270 -0
- loopy/mcp/probe.py +37 -0
- loopy/mcp/reload.py +96 -0
- loopy/mcp/result.py +38 -0
- loopy/mcp/tool.py +54 -0
- loopy/py.typed +0 -0
- loopy/session.py +170 -0
- loopy/system1/__init__.py +6 -0
- loopy/system1/base.py +152 -0
- loopy/system1/display.py +62 -0
- loopy/system1/jev.py +162 -0
- loopy/system1/tool.py +469 -0
- loopy/system1/tracking.py +121 -0
- loopy/tools/__init__.py +1 -0
- loopy/tools/base.py +95 -0
- loopy/tools/builtins/__init__.py +12 -0
- loopy/tools/builtins/ask_user.py +53 -0
- loopy/tools/builtins/bash.py +50 -0
- loopy/tools/builtins/edit.py +70 -0
- loopy/tools/builtins/exec.py +44 -0
- loopy/tools/builtins/files.py +31 -0
- loopy/tools/builtins/grep.py +140 -0
- loopy/tools/builtins/load_tool.py +50 -0
- loopy/tools/builtins/read.py +54 -0
- loopy/tools/builtins/tool_control.py +105 -0
- loopy/tools/builtins/write.py +69 -0
- loopy/tools/command.py +125 -0
- loopy/tools/dispatcher.py +570 -0
- loopy/tools/loader.py +103 -0
- loopy/tools/process.py +160 -0
- loopy/tools/registry.py +214 -0
- loopy/tools/remote.py +197 -0
- loopy/tools/result_store.py +93 -0
- loopy/tools/runtime.py +315 -0
- loopy/tools/schema.py +58 -0
- loopy/tools/transport/__init__.py +1 -0
- loopy/tools/transport/client.py +144 -0
- loopy/tools/transport/protocol.py +25 -0
- loopy/tools/transport/server.py +126 -0
- loopy/tools/transport/worker.py +135 -0
- loopy/types.py +53 -0
- loopy_agent_fs-0.0.2.dist-info/METADATA +846 -0
- loopy_agent_fs-0.0.2.dist-info/RECORD +109 -0
- loopy_agent_fs-0.0.2.dist-info/WHEEL +4 -0
- 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)
|