feat(agent-os): implement runtime framework

This commit is contained in:
2026-07-07 10:12:27 +08:00
parent bce254790c
commit 8a5ce63069
10 changed files with 1059 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
"""Agent quality, acceptance, and performance evaluation."""
from __future__ import annotations
from dataclasses import dataclass
from statistics import mean
from typing import Any
@dataclass
class EvaluationInput:
correctness: float = 0.0
completeness: float = 0.0
safety: float = 0.0
latency_ms: float = 0.0
retries: int = 0
accepted: bool = False
notes: str = ""
class AgentEvaluator:
def quality_score(self, item: EvaluationInput) -> float:
return self._bounded(mean([item.correctness, item.completeness, item.safety]))
def acceptance_score(self, item: EvaluationInput) -> float:
base = 1.0 if item.accepted else 0.0
return self._bounded((base * 0.5) + (item.correctness * 0.3) + (item.safety * 0.2))
def performance_feedback(self, item: EvaluationInput) -> dict[str, Any]:
latency_penalty = min(item.latency_ms / 60000.0, 1.0)
retry_penalty = min(item.retries * 0.1, 0.5)
performance = self._bounded(1.0 - latency_penalty - retry_penalty)
return {
"performance_score": performance,
"latency_ms": item.latency_ms,
"retries": item.retries,
"recommendation": self._recommendation(item, performance),
}
def evaluate(self, item: EvaluationInput) -> dict[str, Any]:
quality = self.quality_score(item)
acceptance = self.acceptance_score(item)
feedback = self.performance_feedback(item)
overall = self._bounded((quality * 0.45) + (acceptance * 0.35) + (feedback["performance_score"] * 0.20))
return {
"quality_score": quality,
"acceptance_score": acceptance,
"overall_score": overall,
"feedback": feedback,
"notes": item.notes,
}
def _recommendation(self, item: EvaluationInput, performance: float) -> str:
if item.safety < 0.8:
return "route_to_review"
if not item.accepted:
return "retry_or_handoff"
if performance < 0.5:
return "optimize_or_parallelize"
return "accepted"
def _bounded(self, value: float) -> float:
return max(0.0, min(1.0, float(value)))
+114
View File
@@ -0,0 +1,114 @@
"""Agent registration and discovery for Agent OS."""
from __future__ import annotations
from dataclasses import dataclass, field
import time
from typing import Any
@dataclass
class AgentRecord:
agent_id: str
name: str
capabilities: set[str] = field(default_factory=set)
skills: set[str] = field(default_factory=set)
status: str = "registered"
metadata: dict[str, Any] = field(default_factory=dict)
registered_at: float = field(default_factory=time.time)
updated_at: float = field(default_factory=time.time)
def snapshot(self) -> dict[str, Any]:
return {
"agent_id": self.agent_id,
"name": self.name,
"capabilities": sorted(self.capabilities),
"skills": sorted(self.skills),
"status": self.status,
"metadata": dict(self.metadata),
"registered_at": self.registered_at,
"updated_at": self.updated_at,
}
class AgentRegistry:
def __init__(self) -> None:
self._agents: dict[str, AgentRecord] = {}
def register_agent(
self,
agent_id: str,
name: str,
capabilities: list[str] | set[str] | None = None,
skills: list[str] | set[str] | None = None,
metadata: dict[str, Any] | None = None,
) -> dict[str, Any]:
if agent_id in self._agents:
raise ValueError(f"agent already registered: {agent_id}")
record = AgentRecord(
agent_id=agent_id,
name=name,
capabilities=set(capabilities or []),
skills=set(skills or []),
metadata=dict(metadata or {}),
)
self._agents[agent_id] = record
return record.snapshot()
def get_agent(self, agent_id: str) -> dict[str, Any] | None:
record = self._agents.get(agent_id)
return record.snapshot() if record else None
def list_agents(self, status: str | None = None) -> list[dict[str, Any]]:
records = self._agents.values()
if status is not None:
records = [record for record in records if record.status == status]
return [record.snapshot() for record in records]
def update_status(self, agent_id: str, status: str) -> dict[str, Any]:
record = self._require(agent_id)
record.status = status
record.updated_at = time.time()
return record.snapshot()
def add_capability(self, agent_id: str, capability: str) -> dict[str, Any]:
record = self._require(agent_id)
record.capabilities.add(capability)
record.updated_at = time.time()
return record.snapshot()
def add_skill(self, agent_id: str, skill: str) -> dict[str, Any]:
record = self._require(agent_id)
record.skills.add(skill)
record.updated_at = time.time()
return record.snapshot()
def find_by_capability(self, capability: str, only_active: bool = True) -> list[dict[str, Any]]:
matches = []
for record in self._agents.values():
if capability in record.capabilities and (not only_active or record.status in {"registered", "idle", "running"}):
matches.append(record.snapshot())
return matches
def discover_capabilities(self) -> dict[str, list[str]]:
capabilities: dict[str, list[str]] = {}
for record in self._agents.values():
for capability in record.capabilities:
capabilities.setdefault(capability, []).append(record.agent_id)
return {name: sorted(agent_ids) for name, agent_ids in capabilities.items()}
def discover_skills(self) -> dict[str, list[str]]:
skills: dict[str, list[str]] = {}
for record in self._agents.values():
for skill in record.skills:
skills.setdefault(skill, []).append(record.agent_id)
return {name: sorted(agent_ids) for name, agent_ids in skills.items()}
def unregister_agent(self, agent_id: str) -> bool:
return self._agents.pop(agent_id, None) is not None
def _require(self, agent_id: str) -> AgentRecord:
try:
return self._agents[agent_id]
except KeyError as exc:
raise KeyError(f"agent not registered: {agent_id}") from exc
+154
View File
@@ -0,0 +1,154 @@
"""Agent lifecycle management for the Agent OS runtime."""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
import threading
import time
from typing import Any, Callable
class AgentState(str, Enum):
REGISTERED = "registered"
STARTING = "starting"
RUNNING = "running"
STOPPING = "stopping"
STOPPED = "stopped"
UNHEALTHY = "unhealthy"
FAILED = "failed"
LifecycleHook = Callable[["ManagedAgent"], None]
HealthHook = Callable[["ManagedAgent"], bool]
@dataclass
class ManagedAgent:
agent_id: str
name: str
capabilities: set[str] = field(default_factory=set)
metadata: dict[str, Any] = field(default_factory=dict)
state: AgentState = AgentState.REGISTERED
started_at: float | None = None
stopped_at: float | None = None
heartbeat_at: float | None = None
failure_count: int = 0
last_error: str = ""
on_start: LifecycleHook | None = None
on_stop: LifecycleHook | None = None
on_recover: LifecycleHook | None = None
on_health: HealthHook | None = None
def snapshot(self) -> dict[str, Any]:
return {
"agent_id": self.agent_id,
"name": self.name,
"capabilities": sorted(self.capabilities),
"metadata": dict(self.metadata),
"state": self.state.value,
"started_at": self.started_at,
"stopped_at": self.stopped_at,
"heartbeat_at": self.heartbeat_at,
"failure_count": self.failure_count,
"last_error": self.last_error,
}
class AgentRuntimeManager:
"""Track and control agent lifecycle without owning process credentials."""
def __init__(self) -> None:
self._agents: dict[str, ManagedAgent] = {}
self._lock = threading.RLock()
def add_agent(self, agent: ManagedAgent) -> ManagedAgent:
with self._lock:
if agent.agent_id in self._agents:
raise ValueError(f"agent already exists: {agent.agent_id}")
self._agents[agent.agent_id] = agent
return agent
def get_agent(self, agent_id: str) -> ManagedAgent:
with self._lock:
try:
return self._agents[agent_id]
except KeyError as exc:
raise KeyError(f"agent not found: {agent_id}") from exc
def list_agents(self) -> list[dict[str, Any]]:
with self._lock:
return [agent.snapshot() for agent in self._agents.values()]
def start(self, agent_id: str) -> dict[str, Any]:
agent = self.get_agent(agent_id)
with self._lock:
if agent.state == AgentState.RUNNING:
return agent.snapshot()
agent.state = AgentState.STARTING
try:
if agent.on_start:
agent.on_start(agent)
now = time.time()
agent.started_at = now
agent.heartbeat_at = now
agent.stopped_at = None
agent.last_error = ""
agent.state = AgentState.RUNNING
except Exception as exc:
agent.failure_count += 1
agent.last_error = str(exc)[:500]
agent.state = AgentState.FAILED
return agent.snapshot()
def stop(self, agent_id: str) -> dict[str, Any]:
agent = self.get_agent(agent_id)
with self._lock:
if agent.state == AgentState.STOPPED:
return agent.snapshot()
agent.state = AgentState.STOPPING
try:
if agent.on_stop:
agent.on_stop(agent)
agent.stopped_at = time.time()
agent.state = AgentState.STOPPED
except Exception as exc:
agent.failure_count += 1
agent.last_error = str(exc)[:500]
agent.state = AgentState.FAILED
return agent.snapshot()
def restart(self, agent_id: str) -> dict[str, Any]:
self.stop(agent_id)
return self.start(agent_id)
def health_check(self, agent_id: str) -> dict[str, Any]:
agent = self.get_agent(agent_id)
with self._lock:
healthy = agent.state == AgentState.RUNNING
if healthy and agent.on_health:
try:
healthy = bool(agent.on_health(agent))
except Exception as exc:
healthy = False
agent.last_error = str(exc)[:500]
if healthy:
agent.heartbeat_at = time.time()
elif agent.state == AgentState.RUNNING:
agent.state = AgentState.UNHEALTHY
snapshot = agent.snapshot()
snapshot["healthy"] = healthy
return snapshot
def recovery(self, agent_id: str) -> dict[str, Any]:
agent = self.get_agent(agent_id)
with self._lock:
if agent.on_recover:
try:
agent.on_recover(agent)
except Exception as exc:
agent.failure_count += 1
agent.last_error = str(exc)[:500]
agent.state = AgentState.FAILED
return agent.snapshot()
return self.restart(agent_id)
+98
View File
@@ -0,0 +1,98 @@
"""Public API facade for Agent OS runtime."""
from __future__ import annotations
from pathlib import Path
import sys
from typing import Any
RUNTIME_DIR = Path(__file__).resolve().parent
if str(RUNTIME_DIR) not in sys.path:
sys.path.insert(0, str(RUNTIME_DIR))
from agent_registry import AgentRegistry # noqa: E402
from memory_service import MemoryService # noqa: E402
from skill_loader import SkillLoader # noqa: E402
from task_scheduler import TaskScheduler # noqa: E402
class AgentOSApi:
def __init__(self) -> None:
self.registry = AgentRegistry()
self.memory = MemoryService()
self.skills = SkillLoader()
self.scheduler = TaskScheduler(registry=self.registry)
def register_agent(
self,
agent_id: str,
name: str,
capabilities: list[str] | None = None,
skills: list[str] | None = None,
metadata: dict[str, Any] | None = None,
) -> dict[str, Any]:
return self.registry.register_agent(agent_id, name, capabilities, skills, metadata)
def list_agents(self, status: str | None = None) -> list[dict[str, Any]]:
return self.registry.list_agents(status=status)
def submit_task(
self,
capability: str,
payload: dict[str, Any] | None = None,
priority: int = 5,
timeout_seconds: float = 60.0,
max_retries: int = 1,
) -> dict[str, Any]:
return self.scheduler.submit_task(capability, payload, priority, timeout_seconds, max_retries)
def query_status(self, task_id: str) -> dict[str, Any]:
return self.scheduler.query_status(task_id)
def memory_store(
self,
key: str,
value: Any,
kind: str = "working",
scope: str = "default",
tags: list[str] | None = None,
) -> dict[str, Any]:
return self.memory.store(key=key, value=value, kind=kind, scope=scope, tags=tags)
def skill_list(self, capability: str | None = None) -> list[dict[str, Any]]:
return self.skills.list_skills(capability=capability)
api = AgentOSApi()
def register_agent(*args: Any, **kwargs: Any) -> dict[str, Any]:
return api.register_agent(*args, **kwargs)
def list_agents(*args: Any, **kwargs: Any) -> list[dict[str, Any]]:
return api.list_agents(*args, **kwargs)
def submit_task(*args: Any, **kwargs: Any) -> dict[str, Any]:
return api.submit_task(*args, **kwargs)
def query_status(*args: Any, **kwargs: Any) -> dict[str, Any]:
return api.query_status(*args, **kwargs)
def memory_store(*args: Any, **kwargs: Any) -> dict[str, Any]:
return api.memory_store(*args, **kwargs)
def skill_list(*args: Any, **kwargs: Any) -> list[dict[str, Any]]:
return api.skill_list(*args, **kwargs)
if __name__ == "__main__":
register_agent("agent.local", "Local Agent", capabilities=["self-test"], skills=["core"])
task = submit_task("self-test", {"message": "hello"})
memory_store("last_task", task["task_id"], kind="working")
print({"agents": list_agents(), "task": query_status(task["task_id"]), "skills": skill_list()})
+75
View File
@@ -0,0 +1,75 @@
"""Weighted consensus engine for multi-agent decisions."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
DEFAULT_WEIGHTS = {
"custom_gpt": 2,
"windows_codex": 2,
"server_agent": 1,
}
@dataclass
class Vote:
voter_id: str
voter_type: str
choice: str
confidence: float = 1.0
rationale: str = ""
def weight(self, weights: dict[str, int]) -> float:
return float(weights.get(self.voter_type, weights.get("server_agent", 1))) * max(0.0, min(1.0, self.confidence))
class ConsensusEngine:
def __init__(self, weights: dict[str, int] | None = None) -> None:
self.weights = dict(DEFAULT_WEIGHTS)
if weights:
self.weights.update(weights)
def weighted_vote(self, votes: list[Vote]) -> dict[str, Any]:
totals: dict[str, float] = {}
for vote in votes:
totals[vote.choice] = totals.get(vote.choice, 0.0) + vote.weight(self.weights)
if not totals:
return {"decision": "owner_escalation_required", "totals": {}, "tie": True}
ordered = sorted(totals.items(), key=lambda item: (-item[1], item[0]))
top_score = ordered[0][1]
winners = [choice for choice, score in ordered if score == top_score]
if len(winners) > 1:
return self.owner_escalation(votes, totals, reason="weighted_tie")
return {"decision": winners[0], "totals": totals, "tie": False}
def conflict_detection(self, votes: list[Vote]) -> dict[str, Any]:
choices = sorted({vote.choice for vote in votes})
return {"conflict": len(choices) > 1, "choices": choices, "vote_count": len(votes)}
def owner_escalation(self, votes: list[Vote], totals: dict[str, float] | None = None, reason: str = "conflict") -> dict[str, Any]:
return {
"decision": "owner_escalation_required",
"reason": reason,
"tie": True,
"totals": totals or {},
"review_required": "owner/admin",
"votes": [
{
"voter_id": vote.voter_id,
"voter_type": vote.voter_type,
"choice": vote.choice,
"confidence": vote.confidence,
"rationale": vote.rationale,
}
for vote in votes
],
}
def decide(self, votes: list[Vote]) -> dict[str, Any]:
conflict = self.conflict_detection(votes)
result = self.weighted_vote(votes)
result["conflict"] = conflict["conflict"]
result["choices"] = conflict["choices"]
return result
+91
View File
@@ -0,0 +1,91 @@
"""Closed-loop self-evolution engine."""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
import time
import uuid
from typing import Any, Callable
class EvolutionStage(str, Enum):
DETECT = "detect"
PLAN = "plan"
SCHEDULE = "schedule"
EXECUTE = "execute"
VALIDATE = "validate"
VOTE = "vote"
PATCH = "patch"
LEARN = "learn"
@dataclass
class EvolutionRun:
run_id: str = field(default_factory=lambda: str(uuid.uuid4()))
stage: EvolutionStage = EvolutionStage.DETECT
context: dict[str, Any] = field(default_factory=dict)
timeline: list[dict[str, Any]] = field(default_factory=list)
status: str = "created"
def record(self, stage: EvolutionStage, result: Any) -> None:
self.stage = stage
self.timeline.append({"stage": stage.value, "result": result, "ts": time.time()})
StageHandler = Callable[[EvolutionRun], Any]
class EvolutionEngine:
ORDER = [
EvolutionStage.DETECT,
EvolutionStage.PLAN,
EvolutionStage.SCHEDULE,
EvolutionStage.EXECUTE,
EvolutionStage.VALIDATE,
EvolutionStage.VOTE,
EvolutionStage.PATCH,
EvolutionStage.LEARN,
]
def __init__(self) -> None:
self._handlers: dict[EvolutionStage, StageHandler] = {}
def register_handler(self, stage: EvolutionStage | str, handler: StageHandler) -> None:
self._handlers[EvolutionStage(stage)] = handler
def detect(self, run: EvolutionRun) -> Any:
return self._handle(EvolutionStage.DETECT, run, {"detected": True, "signals": run.context.get("signals", [])})
def plan(self, run: EvolutionRun) -> Any:
return self._handle(EvolutionStage.PLAN, run, {"plan": run.context.get("plan", [])})
def schedule(self, run: EvolutionRun) -> Any:
return self._handle(EvolutionStage.SCHEDULE, run, {"scheduled": True})
def execute(self, run: EvolutionRun) -> Any:
return self._handle(EvolutionStage.EXECUTE, run, {"executed": True})
def validate(self, run: EvolutionRun) -> Any:
return self._handle(EvolutionStage.VALIDATE, run, {"valid": True})
def vote(self, run: EvolutionRun) -> Any:
return self._handle(EvolutionStage.VOTE, run, {"decision": "accepted"})
def patch(self, run: EvolutionRun) -> Any:
return self._handle(EvolutionStage.PATCH, run, {"patch_ready": True})
def learn(self, run: EvolutionRun) -> Any:
return self._handle(EvolutionStage.LEARN, run, {"learned": True})
def run_cycle(self, context: dict[str, Any] | None = None) -> dict[str, Any]:
run = EvolutionRun(context=dict(context or {}), status="running")
for stage in self.ORDER:
result = getattr(self, stage.value)(run)
run.record(stage, result)
run.status = "completed"
return {"run_id": run.run_id, "status": run.status, "timeline": run.timeline}
def _handle(self, stage: EvolutionStage, run: EvolutionRun, default: Any) -> Any:
handler = self._handlers.get(stage)
return handler(run) if handler else default
+107
View File
@@ -0,0 +1,107 @@
"""Memory service with secret-safe storage checks."""
from __future__ import annotations
from dataclasses import dataclass, field
import json
import re
import time
from typing import Any
SECRET_PATTERNS = [
re.compile(pattern, re.IGNORECASE)
for pattern in (
r"api[_-]?key",
r"password",
r"secret",
r"token",
r"bearer\s+[a-z0-9._-]+",
r"-----BEGIN [A-Z ]*PRIVATE KEY-----",
)
]
@dataclass
class MemoryRecord:
key: str
value: Any
kind: str
scope: str
tags: set[str] = field(default_factory=set)
created_at: float = field(default_factory=time.time)
updated_at: float = field(default_factory=time.time)
def snapshot(self) -> dict[str, Any]:
return {
"key": self.key,
"value": self.value,
"kind": self.kind,
"scope": self.scope,
"tags": sorted(self.tags),
"created_at": self.created_at,
"updated_at": self.updated_at,
}
class MemoryService:
VALID_KINDS = {"working", "long_term", "shared_knowledge", "reflection"}
def __init__(self) -> None:
self._records: dict[str, MemoryRecord] = {}
def store(
self,
key: str,
value: Any,
kind: str = "working",
scope: str = "default",
tags: list[str] | set[str] | None = None,
) -> dict[str, Any]:
if kind not in self.VALID_KINDS:
raise ValueError(f"unsupported memory kind: {kind}")
self._reject_secret(key, value)
record_key = self._record_key(scope, key)
now = time.time()
existing = self._records.get(record_key)
if existing:
existing.value = value
existing.kind = kind
existing.tags = set(tags or existing.tags)
existing.updated_at = now
return existing.snapshot()
record = MemoryRecord(key=key, value=value, kind=kind, scope=scope, tags=set(tags or []))
self._records[record_key] = record
return record.snapshot()
def get(self, key: str, scope: str = "default") -> dict[str, Any] | None:
record = self._records.get(self._record_key(scope, key))
return record.snapshot() if record else None
def search(self, query: str = "", kind: str | None = None, scope: str | None = None) -> list[dict[str, Any]]:
query_lower = query.lower()
results = []
for record in self._records.values():
if kind and record.kind != kind:
continue
if scope and record.scope != scope:
continue
haystack = f"{record.key} {json.dumps(record.value, ensure_ascii=False, default=str)}".lower()
if query_lower in haystack:
results.append(record.snapshot())
return results
def delete(self, key: str, scope: str = "default") -> bool:
return self._records.pop(self._record_key(scope, key), None) is not None
def reflect(self, key: str, value: Any, scope: str = "default") -> dict[str, Any]:
return self.store(key=key, value=value, kind="reflection", scope=scope, tags={"reflection"})
def _record_key(self, scope: str, key: str) -> str:
return f"{scope}:{key}"
def _reject_secret(self, key: str, value: Any) -> None:
serialized = f"{key} {json.dumps(value, ensure_ascii=False, default=str)}"
for pattern in SECRET_PATTERNS:
if pattern.search(serialized):
raise ValueError("memory refuses to store secret-like material")
+93
View File
@@ -0,0 +1,93 @@
"""In-process message bus for Agent OS agents and task events."""
from __future__ import annotations
from dataclasses import dataclass, field
import time
import uuid
from typing import Any, Callable
MessageHandler = Callable[["AgentMessage"], None]
@dataclass
class AgentMessage:
sender: str
recipient: str
message_type: str
payload: dict[str, Any] = field(default_factory=dict)
message_id: str = field(default_factory=lambda: str(uuid.uuid4()))
created_at: float = field(default_factory=time.time)
def snapshot(self) -> dict[str, Any]:
return {
"message_id": self.message_id,
"sender": self.sender,
"recipient": self.recipient,
"message_type": self.message_type,
"payload": dict(self.payload),
"created_at": self.created_at,
}
@dataclass
class TaskEvent:
task_id: str
event_type: str
source: str
detail: dict[str, Any] = field(default_factory=dict)
created_at: float = field(default_factory=time.time)
def to_message(self) -> AgentMessage:
return AgentMessage(
sender=self.source,
recipient="task-events",
message_type=self.event_type,
payload={"task_id": self.task_id, "detail": self.detail, "created_at": self.created_at},
)
class MessageBus:
def __init__(self) -> None:
self._subscribers: dict[str, list[MessageHandler]] = {}
self._routes: dict[str, str] = {}
self._history: list[dict[str, Any]] = []
def subscribe(self, topic: str, handler: MessageHandler) -> None:
self._subscribers.setdefault(topic, []).append(handler)
def unsubscribe(self, topic: str, handler: MessageHandler) -> bool:
handlers = self._subscribers.get(topic, [])
if handler not in handlers:
return False
handlers.remove(handler)
return True
def route(self, message_type: str, topic: str) -> None:
self._routes[message_type] = topic
def publish(self, topic: str, message: AgentMessage) -> dict[str, Any]:
self._history.append({"topic": topic, **message.snapshot()})
delivered = 0
for handler in list(self._subscribers.get(topic, [])):
handler(message)
delivered += 1
return {"topic": topic, "message_id": message.message_id, "delivered": delivered}
def send(self, message: AgentMessage) -> dict[str, Any]:
topic = self._routes.get(message.message_type, message.recipient)
return self.publish(topic, message)
def broadcast(self, sender: str, message_type: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
message = AgentMessage(sender=sender, recipient="broadcast", message_type=message_type, payload=dict(payload or {}))
total = 0
for topic in list(self._subscribers):
total += self.publish(topic, message)["delivered"]
return {"message_id": message.message_id, "delivered": total}
def emit_task_event(self, event: TaskEvent) -> dict[str, Any]:
return self.send(event.to_message())
def history(self, limit: int = 100) -> list[dict[str, Any]]:
return self._history[-limit:]
+105
View File
@@ -0,0 +1,105 @@
"""Skill installation and validation registry for Agent OS."""
from __future__ import annotations
from dataclasses import dataclass, field
import re
from typing import Any
NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
VERSION_RE = re.compile(r"^[0-9]+(\.[0-9]+){0,2}([+-][A-Za-z0-9._-]+)?$")
@dataclass
class SkillManifest:
name: str
version: str
capabilities: set[str] = field(default_factory=set)
dependencies: set[str] = field(default_factory=set)
metadata: dict[str, Any] = field(default_factory=dict)
enabled: bool = True
def snapshot(self) -> dict[str, Any]:
return {
"name": self.name,
"version": self.version,
"capabilities": sorted(self.capabilities),
"dependencies": sorted(self.dependencies),
"metadata": dict(self.metadata),
"enabled": self.enabled,
}
class SkillLoader:
def __init__(self) -> None:
self._skills: dict[str, SkillManifest] = {}
def install_skill(
self,
name: str,
version: str,
capabilities: list[str] | set[str] | None = None,
dependencies: list[str] | set[str] | None = None,
metadata: dict[str, Any] | None = None,
) -> dict[str, Any]:
manifest = SkillManifest(
name=name,
version=version,
capabilities=set(capabilities or []),
dependencies=set(dependencies or []),
metadata=dict(metadata or {}),
)
self.validate_skill(manifest)
missing = self.dependency_check(manifest)
if missing:
raise ValueError(f"missing skill dependencies: {', '.join(missing)}")
self._skills[name] = manifest
return manifest.snapshot()
def validate_skill(self, manifest: SkillManifest) -> bool:
if not NAME_RE.match(manifest.name):
raise ValueError("invalid skill name")
if not VERSION_RE.match(manifest.version):
raise ValueError("invalid skill version")
return True
def dependency_check(self, manifest: SkillManifest) -> list[str]:
return sorted(dep for dep in manifest.dependencies if dep not in self._skills)
def set_version(self, name: str, version: str) -> dict[str, Any]:
manifest = self._require(name)
candidate = SkillManifest(
name=manifest.name,
version=version,
capabilities=set(manifest.capabilities),
dependencies=set(manifest.dependencies),
metadata=dict(manifest.metadata),
enabled=manifest.enabled,
)
self.validate_skill(candidate)
manifest.version = version
return manifest.snapshot()
def list_skills(self, capability: str | None = None, enabled_only: bool = True) -> list[dict[str, Any]]:
skills = self._skills.values()
if capability:
skills = [skill for skill in skills if capability in skill.capabilities]
if enabled_only:
skills = [skill for skill in skills if skill.enabled]
return [skill.snapshot() for skill in skills]
def get_skill(self, name: str) -> dict[str, Any] | None:
manifest = self._skills.get(name)
return manifest.snapshot() if manifest else None
def enable_skill(self, name: str, enabled: bool = True) -> dict[str, Any]:
manifest = self._require(name)
manifest.enabled = enabled
return manifest.snapshot()
def _require(self, name: str) -> SkillManifest:
try:
return self._skills[name]
except KeyError as exc:
raise KeyError(f"skill not installed: {name}") from exc
+159
View File
@@ -0,0 +1,159 @@
"""Priority task scheduling and handoff for Agent OS."""
from __future__ import annotations
from concurrent.futures import ThreadPoolExecutor, TimeoutError
from dataclasses import dataclass, field
import heapq
import time
import uuid
from typing import Any, Callable
TaskHandler = Callable[[dict[str, Any], dict[str, Any]], Any]
@dataclass(order=True)
class ScheduledTask:
sort_key: tuple[int, float] = field(init=False, repr=False)
priority: int
created_at: float
capability: str = field(compare=False)
payload: dict[str, Any] = field(default_factory=dict, compare=False)
task_id: str = field(default_factory=lambda: str(uuid.uuid4()), compare=False)
timeout_seconds: float = field(default=60.0, compare=False)
max_retries: int = field(default=1, compare=False)
attempts: int = field(default=0, compare=False)
status: str = field(default="queued", compare=False)
assigned_agent_id: str = field(default="", compare=False)
result: Any = field(default=None, compare=False)
error: str = field(default="", compare=False)
def __post_init__(self) -> None:
self.sort_key = (-self.priority, self.created_at)
def snapshot(self) -> dict[str, Any]:
return {
"task_id": self.task_id,
"capability": self.capability,
"priority": self.priority,
"created_at": self.created_at,
"timeout_seconds": self.timeout_seconds,
"max_retries": self.max_retries,
"attempts": self.attempts,
"status": self.status,
"assigned_agent_id": self.assigned_agent_id,
"result": self.result,
"error": self.error,
}
class TaskScheduler:
def __init__(self, registry: Any | None = None, max_workers: int = 4) -> None:
self.registry = registry
self.executor = ThreadPoolExecutor(max_workers=max_workers)
self._queue: list[ScheduledTask] = []
self._tasks: dict[str, ScheduledTask] = {}
self._handlers: dict[str, TaskHandler] = {}
def register_handler(self, capability: str, handler: TaskHandler) -> None:
self._handlers[capability] = handler
def submit_task(
self,
capability: str,
payload: dict[str, Any] | None = None,
priority: int = 5,
timeout_seconds: float = 60.0,
max_retries: int = 1,
) -> dict[str, Any]:
task = ScheduledTask(
priority=priority,
created_at=time.time(),
capability=capability,
payload=dict(payload or {}),
timeout_seconds=timeout_seconds,
max_retries=max_retries,
)
self._tasks[task.task_id] = task
heapq.heappush(self._queue, task)
return task.snapshot()
def match_agent(self, capability: str) -> dict[str, Any] | None:
if self.registry is None:
return {"agent_id": "local-handler", "capabilities": [capability], "status": "running"}
matches = self.registry.find_by_capability(capability)
return matches[0] if matches else None
def run_next(self) -> dict[str, Any] | None:
if not self._queue:
return None
task = heapq.heappop(self._queue)
return self._execute(task).snapshot()
def run_until_idle(self, parallel: bool = True) -> list[dict[str, Any]]:
if not parallel:
results = []
while self._queue:
item = self.run_next()
if item:
results.append(item)
return results
tasks = [heapq.heappop(self._queue) for _ in range(len(self._queue))]
futures = [self.executor.submit(self._execute, task) for task in tasks]
return [future.result().snapshot() for future in futures]
def handoff(self, task_id: str, target_agent_id: str) -> dict[str, Any]:
task = self._require(task_id)
task.assigned_agent_id = target_agent_id
task.status = "handoff"
return task.snapshot()
def query_status(self, task_id: str) -> dict[str, Any]:
return self._require(task_id).snapshot()
def _execute(self, task: ScheduledTask) -> ScheduledTask:
agent = self.match_agent(task.capability)
if not agent:
task.status = "waiting_for_handoff"
task.error = f"no agent for capability: {task.capability}"
return task
task.assigned_agent_id = str(agent.get("agent_id") or "")
handler = self._handlers.get(task.capability)
if handler is None:
task.status = "waiting_for_handoff"
task.error = f"no handler for capability: {task.capability}"
return task
while task.attempts <= task.max_retries:
task.attempts += 1
task.status = "running"
attempt_executor = ThreadPoolExecutor(max_workers=1)
future = attempt_executor.submit(handler, task.payload, agent)
try:
task.result = future.result(timeout=task.timeout_seconds)
task.status = "completed"
task.error = ""
return task
except TimeoutError:
future.cancel()
task.error = "timeout"
except Exception as exc:
task.error = str(exc)[:500]
finally:
attempt_executor.shutdown(wait=False, cancel_futures=True)
if task.attempts <= task.max_retries:
task.status = "retrying"
time.sleep(0.05)
task.status = "failed"
return task
def _require(self, task_id: str) -> ScheduledTask:
try:
return self._tasks[task_id]
except KeyError as exc:
raise KeyError(f"task not found: {task_id}") from exc