| from __future__ import annotations |
|
|
| from datetime import datetime, timezone |
| from enum import Enum |
| from typing import Any, Callable, Literal, Optional, Union |
|
|
| from pydantic import BaseModel, Field, model_validator |
|
|
|
|
| |
| |
| |
|
|
| class Role(str, Enum): |
| SYSTEM = "system" |
| USER = "user" |
| ASSISTANT = "assistant" |
| TOOL = "tool" |
|
|
|
|
| class AgentState(str, Enum): |
| IDLE = "IDLE" |
| RUNNING = "RUNNING" |
| FINISHED = "FINISHED" |
| ERROR = "ERROR" |
|
|
|
|
| |
| |
| |
|
|
| class Function(BaseModel): |
| name: str |
| arguments: str |
|
|
|
|
| class ToolCall(BaseModel): |
| id: str |
| type: Literal["function"] = "function" |
| function: Function |
|
|
|
|
| class Message(BaseModel): |
| role: Role |
| content: Optional[str] = None |
| tool_calls: Optional[list[ToolCall]] = None |
| tool_call_id: Optional[str] = None |
| name: Optional[str] = None |
|
|
| @classmethod |
| def system(cls, content: str) -> "Message": |
| return cls(role=Role.SYSTEM, content=content) |
|
|
| @classmethod |
| def user(cls, content: str) -> "Message": |
| return cls(role=Role.USER, content=content) |
|
|
| @classmethod |
| def assistant( |
| cls, |
| content: Optional[str] = None, |
| tool_calls: Optional[list[ToolCall]] = None, |
| ) -> "Message": |
| return cls(role=Role.ASSISTANT, content=content, tool_calls=tool_calls) |
|
|
| @classmethod |
| def tool(cls, content: str, tool_call_id: str, name: str) -> "Message": |
| return cls(role=Role.TOOL, content=content, tool_call_id=tool_call_id, name=name) |
|
|
| def to_dict(self) -> dict[str, Any]: |
| d: dict[str, Any] = {"role": self.role.value} |
| if self.content is not None: |
| d["content"] = self.content |
| if self.tool_calls: |
| d["tool_calls"] = [ |
| { |
| "id": tc.id, |
| "type": tc.type, |
| "function": { |
| "name": tc.function.name, |
| "arguments": tc.function.arguments, |
| }, |
| } |
| for tc in self.tool_calls |
| ] |
| if self.tool_call_id: |
| d["tool_call_id"] = self.tool_call_id |
| if self.name: |
| d["name"] = self.name |
| return d |
|
|
|
|
| |
| |
| |
|
|
| class Memory(BaseModel): |
| messages: list[Message] = Field(default_factory=list) |
| max_messages: int = 100 |
|
|
| def add(self, message: Message) -> None: |
| self.messages.append(message) |
| self._trim() |
|
|
| def _trim(self) -> None: |
| if len(self.messages) <= self.max_messages: |
| return |
| system = [m for m in self.messages if m.role == Role.SYSTEM] |
| rest = [m for m in self.messages if m.role != Role.SYSTEM] |
| keep = max(self.max_messages - len(system), 0) |
| self.messages = system + rest[-keep:] if keep else system |
|
|
| def to_list(self) -> list[dict[str, Any]]: |
| return [m.to_dict() for m in self.messages] |
|
|
| def clear(self) -> None: |
| self.messages = [] |
|
|
| def token_estimate(self) -> int: |
| """Estimate token count using 3.5 chars/token (more conservative than //4). |
| For mixed content (code, non-English) this is still approximate but better.""" |
| total = sum(len(m.content or "") for m in self.messages) |
| return int(total / 3.5) |
|
|
|
|
| |
| |
| |
|
|
| def _utcnow() -> datetime: |
| """UTC now β replacement for deprecated datetime.utcnow().""" |
| return datetime.now(timezone.utc) |
|
|
|
|
| class Observation(BaseModel): |
| """Result of a single tool execution.""" |
| tool_name: str |
| args: dict[str, Any] |
| output: Optional[str] |
| error: Optional[str] |
| success: bool |
| attempt: int = 1 |
| duration_ms: int = 0 |
| timestamp: datetime = Field(default_factory=_utcnow) |
|
|
| def summary(self) -> str: |
| if self.success: |
| out = (self.output or "")[:400] |
| return f"[{self.tool_name}] β {out}" |
| return f"[{self.tool_name}] β ERROR: {self.error}" |
|
|
|
|
| class Reflection(BaseModel): |
| """LLM-generated reflection on whether an observation solved the goal.""" |
| step_goal: str |
| observation_summary: str |
| solved: bool |
| reason: str |
| next_action: Optional[str] = None |
|
|
| def to_prompt(self) -> str: |
| status = "SOLVED" if self.solved else "NOT SOLVED" |
| lines = [ |
| f"Reflection [{status}]:", |
| f" Goal: {self.step_goal}", |
| f" Observation: {self.observation_summary}", |
| f" Reason: {self.reason}", |
| ] |
| if self.next_action: |
| lines.append(f" Next action: {self.next_action}") |
| return "\n".join(lines) |
|
|
|
|
| class TaskStep(BaseModel): |
| """A single step in the PAORR execution history.""" |
| step_number: int |
| goal: str |
| observations: list[Observation] = Field(default_factory=list) |
| reflection: Optional[Reflection] = None |
| resolved: bool = False |
| timestamp: datetime = Field(default_factory=_utcnow) |
|
|
| def summary(self) -> str: |
| status = "β" if self.resolved else "β" |
| obs_summaries = " | ".join(o.summary() for o in self.observations[-3:]) |
| return f"Step {self.step_number} {status}: {self.goal[:60]} β {obs_summaries}" |
|
|
|
|
| class TaskHistory(BaseModel): |
| """Persistent log of all steps across a task run.""" |
| task_id: str |
| original_goal: str |
| steps: list[TaskStep] = Field(default_factory=list) |
| created_at: datetime = Field(default_factory=_utcnow) |
|
|
| def add_step(self, goal: str) -> TaskStep: |
| step = TaskStep(step_number=len(self.steps) + 1, goal=goal) |
| self.steps.append(step) |
| return step |
|
|
| def last_step(self) -> Optional[TaskStep]: |
| return self.steps[-1] if self.steps else None |
|
|
| def context_summary(self, max_steps: int = 5) -> str: |
| recent = self.steps[-max_steps:] |
| if not recent: |
| return "No prior steps." |
| lines = ["=== Task History (recent steps) ==="] |
| for s in recent: |
| lines.append(s.summary()) |
| lines.append("=== End History ===") |
| return "\n".join(lines) |
|
|
| def is_looping(self, window: int = 3) -> bool: |
| if len(self.steps) < window: |
| return False |
| recent = self.steps[-window:] |
| if all(not s.resolved for s in recent): |
| tool_names = [ |
| o.tool_name |
| for s in recent |
| for o in s.observations |
| ] |
| if len(set(tool_names)) == 1 and tool_names: |
| return True |
| return False |
|
|
|
|
| |
| |
| |
|
|
| class ToolResult(BaseModel): |
| output: Optional[str] = None |
| error: Optional[str] = None |
| system: Optional[str] = None |
| base64_image: Optional[str] = None |
|
|
| @property |
| def success(self) -> bool: |
| return self.error is None |
|
|
| def __str__(self) -> str: |
| parts = [] |
| if self.output: |
| parts.append(self.output) |
| if self.error: |
| parts.append(f"ERROR: {self.error}") |
| if self.system: |
| parts.append(f"[system: {self.system}]") |
| return "\n".join(parts) if parts else "(no output)" |
|
|
|
|
| |
| |
| |
|
|
| class StepStatus(str, Enum): |
| NOT_STARTED = "not_started" |
| IN_PROGRESS = "in_progress" |
| COMPLETED = "completed" |
| BLOCKED = "blocked" |
|
|
|
|
| class PlanStep(BaseModel): |
| id: int |
| description: str |
| status: StepStatus = StepStatus.NOT_STARTED |
| assigned_to: Optional[str] = None |
| notes: Optional[str] = None |
| success_criteria: Optional[str] = None |
| success_score: float = 0.0 |
|
|
|
|
| class Plan(BaseModel): |
| id: str |
| title: str |
| steps: list[PlanStep] = Field(default_factory=list) |
|
|
| def next_step(self) -> Optional[PlanStep]: |
| for step in self.steps: |
| if step.status == StepStatus.NOT_STARTED: |
| return step |
| return None |
|
|
| def is_complete(self) -> bool: |
| return all(s.status == StepStatus.COMPLETED for s in self.steps) |
|
|
| def completion_rate(self) -> float: |
| if not self.steps: |
| return 0.0 |
| done = sum(1 for s in self.steps if s.status == StepStatus.COMPLETED) |
| return done / len(self.steps) |
|
|
|
|
| |
| |
| |
|
|
| class RetryPolicy(BaseModel): |
| """ |
| Declarative retry configuration. |
| Used by agents, flows, and the orchestrator. |
| """ |
| max_attempts: int = 3 |
| base_wait_s: float = 1.0 |
| max_wait_s: float = 30.0 |
| exponential_base: float = 2.0 |
| jitter: bool = True |
|
|
| def wait_for(self, attempt: int) -> float: |
| """Return the wait duration (seconds) before the given attempt number.""" |
| import random |
| wait = min( |
| self.base_wait_s * (self.exponential_base ** (attempt - 1)), |
| self.max_wait_s, |
| ) |
| if self.jitter: |
| wait += random.uniform(0, wait * 0.3) |
| return round(wait, 2) |
|
|
|
|
| |
| |
| |
|
|
| class AgentRunConfig(BaseModel): |
| """ |
| Typed contract for starting an agent run. |
| Passed to server endpoints and CLI entry points. |
| """ |
| prompt: str |
| mode: str = "build" |
| max_steps: int = 30 |
| timeout: int = 3600 |
| session_id: Optional[str] = None |
| retry_policy: RetryPolicy = Field(default_factory=RetryPolicy) |
|
|
| @model_validator(mode="after") |
| def _validate_mode(self) -> "AgentRunConfig": |
| if self.mode not in ("build", "plan"): |
| raise ValueError(f"mode must be 'build' or 'plan', got '{self.mode}'") |
| return self |
|
|
|
|
| class AgentRunResult(BaseModel): |
| """Typed result returned after an agent run completes.""" |
| session_id: str |
| agent_name: str |
| prompt: str |
| output: str |
| state: AgentState |
| step_count: int |
| duration_s: float |
| success: bool |
| trace_id: Optional[str] = None |
|
|
| @property |
| def failed(self) -> bool: |
| return not self.success |
|
|
|
|
| |
| |
| |
|
|
| class RoleDecision(str, Enum): |
| """ |
| Decision made by a role after processing its input. |
| |
| PROCEED β output is ready; publish to the next role |
| RETRY β output is incomplete; retry with a focused correction prompt |
| ESCALATE β input is ambiguous/unclear; request clarification or skip |
| BLOCKED β role cannot proceed even after retries; pipeline should continue with error |
| """ |
| PROCEED = "proceed" |
| RETRY = "retry" |
| ESCALATE = "escalate" |
| BLOCKED = "blocked" |
|
|
|
|
| class RoleResult(BaseModel): |
| """Typed result from a single role execution.""" |
| role_name: str |
| decision: RoleDecision |
| output: str |
| artefact: Optional[str] = None |
| duration_s: float = 0.0 |
| retry_count: int = 0 |
| escalation_reason: Optional[str] = None |
|
|
| @property |
| def succeeded(self) -> bool: |
| return self.decision in (RoleDecision.PROCEED, RoleDecision.ESCALATE) |
|
|
|
|
| |
| |
| |
|
|
| class FlowStepResult(BaseModel): |
| """Result of a single step in a PlanningFlow execution.""" |
| step_id: int |
| description: str |
| status: StepStatus |
| output: Optional[str] = None |
| error: Optional[str] = None |
| attempts: int = 1 |
| duration_s: float = 0.0 |
| success_score: float = 0.0 |
|
|
|
|
| class FlowResult(BaseModel): |
| """Aggregated result from a complete PlanningFlow execution.""" |
| flow_id: str |
| goal: str |
| steps: list[FlowStepResult] = Field(default_factory=list) |
| total_duration_s: float = 0.0 |
| timed_out: bool = False |
|
|
| @property |
| def success_rate(self) -> float: |
| if not self.steps: |
| return 0.0 |
| done = sum(1 for s in self.steps if s.status == StepStatus.COMPLETED) |
| return done / len(self.steps) |
|
|
| @property |
| def avg_success_score(self) -> float: |
| completed = [s for s in self.steps if s.status == StepStatus.COMPLETED] |
| if not completed: |
| return 0.0 |
| return sum(s.success_score for s in completed) / len(completed) |
|
|
|
|
| |
| |
| |
|
|
| class PipelineStageResult(BaseModel): |
| """Result of a single role stage in the orchestrator pipeline.""" |
| role_name: str |
| status: str |
| output: str |
| duration_s: float = 0.0 |
| decision: Optional[str] = None |
|
|
|
|
| class PipelineResult(BaseModel): |
| """Aggregated result from a complete MultiAgentOrchestrator run.""" |
| pipeline_id: str |
| goal: str |
| stages: list[PipelineStageResult] = Field(default_factory=list) |
| total_duration_s: float = 0.0 |
| timed_out: bool = False |
| verdict: str = "unknown" |
|
|
| def to_summary(self) -> str: |
| lines = [ |
| "βββββββββββββββββββββββββββββββββββββββββββββββββββ", |
| " ManusClaw Multi-Agent Pipeline β Final Report", |
| "βββββββββββββββββββββββββββββββββββββββββββββββββββ", |
| f" Pipeline ID : {self.pipeline_id}", |
| f" Goal : {self.goal[:80]}", |
| f" Duration : {self.total_duration_s:.1f}s", |
| f" Verdict : {self.verdict.upper()}", |
| "", |
| ] |
| for stage in self.stages: |
| icon = "β" if stage.status == "completed" else "β" |
| lines.append(f" {icon} {stage.role_name.replace('_', ' ').title():<20s} " |
| f"[{stage.duration_s:.1f}s] β {stage.output[:100]}...") |
| if self.timed_out: |
| lines.append("\n β± Pipeline timed out before all stages completed.") |
| lines.append("βββββββββββββββββββββββββββββββββββββββββββββββββββ") |
| return "\n".join(lines) |
|
|
|
|
| |
| |
| |
|
|
| class ToolCallContract(BaseModel): |
| """ |
| Typed contract for a tool call. |
| Used for validation, documentation, and testing. |
| """ |
| tool_name: str |
| args: dict[str, Any] |
| expected_output_type: str = "text" |
| timeout_s: Optional[int] = None |
| retry_policy: RetryPolicy = Field(default_factory=RetryPolicy) |
|
|
| def validate_args(self, schema: dict) -> tuple[bool, str]: |
| """Validate args against a JSON Schema dict. Returns (valid, error_msg).""" |
| required = schema.get("required", []) |
| missing = [k for k in required if k not in self.args] |
| if missing: |
| return False, f"Missing required args: {missing}" |
| return True, "" |