@team-agent/installer 0.1.11 → 0.2.0

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 (110) hide show
  1. package/crates/team-agent-core/src/lib.rs +50 -5
  2. package/package.json +1 -1
  3. package/schemas/team.schema.json +1 -0
  4. package/src/team_agent/approvals/__init__.py +65 -0
  5. package/src/team_agent/approvals/constants.py +6 -0
  6. package/src/team_agent/approvals/parsing.py +176 -0
  7. package/src/team_agent/approvals/runtime_prompts.py +171 -0
  8. package/src/team_agent/approvals/status.py +165 -0
  9. package/src/team_agent/cli/__init__.py +135 -0
  10. package/src/team_agent/cli/commands.py +335 -0
  11. package/src/team_agent/cli/e2e.py +202 -0
  12. package/src/team_agent/cli/helpers.py +137 -0
  13. package/src/team_agent/cli/parser.py +470 -0
  14. package/src/team_agent/compiler.py +98 -33
  15. package/src/team_agent/coordinator/__init__.py +53 -0
  16. package/src/team_agent/{coordinator.py → coordinator/__main__.py} +3 -1
  17. package/src/team_agent/coordinator/lifecycle.py +319 -0
  18. package/src/team_agent/coordinator/metadata.py +61 -0
  19. package/src/team_agent/coordinator/paths.py +17 -0
  20. package/src/team_agent/diagnose/__init__.py +48 -0
  21. package/src/team_agent/diagnose/checks.py +101 -0
  22. package/src/team_agent/diagnose/health.py +241 -0
  23. package/src/team_agent/diagnose/preflight.py +194 -0
  24. package/src/team_agent/diagnose/quick_start.py +233 -0
  25. package/src/team_agent/display/__init__.py +61 -0
  26. package/src/team_agent/display/close.py +147 -0
  27. package/src/team_agent/display/ghostty.py +77 -0
  28. package/src/team_agent/display/worker_window.py +110 -0
  29. package/src/team_agent/display/workspace.py +473 -0
  30. package/src/team_agent/launch/__init__.py +41 -0
  31. package/src/team_agent/launch/bootstrap.py +85 -0
  32. package/src/team_agent/launch/config.py +106 -0
  33. package/src/team_agent/launch/core.py +291 -0
  34. package/src/team_agent/launch/requirements.py +57 -0
  35. package/src/team_agent/leader/__init__.py +320 -0
  36. package/src/team_agent/lifecycle/__init__.py +5 -0
  37. package/src/team_agent/lifecycle/agents.py +226 -0
  38. package/src/team_agent/lifecycle/operations.py +321 -0
  39. package/src/team_agent/lifecycle/start.py +360 -0
  40. package/src/team_agent/mcp_server/__init__.py +42 -0
  41. package/src/team_agent/mcp_server/__main__.py +7 -0
  42. package/src/team_agent/mcp_server/contracts.py +148 -0
  43. package/src/team_agent/mcp_server/normalize.py +257 -0
  44. package/src/team_agent/mcp_server/server.py +150 -0
  45. package/src/team_agent/mcp_server/tools.py +205 -0
  46. package/src/team_agent/message_store/__init__.py +23 -0
  47. package/src/team_agent/message_store/agent_health.py +109 -0
  48. package/src/team_agent/{message_store.py → message_store/core.py} +188 -245
  49. package/src/team_agent/message_store/result_watchers.py +102 -0
  50. package/src/team_agent/message_store/schema.py +266 -0
  51. package/src/team_agent/messaging/__init__.py +1 -0
  52. package/src/team_agent/messaging/activity_detector.py +190 -0
  53. package/src/team_agent/messaging/delivery.py +128 -0
  54. package/src/team_agent/messaging/deps.py +263 -0
  55. package/src/team_agent/messaging/idle_alerts.py +217 -0
  56. package/src/team_agent/messaging/internal_delivery.py +46 -0
  57. package/src/team_agent/messaging/leader.py +317 -0
  58. package/src/team_agent/messaging/leader_panes.py +343 -0
  59. package/src/team_agent/messaging/result_delivery.py +300 -0
  60. package/src/team_agent/messaging/results.py +456 -0
  61. package/src/team_agent/messaging/scheduler.py +418 -0
  62. package/src/team_agent/messaging/send.py +493 -0
  63. package/src/team_agent/messaging/tmux_io.py +337 -0
  64. package/src/team_agent/messaging/tmux_prompt.py +229 -0
  65. package/src/team_agent/orchestrator/__init__.py +376 -0
  66. package/src/team_agent/orchestrator/plan.py +122 -0
  67. package/src/team_agent/orchestrator/state.py +128 -0
  68. package/src/team_agent/profiles/__init__.py +82 -0
  69. package/src/team_agent/profiles/constants.py +19 -0
  70. package/src/team_agent/profiles/core.py +407 -0
  71. package/src/team_agent/profiles/helpers.py +69 -0
  72. package/src/team_agent/profiles/provider_env.py +188 -0
  73. package/src/team_agent/profiles/smoke.py +201 -0
  74. package/src/team_agent/provider_cli/__init__.py +43 -0
  75. package/src/team_agent/provider_cli/adapter.py +167 -0
  76. package/src/team_agent/provider_cli/base.py +48 -0
  77. package/src/team_agent/provider_cli/claude.py +457 -0
  78. package/src/team_agent/provider_cli/codex.py +319 -0
  79. package/src/team_agent/provider_cli/copilot.py +8 -0
  80. package/src/team_agent/provider_cli/fake.py +39 -0
  81. package/src/team_agent/provider_cli/gemini.py +95 -0
  82. package/src/team_agent/provider_cli/opencode.py +8 -0
  83. package/src/team_agent/provider_cli/prompt.py +62 -0
  84. package/src/team_agent/provider_cli/registry.py +18 -0
  85. package/src/team_agent/provider_cli/unsupported.py +32 -0
  86. package/src/team_agent/providers.py +67 -949
  87. package/src/team_agent/quality_gates.py +104 -0
  88. package/src/team_agent/restart/__init__.py +34 -0
  89. package/src/team_agent/restart/orchestration.py +328 -0
  90. package/src/team_agent/restart/selection.py +89 -0
  91. package/src/team_agent/restart/snapshot.py +70 -0
  92. package/src/team_agent/runtime.py +802 -5893
  93. package/src/team_agent/rust_core.py +22 -5
  94. package/src/team_agent/sessions/__init__.py +25 -0
  95. package/src/team_agent/sessions/capture.py +93 -0
  96. package/src/team_agent/sessions/inventory.py +44 -0
  97. package/src/team_agent/sessions/resume.py +135 -0
  98. package/src/team_agent/spec.py +3 -1
  99. package/src/team_agent/state.py +204 -4
  100. package/src/team_agent/status/__init__.py +63 -0
  101. package/src/team_agent/status/approvals.py +52 -0
  102. package/src/team_agent/status/compact.py +158 -0
  103. package/src/team_agent/status/constants.py +18 -0
  104. package/src/team_agent/status/inbox.py +28 -0
  105. package/src/team_agent/status/peek.py +117 -0
  106. package/src/team_agent/status/queries.py +168 -0
  107. package/src/team_agent/terminal.py +57 -0
  108. package/src/team_agent/cli.py +0 -858
  109. package/src/team_agent/mcp_server.py +0 -579
  110. package/src/team_agent/profiles.py +0 -882
@@ -0,0 +1,493 @@
1
+ from __future__ import annotations
2
+
3
+ from team_agent.messaging.deps import (
4
+ EventLog,
5
+ MessageStore,
6
+ RuntimeError,
7
+ _capture_missing_sessions,
8
+ _current_task_for_agent,
9
+ _deliver_pending_message,
10
+ _find_agent,
11
+ _find_task,
12
+ _is_leader_sender,
13
+ _is_leader_target,
14
+ _is_runtime_team_agent,
15
+ _leader_id,
16
+ _message_by_id,
17
+ _mirror_peer_message_to_leader,
18
+ _runtime_lock,
19
+ _runtime_team_agent_ids,
20
+ _send_to_leader_receiver,
21
+ check_team_owner,
22
+ load_runtime_state,
23
+ load_spec,
24
+ missing_tools,
25
+ route_task,
26
+ save_team_scoped_state,
27
+ select_runtime_state,
28
+ ambiguous_team_target_result,
29
+ team_state_key,
30
+ update_task_status,
31
+ )
32
+
33
+ from pathlib import Path
34
+ from typing import Any
35
+
36
+ def send_message(
37
+ workspace: Path,
38
+ target: str | list[str] | None,
39
+ content: str,
40
+ task_id: str | None = None,
41
+ sender: str = "leader",
42
+ requires_ack: bool = True,
43
+ confirm_human: bool = False,
44
+ wait_visible: bool = True,
45
+ timeout: float = 30.0,
46
+ lock_timeout: float = 5.0,
47
+ watch_result: bool = False,
48
+ block_until_delivered: bool = True,
49
+ team: str | None = None,
50
+ ) -> dict[str, Any]:
51
+ with _runtime_lock(workspace, "send", timeout=lock_timeout):
52
+ return _send_message_unlocked(
53
+ workspace,
54
+ target,
55
+ content,
56
+ task_id=task_id,
57
+ sender=sender,
58
+ requires_ack=requires_ack,
59
+ confirm_human=confirm_human,
60
+ wait_visible=wait_visible,
61
+ timeout=timeout,
62
+ watch_result=watch_result,
63
+ block_until_delivered=block_until_delivered,
64
+ team=team,
65
+ )
66
+
67
+
68
+ def _send_message_unlocked(
69
+ workspace: Path,
70
+ target: str | list[str] | None,
71
+ content: str,
72
+ task_id: str | None = None,
73
+ sender: str = "leader",
74
+ requires_ack: bool = True,
75
+ confirm_human: bool = False,
76
+ wait_visible: bool = True,
77
+ timeout: float = 30.0,
78
+ watch_result: bool = False,
79
+ block_until_delivered: bool = True,
80
+ team: str | None = None,
81
+ ) -> dict[str, Any]:
82
+ if team is None:
83
+ ambiguous = ambiguous_team_target_result(load_runtime_state(workspace))
84
+ if ambiguous:
85
+ return ambiguous
86
+ state = select_runtime_state(workspace, team)
87
+ gate = check_team_owner(state)
88
+ if gate:
89
+ return gate
90
+ spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
91
+ spec = load_spec(spec_path)
92
+ event_log = EventLog(workspace)
93
+ owner_team_id = team_state_key(state)
94
+ leader_id = _leader_id(state, spec)
95
+
96
+ if isinstance(target, list):
97
+ if watch_result:
98
+ return {"ok": False, "status": "failed", "reason": "watch_result_not_supported_for_fanout", "to": target}
99
+ return _fanout_message_unlocked(
100
+ workspace,
101
+ state,
102
+ spec,
103
+ event_log,
104
+ target,
105
+ content,
106
+ task_id=task_id,
107
+ sender=sender,
108
+ requires_ack=requires_ack,
109
+ wait_visible=wait_visible,
110
+ timeout=timeout,
111
+ block_until_delivered=block_until_delivered,
112
+ owner_team_id=owner_team_id,
113
+ )
114
+
115
+ if target == "*":
116
+ if watch_result:
117
+ return {"ok": False, "status": "failed", "reason": "watch_result_not_supported_for_broadcast", "to": target}
118
+ return _broadcast_message_unlocked(
119
+ workspace,
120
+ state,
121
+ spec,
122
+ event_log,
123
+ content,
124
+ task_id=task_id,
125
+ sender=sender,
126
+ requires_ack=requires_ack,
127
+ wait_visible=wait_visible,
128
+ timeout=timeout,
129
+ block_until_delivered=block_until_delivered,
130
+ owner_team_id=owner_team_id,
131
+ )
132
+
133
+ return _send_single_message_unlocked(
134
+ workspace,
135
+ state,
136
+ spec,
137
+ event_log,
138
+ target,
139
+ content,
140
+ task_id=task_id,
141
+ sender=sender,
142
+ requires_ack=requires_ack,
143
+ confirm_human=confirm_human,
144
+ wait_visible=wait_visible,
145
+ timeout=timeout,
146
+ watch_result=watch_result,
147
+ block_until_delivered=block_until_delivered,
148
+ owner_team_id=owner_team_id,
149
+ )
150
+
151
+
152
+ def _send_single_message_unlocked(
153
+ workspace: Path,
154
+ state: dict[str, Any],
155
+ spec: dict[str, Any],
156
+ event_log: EventLog,
157
+ target: str | None,
158
+ content: str,
159
+ *,
160
+ task_id: str | None = None,
161
+ sender: str = "leader",
162
+ requires_ack: bool = True,
163
+ confirm_human: bool = False,
164
+ wait_visible: bool = True,
165
+ timeout: float = 30.0,
166
+ watch_result: bool = False,
167
+ mirror_peer: bool = True,
168
+ route_task_id: bool = True,
169
+ block_until_delivered: bool = True,
170
+ owner_team_id: str | None = None,
171
+ ) -> dict[str, Any]:
172
+ leader_id = _leader_id(state, spec)
173
+
174
+ if _is_leader_target(target, leader_id) and not _is_leader_sender(sender, leader_id):
175
+ return _send_to_leader_receiver(workspace, state, leader_id, content, task_id, sender, requires_ack, event_log)
176
+
177
+ if task_id and route_task_id:
178
+ task = _find_task(state.get("tasks", []), task_id)
179
+ if task.get("human_confirmation") and not task.get("human_confirmed"):
180
+ if not confirm_human:
181
+ update_task_status(state["tasks"], task_id, "blocked", "human confirmation required before dispatch")
182
+ save_team_scoped_state(workspace, state)
183
+ event_log.write(
184
+ "send.human_confirmation_required",
185
+ task_id=task_id,
186
+ requested_target=target,
187
+ )
188
+ return {
189
+ "ok": False,
190
+ "status": "blocked",
191
+ "reason": "human_confirmation_required",
192
+ "task_id": task_id,
193
+ }
194
+ task["human_confirmed"] = True
195
+ event_log.write("send.human_confirmation_granted", task_id=task_id, confirmed_by=sender)
196
+ route = route_task(spec, task)
197
+ routed_target = route["agent_id"]
198
+ requested_target = target
199
+ target = target or routed_target
200
+ task["assignee"] = target
201
+ event_log.write(
202
+ "routing.decision",
203
+ source="send",
204
+ task_id=task_id,
205
+ route_agent=routed_target,
206
+ selected_agent=target,
207
+ reason=route["reason"],
208
+ manual_override=bool(requested_target and requested_target != routed_target),
209
+ )
210
+ agent = _find_agent(spec, target)
211
+ if agent:
212
+ missing = missing_tools(agent, task)
213
+ if missing:
214
+ update_task_status(state["tasks"], task_id, "blocked", f"missing permissions: {', '.join(missing)}")
215
+ save_team_scoped_state(workspace, state)
216
+ event_log.write(
217
+ "send.blocked_missing_permissions",
218
+ task_id=task_id,
219
+ agent_id=target,
220
+ missing_tools=missing,
221
+ )
222
+ return {
223
+ "ok": False,
224
+ "status": "blocked",
225
+ "task_id": task_id,
226
+ "agent_id": target,
227
+ "missing_tools": missing,
228
+ }
229
+
230
+ if not target:
231
+ raise RuntimeError("send requires target or --task")
232
+ if not _is_leader_target(target, leader_id) and not _is_runtime_team_agent(target, state, spec):
233
+ event_log.write("send.target_rejected", sender=sender, target=target, reason="target_not_in_team")
234
+ return {"ok": False, "status": "refused", "reason": "target_not_in_team", "from": sender, "to": target}
235
+ store = MessageStore(workspace)
236
+ message_id = store.create_message(task_id, sender, target, content, requires_ack=requires_ack, owner_team_id=owner_team_id)
237
+ if not block_until_delivered:
238
+ watch: dict[str, Any] | None = None
239
+ if watch_result:
240
+ watch_task_id = task_id or _current_task_for_agent(state.get("tasks", []), str(target))
241
+ watcher_id = store.create_result_watcher(watch_task_id, str(target), message_id, leader_id, owner_team_id=owner_team_id)
242
+ watch = {
243
+ "status": "registered",
244
+ "watcher_id": watcher_id,
245
+ "task_id": watch_task_id,
246
+ "agent_id": target,
247
+ "notice": (
248
+ "Team Agent will deliver this message when the worker is available, "
249
+ "then collect the result and notify the leader when this task reports completion."
250
+ ),
251
+ }
252
+ event_log.write(
253
+ "result_watcher.created",
254
+ watcher_id=watcher_id,
255
+ task_id=watch_task_id,
256
+ agent_id=target,
257
+ message_id=message_id,
258
+ )
259
+ _capture_missing_sessions(workspace, state, event_log, timeout_s=0.0, log_miss=False)
260
+ save_team_scoped_state(workspace, state)
261
+ event_log.write(
262
+ "send.durably_stored",
263
+ message_id=message_id,
264
+ target=target,
265
+ sender=sender,
266
+ task_id=task_id,
267
+ )
268
+ result = {
269
+ "ok": True,
270
+ "message_id": message_id,
271
+ "status": "queued",
272
+ "message_status": "accepted",
273
+ "to": target,
274
+ "queued": True,
275
+ "durably_stored": True,
276
+ "reason": "deferred_to_coordinator",
277
+ "visible": False,
278
+ "submitted": False,
279
+ }
280
+ if watch is not None:
281
+ result["watch_result"] = True
282
+ result["watch"] = watch
283
+ return result
284
+ delivered_result = _deliver_pending_message(workspace, state, message_id, wait_visible=wait_visible, timeout=timeout)
285
+ row = _message_by_id(store, message_id)
286
+ message_status = row["status"] if row else delivered_result.get("message_status", delivered_result.get("status", "accepted"))
287
+ if (
288
+ mirror_peer
289
+ and not _is_leader_sender(sender, leader_id)
290
+ and not _is_leader_target(target, leader_id)
291
+ and delivered_result.get("ok")
292
+ and not delivered_result.get("queued")
293
+ ):
294
+ _mirror_peer_message_to_leader(workspace, state, sender, target, content, task_id, event_log)
295
+ watch: dict[str, Any] | None = None
296
+ if watch_result and delivered_result.get("ok"):
297
+ watch_task_id = task_id or _current_task_for_agent(state.get("tasks", []), str(target))
298
+ watcher_id = store.create_result_watcher(watch_task_id, str(target), message_id, leader_id, owner_team_id=owner_team_id)
299
+ watch = {
300
+ "status": "registered",
301
+ "watcher_id": watcher_id,
302
+ "task_id": watch_task_id,
303
+ "agent_id": target,
304
+ "notice": (
305
+ "Team Agent will deliver this message when the worker is available, "
306
+ "then collect the result and notify the leader when this task reports completion."
307
+ if delivered_result.get("queued")
308
+ else "Team Agent will collect the result and notify the leader when this task reports completion."
309
+ ),
310
+ }
311
+ event_log.write(
312
+ "result_watcher.created",
313
+ watcher_id=watcher_id,
314
+ task_id=watch_task_id,
315
+ agent_id=target,
316
+ message_id=message_id,
317
+ )
318
+ _capture_missing_sessions(workspace, state, event_log, timeout_s=0.0, log_miss=False)
319
+ save_team_scoped_state(workspace, state)
320
+ result = {
321
+ "ok": bool(delivered_result.get("ok")),
322
+ "message_id": message_id,
323
+ "status": delivered_result.get("status", message_status),
324
+ "message_status": message_status,
325
+ "to": target,
326
+ "visible": message_status in {"visible", "submitted"},
327
+ "submitted": message_status in {"visible", "submitted", "submitted_unverified", "delivered", "acknowledged"},
328
+ "verification": delivered_result.get("verification"),
329
+ "submit_verification": delivered_result.get("submit_verification"),
330
+ "turn_verification": delivered_result.get("turn_verification"),
331
+ }
332
+ if delivered_result.get("queued"):
333
+ result["queued"] = True
334
+ result["reason"] = delivered_result.get("reason")
335
+ if delivered_result.get("warning"):
336
+ result["warning"] = delivered_result["warning"]
337
+ for key in ("paste_attempts", "submit_attempts"):
338
+ if key in delivered_result:
339
+ result[key] = delivered_result[key]
340
+ if watch is not None:
341
+ result["watch_result"] = True
342
+ result["watch"] = watch
343
+ return result
344
+
345
+
346
+ def _broadcast_message_unlocked(
347
+ workspace: Path,
348
+ state: dict[str, Any],
349
+ spec: dict[str, Any],
350
+ event_log: EventLog,
351
+ content: str,
352
+ *,
353
+ task_id: str | None,
354
+ sender: str,
355
+ requires_ack: bool,
356
+ wait_visible: bool,
357
+ timeout: float,
358
+ block_until_delivered: bool = True,
359
+ owner_team_id: str | None = None,
360
+ ) -> dict[str, Any]:
361
+ targets = _broadcast_targets(state, spec, sender)
362
+ if not targets:
363
+ event_log.write("send.broadcast_skipped", sender=sender, reason="no_team_recipients")
364
+ return {"ok": False, "status": "failed", "reason": "no_team_recipients", "to": "*", "targets": []}
365
+ event_log.write("send.broadcast_start", sender=sender, targets=targets, task_id=task_id)
366
+ deliveries: list[dict[str, Any]] = []
367
+ for recipient in targets:
368
+ result = _send_single_message_unlocked(
369
+ workspace,
370
+ state,
371
+ spec,
372
+ event_log,
373
+ recipient,
374
+ content,
375
+ task_id=task_id,
376
+ sender=sender,
377
+ requires_ack=requires_ack,
378
+ confirm_human=False,
379
+ wait_visible=wait_visible,
380
+ timeout=timeout,
381
+ watch_result=False,
382
+ mirror_peer=False,
383
+ route_task_id=False,
384
+ block_until_delivered=block_until_delivered,
385
+ owner_team_id=owner_team_id,
386
+ )
387
+ deliveries.append(_compact_broadcast_delivery(result))
388
+ failed = [item for item in deliveries if not item.get("ok")]
389
+ status = "broadcast_delivered" if not failed else "broadcast_partial"
390
+ event_log.write(
391
+ "send.broadcast_complete",
392
+ sender=sender,
393
+ targets=targets,
394
+ status=status,
395
+ delivered_count=len(deliveries) - len(failed),
396
+ failed_count=len(failed),
397
+ )
398
+ return {
399
+ "ok": not failed,
400
+ "status": status,
401
+ "to": "*",
402
+ "targets": targets,
403
+ "delivered_count": len(deliveries) - len(failed),
404
+ "failed_count": len(failed),
405
+ "deliveries": deliveries,
406
+ }
407
+
408
+
409
+ def _fanout_message_unlocked(
410
+ workspace: Path,
411
+ state: dict[str, Any],
412
+ spec: dict[str, Any],
413
+ event_log: EventLog,
414
+ targets: list[str],
415
+ content: str,
416
+ *,
417
+ task_id: str | None,
418
+ sender: str,
419
+ requires_ack: bool,
420
+ wait_visible: bool,
421
+ timeout: float,
422
+ block_until_delivered: bool = True,
423
+ owner_team_id: str | None = None,
424
+ ) -> dict[str, Any]:
425
+ raw_recipients = [target for target in targets if target]
426
+ recipients = list(dict.fromkeys(raw_recipients))
427
+ if not recipients:
428
+ return {"ok": False, "status": "failed", "reason": "no_fanout_recipients", "to": targets, "targets": []}
429
+ if len(raw_recipients) != len(recipients):
430
+ event_log.write("send.fanout_dedupe", sender=sender, task_id=task_id,
431
+ raw_targets=raw_recipients, deduped_targets=recipients,
432
+ duplicate_count=len(raw_recipients) - len(recipients))
433
+ event_log.write("send.fanout_start", sender=sender, targets=recipients, task_id=task_id)
434
+ deliveries: list[dict[str, Any]] = []
435
+ by_recipient: dict[str, dict[str, Any]] = {}
436
+ for recipient in recipients:
437
+ result = _send_single_message_unlocked(
438
+ workspace,
439
+ state,
440
+ spec,
441
+ event_log,
442
+ recipient,
443
+ content,
444
+ task_id=task_id,
445
+ sender=sender,
446
+ requires_ack=requires_ack,
447
+ confirm_human=False,
448
+ wait_visible=wait_visible,
449
+ timeout=timeout,
450
+ watch_result=False,
451
+ mirror_peer=False,
452
+ route_task_id=False,
453
+ block_until_delivered=block_until_delivered,
454
+ owner_team_id=owner_team_id,
455
+ )
456
+ compact = _compact_fanout_delivery(result)
457
+ deliveries.append(compact)
458
+ by_recipient[recipient] = compact
459
+ failed = [item for item in deliveries if not item.get("delivered")]
460
+ status = "fanout_delivered" if not failed else "fanout_partial"
461
+ aggregate = {
462
+ "ok": True,
463
+ "status": status,
464
+ "to": recipients,
465
+ "targets": recipients,
466
+ "delivered_count": len(deliveries) - len(failed),
467
+ "failed_count": len(failed),
468
+ "deliveries": deliveries,
469
+ "recipients": by_recipient,
470
+ }
471
+ event_log.write("send.fanout_status", **aggregate)
472
+ return aggregate
473
+
474
+
475
+ def _broadcast_targets(state: dict[str, Any], spec: dict[str, Any], sender: str) -> list[str]:
476
+ leader_id = _leader_id(state, spec)
477
+ targets = [leader_id, *_runtime_team_agent_ids(state, spec)]
478
+ if _is_leader_sender(sender, leader_id):
479
+ excluded = {leader_id}
480
+ else:
481
+ excluded = {sender}
482
+ return [target for target in targets if target not in excluded]
483
+
484
+
485
+ def _compact_broadcast_delivery(result: dict[str, Any]) -> dict[str, Any]:
486
+ keys = ["ok", "status", "message_id", "to", "reason", "channel"]
487
+ return {key: result[key] for key in keys if key in result}
488
+
489
+
490
+ def _compact_fanout_delivery(result: dict[str, Any]) -> dict[str, Any]:
491
+ compact = _compact_broadcast_delivery(result)
492
+ compact["delivered"] = bool(result.get("submitted") or result.get("visible") or result.get("status") in {"submitted", "visible", "delivered", "acknowledged"})
493
+ return compact