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 +21 -0
- aios/agent.py +282 -0
- aios/cli/__init__.py +0 -0
- aios/cli/main.py +505 -0
- aios/cli/templates.py +56 -0
- aios/config.py +83 -0
- aios/identity/__init__.py +3 -0
- aios/identity/core.py +107 -0
- aios/memory/__init__.py +3 -0
- aios/memory/store.py +120 -0
- aios/models/__init__.py +3 -0
- aios/models/router.py +96 -0
- aios/runtime/__init__.py +4 -0
- aios/runtime/checkpoint.py +138 -0
- aios/runtime/process.py +97 -0
- aios/scheduling.py +90 -0
- aios/tools/__init__.py +3 -0
- aios/tools/builtin/__init__.py +27 -0
- aios/tools/builtin/filesystem.py +115 -0
- aios/tools/builtin/github.py +215 -0
- aios/tools/builtin/http.py +88 -0
- aios/tools/builtin/shell.py +108 -0
- aios/tools/builtin/web.py +89 -0
- aios/tools/registry.py +111 -0
- aios/web/__init__.py +3 -0
- aios/web/app.py +318 -0
- aios_runtime-0.1.0.dist-info/METADATA +320 -0
- aios_runtime-0.1.0.dist-info/RECORD +31 -0
- aios_runtime-0.1.0.dist-info/WHEEL +4 -0
- aios_runtime-0.1.0.dist-info/entry_points.txt +2 -0
- aios_runtime-0.1.0.dist-info/licenses/LICENSE +21 -0
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
|