From 7ae99f2bc3e518500fecc904fc456101df71e351 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Tue, 10 Mar 2026 11:09:11 -0400 Subject: [PATCH] feat(personas): Add persona_id support to Ticket/WorkerContext and ConductorEngine --- .../tracks/agent_personas_20260309/plan.md | 6 +-- config.toml | 12 ++--- manualslop_layout.ini | 52 +++++++++---------- personas.toml | 10 ++++ src/models.py | 16 ++++-- src/multi_agent_conductor.py | 19 ++++++- tests/test_persona_id.py | 28 ++++++++++ 7 files changed, 103 insertions(+), 40 deletions(-) create mode 100644 personas.toml create mode 100644 tests/test_persona_id.py diff --git a/conductor/tracks/agent_personas_20260309/plan.md b/conductor/tracks/agent_personas_20260309/plan.md index c2d1ce1..1a52a2f 100644 --- a/conductor/tracks/agent_personas_20260309/plan.md +++ b/conductor/tracks/agent_personas_20260309/plan.md @@ -8,9 +8,9 @@ - [x] Task: Conductor - User Manual Verification 'Phase 1: Core Model and Migration' (Protocol in workflow.md) ## Phase 2: Granular MMA Integration -- [ ] Task: Write Tests: Verify that a `Ticket` or `Track` can hold a `persona_id` override. -- [ ] Task: Implement: Update the MMA internal state to support per-epic, per-track, and per-task Persona assignments. -- [ ] Task: Implement: Update the `WorkerContext` and `ConductorEngine` to resolve and apply the correct Persona before spawning an agent. +- [x] Task: Write Tests: Verify that a `Ticket` or `Track` can hold a `persona_id` override. +- [x] Task: Implement: Update the MMA internal state to support per-epic, per-track, and per-task Persona assignments. +- [x] Task: Implement: Update the `WorkerContext` and `ConductorEngine` to resolve and apply the correct Persona before spawning an agent. - [ ] Task: Implement: Add "Persona" metadata to the Tier Stream logs to visually confirm which profile is active. - [ ] Task: Conductor - User Manual Verification 'Phase 2: Granular MMA Integration' (Protocol in workflow.md) diff --git a/config.toml b/config.toml index c147b58..4360bf9 100644 --- a/config.toml +++ b/config.toml @@ -22,10 +22,10 @@ active = "C:/projects/gencpp/gencpp_sloppy.toml" separate_message_panel = false separate_response_panel = false separate_tool_calls_panel = false -bg_shader_enabled = true +bg_shader_enabled = false crt_filter_enabled = false separate_task_dag = false -separate_usage_analytics = false +separate_usage_analytics = true separate_tier1 = false separate_tier2 = false separate_tier3 = false @@ -48,8 +48,8 @@ separate_tier4 = false "Tier 4: QA" = false "Discussion Hub" = true "Operations Hub" = true -Message = true -Response = true +Message = false +Response = false "Tool Calls" = false Theme = true "Log Management" = true @@ -60,8 +60,8 @@ palette = "Nord Dark" font_path = "C:/projects/manual_slop/assets/fonts/Inter-Regular.ttf" font_size = 14.0 scale = 1.2000000476837158 -transparency = 0.550000011920929 -child_transparency = 0.6399999856948853 +transparency = 1.0 +child_transparency = 1.0 [mma] max_workers = 4 diff --git a/manualslop_layout.ini b/manualslop_layout.ini index e27f03f..14654b8 100644 --- a/manualslop_layout.ini +++ b/manualslop_layout.ini @@ -73,8 +73,8 @@ Collapsed=0 DockId=0xAFC85805,2 [Window][Theme] -Pos=0,977 -Size=659,1160 +Pos=0,1602 +Size=387,935 Collapsed=0 DockId=0x00000002,2 @@ -90,8 +90,8 @@ Collapsed=0 DockId=0x0000000C,2 [Window][Context Hub] -Pos=0,977 -Size=659,1160 +Pos=0,1602 +Size=387,935 Collapsed=0 DockId=0x00000002,1 @@ -102,26 +102,26 @@ Collapsed=0 DockId=0x0000000D,0 [Window][Discussion Hub] -Pos=1660,28 -Size=1243,2109 +Pos=680,28 +Size=452,2509 Collapsed=0 DockId=0x00000013,0 [Window][Operations Hub] -Pos=661,28 -Size=997,2109 +Pos=389,28 +Size=289,2509 Collapsed=0 DockId=0x00000012,0 [Window][Files & Media] -Pos=0,977 -Size=659,1160 +Pos=0,1602 +Size=387,935 Collapsed=0 DockId=0x00000002,0 [Window][AI Settings] Pos=0,28 -Size=659,947 +Size=387,1572 Collapsed=0 DockId=0x00000001,0 @@ -131,14 +131,14 @@ Size=416,325 Collapsed=0 [Window][MMA Dashboard] -Pos=2905,28 -Size=935,2109 +Pos=1134,28 +Size=306,2509 Collapsed=0 DockId=0x0000000C,0 [Window][Log Management] -Pos=2905,28 -Size=935,2109 +Pos=1134,28 +Size=306,2509 Collapsed=0 DockId=0x0000000C,1 @@ -337,8 +337,8 @@ Size=275,375 Collapsed=0 [Window][Tool Preset Manager] -Pos=827,642 -Size=973,688 +Pos=192,440 +Size=1066,1324 Collapsed=0 [Table][0xFB6E3870,4] @@ -429,17 +429,17 @@ Column 2 Weight=1.0000 DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02 -DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=3840,2109 Split=X - DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2903,1183 Split=X +DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=1440,2509 Split=X + DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=1132,1183 Split=X DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2 - DockNode ID=0x00000007 Parent=0x0000000B SizeRef=659,858 Split=Y Selected=0x8CA2375C - DockNode ID=0x00000001 Parent=0x00000007 SizeRef=824,947 CentralNode=1 Selected=0x7BD57D6A - DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,1160 Selected=0x1DCB2623 - DockNode ID=0x0000000E Parent=0x0000000B SizeRef=2242,858 Split=X Selected=0x418C7449 - DockNode ID=0x00000012 Parent=0x0000000E SizeRef=997,402 Selected=0x418C7449 - DockNode ID=0x00000013 Parent=0x0000000E SizeRef=1243,402 Selected=0x6F2B5B04 + DockNode ID=0x00000007 Parent=0x0000000B SizeRef=387,858 Split=Y Selected=0x8CA2375C + DockNode ID=0x00000001 Parent=0x00000007 SizeRef=824,1172 CentralNode=1 Selected=0x7BD57D6A + DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,935 Selected=0x1DCB2623 + DockNode ID=0x0000000E Parent=0x0000000B SizeRef=743,858 Split=X Selected=0x418C7449 + DockNode ID=0x00000012 Parent=0x0000000E SizeRef=289,402 Selected=0x418C7449 + DockNode ID=0x00000013 Parent=0x0000000E SizeRef=452,402 Selected=0x6F2B5B04 DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6 - DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=935,1183 Split=Y Selected=0x3AEC3498 + DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=306,1183 Split=Y Selected=0x3AEC3498 DockNode ID=0x0000000C Parent=0x00000004 SizeRef=1074,1208 Selected=0x3AEC3498 DockNode ID=0x0000000F Parent=0x00000004 SizeRef=1074,899 Selected=0x5CDB7A4B diff --git a/personas.toml b/personas.toml new file mode 100644 index 0000000..454c763 --- /dev/null +++ b/personas.toml @@ -0,0 +1,10 @@ +[personas.Default] +system_prompt = "" +provider = "minimax" +model = "MiniMax-M2.5" +preferred_models = [ + "MiniMax-M2.5", +] +temperature = 0.0 +top_p = 1.0 +max_output_tokens = 32000 diff --git a/src/models.py b/src/models.py index 4a8bf06..2439fe6 100644 --- a/src/models.py +++ b/src/models.py @@ -121,22 +121,25 @@ def parse_history_entries(history_strings: list[str], roles: list[str]) -> list[ entries.append({"role": role, "content": content, "collapsed": True, "ts": ts}) return entries +@dataclass +@dataclass @dataclass class Ticket: id: str description: str + target_symbols: List[str] = field(default_factory=list) + context_requirements: List[str] = field(default_factory=list) + depends_on: List[str] = field(default_factory=list) status: str = "todo" assigned_to: str = "unassigned" priority: str = "medium" target_file: Optional[str] = None - target_symbols: List[str] = field(default_factory=list) - context_requirements: List[str] = field(default_factory=list) - depends_on: List[str] = field(default_factory=list) blocked_reason: Optional[str] = None step_mode: bool = False retry_count: int = 0 manual_block: bool = False model_override: Optional[str] = None + persona_id: Optional[str] = None def mark_blocked(self, reason: str) -> None: self.status = "blocked" @@ -175,6 +178,7 @@ class Ticket: "retry_count": self.retry_count, "manual_block": self.manual_block, "model_override": self.model_override, + "persona_id": self.persona_id, } @classmethod @@ -194,6 +198,7 @@ class Ticket: retry_count=data.get("retry_count", 0), manual_block=data.get("manual_block", False), model_override=data.get("model_override"), + persona_id=data.get("persona_id"), ) @@ -224,12 +229,15 @@ class Track: ) +@dataclass +@dataclass @dataclass class WorkerContext: ticket_id: str model_name: str - tool_preset: Optional[str] = None messages: List[Dict[str, Any]] = field(default_factory=list) + tool_preset: Optional[str] = None + persona_id: Optional[str] = None @dataclass diff --git a/src/multi_agent_conductor.py b/src/multi_agent_conductor.py index 07208ea..a15dda4 100644 --- a/src/multi_agent_conductor.py +++ b/src/multi_agent_conductor.py @@ -295,7 +295,8 @@ class ConductorEngine: ticket_id=ticket.id, model_name=model_name, messages=[], - tool_preset=self.tier_usage["Tier 3"]["tool_preset"] + tool_preset=self.tier_usage["Tier 3"]["tool_preset"], + persona_id=ticket.persona_id ) context_files = ticket.context_requirements if ticket.context_requirements else None @@ -410,6 +411,22 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files: ai_client.set_provider(ai_client.get_provider(), context.model_name) ai_client.set_tool_preset(context.tool_preset) + # Apply Persona if specified + if context.persona_id: + from src.personas import PersonaManager + from src import paths + pm = PersonaManager(Path(paths.get_project_personas_path(Path.cwd())) if paths.get_project_personas_path(Path.cwd()).exists() else None) + try: + personas = pm.load_all() + if context.persona_id in personas: + persona = personas[context.persona_id] + if persona.system_prompt: + ai_client.set_custom_system_prompt(persona.system_prompt) + if persona.bias_profile: + ai_client.set_bias_profile(persona.bias_profile) + except Exception as e: + print(f"[WARN] Failed to load persona {context.persona_id}: {e}") + # Check for abort BEFORE any major work if engine and hasattr(engine, "_abort_events"): abort_event = engine._abort_events.get(ticket.id) diff --git a/tests/test_persona_id.py b/tests/test_persona_id.py new file mode 100644 index 0000000..c5470eb --- /dev/null +++ b/tests/test_persona_id.py @@ -0,0 +1,28 @@ +import pytest +from src.models import Ticket, WorkerContext + + +def test_ticket_persona_id_serialization(): + ticket = Ticket( + id="test-1", description="Test task", persona_id="SecuritySpecialist" + ) + data = ticket.to_dict() + assert data["persona_id"] == "SecuritySpecialist" + + +def test_ticket_persona_id_deserialization(): + data = {"id": "test-2", "description": "Test task 2", "persona_id": "CodeReviewer"} + ticket = Ticket.from_dict(data) + assert ticket.persona_id == "CodeReviewer" + + +def test_ticket_persona_id_default(): + ticket = Ticket(id="test-3", description="Test") + assert ticket.persona_id is None + + +def test_worker_context_persona_id(): + ctx = WorkerContext( + ticket_id="test-1", model_name="gemini-2.5-flash", persona_id="DebugHelper" + ) + assert ctx.persona_id == "DebugHelper"