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 json |
| #4 | from dataclasses import replace |
| #5 | from typing import Any |
| #6 | |
| #7 | from agent_libos.config import DEFAULT_CONFIG |
| #8 | from agent_libos.utils.ids import estimate_tokens, utc_now |
| #9 | from agent_libos.models import ( |
| #10 | AgentImage, |
| #11 | AgentObject, |
| #12 | AgentProcess, |
| #13 | Capability, |
| #14 | Event, |
| #15 | MaterializedContext, |
| #16 | MemoryView, |
| #17 | ObjectHandle, |
| #18 | ObjectMetadata, |
| #19 | ObjectPatch, |
| #20 | ObjectRight, |
| #21 | ObjectType, |
| #22 | ViewMode, |
| #23 | ) |
| #24 | |
| #25 | _LLM_CONTEXT_DEFAULTS = DEFAULT_CONFIG.llm_context |
| #26 | LLM_CONTEXT_POLICY = _LLM_CONTEXT_DEFAULTS.policy |
| #27 | LLM_CONTEXT_SCHEMA_VERSION = _LLM_CONTEXT_DEFAULTS.schema_version |
| #28 | |
| #29 | |
| #30 | class LLMContextMemory: |
| #31 | """Maintains the prompt context as a mutable, append-only Object Memory object.""" |
| #32 | |
| #33 | def __init__(self, runtime: Any): |
| #34 | self.runtime = runtime |
| #35 | |
| #36 | def prepare( |
| #37 | self, |
| #38 | pid: str, |
| #39 | image: AgentImage, |
| #40 | process: AgentProcess, |
| #41 | source_context: MaterializedContext, |
| #42 | events: list[Event], |
| #43 | capabilities: list[Capability], |
| #44 | tools: list[dict[str, Any]], |
| #45 | ) -> MaterializedContext: |
| #46 | handle = self.ensure(pid, image, process, tools) |
| #47 | obj = self.runtime.memory.get_object(pid, handle) |
| #48 | payload = self._payload(obj) |
| #49 | changed = self._append_deltas( |
| #50 | payload=payload, |
| #51 | process=process, |
| #52 | image=image, |
| #53 | source_context=source_context, |
| #54 | events=events, |
| #55 | capabilities=capabilities, |
| #56 | tools=tools, |
| #57 | ) |
| #58 | if changed: |
| #59 | metadata = ObjectMetadata( |
| #60 | title=f"LLM context for {pid}", |
| #61 | summary="Append-only process prompt context optimized for prompt caching.", |
| #62 | tags=["llm_context", "prompt_cache"], |
| #63 | token_estimate=estimate_tokens(payload), |
| #64 | ) |
| #65 | self.runtime.memory.update_object( |
| #66 | pid, |
| #67 | handle, |
| #68 | ObjectPatch(payload=payload, metadata=metadata), |
| #69 | ) |
| #70 | obj = self.runtime.memory.get_object(pid, handle) |
| #71 | rendered = self.render(obj.payload) |
| #72 | return MaterializedContext( |
| #73 | text=rendered, |
| #74 | object_refs=[obj.oid, *source_context.object_refs], |
| #75 | token_count=estimate_tokens(rendered), |
| #76 | omitted_objects=source_context.omitted_objects, |
| #77 | policy_used=LLM_CONTEXT_POLICY, |
| #78 | ) |
| #79 | |
| #80 | def ensure(self, pid: str, image: AgentImage, process: AgentProcess, tools: list[dict[str, Any]]) -> ObjectHandle: |
| #81 | name = context_object_name(pid) |
| #82 | namespace = self.runtime.memory.resolve_namespace(pid) |
| #83 | existing = self.runtime.store.get_object_by_name(name, namespace=namespace) |
| #84 | rights = { |
| #85 | ObjectRight.READ.value, |
| #86 | ObjectRight.WRITE.value, |
| #87 | ObjectRight.MATERIALIZE.value, |
| #88 | ObjectRight.LINK.value, |
| #89 | ObjectRight.DIFF.value, |
| #90 | } |
| #91 | if existing is None: |
| #92 | handle = self.runtime.memory.create_object( |
| #93 | pid=pid, |
| #94 | object_type=ObjectType.PROCESS_STATE, |
| #95 | payload=self._initial_payload(pid, image, process, tools), |
| #96 | metadata=ObjectMetadata( |
| #97 | title=f"LLM context for {pid}", |
| #98 | summary="Append-only process prompt context optimized for prompt caching.", |
| #99 | tags=["llm_context", "prompt_cache"], |
| #100 | ), |
| #101 | immutable=False, |
| #102 | name=name, |
| #103 | ) |
| #104 | else: |
| #105 | handle = self.runtime.capability.handle_for_object( |
| #106 | pid, |
| #107 | existing.oid, |
| #108 | rights, |
| #109 | issued_by="llm.context", |
| #110 | ) |
| #111 | self._add_handle_to_view(pid, handle) |
| #112 | return handle |
| #113 | |
| #114 | def view_without_context(self, pid: str, view: MemoryView) -> MemoryView: |
| #115 | context_oid = self._context_oid(pid) |
| #116 | if context_oid is None: |
| #117 | roots = list(view.roots) |
| #118 | else: |
| #119 | roots = [handle for handle in view.roots if handle.oid != context_oid] |
| #120 | return replace(view, roots=roots) |
| #121 | |
| #122 | def render(self, payload: dict[str, Any]) -> str: |
| #123 | static_prefix = payload.get("static_prefix", {}) |
| #124 | lines = [ |
| #125 | "LLM context object:", |
| #126 | "Cache strategy: append_only_stable_prefix", |
| #127 | "The static prefix below should remain stable. New process facts are appended after it.", |
| #128 | "", |
| #129 | "Static prefix:", |
| #130 | _stable_json(static_prefix), |
| #131 | "", |
| #132 | "Append-only entries:", |
| #133 | ] |
| #134 | for entry in payload.get("entries", []): |
| #135 | lines.append("") |
| #136 | lines.append("---") |
| #137 | lines.append(_stable_json(entry)) |
| #138 | return "\n".join(lines).rstrip() |
| #139 | |
| #140 | def _initial_payload( |
| #141 | self, |
| #142 | pid: str, |
| #143 | image: AgentImage, |
| #144 | process: AgentProcess, |
| #145 | tools: list[dict[str, Any]], |
| #146 | ) -> dict[str, Any]: |
| #147 | return { |
| #148 | "kind": "llm_context", |
| #149 | "schema_version": LLM_CONTEXT_SCHEMA_VERSION, |
| #150 | "cache_strategy": { |
| #151 | "mode": "append_only_stable_prefix", |
| #152 | "reason": "Keep repeated instructions, tool names, and early process context at the front; append changes at the end.", |
| #153 | }, |
| #154 | "static_prefix": { |
| #155 | "pid": pid, |
| #156 | "image_id": image.image_id, |
| #157 | "image_name": image.name, |
| #158 | "image_version": image.version, |
| #159 | "safety_profile": image.safety_profile, |
| #160 | "context_policy": image.context_policy, |
| #161 | "parent_pid": process.parent_pid, |
| #162 | "initial_working_directory": process.working_directory, |
| #163 | "goal_oid": process.goal_oid, |
| #164 | "tool_names": sorted(_tool_name(tool) for tool in tools if _tool_name(tool)), |
| #165 | }, |
| #166 | "entries": [ |
| #167 | { |
| #168 | "kind": "process_started", |
| #169 | "at": process.created_at, |
| #170 | "pid": pid, |
| #171 | "parent_pid": process.parent_pid, |
| #172 | "goal_oid": process.goal_oid, |
| #173 | "working_directory": process.working_directory, |
| #174 | } |
| #175 | ], |
| #176 | "captured": { |
| #177 | "object_oids": [], |
| #178 | "event_ids": [], |
| #179 | "capability_signature": None, |
| #180 | "tool_signature": _tool_signature(tools), |
| #181 | "process_signature": None, |
| #182 | }, |
| #183 | } |
| #184 | |
| #185 | def _append_deltas( |
| #186 | self, |
| #187 | payload: dict[str, Any], |
| #188 | process: AgentProcess, |
| #189 | image: AgentImage, |
| #190 | source_context: MaterializedContext, |
| #191 | events: list[Event], |
| #192 | capabilities: list[Capability], |
| #193 | tools: list[dict[str, Any]], |
| #194 | ) -> bool: |
| #195 | changed = False |
| #196 | captured = payload.setdefault("captured", {}) |
| #197 | entries = payload.setdefault("entries", []) |
| #198 | |
| #199 | process_signature = { |
| #200 | "status": process.status.value, |
| #201 | "status_message": process.status_message, |
| #202 | "checkpoint_head": process.checkpoint_head, |
| #203 | "image_id": image.image_id, |
| #204 | "working_directory": process.working_directory, |
| #205 | } |
| #206 | if captured.get("process_signature") != process_signature: |
| #207 | entries.append({"kind": "process_snapshot", "at": utc_now(), **process_signature}) |
| #208 | captured["process_signature"] = process_signature |
| #209 | changed = True |
| #210 | |
| #211 | capability_signature = _capability_signature(capabilities) |
| #212 | if captured.get("capability_signature") != capability_signature: |
| #213 | entries.append({"kind": "capabilities_snapshot", "at": utc_now(), "capabilities": capability_signature}) |
| #214 | captured["capability_signature"] = capability_signature |
| #215 | changed = True |
| #216 | |
| #217 | tool_signature = _tool_signature(tools) |
| #218 | if captured.get("tool_signature") != tool_signature: |
| #219 | entries.append({"kind": "tool_table_snapshot", "at": utc_now(), "tools": tool_signature}) |
| #220 | captured["tool_signature"] = tool_signature |
| #221 | changed = True |
| #222 | |
| #223 | captured_events = set(captured.get("event_ids", [])) |
| #224 | new_events = [ |
| #225 | event for event in events[-_LLM_CONTEXT_DEFAULTS.recent_event_limit :] if event.event_id not in captured_events |
| #226 | ] |
| #227 | if new_events: |
| #228 | entries.append( |
| #229 | { |
| #230 | "kind": "events_delta", |
| #231 | "at": utc_now(), |
| #232 | "events": [ |
| #233 | { |
| #234 | "event_id": event.event_id, |
| #235 | "type": event.type.value, |
| #236 | "source": event.source, |
| #237 | "target": event.target, |
| #238 | "payload": event.payload, |
| #239 | } |
| #240 | for event in new_events |
| #241 | ], |
| #242 | } |
| #243 | ) |
| #244 | captured["event_ids"] = sorted(captured_events | {event.event_id for event in new_events}) |
| #245 | changed = True |
| #246 | |
| #247 | captured_oids = set(captured.get("object_oids", [])) |
| #248 | new_oids = [oid for oid in source_context.object_refs if oid not in captured_oids] |
| #249 | if new_oids: |
| #250 | entries.append( |
| #251 | { |
| #252 | "kind": "memory_delta", |
| #253 | "at": utc_now(), |
| #254 | "policy": source_context.policy_used, |
| #255 | "token_estimate": source_context.token_count, |
| #256 | "omitted_objects": list(source_context.omitted_objects), |
| #257 | "objects": [self._object_entry(oid) for oid in new_oids], |
| #258 | } |
| #259 | ) |
| #260 | captured["object_oids"] = sorted(captured_oids | set(new_oids)) |
| #261 | changed = True |
| #262 | |
| #263 | if source_context.omitted_objects: |
| #264 | omitted = sorted(set(source_context.omitted_objects)) |
| #265 | if captured.get("omitted_objects") != omitted: |
| #266 | entries.append({"kind": "context_omissions", "at": utc_now(), "omitted_objects": omitted}) |
| #267 | captured["omitted_objects"] = omitted |
| #268 | changed = True |
| #269 | |
| #270 | return changed |
| #271 | |
| #272 | def _object_entry(self, oid: str) -> dict[str, Any]: |
| #273 | obj = self.runtime.store.get_object(oid) |
| #274 | if obj is None: |
| #275 | return {"oid": oid, "missing": True} |
| #276 | return { |
| #277 | "oid": obj.oid, |
| #278 | "name": obj.name, |
| #279 | "type": obj.type.value, |
| #280 | "version": obj.version, |
| #281 | "title": obj.metadata.title, |
| #282 | "summary": obj.metadata.summary, |
| #283 | "payload": obj.payload, |
| #284 | } |
| #285 | |
| #286 | def _payload(self, obj: AgentObject) -> dict[str, Any]: |
| #287 | if not isinstance(obj.payload, dict) or obj.payload.get("kind") != "llm_context": |
| #288 | raise ValueError(f"object is not an LLM context object: {obj.name}") |
| #289 | return obj.payload |
| #290 | |
| #291 | def _context_oid(self, pid: str) -> str | None: |
| #292 | obj = self.runtime.store.get_object_by_name( |
| #293 | context_object_name(pid), |
| #294 | namespace=self.runtime.memory.resolve_namespace(pid), |
| #295 | ) |
| #296 | return obj.oid if obj is not None else None |
| #297 | |
| #298 | def _add_handle_to_view(self, pid: str, handle: ObjectHandle) -> None: |
| #299 | process = self.runtime.process.get(pid) |
| #300 | if process.memory_view is None: |
| #301 | process.memory_view = self.runtime.memory.create_view(pid, [handle], mode=ViewMode.MUTABLE) |
| #302 | elif all(existing.oid != handle.oid for existing in process.memory_view.roots): |
| #303 | process.memory_view.roots.insert(0, handle) |
| #304 | else: |
| #305 | process.memory_view.roots = [ |
| #306 | handle if existing.oid == handle.oid and "write" not in existing.rights else existing |
| #307 | for existing in process.memory_view.roots |
| #308 | ] |
| #309 | process.updated_at = utc_now() |
| #310 | self.runtime.store.update_process(process) |
| #311 | |
| #312 | |
| #313 | def context_object_name(pid: str) -> str: |
| #314 | return f"{_LLM_CONTEXT_DEFAULTS.object_name_prefix}:{pid}" |
| #315 | |
| #316 | |
| #317 | def _tool_name(tool: dict[str, Any]) -> str | None: |
| #318 | spec_json = tool.get("spec_json") |
| #319 | if isinstance(spec_json, str): |
| #320 | try: |
| #321 | spec = json.loads(spec_json) |
| #322 | if isinstance(spec, dict): |
| #323 | return str(spec.get("name") or tool.get("name") or "") |
| #324 | except json.JSONDecodeError: |
| #325 | pass |
| #326 | return str(tool.get("name") or "") or None |
| #327 | |
| #328 | |
| #329 | def _tool_signature(tools: list[dict[str, Any]]) -> list[dict[str, Any]]: |
| #330 | result: list[dict[str, Any]] = [] |
| #331 | for tool in tools: |
| #332 | spec_json = tool.get("spec_json") |
| #333 | spec = {} |
| #334 | if isinstance(spec_json, str): |
| #335 | try: |
| #336 | decoded = json.loads(spec_json) |
| #337 | if isinstance(decoded, dict): |
| #338 | spec = decoded |
| #339 | except json.JSONDecodeError: |
| #340 | spec = {} |
| #341 | name = str(spec.get("name") or tool.get("name") or "") |
| #342 | if not name: |
| #343 | continue |
| #344 | result.append( |
| #345 | { |
| #346 | "name": name, |
| #347 | "description": spec.get("description", ""), |
| #348 | "tags": spec.get("tags", []), |
| #349 | "policy": spec.get("policy", {}), |
| #350 | "input_schema": spec.get("input_schema", {}), |
| #351 | } |
| #352 | ) |
| #353 | return sorted(result, key=lambda item: item["name"]) |
| #354 | |
| #355 | |
| #356 | def _capability_signature(capabilities: list[Capability]) -> list[dict[str, Any]]: |
| #357 | return sorted( |
| #358 | [ |
| #359 | { |
| #360 | "resource": cap.resource, |
| #361 | "rights": sorted(cap.rights), |
| #362 | "permission_policy": cap.constraints.get("permission_policy", "always_allow"), |
| #363 | "expires_at": cap.expires_at, |
| #364 | } |
| #365 | for cap in capabilities |
| #366 | if not cap.revoked |
| #367 | ], |
| #368 | key=lambda item: (item["resource"], ",".join(item["rights"])), |
| #369 | ) |
| #370 | |
| #371 | |
| #372 | def _stable_json(value: Any) -> str: |
| #373 | return json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2, default=str) |
| #374 |