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
pal/sdk/pipeline.py ADDED
@@ -0,0 +1,240 @@
1
+ """
2
+ AIOS SDK (AIOS section 3.9): what an agent developer actually imports
3
+ and uses. Wraps every kernel manager behind one small, agent-facing
4
+ interface -- chat(), remember()/recall(), read()/write()/delete(),
5
+ call_tool(), search_knowledge(), request_access() -- so building an
6
+ agent means calling straightforward methods, not assembling
7
+ LLMSyscalls, StorageSyscalls, and ToolCallRequests by hand. Every
8
+ method here is a thin pass-through to the corresponding manager; the
9
+ abstraction IS not having to know those managers exist, matching "this
10
+ SDK... abstracts complex system calls, allowing developers to focus on
11
+ the agent's internal workflows."
12
+
13
+ One AgentSDK instance is scoped to exactly one agent_id -- every
14
+ storage/memory/tool call it makes is attributed to that agent, and
15
+ every access-manager check runs as that agent. OrchestratorPipeline.
16
+ sdk_for() is the usual way to get one, wired to the orchestrator's
17
+ shared managers.
18
+ """
19
+ import time
20
+ from pathlib import Path
21
+ from types import SimpleNamespace
22
+ from typing import Any, Optional
23
+
24
+ from pal.access_manager.components.base import AccessDecision, OperationType
25
+ from pal.access_manager.pipeline import AccessManagerPipeline
26
+ from pal.errors import PalError, UserInputError
27
+ from pal.llm_wrapper.components.base import LLMResponse, LLMSyscall, LLMWrapper
28
+ from pal.llm_wrapper.pipeline import LLMWrapperPipeline
29
+ from pal.memory.pipeline import MemoryPipeline
30
+ from pal.os_kernel import OSKernel
31
+ from pal.sdk.os_api import OSAPI
32
+ from pal.sdk.queries import (
33
+ OS_KERNEL,
34
+ PAL_KERNEL,
35
+ LLMQuery,
36
+ MemoryQuery,
37
+ OSQuery,
38
+ Query,
39
+ Response,
40
+ StorageQuery,
41
+ ToolQuery,
42
+ )
43
+ from pal.sdk.savers import CallSavers
44
+ from pal.storage_manager.pipeline import StorageManagerPipeline
45
+ from pal.storage_manager.utils.sdk import StorageSDK
46
+ from pal.tool_manager.pipeline import ToolManagerPipeline
47
+
48
+
49
+ class AgentSDK:
50
+ def __init__(
51
+ self,
52
+ agent_id: str,
53
+ llm_wrapper: Optional[LLMWrapperPipeline],
54
+ storage_manager: StorageManagerPipeline,
55
+ tool_manager: ToolManagerPipeline,
56
+ access_manager: AccessManagerPipeline,
57
+ memory: Optional[MemoryPipeline] = None,
58
+ syscall_scheduler=None,
59
+ savers: Optional[CallSavers] = None,
60
+ os_kernel: Optional[OSKernel] = None,
61
+ ):
62
+ self.agent_id = agent_id
63
+ self.savers = savers
64
+ self._os_kernel = os_kernel
65
+ self.os = OSAPI(self)
66
+ self._llm_wrapper = llm_wrapper
67
+ self._syscall_scheduler = syscall_scheduler
68
+ self._storage = StorageSDK(agent_id, storage_manager)
69
+ self._tool_manager = tool_manager
70
+ self._access_manager = access_manager
71
+ self.memory = memory or MemoryPipeline(agent_id=agent_id, storage_manager=storage_manager)
72
+
73
+ @classmethod
74
+ def standalone(cls, core, agent_id: str = "agent", root: Optional[str] = None,
75
+ savers: Optional[CallSavers] = None, os_kernel: Optional[OSKernel] = None) -> "AgentSDK":
76
+ """A ready-to-use SDK with no orchestrator: one call to start using Pal from your own agent code.
77
+ `core` is an LLMWrapper or a WrapperConfig. Storage lives under `root` (default ~/.pal/sdk). Built-in tools
78
+ (calculator, time, word count) are registered. Light savers (tidy) are on; the exact-repeat cache is off."""
79
+ from pal.llm_wrapper.components.base import WrapperConfig
80
+ from pal.llm_wrapper.utils.registry import build_wrapper
81
+ from pal.paths import data_dir
82
+ from pal.sdk.components.tool_catalog import register_builtin_tools
83
+
84
+ active = build_wrapper(core) if isinstance(core, WrapperConfig) else core
85
+ base = root or data_dir("sdk")
86
+ storage = StorageManagerPipeline(root=str(base))
87
+ tools = ToolManagerPipeline()
88
+ register_builtin_tools(tools)
89
+ access = AccessManagerPipeline()
90
+ kernel = os_kernel or OSKernel(access=access, root=Path(base) / "workspaces")
91
+ return cls(agent_id, SimpleNamespace(core=active), storage, tools, access, # type: ignore[arg-type]
92
+ savers=savers if savers is not None else CallSavers(), os_kernel=kernel)
93
+
94
+ @property
95
+ def stats(self) -> dict:
96
+ """Calls, cache hits, tidied characters, provider-reported tokens and time spent in model calls."""
97
+ s = self.savers.stats if self.savers else None
98
+ return {k: getattr(s, k) for k in ("calls", "cache_hits", "chars_tidied", "prompt_tokens",
99
+ "completion_tokens", "seconds")} if s else {}
100
+
101
+ # -- chat / inference -------------------------------------------------------
102
+
103
+ def chat_response(self, messages: list[dict], core: Optional[LLMWrapper] = None, **kwargs) -> LLMResponse:
104
+ """Runs one LLM inference call and returns the full response (text plus token counts).
105
+ Under the hood this is an LLM syscall -- through the shared centralized SyscallScheduler when
106
+ one was supplied (AIOS 3.3), or directly against the core otherwise. `core` overrides the
107
+ SDK's own core for this call (e.g. a per-request API model chosen in the UI)."""
108
+ active = core or (self._llm_wrapper.core if self._llm_wrapper is not None else None)
109
+ if active is None:
110
+ raise RuntimeError("no LLM core: pass core= or construct the SDK with llm_wrapper")
111
+ sv = self.savers
112
+ key = None
113
+ if sv is not None:
114
+ messages = sv.prepare(messages)
115
+ key = sv.key(getattr(getattr(active, "config", None), "model", ""), messages, kwargs)
116
+ cached = sv.get(key)
117
+ if cached is not None:
118
+ sv.stats.calls += 1
119
+ return cached
120
+ started = time.perf_counter()
121
+ resp = self._infer(active, messages, kwargs)
122
+ if sv is not None:
123
+ sv.stats.calls += 1
124
+ sv.stats.prompt_tokens += resp.prompt_tokens or 0
125
+ sv.stats.completion_tokens += resp.completion_tokens or 0
126
+ sv.stats.seconds += time.perf_counter() - started
127
+ sv.put(key or "", resp)
128
+ return resp
129
+
130
+ def _infer(self, active: LLMWrapper, messages: list[dict], kwargs: dict) -> LLMResponse:
131
+ syscall = LLMSyscall(messages=messages, **kwargs)
132
+ if self._syscall_scheduler is not None:
133
+ request_id = self._syscall_scheduler.submit(self.agent_id, active, syscall)
134
+ req = self._syscall_scheduler.get_result(request_id)
135
+ while req.status.value in ("queued", "running"):
136
+ self._syscall_scheduler.tick()
137
+ req = self._syscall_scheduler.get_result(request_id)
138
+ if req.status.value == "error":
139
+ raise RuntimeError(f"chat failed: {req.error}")
140
+ return req.result
141
+ return active.chat(syscall)
142
+
143
+ def chat(self, messages: list[dict], core: Optional[LLMWrapper] = None, **kwargs) -> str:
144
+ """Like chat_response(), returning only the reply text."""
145
+ return self.chat_response(messages, core=core, **kwargs).text
146
+
147
+ # -- query routing: LLM queries to the Pal kernel, non-LLM queries to the OS kernel ----------------------
148
+ def send_request(self, query: Query) -> Response:
149
+ """The one place that decides where a query goes, by its class alone:
150
+ LLMQuery to the Pal kernel (model), Memory/Storage/Tool queries to the Pal kernel's managers, OSQuery to the
151
+ OS kernel (Ubuntu: files, network, system facts). A refusal or failure comes back as Response(ok=False)."""
152
+ cls = getattr(query, "query_class", "")
153
+ dest = OS_KERNEL if cls == "os" else PAL_KERNEL
154
+ try:
155
+ if isinstance(query, LLMQuery):
156
+ kw = {k: v for k, v in (("max_tokens", query.max_tokens), ("temperature", query.temperature)) if v is not None}
157
+ return Response(True, self.chat_response(query.messages, **kw), handled_by=dest)
158
+ if isinstance(query, MemoryQuery):
159
+ data = self.remember(**query.params) if query.op == "remember" else self.recall(**query.params)
160
+ return Response(True, data, handled_by=dest)
161
+ if isinstance(query, StorageQuery):
162
+ return Response(True, self._storage_op(query), handled_by=dest)
163
+ if isinstance(query, ToolQuery):
164
+ return Response(True, self.call_tool(query.tool, **query.args), handled_by=dest)
165
+ if isinstance(query, OSQuery):
166
+ if self._os_kernel is None:
167
+ raise UserInputError("this SDK has no OS kernel interface")
168
+ return Response(True, self._os_kernel.handle(self.agent_id, query.op, query.params), handled_by=dest)
169
+ except (PalError, RuntimeError, ValueError, TypeError, KeyError) as e:
170
+ return Response(False, error=str(e), handled_by=dest)
171
+ raise UserInputError(f"unknown query class {cls!r}")
172
+
173
+ def _storage_op(self, q: StorageQuery) -> Any:
174
+ ops = {"read": self.read, "write": self.write, "delete": self.delete, "list": self.list_keys,
175
+ "add_knowledge": self.add_knowledge, "search_knowledge": self.search_knowledge}
176
+ if q.op not in ops:
177
+ raise UserInputError(f"unknown storage operation {q.op!r}")
178
+ return ops[q.op](**q.params)
179
+
180
+ # -- memory (AIOS 3.5) --------------------------------------------------------
181
+
182
+ def remember(self, content: str, tags: Optional[list[str]] = None, session_id: Optional[str] = None):
183
+ return self.memory.add(content, session_id=session_id or self.agent_id, tags=tags)
184
+
185
+ def recall(self, query: str, top_k: int = 3):
186
+ return self.memory.retrieve(query, top_k=top_k)
187
+
188
+ # -- storage: own data (AIOS 3.6) ---------------------------------------------
189
+
190
+ def read(self, key: str) -> Optional[Any]:
191
+ return self._storage.read(key)
192
+
193
+ def write(self, key: str, data: Any) -> None:
194
+ self._storage.write(key, data)
195
+
196
+ def delete(self, key: str) -> None:
197
+ self._storage.delete(key)
198
+
199
+ def list_keys(self, prefix: str = "") -> list[str]:
200
+ return self._storage.list_keys(prefix=prefix)
201
+
202
+ def add_knowledge(self, doc_id: str, text: str, metadata: Optional[dict] = None) -> None:
203
+ self._storage.vector_upsert(doc_id, text, metadata=metadata)
204
+
205
+ def search_knowledge(self, query: str, top_k: int = 3) -> list[dict]:
206
+ return self._storage.vector_query(query, top_k=top_k)
207
+
208
+ # -- storage: another agent's data, subject to the Access Manager (3.8) ------
209
+
210
+ def read_from(self, target_agent_id: str, key: str) -> Optional[Any]:
211
+ return self._storage.read_from(target_agent_id, key)
212
+
213
+ def write_to(self, target_agent_id: str, key: str, data: Any) -> None:
214
+ self._storage.write_to(target_agent_id, key, data)
215
+
216
+ def delete_from(self, target_agent_id: str, key: str) -> None:
217
+ self._storage.delete_from(target_agent_id, key)
218
+
219
+ def request_access(self, target_agent_id: str, operation: OperationType, resource: str = "") -> AccessDecision:
220
+ """Checks (without performing) whether this agent would be
221
+ allowed to touch target_agent_id's resource -- useful for an
222
+ agent to decide whether to even attempt a cross-agent call
223
+ before issuing it."""
224
+ return self._access_manager.check_access(self.agent_id, target_agent_id, operation, resource=resource)
225
+
226
+ # -- tools (AIOS 3.7) -----------------------------------------------------------
227
+
228
+ def call_tool(self, tool_name: str, **kwargs) -> Any:
229
+ result = self._tool_manager.invoke(self.agent_id, tool_name, kwargs)
230
+ if not result.ok:
231
+ raise RuntimeError(f"tool '{tool_name}' failed: {result.error}")
232
+ return result.result
233
+
234
+ def list_tools(self) -> list[str]:
235
+ return self._tool_manager.list_tools()
236
+
237
+ def close(self) -> None:
238
+ """Delete this agent's stored files and vectors (a chat session ended). Memory notes are
239
+ kept: they are the cross-session part."""
240
+ self._storage.storage_manager.purge_agent(self.agent_id)
pal/sdk/queries.py ADDED
@@ -0,0 +1,68 @@
1
+ """Typed queries an agent sends through the SDK, and where each kind goes.
2
+
3
+ LLMQuery -> Pal kernel (model work: reasoning, planning, tool parsing, code)
4
+ MemoryQuery, StorageQuery, ToolQuery -> Pal kernel (its memory, storage and tool managers)
5
+ OSQuery -> OS kernel (file operations, network requests, system facts)
6
+
7
+ An OSQuery never reaches a model and an LLMQuery never touches the filesystem or the network: the router in
8
+ `AgentSDK.send_request` is the only place that decides, and it decides by query class alone.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, ClassVar
14
+
15
+ PAL_KERNEL = "pal_kernel"
16
+ OS_KERNEL = "os_kernel"
17
+
18
+
19
+ @dataclass
20
+ class Query:
21
+ query_class: ClassVar[str] = ""
22
+
23
+
24
+ @dataclass
25
+ class LLMQuery(Query):
26
+ query_class: ClassVar[str] = "llm"
27
+ messages: list[dict] = field(default_factory=list)
28
+ max_tokens: int | None = None
29
+ temperature: float | None = None
30
+
31
+
32
+ @dataclass
33
+ class MemoryQuery(Query):
34
+ """op: remember | recall"""
35
+ query_class: ClassVar[str] = "memory"
36
+ op: str = "recall"
37
+ params: dict = field(default_factory=dict)
38
+
39
+
40
+ @dataclass
41
+ class StorageQuery(Query):
42
+ """op: read | write | delete | list | add_knowledge | search_knowledge"""
43
+ query_class: ClassVar[str] = "storage"
44
+ op: str = "read"
45
+ params: dict = field(default_factory=dict)
46
+
47
+
48
+ @dataclass
49
+ class ToolQuery(Query):
50
+ query_class: ClassVar[str] = "tool"
51
+ tool: str = ""
52
+ args: dict = field(default_factory=dict)
53
+
54
+
55
+ @dataclass
56
+ class OSQuery(Query):
57
+ """op: file_read | file_write | file_list | file_delete | net_get | net_post | sys_info | keyboard | mouse"""
58
+ query_class: ClassVar[str] = "os"
59
+ op: str = "sys_info"
60
+ params: dict = field(default_factory=dict)
61
+
62
+
63
+ @dataclass
64
+ class Response:
65
+ ok: bool
66
+ data: Any = None
67
+ error: str = ""
68
+ handled_by: str = "" # PAL_KERNEL or OS_KERNEL
pal/sdk/savers.py ADDED
@@ -0,0 +1,88 @@
1
+ """Light token savers for calls that come from agents Pal does not control (adapted frameworks).
2
+
3
+ The full token-saving pipeline (input trimming by a local model, document and code-graph retrieval, paged history,
4
+ output rules) belongs to the chat turn, where Pal owns the prompt. A framework such as AutoGen owns its own
5
+ prompts, so here only safe, deterministic steps run:
6
+ * tidy: trailing spaces and runs of blank lines are removed outside code blocks (code is never touched);
7
+ * optional exact-repeat cache: an identical request inside the TTL is answered with no model call. Off by default,
8
+ because agent loops sometimes retry on purpose and expect a different answer.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import re
15
+ import threading
16
+ import time
17
+ from collections import OrderedDict
18
+ from dataclasses import dataclass, replace
19
+
20
+ from pal.llm_wrapper.components.base import LLMResponse
21
+
22
+ _TRAILING = re.compile(r"[ \t]+(?=\n)")
23
+ _BLANKS = re.compile(r"\n{3,}")
24
+
25
+
26
+ def tidy(text: str) -> str:
27
+ parts = text.split("```")
28
+ for i in range(0, len(parts), 2): # even parts are outside fenced code
29
+ parts[i] = _BLANKS.sub("\n\n", _TRAILING.sub("", parts[i]))
30
+ return "```".join(parts)
31
+
32
+
33
+ @dataclass
34
+ class CallStats:
35
+ calls: int = 0
36
+ cache_hits: int = 0
37
+ chars_tidied: int = 0
38
+ prompt_tokens: int = 0
39
+ completion_tokens: int = 0
40
+ seconds: float = 0.0
41
+
42
+
43
+ class CallSavers:
44
+ def __init__(self, tidy_messages: bool = True, cache: bool = False, cache_size: int = 64, cache_ttl: float = 300.0):
45
+ self.tidy_messages = tidy_messages
46
+ self.cache = cache
47
+ self.cache_size = cache_size
48
+ self.cache_ttl = cache_ttl
49
+ self.stats = CallStats()
50
+ self._lock = threading.Lock()
51
+ self._store: OrderedDict[str, tuple[float, LLMResponse]] = OrderedDict()
52
+
53
+ def prepare(self, messages: list[dict]) -> list[dict]:
54
+ if not self.tidy_messages:
55
+ return messages
56
+ out = []
57
+ for m in messages:
58
+ c = m.get("content")
59
+ if isinstance(c, str):
60
+ t = tidy(c)
61
+ self.stats.chars_tidied += len(c) - len(t)
62
+ m = {**m, "content": t}
63
+ out.append(m)
64
+ return out
65
+
66
+ def key(self, model: str, messages: list[dict], kwargs: dict) -> str:
67
+ blob = json.dumps([model, messages, sorted(kwargs.items())], sort_keys=True, default=str)
68
+ return hashlib.sha256(blob.encode("utf-8")).hexdigest()
69
+
70
+ def get(self, key: str) -> LLMResponse | None:
71
+ if not self.cache:
72
+ return None
73
+ with self._lock:
74
+ hit = self._store.get(key)
75
+ if hit is None or time.monotonic() - hit[0] > self.cache_ttl:
76
+ self._store.pop(key, None)
77
+ return None
78
+ self._store.move_to_end(key)
79
+ self.stats.cache_hits += 1
80
+ return replace(hit[1], prompt_tokens=0, completion_tokens=0, meta={**hit[1].meta, "cache": True})
81
+
82
+ def put(self, key: str, resp: LLMResponse) -> None:
83
+ if not self.cache:
84
+ return
85
+ with self._lock:
86
+ self._store[key] = (time.monotonic(), resp)
87
+ while len(self._store) > self.cache_size:
88
+ self._store.popitem(last=False)
@@ -0,0 +1,2 @@
1
+ """No shared utilities yet for sdk -- reserved for future helpers (e.g.
2
+ config-file-driven agent bootstrapping)."""
@@ -0,0 +1,17 @@
1
+ from pal.storage_manager.components.base import StorageOp, StorageResponse, StorageSyscall
2
+ from pal.storage_manager.components.local_file_backend import LocalFileBackend
3
+ from pal.storage_manager.components.vector_store_backend import VectorStoreBackend
4
+ from pal.storage_manager.pipeline import StorageManagerPipeline
5
+ from pal.storage_manager.utils.memory_adapter import AgentScopedMemoryStorage
6
+ from pal.storage_manager.utils.sdk import StorageSDK
7
+
8
+ __all__ = [
9
+ "StorageManagerPipeline",
10
+ "StorageOp",
11
+ "StorageResponse",
12
+ "StorageSyscall",
13
+ "LocalFileBackend",
14
+ "VectorStoreBackend",
15
+ "StorageSDK",
16
+ "AgentScopedMemoryStorage",
17
+ ]
@@ -0,0 +1,19 @@
1
+ from pal.storage_manager.components.base import StorageOp, StorageResponse, StorageSyscall
2
+ from pal.storage_manager.components.local_file_backend import LocalFileBackend
3
+ from pal.storage_manager.components.storage_queue import (
4
+ StorageRequest,
5
+ StorageSyscallQueue,
6
+ StorageSyscallStatus,
7
+ )
8
+ from pal.storage_manager.components.vector_store_backend import VectorStoreBackend
9
+
10
+ __all__ = [
11
+ "StorageOp",
12
+ "StorageResponse",
13
+ "StorageSyscall",
14
+ "LocalFileBackend",
15
+ "StorageRequest",
16
+ "StorageSyscallQueue",
17
+ "StorageSyscallStatus",
18
+ "VectorStoreBackend",
19
+ ]
@@ -0,0 +1,50 @@
1
+ """
2
+ Unified system-call layer for storage. Agents (via the SDK,
3
+ utils/sdk.py) and other modules (the memory manager, when it swaps
4
+ data to disk) never talk to a backend directly -- they issue a
5
+ StorageSyscall and get a StorageResponse back, same "syscall, not a
6
+ direct backend call" shape as llm_wrapper' LLMSyscall/LLMResponse.
7
+ """
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum
10
+ from typing import Any, Optional
11
+
12
+
13
+ class StorageOp(str, Enum):
14
+ READ = "read"
15
+ WRITE = "write"
16
+ DELETE = "delete"
17
+ LIST = "list"
18
+ VECTOR_UPSERT = "vector_upsert"
19
+ VECTOR_QUERY = "vector_query"
20
+
21
+
22
+ @dataclass
23
+ class StorageSyscall:
24
+ """One storage request, always scoped to the agent that owns the
25
+ data -- "the storage manager reads and writes data based on the
26
+ agent ID passed from the memory manager" applies just as much to an
27
+ agent's own direct SDK calls, so agent_id is required on every op,
28
+ not just memory-manager swaps."""
29
+
30
+ agent_id: str # whose resource this operates on (the "owner"/target)
31
+ op: StorageOp
32
+ key: Optional[str] = None # file ops: the key/filename; LIST: prefix filter
33
+ data: Any = None # WRITE: value to store; VECTOR_UPSERT: document text
34
+ query_text: Optional[str] = None # VECTOR_QUERY
35
+ top_k: int = 3 # VECTOR_QUERY
36
+ metadata: dict = field(default_factory=dict) # VECTOR_UPSERT
37
+ # Who is actually making this call -- distinct from agent_id when an
38
+ # agent reads/writes ANOTHER agent's resource. None means "the owner
39
+ # is calling on its own behalf", the common case, which needs no
40
+ # access-manager cross-agent check (AIOS 3.8). Only meaningful when
41
+ # a StorageManagerPipeline has an access_manager wired in; ignored
42
+ # otherwise.
43
+ requesting_agent_id: Optional[str] = None
44
+
45
+
46
+ @dataclass
47
+ class StorageResponse:
48
+ ok: bool
49
+ data: Any = None
50
+ error: Optional[str] = None
@@ -0,0 +1,84 @@
1
+ """
2
+ Local-file storage backend, with one root subdirectory PER AGENT so different agents' files never
3
+ collide. Values are stored as JSON when JSON-serializable and as raw bytes otherwise.
4
+
5
+ Portable and safe on Linux, macOS and Windows:
6
+ - agent ids and keys are validated (no `..`, absolute paths, drive letters, backslashes, characters
7
+ or device names Windows forbids), and the resolved path must stay inside the agent's folder;
8
+ - writes are atomic (temp file + rename), so a crash never leaves a half-written value;
9
+ - keys are always reported with forward slashes.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import shutil
15
+ from pathlib import Path
16
+ from typing import Any, Optional
17
+
18
+ from pal.errors import UserInputError
19
+ from pal.paths import atomic_write_bytes, is_safe_relative_key, safe_id
20
+
21
+
22
+ class InvalidStorageKey(UserInputError):
23
+ pass
24
+
25
+
26
+ class LocalFileBackend:
27
+ def __init__(self, root: Path):
28
+ self.root = Path(root)
29
+ self.root.mkdir(parents=True, exist_ok=True)
30
+
31
+ def _agent_dir(self, agent_id: str) -> Path:
32
+ if not agent_id or safe_id(agent_id) != agent_id:
33
+ raise InvalidStorageKey(f"invalid agent id: {agent_id!r}")
34
+ d = self.root / agent_id
35
+ d.mkdir(parents=True, exist_ok=True)
36
+ return d
37
+
38
+ def _path(self, agent_id: str, key: str) -> Path:
39
+ base = self._agent_dir(agent_id)
40
+ if not is_safe_relative_key(key):
41
+ raise InvalidStorageKey(f"invalid storage key: {key!r}")
42
+ path = (base / key).resolve()
43
+ if base.resolve() not in path.parents:
44
+ raise InvalidStorageKey(f"storage key escapes its folder: {key!r}")
45
+ return path
46
+
47
+ def write(self, agent_id: str, key: str, data: Any) -> None:
48
+ path = self._path(agent_id, key)
49
+ raw = bytes(data) if isinstance(data, (bytes, bytearray)) else json.dumps(data).encode("utf-8")
50
+ atomic_write_bytes(path, raw)
51
+
52
+ def read(self, agent_id: str, key: str) -> Optional[Any]:
53
+ path = self._path(agent_id, key)
54
+ if not path.is_file():
55
+ return None
56
+ try:
57
+ return json.loads(path.read_text(encoding="utf-8"))
58
+ except (UnicodeDecodeError, json.JSONDecodeError):
59
+ return path.read_bytes()
60
+
61
+ def delete(self, agent_id: str, key: str) -> None:
62
+ path = self._path(agent_id, key)
63
+ if path.is_file():
64
+ path.unlink()
65
+
66
+ def exists(self, agent_id: str, key: str) -> bool:
67
+ return self._path(agent_id, key).is_file()
68
+
69
+ def list_keys(self, agent_id: str, prefix: str = "") -> list[str]:
70
+ agent_dir = self._agent_dir(agent_id)
71
+ keys = []
72
+ for p in agent_dir.rglob("*"):
73
+ if not p.is_file() or p.name.endswith(".tmp"):
74
+ continue
75
+ rel = p.relative_to(agent_dir).as_posix()
76
+ if rel.startswith(prefix):
77
+ keys.append(rel)
78
+ return sorted(keys)
79
+
80
+ def purge(self, agent_id: str) -> None:
81
+ """Delete everything stored for one agent."""
82
+ d = self.root / agent_id
83
+ if d.is_dir() and safe_id(agent_id) == agent_id:
84
+ shutil.rmtree(d, ignore_errors=True)
@@ -0,0 +1,91 @@
1
+ """
2
+ Centralized storage system-call queue (AIOS section 3.6): "the agent
3
+ calls the storage API in the SDK, which is further converted into
4
+ storage-related system calls and put into the storage queue by the
5
+ scheduler. The storage manager then processes the requests in the
6
+ queue."
7
+
8
+ Mirrors how llm_wrapper' inference requests are centralized in
9
+ orchestrator/components/syscall_scheduler.py (section 3.3) -- one
10
+ queue, submit() to add work, something drives it forward. Storage ops
11
+ are effectively instantaneous local file/DB operations (no
12
+ multi-second generation to time-slice), so this queue only needs FIFO
13
+ processing; there's no equivalent need for Round Robin / context-
14
+ interrupt machinery here.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import threading
19
+ import time
20
+ import uuid
21
+ from collections import deque
22
+ from dataclasses import dataclass, field
23
+ from enum import Enum
24
+ from typing import Optional
25
+
26
+ from pal.storage_manager.components.base import StorageResponse, StorageSyscall
27
+
28
+
29
+ class StorageSyscallStatus(str, Enum):
30
+ QUEUED = "queued"
31
+ DONE = "done"
32
+ ERROR = "error"
33
+
34
+
35
+ @dataclass
36
+ class StorageRequest:
37
+ syscall: StorageSyscall
38
+ id: str = field(default_factory=lambda: str(uuid.uuid4()))
39
+ submitted_at: float = field(default_factory=time.time)
40
+ status: StorageSyscallStatus = StorageSyscallStatus.QUEUED
41
+ response: Optional[StorageResponse] = None
42
+
43
+
44
+ class StorageSyscallQueue:
45
+ def __init__(self, pipeline):
46
+ # `pipeline` is a StorageManagerPipeline, kept untyped here to
47
+ # avoid a circular import between this module and pipeline.py
48
+ # (which owns the queue and passes itself in).
49
+ self.pipeline = pipeline
50
+ self.queue: deque[StorageRequest] = deque()
51
+ self.requests: dict[str, StorageRequest] = {}
52
+ self._lock = threading.Lock()
53
+
54
+ def submit(self, syscall: StorageSyscall) -> str:
55
+ req = StorageRequest(syscall=syscall)
56
+ with self._lock:
57
+ self.requests[req.id] = req
58
+ self.queue.append(req)
59
+ return req.id
60
+
61
+ def process_next(self) -> Optional[StorageRequest]:
62
+ with self._lock:
63
+ if not self.queue:
64
+ return None
65
+ req = self.queue.popleft()
66
+ try:
67
+ req.response = self.pipeline.execute(req.syscall)
68
+ req.status = StorageSyscallStatus.DONE if req.response.ok else StorageSyscallStatus.ERROR
69
+ except Exception as e:
70
+ req.response = StorageResponse(ok=False, error=str(e))
71
+ req.status = StorageSyscallStatus.ERROR
72
+ return req
73
+
74
+ def drain(self, max_iterations: int = 10_000) -> list[StorageRequest]:
75
+ finished = []
76
+ i = 0
77
+ while self.queue and i < max_iterations:
78
+ finished.append(self.process_next())
79
+ i += 1
80
+ return finished
81
+
82
+ def get_result(self, request_id: str) -> StorageRequest:
83
+ return self.requests[request_id]
84
+
85
+ def forget(self, request_id: str) -> None:
86
+ """Drop a finished request. Callers that wait synchronously (the SDK) call this so the
87
+ request table does not grow for the life of the process."""
88
+ self.requests.pop(request_id, None)
89
+
90
+ def pending_count(self) -> int:
91
+ return len(self.queue)