feat(F-LOOP): true ReAct loop with structured Thought/Action/Observation step tracking
Check Cross-Plugin Imports / check (push) Has been cancelled
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:
@@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user