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.
- cli/__init__.py +0 -0
- cli/action_item_escalation.py +177 -0
- cli/agent_client.py +554 -0
- cli/bridge_state.py +43 -0
- cli/commands/__init__.py +13 -0
- cli/commands/action.py +142 -0
- cli/commands/agent.py +117 -0
- cli/commands/audit.py +68 -0
- cli/commands/docs.py +179 -0
- cli/commands/experiment.py +755 -0
- cli/commands/feedback.py +106 -0
- cli/commands/notification.py +213 -0
- cli/commands/persona.py +63 -0
- cli/commands/project.py +87 -0
- cli/commands/runtime.py +105 -0
- cli/commands/topic.py +361 -0
- cli/e2e_collab.py +602 -0
- cli/git_checkpoint.py +68 -0
- cli/host_worker_types.py +151 -0
- cli/main.py +1553 -0
- cli/map_command_client.py +497 -0
- cli/participant_worker.py +255 -0
- cli/reviewer_worker.py +263 -0
- cli/runtime/__init__.py +5 -0
- cli/runtime/run_lock.py +497 -0
- cli/runtime_chat.py +317 -0
- cli/session_wake_log.py +235 -0
- cli/simple_waker.py +950 -0
- cli/table_render.py +113 -0
- cli/wake_backend.py +236 -0
- cli/worker_cycle_log.py +36 -0
- map_client/__init__.py +37 -0
- map_client/bootstrap.py +193 -0
- map_client/client.py +1045 -0
- map_client/config.py +21 -0
- map_client/errors.py +283 -0
- map_client/exceptions.py +130 -0
- map_client/plan_evidence.py +159 -0
- map_client/project_config.py +153 -0
- map_client/result_template.py +167 -0
- map_client/testing.py +27 -0
- map_mcp/__init__.py +4 -0
- map_mcp/_utils.py +28 -0
- map_mcp/auth.py +34 -0
- map_mcp/config.py +50 -0
- map_mcp/context.py +39 -0
- map_mcp/main.py +75 -0
- map_mcp/server.py +573 -0
- map_mcp/session.py +79 -0
- map_sdk/__init__.py +29 -0
- map_sdk/evidence.py +68 -0
- map_types/__init__.py +203 -0
- map_types/enums.py +199 -0
- map_types/schemas.py +1351 -0
- multi_agent_platform-0.1.0.dist-info/METADATA +298 -0
- multi_agent_platform-0.1.0.dist-info/RECORD +144 -0
- multi_agent_platform-0.1.0.dist-info/WHEEL +5 -0
- multi_agent_platform-0.1.0.dist-info/entry_points.txt +6 -0
- multi_agent_platform-0.1.0.dist-info/licenses/LICENSE +21 -0
- multi_agent_platform-0.1.0.dist-info/top_level.txt +6 -0
- server/__init__.py +0 -0
- server/__version__.py +14 -0
- server/api/__init__.py +0 -0
- server/api/action_items.py +138 -0
- server/api/agents.py +412 -0
- server/api/audit.py +54 -0
- server/api/background_tasks.py +18 -0
- server/api/common.py +117 -0
- server/api/deps.py +30 -0
- server/api/experiments.py +858 -0
- server/api/feedback.py +75 -0
- server/api/notifications.py +22 -0
- server/api/projects.py +209 -0
- server/api/router.py +25 -0
- server/api/status.py +33 -0
- server/api/topics.py +302 -0
- server/api/webhooks.py +74 -0
- server/auth/__init__.py +8 -0
- server/auth/experiment_access.py +66 -0
- server/config.py +38 -0
- server/db/__init__.py +3 -0
- server/db/base.py +5 -0
- server/db/deadlock_retry.py +146 -0
- server/db/session.py +41 -0
- server/domain/__init__.py +3 -0
- server/domain/encrypted_types.py +63 -0
- server/domain/models.py +713 -0
- server/domain/schemas.py +3 -0
- server/domain/state_machine.py +79 -0
- server/domain/topic_ack_constants.py +9 -0
- server/main.py +148 -0
- server/scripts/__init__.py +0 -0
- server/scripts/migrate_notification_unique.py +231 -0
- server/scripts/purge_audit_pollution.py +116 -0
- server/services/__init__.py +0 -0
- server/services/_lookups.py +26 -0
- server/services/acceptance_service.py +90 -0
- server/services/action_item_migration_service.py +190 -0
- server/services/action_item_service.py +200 -0
- server/services/agent_work_service.py +405 -0
- server/services/archive_lint_service.py +156 -0
- server/services/audit_service.py +457 -0
- server/services/auth.py +66 -0
- server/services/comment_service.py +173 -0
- server/services/errors.py +65 -0
- server/services/escalation_resolver.py +248 -0
- server/services/evidence_service.py +88 -0
- server/services/experiment_capabilities_service.py +277 -0
- server/services/inbound_event_service.py +111 -0
- server/services/lock_service.py +273 -0
- server/services/log_service.py +202 -0
- server/services/mention_service.py +730 -0
- server/services/notification_service.py +939 -0
- server/services/notification_stream.py +138 -0
- server/services/permissions.py +147 -0
- server/services/persona_activity_service.py +108 -0
- server/services/phase_owner_resolver.py +95 -0
- server/services/phase_service.py +381 -0
- server/services/plan_marker_service.py +235 -0
- server/services/plan_service.py +186 -0
- server/services/platform_feedback_service.py +114 -0
- server/services/project_service.py +534 -0
- server/services/project_status_service.py +132 -0
- server/services/review_service.py +707 -0
- server/services/secret_encryption.py +97 -0
- server/services/similarity_service.py +119 -0
- server/services/sse_event_schemas.py +17 -0
- server/services/status_service.py +68 -0
- server/services/template_service.py +134 -0
- server/services/text_utils.py +19 -0
- server/services/thread_activity.py +180 -0
- server/services/todo_persona_filter.py +73 -0
- server/services/todo_service.py +604 -0
- server/services/topic_ack_service.py +312 -0
- server/services/topic_action_item_ops.py +538 -0
- server/services/topic_comment_kind.py +14 -0
- server/services/topic_comment_service.py +237 -0
- server/services/topic_helpers.py +32 -0
- server/services/topic_lifecycle_service.py +478 -0
- server/services/topic_progress_service.py +40 -0
- server/services/topic_resolve_service.py +234 -0
- server/services/topic_service.py +102 -0
- server/services/topic_work_item_service.py +570 -0
- 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)
|