repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
Mirrored from https://github.com/yingqi-z20/Agent-libOS
stars
latest
clone command
git clone gitlawb://did:key:z6MkqRzA...RfoM/yingqi-z20-Agen...git clone gitlawb://did:key:z6MkqRzA.../yingqi-z20-Agen...d98dd2c9IPC1d ago| #1 | from __future__ import annotations |
| #2 | |
| #3 | import asyncio |
| #4 | from pathlib import Path |
| #5 | from typing import Any |
| #6 | |
| #7 | from agent_libos.capability.manager import CapabilityManager |
| #8 | from agent_libos.config import DEFAULT_CONFIG, AgentLibOSConfig |
| #9 | from agent_libos.primitives import ClockPrimitive, FilesystemAdapter, ShellAdapter |
| #10 | from agent_libos.human.manager import HumanObjectManager |
| #11 | from agent_libos.images import build_default_images |
| #12 | from agent_libos.llm.client import LLMClient |
| #13 | from agent_libos.llm.executor import LLMProcessExecutor |
| #14 | from agent_libos.memory.object_memory import ObjectMemoryManager |
| #15 | from agent_libos.models import AgentImage |
| #16 | from agent_libos.models.exceptions import NotFound |
| #17 | from agent_libos.runtime.audit_manager import AuditManager |
| #18 | from agent_libos.runtime.checkpoint_manager import CheckpointManager |
| #19 | from agent_libos.runtime.event_bus import EventBus |
| #20 | from agent_libos.runtime.image_registry import ImageRegistryPrimitive |
| #21 | from agent_libos.runtime.message_manager import ProcessMessageManager |
| #22 | from agent_libos.runtime.process_manager import ProcessManager |
| #23 | from agent_libos.runtime.scheduler import SimpleScheduler |
| #24 | from agent_libos.skills.linker import SkillLinker |
| #25 | from agent_libos.skills.registry import RuntimeSkillRegistry |
| #26 | from agent_libos.storage import SQLiteStore |
| #27 | from agent_libos.substrate import LocalResourceProviderSubstrate, ResourceProviderSubstrate |
| #28 | from agent_libos.tools.broker import ToolBroker |
| #29 | from agent_libos.tools.builtin import ( |
| #30 | AskHumanTool, |
| #31 | AppendMemoryObjectTool, |
| #32 | CreateMemoryNamespaceTool, |
| #33 | CreateMemoryObjectTool, |
| #34 | CreateObjectFromFileTool, |
| #35 | DeleteDirectoryTool, |
| #36 | DeleteFileTool, |
| #37 | EchoTool, |
| #38 | ExecProcessTool, |
| #39 | ForkChildProcessTool, |
| #40 | GetCurrentTimeTool, |
| #41 | GetWorkingDirectoryTool, |
| #42 | HumanOutputTool, |
| #43 | LoadImageFromYamlTool, |
| #44 | ListChildProcessesTool, |
| #45 | ListMemoryNamespaceTool, |
| #46 | MergeChildMemoryTool, |
| #47 | ParsePytestLogTool, |
| #48 | ProcessExitTool, |
| #49 | ProposeJitTool, |
| #50 | ReadDirectoryTool, |
| #51 | ReadMemoryObjectTool, |
| #52 | ReadProcessMessagesTool, |
| #53 | ReceiveProcessMessagesTool, |
| #54 | ReadTextFileTool, |
| #55 | RequestPermissionTool, |
| #56 | RegisterJitTool, |
| #57 | RunShellCommandTool, |
| #58 | SendProcessMessageTool, |
| #59 | SignalChildProcessTool, |
| #60 | SleepTool, |
| #61 | SpawnChildProcessTool, |
| #62 | SetWorkingDirectoryTool, |
| #63 | ValidateJitTool, |
| #64 | WaitChildProcessTool, |
| #65 | WriteDirectoryTool, |
| #66 | WriteObjectToFileTool, |
| #67 | WriteTextFileTool, |
| #68 | ) |
| #69 | |
| #70 | _RUNTIME_DEFAULTS = DEFAULT_CONFIG.runtime |
| #71 | |
| #72 | |
| #73 | class Runtime: |
| #74 | """Composition root for the MVP libOS runtime.""" |
| #75 | |
| #76 | def __init__( |
| #77 | self, |
| #78 | store: SQLiteStore, |
| #79 | llm_client: LLMClient | None = None, |
| #80 | substrate: ResourceProviderSubstrate | None = None, |
| #81 | config: AgentLibOSConfig | None = None, |
| #82 | ): |
| #83 | self.config = config or DEFAULT_CONFIG |
| #84 | self.substrate = substrate or LocalResourceProviderSubstrate( |
| #85 | Path.cwd().resolve(), |
| #86 | namespace=self.config.runtime.workspace_namespace, |
| #87 | ) |
| #88 | self.workspace_root = Path(getattr(self.substrate, "workspace_root", self.substrate.workspace_display)) |
| #89 | self.store = store |
| #90 | self.audit = AuditManager(store) |
| #91 | self.events = EventBus(store) |
| #92 | self.capability = CapabilityManager(store, self.audit, self.events, config=self.config) |
| #93 | self.memory = ObjectMemoryManager(store, self.capability, self.audit, self.events, config=self.config) |
| #94 | self.human = HumanObjectManager( |
| #95 | store, |
| #96 | self.capability, |
| #97 | self.audit, |
| #98 | self.events, |
| #99 | provider=self.substrate.human, |
| #100 | config=self.config, |
| #101 | ) |
| #102 | self.messages = ProcessMessageManager(store, self.audit, self.events) |
| #103 | self.human.bind_messages(self.messages) |
| #104 | self.clock = ClockPrimitive( |
| #105 | self.audit, |
| #106 | self.events, |
| #107 | max_sleep_seconds=self.config.tools.max_sleep_seconds, |
| #108 | provider=self.substrate.clock, |
| #109 | ) |
| #110 | self.filesystem = FilesystemAdapter( |
| #111 | self.capability, |
| #112 | self.audit, |
| #113 | self.events, |
| #114 | human=self.human, |
| #115 | provider=self.substrate.filesystem, |
| #116 | ) |
| #117 | self.shell = ShellAdapter( |
| #118 | self.capability, |
| #119 | self.audit, |
| #120 | self.events, |
| #121 | human=self.human, |
| #122 | provider=self.substrate.shell, |
| #123 | config=self.config, |
| #124 | ) |
| #125 | self.tools = ToolBroker( |
| #126 | store, |
| #127 | self.memory, |
| #128 | self.capability, |
| #129 | self.human, |
| #130 | self.audit, |
| #131 | self.events, |
| #132 | workspace_root=self.workspace_root, |
| #133 | config=self.config, |
| #134 | ) |
| #135 | self.tools.runtime = self |
| #136 | self.process = ProcessManager(store, self.memory, self.capability, self.audit, self.events, config=self.config) |
| #137 | self.scheduler = SimpleScheduler(store, self.audit, poll_interval_s=self.config.scheduler.poll_interval_s) |
| #138 | self.checkpoint = CheckpointManager(store, self.audit, self.events) |
| #139 | self.skill_registry = RuntimeSkillRegistry() |
| #140 | self.skills = SkillLinker(store, self.skill_registry, self.audit) |
| #141 | self.images: dict[str, AgentImage] = build_default_images(self.config) |
| #142 | self.image_registry = ImageRegistryPrimitive( |
| #143 | self.images, |
| #144 | self.capability, |
| #145 | self.audit, |
| #146 | self.events, |
| #147 | self.tools.resolve, |
| #148 | config=self.config, |
| #149 | ) |
| #150 | self.llm = LLMProcessExecutor(self, llm_client, config=self.config) |
| #151 | self._current_human_auto_approve: bool | None = None |
| #152 | self._current_human_auto_policy: str | None = None |
| #153 | self._current_human_auto_answer: str | None = None |
| #154 | self._register_builtin_tools() |
| #155 | self.process.add_after_spawn_hook(self._configure_process_tools_and_capabilities) |
| #156 | |
| #157 | @classmethod |
| #158 | def open( |
| #159 | cls, |
| #160 | target: str | Path = _RUNTIME_DEFAULTS.local_store_target, |
| #161 | substrate: ResourceProviderSubstrate | None = None, |
| #162 | config: AgentLibOSConfig | None = None, |
| #163 | ) -> "Runtime": |
| #164 | selected_config = config or DEFAULT_CONFIG |
| #165 | if str(target) == selected_config.runtime.local_store_target: |
| #166 | return cls(SQLiteStore(":memory:"), substrate=substrate, config=selected_config) |
| #167 | return cls(SQLiteStore(str(target)), substrate=substrate, config=selected_config) |
| #168 | |
| #169 | def close(self) -> None: |
| #170 | self.store.close() |
| #171 | |
| #172 | def run_process_once(self, pid: str) -> dict[str, Any]: |
| #173 | return self.llm.run_once(pid) |
| #174 | |
| #175 | async def arun_process_once(self, pid: str) -> dict[str, Any]: |
| #176 | return await self.llm.arun_once(pid) |
| #177 | |
| #178 | def run_next_process_once(self) -> Any: |
| #179 | return self.scheduler.run_once(self.arun_process_once) |
| #180 | |
| #181 | async def arun_next_process_once(self) -> Any: |
| #182 | return await self.scheduler.arun_once(self.arun_process_once) |
| #183 | |
| #184 | def run_until_idle( |
| #185 | self, |
| #186 | max_quanta: int | None = None, |
| #187 | *, |
| #188 | process_human_queue: bool = True, |
| #189 | human: str | None = None, |
| #190 | human_auto_approve: bool | None = None, |
| #191 | human_auto_policy: str | None = None, |
| #192 | human_auto_answer: str | None = None, |
| #193 | ) -> list[Any]: |
| #194 | try: |
| #195 | asyncio.get_running_loop() |
| #196 | except RuntimeError: |
| #197 | return asyncio.run( |
| #198 | self.arun_until_idle( |
| #199 | max_quanta=max_quanta, |
| #200 | process_human_queue=process_human_queue, |
| #201 | human=human, |
| #202 | human_auto_approve=human_auto_approve, |
| #203 | human_auto_policy=human_auto_policy, |
| #204 | human_auto_answer=human_auto_answer, |
| #205 | ) |
| #206 | ) |
| #207 | raise RuntimeError("Cannot call run_until_idle() inside a running event loop. Use await arun_until_idle(...).") |
| #208 | |
| #209 | async def arun_until_idle( |
| #210 | self, |
| #211 | max_quanta: int | None = None, |
| #212 | *, |
| #213 | process_human_queue: bool = True, |
| #214 | human: str | None = None, |
| #215 | human_auto_approve: bool | None = None, |
| #216 | human_auto_policy: str | None = None, |
| #217 | human_auto_answer: str | None = None, |
| #218 | ) -> list[Any]: |
| #219 | results: list[Any] = [] |
| #220 | remaining = max_quanta if max_quanta is not None else self.config.runtime.run_until_idle_max_quanta |
| #221 | selected_human = human or self.config.runtime.default_human |
| #222 | previous_human_context = ( |
| #223 | self._current_human_auto_approve, |
| #224 | self._current_human_auto_policy, |
| #225 | self._current_human_auto_answer, |
| #226 | ) |
| #227 | self._current_human_auto_approve = human_auto_approve |
| #228 | self._current_human_auto_policy = human_auto_policy |
| #229 | self._current_human_auto_answer = human_auto_answer |
| #230 | try: |
| #231 | while remaining > 0: |
| #232 | # Run all currently runnable processes first. Human queue work below |
| #233 | # may wake a process, so this loop intentionally alternates between |
| #234 | # process execution and terminal queue draining. |
| #235 | batch = await self.scheduler.arun_until_idle(self.arun_process_once, max_quanta=remaining) |
| #236 | results.extend(batch) |
| #237 | remaining -= len(batch) |
| #238 | if not process_human_queue: |
| #239 | break |
| #240 | processed = await self.human.adrain_terminal_queue( |
| #241 | human=selected_human, |
| #242 | auto_approve=human_auto_approve, |
| #243 | auto_policy=human_auto_policy, |
| #244 | auto_answer=human_auto_answer, |
| #245 | ) |
| #246 | if not processed: |
| #247 | break |
| #248 | self.audit.record( |
| #249 | actor="runtime", |
| #250 | action="runtime.human_queue_drained", |
| #251 | target=f"human:{selected_human}", |
| #252 | decision={"request_ids": [request.request_id for request in processed]}, |
| #253 | ) |
| #254 | await asyncio.sleep(0) |
| #255 | finally: |
| #256 | ( |
| #257 | self._current_human_auto_approve, |
| #258 | self._current_human_auto_policy, |
| #259 | self._current_human_auto_answer, |
| #260 | ) = previous_human_context |
| #261 | return results |
| #262 | |
| #263 | def register_image(self, image: AgentImage | dict[str, Any], *, actor: str = "runtime", replace: bool = False) -> None: |
| #264 | self.image_registry.register(image, actor=actor, replace=replace) |
| #265 | |
| #266 | def get_image(self, image_id: str) -> AgentImage: |
| #267 | return self.images[image_id] |
| #268 | |
| #269 | def exec_process( |
| #270 | self, |
| #271 | pid: str, |
| #272 | image: str, |
| #273 | *, |
| #274 | args: dict[str, Any] | None = None, |
| #275 | goal: dict[str, Any] | str | None = None, |
| #276 | preserve_memory: bool = True, |
| #277 | preserve_capabilities: bool = False, |
| #278 | ) -> Any: |
| #279 | self._require_image(image) |
| #280 | self.process.exec( |
| #281 | pid, |
| #282 | image, |
| #283 | args=args, |
| #284 | goal=goal, |
| #285 | preserve_memory=preserve_memory, |
| #286 | preserve_capabilities=preserve_capabilities, |
| #287 | ) |
| #288 | # Exec swaps the process image and tool table, but deliberately does not |
| #289 | # apply image required_capabilities. Exec may preserve existing |
| #290 | # capabilities or shrink them; it never grants new external authority. |
| #291 | self._configure_process_tools_for_image(pid, image, assigned_by=f"process.exec:{image}") |
| #292 | return self.process.get(pid) |
| #293 | |
| #294 | def spawn_child_process( |
| #295 | self, |
| #296 | parent: str, |
| #297 | goal: dict[str, Any] | str, |
| #298 | *, |
| #299 | image: str | None = None, |
| #300 | inherit_capabilities: list[dict[str, Any]] | None = None, |
| #301 | working_directory: str | None = None, |
| #302 | ) -> str: |
| #303 | parent_process = self.process.get(parent) |
| #304 | selected_image = image or parent_process.image_id |
| #305 | self._require_image(selected_image) |
| #306 | selected_cwd = ( |
| #307 | self.resolve_process_working_directory(parent, working_directory) |
| #308 | if working_directory is not None |
| #309 | else parent_process.working_directory |
| #310 | ) |
| #311 | return self.process.spawn_child( |
| #312 | parent=parent, |
| #313 | goal=goal, |
| #314 | image=selected_image, |
| #315 | inherit_capabilities=inherit_capabilities, |
| #316 | working_directory=selected_cwd, |
| #317 | ) |
| #318 | |
| #319 | def set_process_working_directory(self, pid: str, path: str) -> Any: |
| #320 | relative = self.resolve_process_working_directory(pid, path) |
| #321 | return self.process.set_working_directory(pid, relative) |
| #322 | |
| #323 | def resolve_process_working_directory(self, pid: str, path: str) -> str: |
| #324 | current_cwd = self.process.working_directory(pid) |
| #325 | target, relative = self.filesystem.resolve_path(path, cwd=current_cwd) |
| #326 | state = self.filesystem.provider.state(target) |
| #327 | if not state.exists: |
| #328 | raise NotFound(f"working directory does not exist: {relative}") |
| #329 | if state.kind != "directory": |
| #330 | raise NotFound(f"working directory is not a directory: {relative}") |
| #331 | return relative or "." |
| #332 | |
| #333 | def _configure_process_tools_and_capabilities(self, pid: str, image_id: str) -> None: |
| #334 | process = self.store.get_process(pid) |
| #335 | image = self.images.get(image_id) or self.images[self.config.runtime.default_image_id] |
| #336 | # Tool visibility is fixed from the AgentImage at process creation time. |
| #337 | # External-resource authority is still enforced later by the primitives. |
| #338 | try: |
| #339 | self._configure_process_tools_for_image(pid, image.image_id, assigned_by=f"image:{image_id}") |
| #340 | except Exception as exc: |
| #341 | self.audit.record( |
| #342 | actor="runtime", |
| #343 | action="image.default_tool_configure_failed", |
| #344 | target=f"process:{pid}", |
| #345 | decision={"image": image_id, "error": str(exc)}, |
| #346 | ) |
| #347 | if process is not None and process.parent_pid is not None: |
| #348 | self.audit.record( |
| #349 | actor="runtime", |
| #350 | action="image.default_capability_skipped_for_child", |
| #351 | target=f"process:{pid}", |
| #352 | decision={"image": image_id, "parent_pid": process.parent_pid}, |
| #353 | ) |
| #354 | return |
| #355 | for spec in image.required_capabilities: |
| #356 | try: |
| #357 | self.capability.grant( |
| #358 | subject=pid, |
| #359 | resource=spec["resource"], |
| #360 | rights=spec.get("rights", []), |
| #361 | issued_by=f"image:{image_id}", |
| #362 | constraints=spec.get("constraints"), |
| #363 | expires_at=spec.get("expires_at"), |
| #364 | delegable=spec.get("delegable", False), |
| #365 | revocable=spec.get("revocable", True), |
| #366 | ) |
| #367 | except Exception as exc: |
| #368 | self.audit.record( |
| #369 | actor="runtime", |
| #370 | action="image.default_capability_grant_failed", |
| #371 | target=f"process:{pid}", |
| #372 | decision={"capability": spec, "error": str(exc)}, |
| #373 | ) |
| #374 | |
| #375 | def _require_image(self, image_id: str) -> AgentImage: |
| #376 | image = self.images.get(image_id) |
| #377 | if image is None: |
| #378 | raise NotFound(f"agent image not found: {image_id}") |
| #379 | return image |
| #380 | |
| #381 | def _configure_process_tools_for_image(self, pid: str, image_id: str, assigned_by: str) -> dict[str, str]: |
| #382 | image = self._require_image(image_id) |
| #383 | tool_names = {"process_exit", "create_memory_object", *image.default_tools} |
| #384 | return self.tools.configure_process_tools(pid, sorted(tool_names), assigned_by=assigned_by) |
| #385 | |
| #386 | def _register_builtin_tools(self) -> None: |
| #387 | self.tools.register_tool(EchoTool(), registered_by="runtime") |
| #388 | self.tools.register_tool(ExecProcessTool(), registered_by="runtime") |
| #389 | self.tools.register_tool(GetCurrentTimeTool(), registered_by="runtime") |
| #390 | self.tools.register_tool(SleepTool(), registered_by="runtime") |
| #391 | self.tools.register_tool(ParsePytestLogTool(), registered_by="runtime") |
| #392 | self.tools.register_tool(AppendMemoryObjectTool(), registered_by="runtime") |
| #393 | self.tools.register_tool(CreateMemoryNamespaceTool(), registered_by="runtime") |
| #394 | self.tools.register_tool(CreateMemoryObjectTool(), registered_by="runtime") |
| #395 | self.tools.register_tool(CreateObjectFromFileTool(), registered_by="runtime") |
| #396 | self.tools.register_tool(DeleteDirectoryTool(), registered_by="runtime") |
| #397 | self.tools.register_tool(DeleteFileTool(), registered_by="runtime") |
| #398 | self.tools.register_tool(ForkChildProcessTool(), registered_by="runtime") |
| #399 | self.tools.register_tool(GetWorkingDirectoryTool(), registered_by="runtime") |
| #400 | self.tools.register_tool(LoadImageFromYamlTool(), registered_by="runtime") |
| #401 | self.tools.register_tool(ListChildProcessesTool(), registered_by="runtime") |
| #402 | self.tools.register_tool(ListMemoryNamespaceTool(), registered_by="runtime") |
| #403 | self.tools.register_tool(MergeChildMemoryTool(), registered_by="runtime") |
| #404 | self.tools.register_tool(ProcessExitTool(), registered_by="runtime") |
| #405 | self.tools.register_tool(ProposeJitTool(), registered_by="runtime") |
| #406 | self.tools.register_tool(RequestPermissionTool(), registered_by="runtime") |
| #407 | self.tools.register_tool(ReadDirectoryTool(), registered_by="runtime") |
| #408 | self.tools.register_tool(ReadMemoryObjectTool(), registered_by="runtime") |
| #409 | self.tools.register_tool(ReadProcessMessagesTool(), registered_by="runtime") |
| #410 | self.tools.register_tool(ReceiveProcessMessagesTool(), registered_by="runtime") |
| #411 | self.tools.register_tool(ReadTextFileTool(), registered_by="runtime") |
| #412 | self.tools.register_tool(RegisterJitTool(), registered_by="runtime") |
| #413 | self.tools.register_tool(SignalChildProcessTool(), registered_by="runtime") |
| #414 | self.tools.register_tool(SetWorkingDirectoryTool(), registered_by="runtime") |
| #415 | self.tools.register_tool(SpawnChildProcessTool(), registered_by="runtime") |
| #416 | self.tools.register_tool(ValidateJitTool(), registered_by="runtime") |
| #417 | self.tools.register_tool(WriteObjectToFileTool(), registered_by="runtime") |
| #418 | self.tools.register_tool(WriteDirectoryTool(), registered_by="runtime") |
| #419 | self.tools.register_tool(WriteTextFileTool(), registered_by="runtime") |
| #420 | self.tools.register_tool(AskHumanTool(), registered_by="runtime") |
| #421 | self.tools.register_tool(HumanOutputTool(), registered_by="runtime") |
| #422 | self.tools.register_tool(WaitChildProcessTool(), registered_by="runtime") |
| #423 | self.tools.register_tool(RunShellCommandTool(), registered_by="runtime") |
| #424 | self.tools.register_tool(SendProcessMessageTool(), registered_by="runtime") |
| #425 |