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
cli/__init__.py ADDED
File without changes
@@ -0,0 +1,177 @@
1
+ """Action-item escalation timeline (experiment B, plan §3 / I4).
2
+
3
+ 纯决策模块:根据 action_item 的 ``first_open_at`` / ``last_woken_at`` /
4
+ ``wake_count`` / ``stale_at`` 字段判断当前时刻应该 WAKE / STALE / SKIP。
5
+ 无 I/O,无时钟,``now`` 由调用方注入以便单元测试钉住时间。
6
+
7
+ 阈值常量镜像 ``server.services.action_item_service``(
8
+ ``WAKE_STAGE_THRESHOLDS`` / ``WAKE_REPEAT_INTERVAL_DAYS`` /
9
+ ``WAKE_MAX_COUNT_BEFORE_STALE``)。两侧需保持一致——
10
+ ``test_threshold_constants_match_plan_section_three`` 是这条不变式的 lint。
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+ from datetime import UTC, datetime, timedelta
17
+ from enum import Enum
18
+ from typing import Any
19
+
20
+
21
+ class ActionItemWakeDecision(str, Enum):
22
+ """Outcome of ``should_wake_action_item`` for one action_item at ``now``."""
23
+
24
+ WAKE = "wake"
25
+ STALE = "stale"
26
+ SKIP = "skip"
27
+
28
+
29
+ # Mirrored from server.services.action_item_service. Importing would force
30
+ # the waker CLI to depend on the server-side stack (SQLAlchemy models +
31
+ # FastAPI deps), which violates the waker's no-FastAPI invariant. Kept
32
+ # short and obvious so a reviewer can spot drift.
33
+ _WAKE_STAGE_HOURS: tuple[tuple[int, int], ...] = (
34
+ (1, 24), # wake_count_after_increment=1 → first wake at T+24h
35
+ (2, 72), # wake_count_after_increment=2 → second wake at T+72h
36
+ )
37
+ _WAKE_REPEAT_DAYS = 7 # after the 72h wake, fire every 7d
38
+ _WAKE_MAX_BEFORE_STALE = 4 # 4th unanswered wake → stale
39
+
40
+
41
+ def _parse_iso_datetime(value: Any) -> datetime | None:
42
+ """Parse an ISO-8601 string into an aware UTC datetime.
43
+
44
+ Defensive about the variety of shapes the SDK / API may emit
45
+ (``...Z`` vs ``...+00:00`` vs naive ISO). Returns ``None`` if the value
46
+ is missing or unparseable — callers treat ``None`` as "skip".
47
+ """
48
+ if value is None or value == "":
49
+ return None
50
+ if isinstance(value, datetime):
51
+ return value if value.tzinfo else value.replace(tzinfo=UTC)
52
+ try:
53
+ parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
54
+ except ValueError:
55
+ return None
56
+ return parsed if parsed.tzinfo else parsed.replace(tzinfo=UTC)
57
+
58
+
59
+ def should_wake_action_item(
60
+ item: dict[str, Any],
61
+ *,
62
+ now: datetime | None = None,
63
+ ) -> ActionItemWakeDecision:
64
+ """Decide the escalation action for one open action_item at ``now``.
65
+
66
+ Pure function (no I/O, no clock). Returns ``WAKE`` when the waker
67
+ should bump ``wake_count`` and emit a wake event, ``STALE`` when the
68
+ 4th wake has already fired and the assignee still hasn't responded
69
+ (we mark stale + stop waking), or ``SKIP`` when the escalation
70
+ timeline does not yet call for an action.
71
+
72
+ Rules (plan §3, mirrored from ``server.services.action_item_service``):
73
+
74
+ - Only open items are eligible; closed items (done / cancelled) and
75
+ unassigned items (no ``owner_agent_id``) are skipped.
76
+ - Stale items (``stale_at`` set) are skipped forever — once we've
77
+ written the diagnostic audit row we stop bothering the assignee.
78
+ - First wake fires 24h after ``first_open_at``; second at 72h; further
79
+ wakes every 7d; the (N+1)th check after the 4th wake writes stale.
80
+ - ``now`` defaults to ``datetime.now(UTC)`` — callers that need a
81
+ deterministic clock (tests, dogfood) inject their own.
82
+ """
83
+ if not isinstance(item, dict):
84
+ return ActionItemWakeDecision.SKIP
85
+ if item.get("status") != "open":
86
+ return ActionItemWakeDecision.SKIP
87
+ if not item.get("owner_agent_id"):
88
+ # plan §3 「仅 assignee」 — unassigned items must not be woken by
89
+ # anyone, the waker has no addressee.
90
+ return ActionItemWakeDecision.SKIP
91
+ if _parse_iso_datetime(item.get("stale_at")) is not None:
92
+ return ActionItemWakeDecision.SKIP
93
+
94
+ current = now or datetime.now(UTC)
95
+ first_open_at = _parse_iso_datetime(item.get("first_open_at"))
96
+ if first_open_at is None:
97
+ # Pre-I1 backfill may have missed this row (closed before I1, etc).
98
+ # Conservatively skip — re-running the backfill is the remediation.
99
+ return ActionItemWakeDecision.SKIP
100
+ last_woken_at = _parse_iso_datetime(item.get("last_woken_at"))
101
+ wake_count = int(item.get("wake_count") or 0)
102
+
103
+ elapsed = current - first_open_at
104
+
105
+ # Stage 1 & 2: hard thresholds keyed to first_open_at.
106
+ for target_count, min_hours in _WAKE_STAGE_HOURS:
107
+ if wake_count + 1 == target_count and elapsed >= timedelta(hours=min_hours):
108
+ return ActionItemWakeDecision.WAKE
109
+
110
+ # Stages 3+: every 7d after the last wake.
111
+ if (
112
+ 2 <= wake_count < _WAKE_MAX_BEFORE_STALE
113
+ and last_woken_at is not None
114
+ and current - last_woken_at >= timedelta(days=_WAKE_REPEAT_DAYS)
115
+ ):
116
+ return ActionItemWakeDecision.WAKE
117
+
118
+ # Past the 4th unanswered wake: write stale.
119
+ if (
120
+ wake_count >= _WAKE_MAX_BEFORE_STALE
121
+ and last_woken_at is not None
122
+ and current - last_woken_at >= timedelta(days=_WAKE_REPEAT_DAYS)
123
+ ):
124
+ return ActionItemWakeDecision.STALE
125
+
126
+ return ActionItemWakeDecision.SKIP
127
+
128
+
129
+ def scan_pending_action_items(
130
+ action_items: list[dict[str, Any]],
131
+ *,
132
+ persona_agent_id: str | None,
133
+ now: datetime | None = None,
134
+ ) -> list[tuple[str, ActionItemWakeDecision]]:
135
+ """Filter the ``action_items`` todo payload through ``should_wake_action_item``.
136
+
137
+ Returns a list of ``(action_item_id, decision)`` pairs. Only items
138
+ owned by ``persona_agent_id`` are considered — the waker must never
139
+ wake another agent's action_item even if it shows up in the
140
+ cross-project listing. Items whose owner doesn't match are skipped
141
+ silently (not raised) — the per-persona todos feed already scopes
142
+ this but we re-check defensively against potential payload leakage.
143
+ """
144
+ decisions: list[tuple[str, ActionItemWakeDecision]] = []
145
+ for item in action_items or []:
146
+ if not isinstance(item, dict):
147
+ continue
148
+ item_id = str(item.get("id") or "")
149
+ if not item_id:
150
+ continue
151
+ if persona_agent_id and str(item.get("owner_agent_id") or "") != str(persona_agent_id):
152
+ continue
153
+ decisions.append((item_id, should_wake_action_item(item, now=now)))
154
+ return decisions
155
+
156
+
157
+ @dataclass(frozen=True)
158
+ class EscalationDecisionCounts:
159
+ """Aggregated counts of WAKE / STALE / SKIP for one scan."""
160
+
161
+ wake: int = 0
162
+ stale: int = 0
163
+ skip: int = 0
164
+
165
+ @classmethod
166
+ def from_decisions(
167
+ cls, decisions: list[tuple[str, ActionItemWakeDecision]]
168
+ ) -> EscalationDecisionCounts:
169
+ wake = stale = skip = 0
170
+ for _, decision in decisions:
171
+ if decision is ActionItemWakeDecision.WAKE:
172
+ wake += 1
173
+ elif decision is ActionItemWakeDecision.STALE:
174
+ stale += 1
175
+ else:
176
+ skip += 1
177
+ return cls(wake=wake, stale=stale, skip=skip)