@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,456 @@
1
+ from __future__ import annotations
2
+
3
+ from team_agent.messaging.deps import (
4
+ EventLog,
5
+ MessageStore,
6
+ RuntimeError,
7
+ ValidationError,
8
+ _capture_missing_sessions,
9
+ _deliver_pending_messages,
10
+ _find_task,
11
+ _find_task_or_none,
12
+ _handle_provider_runtime_prompts,
13
+ _handle_provider_startup_prompts,
14
+ _is_message_scoped_result,
15
+ _leader_id,
16
+ _leader_receiver_is_direct,
17
+ _notify_leader_of_report_result as _runtime_notify_leader_of_report_result,
18
+ _rediscover_leader_receiver,
19
+ _refresh_agent_runtime_statuses,
20
+ _result_status_to_task_status,
21
+ _validate_leader_receiver,
22
+ copy,
23
+ datetime,
24
+ json,
25
+ load_runtime_state,
26
+ load_spec,
27
+ save_runtime_state,
28
+ save_team_scoped_state,
29
+ start_coordinator,
30
+ team_state_key,
31
+ timezone,
32
+ update_task_status,
33
+ validate_result_envelope,
34
+ write_team_state,
35
+ )
36
+ from team_agent.messaging.result_delivery import (
37
+ format_result_watcher_notification as _format_result_watcher_notification,
38
+ notify_result_watchers as _notify_result_watchers,
39
+ retry_result_deliveries as _retry_result_deliveries,
40
+ )
41
+
42
+ from pathlib import Path
43
+ from typing import Any
44
+
45
+ def collect(workspace: Path, result_file: Path | None = None, *, ensure_coordinator: bool = True) -> dict[str, Any]:
46
+ state = load_runtime_state(workspace)
47
+ spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
48
+ spec = load_spec(spec_path)
49
+ store = MessageStore(workspace)
50
+ event_log = EventLog(workspace)
51
+ _refresh_agent_runtime_statuses(workspace, state, event_log)
52
+ _handle_provider_startup_prompts(workspace, state, event_log)
53
+ _handle_provider_runtime_prompts(workspace, state, event_log)
54
+ delivered_messages = _deliver_pending_messages(workspace, state, event_log)
55
+ _capture_missing_sessions(workspace, state, event_log, timeout_s=0.0, log_miss=False)
56
+
57
+ invalid_results: list[dict[str, Any]] = []
58
+ if result_file:
59
+ envelope: Any = None
60
+ try:
61
+ envelope = json.loads(result_file.read_text(encoding="utf-8"))
62
+ validate_result_envelope(envelope)
63
+ except (json.JSONDecodeError, ValidationError) as exc:
64
+ invalid_results.append(
65
+ _record_invalid_result(
66
+ event_log,
67
+ error=str(exc),
68
+ result_file=result_file,
69
+ envelope=envelope,
70
+ )
71
+ )
72
+ else:
73
+ store.add_result(envelope)
74
+
75
+ rows = store.results(uncollected_only=True)
76
+ valid_rows: list[tuple[dict[str, Any], dict[str, Any], dict[str, Any] | None]] = []
77
+ for row in rows:
78
+ envelope: Any = None
79
+ try:
80
+ envelope = json.loads(row["envelope"])
81
+ validate_result_envelope(envelope)
82
+ task = _find_task_or_none(state["tasks"], envelope["task_id"])
83
+ if task is None and not _is_message_scoped_result(store, envelope):
84
+ raise RuntimeError(f"unknown task id: {envelope['task_id']}")
85
+ except (json.JSONDecodeError, ValidationError, RuntimeError) as exc:
86
+ invalid_results.append(
87
+ _record_invalid_result(
88
+ event_log,
89
+ error=str(exc),
90
+ result_id=row["result_id"],
91
+ envelope=envelope,
92
+ )
93
+ )
94
+ store.mark_result_invalid(row["result_id"], str(exc))
95
+ else:
96
+ valid_rows.append((row, envelope, task))
97
+
98
+ if invalid_results:
99
+ save_runtime_state(workspace, state)
100
+ state_path = write_team_state(workspace, spec, state, _team_state_result_entries(store, []))
101
+ coordinator = _ensure_coordinator_after_collect(workspace, state, event_log) if ensure_coordinator else {"ok": False, "status": "not_required"}
102
+ return {
103
+ "ok": False,
104
+ "collected": [],
105
+ "collected_results": [],
106
+ "delivered_messages": delivered_messages,
107
+ "invalid_results": invalid_results,
108
+ "results": store.result_counts(),
109
+ "state_file": str(state_path),
110
+ "coordinator": coordinator,
111
+ }
112
+
113
+ collected: list[dict[str, Any]] = []
114
+ collected_results: list[dict[str, Any]] = []
115
+ next_state = copy.deepcopy(state)
116
+ for row, envelope, task in valid_rows:
117
+ if task is not None:
118
+ next_task = _find_task(next_state["tasks"], envelope["task_id"])
119
+ task_status = _result_status_to_task_status(next_task, envelope["status"])
120
+ update_task_status(
121
+ next_state["tasks"],
122
+ envelope["task_id"],
123
+ task_status,
124
+ envelope.get("summary"),
125
+ envelope.get("artifacts", []),
126
+ )
127
+ next_task["accepted_result_id"] = row["result_id"]
128
+ else:
129
+ task_status = "message_scoped"
130
+ collected.append(envelope)
131
+ collected_results.append(
132
+ {
133
+ "result_id": row["result_id"],
134
+ "task_id": envelope["task_id"],
135
+ "agent_id": envelope["agent_id"],
136
+ "status": envelope["status"],
137
+ "summary": envelope.get("summary"),
138
+ "tests": envelope.get("tests", []),
139
+ "created_at": row.get("created_at"),
140
+ "scope": "task" if task is not None else "message",
141
+ }
142
+ )
143
+ event_log.write(
144
+ "collect.result",
145
+ result_id=row["result_id"],
146
+ task_id=envelope["task_id"],
147
+ status=envelope["status"],
148
+ task_status=task_status,
149
+ retry_count=task.get("retry_count") if task else None,
150
+ retry_limit=task.get("retry_limit") if task else None,
151
+ scope="task" if task is not None else "message",
152
+ )
153
+ state_path = write_team_state(workspace, spec, next_state, _team_state_result_entries(store, collected))
154
+ save_runtime_state(workspace, next_state)
155
+ for row, _, _ in valid_rows:
156
+ store.mark_result_collected(row["result_id"])
157
+ coordinator = _ensure_coordinator_after_collect(workspace, next_state, event_log) if ensure_coordinator else {"ok": False, "status": "not_required"}
158
+ return {
159
+ "ok": not invalid_results,
160
+ "collected": collected,
161
+ "collected_results": collected_results,
162
+ "delivered_messages": delivered_messages,
163
+ "invalid_results": invalid_results,
164
+ "results": store.result_counts(),
165
+ "state_file": str(state_path),
166
+ "coordinator": coordinator,
167
+ }
168
+
169
+
170
+ def _team_state_result_entries(store: MessageStore, collected: list[dict[str, Any]]) -> list[dict[str, Any]]:
171
+ if collected:
172
+ return [{"envelope": env} for env in collected]
173
+ return [{"envelope": row["envelope"]} for row in store.latest_results(limit=5)]
174
+
175
+
176
+ def _ensure_coordinator_after_collect(workspace: Path, state: dict[str, Any], event_log: EventLog) -> dict[str, Any]:
177
+ if not _coordinator_should_run(state):
178
+ return {"ok": False, "status": "not_required"}
179
+ try:
180
+ coordinator = start_coordinator(workspace)
181
+ except Exception as exc:
182
+ coordinator = {"ok": False, "status": "start_failed", "error": str(exc)}
183
+ event_log.write("collect.coordinator_checked", coordinator=coordinator)
184
+ return coordinator
185
+
186
+
187
+ def _coordinator_should_run(state: dict[str, Any]) -> bool:
188
+ return bool(state.get("session_name") or _leader_receiver_is_direct(state.get("leader_receiver", {})))
189
+
190
+
191
+ def report_result(workspace: Path, envelope: dict[str, Any]) -> dict[str, Any]:
192
+ validate_result_envelope(envelope)
193
+ store = MessageStore(workspace)
194
+ owner_team_id = _owner_team_id_for_report(store, envelope)
195
+ result_id = store.add_result(envelope, owner_team_id=owner_team_id)
196
+ acknowledged = store.acknowledge_task_messages(envelope["task_id"], envelope["agent_id"], owner_team_id=owner_team_id)
197
+ if not acknowledged:
198
+ acknowledged = store.acknowledge_message(envelope["task_id"], envelope["agent_id"], owner_team_id=owner_team_id)
199
+ event_log = EventLog(workspace)
200
+ notification = _runtime_notify_leader_of_report_result(workspace, envelope, result_id, event_log, owner_team_id=owner_team_id)
201
+ leader_notified = bool(notification.get("ok")) and notification.get("status") in {"submitted", "visible", "delivered", "acknowledged"}
202
+ event_log.write(
203
+ "mcp.report_result",
204
+ result_id=result_id,
205
+ task_id=envelope["task_id"],
206
+ agent_id=envelope["agent_id"],
207
+ acknowledged_messages=acknowledged,
208
+ leader_notified=leader_notified,
209
+ notification_message_id=notification.get("message_id"),
210
+ notification_status=notification.get("status"),
211
+ notification_channel=notification.get("channel"),
212
+ notification_event_id=notification.get("event_id"),
213
+ owner_team_id=owner_team_id,
214
+ )
215
+ _orchestrator_advance_on_report_result(workspace, envelope, event_log)
216
+ return {
217
+ "ok": True,
218
+ "result_id": result_id,
219
+ "task_id": envelope["task_id"],
220
+ "agent_id": envelope["agent_id"],
221
+ "acknowledged_messages": acknowledged,
222
+ "leader_notified": leader_notified,
223
+ "notification_message_id": notification.get("message_id"),
224
+ "notification_status": notification.get("status"),
225
+ "notification_channel": notification.get("channel"),
226
+ "notification_event_id": notification.get("event_id"),
227
+ }
228
+
229
+
230
+ def _notify_leader_of_report_result(
231
+ workspace: Path,
232
+ envelope: dict[str, Any],
233
+ result_id: str,
234
+ event_log: EventLog,
235
+ owner_team_id: str | None = None,
236
+ ) -> dict[str, Any]:
237
+ state = load_runtime_state(workspace)
238
+ if owner_team_id:
239
+ state = _team_state_by_owner_id(state, owner_team_id) or state
240
+ spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
241
+ spec = load_spec(spec_path) if spec_path.exists() else {}
242
+ leader_id = _leader_id(state, spec)
243
+ state = _refresh_leader_receiver_or_flag_rebind(workspace, state, event_log, persist=owner_team_id is None)
244
+ content = _format_report_result_notification(envelope, result_id)
245
+ store = MessageStore(workspace)
246
+ event_owner_team_id = owner_team_id or _state_owner_team_id(state)
247
+ event_id = store.add_scheduled_event(
248
+ datetime.now(timezone.utc).isoformat(),
249
+ leader_id,
250
+ "send",
251
+ {
252
+ "content": content,
253
+ "task_id": envelope["task_id"],
254
+ "sender": envelope["agent_id"],
255
+ "requires_ack": False,
256
+ "wait_visible": True,
257
+ "timeout": 30.0,
258
+ "max_attempts": 3,
259
+ },
260
+ owner_team_id=event_owner_team_id,
261
+ )
262
+ coordinator = {"ok": False, "status": "not_started"}
263
+ if state.get("session_name") or _leader_receiver_is_direct(state.get("leader_receiver", {})):
264
+ try:
265
+ coordinator = start_coordinator(workspace)
266
+ except Exception as exc:
267
+ coordinator = {"ok": False, "status": "start_failed", "error": str(exc)}
268
+ notification = {
269
+ "ok": True,
270
+ "status": "queued",
271
+ "channel": "coordinator",
272
+ "event_id": event_id,
273
+ "coordinator": coordinator,
274
+ }
275
+ event_log.write(
276
+ "mcp.report_result_notify_queued",
277
+ result_id=result_id,
278
+ task_id=envelope["task_id"],
279
+ agent_id=envelope["agent_id"],
280
+ event_id=event_id,
281
+ target=leader_id,
282
+ coordinator=coordinator,
283
+ owner_team_id=event_owner_team_id,
284
+ )
285
+ return notification
286
+
287
+
288
+ def _orchestrator_advance_on_report_result(
289
+ workspace: Path,
290
+ envelope: dict[str, Any],
291
+ event_log: EventLog,
292
+ ) -> None:
293
+ try:
294
+ from team_agent import orchestrator
295
+ except Exception as exc:
296
+ event_log.write(
297
+ "orchestrator.advance_skipped",
298
+ reason="import_failed",
299
+ error=str(exc),
300
+ task_id=envelope.get("task_id"),
301
+ agent_id=envelope.get("agent_id"),
302
+ )
303
+ return
304
+ try:
305
+ outcome = orchestrator.handle_report_result(workspace, envelope)
306
+ except Exception as exc:
307
+ event_log.write(
308
+ "orchestrator.advance_failed",
309
+ error=str(exc),
310
+ task_id=envelope.get("task_id"),
311
+ agent_id=envelope.get("agent_id"),
312
+ )
313
+ return
314
+ event_log.write(
315
+ "orchestrator.advance_processed",
316
+ outcome_status=outcome.get("status"),
317
+ plan_id=outcome.get("plan_id"),
318
+ current_stage=outcome.get("current_stage"),
319
+ halt_reason=outcome.get("halt_reason"),
320
+ halt_artifact=outcome.get("halt_artifact"),
321
+ task_id=envelope.get("task_id"),
322
+ agent_id=envelope.get("agent_id"),
323
+ )
324
+
325
+
326
+ def _owner_team_id_for_report(store: MessageStore, envelope: dict[str, Any]) -> str | None:
327
+ for row in reversed(store.messages()):
328
+ if row.get("recipient") != envelope["agent_id"]:
329
+ continue
330
+ if row.get("task_id") not in {envelope["task_id"], None} and row.get("message_id") != envelope["task_id"]:
331
+ continue
332
+ if row.get("owner_team_id"):
333
+ return str(row["owner_team_id"])
334
+ return None
335
+
336
+
337
+ def _team_state_by_owner_id(workspace_state: dict[str, Any], owner_team_id: str) -> dict[str, Any] | None:
338
+ if team_state_key(workspace_state) == owner_team_id:
339
+ return workspace_state
340
+ teams = workspace_state.get("teams")
341
+ if not isinstance(teams, dict):
342
+ return None
343
+ state = teams.get(owner_team_id)
344
+ return state if isinstance(state, dict) else None
345
+
346
+
347
+ def _state_owner_team_id(state: dict[str, Any]) -> str | None:
348
+ if state.get("session_name"):
349
+ return team_state_key(state)
350
+ return None
351
+
352
+
353
+ def _refresh_leader_receiver_or_flag_rebind(
354
+ workspace: Path,
355
+ state: dict[str, Any],
356
+ event_log: EventLog,
357
+ persist: bool = True,
358
+ ) -> dict[str, Any]:
359
+ receiver = state.get("leader_receiver") or {}
360
+ if receiver.get("mode") != "direct_tmux":
361
+ return state
362
+ validation = _validate_leader_receiver(receiver)
363
+ if validation.get("ok"):
364
+ return state
365
+ owner_identity = state.get("team_owner") or None
366
+ rediscovered = _rediscover_leader_receiver(receiver, event_log, owner_identity)
367
+ if rediscovered.get("status") == "updated":
368
+ state["leader_receiver"] = rediscovered["receiver"]
369
+ if persist:
370
+ save_runtime_state(workspace, state)
371
+ else:
372
+ save_team_scoped_state(workspace, state)
373
+ event_log.write(
374
+ "leader_receiver.rebind_applied",
375
+ old_pane_id=receiver.get("pane_id"),
376
+ new_pane_id=rediscovered["receiver"].get("pane_id"),
377
+ reason=validation.get("reason"),
378
+ source="report_result_notify",
379
+ owner_identity=owner_identity,
380
+ )
381
+ return state
382
+ event_log.write(
383
+ "leader_receiver.rebind_required",
384
+ old_pane_id=receiver.get("pane_id"),
385
+ reason=validation.get("reason"),
386
+ validation_error=validation.get("error"),
387
+ rediscovery_status=rediscovered.get("status"),
388
+ provider=receiver.get("provider"),
389
+ source="report_result_notify",
390
+ owner_identity=owner_identity,
391
+ )
392
+ return state
393
+
394
+
395
+ def _format_report_result_notification(envelope: dict[str, Any], result_id: str) -> str:
396
+ lines = [
397
+ f"Task {envelope['task_id']} reported {envelope['status']} from {envelope['agent_id']}: {envelope.get('summary') or 'completed'}",
398
+ f"Result id: {result_id}",
399
+ "Team Agent stored this result. The coordinator/collect path will update team_state.md; no manual polling loop is needed.",
400
+ ]
401
+ tests = envelope.get("tests") or []
402
+ rendered_tests: list[str] = []
403
+ for test in tests[:3]:
404
+ if isinstance(test, dict):
405
+ command = test.get("command") or "test"
406
+ status = test.get("status") or "unknown"
407
+ rendered_tests.append(f"{command}={status}")
408
+ if rendered_tests:
409
+ lines.insert(1, "Tests: " + "; ".join(rendered_tests))
410
+ return "\n".join(lines)
411
+
412
+
413
+ def _record_invalid_result(
414
+ event_log: EventLog,
415
+ error: str,
416
+ result_file: Path | None = None,
417
+ result_id: str | None = None,
418
+ envelope: Any = None,
419
+ ) -> dict[str, Any]:
420
+ task_id = envelope.get("task_id") if isinstance(envelope, dict) else None
421
+ agent_id = envelope.get("agent_id") if isinstance(envelope, dict) else None
422
+ event_log.write(
423
+ "collect.invalid_result",
424
+ result_id=result_id,
425
+ result_file=str(result_file) if result_file else None,
426
+ task_id=task_id,
427
+ agent_id=agent_id,
428
+ error=error,
429
+ )
430
+ return {
431
+ "result_id": result_id,
432
+ "path": str(result_file) if result_file else None,
433
+ "task_id": task_id,
434
+ "agent_id": agent_id,
435
+ "error": error,
436
+ }
437
+
438
+
439
+ def _collect_results_and_notify_watchers(workspace: Path, event_log: EventLog) -> dict[str, Any]:
440
+ store = MessageStore(workspace)
441
+ result: dict[str, Any] = {"ok": True, "collected_results": []}
442
+ if store.results(uncollected_only=True):
443
+ result = collect(workspace)
444
+ if not result.get("ok"):
445
+ event_log.write("coordinator.result_collect_failed", invalid_results=result.get("invalid_results", []))
446
+ return {"ok": False, "collected": 0, "notified": [], "error": "collect_failed"}
447
+ notified: list[dict[str, Any]] = []
448
+ for item in result.get("collected_results", []):
449
+ notified.extend(_notify_result_watchers(workspace, item, event_log))
450
+ notified.extend(_retry_result_deliveries(workspace, event_log))
451
+ event_log.write(
452
+ "coordinator.result_collect",
453
+ collected=len(result.get("collected_results", [])),
454
+ notified=len(notified),
455
+ )
456
+ return {"ok": True, "collected": len(result.get("collected_results", [])), "notified": notified}