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