multi-agent-platform 0.1.0__py3-none-any.whl

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 (144) hide show
  1. cli/__init__.py +0 -0
  2. cli/action_item_escalation.py +177 -0
  3. cli/agent_client.py +554 -0
  4. cli/bridge_state.py +43 -0
  5. cli/commands/__init__.py +13 -0
  6. cli/commands/action.py +142 -0
  7. cli/commands/agent.py +117 -0
  8. cli/commands/audit.py +68 -0
  9. cli/commands/docs.py +179 -0
  10. cli/commands/experiment.py +755 -0
  11. cli/commands/feedback.py +106 -0
  12. cli/commands/notification.py +213 -0
  13. cli/commands/persona.py +63 -0
  14. cli/commands/project.py +87 -0
  15. cli/commands/runtime.py +105 -0
  16. cli/commands/topic.py +361 -0
  17. cli/e2e_collab.py +602 -0
  18. cli/git_checkpoint.py +68 -0
  19. cli/host_worker_types.py +151 -0
  20. cli/main.py +1553 -0
  21. cli/map_command_client.py +497 -0
  22. cli/participant_worker.py +255 -0
  23. cli/reviewer_worker.py +263 -0
  24. cli/runtime/__init__.py +5 -0
  25. cli/runtime/run_lock.py +497 -0
  26. cli/runtime_chat.py +317 -0
  27. cli/session_wake_log.py +235 -0
  28. cli/simple_waker.py +950 -0
  29. cli/table_render.py +113 -0
  30. cli/wake_backend.py +236 -0
  31. cli/worker_cycle_log.py +36 -0
  32. map_client/__init__.py +37 -0
  33. map_client/bootstrap.py +193 -0
  34. map_client/client.py +1045 -0
  35. map_client/config.py +21 -0
  36. map_client/errors.py +283 -0
  37. map_client/exceptions.py +130 -0
  38. map_client/plan_evidence.py +159 -0
  39. map_client/project_config.py +153 -0
  40. map_client/result_template.py +167 -0
  41. map_client/testing.py +27 -0
  42. map_mcp/__init__.py +4 -0
  43. map_mcp/_utils.py +28 -0
  44. map_mcp/auth.py +34 -0
  45. map_mcp/config.py +50 -0
  46. map_mcp/context.py +39 -0
  47. map_mcp/main.py +75 -0
  48. map_mcp/server.py +573 -0
  49. map_mcp/session.py +79 -0
  50. map_sdk/__init__.py +29 -0
  51. map_sdk/evidence.py +68 -0
  52. map_types/__init__.py +203 -0
  53. map_types/enums.py +199 -0
  54. map_types/schemas.py +1351 -0
  55. multi_agent_platform-0.1.0.dist-info/METADATA +298 -0
  56. multi_agent_platform-0.1.0.dist-info/RECORD +144 -0
  57. multi_agent_platform-0.1.0.dist-info/WHEEL +5 -0
  58. multi_agent_platform-0.1.0.dist-info/entry_points.txt +6 -0
  59. multi_agent_platform-0.1.0.dist-info/licenses/LICENSE +21 -0
  60. multi_agent_platform-0.1.0.dist-info/top_level.txt +6 -0
  61. server/__init__.py +0 -0
  62. server/__version__.py +14 -0
  63. server/api/__init__.py +0 -0
  64. server/api/action_items.py +138 -0
  65. server/api/agents.py +412 -0
  66. server/api/audit.py +54 -0
  67. server/api/background_tasks.py +18 -0
  68. server/api/common.py +117 -0
  69. server/api/deps.py +30 -0
  70. server/api/experiments.py +858 -0
  71. server/api/feedback.py +75 -0
  72. server/api/notifications.py +22 -0
  73. server/api/projects.py +209 -0
  74. server/api/router.py +25 -0
  75. server/api/status.py +33 -0
  76. server/api/topics.py +302 -0
  77. server/api/webhooks.py +74 -0
  78. server/auth/__init__.py +8 -0
  79. server/auth/experiment_access.py +66 -0
  80. server/config.py +38 -0
  81. server/db/__init__.py +3 -0
  82. server/db/base.py +5 -0
  83. server/db/deadlock_retry.py +146 -0
  84. server/db/session.py +41 -0
  85. server/domain/__init__.py +3 -0
  86. server/domain/encrypted_types.py +63 -0
  87. server/domain/models.py +713 -0
  88. server/domain/schemas.py +3 -0
  89. server/domain/state_machine.py +79 -0
  90. server/domain/topic_ack_constants.py +9 -0
  91. server/main.py +148 -0
  92. server/scripts/__init__.py +0 -0
  93. server/scripts/migrate_notification_unique.py +231 -0
  94. server/scripts/purge_audit_pollution.py +116 -0
  95. server/services/__init__.py +0 -0
  96. server/services/_lookups.py +26 -0
  97. server/services/acceptance_service.py +90 -0
  98. server/services/action_item_migration_service.py +190 -0
  99. server/services/action_item_service.py +200 -0
  100. server/services/agent_work_service.py +405 -0
  101. server/services/archive_lint_service.py +156 -0
  102. server/services/audit_service.py +457 -0
  103. server/services/auth.py +66 -0
  104. server/services/comment_service.py +173 -0
  105. server/services/errors.py +65 -0
  106. server/services/escalation_resolver.py +248 -0
  107. server/services/evidence_service.py +88 -0
  108. server/services/experiment_capabilities_service.py +277 -0
  109. server/services/inbound_event_service.py +111 -0
  110. server/services/lock_service.py +273 -0
  111. server/services/log_service.py +202 -0
  112. server/services/mention_service.py +730 -0
  113. server/services/notification_service.py +939 -0
  114. server/services/notification_stream.py +138 -0
  115. server/services/permissions.py +147 -0
  116. server/services/persona_activity_service.py +108 -0
  117. server/services/phase_owner_resolver.py +95 -0
  118. server/services/phase_service.py +381 -0
  119. server/services/plan_marker_service.py +235 -0
  120. server/services/plan_service.py +186 -0
  121. server/services/platform_feedback_service.py +114 -0
  122. server/services/project_service.py +534 -0
  123. server/services/project_status_service.py +132 -0
  124. server/services/review_service.py +707 -0
  125. server/services/secret_encryption.py +97 -0
  126. server/services/similarity_service.py +119 -0
  127. server/services/sse_event_schemas.py +17 -0
  128. server/services/status_service.py +68 -0
  129. server/services/template_service.py +134 -0
  130. server/services/text_utils.py +19 -0
  131. server/services/thread_activity.py +180 -0
  132. server/services/todo_persona_filter.py +73 -0
  133. server/services/todo_service.py +604 -0
  134. server/services/topic_ack_service.py +312 -0
  135. server/services/topic_action_item_ops.py +538 -0
  136. server/services/topic_comment_kind.py +14 -0
  137. server/services/topic_comment_service.py +237 -0
  138. server/services/topic_helpers.py +32 -0
  139. server/services/topic_lifecycle_service.py +478 -0
  140. server/services/topic_progress_service.py +40 -0
  141. server/services/topic_resolve_service.py +234 -0
  142. server/services/topic_service.py +102 -0
  143. server/services/topic_work_item_service.py +570 -0
  144. server/services/webhook_service.py +273 -0
@@ -0,0 +1,312 @@
1
+ """Advance-round participant acknowledgement helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import uuid
7
+ from datetime import UTC, datetime
8
+
9
+ from sqlalchemy import select
10
+ from sqlalchemy.orm import Session
11
+
12
+ from server.domain.models import Topic, TopicComment
13
+ from server.domain.topic_ack_constants import (
14
+ ACK_ACCEPT_MARKER,
15
+ ACK_DISMISS_MARKER,
16
+ ACK_REJECT_MARKER,
17
+ ADVANCE_ROUND_ACK_TIMEOUT,
18
+ )
19
+ from server.services.errors import ConflictError
20
+ from server.services.thread_activity import topic_comment_order_clauses, topic_comment_sort_key
21
+
22
+ ROUND_SUMMARY_RE = re.compile(r"^##\s*Round\s+\d+\s+Summary\b", re.MULTILINE | re.IGNORECASE)
23
+
24
+
25
+ class AdvanceRoundError(ConflictError):
26
+ def __init__(self, message: str, *, reason: str) -> None:
27
+ super().__init__(message)
28
+ self.reason = reason
29
+
30
+
31
+ def _ack_kind(body: str) -> str | None:
32
+ text = body.strip()
33
+ if ACK_REJECT_MARKER in text:
34
+ return "reject"
35
+ if ACK_DISMISS_MARKER in text:
36
+ return "dismiss"
37
+ if ACK_ACCEPT_MARKER in text:
38
+ return "accept"
39
+ return None
40
+
41
+
42
+ def _topic_comments(db: Session, topic_id: uuid.UUID) -> list[TopicComment]:
43
+ return list(
44
+ db.scalars(
45
+ select(TopicComment)
46
+ .where(TopicComment.topic_id == topic_id)
47
+ .order_by(*topic_comment_order_clauses())
48
+ )
49
+ )
50
+
51
+
52
+ def _comments_for_topic(
53
+ db: Session,
54
+ topic_id: uuid.UUID,
55
+ comments: list[TopicComment] | None,
56
+ ) -> list[TopicComment]:
57
+ """Return preloaded comments when provided; otherwise load from DB."""
58
+ if comments is not None:
59
+ return comments
60
+ return _topic_comments(db, topic_id)
61
+
62
+
63
+ def required_ack_agent_ids(
64
+ db: Session,
65
+ topic: Topic,
66
+ *,
67
+ comments: list[TopicComment] | None = None,
68
+ ) -> set[uuid.UUID]:
69
+ """Participants who must ack before advance (dynamic set)."""
70
+ required: set[uuid.UUID] = set()
71
+ dismissed: set[uuid.UUID] = set()
72
+ for comment in _comments_for_topic(db, topic.id, comments):
73
+ if comment.author_agent_id == topic.creator_agent_id:
74
+ continue
75
+ kind = _ack_kind(comment.body)
76
+ if kind == "dismiss":
77
+ dismissed.add(comment.author_agent_id)
78
+ continue
79
+ required.add(comment.author_agent_id)
80
+ return required - dismissed
81
+
82
+
83
+ def latest_ack_kind_since(
84
+ comments: list[TopicComment],
85
+ agent_id: uuid.UUID,
86
+ *,
87
+ since: datetime,
88
+ ) -> str | None:
89
+ """Return the agent's most recent ack kind on or after ``since``."""
90
+ cutoff = _as_utc(since)
91
+ latest_kind: str | None = None
92
+ latest_at: datetime | None = None
93
+ for comment in comments:
94
+ if comment.author_agent_id != agent_id:
95
+ continue
96
+ created_at = _as_utc(comment.created_at)
97
+ if created_at < cutoff:
98
+ continue
99
+ kind = _ack_kind(comment.body)
100
+ if kind is None:
101
+ continue
102
+ if latest_at is None or created_at >= latest_at:
103
+ latest_at = created_at
104
+ latest_kind = kind
105
+ return latest_kind
106
+
107
+
108
+ def acknowledged_agent_ids(
109
+ db: Session,
110
+ topic: Topic,
111
+ *,
112
+ host_ack_ids: list[uuid.UUID],
113
+ comments: list[TopicComment] | None = None,
114
+ ) -> set[uuid.UUID]:
115
+ acked = set(host_ack_ids)
116
+ loaded = _comments_for_topic(db, topic.id, comments)
117
+ cutoff = _ack_cutoff_for_topic(db, topic, comments=loaded)
118
+ if cutoff is None:
119
+ return acked
120
+ for agent_id in required_ack_agent_ids(db, topic, comments=loaded):
121
+ if latest_ack_kind_since(loaded, agent_id, since=cutoff) == "accept":
122
+ acked.add(agent_id)
123
+ return acked
124
+
125
+
126
+ def reject_agent_ids(
127
+ db: Session,
128
+ topic: Topic,
129
+ *,
130
+ comments: list[TopicComment] | None = None,
131
+ ) -> set[uuid.UUID]:
132
+ loaded = _comments_for_topic(db, topic.id, comments)
133
+ cutoff = _ack_cutoff_for_topic(db, topic, comments=loaded)
134
+ if cutoff is None:
135
+ return set()
136
+ rejected: set[uuid.UUID] = set()
137
+ for agent_id in required_ack_agent_ids(db, topic, comments=loaded):
138
+ if latest_ack_kind_since(loaded, agent_id, since=cutoff) == "reject":
139
+ rejected.add(agent_id)
140
+ return rejected
141
+
142
+
143
+ def advance_round_ack_state(
144
+ db: Session,
145
+ topic: Topic,
146
+ *,
147
+ now: datetime | None = None,
148
+ comments: list[TopicComment] | None = None,
149
+ ) -> str:
150
+ """Return ack gate state for the current Round Summary: none|pending|rejected|ready."""
151
+ if topic.archived_at is not None:
152
+ return "none"
153
+ if topic.advance_round_pending_since is None:
154
+ return "none"
155
+ loaded = _comments_for_topic(db, topic.id, comments)
156
+ if _ack_cutoff_for_topic(db, topic, comments=loaded) is None:
157
+ return "none"
158
+ if reject_agent_ids(db, topic, comments=loaded):
159
+ return "rejected"
160
+ required = required_ack_agent_ids(db, topic, comments=loaded)
161
+ if not required:
162
+ return "ready"
163
+ acked = acknowledged_agent_ids(db, topic, host_ack_ids=[], comments=loaded)
164
+ if not (required - acked):
165
+ return "ready"
166
+ if ack_timeout_elapsed(topic, now=now):
167
+ return "ready"
168
+ return "pending"
169
+
170
+
171
+ def _as_utc(value: datetime) -> datetime:
172
+ if value.tzinfo is None:
173
+ return value.replace(tzinfo=UTC)
174
+ return value.astimezone(UTC)
175
+
176
+
177
+ def ack_timeout_elapsed(topic: Topic, *, now: datetime | None = None) -> bool:
178
+ if topic.advance_round_pending_since is None:
179
+ return False
180
+ current = _as_utc(now or datetime.now(UTC))
181
+ pending = _as_utc(topic.advance_round_pending_since)
182
+ return current - pending >= ADVANCE_ROUND_ACK_TIMEOUT
183
+
184
+
185
+ def validate_advance_ack(
186
+ db: Session,
187
+ topic: Topic,
188
+ *,
189
+ acknowledged_by: list[uuid.UUID],
190
+ now: datetime | None = None,
191
+ ) -> None:
192
+ if topic.archived_at is not None:
193
+ raise AdvanceRoundError("Cannot advance an archived topic", reason="archived_topic")
194
+
195
+ rejected = reject_agent_ids(db, topic)
196
+ if rejected:
197
+ raise AdvanceRoundError(
198
+ "Participant rejected advancing the discussion round",
199
+ reason="ack_rejected",
200
+ )
201
+
202
+ required = required_ack_agent_ids(db, topic)
203
+ if not required:
204
+ topic.advance_round_pending_since = None
205
+ return
206
+
207
+ acked = acknowledged_agent_ids(db, topic, host_ack_ids=acknowledged_by)
208
+ missing = required - acked
209
+ if not missing:
210
+ topic.advance_round_pending_since = None
211
+ return
212
+
213
+ if ack_timeout_elapsed(topic, now=now):
214
+ topic.advance_round_pending_since = None
215
+ return
216
+
217
+ if topic.advance_round_pending_since is None:
218
+ topic.advance_round_pending_since = now or datetime.now(UTC)
219
+ db.commit()
220
+ db.refresh(topic)
221
+
222
+ raise AdvanceRoundError(
223
+ "Waiting for participant acknowledgement before advancing round",
224
+ reason="ack_pending",
225
+ )
226
+
227
+
228
+ def participant_ack_body(kind: str) -> str:
229
+ marker = {
230
+ "accept": ACK_ACCEPT_MARKER,
231
+ "reject": ACK_REJECT_MARKER,
232
+ "dismiss": ACK_DISMISS_MARKER,
233
+ }[kind]
234
+ return f"Participant round acknowledgement ({marker})."
235
+
236
+
237
+ def is_round_summary_comment(body: str) -> bool:
238
+ """Top-level Round N Summary posts use this heading (see topic-host Skill)."""
239
+ return bool(ROUND_SUMMARY_RE.search(body.strip()))
240
+
241
+
242
+ def latest_host_round_summary_comment(
243
+ comments: list[TopicComment],
244
+ *,
245
+ host_agent_id: uuid.UUID,
246
+ ) -> TopicComment | None:
247
+ candidates = [
248
+ comment
249
+ for comment in comments
250
+ if comment.author_agent_id == host_agent_id
251
+ and comment.parent_comment_id is None
252
+ and is_round_summary_comment(comment.body)
253
+ ]
254
+ if not candidates:
255
+ return None
256
+ return max(candidates, key=topic_comment_sort_key)
257
+
258
+
259
+ def _ack_cutoff_for_topic(
260
+ db: Session,
261
+ topic: Topic,
262
+ *,
263
+ comments: list[TopicComment] | None = None,
264
+ ) -> datetime | None:
265
+ loaded = _comments_for_topic(db, topic.id, comments)
266
+ summary = latest_host_round_summary_comment(loaded, host_agent_id=topic.creator_agent_id)
267
+ if summary is not None:
268
+ return _as_utc(summary.created_at)
269
+ if topic.advance_round_pending_since is not None:
270
+ return _as_utc(topic.advance_round_pending_since)
271
+ return None
272
+
273
+
274
+ def agent_has_round_ack_since(
275
+ comments: list[TopicComment],
276
+ agent_id: uuid.UUID,
277
+ *,
278
+ since: datetime,
279
+ ) -> bool:
280
+ cutoff = _as_utc(since)
281
+ for comment in comments:
282
+ if comment.author_agent_id != agent_id:
283
+ continue
284
+ if _as_utc(comment.created_at) < cutoff:
285
+ continue
286
+ if _ack_kind(comment.body) in {"accept", "reject", "dismiss"}:
287
+ return True
288
+ return False
289
+
290
+
291
+ def agent_needs_round_ack(
292
+ db: Session,
293
+ topic: Topic,
294
+ agent_id: uuid.UUID,
295
+ *,
296
+ comments: list[TopicComment] | None = None,
297
+ ) -> bool:
298
+ """True when agent must post --ack accept/reject/dismiss for the current Summary."""
299
+ if agent_id == topic.creator_agent_id:
300
+ return False
301
+ loaded = _comments_for_topic(db, topic.id, comments)
302
+ required = required_ack_agent_ids(db, topic, comments=loaded)
303
+ if agent_id not in required:
304
+ return False
305
+ cutoff = _ack_cutoff_for_topic(db, topic, comments=loaded)
306
+ if cutoff is None:
307
+ return False
308
+ return not agent_has_round_ack_since(loaded, agent_id, since=cutoff)
309
+
310
+
311
+ def mark_round_ack_pending(topic: Topic, *, now: datetime | None = None) -> None:
312
+ topic.advance_round_pending_since = now or datetime.now(UTC)