Files
magnus919_agent-skills/agent-council/agent_council/state.py
Magnus Hedemark 9f3a68bd66 feat: replace fake personas with real profiles from hermes-profiles
Instead of the compose phase fabricating personas with fake backgrounds,
the council now draws from 39 real professional profiles via a git
submodule (https://github.com/magnus919/hermes-profiles).

Key changes:
- New select phase reads SOUL.md + profile.yaml from profiles submodule
- Auto-updates submodule before selection via git submodule update --remote
- --profiles flag for explicit selection (comma-separated names)
- Auto-selection by keyword overlap with profile descriptions when omitted
- Each agent's identity is their real SOUL.md — actual methodology,
  values, and operating principles, not invented backgrounds
- Falls back to composed personas if profile library is unavailable
- Real profiles produce genuine methodological disagreement (debugger
  said 'unanswerable without a verified process' to naming question)

Signed-off-by: Magnus Hedemark <magnus919@pm.me>
2026-07-10 00:17:56 -04:00

187 lines
6.2 KiB
Python

"""Typed state model and phase output schemas."""
from dataclasses import dataclass, field
from typing import Literal
from pydantic import BaseModel, Field
# ── Profile info (from submoduled hermes-profiles) ──
@dataclass
class ProfileInfo:
"""A real profile drawn from the hermes-profiles library.
Used in place of fabricated AgentPersona when profiles are available.
The name is the profile directory name (e.g. 'debugger').
The soul_content is the full identity document.
"""
name: str
description: str
soul_content: str
# ── Phase output schemas (validated Pydantic models) ──
class AgentPersona(BaseModel):
"""Profile for a single debate agent."""
name: str
background: str = Field(description="One-paragraph career background")
expertise: str = Field(description="Specific domain expertise")
approach: str = Field(description="Analytical approach they bring")
bias: str = Field(description="What experience or bias they bring to THIS question")
class Premortem(BaseModel):
"""Pre-mortem: agent envisions how the decision already failed."""
agent_name: str
failure_scenario: str = Field(description="Narrative of how the decision failed")
root_causes: list[str] = Field(description="What went wrong")
early_warning_signals: list[str] = Field(description="What to watch for")
class Position(BaseModel):
"""Agent's initial position on the question."""
agent_name: str
stance: str = Field(description="Position on the question")
reasoning: list[str] = Field(description="Chain of reasoning")
confidence: float = Field(ge=0, le=1, description="Confidence in this position")
key_assumptions: list[str] = Field(description="Assumptions that must hold")
class CrossExamination(BaseModel):
"""Agent's response after reading all other positions."""
agent_name: str
concessions: list[str] = Field(description="Points where the agent conceded or shifted")
remaining_disagreements: list[str] = Field(description="Points still in dispute")
updated_position: str | None = Field(
default=None, description="Revised position, if changed"
)
updated_confidence: float | None = Field(
default=None, ge=0, le=1, description="Updated confidence, if changed"
)
reflection: str = Field(
description="What the agent learned from other perspectives"
)
new_evidence_needed: list[str] = Field(
default_factory=list,
description="What evidence would close remaining gaps",
)
class RiskVector(BaseModel):
"""A risk identified during the debate, with position-relative context."""
description: str
agents_who_flagged: list[str]
severity: Literal["low", "medium", "high"]
phase_discovered: Literal["premortem", "position", "cross_examine"] = Field(
description="Which phase first surfaced this risk. "
"Premortem risks are seen BEFORE positional commitment."
)
class RoundMetrics(BaseModel):
"""Convergence metrics for a single cross-examination round."""
round: int
mean_confidence: float
dispersion: float = Field(description="Standard deviation of agent confidences")
new_arguments: int = Field(description="Arguments not seen in prior rounds")
concessions_made: int
stopped_early: bool = Field(
default=False,
description="True if this round was cut short by convergence detection",
)
class Disagreement(BaseModel):
"""A point of genuine disagreement that survived cross-examination."""
topic: str
positions: dict[str, str] = Field(
description="Agent name -> summary of their position on this topic"
)
unresolved: bool = Field(
default=True,
description="Whether this disagreement persisted after all rounds",
)
class Synthesis(BaseModel):
"""Structured output of a completed council debate."""
# Metadata
question: str
mode: str
num_agents: int
rounds_completed: int
stopped_reason: Literal[
"converged",
"max_rounds",
"diminishing_returns",
"genuine_disagreement",
] = Field(description="Why the debate stopped")
# Convergence diagnostics
confidence_history: list[RoundMetrics] = Field(
description="One entry per cross-examination round"
)
final_dispersion: float
mean_confidence_delta: float = Field(
description="Change in mean confidence from first to last round"
)
# Content: premortem phase (pre-positional)
shared_risks: list[RiskVector] = Field(
description="Risks identified during pre-mortem before any agent "
"formed a position. Compare with shared_concerns to see which "
"worries survived cross-examination."
)
# Content: cross-examination phase (post-positional)
shared_concerns: list[str] = Field(
description="Concerns that survived cross-examination and are shared "
"across agents. A risk in shared_risks that also appears here was "
"confirmed by debate. A risk in shared_risks absent here is either "
"resolved or buried by positional commitment."
)
disagreements: list[Disagreement]
assumptions_per_position: dict[str, list[str]] = Field(
description="Agent name -> assumptions that would need to hold "
"for their position to be correct"
)
risk_vectors: list[RiskVector]
principal_path: str = Field(description="Narrative synthesis of the decision landscape")
# ── Orchestration state (mutable dataclass) ──
@dataclass
class CouncilState:
"""Mutable state that flows through the debate graph."""
question: str
mode: str = "medium"
max_rounds: int = 4
convergence_threshold: float = 0.10
personas: list[AgentPersona] = field(default_factory=list)
profiles: list[ProfileInfo] = field(default_factory=list)
"""Real profiles from the hermes-profiles library, if available.
Takes priority over fabricated personas when present."""
premortems: dict[str, Premortem] = field(default_factory=dict)
positions: dict[str, Position] = field(default_factory=dict)
cross_examination_rounds: list[dict[str, CrossExamination]] = field(
default_factory=list
)
synthesis: Synthesis | None = None
round_number: int = 0