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/__init__.py ADDED
@@ -0,0 +1,29 @@
1
+ """Pal: a model-agnostic chat layer that cuts tokens and latency.
2
+
3
+ Importing the package is cheap on purpose. The older `Pipeline` (which pulls in the compression
4
+ stack: nltk, spacy, ...) is loaded only if you ask for it.
5
+ """
6
+ from importlib import import_module
7
+ from importlib.metadata import PackageNotFoundError, version
8
+
9
+
10
+ def _version() -> str:
11
+ for name in ("pal-code", "pal-ai", "pal-aios"): # the current name first, then earlier ones
12
+ try:
13
+ return version(name)
14
+ except PackageNotFoundError:
15
+ continue
16
+ return "0.1.0" # a source checkout that is not installed
17
+
18
+
19
+ __version__ = _version()
20
+
21
+ __all__ = ["Pipeline", "WrapperConfig", "__version__"]
22
+
23
+ _LAZY = {"Pipeline": "pal.pipeline", "WrapperConfig": "pal.llm_wrapper.components.base"}
24
+
25
+
26
+ def __getattr__(name: str):
27
+ if name in _LAZY:
28
+ return getattr(import_module(_LAZY[name]), name)
29
+ raise AttributeError(f"module 'pal' has no attribute {name!r}")
pal/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from pal.terminal.cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,16 @@
1
+ from pal.access_manager.components.access_control import AccessController
2
+ from pal.access_manager.components.base import DESTRUCTIVE_OPS, AccessDecision, AccessRequest, OperationType
3
+ from pal.access_manager.components.intervention import UserInterventionGate
4
+ from pal.access_manager.components.privilege_registry import PrivilegeRegistry
5
+ from pal.access_manager.pipeline import AccessManagerPipeline
6
+
7
+ __all__ = [
8
+ "AccessManagerPipeline",
9
+ "AccessDecision",
10
+ "AccessRequest",
11
+ "DESTRUCTIVE_OPS",
12
+ "OperationType",
13
+ "AccessController",
14
+ "UserInterventionGate",
15
+ "PrivilegeRegistry",
16
+ ]
@@ -0,0 +1,14 @@
1
+ from pal.access_manager.components.access_control import AccessController
2
+ from pal.access_manager.components.base import DESTRUCTIVE_OPS, AccessDecision, AccessRequest, OperationType
3
+ from pal.access_manager.components.intervention import UserInterventionGate
4
+ from pal.access_manager.components.privilege_registry import PrivilegeRegistry
5
+
6
+ __all__ = [
7
+ "AccessController",
8
+ "AccessDecision",
9
+ "AccessRequest",
10
+ "DESTRUCTIVE_OPS",
11
+ "OperationType",
12
+ "UserInterventionGate",
13
+ "PrivilegeRegistry",
14
+ ]
@@ -0,0 +1,41 @@
1
+ """
2
+ Privilege-based access control (AIOS section 3.8): validates a
3
+ cross-agent access request against the privilege-group hashmap BEFORE
4
+ execution -- an agent may only access another agent's resources if
5
+ both are assigned to the same shared privilege domain. An agent acting
6
+ on its OWN resources is always permitted regardless of privilege group
7
+ (3.8 is specifically about cross-agent data access; a resource owner
8
+ always has access to what it owns).
9
+ """
10
+ from pal.access_manager.components.base import DESTRUCTIVE_OPS, AccessDecision, AccessRequest
11
+ from pal.access_manager.components.privilege_registry import PrivilegeRegistry
12
+
13
+
14
+ class AccessController:
15
+ def __init__(self, registry: PrivilegeRegistry):
16
+ self.registry = registry
17
+
18
+ def check(self, request: AccessRequest) -> AccessDecision:
19
+ requires_confirmation = request.operation in DESTRUCTIVE_OPS
20
+
21
+ if request.requesting_agent_id == request.target_agent_id:
22
+ return AccessDecision(allowed=True, requires_confirmation=requires_confirmation)
23
+
24
+ if self.registry.same_group(request.requesting_agent_id, request.target_agent_id):
25
+ return AccessDecision(allowed=True, requires_confirmation=requires_confirmation)
26
+
27
+ group_a = self.registry.group_of(request.requesting_agent_id)
28
+ group_b = self.registry.group_of(request.target_agent_id)
29
+ if group_a is None or group_b is None:
30
+ unassigned = [
31
+ aid for aid, g in ((request.requesting_agent_id, group_a), (request.target_agent_id, group_b))
32
+ if g is None
33
+ ]
34
+ reason = f"agent(s) not assigned a privilege group: {unassigned}"
35
+ else:
36
+ reason = (
37
+ f"'{request.requesting_agent_id}' (group '{group_a}') is not in the same "
38
+ f"privilege group as '{request.target_agent_id}' (group '{group_b}')"
39
+ )
40
+
41
+ return AccessDecision(allowed=False, requires_confirmation=requires_confirmation, reason=reason)
@@ -0,0 +1,39 @@
1
+ """
2
+ Access request/decision shapes for the Access Manager (AIOS section
3
+ 3.8). An AccessRequest always names both the requesting agent and the
4
+ agent whose resource is being touched -- when they're the same agent,
5
+ there's nothing cross-agent to regulate, only user-intervention (for
6
+ destructive ops) applies.
7
+ """
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from typing import Optional
11
+
12
+
13
+ class OperationType(str, Enum):
14
+ READ = "read"
15
+ WRITE = "write"
16
+ DELETE = "delete"
17
+ OVERWRITE = "overwrite"
18
+ PRIVILEGE_MODIFY = "privilege_modify"
19
+
20
+
21
+ # "irreversible operations (deletion, overwrite, privilege modification)"
22
+ DESTRUCTIVE_OPS = {OperationType.DELETE, OperationType.OVERWRITE, OperationType.PRIVILEGE_MODIFY}
23
+
24
+
25
+ @dataclass
26
+ class AccessRequest:
27
+ requesting_agent_id: str
28
+ target_agent_id: str # whose resource is being accessed -- equal to
29
+ # requesting_agent_id for an agent acting on
30
+ # its own data
31
+ operation: OperationType
32
+ resource: str = "" # a key, tool name, or other resource descriptor -- for logging/audit only
33
+
34
+
35
+ @dataclass
36
+ class AccessDecision:
37
+ allowed: bool
38
+ requires_confirmation: bool
39
+ reason: Optional[str] = None
@@ -0,0 +1,31 @@
1
+ """
2
+ User intervention interface (AIOS section 3.8): irreversible operations
3
+ (deletion, overwrite, privilege modification) require explicit user
4
+ confirmation before proceeding, even when privilege-based access
5
+ control alone would permit them.
6
+
7
+ The actual UI a real deployment asks the user through (a CLI prompt, a
8
+ web confirmation dialog, ...) is out of scope for this project -- what
9
+ this implements is the gate itself: a pluggable confirmation_handler
10
+ callback the surrounding application registers. The default handler
11
+ denies everything, safely, rather than silently approving destructive
12
+ operations when no real confirmation UI has been wired up.
13
+ """
14
+ from typing import Callable, Optional
15
+
16
+ from pal.access_manager.components.base import AccessRequest
17
+
18
+
19
+ def _default_deny_handler(request: AccessRequest) -> bool:
20
+ return False
21
+
22
+
23
+ class UserInterventionGate:
24
+ def __init__(self, confirmation_handler: Optional[Callable[[AccessRequest], bool]] = None):
25
+ self.confirmation_handler = confirmation_handler or _default_deny_handler
26
+
27
+ def set_handler(self, handler: Callable[[AccessRequest], bool]) -> None:
28
+ self.confirmation_handler = handler
29
+
30
+ def confirm(self, request: AccessRequest) -> bool:
31
+ return bool(self.confirmation_handler(request))
@@ -0,0 +1,30 @@
1
+ """
2
+ Privilege-group hashmap (AIOS section 3.8): agent_id -> privilege
3
+ group, enforced by AccessController before any cross-agent access is
4
+ permitted. Each agent is assigned to exactly one group; agents sharing
5
+ a group are in the same "privilege domain" and may access each other's
6
+ resources.
7
+ """
8
+ from typing import Optional
9
+
10
+
11
+ class PrivilegeRegistry:
12
+ def __init__(self):
13
+ self._groups: dict[str, str] = {}
14
+
15
+ def assign(self, agent_id: str, group: str) -> None:
16
+ self._groups[agent_id] = group
17
+
18
+ def group_of(self, agent_id: str) -> Optional[str]:
19
+ return self._groups.get(agent_id)
20
+
21
+ def same_group(self, agent_a: str, agent_b: str) -> bool:
22
+ group_a = self.group_of(agent_a)
23
+ group_b = self.group_of(agent_b)
24
+ return group_a is not None and group_a == group_b
25
+
26
+ def revoke(self, agent_id: str) -> None:
27
+ self._groups.pop(agent_id, None)
28
+
29
+ def all_assignments(self) -> dict[str, str]:
30
+ return dict(self._groups)
@@ -0,0 +1,68 @@
1
+ """
2
+ Access Manager (AIOS section 3.8): privilege-based access control for
3
+ cross-agent resource access (components/privilege_registry.py,
4
+ components/access_control.py), plus a user-intervention gate for
5
+ irreversible operations (components/intervention.py).
6
+ """
7
+ from typing import Callable, Optional
8
+
9
+ from pal.access_manager.components.access_control import AccessController
10
+ from pal.access_manager.components.base import AccessDecision, AccessRequest, OperationType
11
+ from pal.access_manager.components.intervention import UserInterventionGate
12
+ from pal.access_manager.components.privilege_registry import PrivilegeRegistry
13
+
14
+
15
+ class AccessManagerPipeline:
16
+ def __init__(self, confirmation_handler: Optional[Callable[[AccessRequest], bool]] = None):
17
+ self.registry = PrivilegeRegistry()
18
+ self.controller = AccessController(self.registry)
19
+ self.intervention = UserInterventionGate(confirmation_handler)
20
+
21
+ def assign_privilege_group(self, agent_id: str, group: str) -> None:
22
+ self.registry.assign(agent_id, group)
23
+
24
+ def set_confirmation_handler(self, handler: Callable[[AccessRequest], bool]) -> None:
25
+ self.intervention.set_handler(handler)
26
+
27
+ def check_access(
28
+ self, requesting_agent_id: str, target_agent_id: str, operation: OperationType, resource: str = ""
29
+ ) -> AccessDecision:
30
+ """Privilege check only -- does not trigger user intervention,
31
+ even for a destructive operation. Use authorize() for the full
32
+ gate."""
33
+ request = AccessRequest(
34
+ requesting_agent_id=requesting_agent_id,
35
+ target_agent_id=target_agent_id,
36
+ operation=operation,
37
+ resource=resource,
38
+ )
39
+ return self.controller.check(request)
40
+
41
+ def authorize(
42
+ self, requesting_agent_id: str, target_agent_id: str, operation: OperationType, resource: str = ""
43
+ ) -> AccessDecision:
44
+ """Full authorization: privilege check first, then -- only if
45
+ that passed AND the operation is destructive -- user
46
+ confirmation. Returns one AccessDecision reflecting both
47
+ checks: a privilege-permitted destructive op the user declines
48
+ to confirm comes back allowed=False, not the privilege-only
49
+ result."""
50
+ request = AccessRequest(
51
+ requesting_agent_id=requesting_agent_id,
52
+ target_agent_id=target_agent_id,
53
+ operation=operation,
54
+ resource=resource,
55
+ )
56
+ decision = self.controller.check(request)
57
+
58
+ if not decision.allowed:
59
+ return decision
60
+
61
+ if decision.requires_confirmation and not self.intervention.confirm(request):
62
+ return AccessDecision(
63
+ allowed=False,
64
+ requires_confirmation=True,
65
+ reason="user declined confirmation for an irreversible operation",
66
+ )
67
+
68
+ return decision
@@ -0,0 +1,2 @@
1
+ """No shared utilities yet for access_manager -- reserved for future
2
+ helpers (e.g. loading privilege-group assignments from a config file)."""
@@ -0,0 +1,3 @@
1
+ from pal.agent_constraints.pipeline import AgentConstraintsPipeline, with_agent_constraints
2
+
3
+ __all__ = ["AgentConstraintsPipeline", "with_agent_constraints"]
@@ -0,0 +1,13 @@
1
+ Output short. No fluff. No intro. No outro.
2
+
3
+ Bad: "Sure! Happy to help. Issue likely caused by..."
4
+ Good: "Bug in auth middleware."
5
+
6
+ Give exactly what user ask. Yes/no question: answer "yes" or "no" only. No extra text.
7
+
8
+ Code: fewest lines possible. No comments unless asked.
9
+ No em dash in output.
10
+ No options unless asked. Give one answer.
11
+ Do not run code unless asked. If output already given, do not run.
12
+ Opinion only if asked or needed.
13
+ No question at end. Never ask "want me to do X or Y?"
@@ -0,0 +1,3 @@
1
+ from pal.agent_constraints.components.constraints import load_agent_constraints
2
+
3
+ __all__ = ["load_agent_constraints"]
@@ -0,0 +1,22 @@
1
+ """
2
+ Permanent behavior-constraint document, not a text-processing function.
3
+
4
+ The insight: trimming output AFTER the model generates it only saves
5
+ what gets displayed/logged -- the model already paid the generation
6
+ cost (and, for API backends, the billed cost) for every verbose token.
7
+ Instructing the model up front to be terse means fewer output tokens
8
+ get generated in the first place. That's a strictly better lever than
9
+ post-hoc compression, so this is wired in as a permanent addition to
10
+ every system prompt.
11
+ """
12
+ from pathlib import Path
13
+
14
+ _CONSTRAINTS_FILE = Path(__file__).parent / "AGENT_OUTPUT.md"
15
+
16
+
17
+ def load_agent_constraints() -> str:
18
+ """Returns the permanent output-behavior constraints as text, ready
19
+ to append to any system prompt."""
20
+ if not _CONSTRAINTS_FILE.exists():
21
+ return ""
22
+ return _CONSTRAINTS_FILE.read_text(encoding="utf-8").strip()
@@ -0,0 +1,22 @@
1
+ from pal.agent_constraints.components.constraints import load_agent_constraints
2
+
3
+
4
+ class AgentConstraintsPipeline:
5
+ def with_constraints(self, system_prompt: str) -> str:
6
+ """Merges the permanent output-behavior constraints into a given
7
+ system prompt. Idempotent-ish: if the constraints text is
8
+ already present (e.g. called twice on the same prompt), it
9
+ isn't duplicated."""
10
+ constraints = load_agent_constraints()
11
+ if not constraints:
12
+ return system_prompt
13
+ if constraints in system_prompt:
14
+ return system_prompt
15
+ return f"{system_prompt}\n\n{constraints}"
16
+
17
+
18
+ # Module-level convenience, mirroring the old with_agent_constraints()
19
+ # free function so callers that just need the one operation don't have
20
+ # to instantiate the pipeline.
21
+ def with_agent_constraints(system_prompt: str) -> str:
22
+ return AgentConstraintsPipeline().with_constraints(system_prompt)
@@ -0,0 +1,2 @@
1
+ """No shared utilities yet for agent_constraints -- reserved for future
2
+ constraint-set variants (e.g. per-task-type constraint documents)."""
pal/agents/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """The agent application layer. Only agents live here.
2
+
3
+ Everything an agent needs (spec type, base class, ReAct and Reflexion patterns, framework adapters) is in
4
+ `pal.sdk`. Add an agent by adding a NativeAgent subclass with a spec and listing it below.
5
+ """
6
+ from pal.agents.coding import CodingAgent
7
+ from pal.agents.docs import DocumentationAgent
8
+ from pal.sdk.agent import NativeAgent
9
+
10
+ AGENTS: dict[str, type[NativeAgent]] = {a.spec.id: a for a in (CodingAgent, DocumentationAgent) if a.spec}
11
+
12
+ __all__ = ["AGENTS", "CodingAgent", "DocumentationAgent"]
pal/agents/coding.py ADDED
@@ -0,0 +1,14 @@
1
+ from pal.sdk.agent import AgentSpec, NativeAgent
2
+
3
+
4
+ class CodingAgent(NativeAgent):
5
+ """Explains, reviews and writes code from an attached source zip (code graph + read-only source tool)."""
6
+
7
+ spec = AgentSpec(
8
+ id="coding", name="Coding agent",
9
+ description="Explains, reviews and writes code. Attach a source zip and ask about it.",
10
+ role="Role: coding agent. Answer only from the supplied code, graph excerpts and files; say so if something "
11
+ "is not there. Give minimal, correct code in fenced blocks and name the file. Explain only what was asked.",
12
+ key_env="GEMINI_API_KEY_CODING",
13
+ tools=frozenset({"read_source", "search_documents", "read_document", "find_usages", "search_source", "list_project_files",
14
+ "query_code_graph", "build_code_graph", "file_to_markdown"}))
pal/agents/docs.py ADDED
@@ -0,0 +1,15 @@
1
+ from pal.sdk.agent import AgentSpec, NativeAgent
2
+
3
+
4
+ class DocumentationAgent(NativeAgent):
5
+ """Writes README, API reference and docstrings strictly from the code or text it is given."""
6
+
7
+ spec = AgentSpec(
8
+ id="docs", name="Documentation agent",
9
+ description="Writes README, API reference and docstrings from code or text you attach.",
10
+ role="Role: documentation agent. Write accurate, concise markdown documentation (README, API reference, "
11
+ "docstrings, changelog) strictly from the supplied code or text. Never invent behaviour: mark unknowns "
12
+ "as TODO. Use headings and short sentences.",
13
+ key_env="GEMINI_API_KEY_DOCS", max_tokens=1536,
14
+ tools=frozenset({"read_source", "search_documents", "read_document", "document_outline", "list_project_files", "search_source",
15
+ "save_file", "query_code_graph", "build_code_graph", "file_to_markdown"}))
pal/chat/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from pal.chat.providers import API_PROVIDERS, build_api_core
2
+ from pal.chat.service import ChatService, TurnResult
3
+
4
+ __all__ = ["API_PROVIDERS", "ChatService", "TurnResult", "build_api_core"]
pal/chat/demo.py ADDED
@@ -0,0 +1,231 @@
1
+ """The experiment tab's demo: the same 6 messages, sent two ways, measured.
2
+
3
+ naive : what a plain chat client does. Full history every turn (attachments included), the raw
4
+ message, no output rules, every message to the API model.
5
+ pal : ChatService.send() with everything on (routing, input savers, direct answers, document
6
+ index, paging, output rules).
7
+
8
+ Both arms use the same API model and count the tokens the provider itself reports. Latency is
9
+ wall-clock time per turn; Pal's includes the local-model time, so a slow local model can make
10
+ Pal *slower* on some turns, and the summary says so instead of hiding it. Demo turns read and
11
+ write no memory and are not added to the Stats.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import threading
16
+ import time
17
+ import uuid
18
+ from collections import OrderedDict
19
+ from dataclasses import dataclass, field
20
+ from typing import Any
21
+
22
+ from pal.chat.service import ChatService
23
+ from pal.errors import NotFoundError, UserInputError
24
+ from pal.llm_wrapper.components.base import LLMSyscall
25
+
26
+ SURVEY_NAME = "student-survey.md"
27
+ SURVEY = """# Papertrail student survey (120 students, 4 campuses)
28
+
29
+ ## Who answered
30
+ - 120 students: 58 undergraduates, 41 master's students, 21 PhD students.
31
+ - 71% take notes on a laptop, 19% on a tablet, 10% on paper only.
32
+ - 64% use more than one note app today; the most common are Google Docs (62%), Notion (38%) and OneNote (27%).
33
+
34
+ ## What they dislike about current tools
35
+ - Finding old notes is the top complaint: 57% say they often cannot find a note they wrote last semester.
36
+ - 41% say syncing between phone and laptop fails at least once a month.
37
+ - 33% say the tools are too complicated for lecture notes and slow to open in class.
38
+ - 18% say subscription prices are too high for a student budget.
39
+
40
+ ## What they would pay
41
+ - 46% would pay nothing for a note app.
42
+ - 38% would pay up to 3 dollars a month.
43
+ - 12% would pay 3 to 6 dollars a month.
44
+ - 4% would pay more than 6 dollars a month.
45
+ - 72% say a free tier with a paid upgrade is what they prefer.
46
+
47
+ ## Where they hear about new apps
48
+ - Instagram: 61%. Student WhatsApp and Telegram groups: 48%.
49
+ - Campus posters and notice boards: 35%. Friends and classmates: 33%.
50
+ - Reddit: 22%. Email newsletters from the university: 14%. TikTok: 12%.
51
+
52
+ ## Features they asked for most
53
+ - Search that also finds words inside handwriting and photos of slides: 52%.
54
+ - Works offline in lecture halls: 44%.
55
+ - Share a notebook with a study group: 39%.
56
+ - Turn notes into flashcards: 27%.
57
+
58
+ ## Quotes
59
+ - "I write everything down and then never find it again."
60
+ - "I would try anything that opens instantly and does not need a login."
61
+ - "I only download new apps when three friends already use them."
62
+ """
63
+
64
+ STEPS: list[dict[str, Any]] = [
65
+ {"message": "Hi there! So, I was basically wondering if you could please help me plan a small launch for "
66
+ "my new note-taking app called Papertrail. It is really just for students, and I would like to "
67
+ "keep the whole thing under a budget of 500 dollars, if that is at all possible.",
68
+ "route": "api"},
69
+ {"message": "Thanks so much. Here are my survey notes, please just read them and tell me the three most "
70
+ "important things that students say.",
71
+ "attach": True, "route": "api"},
72
+ {"message": "what is 500*0.35", "route": None},
73
+ {"message": "Quick question, what does ROI stand for?", "route": None},
74
+ {"message": "Given the survey notes and the budget, could you please suggest the three channels that I "
75
+ "should focus on, and roughly how to split the 500 dollars between them?",
76
+ "route": "api"},
77
+ {"message": "Great, now please put everything together into a one page launch brief for Papertrail: goal, "
78
+ "audience, budget split, channels, an email subject line, and a first week timeline.",
79
+ "route": "api"},
80
+ ]
81
+
82
+ _MAX_RUNS = 20
83
+ _TTL = 3600.0
84
+ _NAIVE_SYSTEM_NOTE = "" # a naive client adds nothing to the base prompt: no output rules
85
+
86
+
87
+ @dataclass
88
+ class _Run:
89
+ id: str
90
+ sid: str
91
+ provider: str
92
+ model: str
93
+ naive_history: list[dict] = field(default_factory=list)
94
+ steps: list[dict] = field(default_factory=list)
95
+ local: dict = field(default_factory=dict)
96
+ created: float = field(default_factory=time.time)
97
+ lock: threading.Lock = field(default_factory=threading.Lock)
98
+
99
+
100
+ def _public_steps() -> list[dict]:
101
+ return [{"message": s["message"], "attachment": SURVEY_NAME if s.get("attach") else None} for s in STEPS]
102
+
103
+
104
+ class Demo:
105
+ def __init__(self, service: ChatService):
106
+ self.service = service
107
+ self._runs: OrderedDict[str, _Run] = OrderedDict()
108
+ self._lock = threading.Lock()
109
+
110
+ # -- lifecycle ---------------------------------------------------------------------------------
111
+ def start(self, provider: str, model: str, api_key: str | None) -> dict:
112
+ self.service.api_core(provider, model, api_key) # fail fast on a bad provider, model or missing key
113
+ run = _Run(id=uuid.uuid4().hex, sid=f"demo-{uuid.uuid4().hex[:24]}", provider=provider, model=model,
114
+ local=self.service.local_status())
115
+ with self._lock:
116
+ self._expire()
117
+ self._runs[run.id] = run
118
+ while len(self._runs) > _MAX_RUNS:
119
+ _, old = self._runs.popitem(last=False)
120
+ self.service.reset(old.sid)
121
+ return {"run_id": run.id, "steps": _public_steps(), "local": run.local}
122
+
123
+ def _expire(self) -> None:
124
+ now = time.time()
125
+ for k in [k for k, r in self._runs.items() if now - r.created > _TTL]:
126
+ self.service.reset(self._runs.pop(k).sid)
127
+
128
+ def _get(self, run_id: str) -> _Run:
129
+ with self._lock:
130
+ run = self._runs.get(run_id)
131
+ if run is None:
132
+ raise NotFoundError("demo run not found or expired: start it again")
133
+ return run
134
+
135
+ def cancel(self, run_id: str) -> None:
136
+ with self._lock:
137
+ run = self._runs.pop(run_id, None)
138
+ if run:
139
+ self.service.reset(run.sid)
140
+
141
+ # -- one step ---------------------------------------------------------------------------------------------
142
+ def step(self, run_id: str, api_key: str | None) -> dict:
143
+ run = self._get(run_id)
144
+ with run.lock:
145
+ i = len(run.steps)
146
+ if i >= len(STEPS):
147
+ raise UserInputError("the demo already finished")
148
+ spec = STEPS[i]
149
+ api = self.service.api_core(run.provider, run.model, api_key)
150
+
151
+ # naive arm: full history, raw message, the file re-sent every time it is in the history
152
+ user_text = spec["message"] + (f"\n\n[file: {SURVEY_NAME}]\n{SURVEY}" if spec.get("attach") else "")
153
+ messages = [{"role": "system", "content": self.service.settings.base_system_prompt}]
154
+ messages += run.naive_history + [{"role": "user", "content": user_text}]
155
+ t0 = time.perf_counter()
156
+ try:
157
+ resp = api.chat(LLMSyscall(messages=messages, max_tokens=2048))
158
+ except Exception as e: # noqa: BLE001
159
+ from pal.errors import ProviderError
160
+ secret = getattr(getattr(api, "config", None), "api_key", None) or ""
161
+ text = str(e).replace(secret, "***") if secret else str(e)
162
+ raise ProviderError(f"naive arm: {type(e).__name__}: {text}") from e
163
+ naive = {"input_tokens": resp.prompt_tokens or 0, "output_tokens": resp.completion_tokens or 0,
164
+ "latency": round(time.perf_counter() - t0, 3), "reply": resp.text or ""}
165
+
166
+ # Pal arm: the real service, on its own throwaway session
167
+ attachments = [{"name": SURVEY_NAME, "text": SURVEY, "kind": "document"}] if spec.get("attach") else []
168
+ res = self.service.send(run.sid, run.provider, run.model, api_key, spec["message"], attachments,
169
+ None, route=spec["route"], ephemeral=True)
170
+ pal = {"route": res.handled_by, "reason": res.route_reason, "input_tokens": res.input_tokens,
171
+ "output_tokens": res.output_tokens, "latency": res.latency.get("total", 0.0),
172
+ "latency_local": res.latency.get("local", 0.0), "latency_api": res.latency.get("api", 0.0),
173
+ "tools": [t["name"] for t in res.tools], "reply": res.reply,
174
+ "reduced_tokens_saved": getattr(res.saver, "tokens_saved", 0),
175
+ "history_turns_sent": (res.context or {}).get("turns_sent"),
176
+ "history_tokens_saved": (res.context or {}).get("tokens_saved", 0),
177
+ "document_chunks": (res.documents or {}).get("chunks", 0)}
178
+
179
+ # commit only after both arms succeeded, so a retry of this step stays consistent
180
+ run.naive_history += [{"role": "user", "content": user_text},
181
+ {"role": "assistant", "content": naive["reply"]}]
182
+ row = {"step": i + 1, "message": spec["message"], "naive": naive, "pal": pal}
183
+ run.steps.append(row)
184
+ out: dict[str, Any] = {"row": row, "done": len(run.steps) == len(STEPS)}
185
+ if out["done"]:
186
+ out["summary"] = self._summary(run)
187
+ self.cancel(run.id)
188
+ return out
189
+
190
+ # -- totals ---------------------------------------------------------------------------------------------------
191
+ @staticmethod
192
+ def _pct(saved: float, base: float) -> float:
193
+ return round(100 * saved / base, 1) if base else 0.0
194
+
195
+ def _summary(self, run: _Run) -> dict:
196
+ rows = run.steps
197
+ n_in = sum(r["naive"]["input_tokens"] for r in rows)
198
+ n_out = sum(r["naive"]["output_tokens"] for r in rows)
199
+ m_in = sum(r["pal"]["input_tokens"] for r in rows)
200
+ m_out = sum(r["pal"]["output_tokens"] for r in rows)
201
+ n_lat = sum(r["naive"]["latency"] for r in rows)
202
+ m_lat = sum(r["pal"]["latency"] for r in rows)
203
+ routes = [r["pal"]["route"] for r in rows]
204
+ saved_tokens = (n_in + n_out) - (m_in + m_out)
205
+ saved_lat = n_lat - m_lat
206
+ local_ok = run.local.get("ok")
207
+ notes = []
208
+ if local_ok is False:
209
+ notes.append("The local model was not reachable, so input reduction and local answering did not run. "
210
+ "The saving below comes from direct answers, the output rules and document handling only. "
211
+ "Start Ollama and run the demo again to see the full effect.")
212
+ if saved_lat < 0:
213
+ notes.append("Pal was slower overall: the local model's time is included, and on a slow machine it "
214
+ "can cost more than it saves.")
215
+ notes.append("Pal's output rules make replies shorter, which is part of the token saving and also "
216
+ "changes the style of the answers.")
217
+ return {
218
+ "naive": {"input_tokens": n_in, "output_tokens": n_out, "total_tokens": n_in + n_out,
219
+ "latency": round(n_lat, 2), "api_calls": len(rows)},
220
+ "pal": {"input_tokens": m_in, "output_tokens": m_out, "total_tokens": m_in + m_out,
221
+ "latency": round(m_lat, 2),
222
+ "api_calls": sum(1 for r in routes if r == "api"),
223
+ "local_answers": routes.count("local"), "direct_answers": routes.count("tool"),
224
+ "tool_calls": sum(len(r["pal"]["tools"]) for r in rows)},
225
+ "tokens_saved": saved_tokens, "tokens_saved_pct": self._pct(saved_tokens, n_in + n_out),
226
+ "input_saved": n_in - m_in, "output_saved": n_out - m_out,
227
+ "latency_saved": round(saved_lat, 2), "latency_saved_pct": self._pct(saved_lat, n_lat),
228
+ "api_calls_avoided": len(rows) - sum(1 for r in routes if r == "api"),
229
+ "local": run.local, "notes": notes,
230
+ "result": rows[-1]["pal"]["reply"], "result_naive": rows[-1]["naive"]["reply"],
231
+ }