trinity-agent 0.2.2__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/PKG-INFO +1 -1
  2. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/pyproject.toml +1 -1
  3. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/__init__.py +1 -1
  4. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/deliberation/protocol.py +291 -218
  5. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/orchestrator.py +11 -0
  6. trinity_agent-0.3.0/src/trinity/tui/app.py +584 -0
  7. trinity_agent-0.3.0/src/trinity/tui/events.py +76 -0
  8. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/tui/session.py +39 -54
  9. trinity_agent-0.3.0/src/trinity/tui/theme.py +88 -0
  10. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_tui.py +194 -0
  11. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_tui_session.py +45 -0
  12. trinity_agent-0.2.2/src/trinity/tui/app.py +0 -396
  13. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/.gitignore +0 -0
  14. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/LICENSE +0 -0
  15. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/README.md +0 -0
  16. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/docs/checkpoint.md +0 -0
  17. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/docs/phase-6-plan.md +0 -0
  18. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/docs/reference-architecture.md +0 -0
  19. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/docs/test-results/phase-1-T.md +0 -0
  20. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/docs/test-results/phase-2-T.md +0 -0
  21. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/docs/test-results/phase-3-T.md +0 -0
  22. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/docs/test-results/phase-4-T.md +0 -0
  23. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/docs/test-results/phase-5-T.md +0 -0
  24. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/docs/test-results/phase-6-T.md +0 -0
  25. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/__main__.py +0 -0
  26. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/agents/__init__.py +0 -0
  27. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/agents/base.py +0 -0
  28. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/agents/claude_agent.py +0 -0
  29. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/agents/codex_agent.py +0 -0
  30. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/agents/factory.py +0 -0
  31. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/agents/gemini_agent.py +0 -0
  32. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/cli.py +0 -0
  33. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/completion/__init__.py +0 -0
  34. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/completion/base.py +0 -0
  35. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/completion/hook.py +0 -0
  36. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/completion/idle.py +0 -0
  37. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/completion/prompt.py +0 -0
  38. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/config.py +0 -0
  39. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/context/__init__.py +0 -0
  40. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/context/monitor.py +0 -0
  41. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/context/rotator.py +0 -0
  42. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/context/shared.py +0 -0
  43. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/deliberation/__init__.py +0 -0
  44. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/deliberation/consensus.py +0 -0
  45. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/deliberation/distributor.py +0 -0
  46. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/error_handler.py +0 -0
  47. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/health/__init__.py +0 -0
  48. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/health/checker.py +0 -0
  49. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/logging.py +0 -0
  50. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/models.py +0 -0
  51. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/retry.py +0 -0
  52. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/setup/__init__.py +0 -0
  53. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/setup/detector.py +0 -0
  54. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/setup/wizard.py +0 -0
  55. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/tmux/__init__.py +0 -0
  56. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/tmux/layout.py +0 -0
  57. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/tmux/pane.py +0 -0
  58. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/tmux/session.py +0 -0
  59. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/tui/__init__.py +0 -0
  60. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/workspace/__init__.py +0 -0
  61. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/workspace/isolation.py +0 -0
  62. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/src/trinity/workspace/managed_home.py +0 -0
  63. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/templates/trinity.config.example +0 -0
  64. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/conftest.py +0 -0
  65. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_agent_factory.py +0 -0
  66. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_claude_agent.py +0 -0
  67. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_cli.py +0 -0
  68. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_cli_detector.py +0 -0
  69. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_cli_v2.py +0 -0
  70. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_codex_agent.py +0 -0
  71. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_completion.py +0 -0
  72. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_config.py +0 -0
  73. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_consensus_v2.py +0 -0
  74. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_context_monitor.py +0 -0
  75. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_deliberation.py +0 -0
  76. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_e2e.py +0 -0
  77. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_error_handling.py +0 -0
  78. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_gemini_agent.py +0 -0
  79. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_health_checker.py +0 -0
  80. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_interactive_claude.py +0 -0
  81. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_logging.py +0 -0
  82. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_managed_home.py +0 -0
  83. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_models.py +0 -0
  84. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_multi_provider.py +0 -0
  85. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_orchestrator.py +0 -0
  86. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_protocol.py +0 -0
  87. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_protocol_v2.py +0 -0
  88. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_retry.py +0 -0
  89. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_session_handoff.py +0 -0
  90. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_session_rotator.py +0 -0
  91. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_setup_wizard.py +0 -0
  92. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_shared_context.py +0 -0
  93. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_tmux.py +0 -0
  94. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_tmux_integration.py +0 -0
  95. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_tmux_layout.py +0 -0
  96. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/tests/test_workspace.py +0 -0
  97. {trinity_agent-0.2.2 → trinity_agent-0.3.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trinity-agent
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Three minds, one context — Multi-agent AI orchestrator for Claude Code, Codex, and Gemini CLI.
5
5
  Project-URL: Homepage, https://github.com/hongdangmoo49/Trinity
6
6
  Project-URL: Repository, https://github.com/hongdangmoo49/Trinity
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "trinity-agent"
7
- version = "0.2.2"
7
+ version = "0.3.0"
8
8
  description = "Three minds, one context — Multi-agent AI orchestrator for Claude Code, Codex, and Gemini CLI."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """Trinity — Three minds, one context."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.3.0"
@@ -1,218 +1,291 @@
1
- """Deliberation protocol — round-based deliberation loop."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- import logging
7
- import time
8
-
9
- from trinity.agents.base import AgentWrapper
10
- from trinity.context.shared import SharedContextEngine
11
- from trinity.deliberation.consensus import ConsensusEngine
12
- from trinity.deliberation.distributor import TaskDistributor
13
- from trinity.models import (
14
- ConsensusResult,
15
- DeliberationMessage,
16
- DeliberationResult,
17
- MessageRole,
18
- TaskAssignment,
19
- )
20
-
21
- logger = logging.getLogger(__name__)
22
-
23
-
24
- class DeliberationProtocol:
25
- """Round-based deliberation: opinions → counter → consensus → tasks.
26
-
27
- Each round:
28
- 1. Build a round-specific prompt for all agents
29
- 2. Send to all agents in parallel (asyncio.gather)
30
- 3. Collect responses write to shared.md
31
- 4. Check for consensus
32
- 5. If reached, distribute tasks. Otherwise, next round.
33
- """
34
-
35
- def __init__(
36
- self,
37
- agents: dict[str, AgentWrapper],
38
- shared: SharedContextEngine,
39
- consensus_engine: ConsensusEngine | None = None,
40
- distributor: TaskDistributor | None = None,
41
- max_rounds: int = 5,
42
- round_timeout: float = 120.0,
43
- tmux_manager=None,
44
- ):
45
- self.agents = agents
46
- self.shared = shared
47
- self.consensus_engine = consensus_engine or ConsensusEngine()
48
- self.distributor = distributor or TaskDistributor()
49
- self.max_rounds = max_rounds
50
- self.round_timeout = round_timeout
51
- self.tmux_manager = tmux_manager
52
-
53
- async def run(self, user_prompt: str) -> DeliberationResult:
54
- """Execute full deliberation loop."""
55
- start_time = time.time()
56
- agent_names = list(self.agents.keys())
57
-
58
- # Initialize shared.md
59
- self.shared.initialize(goal=user_prompt, agent_names=agent_names)
60
-
61
- consensus: ConsensusResult | None = None
62
- round_num = 0
63
-
64
- for round_num in range(1, self.max_rounds + 1):
65
- logger.info(f"=== Round {round_num}/{self.max_rounds} ===")
66
-
67
- # Update tmux pane titles to show round progress
68
- self._update_pane_titles(f"Round {round_num}/{self.max_rounds}")
69
-
70
- # Build prompt for this round
71
- round_prompt = self._build_round_prompt(round_num, user_prompt)
72
-
73
- # Collect opinions from all agents in parallel
74
- opinions = await self._collect_opinions(round_num, round_prompt)
75
-
76
- # Write opinions to shared.md
77
- for name, msg in opinions.items():
78
- self.shared.append_opinion(name, round_num, msg.content)
79
-
80
- # Update message round_num (it was set to 0 in agent)
81
- for name, msg in opinions.items():
82
- msg.round_num = round_num
83
-
84
- # Check consensus
85
- opinion_texts = {name: msg.content for name, msg in opinions.items()}
86
- consensus = self.consensus_engine.evaluate(opinion_texts)
87
-
88
- if consensus.reached:
89
- logger.info(f"Consensus reached at round {round_num}!")
90
- self.shared.update_consensus(consensus.summary)
91
- self._update_pane_titles("✓ Consensus!")
92
- break
93
-
94
- logger.info(f"No consensus yet. Continuing to round {round_num + 1}.")
95
-
96
- # Update pane titles for task distribution phase
97
- self._update_pane_titles("Distributing tasks...")
98
-
99
- # If no consensus after all rounds, force conclusion
100
- if consensus and not consensus.reached:
101
- logger.warning(f"Max rounds ({self.max_rounds}) reached. Forcing conclusion.")
102
- consensus = ConsensusResult(
103
- reached=True, # Force it
104
- agreement_count=consensus.agreement_count,
105
- total_agents=consensus.total_agents,
106
- opinions=consensus.opinions,
107
- summary=f"Forced conclusion after {self.max_rounds} rounds. "
108
- f"Majority opinion selected.",
109
- )
110
- self.shared.update_consensus(consensus.summary)
111
-
112
- # Distribute tasks
113
- tasks = self.distributor.distribute(
114
- consensus_text=consensus.summary if consensus else user_prompt,
115
- agents={name: ag.spec for name, ag in self.agents.items()},
116
- )
117
-
118
- # Write tasks to shared.md
119
- task_dict = {t.agent_name: t.task_description for t in tasks}
120
- self.shared.update_tasks(task_dict)
121
-
122
- # Calculate totals
123
- total_tokens = sum(
124
- ag.context_usage.used for ag in self.agents.values()
125
- )
126
- elapsed = time.time() - start_time
127
-
128
- return DeliberationResult(
129
- user_prompt=user_prompt,
130
- rounds_completed=round_num,
131
- consensus=consensus,
132
- tasks=tasks,
133
- total_tokens_used=total_tokens,
134
- duration_seconds=elapsed,
135
- )
136
-
137
- async def _collect_opinions(
138
- self, round_num: int, prompt: str
139
- ) -> dict[str, DeliberationMessage]:
140
- """Send prompt to all agents in parallel and collect responses."""
141
- tasks = {
142
- name: agent.send_and_wait(prompt, timeout=self.round_timeout)
143
- for name, agent in self.agents.items()
144
- }
145
-
146
- results = await asyncio.gather(
147
- *tasks.values(), return_exceptions=True
148
- )
149
-
150
- opinions: dict[str, DeliberationMessage] = {}
151
- for name, result in zip(tasks.keys(), results):
152
- if isinstance(result, Exception):
153
- logger.error(f"[{name}] Error in round {round_num}: {result}")
154
- opinions[name] = DeliberationMessage(
155
- source=name,
156
- target="all",
157
- round_num=round_num,
158
- role=MessageRole.OPINION,
159
- content=f"[Error: {result}]",
160
- )
161
- elif isinstance(result, DeliberationMessage):
162
- result.round_num = round_num
163
- opinions[name] = result
164
- else:
165
- logger.warning(f"[{name}] Unexpected result type: {type(result)}")
166
-
167
- return opinions
168
-
169
- def _build_round_prompt(self, round_num: int, user_prompt: str) -> str:
170
- """Build the prompt for a specific deliberation round."""
171
- if round_num == 1:
172
- return (
173
- f"Read the shared context below for background.\n\n"
174
- f"User's request: {user_prompt}\n\n"
175
- f"Share your initial opinion. Be specific and concise.\n"
176
- f"State your recommendation and key reasoning.\n"
177
- f"Keep your response under 500 words."
178
- )
179
- else:
180
- # Read previous round opinions from shared.md
181
- prev_section = self.shared.read_section(f"Round {round_num - 1} Opinions")
182
- prev_context = prev_section or "(previous round opinions not available)"
183
-
184
- return (
185
- f"Previous round opinions:\n\n"
186
- f"{prev_context}\n\n"
187
- f"---\n\n"
188
- f"For each other agent's opinion above, state whether you AGREE or DISAGREE "
189
- f"and explain why. If you disagree, propose an alternative.\n"
190
- f"End your response with either 'I AGREE with [name]' or your counter-proposal.\n"
191
- f"Keep your response under 300 words."
192
- )
193
-
194
- def _update_pane_titles(self, status_text: str) -> None:
195
- """Update tmux pane titles to show round progress (Phase 2 feature)."""
196
- if not self.tmux_manager:
197
- return
198
-
199
- import subprocess
200
-
201
- for name in self.agents:
202
- pane = self.tmux_manager.get_pane(name)
203
- if pane:
204
- try:
205
- subprocess.run(
206
- [
207
- "tmux",
208
- "select-pane",
209
- "-t",
210
- pane.pane_id,
211
- "-T",
212
- f"{name}: {status_text}",
213
- ],
214
- capture_output=True,
215
- timeout=5,
216
- )
217
- except Exception:
218
- pass # Non-critical — don't fail deliberation for title update
1
+ """Deliberation protocol — round-based deliberation loop."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import time
8
+ from typing import Callable
9
+
10
+ from trinity.agents.base import AgentWrapper
11
+ from trinity.context.shared import SharedContextEngine
12
+ from trinity.deliberation.consensus import ConsensusEngine
13
+ from trinity.deliberation.distributor import TaskDistributor
14
+ from trinity.models import (
15
+ ConsensusResult,
16
+ DeliberationMessage,
17
+ DeliberationResult,
18
+ MessageRole,
19
+ TaskAssignment,
20
+ )
21
+ from trinity.tui.events import TUIEvent, TUIEventType
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class DeliberationProtocol:
27
+ """Round-based deliberation: opinions → counter → consensus → tasks.
28
+
29
+ Each round:
30
+ 1. Build a round-specific prompt for all agents
31
+ 2. Send to all agents in parallel (with per-agent completion streaming)
32
+ 3. Collect responses write to shared.md
33
+ 4. Check for consensus
34
+ 5. If reached, distribute tasks. Otherwise, next round.
35
+
36
+ When an event_callback is provided, events are emitted for each
37
+ agent completion, round transition, and consensus evaluation,
38
+ enabling real-time TUI updates.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ agents: dict[str, AgentWrapper],
44
+ shared: SharedContextEngine,
45
+ consensus_engine: ConsensusEngine | None = None,
46
+ distributor: TaskDistributor | None = None,
47
+ max_rounds: int = 5,
48
+ round_timeout: float = 120.0,
49
+ tmux_manager=None,
50
+ event_callback: Callable[[TUIEvent], None] | None = None,
51
+ ):
52
+ self.agents = agents
53
+ self.shared = shared
54
+ self.consensus_engine = consensus_engine or ConsensusEngine()
55
+ self.distributor = distributor or TaskDistributor()
56
+ self.max_rounds = max_rounds
57
+ self.round_timeout = round_timeout
58
+ self.tmux_manager = tmux_manager
59
+ self._event_callback = event_callback
60
+
61
+ def _emit(self, event_type: TUIEventType, **kwargs) -> None:
62
+ """Emit a TUI event if callback is registered."""
63
+ if self._event_callback:
64
+ self._event_callback(TUIEvent(type=event_type, data=kwargs))
65
+
66
+ async def run(self, user_prompt: str) -> DeliberationResult:
67
+ """Execute full deliberation loop."""
68
+ start_time = time.time()
69
+ agent_names = list(self.agents.keys())
70
+
71
+ # Initialize shared.md
72
+ self.shared.initialize(goal=user_prompt, agent_names=agent_names)
73
+
74
+ consensus: ConsensusResult | None = None
75
+ round_num = 0
76
+
77
+ for round_num in range(1, self.max_rounds + 1):
78
+ logger.info(f"=== Round {round_num}/{self.max_rounds} ===")
79
+
80
+ # Emit round start
81
+ self._emit(TUIEventType.ROUND_START, round_num=round_num)
82
+
83
+ # Update tmux pane titles to show round progress
84
+ self._update_pane_titles(f"Round {round_num}/{self.max_rounds}")
85
+
86
+ # Build prompt for this round
87
+ round_prompt = self._build_round_prompt(round_num, user_prompt)
88
+
89
+ # Collect opinions from all agents (with per-agent streaming)
90
+ opinions = await self._collect_opinions(round_num, round_prompt)
91
+
92
+ # Write opinions to shared.md
93
+ for name, msg in opinions.items():
94
+ self.shared.append_opinion(name, round_num, msg.content)
95
+
96
+ # Update message round_num (it was set to 0 in agent)
97
+ for name, msg in opinions.items():
98
+ msg.round_num = round_num
99
+
100
+ # Check consensus
101
+ self._emit(TUIEventType.CONSENSUS_CHECKING, round_num=round_num)
102
+
103
+ opinion_texts = {name: msg.content for name, msg in opinions.items()}
104
+ consensus = self.consensus_engine.evaluate(opinion_texts)
105
+
106
+ if consensus.reached:
107
+ logger.info(f"Consensus reached at round {round_num}!")
108
+ self.shared.update_consensus(consensus.summary)
109
+ self._update_pane_titles("✓ Consensus!")
110
+
111
+ self._emit(
112
+ TUIEventType.CONSENSUS_RESULT,
113
+ reached=True,
114
+ agreement_count=consensus.agreement_count,
115
+ total_agents=consensus.total_agents,
116
+ summary=consensus.summary,
117
+ round_num=round_num,
118
+ )
119
+ break
120
+
121
+ logger.info(f"No consensus yet. Continuing to round {round_num + 1}.")
122
+
123
+ self._emit(
124
+ TUIEventType.CONSENSUS_RESULT,
125
+ reached=False,
126
+ agreement_count=consensus.agreement_count,
127
+ total_agents=consensus.total_agents,
128
+ summary="",
129
+ round_num=round_num,
130
+ )
131
+
132
+ # Update pane titles for task distribution phase
133
+ self._update_pane_titles("Distributing tasks...")
134
+
135
+ # If no consensus after all rounds, force conclusion
136
+ if consensus and not consensus.reached:
137
+ logger.warning(f"Max rounds ({self.max_rounds}) reached. Forcing conclusion.")
138
+ consensus = ConsensusResult(
139
+ reached=True, # Force it
140
+ agreement_count=consensus.agreement_count,
141
+ total_agents=consensus.total_agents,
142
+ opinions=consensus.opinions,
143
+ summary=f"Forced conclusion after {self.max_rounds} rounds. "
144
+ f"Majority opinion selected.",
145
+ )
146
+ self.shared.update_consensus(consensus.summary)
147
+
148
+ # Distribute tasks
149
+ tasks = self.distributor.distribute(
150
+ consensus_text=consensus.summary if consensus else user_prompt,
151
+ agents={name: ag.spec for name, ag in self.agents.items()},
152
+ )
153
+
154
+ # Write tasks to shared.md
155
+ task_dict = {t.agent_name: t.task_description for t in tasks}
156
+ self.shared.update_tasks(task_dict)
157
+
158
+ # Calculate totals
159
+ total_tokens = sum(
160
+ ag.context_usage.used for ag in self.agents.values()
161
+ )
162
+ elapsed = time.time() - start_time
163
+
164
+ self._emit(TUIEventType.DELIBERATION_DONE)
165
+
166
+ return DeliberationResult(
167
+ user_prompt=user_prompt,
168
+ rounds_completed=round_num,
169
+ consensus=consensus,
170
+ tasks=tasks,
171
+ total_tokens_used=total_tokens,
172
+ duration_seconds=elapsed,
173
+ )
174
+
175
+ async def _collect_opinions(
176
+ self, round_num: int, prompt: str
177
+ ) -> dict[str, DeliberationMessage]:
178
+ """Send prompt to all agents in parallel and collect responses.
179
+
180
+ Uses asyncio.wait(FIRST_COMPLETED) instead of asyncio.gather
181
+ to enable per-agent completion streaming via events.
182
+ """
183
+ # Create tasks with agent names attached
184
+ pending: set[asyncio.Task] = set()
185
+ task_to_name: dict[asyncio.Task, str] = {}
186
+
187
+ for name, agent in self.agents.items():
188
+ coro = agent.send_and_wait(prompt, timeout=self.round_timeout)
189
+ task = asyncio.ensure_future(coro)
190
+ task_to_name[task] = name
191
+ pending.add(task)
192
+ self._emit(TUIEventType.AGENT_THINKING, agent=name, round_num=round_num)
193
+
194
+ opinions: dict[str, DeliberationMessage] = {}
195
+
196
+ while pending:
197
+ done, pending = await asyncio.wait(
198
+ pending, return_when=asyncio.FIRST_COMPLETED
199
+ )
200
+
201
+ for task in done:
202
+ name = task_to_name[task]
203
+ try:
204
+ result = task.result()
205
+ except Exception as exc:
206
+ logger.error(f"[{name}] Error in round {round_num}: {exc}")
207
+ opinions[name] = DeliberationMessage(
208
+ source=name,
209
+ target="all",
210
+ round_num=round_num,
211
+ role=MessageRole.OPINION,
212
+ content=f"[Error: {exc}]",
213
+ )
214
+ self._emit(
215
+ TUIEventType.AGENT_ERROR,
216
+ agent=name,
217
+ error=str(exc),
218
+ round_num=round_num,
219
+ )
220
+ continue
221
+
222
+ if isinstance(result, DeliberationMessage):
223
+ result.round_num = round_num
224
+ opinions[name] = result
225
+ self._emit(
226
+ TUIEventType.AGENT_RESPONDED,
227
+ agent=name,
228
+ content=result.content,
229
+ round_num=round_num,
230
+ )
231
+ else:
232
+ logger.warning(f"[{name}] Unexpected result type: {type(result)}")
233
+ self._emit(
234
+ TUIEventType.AGENT_ERROR,
235
+ agent=name,
236
+ error=f"Unexpected result type: {type(result)}",
237
+ round_num=round_num,
238
+ )
239
+
240
+ return opinions
241
+
242
+ def _build_round_prompt(self, round_num: int, user_prompt: str) -> str:
243
+ """Build the prompt for a specific deliberation round."""
244
+ if round_num == 1:
245
+ return (
246
+ f"Read the shared context below for background.\n\n"
247
+ f"User's request: {user_prompt}\n\n"
248
+ f"Share your initial opinion. Be specific and concise.\n"
249
+ f"State your recommendation and key reasoning.\n"
250
+ f"Keep your response under 500 words."
251
+ )
252
+ else:
253
+ # Read previous round opinions from shared.md
254
+ prev_section = self.shared.read_section(f"Round {round_num - 1} Opinions")
255
+ prev_context = prev_section or "(previous round opinions not available)"
256
+
257
+ return (
258
+ f"Previous round opinions:\n\n"
259
+ f"{prev_context}\n\n"
260
+ f"---\n\n"
261
+ f"For each other agent's opinion above, state whether you AGREE or DISAGREE "
262
+ f"and explain why. If you disagree, propose an alternative.\n"
263
+ f"End your response with either 'I AGREE with [name]' or your counter-proposal.\n"
264
+ f"Keep your response under 300 words."
265
+ )
266
+
267
+ def _update_pane_titles(self, status_text: str) -> None:
268
+ """Update tmux pane titles to show round progress (Phase 2 feature)."""
269
+ if not self.tmux_manager:
270
+ return
271
+
272
+ import subprocess
273
+
274
+ for name in self.agents:
275
+ pane = self.tmux_manager.get_pane(name)
276
+ if pane:
277
+ try:
278
+ subprocess.run(
279
+ [
280
+ "tmux",
281
+ "select-pane",
282
+ "-t",
283
+ pane.pane_id,
284
+ "-T",
285
+ f"{name}: {status_text}",
286
+ ],
287
+ capture_output=True,
288
+ timeout=5,
289
+ )
290
+ except Exception:
291
+ pass # Non-critical — don't fail deliberation for title update
@@ -40,6 +40,15 @@ class TrinityOrchestrator:
40
40
  self.context_monitor: ContextMonitor | None = None
41
41
  self.session_rotator: SessionRotator | None = None
42
42
  self.health_checker: HealthChecker | None = None
43
+ self._event_bus = None
44
+
45
+ def set_event_bus(self, bus) -> None:
46
+ """Set the TUI event bus for real-time deliberation updates.
47
+
48
+ Args:
49
+ bus: A TUIEventBus instance from trinity.tui.events.
50
+ """
51
+ self._event_bus = bus
43
52
 
44
53
  def _ensure_initialized(self) -> None:
45
54
  """Lazy initialization: create agents, shared context, protocol."""
@@ -70,6 +79,7 @@ class TrinityOrchestrator:
70
79
  )
71
80
 
72
81
  # Create deliberation protocol
82
+ event_callback = self._event_bus.emit if self._event_bus else None
73
83
  self.protocol = DeliberationProtocol(
74
84
  agents=self.agents,
75
85
  shared=self.shared,
@@ -80,6 +90,7 @@ class TrinityOrchestrator:
80
90
  max_rounds=self.config.max_deliberation_rounds,
81
91
  round_timeout=self.config.round_timeout_seconds,
82
92
  tmux_manager=self.tmux_manager if self.interactive else None,
93
+ event_callback=event_callback,
83
94
  )
84
95
 
85
96
  # Create context monitor and session rotator