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,215 @@
1
+ """The local model's scheduler: preemptive priority, first come first served inside each class.
2
+
3
+ One job runs at a time. Classes in order: AGENT, then CHAT, then OTHER. When a job of a better class is waiting, the
4
+ running job is paused at its next safe point and the better job goes first; the paused job then resumes before any later
5
+ arrival of its own class, so first come first served is kept inside a class.
6
+
7
+ Three kinds of job, because a model call cannot always be paused:
8
+ * atomic one call that finishes (route, trim, verify: structured and short). Never paused.
9
+ * steps a generator of small pieces (summarise a section, tidy a note). Paused between pieces: no state to save.
10
+ * stream a plain-text generation. Paused between chunks: the stream is closed (so the model stops), the text so far is
11
+ kept, and the job resumes by continuing from that text. Only for cores that really stream.
12
+ A job can be paused at most `max_pauses` times, then it runs to the end, so it cannot be postponed forever.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ import threading
18
+ import time
19
+ from collections import deque
20
+ from collections.abc import Callable, Iterator
21
+ from typing import Any
22
+
23
+ from pal.llm_scheduler.types import ExpiredError, QueueFullError, SchedulerError, TaskClass, Ticket
24
+ from pal.llm_wrapper.components.base import LLMSyscall, LLMWrapper
25
+
26
+ log = logging.getLogger(__name__)
27
+
28
+
29
+ class _Job:
30
+ preemptible = False
31
+
32
+ def __init__(self, ticket: Ticket, cls: TaskClass, deadline: float | None = None):
33
+ self.ticket, self.cls, self.deadline = ticket, cls, deadline
34
+ self.result: Any = None
35
+
36
+ def run(self, should_stop: Callable[[], bool]) -> bool: # True when finished
37
+ raise NotImplementedError
38
+
39
+
40
+ class _AtomicJob(_Job):
41
+ def __init__(self, ticket: Ticket, cls: TaskClass, fn: Callable[[], Any], deadline: float | None = None):
42
+ super().__init__(ticket, cls, deadline)
43
+ self.fn = fn
44
+
45
+ def run(self, should_stop: Callable[[], bool]) -> bool:
46
+ self.result = self.fn()
47
+ return True
48
+
49
+
50
+ class _StepsJob(_Job):
51
+ preemptible = True
52
+
53
+ def __init__(self, ticket: Ticket, cls: TaskClass, steps: Iterator[Any], deadline: float | None = None):
54
+ super().__init__(ticket, cls, deadline)
55
+ self.steps = steps
56
+
57
+ def run(self, should_stop: Callable[[], bool]) -> bool:
58
+ while True:
59
+ try:
60
+ next(self.steps)
61
+ except StopIteration as done:
62
+ self.result = done.value
63
+ return True
64
+ if should_stop():
65
+ return False
66
+
67
+
68
+ class _StreamJob(_Job):
69
+ preemptible = True
70
+
71
+ def __init__(self, ticket: Ticket, cls: TaskClass, core: LLMWrapper, syscall: LLMSyscall):
72
+ super().__init__(ticket, cls)
73
+ self.core, self.syscall, self.text = core, syscall, ""
74
+
75
+ def _continuation(self) -> LLMSyscall:
76
+ from pal.orchestrator.components.context_interrupt import ( # lazy: avoids an import cycle
77
+ ContextInterruptController,
78
+ ContextSnapshot,
79
+ InterruptResult,
80
+ )
81
+ partial = InterruptResult(complete=False, text_so_far=self.text, elapsed=0.0,
82
+ snapshot=ContextSnapshot(mode="text", text_so_far=self.text))
83
+ return ContextInterruptController().build_continuation(self.syscall, partial)
84
+
85
+ def run(self, should_stop: Callable[[], bool]) -> bool:
86
+ stream = iter(self.core.stream_chat(self.syscall if not self.text else self._continuation()))
87
+ try:
88
+ chunk = next(stream, None)
89
+ while chunk is not None:
90
+ self.text += chunk
91
+ chunk = next(stream, None) # look at the next chunk before deciding to stop,
92
+ if chunk is None: # so a stream that has just ended is never paused
93
+ break
94
+ if should_stop():
95
+ self.text += chunk
96
+ return False
97
+ finally:
98
+ close = getattr(stream, "close", None)
99
+ if close:
100
+ close() # closing the stream makes the model stop generating
101
+ self.result = self.text
102
+ return True
103
+
104
+
105
+ class LocalScheduler:
106
+ def __init__(self, max_pauses: int = 2, other_stale_seconds: float = 600.0, max_queue: int = 200):
107
+ self.max_pauses = max_pauses
108
+ self.other_stale_seconds = other_stale_seconds
109
+ self.max_queue = max_queue
110
+ self._queues: dict[TaskClass, deque[_Job]] = {c: deque() for c in TaskClass}
111
+ self._cond = threading.Condition()
112
+ self._closed = False
113
+ self.history: deque[dict] = deque(maxlen=500)
114
+ self._thread = threading.Thread(target=self._worker, name="pal-local-scheduler", daemon=True)
115
+ self._thread.start()
116
+
117
+ # -- submitting -------------------------------------------------------------------------------------------
118
+ def submit(self, fn: Callable[[], Any], task_class: TaskClass = TaskClass.CHAT, label: str = "local job") -> Ticket:
119
+ """An atomic job: runs to the end, never paused."""
120
+ t = Ticket(label, task_class)
121
+ return self._enqueue(_AtomicJob(t, task_class, fn, self._deadline(task_class, None)))
122
+
123
+ def submit_steps(self, steps: Iterator[Any], task_class: TaskClass = TaskClass.OTHER, label: str = "local steps",
124
+ expires_after: float | None = None) -> Ticket:
125
+ """A job made of small pieces: `steps` yields after each piece; its return value is the result."""
126
+ t = Ticket(label, task_class)
127
+ return self._enqueue(_StepsJob(t, task_class, steps, self._deadline(task_class, expires_after)))
128
+
129
+ def submit_stream(self, core: LLMWrapper, syscall: LLMSyscall, task_class: TaskClass = TaskClass.CHAT,
130
+ label: str = "local answer") -> Ticket:
131
+ """A plain-text generation that can be paused and resumed. The result is the whole text."""
132
+ t = Ticket(label, task_class)
133
+ return self._enqueue(_StreamJob(t, task_class, core, syscall))
134
+
135
+ def _deadline(self, cls: TaskClass, expires_after: float | None) -> float | None:
136
+ if expires_after is None and cls == TaskClass.OTHER:
137
+ expires_after = self.other_stale_seconds
138
+ return time.monotonic() + expires_after if expires_after else None
139
+
140
+ def _enqueue(self, job: _Job) -> Ticket:
141
+ with self._cond:
142
+ if self._closed:
143
+ raise SchedulerError("the scheduler is closed")
144
+ if sum(len(q) for q in self._queues.values()) >= self.max_queue:
145
+ raise QueueFullError(f"too many local jobs are waiting ({self.max_queue})")
146
+ self._queues[job.cls].append(job)
147
+ self._cond.notify()
148
+ return job.ticket
149
+
150
+ def pending(self) -> dict[str, int]:
151
+ with self._cond:
152
+ return {c.name.lower(): len(q) for c, q in self._queues.items()}
153
+
154
+ # -- the worker ----------------------------------------------------------------------------------------------
155
+ def _higher_waiting(self, cls: TaskClass) -> bool:
156
+ with self._cond:
157
+ return any(self._queues[c] for c in TaskClass if c < cls)
158
+
159
+ def _next_locked(self) -> _Job | None:
160
+ for cls in TaskClass:
161
+ q = self._queues[cls]
162
+ while q:
163
+ job = q.popleft()
164
+ if job.deadline is not None and time.monotonic() > job.deadline and job.ticket.started is None:
165
+ job.ticket.set_error(ExpiredError(f"{job.ticket.label} waited too long and was dropped"))
166
+ continue
167
+ return job
168
+ return None
169
+
170
+ def _worker(self) -> None:
171
+ while True:
172
+ with self._cond:
173
+ job = self._next_locked()
174
+ while job is None:
175
+ if self._closed:
176
+ return
177
+ self._cond.wait(timeout=1.0)
178
+ job = self._next_locked()
179
+ self._run(job)
180
+
181
+ def _run(self, job: _Job) -> None:
182
+ t = job.ticket
183
+ if t.started is None:
184
+ t.started = time.monotonic()
185
+ began = time.monotonic()
186
+
187
+ def should_stop() -> bool:
188
+ return job.preemptible and t.pauses < self.max_pauses and self._higher_waiting(job.cls)
189
+
190
+ try:
191
+ finished = job.run(should_stop)
192
+ except BaseException as e: # noqa: BLE001 (the error belongs to whoever submitted the job)
193
+ t.run_seconds += time.monotonic() - began
194
+ t.set_error(e)
195
+ return
196
+ t.run_seconds += time.monotonic() - began
197
+ if finished:
198
+ self.history.append({"label": t.label, "class": job.cls.name, "waited": round(t.waited, 4),
199
+ "ran": round(t.run_seconds, 4), "pauses": t.pauses})
200
+ t.set_result(job.result)
201
+ return
202
+ t.pauses += 1
203
+ log.info("paused %s (%s) for a higher-priority job; pause %d", t.label, job.cls.name, t.pauses)
204
+ with self._cond:
205
+ self._queues[job.cls].appendleft(job) # resumes before later arrivals of its own class
206
+
207
+ def close(self) -> None:
208
+ with self._cond:
209
+ self._closed = True
210
+ dropped = [j for q in self._queues.values() for j in q]
211
+ for q in self._queues.values():
212
+ q.clear()
213
+ self._cond.notify_all()
214
+ for j in dropped:
215
+ j.ticket.set_error(SchedulerError("the scheduler was closed"))
@@ -0,0 +1,42 @@
1
+ """The scheduler as one object: the local model's preemptive priority scheduler, the API model's first-come-first-served
2
+ lines, and the slots for code work."""
3
+ from __future__ import annotations
4
+
5
+ from pal.config import SchedulerConfig
6
+ from pal.llm_scheduler import core as _core
7
+ from pal.llm_scheduler.api import ApiScheduler
8
+ from pal.llm_scheduler.code import CodeSlots
9
+ from pal.llm_scheduler.core import ScheduledWrapper
10
+ from pal.llm_scheduler.local import LocalScheduler
11
+ from pal.llm_wrapper.components.base import LLMWrapper
12
+
13
+
14
+ class LLMScheduler:
15
+ def __init__(self, cfg: SchedulerConfig | dict | None = None):
16
+ self.cfg = SchedulerConfig.coerce(cfg)
17
+ c = self.cfg
18
+ self.local = LocalScheduler(c.local_max_pauses, c.other_stale_seconds, c.max_queue) if c.enabled else None
19
+ self.api = ApiScheduler(c.api_retry_429, c.api_backoff_seconds, c.max_queue, enabled=c.enabled)
20
+ self.code = CodeSlots(c.code_workers if c.enabled else 0)
21
+ self.task = _core.task
22
+ self.track = _core.track
23
+
24
+ def wrap_local(self, core: LLMWrapper | None) -> LLMWrapper | None:
25
+ if core is None or isinstance(core, ScheduledWrapper):
26
+ return core
27
+ return ScheduledWrapper(core, self.local)
28
+
29
+ def interval_for(self, provider: str, model: str = "") -> float:
30
+ """Seconds between call starts on this provider's line. OpenRouter free models are limited to about 20 a minute."""
31
+ if provider == "openrouter" and model.endswith(":free"):
32
+ return float(self.cfg.free_model_interval)
33
+ return float(self.cfg.api_min_interval.get(provider, 0.0))
34
+
35
+ def snapshot(self) -> dict:
36
+ return {"enabled": self.cfg.enabled, "local_waiting": self.local.pending() if self.local else {},
37
+ "api_waiting": self.api.pending()}
38
+
39
+ def close(self) -> None:
40
+ if self.local:
41
+ self.local.close()
42
+ self.api.close()
@@ -0,0 +1,92 @@
1
+ """Shared types for the scheduler: who asked (task class), a handle on a submitted job (ticket), and per-turn statistics."""
2
+ from __future__ import annotations
3
+
4
+ import threading
5
+ import time
6
+ from enum import IntEnum
7
+ from typing import Any
8
+
9
+ from pal.errors import PalError
10
+
11
+
12
+ class TaskClass(IntEnum):
13
+ """Who asked, which decides local-model priority. A lower number is served first."""
14
+
15
+ AGENT = 0 # preparation for a request sent to an agent
16
+ CHAT = 1 # preparation and easy answers for plain chat
17
+ OTHER = 2 # background work nobody is waiting for
18
+
19
+
20
+ class SchedulerError(PalError):
21
+ http_status = 503
22
+ public = True
23
+
24
+
25
+ class QueueFullError(SchedulerError):
26
+ """Too many jobs are already waiting."""
27
+
28
+
29
+ class ExpiredError(SchedulerError):
30
+ """A background job waited too long and was dropped."""
31
+
32
+
33
+ class Ticket:
34
+ """Returned by every submit. `result()` waits for the job; the other fields say how it went."""
35
+
36
+ def __init__(self, label: str, task_class: TaskClass | None = None):
37
+ self.label = label
38
+ self.task_class = task_class
39
+ self.submitted = time.monotonic()
40
+ self.started: float | None = None # first time the job began to run
41
+ self.run_seconds = 0.0 # time actually running (all slices added up)
42
+ self.pauses = 0 # times a higher-priority job paused it
43
+ self._done = threading.Event()
44
+ self._result: Any = None
45
+ self._error: BaseException | None = None
46
+
47
+ @property
48
+ def waited(self) -> float:
49
+ """Seconds between submitting and first starting (what the user feels as queueing)."""
50
+ return (self.started - self.submitted) if self.started is not None else time.monotonic() - self.submitted
51
+
52
+ @property
53
+ def done(self) -> bool:
54
+ return self._done.is_set()
55
+
56
+ @property
57
+ def error(self) -> BaseException | None:
58
+ return self._error
59
+
60
+ def set_result(self, value: Any) -> None:
61
+ self._result = value
62
+ self._done.set()
63
+
64
+ def set_error(self, error: BaseException) -> None:
65
+ self._error = error
66
+ self._done.set()
67
+
68
+ def result(self, timeout: float | None = None) -> Any:
69
+ if not self._done.wait(timeout):
70
+ raise TimeoutError(f"{self.label} did not finish within {timeout} s")
71
+ if self._error is not None:
72
+ raise self._error
73
+ return self._result
74
+
75
+
76
+ class TurnStats:
77
+ """Queueing numbers for one chat turn, filled in by the scheduler as the turn's jobs finish."""
78
+
79
+ def __init__(self) -> None:
80
+ self._lock = threading.Lock()
81
+ self.wait_local = 0.0
82
+ self.wait_api = 0.0
83
+ self.local_pauses = 0
84
+
85
+ def add_local(self, t: Ticket) -> None:
86
+ with self._lock:
87
+ self.wait_local += t.waited
88
+ self.local_pauses += t.pauses
89
+
90
+ def add_api(self, t: Ticket) -> None:
91
+ with self._lock:
92
+ self.wait_api += t.waited
@@ -0,0 +1,34 @@
1
+ from pal.llm_wrapper.adapter import LLMAdapter
2
+ from pal.llm_wrapper.components.adapter_types import (
3
+ LLMAdapterResponse,
4
+ LLMAdapterSyscall,
5
+ LLMConfig,
6
+ RouterStrategy,
7
+ )
8
+ from pal.llm_wrapper.components.base import (
9
+ DeploymentType,
10
+ LLMResponse,
11
+ LLMSyscall,
12
+ LLMWrapper,
13
+ WrapperCapabilities,
14
+ WrapperConfig,
15
+ )
16
+ from pal.llm_wrapper.pipeline import LLMWrapperPipeline
17
+ from pal.llm_wrapper.utils.registry import available_backends, build_wrapper
18
+
19
+ __all__ = [
20
+ "WrapperCapabilities",
21
+ "WrapperConfig",
22
+ "DeploymentType",
23
+ "LLMAdapter",
24
+ "LLMAdapterResponse",
25
+ "LLMAdapterSyscall",
26
+ "LLMConfig",
27
+ "LLMWrapper",
28
+ "LLMWrapperPipeline",
29
+ "LLMResponse",
30
+ "LLMSyscall",
31
+ "RouterStrategy",
32
+ "available_backends",
33
+ "build_wrapper",
34
+ ]