pal-code 0.1.0__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 (282) hide show
  1. pal/__init__.py +29 -0
  2. pal/__main__.py +3 -0
  3. pal/access_manager/__init__.py +16 -0
  4. pal/access_manager/components/__init__.py +14 -0
  5. pal/access_manager/components/access_control.py +41 -0
  6. pal/access_manager/components/base.py +39 -0
  7. pal/access_manager/components/intervention.py +31 -0
  8. pal/access_manager/components/privilege_registry.py +30 -0
  9. pal/access_manager/pipeline.py +68 -0
  10. pal/access_manager/utils/__init__.py +2 -0
  11. pal/agent_constraints/__init__.py +3 -0
  12. pal/agent_constraints/components/AGENT_OUTPUT.md +13 -0
  13. pal/agent_constraints/components/__init__.py +3 -0
  14. pal/agent_constraints/components/constraints.py +22 -0
  15. pal/agent_constraints/pipeline.py +22 -0
  16. pal/agent_constraints/utils/__init__.py +2 -0
  17. pal/agents/__init__.py +12 -0
  18. pal/agents/coding.py +14 -0
  19. pal/agents/docs.py +15 -0
  20. pal/chat/__init__.py +4 -0
  21. pal/chat/demo.py +231 -0
  22. pal/chat/providers.py +23 -0
  23. pal/chat/service.py +591 -0
  24. pal/chat/services/__init__.py +26 -0
  25. pal/chat/services/base.py +142 -0
  26. pal/chat/services/data.py +142 -0
  27. pal/chat/services/media.py +123 -0
  28. pal/chat/services/web.py +60 -0
  29. pal/chat/sessions.py +108 -0
  30. pal/chat/tools.py +446 -0
  31. pal/chat/uploads.py +81 -0
  32. pal/cli.py +502 -0
  33. pal/compression/__init__.py +12 -0
  34. pal/compression/components/__init__.py +64 -0
  35. pal/compression/components/adverbs.py +20 -0
  36. pal/compression/components/hedges.py +54 -0
  37. pal/compression/components/interjections.py +20 -0
  38. pal/compression/components/minifier.py +56 -0
  39. pal/compression/components/output_compressor.py +13 -0
  40. pal/compression/components/pleonasms.py +54 -0
  41. pal/compression/components/punctuations.py +36 -0
  42. pal/compression/components/stopwords.py +62 -0
  43. pal/compression/history.py +346 -0
  44. pal/compression/pipeline.py +58 -0
  45. pal/compression/utils/__init__.py +30 -0
  46. pal/compression/utils/negations.py +18 -0
  47. pal/compression/utils/nltk_setup.py +20 -0
  48. pal/compression/utils/pronouns.py +27 -0
  49. pal/compression/utils/proselint_setup.py +9 -0
  50. pal/compression/utils/spacy_setup.py +22 -0
  51. pal/config/default.yaml +163 -0
  52. pal/config.py +504 -0
  53. pal/context_manager/__init__.py +7 -0
  54. pal/context_manager/chat_context.py +117 -0
  55. pal/context_manager/components/__init__.py +5 -0
  56. pal/context_manager/components/assembler.py +75 -0
  57. pal/context_manager/components/context_cache.py +45 -0
  58. pal/context_manager/components/kv_bridge.py +16 -0
  59. pal/context_manager/components/pager.py +48 -0
  60. pal/context_manager/components/simple_context_manager.py +56 -0
  61. pal/context_manager/components/turns.py +88 -0
  62. pal/context_manager/pipeline.py +27 -0
  63. pal/context_manager/utils/__init__.py +18 -0
  64. pal/context_manager/utils/hashing.py +5 -0
  65. pal/context_manager/utils/relevance.py +59 -0
  66. pal/context_manager/utils/storage.py +37 -0
  67. pal/documents/__init__.py +4 -0
  68. pal/documents/components/__init__.py +0 -0
  69. pal/documents/components/chunker.py +79 -0
  70. pal/documents/components/index.py +155 -0
  71. pal/documents/utils/__init__.py +1 -0
  72. pal/errors.py +40 -0
  73. pal/input_converters/__init__.py +8 -0
  74. pal/input_converters/components/__init__.py +0 -0
  75. pal/input_converters/components/base.py +38 -0
  76. pal/input_converters/components/pandoc_converter.py +87 -0
  77. pal/input_converters/components/pdf_converter.py +46 -0
  78. pal/input_converters/components/pptx_converter.py +52 -0
  79. pal/input_converters/components/xlsx_converter.py +38 -0
  80. pal/input_converters/pipeline.py +72 -0
  81. pal/input_converters/utils/__init__.py +1 -0
  82. pal/input_converters/workspace.py +56 -0
  83. pal/input_savers/INPUT_RULES.md +2 -0
  84. pal/input_savers/__init__.py +5 -0
  85. pal/input_savers/components/__init__.py +0 -0
  86. pal/input_savers/components/base.py +40 -0
  87. pal/input_savers/components/methods.py +41 -0
  88. pal/input_savers/components/verifier.py +39 -0
  89. pal/input_savers/pipeline.py +154 -0
  90. pal/input_savers/utils/__init__.py +0 -0
  91. pal/input_savers/utils/guards.py +94 -0
  92. pal/knowledge_graph/__init__.py +10 -0
  93. pal/knowledge_graph/components/__init__.py +0 -0
  94. pal/knowledge_graph/components/base.py +50 -0
  95. pal/knowledge_graph/components/builder.py +54 -0
  96. pal/knowledge_graph/components/retriever.py +100 -0
  97. pal/knowledge_graph/components/workspace.py +116 -0
  98. pal/knowledge_graph/pipeline.py +215 -0
  99. pal/knowledge_graph/utils/__init__.py +1 -0
  100. pal/latency_tracker/__init__.py +3 -0
  101. pal/latency_tracker/pipeline.py +86 -0
  102. pal/llm_scheduler/__init__.py +11 -0
  103. pal/llm_scheduler/api.py +137 -0
  104. pal/llm_scheduler/code.py +28 -0
  105. pal/llm_scheduler/core.py +92 -0
  106. pal/llm_scheduler/local.py +215 -0
  107. pal/llm_scheduler/pipeline.py +42 -0
  108. pal/llm_scheduler/types.py +92 -0
  109. pal/llm_wrapper/__init__.py +34 -0
  110. pal/llm_wrapper/adapter.py +316 -0
  111. pal/llm_wrapper/components/__init__.py +23 -0
  112. pal/llm_wrapper/components/adapter_types.py +90 -0
  113. pal/llm_wrapper/components/base.py +195 -0
  114. pal/llm_wrapper/components/features.py +104 -0
  115. pal/llm_wrapper/components/litellm_wrapper.py +215 -0
  116. pal/llm_wrapper/components/openrouter_wrapper.py +114 -0
  117. pal/llm_wrapper/modes.py +73 -0
  118. pal/llm_wrapper/pipeline.py +61 -0
  119. pal/llm_wrapper/utils/__init__.py +4 -0
  120. pal/llm_wrapper/utils/registry.py +49 -0
  121. pal/llm_wrapper/utils/token_counting.py +10 -0
  122. pal/local_llm/__init__.py +5 -0
  123. pal/local_llm/components/__init__.py +0 -0
  124. pal/local_llm/components/huggingface_local.py +217 -0
  125. pal/local_llm/components/local_llm.py +60 -0
  126. pal/logging_setup.py +18 -0
  127. pal/memory/__init__.py +7 -0
  128. pal/memory/components/__init__.py +6 -0
  129. pal/memory/components/lru_k.py +56 -0
  130. pal/memory/components/manager.py +98 -0
  131. pal/memory/components/note.py +54 -0
  132. pal/memory/components/storage_manager.py +60 -0
  133. pal/memory/pipeline.py +122 -0
  134. pal/memory/utils/__init__.py +3 -0
  135. pal/memory/utils/keyword_overlap.py +3 -0
  136. pal/memory_tracker/__init__.py +3 -0
  137. pal/memory_tracker/components/__init__.py +0 -0
  138. pal/memory_tracker/components/process.py +56 -0
  139. pal/memory_tracker/pipeline.py +44 -0
  140. pal/metrics/__init__.py +4 -0
  141. pal/metrics/components/__init__.py +4 -0
  142. pal/metrics/components/collector.py +140 -0
  143. pal/metrics/components/exporter.py +28 -0
  144. pal/metrics/pipeline.py +49 -0
  145. pal/metrics/utils/__init__.py +2 -0
  146. pal/notes_manager/__init__.py +5 -0
  147. pal/notes_manager/components/__init__.py +0 -0
  148. pal/notes_manager/components/privacy.py +14 -0
  149. pal/notes_manager/components/recall.py +31 -0
  150. pal/notes_manager/components/tidy.py +28 -0
  151. pal/notes_manager/pipeline.py +138 -0
  152. pal/orchestrator/__init__.py +26 -0
  153. pal/orchestrator/components/__init__.py +26 -0
  154. pal/orchestrator/components/agent.py +105 -0
  155. pal/orchestrator/components/cgroup_manager.py +209 -0
  156. pal/orchestrator/components/context_interrupt.py +220 -0
  157. pal/orchestrator/components/limits.py +9 -0
  158. pal/orchestrator/components/scheduler.py +69 -0
  159. pal/orchestrator/components/syscall_scheduler.py +200 -0
  160. pal/orchestrator/pipeline.py +228 -0
  161. pal/orchestrator/utils/__init__.py +4 -0
  162. pal/orchestrator/utils/device.py +164 -0
  163. pal/orchestrator/utils/protocol.py +32 -0
  164. pal/os_kernel/__init__.py +5 -0
  165. pal/os_kernel/filesystem.py +80 -0
  166. pal/os_kernel/network.py +146 -0
  167. pal/os_kernel/pipeline.py +175 -0
  168. pal/os_kernel/sysinfo.py +36 -0
  169. pal/paths.py +59 -0
  170. pal/pipeline.py +177 -0
  171. pal/py.typed +0 -0
  172. pal/redact.py +19 -0
  173. pal/sdk/__init__.py +33 -0
  174. pal/sdk/adapters.py +43 -0
  175. pal/sdk/agent.py +99 -0
  176. pal/sdk/components/__init__.py +14 -0
  177. pal/sdk/components/autogen_adapter.py +65 -0
  178. pal/sdk/components/autogen_client.py +98 -0
  179. pal/sdk/components/framework_adapter_base.py +69 -0
  180. pal/sdk/components/metagpt_adapter.py +46 -0
  181. pal/sdk/components/open_interpreter_adapter.py +47 -0
  182. pal/sdk/components/tool_catalog.py +44 -0
  183. pal/sdk/os_api.py +84 -0
  184. pal/sdk/patterns.py +129 -0
  185. pal/sdk/pipeline.py +240 -0
  186. pal/sdk/queries.py +68 -0
  187. pal/sdk/savers.py +88 -0
  188. pal/sdk/utils/__init__.py +2 -0
  189. pal/storage_manager/__init__.py +17 -0
  190. pal/storage_manager/components/__init__.py +19 -0
  191. pal/storage_manager/components/base.py +50 -0
  192. pal/storage_manager/components/local_file_backend.py +84 -0
  193. pal/storage_manager/components/storage_queue.py +91 -0
  194. pal/storage_manager/components/vector_store_backend.py +122 -0
  195. pal/storage_manager/pipeline.py +153 -0
  196. pal/storage_manager/utils/__init__.py +4 -0
  197. pal/storage_manager/utils/memory_adapter.py +60 -0
  198. pal/storage_manager/utils/sdk.py +93 -0
  199. pal/studio/__init__.py +0 -0
  200. pal/studio/builder.py +166 -0
  201. pal/studio/catalog.py +90 -0
  202. pal/studio/checks.py +129 -0
  203. pal/studio/clients.py +6 -0
  204. pal/studio/editor.py +137 -0
  205. pal/studio/files.py +6 -0
  206. pal/studio/models.py +6 -0
  207. pal/studio/plan.py +149 -0
  208. pal/studio/planner.py +205 -0
  209. pal/studio/policy.py +69 -0
  210. pal/studio/routes.py +254 -0
  211. pal/studio/runs.py +210 -0
  212. pal/task_router/__init__.py +11 -0
  213. pal/task_router/components/__init__.py +0 -0
  214. pal/task_router/components/answerer.py +37 -0
  215. pal/task_router/components/base.py +34 -0
  216. pal/task_router/components/classifier.py +40 -0
  217. pal/task_router/components/gates.py +23 -0
  218. pal/task_router/pipeline.py +61 -0
  219. pal/task_router/utils/__init__.py +1 -0
  220. pal/terminal/__init__.py +1 -0
  221. pal/terminal/backend.py +141 -0
  222. pal/terminal/bench.py +173 -0
  223. pal/terminal/cli.py +826 -0
  224. pal/terminal/clients.py +170 -0
  225. pal/terminal/commands.py +401 -0
  226. pal/terminal/decide.py +242 -0
  227. pal/terminal/files.py +55 -0
  228. pal/terminal/flow_cli.py +439 -0
  229. pal/terminal/hardware.py +199 -0
  230. pal/terminal/history.py +88 -0
  231. pal/terminal/keys.py +83 -0
  232. pal/terminal/local_models.py +261 -0
  233. pal/terminal/logo.py +117 -0
  234. pal/terminal/models.py +99 -0
  235. pal/terminal/policy.py +54 -0
  236. pal/terminal/project.py +413 -0
  237. pal/terminal/review.py +65 -0
  238. pal/terminal/security.py +239 -0
  239. pal/terminal/session.py +503 -0
  240. pal/terminal/setup.py +354 -0
  241. pal/terminal/syntax.py +98 -0
  242. pal/terminal/theme.py +37 -0
  243. pal/terminal/ui.py +647 -0
  244. pal/terminal/voice.py +137 -0
  245. pal/terminal/workflow.py +624 -0
  246. pal/token_tracker/__init__.py +3 -0
  247. pal/token_tracker/pipeline.py +54 -0
  248. pal/tokens.py +21 -0
  249. pal/tool_catalog.py +107 -0
  250. pal/tool_manager/__init__.py +17 -0
  251. pal/tool_manager/builtin.py +82 -0
  252. pal/tool_manager/components/__init__.py +19 -0
  253. pal/tool_manager/components/base.py +53 -0
  254. pal/tool_manager/components/conflict_resolver.py +27 -0
  255. pal/tool_manager/components/param_validator.py +41 -0
  256. pal/tool_manager/components/tool_call_queue.py +111 -0
  257. pal/tool_manager/components/tool_registry.py +60 -0
  258. pal/tool_manager/pipeline.py +80 -0
  259. pal/tool_manager/utils/__init__.py +3 -0
  260. pal/web/__init__.py +3 -0
  261. pal/web/app.py +276 -0
  262. pal/web/static/editor.js +219 -0
  263. pal/web/static/fonts/LICENSE-Inter.txt +3 -0
  264. pal/web/static/fonts/inter-400.woff2 +0 -0
  265. pal/web/static/fonts/inter-500.woff2 +0 -0
  266. pal/web/static/fonts/inter-600.woff2 +0 -0
  267. pal/web/static/fonts/inter-700.woff2 +0 -0
  268. pal/web/static/inter.css +5 -0
  269. pal/web/static/pal-logo-256.png +0 -0
  270. pal/web/static/pal-logo-32.png +0 -0
  271. pal/web/static/pal-logo-64.png +0 -0
  272. pal/web/static/studio.css +306 -0
  273. pal/web/static/studio.js +533 -0
  274. pal/web/static/voice.js +76 -0
  275. pal/web/templates/index.html +368 -0
  276. pal/web/templates/studio.html +120 -0
  277. pal_code-0.1.0.dist-info/METADATA +379 -0
  278. pal_code-0.1.0.dist-info/RECORD +282 -0
  279. pal_code-0.1.0.dist-info/WHEEL +5 -0
  280. pal_code-0.1.0.dist-info/entry_points.txt +2 -0
  281. pal_code-0.1.0.dist-info/licenses/LICENSE +21 -0
  282. pal_code-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,175 @@
1
+ """The OS kernel interface: where non-LLM queries go.
2
+
3
+ An agent's non-LLM requests (file operations, network requests, system facts) are not model work and never touch the
4
+ LLM. They are system calls into the operating system, here Ubuntu's Linux kernel: files through its filesystem, requests
5
+ through its network stack, facts through /proc. This class is the guarded doorway: a per-agent workspace, size and
6
+ network limits, the access manager's confirmation gate for destructive operations, and an audit trail (operation and
7
+ target only, never file contents).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ import logging
13
+ from collections import deque
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from pal.access_manager.components.base import OperationType
18
+ from pal.access_manager.pipeline import AccessManagerPipeline
19
+ from pal.config import OsKernelConfig
20
+ from pal.os_kernel import network
21
+ from pal.os_kernel.filesystem import OSKernelError, Workspace
22
+ from pal.os_kernel.sysinfo import system_info
23
+ from pal.paths import data_dir, safe_id
24
+
25
+ log = logging.getLogger(__name__)
26
+
27
+ OS_OPS = ("file_read", "file_write", "file_list", "file_delete", "file_read_b64", "net_get", "net_post", "net_request", "sys_info",
28
+ "keyboard", "mouse")
29
+
30
+ # the file extension for a downloaded media type, used when a caller saves a response as "name.{ext}"
31
+ _EXT = {"image/png": "png", "image/jpeg": "jpg", "image/webp": "webp", "image/gif": "gif", "audio/flac": "flac",
32
+ "audio/x-flac": "flac", "audio/wav": "wav", "audio/x-wav": "wav", "audio/wave": "wav", "audio/mpeg": "mp3",
33
+ "audio/ogg": "ogg", "audio/mp4": "m4a"}
34
+
35
+
36
+ class OSKernel:
37
+ def __init__(self, cfg: OsKernelConfig | dict | None = None, access: AccessManagerPipeline | None = None,
38
+ root: Path | None = None):
39
+ self.cfg = OsKernelConfig.coerce(cfg)
40
+ self.access = access or AccessManagerPipeline()
41
+ self.root = Path(root) if root else (Path(self.cfg.workspace_dir).expanduser() if self.cfg.workspace_dir
42
+ else data_dir("workspaces"))
43
+ self._workspaces: dict[str, Workspace] = {}
44
+ self.audit: deque[dict] = deque(maxlen=500)
45
+
46
+ def workspace(self, agent_id: str) -> Workspace:
47
+ key = safe_id(agent_id)
48
+ if key not in self._workspaces:
49
+ self._workspaces[key] = Workspace(self.root / key, self.cfg.max_read_bytes, self.cfg.max_write_bytes,
50
+ self.cfg.max_binary_bytes)
51
+ return self._workspaces[key]
52
+
53
+ def drop_workspace(self, agent_id: str) -> bool:
54
+ """Delete an agent's whole workspace: used when a chat session ends, so uploaded and generated files do not outlive it."""
55
+ import shutil
56
+ key = safe_id(agent_id)
57
+ self._workspaces.pop(key, None)
58
+ path = self.root / key
59
+ existed = path.is_dir()
60
+ shutil.rmtree(path, ignore_errors=True)
61
+ return existed
62
+
63
+ def prune_stale(self, prefix: str, max_age_seconds: float) -> int:
64
+ """Delete workspaces whose folder name starts with `prefix` and that nothing has touched for `max_age_seconds` (left behind
65
+ by a crash or an upload whose chat never started). Other workspaces, such as an SDK agent's own, are not touched."""
66
+ import shutil
67
+ import time
68
+ removed = 0
69
+ if not self.root.is_dir():
70
+ return 0
71
+ for d in self.root.iterdir():
72
+ try:
73
+ if d.is_dir() and d.name.startswith(prefix) and time.time() - self._last_touched(d) > max_age_seconds:
74
+ self._workspaces.pop(d.name, None)
75
+ shutil.rmtree(d, ignore_errors=True)
76
+ removed += 1
77
+ except OSError:
78
+ continue
79
+ return removed
80
+
81
+ @staticmethod
82
+ def _last_touched(d: Path) -> float:
83
+ times = [d.stat().st_mtime] + [p.stat().st_mtime for p in d.rglob("*") if p.is_file()]
84
+ return max(times)
85
+
86
+ def _authorize(self, agent_id: str, op: OperationType, resource: str) -> None:
87
+ decision = self.access.authorize(agent_id, agent_id, op, resource=resource)
88
+ if not decision.allowed:
89
+ raise OSKernelError(f"{op.value} of {resource!r} was not permitted: {decision.reason or 'denied'}")
90
+
91
+ def handle(self, agent_id: str, op: str, params: dict | None = None) -> Any:
92
+ params = params or {}
93
+ # a URL is logged without its query string: that is where a service key may be
94
+ target = network.safe_target(str(params["url"])) if params.get("url") else str(params.get("path") or params.get("prefix") or "")
95
+ ok = False
96
+ try:
97
+ if not self.cfg.enabled:
98
+ raise OSKernelError("OS access is turned off in the settings")
99
+ if op not in OS_OPS:
100
+ raise OSKernelError(f"unknown OS operation {op!r}; available: {', '.join(OS_OPS)}")
101
+ result = self._dispatch(agent_id, op, params)
102
+ ok = True
103
+ return result
104
+ finally:
105
+ self.audit.append({"agent": agent_id, "op": op, "target": target[:200], "ok": ok})
106
+ log.info("os call agent=%s op=%s target=%s ok=%s", agent_id, op, target[:120], ok)
107
+
108
+ def _dispatch(self, agent_id: str, op: str, p: dict) -> Any:
109
+ if op in ("keyboard", "mouse"):
110
+ raise OSKernelError("keyboard and mouse control need a desktop session and are not supported here")
111
+ if op == "sys_info":
112
+ return system_info()
113
+ if op in ("net_get", "net_post"):
114
+ return network.request(
115
+ "GET" if op == "net_get" else "POST", str(p.get("url", "")), allow_private=self.cfg.allow_private_network,
116
+ allowed_hosts=list(self.cfg.allowed_hosts), timeout=self.cfg.net_timeout_seconds,
117
+ max_bytes=self.cfg.net_max_bytes, json=p.get("json"))
118
+ if op == "net_request":
119
+ return self._net_request(agent_id, p)
120
+ ws = self.workspace(agent_id)
121
+ if op == "file_list":
122
+ return ws.list(str(p.get("prefix", "")))
123
+ path = str(p.get("path", ""))
124
+ ws.resolve(path) # validate first: nobody is ever asked to confirm an invalid path
125
+ if op == "file_read":
126
+ return ws.read(path)
127
+ if op == "file_read_b64":
128
+ data = ws.read_bytes(path)
129
+ return {"path": path, "bytes": len(data), "b64": base64.b64encode(data).decode("ascii")}
130
+ if op == "file_write":
131
+ self._authorize(agent_id, OperationType.OVERWRITE if ws.exists(path) else OperationType.WRITE, path)
132
+ return {"path": path, "bytes": ws.write(path, p.get("content", ""))}
133
+ if not ws.exists(path): # file_delete: nothing to confirm if there is nothing to delete
134
+ raise OSKernelError(f"no such file: {path}")
135
+ self._authorize(agent_id, OperationType.DELETE, path)
136
+ ws.delete(path)
137
+ return {"path": path, "deleted": True}
138
+
139
+ def _net_request(self, agent_id: str, p: dict) -> dict:
140
+ """A network request with auth headers (from an allow-list), and binary data in either direction through the workspace:
141
+ `body_path` sends a workspace file as the request body, `save_as` stores a non-text answer in the workspace (a literal
142
+ "{ext}" in the name becomes the right extension). Text answers come back as `text`."""
143
+ method = str(p.get("method", "GET")).upper()
144
+ if method not in ("GET", "POST"):
145
+ raise OSKernelError("only GET and POST requests are supported")
146
+ ws = self.workspace(agent_id)
147
+ headers = network.clean_headers(p.get("headers"))
148
+ data = None
149
+ if p.get("body_path"):
150
+ if p.get("json") is not None:
151
+ raise OSKernelError("send either a JSON body or a file, not both")
152
+ data = ws.read_bytes(str(p["body_path"]))
153
+ headers["Content-Type"] = str(p.get("content_type") or "application/octet-stream")
154
+ headers = network.clean_headers(headers)
155
+ save_as = str(p["save_as"]) if p.get("save_as") else ""
156
+ if save_as:
157
+ ws.resolve(save_as.replace("{ext}", "bin")) # validate the name before spending a request
158
+ res = network.request(method, str(p.get("url", "")), allow_private=self.cfg.allow_private_network,
159
+ allowed_hosts=list(self.cfg.allowed_hosts), timeout=self.cfg.net_timeout_seconds,
160
+ max_bytes=self.cfg.net_max_bytes, json=p.get("json"), headers=headers, data=data,
161
+ max_binary_bytes=self.cfg.max_binary_bytes, keep_binary=True)
162
+ body = res.pop("body", None)
163
+ if body is None:
164
+ return res
165
+ res["binary"] = True
166
+ if res["truncated"]:
167
+ raise OSKernelError(f"the answer is larger than the {self.cfg.max_binary_bytes:,}-byte limit")
168
+ if not save_as or res["status"] >= 400:
169
+ return {**res, "saved": ""}
170
+ ext = _EXT.get(res["content_type"].split(";")[0].strip().lower(), "bin")
171
+ path = save_as.replace("{ext}", ext)
172
+ ws.resolve(path)
173
+ self._authorize(agent_id, OperationType.OVERWRITE if ws.exists(path) else OperationType.WRITE, path)
174
+ ws.write_bytes(path, body)
175
+ return {**res, "saved": path}
@@ -0,0 +1,36 @@
1
+ """Read-only facts from the OS kernel. On Linux (Ubuntu) they come from /proc and /etc/os-release."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import platform
6
+ from pathlib import Path
7
+
8
+
9
+ def _distro() -> str:
10
+ try:
11
+ for line in Path("/etc/os-release").read_text(encoding="utf-8").splitlines():
12
+ if line.startswith("PRETTY_NAME="):
13
+ return line.split("=", 1)[1].strip().strip('"')
14
+ except OSError:
15
+ pass
16
+ return platform.system()
17
+
18
+
19
+ def _meminfo() -> dict:
20
+ out = {}
21
+ try:
22
+ for line in Path("/proc/meminfo").read_text(encoding="utf-8").splitlines():
23
+ key, _, rest = line.partition(":")
24
+ if key in ("MemTotal", "MemAvailable"):
25
+ out[key] = int(rest.split()[0]) // 1024
26
+ except (OSError, ValueError, IndexError):
27
+ pass
28
+ return {"total_mb": out.get("MemTotal"), "available_mb": out.get("MemAvailable")} if out else {}
29
+
30
+
31
+ def system_info() -> dict:
32
+ info: dict = {"system": platform.system(), "distro": _distro(), "kernel": platform.release(), "machine": platform.machine(),
33
+ "cpu_count": os.cpu_count(), "memory": _meminfo()}
34
+ if hasattr(os, "getloadavg"):
35
+ info["load_average"] = [round(x, 2) for x in os.getloadavg()]
36
+ return info
pal/paths.py ADDED
@@ -0,0 +1,59 @@
1
+ """Where Pal keeps its data, the same way on Linux, macOS and Windows.
2
+
3
+ Default is `~/.pal` (Path.home() resolves correctly on every OS). Set PAL_HOME to move it.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import re
9
+ from pathlib import Path
10
+
11
+ _WINDOWS_RESERVED = {"con", "prn", "aux", "nul", *(f"com{i}" for i in range(1, 10)), *(f"lpt{i}" for i in range(1, 10))}
12
+ _SAFE_ID = re.compile(r"[^A-Za-z0-9_-]")
13
+
14
+
15
+ def pal_home() -> Path:
16
+ env = os.environ.get("PAL_HOME")
17
+ return Path(env).expanduser() if env else Path.home() / ".pal"
18
+
19
+
20
+ def data_dir(*parts: str) -> Path:
21
+ return pal_home().joinpath(*parts)
22
+
23
+
24
+ def safe_id(value: str, default: str = "default", max_len: int = 64) -> str:
25
+ """Turn any string into something safe as one path component on every OS."""
26
+ out = _SAFE_ID.sub("", value or "")[:max_len]
27
+ if not out or out.lower() in _WINDOWS_RESERVED:
28
+ return default if not out else f"_{out}"
29
+ return out
30
+
31
+
32
+ def is_safe_relative_key(key: str) -> bool:
33
+ """True when `key` is a relative path that stays inside its folder on every OS: no `..`, no drive
34
+ letters or absolute paths, no backslashes, no characters Windows forbids, no reserved device names."""
35
+ if not key or key != key.strip() or "\\" in key or key.startswith("/") or "\x00" in key:
36
+ return False
37
+ for part in key.split("/"):
38
+ if part in ("", ".", "..") or re.search(r'[<>:"|?*\x00-\x1f]', part) or part.endswith((".", " ")):
39
+ return False
40
+ if part.split(".")[0].lower() in _WINDOWS_RESERVED:
41
+ return False
42
+ return True
43
+
44
+
45
+ def atomic_write_bytes(path: Path, data: bytes) -> None:
46
+ """Write via a temp file and rename, so a crash never leaves a half-written file (os.replace is
47
+ atomic on POSIX and replaces an existing file on Windows)."""
48
+ path.parent.mkdir(parents=True, exist_ok=True)
49
+ tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
50
+ try:
51
+ tmp.write_bytes(data)
52
+ os.replace(tmp, path)
53
+ finally:
54
+ if tmp.exists():
55
+ tmp.unlink(missing_ok=True)
56
+
57
+
58
+ def atomic_write_text(path: Path, text: str) -> None:
59
+ atomic_write_bytes(path, text.encode("utf-8"))
pal/pipeline.py ADDED
@@ -0,0 +1,177 @@
1
+ import time
2
+ import uuid
3
+
4
+ from pal.agent_constraints.pipeline import AgentConstraintsPipeline
5
+ from pal.compression.pipeline import CompressionPipeline
6
+ from pal.context_manager.components.pager import Segment
7
+ from pal.context_manager.pipeline import ContextManagerPipeline
8
+ from pal.llm_wrapper.components.base import LLMSyscall, WrapperConfig
9
+ from pal.llm_wrapper.pipeline import LLMWrapperPipeline
10
+ from pal.memory.pipeline import MemoryPipeline
11
+ from pal.metrics.pipeline import MetricsPipeline
12
+
13
+
14
+ class Pipeline:
15
+ """
16
+ One full chat turn, top to bottom:
17
+ user message -> compression + memory recall -> cache + paging ->
18
+ an LLM core (any deployment option) -> compressed reply, with
19
+ metrics recorded and history/memory updated.
20
+
21
+ This is the class every other entry point (webapp, orchestrator
22
+ agents, experiments) drives -- it's the composition root for every
23
+ pal/<component>/pipeline.py above.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ core_config: WrapperConfig,
29
+ session_id: str = None,
30
+ memory_store_path: str = None,
31
+ syscall_scheduler=None,
32
+ agent_name: str = None,
33
+ storage_manager=None,
34
+ ):
35
+ self.llm_wrapper = LLMWrapperPipeline(core_config)
36
+ self.context_manager = ContextManagerPipeline(self.llm_wrapper.core)
37
+ self.compression = CompressionPipeline()
38
+ self.agent_constraints = AgentConstraintsPipeline()
39
+ self.metrics = MetricsPipeline()
40
+ self.history: list[Segment] = []
41
+ self.session_id = session_id or str(uuid.uuid4())
42
+ self.agent_name = agent_name or self.session_id
43
+
44
+ # Persistent, cross-session memory (survives restarts) -- distinct
45
+ # from context_manager's pager, which only pages within THIS open
46
+ # conversation and forgets everything once the process exits.
47
+ # When a shared StorageManagerPipeline is supplied (AIOS 3.6 --
48
+ # see pal/storage_manager/), the memory manager's disk
49
+ # swaps route through it, scoped by this Pipeline's agent_name;
50
+ # otherwise MemoryPipeline falls back to its own private,
51
+ # unscoped local store.
52
+ self.storage_manager = storage_manager
53
+ self.memory = MemoryPipeline(
54
+ store_path=memory_store_path,
55
+ agent_id=self.agent_name,
56
+ storage_manager=storage_manager,
57
+ )
58
+
59
+ # Optional centralized syscall scheduler (AIOS 3.3 -- see
60
+ # orchestrator/components/syscall_scheduler.py). When one is
61
+ # supplied (typically the same SyscallScheduler an
62
+ # OrchestratorPipeline owns, shared across every agent/pipeline
63
+ # in the system), this Pipeline submits its LLM requests there
64
+ # instead of calling the core directly -- so ordering and
65
+ # time-slicing across MULTIPLE pipelines/agents actually goes
66
+ # through one queue, not a private one per Pipeline instance.
67
+ # Left as None by default (e.g. the standalone webapp use case),
68
+ # in which case run() falls back to calling llm_wrapper directly.
69
+ self.syscall_scheduler = syscall_scheduler
70
+
71
+ # kept for callers that reached into the old Router/adapter directly
72
+ # (e.g. the webapp's naive-vs-optimized benchmark path)
73
+ @property
74
+ def core(self):
75
+ return self.llm_wrapper.core
76
+
77
+ def run(self, system_prompt: str, user_message: str, **kwargs) -> str:
78
+ # Permanent output-behavior constraints, merged into every system
79
+ # prompt so the model generates tersely up front rather than
80
+ # being trimmed after the fact.
81
+ system_prompt = self.agent_constraints.with_constraints(system_prompt)
82
+
83
+ compressed_msg = self.compression.compress_input(user_message)
84
+
85
+ # Persistent memory recall: pull relevant notes from PAST
86
+ # sessions (not this conversation's own history). Injected into
87
+ # the system prompt so it doesn't distort the naive-vs-optimized
88
+ # token comparison for history paging below.
89
+ recalled_memories = self.memory.retrieve(compressed_msg, top_k=3)
90
+ if recalled_memories:
91
+ memory_block = "\n".join(f"- {n.content}" for n in recalled_memories)
92
+ system_prompt = f"{system_prompt}\n\nRelevant memory from past sessions:\n{memory_block}"
93
+
94
+ cache_hit, _ = self.context_manager.cache_system_prompt(system_prompt)
95
+ paged = self.context_manager.page_history(self.history, compressed_msg)
96
+
97
+ messages = [{"role": "system", "content": system_prompt}]
98
+ for seg in paged:
99
+ messages.append({"role": seg.role, "content": seg.content})
100
+ messages.append({"role": "user", "content": compressed_msg})
101
+
102
+ # Naive baseline: what a naive/unbounded implementation would
103
+ # send this turn -- the FULL history (not paged/trimmed) plus
104
+ # system prompt and new message. Comparing it against what was
105
+ # actually sent (optimized_tokens below) reflects real paging
106
+ # savings, not a same-payload measured-two-ways artifact.
107
+ naive_messages = [system_prompt] + [seg.content for seg in self.history] + [compressed_msg]
108
+ naive_tokens = sum(self.llm_wrapper.syscall_token_count(t) for t in naive_messages)
109
+
110
+ t0 = time.time()
111
+ response = self._dispatch_chat(messages, **kwargs)
112
+ latency = time.time() - t0
113
+
114
+ output_text = self.compression.compress_output(response.text)
115
+
116
+ # Optimized figure: the server's own reported prompt-eval count,
117
+ # when available. For local server cores this reflects tokens
118
+ # actually reprocessed -- if the backend's KV cache reused a
119
+ # matching prefix, this number comes back lower than the naive
120
+ # estimate on its own. Falls back to the estimate otherwise
121
+ # (meaning: no measurable saving, not a caching failure).
122
+ optimized_tokens = response.prompt_tokens if response.prompt_tokens else naive_tokens
123
+
124
+ self.metrics.record(
125
+ tokens_naive=naive_tokens,
126
+ tokens_optimized=optimized_tokens,
127
+ latency=latency,
128
+ cache_hit=cache_hit,
129
+ )
130
+
131
+ self.history.append(Segment(id=str(len(self.history)), content=compressed_msg, role="user"))
132
+ self.history.append(Segment(id=str(len(self.history)), content=output_text, role="assistant"))
133
+
134
+ # Persist this turn to long-term memory so it can be recalled in
135
+ # a FUTURE session (a future Pipeline instance, possibly after a
136
+ # restart) -- not just later in this same conversation.
137
+ self.memory.add(compressed_msg, session_id=self.session_id, tags=["user_turn"])
138
+
139
+ return output_text
140
+
141
+ def _dispatch_chat(self, messages: list[dict], **kwargs):
142
+ """Routes the actual inference call either through the shared
143
+ centralized SyscallScheduler (when one was supplied -- request
144
+ ordering/time-slicing then happens there, alongside every other
145
+ pipeline/agent sharing that scheduler) or directly through
146
+ llm_wrapper when running standalone."""
147
+ if self.syscall_scheduler is None:
148
+ return self.llm_wrapper.syscall_chat(messages, **kwargs)
149
+
150
+ syscall = LLMSyscall(
151
+ messages=messages,
152
+ model=kwargs.pop("model", None),
153
+ temperature=kwargs.pop("temperature", None),
154
+ max_tokens=kwargs.pop("max_tokens", None),
155
+ stream=kwargs.pop("stream", False),
156
+ params=kwargs,
157
+ )
158
+ request_id = self.syscall_scheduler.submit(self.agent_name, self.llm_wrapper.core, syscall)
159
+
160
+ # Drive the shared scheduler until THIS request reaches a
161
+ # terminal state. Other pipelines/agents sharing the same
162
+ # scheduler may have requests interleaved into these ticks too
163
+ # (that's the point of centralizing) -- this loop just doesn't
164
+ # return control to this Pipeline until its own turn is done.
165
+ req = self.syscall_scheduler.get_result(request_id)
166
+ while req.status.value in ("queued", "running"):
167
+ self.syscall_scheduler.tick()
168
+ req = self.syscall_scheduler.get_result(request_id)
169
+
170
+ if req.status.value == "error":
171
+ raise RuntimeError(f"syscall failed: {req.error}")
172
+ return req.result
173
+
174
+ def summary(self) -> dict:
175
+ """Metrics summary including cache memory overhead, so savings
176
+ can be checked against the cost of the caching layer itself."""
177
+ return self.metrics.summary(cache_overhead_bytes=self.context_manager.size_bytes())
pal/py.typed ADDED
File without changes
pal/redact.py ADDED
@@ -0,0 +1,19 @@
1
+ """Hide API keys in any text that may be shown or logged. Known keys are replaced exactly; strings shaped like common provider
2
+ keys are replaced too, because a provider's error message can echo a key we were never told about."""
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ _SECRET = re.compile(r"(sk-[A-Za-z0-9_\-]{8,}|AIza[0-9A-Za-z_\-]{10,}|AQ\.[A-Za-z0-9_\-]{20,}|AKIA[0-9A-Z]{12,}"
8
+ r"|Bearer\s+[A-Za-z0-9._\-]{12,})")
9
+
10
+
11
+ # a key=value pair whose name says it is a secret, as in a query string: ?api_key=abc&q=cats
12
+ _PAIR = re.compile(r"(?i)\b(api[_-]?key|apikey|appid|app_id|access[_-]?token|auth[_-]?token|token|secret|password|key)=([^&\s\"']+)")
13
+
14
+
15
+ def redact(text: str, *keys: str | None) -> str:
16
+ for k in keys:
17
+ if k:
18
+ text = text.replace(k, "***")
19
+ return _PAIR.sub(r"\1=***", _SECRET.sub("***", text))
pal/sdk/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ """The Pal SDK: build native agents on pre-defined APIs, or adapt agents written with other frameworks.
2
+
3
+ from pal.sdk import AgentSDK, adapt, NativeAgent, ReActAgent, ReflexionAgent
4
+ """
5
+ from pal.sdk.adapters import adapt, supported_frameworks
6
+ from pal.sdk.agent import AgentResult, AgentSpec, NativeAgent, agent_key, list_agents
7
+ from pal.sdk.components.autogen_adapter import AutoGenAdapter
8
+ from pal.sdk.components.autogen_client import autogen_model_client
9
+ from pal.sdk.components.framework_adapter_base import FrameworkAdapter, FrameworkAdapterError
10
+ from pal.sdk.components.metagpt_adapter import MetaGPTAdapter
11
+ from pal.sdk.components.open_interpreter_adapter import OpenInterpreterAdapter
12
+ from pal.sdk.components.tool_catalog import register_builtin_tools
13
+ from pal.sdk.os_api import OSAPI, register_os_tools
14
+ from pal.sdk.patterns import ReActAgent, ReflexionAgent
15
+ from pal.sdk.pipeline import AgentSDK
16
+ from pal.sdk.queries import (
17
+ OS_KERNEL,
18
+ PAL_KERNEL,
19
+ LLMQuery,
20
+ MemoryQuery,
21
+ OSQuery,
22
+ Query,
23
+ Response,
24
+ StorageQuery,
25
+ ToolQuery,
26
+ )
27
+ from pal.sdk.savers import CallSavers
28
+
29
+ __all__ = [
30
+ "AgentResult", "AgentSDK", "LLMQuery", "PAL_KERNEL", "MemoryQuery", "OSAPI", "OSQuery", "OS_KERNEL", "Query", "Response", "StorageQuery", "ToolQuery", "register_os_tools", "AgentSpec", "AutoGenAdapter", "CallSavers", "FrameworkAdapter",
31
+ "FrameworkAdapterError", "MetaGPTAdapter", "NativeAgent", "OpenInterpreterAdapter", "ReActAgent",
32
+ "ReflexionAgent", "adapt", "agent_key", "autogen_model_client", "list_agents", "register_builtin_tools", "supported_frameworks",
33
+ ]
pal/sdk/adapters.py ADDED
@@ -0,0 +1,43 @@
1
+ """One entry point for non-native agents: `adapt("autogen", sdk)`.
2
+
3
+ Third-party frameworks are adapted by replacing the framework's own core model-call function so the call goes to
4
+ the SDK instead; the agent's code is not changed. ReAct and Reflexion are reasoning patterns, not libraries, so
5
+ they ship as native agents (`ReActAgent`, `ReflexionAgent`).
6
+
7
+ sdk = AgentSDK.standalone(WrapperConfig(backend="gemini", model="...", api_key="..."))
8
+ with adapt("autogen", sdk):
9
+ ... # your existing AutoGen code, unchanged
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from pal.sdk.components.autogen_adapter import AutoGenAdapter
14
+ from pal.sdk.components.framework_adapter_base import FrameworkAdapter, FrameworkAdapterError
15
+ from pal.sdk.components.metagpt_adapter import MetaGPTAdapter
16
+ from pal.sdk.components.open_interpreter_adapter import OpenInterpreterAdapter
17
+ from pal.sdk.pipeline import AgentSDK
18
+
19
+ _ADAPTERS: dict[str, type[FrameworkAdapter]] = {
20
+ "autogen": AutoGenAdapter, "open-interpreter": OpenInterpreterAdapter, "metagpt": MetaGPTAdapter}
21
+ _ALIASES = {"open_interpreter": "open-interpreter", "interpreter": "open-interpreter", "pyautogen": "autogen"}
22
+ _PATTERNS = {"react": "ReActAgent", "reflexion": "ReflexionAgent"}
23
+
24
+
25
+ def _norm(name: str) -> str:
26
+ n = name.strip().lower()
27
+ return _ALIASES.get(n, n)
28
+
29
+
30
+ def adapt(framework: str, sdk: AgentSDK) -> FrameworkAdapter:
31
+ """An adapter for `framework`. Use it as a context manager, or call install() and uninstall() yourself."""
32
+ n = _norm(framework)
33
+ if n in _PATTERNS:
34
+ raise FrameworkAdapterError(f"{framework} is a reasoning pattern, not a library: use {_PATTERNS[n]}(sdk) "
35
+ "from pal.sdk")
36
+ cls = _ADAPTERS.get(n)
37
+ if cls is None:
38
+ raise FrameworkAdapterError(f"no adapter for {framework!r}; supported: {', '.join(supported_frameworks())}")
39
+ return cls(sdk)
40
+
41
+
42
+ def supported_frameworks() -> list[str]:
43
+ return [*_ADAPTERS, *_PATTERNS]
pal/sdk/agent.py ADDED
@@ -0,0 +1,99 @@
1
+ """Native agent development: what a developer subclasses to build an agent for Pal.
2
+
3
+ A *native* agent is written for Pal: it talks to the system only through the SDK's pre-defined APIs
4
+ (`self.sdk.chat`, `.remember`, `.recall`, `.write`, `.read`, `.search_knowledge`, `.call_tool`, `.request_access`).
5
+ It never holds a model client, a file handle or a tool directly, so Pal can route, trim, cache, limit and
6
+ measure every call. A *non-native* agent is written with another framework and is adapted instead
7
+ (see `pal.sdk.adapt`).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from collections.abc import Mapping
13
+ from dataclasses import dataclass, field
14
+ from typing import Any, ClassVar, Protocol
15
+
16
+ from pal.errors import UserInputError
17
+
18
+ _READ_TOOLS = frozenset({"read_source", "search_documents", "read_document"})
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class AgentSpec:
23
+ """The small, declarative part of an agent."""
24
+
25
+ id: str
26
+ name: str
27
+ description: str
28
+ role: str # kept short: it is sent with every request
29
+ key_env: str = "" # environment variable holding this agent's API key ("" = none needed)
30
+ tools: frozenset[str] = _READ_TOOLS
31
+ max_tokens: int = 1024 # output cap per call (free tiers are small)
32
+
33
+
34
+ @dataclass
35
+ class AgentResult:
36
+ answer: str
37
+ ok: bool = True
38
+ steps: list[Any] = field(default_factory=list) # ReAct steps, Reflexion trials, ...
39
+ error: str = ""
40
+ meta: dict = field(default_factory=dict)
41
+
42
+
43
+ class Kernel(Protocol):
44
+ """Anything that runs a full token-saving turn: ChatService does."""
45
+
46
+ def send(self, session_id: str, provider: str, model: str, api_key: str | None, message: str,
47
+ attachments: list[dict], *args: Any, **kw: Any) -> Any: ...
48
+
49
+
50
+ class NativeAgent:
51
+ """Base class for native agents.
52
+
53
+ `sdk` gives the pre-defined APIs. `kernel` (optional) is the token-saving turn pipeline; when present, the
54
+ default `run()` uses it, so the agent gets input trimming, document and code-graph retrieval, paged history
55
+ and output rules for free. Without a kernel, `run()` makes one plain SDK call with the agent's role.
56
+ Override `run()` to write your own logic with the SDK calls (see ReActAgent and ReflexionAgent).
57
+ """
58
+
59
+ spec: ClassVar[AgentSpec | None] = None
60
+
61
+ def __init__(self, sdk: Any = None, kernel: Kernel | None = None):
62
+ self.sdk = sdk
63
+ self.kernel = kernel
64
+
65
+ def run(self, task: str, attachments: list[dict] | None = None, session: str = "agent") -> AgentResult:
66
+ spec = self.spec
67
+ if spec is None:
68
+ raise NotImplementedError("a native agent needs a `spec` or its own run()")
69
+ if self.kernel is not None:
70
+ res = self.kernel.send(session, "", "", None, task, list(attachments or []), agent=spec.id)
71
+ return AgentResult(res.reply, meta={"input_tokens": res.input_tokens, "output_tokens": res.output_tokens})
72
+ if self.sdk is None:
73
+ raise RuntimeError("no SDK or kernel: create the agent with sdk= or kernel=")
74
+ text = self.sdk.chat([{"role": "system", "content": spec.role}, {"role": "user", "content": task}],
75
+ max_tokens=spec.max_tokens)
76
+ return AgentResult(text)
77
+
78
+
79
+ def agent_key(spec: AgentSpec, env: Mapping[str, str] | None = None) -> str | None:
80
+ """The agent's own key, else the shared GEMINI_API_KEY."""
81
+ env = os.environ if env is None else env
82
+ return (env.get(spec.key_env) if spec.key_env else None) or env.get("GEMINI_API_KEY") or None
83
+
84
+
85
+ def get_spec(agents: Mapping[str, type[NativeAgent]], agent_id: str) -> AgentSpec:
86
+ cls = agents.get(agent_id)
87
+ if cls is None or cls.spec is None:
88
+ raise UserInputError(f"unknown agent {agent_id!r}; choose one of: {', '.join(agents)}")
89
+ return cls.spec
90
+
91
+
92
+ def list_agents(agents: Mapping[str, type[NativeAgent]], env: Mapping[str, str] | None = None) -> list[dict]:
93
+ out = []
94
+ for cls in agents.values():
95
+ s = cls.spec
96
+ if s is not None:
97
+ out.append({"id": s.id, "name": s.name, "description": s.description,
98
+ "ready": bool(agent_key(s, env)), "key_env": s.key_env})
99
+ return out
@@ -0,0 +1,14 @@
1
+ from pal.sdk.components.autogen_adapter import AutoGenAdapter
2
+ from pal.sdk.components.framework_adapter_base import FrameworkAdapter, FrameworkAdapterError
3
+ from pal.sdk.components.metagpt_adapter import MetaGPTAdapter
4
+ from pal.sdk.components.open_interpreter_adapter import OpenInterpreterAdapter
5
+ from pal.sdk.components.tool_catalog import register_builtin_tools
6
+
7
+ __all__ = [
8
+ "AutoGenAdapter",
9
+ "FrameworkAdapter",
10
+ "FrameworkAdapterError",
11
+ "MetaGPTAdapter",
12
+ "OpenInterpreterAdapter",
13
+ "register_builtin_tools",
14
+ ]