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,122 @@
1
+ """
2
+ Vector store for agent knowledge bases -- backed by chromadb
3
+ (pip install chromadb) when it's installed, one collection per agent so
4
+ different agents' knowledge bases stay isolated. Falls back to a small
5
+ in-memory store, scored via the same embedding/keyword-overlap
6
+ similarity helpers context_manager and memory already use, when
7
+ chromadb isn't available. Functionally equivalent at the scale this
8
+ project targets -- the real difference is chromadb's store is disk-
9
+ backed (survives a restart), the fallback isn't.
10
+ """
11
+ import logging
12
+ from typing import Optional
13
+
14
+ from pal.context_manager.utils.relevance import embedding_similarity
15
+ from pal.memory.utils.keyword_overlap import keyword_overlap
16
+
17
+ log = logging.getLogger(__name__)
18
+
19
+ try:
20
+ import chromadb
21
+ _CHROMADB_AVAILABLE = True
22
+ except ImportError:
23
+ _CHROMADB_AVAILABLE = False
24
+
25
+
26
+ class _NaiveVectorStore:
27
+ """In-process fallback: one dict of doc_id -> (text, metadata) per
28
+ agent, scored at query time rather than indexed ahead of time."""
29
+
30
+ def __init__(self):
31
+ self._collections: dict[str, dict[str, tuple[str, dict]]] = {}
32
+
33
+ def _collection(self, agent_id: str) -> dict:
34
+ return self._collections.setdefault(agent_id, {})
35
+
36
+ def upsert(self, agent_id: str, doc_id: str, text: str, metadata: dict) -> None:
37
+ self._collection(agent_id)[doc_id] = (text, metadata)
38
+
39
+ def delete(self, agent_id: str, doc_id: str) -> None:
40
+ self._collection(agent_id).pop(doc_id, None)
41
+
42
+ def query(self, agent_id: str, query_text: str, top_k: int) -> list[dict]:
43
+ collection = self._collection(agent_id)
44
+ if not collection:
45
+ return []
46
+ doc_ids = list(collection.keys())
47
+ texts = [collection[d][0] for d in doc_ids]
48
+
49
+ sims = embedding_similarity(query_text, texts)
50
+ if all(s == 0.0 for s in sims):
51
+ sims = [keyword_overlap(query_text, t) for t in texts]
52
+
53
+ scored = sorted(zip(doc_ids, sims), key=lambda x: x[1], reverse=True)
54
+ return [
55
+ {"id": doc_id, "text": collection[doc_id][0], "metadata": collection[doc_id][1], "score": score}
56
+ for doc_id, score in scored[:top_k]
57
+ ]
58
+
59
+
60
+ class VectorStoreBackend:
61
+ def __init__(self, persist_directory: Optional[str] = None):
62
+ self.backend = "chromadb" if _CHROMADB_AVAILABLE else "naive_fallback"
63
+ if _CHROMADB_AVAILABLE:
64
+ self._client = (
65
+ chromadb.PersistentClient(path=persist_directory)
66
+ if persist_directory
67
+ else chromadb.EphemeralClient()
68
+ )
69
+ self._collections = {}
70
+ else:
71
+ self._naive = _NaiveVectorStore()
72
+
73
+ def _chroma_collection(self, agent_id: str):
74
+ if agent_id not in self._collections:
75
+ # chromadb collection names have length/charset
76
+ # restrictions -- prefix and truncate to stay valid while
77
+ # keeping per-agent namespacing.
78
+ safe_name = f"agent_{agent_id}".replace(" ", "_")[:63]
79
+ self._collections[agent_id] = self._client.get_or_create_collection(safe_name)
80
+ return self._collections[agent_id]
81
+
82
+ def delete_agent(self, agent_id: str) -> None:
83
+ """Drop an agent's whole collection."""
84
+ if self.backend == "chromadb":
85
+ self._collections.pop(agent_id, None)
86
+ try:
87
+ self._client.delete_collection(f"agent_{agent_id}".replace(" ", "_")[:63])
88
+ except Exception: # noqa: BLE001 (collection was never created)
89
+ log.debug("no collection to delete for %s", agent_id)
90
+ else:
91
+ self._naive._collections.pop(agent_id, None)
92
+
93
+ def upsert(self, agent_id: str, doc_id: str, text: str, metadata: dict = None) -> None:
94
+ metadata = metadata or {}
95
+ if self.backend == "chromadb":
96
+ self._chroma_collection(agent_id).upsert(
97
+ ids=[doc_id], documents=[text], metadatas=[metadata] if metadata else None
98
+ )
99
+ else:
100
+ self._naive.upsert(agent_id, doc_id, text, metadata)
101
+
102
+ def delete(self, agent_id: str, doc_id: str) -> None:
103
+ if self.backend == "chromadb":
104
+ self._chroma_collection(agent_id).delete(ids=[doc_id])
105
+ else:
106
+ self._naive.delete(agent_id, doc_id)
107
+
108
+ def query(self, agent_id: str, query_text: str, top_k: int = 3) -> list[dict]:
109
+ if self.backend == "chromadb":
110
+ collection = self._chroma_collection(agent_id)
111
+ if collection.count() == 0:
112
+ return []
113
+ result = collection.query(query_texts=[query_text], n_results=min(top_k, collection.count()))
114
+ ids = result.get("ids", [[]])[0]
115
+ docs = result.get("documents", [[]])[0]
116
+ metas = result.get("metadatas", [[]])[0] or [{}] * len(ids)
117
+ dists = result.get("distances", [[]])[0] or [None] * len(ids)
118
+ return [
119
+ {"id": doc_id, "text": text, "metadata": meta, "score": (1 - dist) if dist is not None else None}
120
+ for doc_id, text, meta, dist in zip(ids, docs, metas, dists)
121
+ ]
122
+ return self._naive.query(agent_id, query_text, top_k)
@@ -0,0 +1,153 @@
1
+ """
2
+ Storage Manager (AIOS section 3.6): persistent storage for agents --
3
+ files/knowledge bases they depend on, and durable storage for the
4
+ memory manager's RAM-to-disk swaps -- backed by local files
5
+ (LocalFileBackend) and a vector database (VectorStoreBackend, chromadb
6
+ when installed).
7
+
8
+ Two entry points into the same underlying storage, matching the two
9
+ callers section 3.6 describes:
10
+
11
+ - **Direct calls** (read/write/delete/list_keys/vector_upsert/
12
+ vector_query) -- what the memory manager uses when it swaps a note to
13
+ disk, scoped by the agent_id the memory manager passes in.
14
+ - **execute(StorageSyscall)**, reachable either directly or via the
15
+ queue (submit/tick/drain) -- what an agent's own SDK calls
16
+ (utils/sdk.py) go through: "the agent calls the storage API in the
17
+ SDK, which is further converted into storage-related system calls
18
+ and put into the storage queue... the storage manager then processes
19
+ the requests in the queue."
20
+
21
+ Both paths converge on execute()/the same backends -- there's exactly
22
+ one storage manager per process, not a separate implementation per
23
+ caller.
24
+
25
+ When an AccessManagerPipeline (AIOS section 3.8) is supplied, execute()
26
+ authorizes every request before performing it: a cross-agent request
27
+ (syscall.requesting_agent_id differs from syscall.agent_id, the
28
+ resource owner) is checked against the privilege-group hashmap, and any
29
+ destructive operation -- DELETE, or a WRITE that overwrites an existing
30
+ key -- goes through user intervention regardless of who's making the
31
+ call. Without an access_manager, every request is permitted (the
32
+ pre-3.8 behavior), matching how the direct read/write/etc. calls above
33
+ still bypass access control entirely -- those are for the memory
34
+ manager's own swaps, which are not the cross-agent case 3.8 regulates.
35
+ """
36
+ from pathlib import Path
37
+ from typing import Any, Optional
38
+
39
+ from pal.access_manager.components.base import OperationType
40
+ from pal.access_manager.pipeline import AccessManagerPipeline
41
+ from pal.paths import data_dir
42
+ from pal.storage_manager.components.base import StorageOp, StorageResponse, StorageSyscall
43
+ from pal.storage_manager.components.local_file_backend import LocalFileBackend
44
+ from pal.storage_manager.components.storage_queue import StorageSyscallQueue
45
+ from pal.storage_manager.components.vector_store_backend import VectorStoreBackend
46
+
47
+
48
+ class StorageManagerPipeline:
49
+ def __init__(
50
+ self,
51
+ root: Optional[str] = None,
52
+ vector_persist_directory: Optional[str] = None,
53
+ access_manager: Optional[AccessManagerPipeline] = None,
54
+ ):
55
+ root_path = Path(root) if root else data_dir("storage")
56
+ self.files = LocalFileBackend(root_path / "files")
57
+ self.vectors = VectorStoreBackend(persist_directory=vector_persist_directory)
58
+ self.queue = StorageSyscallQueue(self)
59
+ self.access_manager = access_manager
60
+
61
+ # -- direct calls (memory manager swaps, or any in-process caller) --------
62
+
63
+ def read(self, agent_id: str, key: str) -> Optional[Any]:
64
+ return self.files.read(agent_id, key)
65
+
66
+ def write(self, agent_id: str, key: str, data: Any) -> None:
67
+ self.files.write(agent_id, key, data)
68
+
69
+ def delete(self, agent_id: str, key: str) -> None:
70
+ self.files.delete(agent_id, key)
71
+
72
+ def exists(self, agent_id: str, key: str) -> bool:
73
+ return self.files.exists(agent_id, key)
74
+
75
+ def list_keys(self, agent_id: str, prefix: str = "") -> list[str]:
76
+ return self.files.list_keys(agent_id, prefix=prefix)
77
+
78
+ def vector_upsert(self, agent_id: str, doc_id: str, text: str, metadata: dict = None) -> None:
79
+ self.vectors.upsert(agent_id, doc_id, text, metadata or {})
80
+
81
+ def vector_query(self, agent_id: str, query_text: str, top_k: int = 3) -> list[dict]:
82
+ return self.vectors.query(agent_id, query_text, top_k=top_k)
83
+
84
+ # -- system-call entry point (agent SDK calls, direct or via queue) -------
85
+
86
+ def _operation_type(self, syscall: StorageSyscall) -> OperationType:
87
+ if syscall.op == StorageOp.DELETE:
88
+ return OperationType.DELETE
89
+ if syscall.op in (StorageOp.READ, StorageOp.LIST, StorageOp.VECTOR_QUERY):
90
+ return OperationType.READ
91
+ if syscall.op == StorageOp.WRITE:
92
+ # Overwriting an existing key destroys what was there --
93
+ # treat it as destructive (user-intervention-gated) the same
94
+ # as an explicit delete; a write to a fresh key is not.
95
+ return OperationType.OVERWRITE if self.exists(syscall.agent_id, syscall.key) else OperationType.WRITE
96
+ return OperationType.WRITE # VECTOR_UPSERT
97
+
98
+ def execute(self, syscall: StorageSyscall) -> StorageResponse:
99
+ if self.access_manager is not None:
100
+ requesting = syscall.requesting_agent_id or syscall.agent_id
101
+ decision = self.access_manager.authorize(
102
+ requesting_agent_id=requesting,
103
+ target_agent_id=syscall.agent_id,
104
+ operation=self._operation_type(syscall),
105
+ resource=syscall.key or syscall.query_text or "",
106
+ )
107
+ if not decision.allowed:
108
+ return StorageResponse(ok=False, error=f"access denied: {decision.reason}")
109
+
110
+ try:
111
+ if syscall.op == StorageOp.READ:
112
+ return StorageResponse(ok=True, data=self.read(syscall.agent_id, syscall.key))
113
+ if syscall.op == StorageOp.WRITE:
114
+ self.write(syscall.agent_id, syscall.key, syscall.data)
115
+ return StorageResponse(ok=True)
116
+ if syscall.op == StorageOp.DELETE:
117
+ self.delete(syscall.agent_id, syscall.key)
118
+ return StorageResponse(ok=True)
119
+ if syscall.op == StorageOp.LIST:
120
+ return StorageResponse(ok=True, data=self.list_keys(syscall.agent_id, prefix=syscall.key or ""))
121
+ if syscall.op == StorageOp.VECTOR_UPSERT:
122
+ self.vector_upsert(syscall.agent_id, syscall.key, syscall.data, syscall.metadata)
123
+ return StorageResponse(ok=True)
124
+ if syscall.op == StorageOp.VECTOR_QUERY:
125
+ return StorageResponse(
126
+ ok=True,
127
+ data=self.vector_query(syscall.agent_id, syscall.query_text, syscall.top_k),
128
+ )
129
+ return StorageResponse(ok=False, error=f"unknown storage op: {syscall.op}")
130
+ except Exception as e:
131
+ return StorageResponse(ok=False, error=str(e))
132
+
133
+ # -- queue-driven path -- "put into the storage queue by the scheduler" --
134
+
135
+ def submit(self, syscall: StorageSyscall) -> str:
136
+ return self.queue.submit(syscall)
137
+
138
+ def tick(self):
139
+ return self.queue.process_next()
140
+
141
+ def drain(self, max_iterations: int = 10_000):
142
+ return self.queue.drain(max_iterations=max_iterations)
143
+
144
+ def get_result(self, request_id: str):
145
+ return self.queue.get_result(request_id)
146
+
147
+ def forget(self, request_id: str) -> None:
148
+ self.queue.forget(request_id)
149
+
150
+ def purge_agent(self, agent_id: str) -> None:
151
+ """Delete every file and vector stored for an agent (used when a chat session ends)."""
152
+ self.files.purge(agent_id)
153
+ self.vectors.delete_agent(agent_id)
@@ -0,0 +1,4 @@
1
+ from pal.storage_manager.utils.memory_adapter import AgentScopedMemoryStorage
2
+ from pal.storage_manager.utils.sdk import StorageSDK
3
+
4
+ __all__ = ["AgentScopedMemoryStorage", "StorageSDK"]
@@ -0,0 +1,60 @@
1
+ """
2
+ Adapter presenting the same narrow interface
3
+ pal.memory.components.storage_manager.StorageManager exposes
4
+ (write/read/delete/exists/list_ids/read_all, all keyed by MemoryNote.id)
5
+ but backed by the general-purpose StorageManagerPipeline, scoped to one
6
+ agent_id.
7
+
8
+ This is the actual integration point for "when the agent's memory
9
+ usage exceeds the allocated limit, the memory manager calls the storage
10
+ manager to swap the data into the disk... the storage manager reads and
11
+ writes data based on the agent ID passed from the memory manager":
12
+ MemoryManager doesn't know or care which storage implementation sits
13
+ behind this interface, only that it has one -- so it works identically
14
+ whether it's given this adapter (backed by the shared, agent-scoped
15
+ StorageManagerPipeline) or the standalone pal.memory.components.
16
+ storage_manager.StorageManager (a private, unscoped JSON-per-note
17
+ store, still the default when no agent_id/storage_manager is supplied).
18
+ """
19
+ from typing import Optional
20
+
21
+ from pal.memory.components.note import MemoryNote
22
+ from pal.storage_manager.pipeline import StorageManagerPipeline
23
+
24
+ _MEMORY_PREFIX = "memory/"
25
+
26
+
27
+ class AgentScopedMemoryStorage:
28
+ def __init__(self, storage_manager: StorageManagerPipeline, agent_id: str):
29
+ self.storage_manager = storage_manager
30
+ self.agent_id = agent_id
31
+
32
+ def _key(self, note_id: str) -> str:
33
+ return f"{_MEMORY_PREFIX}{note_id}.json"
34
+
35
+ def write(self, note: MemoryNote) -> None:
36
+ self.storage_manager.write(self.agent_id, self._key(note.id), note.to_dict())
37
+
38
+ def read(self, note_id: str) -> Optional[MemoryNote]:
39
+ data = self.storage_manager.read(self.agent_id, self._key(note_id))
40
+ if data is None:
41
+ return None
42
+ return MemoryNote.from_dict(data)
43
+
44
+ def delete(self, note_id: str) -> None:
45
+ self.storage_manager.delete(self.agent_id, self._key(note_id))
46
+
47
+ def exists(self, note_id: str) -> bool:
48
+ return self.storage_manager.exists(self.agent_id, self._key(note_id))
49
+
50
+ def list_ids(self) -> list[str]:
51
+ keys = self.storage_manager.list_keys(self.agent_id, prefix=_MEMORY_PREFIX)
52
+ return [k[len(_MEMORY_PREFIX):-len(".json")] for k in keys if k.endswith(".json")]
53
+
54
+ def read_all(self) -> list[MemoryNote]:
55
+ notes = []
56
+ for note_id in self.list_ids():
57
+ note = self.read(note_id)
58
+ if note is not None:
59
+ notes.append(note)
60
+ return notes
@@ -0,0 +1,93 @@
1
+ """
2
+ Storage SDK: what an agent calls to read/write its own persistent data
3
+ during runtime -- distinct from the memory manager's own direct calls
4
+ into the same storage manager (see utils/memory_adapter.py).
5
+
6
+ "The agent calls the storage API in the SDK, which is further
7
+ converted into storage-related system calls and put into the storage
8
+ queue by the scheduler. The storage manager then processes the
9
+ requests in the queue." read()/write()/etc. here build a StorageSyscall
10
+ and submit it to the shared StorageManagerPipeline's queue, then drain
11
+ that queue until this specific request completes -- so the call still
12
+ looks synchronous to the agent even though it went through the same
13
+ centralized queue every other storage request goes through, the same
14
+ way Pipeline._dispatch_chat drains the LLM syscall scheduler for its
15
+ own request in orchestrator/components/syscall_scheduler.py.
16
+
17
+ read_from()/write_from()/delete_from() are the cross-agent path AIOS
18
+ 3.8's Access Manager regulates: they set requesting_agent_id to this
19
+ SDK's own agent_id while targeting a DIFFERENT agent's resource, so if
20
+ the StorageManagerPipeline has an access_manager wired in, that request
21
+ gets checked against the privilege-group hashmap (and, for delete/
22
+ overwrite, gated behind user intervention) before it's allowed to
23
+ proceed. Plain read()/write()/delete() never trigger a cross-agent
24
+ check -- they always act on this SDK's own agent_id as both requester
25
+ and target.
26
+ """
27
+ from typing import Any, Optional
28
+
29
+ from pal.storage_manager.components.base import StorageOp, StorageSyscall
30
+ from pal.storage_manager.pipeline import StorageManagerPipeline
31
+
32
+
33
+ class StorageSDK:
34
+ def __init__(self, agent_id: str, storage_manager: StorageManagerPipeline):
35
+ self.agent_id = agent_id
36
+ self.storage_manager = storage_manager
37
+
38
+ def _submit_and_wait(self, syscall: StorageSyscall):
39
+ request_id = self.storage_manager.submit(syscall)
40
+ try:
41
+ req = self.storage_manager.get_result(request_id)
42
+ while req.status.value == "queued":
43
+ self.storage_manager.tick()
44
+ req = self.storage_manager.get_result(request_id)
45
+ if req.status.value == "error":
46
+ error = req.response.error if req.response else "unknown error"
47
+ raise RuntimeError(f"storage syscall failed: {error}")
48
+ return req.response
49
+ finally:
50
+ self.storage_manager.forget(request_id)
51
+
52
+ def read(self, key: str) -> Optional[Any]:
53
+ resp = self._submit_and_wait(StorageSyscall(agent_id=self.agent_id, op=StorageOp.READ, key=key))
54
+ return resp.data
55
+
56
+ def write(self, key: str, data: Any) -> None:
57
+ self._submit_and_wait(StorageSyscall(agent_id=self.agent_id, op=StorageOp.WRITE, key=key, data=data))
58
+
59
+ def delete(self, key: str) -> None:
60
+ self._submit_and_wait(StorageSyscall(agent_id=self.agent_id, op=StorageOp.DELETE, key=key))
61
+
62
+ def list_keys(self, prefix: str = "") -> list[str]:
63
+ resp = self._submit_and_wait(StorageSyscall(agent_id=self.agent_id, op=StorageOp.LIST, key=prefix))
64
+ return resp.data
65
+
66
+ def vector_upsert(self, doc_id: str, text: str, metadata: dict = None) -> None:
67
+ self._submit_and_wait(StorageSyscall(
68
+ agent_id=self.agent_id, op=StorageOp.VECTOR_UPSERT, key=doc_id, data=text, metadata=metadata or {},
69
+ ))
70
+
71
+ def vector_query(self, query_text: str, top_k: int = 3) -> list[dict]:
72
+ resp = self._submit_and_wait(StorageSyscall(
73
+ agent_id=self.agent_id, op=StorageOp.VECTOR_QUERY, query_text=query_text, top_k=top_k,
74
+ ))
75
+ return resp.data
76
+
77
+ # -- cross-agent access, subject to the Access Manager (AIOS 3.8) ---------
78
+
79
+ def read_from(self, target_agent_id: str, key: str) -> Optional[Any]:
80
+ resp = self._submit_and_wait(StorageSyscall(
81
+ agent_id=target_agent_id, requesting_agent_id=self.agent_id, op=StorageOp.READ, key=key,
82
+ ))
83
+ return resp.data
84
+
85
+ def write_to(self, target_agent_id: str, key: str, data: Any) -> None:
86
+ self._submit_and_wait(StorageSyscall(
87
+ agent_id=target_agent_id, requesting_agent_id=self.agent_id, op=StorageOp.WRITE, key=key, data=data,
88
+ ))
89
+
90
+ def delete_from(self, target_agent_id: str, key: str) -> None:
91
+ self._submit_and_wait(StorageSyscall(
92
+ agent_id=target_agent_id, requesting_agent_id=self.agent_id, op=StorageOp.DELETE, key=key,
93
+ ))
pal/studio/__init__.py ADDED
File without changes
pal/studio/builder.py ADDED
@@ -0,0 +1,166 @@
1
+ """One build: the plan's steps, one after another, each a request to the model, each reply read for files, checked, and saved under the policy.
2
+
3
+ Everything that happens is reported as an event (`emit(kind, text, **data)`), so the page can show the work as it happens. Nothing the model wrote is
4
+ ever run: files are read, checked for mistakes, and written, if the policy allows.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Callable
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from pal.studio.catalog import ROLES
13
+ from pal.studio.checks import check_project
14
+ from pal.studio.policy import Policy
15
+ from pal.terminal.files import parse_files
16
+
17
+ FORMAT = ("Reply with the files you create or change. Write each file as a line `### FILE: path/name.ext` followed by its COMPLETE content in a code block. "
18
+ "No partial files, no '...' placeholders. After the files you may add one line starting with NOTE: if something needs saying.")
19
+ KIND_RULES = {
20
+ "web_app": "It is a web page: plain HTML, CSS and JavaScript that works when index.html is opened in a browser. No build step, no frameworks to install, no external URLs or CDNs. Start with index.html.",
21
+ "cli_tool": "It is a command-line program in Python 3 using only the standard library. Start with main.py.",
22
+ "script": "It is a Python 3 script using only the standard library. Start with main.py.",
23
+ "library": "It is a small Python 3 library using only the standard library, with a clear public API.",
24
+ "document": "It is a document: write it as Markdown files.",
25
+ "other": "Use plain files that need nothing to be installed.",
26
+ }
27
+ MAX_REPAIRS = 1
28
+
29
+
30
+ def default_name(plan: dict, step: dict) -> str:
31
+ if step["role"] == "docs":
32
+ return "README.md"
33
+ if step["role"] == "tester":
34
+ return "test_main.py"
35
+ return {"web_app": "index.html", "cli_tool": "main.py", "script": "main.py", "library": "main.py", "document": "README.md"}.get(plan["product"]["kind"], "main.txt")
36
+
37
+
38
+ def first_message(plan: dict, step: dict, with_references: bool) -> str:
39
+ team = ", ".join(ROLES[r].name for r in plan["roles"])
40
+ p = plan["product"]
41
+ return "\n".join([
42
+ f"We are building: {p['name']}. {p['summary']}",
43
+ KIND_RULES.get(p["kind"], KIND_RULES["other"]),
44
+ f"The team: {team}. Each step is done by one of them, and every file must work with the files already written (same names, same functions).",
45
+ "The reference files the person attached are available to you." if with_references else "",
46
+ FORMAT, "",
47
+ f"Step 1 ({ROLES[step['role']].name}): {step['task']}", ROLES[step["role"]].prompt])
48
+
49
+
50
+ def step_message(step: dict, i: int, n: int) -> str:
51
+ return (f"Step {i} of {n} ({ROLES[step['role']].name}): {step['task']}\n{ROLES[step['role']].prompt}\n"
52
+ "Keep file names and functions consistent with the files already written. " + FORMAT)
53
+
54
+
55
+ class Build:
56
+ def __init__(self, plan: dict, client, workspace: Path | None, policy: Policy, emit: Callable, cancelled: Callable[[], bool] = lambda: False,
57
+ references: list[dict] | None = None):
58
+ self.plan, self.client, self.workspace, self.policy, self.emit, self.cancelled = plan, client, workspace, policy, emit, cancelled
59
+ self.references = references if plan["permissions"].get("read_references") else []
60
+ self.files: dict[str, str] = {} # everything the build wrote (or would have written, in a dry run)
61
+ self.totals: dict[str, Any] = {"calls": 0, "prompt_tokens": 0, "completion_tokens": 0, "main_tokens": 0, "helper_tokens": 0, "seconds": 0.0,
62
+ "files": 0, "blocked": 0, "problems_left": 0, "steps_done": 0, "steps": len(plan["steps"]), "stopped": ""}
63
+
64
+ # -- one request to the model ----------------------------------------------------------------------------------------
65
+ def _ask(self, message: str, label: str, refs: list[dict] | None = None) -> str:
66
+ reply = self.client.ask(message, refs)
67
+ t = self.totals
68
+ t["calls"] += max(reply.calls, 1)
69
+ t["prompt_tokens"] += reply.prompt_tokens
70
+ t["completion_tokens"] += reply.completion_tokens
71
+ t["main_tokens"] += reply.prompt_tokens + reply.completion_tokens
72
+ t["helper_tokens"] += reply.helper_tokens
73
+ t["seconds"] = round(t["seconds"] + reply.seconds, 3)
74
+ self.emit("model_call", f"{label}: the model read {reply.prompt_tokens:,} tokens and wrote {reply.completion_tokens:,} in {reply.seconds:.1f}s"
75
+ + (f" (Pal's helper used {reply.helper_tokens:,} more)" if reply.helper_tokens else ""),
76
+ prompt_tokens=reply.prompt_tokens, completion_tokens=reply.completion_tokens, helper_tokens=reply.helper_tokens, seconds=round(reply.seconds, 2))
77
+ return reply.text
78
+
79
+ # -- reading and saving the files in a reply -----------------------------------------------------------------------
80
+ def _apply(self, reply: str, step: dict) -> set[str]:
81
+ parsed, unnamed = parse_files(reply, default_name(self.plan, step))
82
+ changed: set[str] = set()
83
+ if unnamed:
84
+ self.emit("warning", f"{unnamed} code block(s) had no file name and were skipped")
85
+ for pf in parsed:
86
+ d = self.policy.check_write(pf.path, pf.content)
87
+ if d.allowed:
88
+ existed = d.path.lower() in {p.lower() for p in self.files}
89
+ self.files[d.path] = pf.content
90
+ if self.workspace is not None:
91
+ target = self.workspace / d.path
92
+ target.parent.mkdir(parents=True, exist_ok=True)
93
+ target.write_text(pf.content, encoding="utf-8", newline="\n")
94
+ changed.add(d.path)
95
+ self.totals["files"] = len(self.files)
96
+ self.emit("file", f"{'changed' if existed else 'wrote'} {d.path} ({len(pf.content.encode('utf-8')):,} bytes)", path=d.path, action="changed" if existed else "created")
97
+ elif d.path and "dry run" in d.reason:
98
+ self.files[d.path] = pf.content
99
+ changed.add(d.path)
100
+ self.totals["files"] = len(self.files)
101
+ self.emit("file", f"would write {d.path} (dry run: not saved)", path=d.path, action="dry")
102
+ else:
103
+ self.totals["blocked"] += 1
104
+ self.emit("blocked", f"did not save {d.path or 'a file'}: {d.reason}", path=d.path, reason=d.reason)
105
+ return changed
106
+
107
+ @staticmethod
108
+ def _describe(problems: dict[str, list[str]]) -> str:
109
+ return "\n".join(f"- {path}: {'; '.join(p)}" for path, p in problems.items())
110
+
111
+ # -- the build ---------------------------------------------------------------------------------------------------------
112
+ def run(self) -> dict:
113
+ steps, n = self.plan["steps"], len(self.plan["steps"])
114
+ for i, step in enumerate(steps, 1):
115
+ if self.cancelled():
116
+ self.totals["stopped"] = "cancelled"
117
+ self.emit("cancelled", "stopped by you")
118
+ return self.totals
119
+ role = ROLES[step["role"]]
120
+ self.emit("step_start", f"Step {i} of {n}: {role.name}: {step['task']}", step=i, total=n, role=step["role"])
121
+ first = i == 1
122
+ msg = first_message(self.plan, step, bool(self.references)) if first else step_message(step, i, n)
123
+ try:
124
+ reply = self._ask(msg, f"step {i}", self.references if first else None)
125
+ except Exception as e: # noqa: BLE001 (the model failed: say so, and stop cleanly)
126
+ self.totals["stopped"] = "error"
127
+ self.emit("error", f"the model failed on step {i}: {type(e).__name__}: {str(e)[:200]}")
128
+ return self.totals
129
+ changed = self._apply(reply, step)
130
+ problems = check_project(self.files, only=changed, links=False)
131
+ repairs = 0
132
+ while problems and repairs < MAX_REPAIRS and not self.cancelled():
133
+ self.emit("check", "found problems: " + "; ".join(f"{p}: {', '.join(v)}" for p, v in list(problems.items())[:3]), problems=problems)
134
+ repairs += 1
135
+ try:
136
+ fix = self._ask("These problems were found in the files:\n" + self._describe(problems) + "\nReply with the corrected COMPLETE files only. " + FORMAT, f"step {i} fix")
137
+ except Exception as e: # noqa: BLE001
138
+ self.emit("error", f"the fix request failed: {type(e).__name__}")
139
+ break
140
+ changed |= self._apply(fix, step)
141
+ problems = check_project(self.files, only=changed, links=False)
142
+ if problems:
143
+ self.emit("check", "problems still there after one fix: " + "; ".join(f"{p}: {', '.join(v)}" for p, v in list(problems.items())[:3]), problems=problems)
144
+ else:
145
+ self.emit("check", "no problems found in the files")
146
+ self.totals["steps_done"] = i
147
+ self.emit("step_done", f"Step {i} done: {len(self.files)} file(s) so far", step=i)
148
+ self._final_check(steps[-1] if steps else None)
149
+ return self.totals
150
+
151
+ def _final_check(self, step: dict | None) -> None:
152
+ """When every step is done: do the files fit together (does every page link to files that exist)? One fix is allowed, then the result is recorded."""
153
+ if step is None or self.cancelled() or self.totals["stopped"]:
154
+ return
155
+ problems = check_project(self.files)
156
+ if problems:
157
+ self.emit("check", "project check, found: " + "; ".join(f"{p}: {', '.join(v)}" for p, v in list(problems.items())[:3]), problems=problems)
158
+ try:
159
+ fix = self._ask("These problems were found in the finished project:\n" + self._describe(problems) + "\nReply with the missing or corrected COMPLETE files only. " + FORMAT, "final fix")
160
+ self._apply(fix, step)
161
+ except Exception as e: # noqa: BLE001
162
+ self.emit("error", f"the final fix request failed: {type(e).__name__}")
163
+ problems = check_project(self.files)
164
+ self.totals["problems_left"] = sum(len(v) for v in problems.values())
165
+ self.emit("check", "project check: the files fit together" if not problems else f"project check: {self.totals['problems_left']} problem(s) left: "
166
+ + "; ".join(f"{p}: {', '.join(v)}" for p, v in list(problems.items())[:3]), problems=problems)