state.py 2.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. """Thin workflow state.
  2. Search pages, candidates, evidence and final buckets stay in the existing database.
  3. This state only contains orchestration pointers and audit summaries.
  4. """
  5. from __future__ import annotations
  6. from dataclasses import dataclass, field
  7. from typing import Any, Literal
  8. from supply_agent.types import AgentResult
  9. Phase = Literal["planning", "searching", "evidence", "evaluating", "done"]
  10. EndKind = Literal["goal_met", "partial", "no_match", "failed", "stopped"]
  11. @dataclass(frozen=True)
  12. class DiscoverySnapshot:
  13. """Small deterministic projection of the database state."""
  14. status: str
  15. search_count: int
  16. candidate_count: int
  17. pending_count: int
  18. primary_count: int
  19. valid_primary_count: int
  20. rejected_count: int
  21. outcome_status: str = ""
  22. @dataclass(frozen=True)
  23. class NodeRun:
  24. """One node's observable result."""
  25. node: str
  26. round_index: int
  27. content: str
  28. iterations: int
  29. tool_calls_made: int
  30. @classmethod
  31. def from_agent_result(
  32. cls,
  33. node: str,
  34. round_index: int,
  35. result: AgentResult,
  36. ) -> "NodeRun":
  37. return cls(
  38. node=node,
  39. round_index=round_index,
  40. content=result.content or "",
  41. iterations=int(result.iterations or 0),
  42. tool_calls_made=int(result.tool_calls_made or 0),
  43. )
  44. @dataclass
  45. class FindAgentState:
  46. """Control state passed through the single-round graph."""
  47. run_id: str
  48. user_input: str
  49. round_index: int = 0
  50. phase: Phase = "planning"
  51. plan: str = ""
  52. stop: bool = False
  53. stop_reason: str = ""
  54. previous_snapshot: DiscoverySnapshot | None = None
  55. snapshot: DiscoverySnapshot | None = None
  56. node_runs: list[NodeRun] = field(default_factory=list)
  57. failures: list[dict[str, Any]] = field(default_factory=list)
  58. @dataclass(frozen=True)
  59. class FindAgentResult:
  60. """Workflow result with technical and business status kept separate."""
  61. run_id: str
  62. status: EndKind
  63. succeeded: bool
  64. business_outcome: str
  65. valid_primary_count: int
  66. rounds: int
  67. final_output: str
  68. node_runs: tuple[NodeRun, ...]
  69. stop_reason: str = ""
  70. @property
  71. def iterations(self) -> int:
  72. return sum(item.iterations for item in self.node_runs)
  73. @property
  74. def tool_calls_made(self) -> int:
  75. return sum(item.tool_calls_made for item in self.node_runs)