aios-runtime 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.
aios/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ """
2
+ Ai.os — Persistent agent runtime.
3
+ Deploy agents that remember, survive crashes, and run forever.
4
+ """
5
+
6
+ from .agent import Agent
7
+ from .scheduling import schedule
8
+ from .tools.builtin import FilesystemMixin, GitHubMixin, HttpMixin, ShellMixin, WebSearchMixin
9
+ from .tools.registry import tool
10
+
11
+ __all__ = [
12
+ "Agent",
13
+ "tool",
14
+ "schedule",
15
+ "WebSearchMixin",
16
+ "FilesystemMixin",
17
+ "ShellMixin",
18
+ "HttpMixin",
19
+ "GitHubMixin",
20
+ ]
21
+ __version__ = "0.1.0"
aios/agent.py ADDED
@@ -0,0 +1,282 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ import json
6
+ import logging
7
+ import traceback
8
+ from abc import abstractmethod
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from .config import load_env, validate_model_key
13
+ from .identity.core import AgentIdentity, load_identity
14
+ from .memory.store import MemoryStore
15
+ from .models.router import ModelRouter
16
+ from .runtime.checkpoint import CheckpointEngine
17
+ from .runtime.process import AIOS_DIR
18
+ from .scheduling import Scheduler, _SCHEDULE_MARKER, parse_interval
19
+ from .tools.registry import ToolRegistry
20
+
21
+ logger = logging.getLogger("aios")
22
+
23
+
24
+ class Agent:
25
+ """
26
+ Base class for all Ai.os agents.
27
+
28
+ Subclass this, declare class-level config, add @tool methods, implement run().
29
+ The runtime handles identity, memory, crash recovery, and model routing.
30
+
31
+ Example::
32
+
33
+ class ResearchAgent(Agent):
34
+ name = "researcher"
35
+ model = "claude-sonnet-4-6"
36
+
37
+ @tool
38
+ async def search_web(self, query: str) -> str:
39
+ ...
40
+
41
+ async def run(self):
42
+ results = await self.search_web("AI papers")
43
+ await self.memory.save("results", results)
44
+
45
+ if __name__ == "__main__":
46
+ ResearchAgent.launch()
47
+ """
48
+
49
+ # ── Agent config (override in subclass) ──────────────────────────────────
50
+ name: str = "agent"
51
+ model: str = "claude-sonnet-4-6"
52
+ version: str = "1.0.0"
53
+ description: str = ""
54
+ system_prompt: str = ""
55
+ temperature: float = 0.7
56
+ max_tokens: int = 4096
57
+ config: dict = {}
58
+
59
+ # ── Runtime state (populated by _bootstrap) ───────────────────────────────
60
+ identity: AgentIdentity
61
+ memory: MemoryStore
62
+ _router: ModelRouter
63
+ _tools: ToolRegistry
64
+ _checkpoint: CheckpointEngine
65
+ _db_path: Path
66
+
67
+ @classmethod
68
+ def _db_path_for(cls) -> Path:
69
+ path = AIOS_DIR / "data" / f"{cls.name}.db"
70
+ path.parent.mkdir(parents=True, exist_ok=True)
71
+ return path
72
+
73
+ async def _bootstrap(self) -> None:
74
+ self._db_path = self._db_path_for()
75
+
76
+ self.identity = await load_identity(
77
+ name=self.name,
78
+ model=self.model,
79
+ version=self.version,
80
+ config=self.config,
81
+ db_path=self._db_path,
82
+ )
83
+
84
+ self.memory = MemoryStore(agent_id=self.identity.id, db_path=self._db_path)
85
+ await self.memory.setup()
86
+
87
+ self._checkpoint = CheckpointEngine(agent_id=self.identity.id, db_path=self._db_path)
88
+ await self._checkpoint.setup()
89
+
90
+ self._router = ModelRouter(
91
+ model=self.model,
92
+ temperature=self.temperature,
93
+ max_tokens=self.max_tokens,
94
+ )
95
+
96
+ self._tools = ToolRegistry()
97
+ self._tools.register_from_agent(self)
98
+ self._install_checkpointed_tools()
99
+
100
+ def _install_checkpointed_tools(self) -> None:
101
+ """
102
+ Replace every @tool method on this instance with a checkpointed wrapper.
103
+ The wrapper checks the cache before executing and saves the result after.
104
+ """
105
+ for defn in self._tools.all():
106
+ original_fn = defn.fn
107
+ tool_name = defn.name
108
+ setattr(self, tool_name, self._make_checkpointed(original_fn, tool_name))
109
+
110
+ def _make_checkpointed(self, fn: Any, tool_name: str) -> Any:
111
+ agent = self
112
+
113
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
114
+ call_args = _bind_args(fn, args, kwargs)
115
+ hit, cached = await agent._checkpoint.get_cached(tool_name, call_args)
116
+ if hit:
117
+ logger.debug("checkpoint replay: %s(%s)", tool_name, call_args)
118
+ return cached
119
+ result = fn(*args, **kwargs)
120
+ if inspect.isawaitable(result):
121
+ result = await result
122
+ await agent._checkpoint.save_result(tool_name, call_args, result)
123
+ return result
124
+
125
+ wrapper.__name__ = tool_name
126
+ return wrapper
127
+
128
+ # ── Agent-to-agent ────────────────────────────────────────────────────────
129
+
130
+ async def call_agent(self, agent_class: "type[Agent]", prompt: str) -> str:
131
+ """
132
+ Instantiate another agent class and ask it a single question.
133
+ The child agent shares this agent's memory scope for read access.
134
+ Returns the child's text response without starting a full lifecycle run.
135
+ """
136
+ child = agent_class()
137
+ await child._bootstrap()
138
+
139
+ # Give the child read-only access to this agent's memory
140
+ child._parent_memory = self.memory # type: ignore[attr-defined]
141
+
142
+ logger.info("[%s] calling agent %s", self.name, agent_class.name)
143
+ response = await child.think(prompt)
144
+ logger.info("[%s] agent %s returned %d chars", self.name, agent_class.name, len(response))
145
+ return response
146
+
147
+ async def spawn_agent(self, agent_class: "type[Agent]") -> None:
148
+ """
149
+ Run another agent's full lifecycle in the background (fire-and-forget).
150
+ The child runs independently with its own memory and checkpoint scope.
151
+ """
152
+ child = agent_class()
153
+ asyncio.create_task(child._execute(), name=f"agent:{agent_class.name}")
154
+ logger.info("[%s] spawned agent %s", self.name, agent_class.name)
155
+
156
+ # ── LLM helpers ───────────────────────────────────────────────────────────
157
+
158
+ async def think(self, prompt: str, context: list[dict] | None = None) -> str:
159
+ """Single-shot LLM call — no tools, returns text."""
160
+ messages = list(context or [])
161
+ messages.append({"role": "user", "content": prompt})
162
+ resp = await self._router.complete(messages=messages, system=self.system_prompt or None)
163
+ return resp.content
164
+
165
+ async def think_with_tools(
166
+ self,
167
+ prompt: str,
168
+ context: list[dict] | None = None,
169
+ max_iterations: int = 10,
170
+ ) -> str:
171
+ """
172
+ Agentic loop: LLM selects and calls tools until it produces a final text response.
173
+ All tool calls are checkpointed — crash here, resume here on restart.
174
+ """
175
+ messages: list[dict] = list(context or [])
176
+ messages.append({"role": "user", "content": prompt})
177
+ tools = self._tools.to_llm_format()
178
+
179
+ for iteration in range(max_iterations):
180
+ resp = await self._router.complete(
181
+ messages=messages,
182
+ tools=tools or None,
183
+ system=self.system_prompt or None,
184
+ )
185
+
186
+ if resp.finish_reason == "stop" or not resp.tool_calls:
187
+ return resp.content
188
+
189
+ # Append assistant turn with tool calls
190
+ messages.append({
191
+ "role": "assistant",
192
+ "content": resp.content,
193
+ "tool_calls": [
194
+ {
195
+ "id": tc["id"],
196
+ "type": "function",
197
+ "function": {"name": tc["name"], "arguments": tc["arguments"]},
198
+ }
199
+ for tc in resp.tool_calls
200
+ ],
201
+ })
202
+
203
+ # Execute tool calls (checkpointing happens inside the wrapped methods)
204
+ for tc in resp.tool_calls:
205
+ try:
206
+ result = await self._tools.call(tc["name"], tc["arguments"])
207
+ except Exception as exc:
208
+ result = f"Error: {exc}"
209
+ logger.warning("tool %s failed: %s", tc["name"], exc)
210
+
211
+ messages.append({
212
+ "role": "tool",
213
+ "tool_call_id": tc["id"],
214
+ "content": json.dumps(result, default=str),
215
+ })
216
+
217
+ return "Max tool-call iterations reached."
218
+
219
+ # ── Lifecycle ─────────────────────────────────────────────────────────────
220
+
221
+ @abstractmethod
222
+ async def run(self) -> None:
223
+ """The agent's main logic. Override this."""
224
+ ...
225
+
226
+ async def on_start(self) -> None:
227
+ """Called once before run(). Override for setup work."""
228
+
229
+ async def on_stop(self) -> None:
230
+ """Called after run() completes or crashes. Override for cleanup."""
231
+
232
+ async def _execute(self) -> None:
233
+ await self._bootstrap()
234
+ run_id = await self._checkpoint.start_run()
235
+
236
+ logger.info("[%s] run started — id=%s model=%s", self.name, run_id[:8], self.model)
237
+ await self.memory.log_event("run_started", {"run_id": run_id})
238
+ await self.on_start()
239
+
240
+ error: str | None = None
241
+ try:
242
+ # Check if run() is decorated with @schedule
243
+ run_method = type(self).run
244
+ if getattr(run_method, _SCHEDULE_MARKER, False):
245
+ interval_str = getattr(run_method, "__aios_interval__", "every 1h")
246
+ seconds = parse_interval(interval_str)
247
+ if seconds > 0:
248
+ scheduler = Scheduler(interval_seconds=seconds)
249
+ await scheduler.run_loop(self.run, self.memory)
250
+ return
251
+ await self.run()
252
+ except Exception:
253
+ error = traceback.format_exc()
254
+ logger.error("[%s] crashed:\n%s", self.name, error)
255
+ await self.memory.log_event("run_crashed", {"error": error[:2000]})
256
+ finally:
257
+ await self.on_stop()
258
+ await self._checkpoint.end_run(error=error)
259
+ if error is None:
260
+ await self.memory.log_event("run_completed", {"run_id": run_id})
261
+ logger.info("[%s] run completed", self.name)
262
+
263
+ @classmethod
264
+ def launch(cls) -> None:
265
+ """Entry point. Call at the bottom of your agent file."""
266
+ load_env()
267
+ validate_model_key(cls.model)
268
+ import os
269
+ log_level = os.environ.get("AIOS_LOG_LEVEL", "INFO").upper()
270
+ logging.basicConfig(
271
+ level=getattr(logging, log_level, logging.INFO),
272
+ format="%(asctime)s %(levelname)-8s %(name)s %(message)s",
273
+ datefmt="%H:%M:%S",
274
+ )
275
+ asyncio.run(cls()._execute())
276
+
277
+
278
+ def _bind_args(fn: Any, args: tuple, kwargs: dict) -> dict:
279
+ sig = inspect.signature(fn)
280
+ bound = sig.bind(*args, **kwargs)
281
+ bound.apply_defaults()
282
+ return dict(bound.arguments)
aios/cli/__init__.py ADDED
File without changes