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,220 @@
1
+ """
2
+ Context interrupt mechanism for Round Robin scheduling (AIOS section
3
+ 3.3 / detailed in their Appendix A.3, which this module is the
4
+ counterpart of here).
5
+
6
+ Long-running LLM inference is exactly the kind of system call that can
7
+ monopolize a shared resource -- one slow generation blocks everything
8
+ queued behind it unless something can pause it. FIFO scheduling doesn't
9
+ need that (it runs each request to completion by design); Round Robin
10
+ does, since giving every request a bounded time slice only works if a
11
+ request that isn't done yet can be suspended and picked back up later
12
+ without losing the work already done.
13
+
14
+ Two snapshot/restore approaches, matching what's actually available
15
+ per core:
16
+
17
+ - **Text-based** (`_run_slice_text` / `ContextSnapshot(mode="text")`):
18
+ for cores with no logits access -- every cloud API and every local
19
+ server this project talks to (Anthropic, Gemini, OpenAI-compatible,
20
+ Ollama). None of them return raw per-step logits over HTTP, so the
21
+ only state available to snapshot is the decoded text generated so
22
+ far. Resumption re-injects that text as a partial assistant turn plus
23
+ a short "continue exactly where you left off" instruction, and lets
24
+ the model carry on from there.
25
+
26
+ - **Logits-based** (`_run_slice_logits` / `ContextSnapshot(mode="logits")`):
27
+ for cores with direct logits access -- currently only
28
+ HuggingFaceLocalLLM, the in-process local model. Generation is
29
+ driven one token at a time via manual beam-search stepping
30
+ (HuggingFaceLocalLLM.beam_search_step), and the snapshot is the
31
+ actual search-tree state (every beam's token sequence + cumulative
32
+ score, see BeamSearchState in huggingface_local.py) rather than
33
+ just its decoded text. Resuming continues the search from that exact
34
+ state -- no re-tokenization, no risk of the model repeating or
35
+ drifting from where it left off the way a text-based "please
36
+ continue" prompt can.
37
+
38
+ Both are request-level approximations of "pause an in-flight
39
+ computation" -- neither literally suspends a running matrix
40
+ multiplication. What differs between them is how much of the model's
41
+ own state survives the pause: text-based keeps only what came out the
42
+ other end; logits-based keeps the search state that produced it.
43
+ """
44
+ from __future__ import annotations
45
+
46
+ import time
47
+ from dataclasses import dataclass
48
+ from typing import Any, Optional
49
+
50
+ from pal.llm_wrapper.components.base import LLMSyscall, LLMWrapper
51
+
52
+ _CONTINUE_INSTRUCTION = (
53
+ "[Continue your previous reply exactly where it left off. Do not "
54
+ "repeat any earlier part of it, do not restart, and do not "
55
+ "acknowledge this instruction.]"
56
+ )
57
+
58
+
59
+ @dataclass
60
+ class ContextSnapshot:
61
+ """The suspended state of one interrupted generation.
62
+
63
+ mode="text": text_so_far is the only state -- decoding must
64
+ restart from a continuation prompt built around it.
65
+ mode="logits": beam_state carries the actual search-tree state
66
+ (an opaque, core-specific object -- BeamSearchState for
67
+ HuggingFaceLocalLLM) that resumption steps forward directly,
68
+ with text_so_far kept alongside purely for human-readable
69
+ inspection/logging, not used to drive resumption.
70
+ """
71
+
72
+ mode: str # "text" | "logits"
73
+ text_so_far: str = ""
74
+ beam_state: Any = None
75
+
76
+
77
+ @dataclass
78
+ class InterruptResult:
79
+ complete: bool
80
+ text_so_far: str
81
+ elapsed: float
82
+ snapshot: ContextSnapshot
83
+ # True when text_so_far is the FULL cumulative text generated across
84
+ # every slice so far (logits mode -- beam decoding naturally tracks
85
+ # whole sequences). False when it's only this slice's delta, to be
86
+ # appended to whatever came before (text mode -- each streamed
87
+ # continuation only yields its own new chunk).
88
+ cumulative: bool = False
89
+ chunks_yielded: int = 0
90
+
91
+
92
+ class ContextInterruptController:
93
+ """Runs one LLMSyscall against one core for at most `quantum`
94
+ seconds, choosing the text-based or logits-based approach based on
95
+ the core's capabilities(), and returns either a complete result or
96
+ a ContextSnapshot the scheduler can use to resume later."""
97
+
98
+ def run_slice(
99
+ self,
100
+ core: LLMWrapper,
101
+ syscall: LLMSyscall,
102
+ quantum: float,
103
+ resume_snapshot: Optional[ContextSnapshot] = None,
104
+ ) -> InterruptResult:
105
+ use_logits = (resume_snapshot is not None and resume_snapshot.mode == "logits") or (
106
+ resume_snapshot is None
107
+ and core.capabilities().supports_logits_snapshot
108
+ and hasattr(core, "init_beam_search")
109
+ and syscall.params.get("use_logits_snapshot", True)
110
+ )
111
+ if use_logits:
112
+ return self._run_slice_logits(core, syscall, quantum, resume_snapshot)
113
+ return self._run_slice_text(core, syscall, quantum)
114
+
115
+ # -- text-based: closed-source / no-logits-access cores --------------------
116
+
117
+ def _run_slice_text(self, core: LLMWrapper, syscall: LLMSyscall, quantum: float) -> InterruptResult:
118
+ text_parts: list[str] = []
119
+ t0 = time.time()
120
+ chunks = 0
121
+
122
+ for chunk in core.stream_chat(syscall):
123
+ text_parts.append(chunk)
124
+ chunks += 1
125
+ elapsed = time.time() - t0
126
+ if elapsed >= quantum:
127
+ # Time slice exhausted mid-generation -- stop pulling
128
+ # from the stream here. Cores without real incremental
129
+ # streaming (LLMWrapper.stream_chat's default fallback,
130
+ # which yields the whole response as one chunk) will
131
+ # already have finished by the time the first chunk
132
+ # arrives, so this branch only actually interrupts
133
+ # something for cores with true token-level streaming.
134
+ text_so_far = "".join(text_parts)
135
+ return InterruptResult(
136
+ complete=False,
137
+ text_so_far=text_so_far,
138
+ elapsed=elapsed,
139
+ snapshot=ContextSnapshot(mode="text", text_so_far=text_so_far),
140
+ cumulative=False,
141
+ chunks_yielded=chunks,
142
+ )
143
+
144
+ text_so_far = "".join(text_parts)
145
+ return InterruptResult(
146
+ complete=True,
147
+ text_so_far=text_so_far,
148
+ elapsed=time.time() - t0,
149
+ snapshot=ContextSnapshot(mode="text", text_so_far=text_so_far),
150
+ cumulative=False,
151
+ chunks_yielded=chunks,
152
+ )
153
+
154
+ def build_continuation(self, syscall: LLMSyscall, partial: InterruptResult) -> LLMSyscall:
155
+ """Text-based resumption: folds the partial output back into the
156
+ message list as prior context and returns a new LLMSyscall ready
157
+ to be re-queued. Only meaningful for mode="text" results -- the
158
+ scheduler doesn't call this for logits-mode interrupts, since
159
+ those resume from `beam_state` directly instead of rebuilding
160
+ the prompt."""
161
+ continued_messages = list(syscall.messages) + [
162
+ {"role": "assistant", "content": partial.text_so_far},
163
+ {"role": "user", "content": _CONTINUE_INSTRUCTION},
164
+ ]
165
+ return LLMSyscall(
166
+ messages=continued_messages,
167
+ model=syscall.model,
168
+ temperature=syscall.temperature,
169
+ max_tokens=syscall.max_tokens,
170
+ stream=syscall.stream,
171
+ params=syscall.params,
172
+ )
173
+
174
+ # -- logits-based: direct logits access, search-tree state -----------------
175
+
176
+ def _run_slice_logits(
177
+ self,
178
+ core: LLMWrapper,
179
+ syscall: LLMSyscall,
180
+ quantum: float,
181
+ resume_snapshot: Optional[ContextSnapshot],
182
+ ) -> InterruptResult:
183
+ t0 = time.time()
184
+ beam_width = syscall.params.get("beam_width", 1)
185
+ max_new_tokens = syscall.max_tokens or 512
186
+
187
+ state = resume_snapshot.beam_state if resume_snapshot is not None else core.init_beam_search(
188
+ syscall, beam_width=beam_width
189
+ )
190
+
191
+ steps_this_slice = 0
192
+ while not state.finished:
193
+ state = core.beam_search_step(state, max_new_tokens=max_new_tokens)
194
+ steps_this_slice += 1
195
+ elapsed = time.time() - t0
196
+
197
+ if elapsed >= quantum and not state.finished:
198
+ # Suspended mid-generation -- snapshot the intermediate
199
+ # search-tree state (not just its decoded text) so
200
+ # resumption continues the beam search exactly where
201
+ # this step left off.
202
+ decoded = core.decode_beam_search(state)
203
+ return InterruptResult(
204
+ complete=False,
205
+ text_so_far=decoded,
206
+ elapsed=elapsed,
207
+ snapshot=ContextSnapshot(mode="logits", text_so_far=decoded, beam_state=state),
208
+ cumulative=True,
209
+ chunks_yielded=steps_this_slice,
210
+ )
211
+
212
+ final_text = core.decode_beam_search(state)
213
+ return InterruptResult(
214
+ complete=True,
215
+ text_so_far=final_text,
216
+ elapsed=time.time() - t0,
217
+ snapshot=ContextSnapshot(mode="logits", text_so_far=final_text, beam_state=state),
218
+ cumulative=True,
219
+ chunks_yielded=steps_this_slice,
220
+ )
@@ -0,0 +1,9 @@
1
+ from pal.orchestrator.components.cgroup_manager import ResourceLimits
2
+
3
+ # Preset resource envelopes for common agent workload sizes. cpu.max
4
+ # format is "<quota> <period>" in microseconds -- e.g. "50000 100000" =
5
+ # 50% of one CPU core.
6
+
7
+ LIGHT = ResourceLimits(memory_max="256M", cpu_max="25000 100000", pids_max="20")
8
+ STANDARD = ResourceLimits(memory_max="512M", cpu_max="50000 100000", pids_max="50")
9
+ HEAVY = ResourceLimits(memory_max="2G", cpu_max="100000 100000", pids_max="100")
@@ -0,0 +1,69 @@
1
+ from dataclasses import dataclass
2
+
3
+ from pal.orchestrator.components.agent import Agent, AgentConfig
4
+ from pal.orchestrator.components.cgroup_manager import CgroupManager
5
+
6
+
7
+ @dataclass
8
+ class ScheduledAgent:
9
+ config: AgentConfig
10
+ priority: int = 0 # higher runs first when slots are contended
11
+ weight: int = 100 # relative cpu.weight if backend supports it
12
+
13
+
14
+ class Scheduler:
15
+ """
16
+ Bounds how many agents run concurrently (max_concurrent), and
17
+ decides which queued agents get a slot next based on priority.
18
+ Doesn't touch cgroup internals directly -- that stays
19
+ CgroupManager's job. This is purely admission control: who gets to
20
+ start, and in what order.
21
+ """
22
+
23
+ def __init__(self, cgroup_manager: CgroupManager, max_concurrent: int = 3):
24
+ self.cgroup_manager = cgroup_manager
25
+ self.max_concurrent = max_concurrent
26
+ self.running: dict[str, Agent] = {}
27
+ self.queue: list[ScheduledAgent] = []
28
+
29
+ def submit(self, config: AgentConfig, priority: int = 0, weight: int = 100) -> None:
30
+ if config.name in self.running:
31
+ raise ValueError(f"agent {config.name} already running")
32
+ if any(s.config.name == config.name for s in self.queue):
33
+ raise ValueError(f"agent {config.name} already queued")
34
+ self.queue.append(ScheduledAgent(config=config, priority=priority, weight=weight))
35
+ self.queue.sort(key=lambda s: s.priority, reverse=True)
36
+
37
+ def tick(self) -> list[str]:
38
+ """
39
+ Admits as many queued agents as available slots allow, in
40
+ priority order. Returns the names of agents newly started this
41
+ tick. Call this after submit(), and again after any stop() to
42
+ backfill freed slots.
43
+ """
44
+ started = []
45
+ while self.queue and len(self.running) < self.max_concurrent:
46
+ scheduled = self.queue.pop(0)
47
+ agent = Agent(scheduled.config, self.cgroup_manager)
48
+ agent.start()
49
+ self.running[scheduled.config.name] = agent
50
+ started.append(scheduled.config.name)
51
+ return started
52
+
53
+ def stop(self, agent_name: str) -> None:
54
+ agent = self.running.pop(agent_name, None)
55
+ if agent is None:
56
+ raise ValueError(f"agent {agent_name} not running")
57
+ agent.stop()
58
+
59
+ def stop_all(self) -> None:
60
+ for name in list(self.running.keys()):
61
+ self.stop(name)
62
+
63
+ def status(self) -> dict:
64
+ return {
65
+ "running": list(self.running.keys()),
66
+ "queued": [s.config.name for s in self.queue],
67
+ "capacity": self.max_concurrent,
68
+ "slots_free": self.max_concurrent - len(self.running),
69
+ }
@@ -0,0 +1,200 @@
1
+ """
2
+ Centralized system-call scheduler (AIOS section 3.3).
3
+
4
+ All LLM inference requests -- from Pipeline.run(), from orchestrator
5
+ Agents, from anywhere else in the system -- get submitted here as
6
+ SyscallRequests instead of each caller hitting an LLMWrapper directly.
7
+ That's the actual point of centralizing: request management
8
+ (queueing, ordering, time-slicing) lives in exactly one place, so
9
+ every other module only has to focus on producing/consuming a
10
+ request/result pair, not on how or when its turn comes.
11
+
12
+ Two scheduling strategies, same as AIOS:
13
+ - FIFO: process queued requests in arrival order, straight through to
14
+ completion. Simple, but a long request can make everything behind it
15
+ wait its full duration.
16
+ - Round Robin: give each queued request a bounded time slice
17
+ (`quantum`), then move to the next one, cycling back to unfinished
18
+ requests later. This is what needs the context-interrupt mechanism in
19
+ context_interrupt.py -- pausing an LLM generation mid-stream and
20
+ resuming it later without restarting from scratch.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import time
25
+ import uuid
26
+ from collections import deque
27
+ from dataclasses import dataclass, field
28
+ from enum import Enum
29
+ from typing import Optional
30
+
31
+ from pal.llm_wrapper.components.base import LLMResponse, LLMSyscall, LLMWrapper
32
+ from pal.orchestrator.components.context_interrupt import ContextInterruptController, ContextSnapshot
33
+
34
+
35
+ class SchedulingStrategy(str, Enum):
36
+ FIFO = "fifo"
37
+ ROUND_ROBIN = "round_robin"
38
+
39
+
40
+ class SyscallStatus(str, Enum):
41
+ QUEUED = "queued"
42
+ RUNNING = "running"
43
+ DONE = "done"
44
+ ERROR = "error"
45
+
46
+
47
+ @dataclass
48
+ class SyscallRequest:
49
+ """One queued LLM inference request. `core` is whichever LLMWrapper
50
+ the submitting module wants this run against -- the scheduler
51
+ doesn't care which deployment option it is, only when it runs."""
52
+
53
+ agent_name: str
54
+ core: LLMWrapper
55
+ syscall: LLMSyscall
56
+ id: str = field(default_factory=lambda: str(uuid.uuid4()))
57
+ submitted_at: float = field(default_factory=time.time)
58
+ priority: int = 0 # FIFO ignores this; reserved for future strategies
59
+
60
+ # internal scheduling state
61
+ status: SyscallStatus = SyscallStatus.QUEUED
62
+ accumulated_text: str = ""
63
+ slices_run: int = 0
64
+ result: Optional[LLMResponse] = None
65
+ error: Optional[str] = None
66
+ # Set when a Round Robin slice is interrupted, so the next tick
67
+ # knows to RESUME (text-based: via the continuation baked into
68
+ # `syscall` already; logits-based: via snapshot.beam_state) rather
69
+ # than starting a fresh generation.
70
+ snapshot: Optional[ContextSnapshot] = None
71
+
72
+
73
+ class SyscallScheduler:
74
+ """
75
+ One centralized queue for every LLM syscall in the system,
76
+ regardless of which module submitted it. Callers submit() a request
77
+ and get back an id; results are collected via get_result() once
78
+ processing has advanced far enough (run_fifo() / run_round_robin()
79
+ drive that, they aren't automatic/background by default -- call
80
+ tick() from whatever event loop the rest of the system already
81
+ has, e.g. the orchestrator's own tick()).
82
+ """
83
+
84
+ def __init__(self, strategy: SchedulingStrategy = SchedulingStrategy.FIFO, rr_quantum: float = 2.0):
85
+ self.strategy = strategy
86
+ self.rr_quantum = rr_quantum
87
+ self.queue: deque[SyscallRequest] = deque()
88
+ self.requests: dict[str, SyscallRequest] = {}
89
+ self._interrupt_controller = ContextInterruptController()
90
+
91
+ def submit(self, agent_name: str, core: LLMWrapper, syscall: LLMSyscall, priority: int = 0) -> str:
92
+ req = SyscallRequest(agent_name=agent_name, core=core, syscall=syscall, priority=priority)
93
+ self.requests[req.id] = req
94
+ self.queue.append(req)
95
+ return req.id
96
+
97
+ def get_result(self, request_id: str) -> SyscallRequest:
98
+ return self.requests[request_id]
99
+
100
+ def pending_count(self) -> int:
101
+ return len(self.queue)
102
+
103
+ # -- scheduling strategies -------------------------------------------------
104
+
105
+ def run_fifo_once(self) -> Optional[SyscallRequest]:
106
+ """Pops the head of the queue and runs it straight through to
107
+ completion. No interruption -- a long request occupies the
108
+ core for its full duration before the next one gets to start,
109
+ by design."""
110
+ if not self.queue:
111
+ return None
112
+
113
+ req = self.queue.popleft()
114
+ req.status = SyscallStatus.RUNNING
115
+ try:
116
+ req.result = req.core.chat(req.syscall)
117
+ req.accumulated_text = req.result.text
118
+ req.status = SyscallStatus.DONE
119
+ except Exception as e:
120
+ req.error = str(e)
121
+ req.status = SyscallStatus.ERROR
122
+ return req
123
+
124
+ def run_round_robin_tick(self) -> Optional[SyscallRequest]:
125
+ """Runs the head-of-queue request for at most `rr_quantum`
126
+ seconds, via ContextInterruptController -- which picks the
127
+ text-based or logits-based snapshot approach depending on what
128
+ the request's core supports. If it finishes within that slice,
129
+ it's marked DONE and NOT re-queued. If the slice runs out
130
+ first:
131
+ - text-based: the partial output is folded into a continuation
132
+ syscall (build_continuation) and pushed to the back of the
133
+ queue.
134
+ - logits-based: the request's `syscall` is left UNCHANGED
135
+ (nothing to rebuild -- the search-tree state itself is what
136
+ gets resumed) and its `snapshot` is stored for the next tick
137
+ to resume from directly.
138
+ Either way, every other queued request gets its own slice
139
+ before this one is revisited -- that's the "round" in Round
140
+ Robin."""
141
+ if not self.queue:
142
+ return None
143
+
144
+ req = self.queue.popleft()
145
+ req.status = SyscallStatus.RUNNING
146
+ try:
147
+ partial = self._interrupt_controller.run_slice(
148
+ req.core, req.syscall, self.rr_quantum, resume_snapshot=req.snapshot
149
+ )
150
+ except Exception as e:
151
+ req.error = str(e)
152
+ req.status = SyscallStatus.ERROR
153
+ return req
154
+
155
+ req.slices_run += 1
156
+ if partial.cumulative:
157
+ # Logits mode: beam decoding naturally tracks the full
158
+ # sequence generated so far, so this IS the total, not a
159
+ # delta to append.
160
+ req.accumulated_text = partial.text_so_far
161
+ else:
162
+ # Text mode: each resumed stream only yields its own new
163
+ # continuation text.
164
+ req.accumulated_text += partial.text_so_far
165
+
166
+ if partial.complete:
167
+ req.result = LLMResponse(text=req.accumulated_text)
168
+ req.status = SyscallStatus.DONE
169
+ req.snapshot = None
170
+ else:
171
+ if partial.snapshot.mode == "text":
172
+ req.syscall = self._interrupt_controller.build_continuation(req.syscall, partial)
173
+ req.snapshot = None
174
+ else:
175
+ req.snapshot = partial.snapshot
176
+ req.status = SyscallStatus.QUEUED
177
+ self.queue.append(req)
178
+
179
+ return req
180
+
181
+ def tick(self) -> Optional[SyscallRequest]:
182
+ """Advances the scheduler by exactly one step, using whichever
183
+ strategy this instance was configured with."""
184
+ if self.strategy == SchedulingStrategy.FIFO:
185
+ return self.run_fifo_once()
186
+ return self.run_round_robin_tick()
187
+
188
+ def drain(self, max_ticks: int = 10_000) -> list[SyscallRequest]:
189
+ """Runs tick() until the queue is empty (every request reaches
190
+ DONE or ERROR) or max_ticks is hit, as a safety bound against
191
+ an unbounded RR cycle. Returns the requests in the order they
192
+ finished."""
193
+ finished = []
194
+ ticks = 0
195
+ while self.queue and ticks < max_ticks:
196
+ req = self.tick()
197
+ ticks += 1
198
+ if req is not None and req.status in (SyscallStatus.DONE, SyscallStatus.ERROR):
199
+ finished.append(req)
200
+ return finished