feat(F-LOOP): true ReAct loop with structured Thought/Action/Observation step tracking
Check Cross-Plugin Imports / check (push) Has been cancelled

- app/ai/agent_loop.py: ReActStep + ReActResult dataclasses, run_react_loop()
  with LLM→Tool→Observe loop, max_steps/timeout graceful stop, ErrorCategory
  retry (TRANSIENT→retry, PERMANENT→stop, PARTIAL→continue), cost accumulation,
  agent.step hook, on_step callback
- app/plugins/builtins/automation/models.py: AgentRunStep model
- alembic/versions/0121_agent_run_steps.py: migration for agent run steps table
- app/plugins/builtins/automation/agent_runner.py: refactored to use run_react_loop(),
  saves steps to DB, updates AgentRun with cost/status/duration
- tests/test_agent_loop.py: 11 tests (all passing, mocked, no DB/LLM needed)
- PROGRESS.md: Phase F started, F-LOOP marked done
This commit is contained in:
Agent Zero
2026-08-17 16:11:26 +02:00
parent da9be1e2f2
commit c760b5961c
6 changed files with 1100 additions and 133 deletions
+29
View File
@@ -16,6 +16,7 @@ from sqlalchemy import (
String,
Text,
UniqueConstraint,
func,
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.dialects.postgresql import UUID as PGUUID
@@ -254,6 +255,34 @@ class AutomationRun(Base, TenantMixin):
dry_run: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
class AgentRunStep(Base, TenantMixin):
"""Individual step in a ReAct loop execution (Thought → Action → Observation)."""
__tablename__ = "automation_agent_run_steps"
__table_args__ = (
Index("ix_agent_run_steps_run", "tenant_id", "agent_run_id"),
)
id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
agent_run_id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True),
ForeignKey("automation_agent_runs.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
step_number: Mapped[int] = mapped_column(Integer, nullable=False)
thought: Mapped[str | None] = mapped_column(Text, nullable=True)
action: Mapped[str | None] = mapped_column(String(255), nullable=True)
action_input: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)
observation: Mapped[str | None] = mapped_column(Text, nullable=True)
cost_usd: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
class AgentSubtask(Base, TenantMixin):
"""A subtask delegated from one agent to another for multi-agent orchestration."""