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
map_types/schemas.py
ADDED
|
@@ -0,0 +1,1351 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import uuid
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Annotated, Any, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field, field_validator, model_validator
|
|
7
|
+
|
|
8
|
+
from map_types.enums import (
|
|
9
|
+
AcceptanceType,
|
|
10
|
+
ActionItemCategory,
|
|
11
|
+
AgentRole,
|
|
12
|
+
CommentAnchorType,
|
|
13
|
+
ExperimentPhase,
|
|
14
|
+
FeedbackCategory,
|
|
15
|
+
FeedbackStatus,
|
|
16
|
+
InboundEventSource,
|
|
17
|
+
NotificationCategory,
|
|
18
|
+
NotificationFingerprintVersion,
|
|
19
|
+
PhaseOwner,
|
|
20
|
+
ResolutionReason,
|
|
21
|
+
ReviewArchivedReason,
|
|
22
|
+
ReviewItemKind,
|
|
23
|
+
ReviewItemStatus,
|
|
24
|
+
ReviewSubstituteKind,
|
|
25
|
+
ReviewVerdict,
|
|
26
|
+
TopicActionItemStatus,
|
|
27
|
+
TopicCommentKind,
|
|
28
|
+
TopicDiscussionRound,
|
|
29
|
+
TopicStatus,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
PROJECT_KEY_PATTERN = re.compile(r"^[a-z0-9][a-z0-9-_]{0,62}$")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ORMModel(BaseModel):
|
|
36
|
+
model_config = ConfigDict(from_attributes=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# --- Project ---
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ProjectCreate(BaseModel):
|
|
43
|
+
project_key: str = Field(min_length=2, max_length=64)
|
|
44
|
+
name: str = Field(min_length=1, max_length=255)
|
|
45
|
+
workspace_path: str = Field(min_length=1, max_length=1024)
|
|
46
|
+
description: str | None = None
|
|
47
|
+
|
|
48
|
+
@field_validator("project_key")
|
|
49
|
+
@classmethod
|
|
50
|
+
def validate_project_key(cls, value: str) -> str:
|
|
51
|
+
key = value.strip().lower()
|
|
52
|
+
if not PROJECT_KEY_PATTERN.match(key):
|
|
53
|
+
raise ValueError("project_key must match [a-z0-9][a-z0-9-_]{0,62}")
|
|
54
|
+
return key
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ProjectUpdate(BaseModel):
|
|
58
|
+
name: str | None = Field(default=None, min_length=1, max_length=255)
|
|
59
|
+
workspace_path: str | None = Field(default=None, min_length=1, max_length=1024)
|
|
60
|
+
description: str | None = None
|
|
61
|
+
archived: bool | None = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ProjectRead(ORMModel):
|
|
65
|
+
id: uuid.UUID
|
|
66
|
+
project_key: str
|
|
67
|
+
name: str
|
|
68
|
+
workspace_path: str
|
|
69
|
+
description: str | None
|
|
70
|
+
current_status_version: int
|
|
71
|
+
created_at: datetime
|
|
72
|
+
archived_at: datetime | None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ProjectStatusRead(BaseModel):
|
|
76
|
+
project: ProjectRead
|
|
77
|
+
experiment_counts_by_phase: dict[str, int]
|
|
78
|
+
active_experiments: list["ExperimentSummaryRead"] = Field(default_factory=list)
|
|
79
|
+
recent_experiments: list["ExperimentSummaryRead"]
|
|
80
|
+
open_topics: list["TopicSummaryRead"] = Field(default_factory=list)
|
|
81
|
+
status_version: int = 0
|
|
82
|
+
status_md: str | None = None
|
|
83
|
+
status_updated_at: datetime | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ProjectStatusRevise(BaseModel):
|
|
87
|
+
content_md: str = Field(min_length=1)
|
|
88
|
+
change_note: str | None = Field(default=None, max_length=1024)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ProjectStatusVersionRead(ORMModel):
|
|
92
|
+
id: uuid.UUID
|
|
93
|
+
project_id: uuid.UUID
|
|
94
|
+
version: int
|
|
95
|
+
content_md: str
|
|
96
|
+
author_agent_id: uuid.UUID
|
|
97
|
+
change_note: str | None
|
|
98
|
+
created_at: datetime
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# --- Plan ---
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class PlanInput(BaseModel):
|
|
105
|
+
content_md: str = Field(min_length=1)
|
|
106
|
+
change_note: str | None = Field(default=None, max_length=1024)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class PlanRevise(BaseModel):
|
|
110
|
+
content_md: str = Field(min_length=1)
|
|
111
|
+
change_note: str | None = Field(default=None, max_length=1024)
|
|
112
|
+
addressed_item_ids: list[uuid.UUID] = Field(default_factory=list)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class PlanVersionRead(ORMModel):
|
|
116
|
+
id: uuid.UUID
|
|
117
|
+
experiment_id: uuid.UUID
|
|
118
|
+
version: int
|
|
119
|
+
content_md: str
|
|
120
|
+
author_agent_id: uuid.UUID
|
|
121
|
+
change_note: str | None
|
|
122
|
+
created_at: datetime
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# --- Review ---
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ReviewCreate(BaseModel):
|
|
129
|
+
reasonable_items: list[str] = Field(default_factory=list)
|
|
130
|
+
unreasonable_items: list[str] = Field(default_factory=list)
|
|
131
|
+
substitute_reason: str | None = Field(
|
|
132
|
+
default=None,
|
|
133
|
+
min_length=1,
|
|
134
|
+
description="Required when admin submits a substitute review (admin_for_others).",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ReviewItemRead(ORMModel):
|
|
139
|
+
id: uuid.UUID
|
|
140
|
+
review_id: uuid.UUID
|
|
141
|
+
kind: ReviewItemKind
|
|
142
|
+
content: str
|
|
143
|
+
status: ReviewItemStatus | None
|
|
144
|
+
last_resolution_reason: ResolutionReason | None = None
|
|
145
|
+
created_at: datetime
|
|
146
|
+
updated_at: datetime
|
|
147
|
+
waived_reason: str | None = Field(
|
|
148
|
+
default=None,
|
|
149
|
+
description=(
|
|
150
|
+
"Populated server-side from the latest accept-result verdict_file "
|
|
151
|
+
"where verdict='waived' for this item. Read-only convenience field "
|
|
152
|
+
"for participant-facing UIs; the source of truth is "
|
|
153
|
+
"experiment_logs.metadata_json.verdict_file."
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ReviewItemUpdate(BaseModel):
|
|
159
|
+
status: ReviewItemStatus
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ReviewRead(ORMModel):
|
|
163
|
+
id: uuid.UUID
|
|
164
|
+
experiment_id: uuid.UUID
|
|
165
|
+
reviewer_agent_id: uuid.UUID
|
|
166
|
+
plan_version: int
|
|
167
|
+
substitute_kind: ReviewSubstituteKind = ReviewSubstituteKind.none
|
|
168
|
+
created_at: datetime
|
|
169
|
+
archived_at: datetime | None = None
|
|
170
|
+
archived_reason: ReviewArchivedReason | None = None
|
|
171
|
+
items: list[ReviewItemRead] = Field(default_factory=list)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# --- Comment ---
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class CommentCreate(BaseModel):
|
|
178
|
+
anchor_type: CommentAnchorType
|
|
179
|
+
anchor_id: uuid.UUID
|
|
180
|
+
parent_id: uuid.UUID | None = None
|
|
181
|
+
body: str = Field(min_length=1)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class CommentRead(ORMModel):
|
|
185
|
+
id: uuid.UUID
|
|
186
|
+
experiment_id: uuid.UUID
|
|
187
|
+
anchor_type: CommentAnchorType
|
|
188
|
+
anchor_id: uuid.UUID
|
|
189
|
+
parent_comment_id: uuid.UUID | None
|
|
190
|
+
author_agent_id: uuid.UUID
|
|
191
|
+
author_name: str | None = None
|
|
192
|
+
body: str
|
|
193
|
+
created_at: datetime
|
|
194
|
+
unresolved_mentions: list[str] = Field(default_factory=list)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class CommentTreeNode(CommentRead):
|
|
198
|
+
children: list["CommentTreeNode"] = Field(default_factory=list)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# --- Experiment ---
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ExperimentCreate(BaseModel):
|
|
205
|
+
title: str = Field(min_length=1, max_length=512)
|
|
206
|
+
description: str | None = None
|
|
207
|
+
plan: PlanInput
|
|
208
|
+
submit_for_review: bool = False
|
|
209
|
+
topic_id: uuid.UUID | None = None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class ExperimentUpdate(BaseModel):
|
|
213
|
+
title: str | None = Field(default=None, min_length=1, max_length=512)
|
|
214
|
+
description: str | None = None
|
|
215
|
+
archived: bool | None = None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class ExperimentSummaryRead(ORMModel):
|
|
219
|
+
id: uuid.UUID
|
|
220
|
+
project_id: uuid.UUID
|
|
221
|
+
creator_agent_id: uuid.UUID
|
|
222
|
+
title: str
|
|
223
|
+
description: str | None
|
|
224
|
+
phase: ExperimentPhase
|
|
225
|
+
current_plan_version: int
|
|
226
|
+
topic_id: uuid.UUID | None = None
|
|
227
|
+
warnings: list[str] = Field(default_factory=list)
|
|
228
|
+
created_at: datetime
|
|
229
|
+
updated_at: datetime
|
|
230
|
+
archived_at: datetime | None = None
|
|
231
|
+
# --- execution lock (per-project; CP-3) ---
|
|
232
|
+
lock_holder_experiment_id: uuid.UUID | None = None
|
|
233
|
+
lock_acquired_at: datetime | None = None
|
|
234
|
+
lock_ttl_seconds: int | None = None
|
|
235
|
+
next_attempt_at: datetime | None = None
|
|
236
|
+
lock_skip_count: int = 0
|
|
237
|
+
# Populated for todos / waker fingerprints; defaults to 0 on list endpoints.
|
|
238
|
+
open_unreasonable_count: int = 0
|
|
239
|
+
log_count: int = 0
|
|
240
|
+
latest_log_summary: str | None = None
|
|
241
|
+
# Per-agent capabilities (experiment plan v2 AC#3); populated when actor context exists.
|
|
242
|
+
actions: list[str] = Field(default_factory=list)
|
|
243
|
+
blocked_on: str | None = None
|
|
244
|
+
# Historical annotation: approved/running/done without qualifying non-creator review.
|
|
245
|
+
legacy_self_review: bool = False
|
|
246
|
+
# --- phase routing (experiment f873c287 I1(b)) -----------------------
|
|
247
|
+
# Decision-owner role for the current phase. Drives the UI "host
|
|
248
|
+
# blocked, waiting on {phase_owner}" copy and the
|
|
249
|
+
# ``informational_only`` partition flag below.
|
|
250
|
+
phase_owner: PhaseOwner = PhaseOwner.host
|
|
251
|
+
# Auto-classification (I1(a)): True iff
|
|
252
|
+
# actions == [] AND blocked_on != None AND phase_owner != host.
|
|
253
|
+
# Surfaces in my_open_experiments so waker / web UI can demote the
|
|
254
|
+
# entry out of the host's actionable obligation bucket. The
|
|
255
|
+
# partition is "informational only" — host can still see and act
|
|
256
|
+
# on it; the UI just shows a "waiting on {phase_owner}" copy.
|
|
257
|
+
informational_only: bool = False
|
|
258
|
+
# 0db51e10 I3(5d): phase visibility whitelist fold. True iff the
|
|
259
|
+
# configured whitelist excludes the current phase for the caller's
|
|
260
|
+
# role. Mirrors ``actions == [] AND blocked_on ==
|
|
261
|
+
# "hidden_for_current_persona"`` so CLI / web UI can render a
|
|
262
|
+
# single "folded" copy without leaking actions.
|
|
263
|
+
hidden_for_current_persona: bool = False
|
|
264
|
+
# b72d0542 I1.b: 4-段 template soft validation result, populated only
|
|
265
|
+
# on ``experiment complete`` calls. None on other endpoints (status,
|
|
266
|
+
# list, etc.) — server sets it explicitly in ``complete_experiment``.
|
|
267
|
+
template_validation: "TemplateValidationSchema | None" = None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class AcceptanceStatusRead(BaseModel):
|
|
271
|
+
id: str
|
|
272
|
+
description: str
|
|
273
|
+
acceptance_type: AcceptanceType
|
|
274
|
+
evidence_provided: bool = False
|
|
275
|
+
reviewer_verdict: str | None = None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class ExperimentDetailRead(ExperimentSummaryRead):
|
|
279
|
+
current_plan: PlanVersionRead | None = None
|
|
280
|
+
plan_version_count: int = 0
|
|
281
|
+
review_count: int = 0
|
|
282
|
+
acceptance_status: list[AcceptanceStatusRead] = Field(default_factory=list)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class ExperimentLockRead(ORMModel):
|
|
286
|
+
"""Per-project execution-lock snapshot (CP-3).
|
|
287
|
+
|
|
288
|
+
定义在 ``map_types.schemas`` 以便 server API 与 SDK 共享同一响应模型;
|
|
289
|
+
server 端用 ``ExperimentLockRead.model_validate(result)`` 从 lock_service
|
|
290
|
+
的结果对象构造(``ORMModel`` 已开启 ``from_attributes``)。
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
experiment_id: uuid.UUID
|
|
294
|
+
project_id: uuid.UUID
|
|
295
|
+
holder: uuid.UUID | None = None
|
|
296
|
+
acquired_at: datetime | None = None
|
|
297
|
+
ttl_seconds: int | None = None
|
|
298
|
+
next_attempt_at: datetime | None = None
|
|
299
|
+
skip_count: int = 0
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class ExperimentLockStalledScanRead(BaseModel):
|
|
303
|
+
notification_ids: list[uuid.UUID]
|
|
304
|
+
emitted_count: int
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# --- Log ---
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class ExperimentLogCreate(BaseModel):
|
|
311
|
+
summary: str = Field(min_length=1, max_length=1024)
|
|
312
|
+
content_md: str = Field(min_length=1)
|
|
313
|
+
metadata: dict | None = None
|
|
314
|
+
# b72d0542 I1.b(2)(e): when the similarity check would emit a warning,
|
|
315
|
+
# callers can set this to True to acknowledge and skip the soft
|
|
316
|
+
# warning. Server writes a ``log.force_skip`` audit row when the
|
|
317
|
+
# warning was actually suppressed (no-op when no warning fired).
|
|
318
|
+
force_skip_similarity: bool = False
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class ExperimentLogRead(ORMModel):
|
|
322
|
+
id: uuid.UUID
|
|
323
|
+
experiment_id: uuid.UUID
|
|
324
|
+
author_agent_id: uuid.UUID
|
|
325
|
+
summary: str
|
|
326
|
+
content_md: str
|
|
327
|
+
metadata_json: dict | None
|
|
328
|
+
created_at: datetime
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# --- 8ac93d4e I1.c — Log create response wrapper with evidence validation ---
|
|
332
|
+
|
|
333
|
+
EvidenceWarningCode = Literal["MISSING_EVIDENCE_KEY"]
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class EvidenceWarningSchema(BaseModel):
|
|
337
|
+
code: EvidenceWarningCode
|
|
338
|
+
missing_key: str
|
|
339
|
+
plan_required: bool = True
|
|
340
|
+
log_provided: bool = False
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class EvidenceValidationSchema(BaseModel):
|
|
344
|
+
"""Soft validation result for an ``ExperimentLogCreate`` payload.
|
|
345
|
+
|
|
346
|
+
The validator never blocks the log save — ``valid`` is always True.
|
|
347
|
+
``warnings`` lists evidence_keys declared in plan frontmatter that are
|
|
348
|
+
missing from the supplied metadata; ``parse_error`` is set when the
|
|
349
|
+
plan frontmatter existed but its YAML failed to parse.
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
warnings: list[EvidenceWarningSchema] = Field(default_factory=list)
|
|
353
|
+
parse_error: str | None = None
|
|
354
|
+
plan_keys: list[str] = Field(default_factory=list)
|
|
355
|
+
valid: bool = True
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class LogCreateResponse(BaseModel):
|
|
359
|
+
"""Wrapper returned by ``POST /experiments/{id}/logs`` (8ac93d4e I1.c).
|
|
360
|
+
|
|
361
|
+
The persisted ``log`` is unchanged from ``ExperimentLogRead`` so v1
|
|
362
|
+
consumers can still parse the response by reaching into ``log``. The
|
|
363
|
+
new ``validation`` field surfaces soft evidence-key warnings without
|
|
364
|
+
breaking v1 schema.
|
|
365
|
+
|
|
366
|
+
b72d0542 I1.b(2)(e): ``similarity_warning`` carries the soft
|
|
367
|
+
content-similarity check result (always None on endpoints other than
|
|
368
|
+
``POST /experiments/{id}/logs``). When the warning fires AND
|
|
369
|
+
``force_skip_similarity=True`` was supplied, the warning is suppressed
|
|
370
|
+
in this response and a ``log.force_skip`` audit row is written
|
|
371
|
+
instead. ``force_skip`` field echoes whether the caller opted in
|
|
372
|
+
(False on responses without a similarity check).
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
log: ExperimentLogRead
|
|
376
|
+
validation: EvidenceValidationSchema
|
|
377
|
+
similarity_warning: "SimilarityWarningSchema | None" = None
|
|
378
|
+
force_skip: bool = False
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# --- b72d0542 I1.b — Result submission 4-段 template validation ---
|
|
382
|
+
|
|
383
|
+
TemplateWarningCode = Literal[
|
|
384
|
+
"MISSING_TEMPLATE_SECTION",
|
|
385
|
+
"NO_LINK_IN_LOG_SECTION",
|
|
386
|
+
"MALFORMED_MARKDOWN_LINK",
|
|
387
|
+
]
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class TemplateWarningSchema(BaseModel):
|
|
391
|
+
code: TemplateWarningCode
|
|
392
|
+
section: str | None = None
|
|
393
|
+
detail: str | None = None
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class TemplateValidationSchema(BaseModel):
|
|
397
|
+
"""Soft validation result for a result submission (b72d0542 I1.b).
|
|
398
|
+
|
|
399
|
+
Mirrors :class:`EvidenceValidationSchema` (8ac93d4e I1.c): ``valid``
|
|
400
|
+
is always True (soft validation never blocks ``experiment complete``).
|
|
401
|
+
``warnings`` lists missing 4-段 sections or malformed markdown links
|
|
402
|
+
detected in the ``## 实施 log`` section body.
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
warnings: list[TemplateWarningSchema] = Field(default_factory=list)
|
|
406
|
+
sections_present: list[str] = Field(default_factory=list)
|
|
407
|
+
log_link_count: int = 0
|
|
408
|
+
valid: bool = True
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# --- b72d0542 I1.b(2)(e) — Log similarity soft warning ------------------
|
|
412
|
+
|
|
413
|
+
SimilarityWarningCode = Literal["HIGH_CONTENT_SIMILARITY"]
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class SimilarityWarningSchema(BaseModel):
|
|
417
|
+
"""Soft warning fired when the new log body is too similar to a
|
|
418
|
+
previous log on the same experiment (b72d0542 I1.b(2)(b)).
|
|
419
|
+
|
|
420
|
+
``score`` is the cosine similarity in ``[0.0, 1.0]`` between the new
|
|
421
|
+
log's content embedding and the most-similar previous log on the
|
|
422
|
+
same experiment. ``threshold`` is the warn threshold (plan default
|
|
423
|
+
0.7). ``ref_log_id`` points to the previous log the score was
|
|
424
|
+
computed against. ``model`` is the embedding model id used (e.g.
|
|
425
|
+
``sentence-transformers/all-MiniLM-L6-v2``).
|
|
426
|
+
|
|
427
|
+
The warning is non-blocking; callers may pass
|
|
428
|
+
``force_skip_similarity=True`` to suppress it AND write a
|
|
429
|
+
``log.force_skip`` audit row instead.
|
|
430
|
+
"""
|
|
431
|
+
|
|
432
|
+
code: SimilarityWarningCode
|
|
433
|
+
score: float
|
|
434
|
+
threshold: float
|
|
435
|
+
ref_log_id: uuid.UUID
|
|
436
|
+
model: str
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class ExperimentComplete(BaseModel):
|
|
440
|
+
summary: str = Field(min_length=1, max_length=1024)
|
|
441
|
+
content_md: str = Field(min_length=1)
|
|
442
|
+
metadata: dict | None = None
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
# --- Review verdict file (accept-result structured) -----------------------
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# WaivedReason v2: min_length=50 (Round 2 Summary consensus) +
|
|
449
|
+
# max_length=1000 (v2 expanded to fit waiver four-part rationale:
|
|
450
|
+
# "豁免什么 / 为何豁免 / 影响哪些下游消费方 / 是否有补偿措施").
|
|
451
|
+
WaivedReason = Annotated[
|
|
452
|
+
str,
|
|
453
|
+
Field(
|
|
454
|
+
min_length=50,
|
|
455
|
+
max_length=1000,
|
|
456
|
+
description="Waiver rationale (review item verdict == waived).",
|
|
457
|
+
),
|
|
458
|
+
]
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class ReviewVerdictItem(BaseModel):
|
|
462
|
+
item_id: uuid.UUID
|
|
463
|
+
verdict: ReviewVerdict
|
|
464
|
+
reason: WaivedReason | None = Field(
|
|
465
|
+
default=None,
|
|
466
|
+
description="Required when verdict == waived; otherwise optional.",
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
@field_validator("verdict", mode="before")
|
|
470
|
+
@classmethod
|
|
471
|
+
def _accept_review_verdict_aliases(cls, v: object) -> object:
|
|
472
|
+
"""cli-ux PR3: accept accept|reject|dismiss as aliases.
|
|
473
|
+
|
|
474
|
+
Maps to the canonical ``ReviewVerdict`` enum:
|
|
475
|
+
|
|
476
|
+
* ``accept`` / ``passed`` → ``ReviewVerdict.passed``
|
|
477
|
+
* ``reject`` / ``failed`` → ``ReviewVerdict.failed``
|
|
478
|
+
* ``dismiss`` / ``waived`` → ``ReviewVerdict.waived``
|
|
479
|
+
|
|
480
|
+
Serialized output is still the canonical ``passed|failed|waived``
|
|
481
|
+
string, so DB rows and downstream JSON consumers see no change.
|
|
482
|
+
"""
|
|
483
|
+
if isinstance(v, str):
|
|
484
|
+
normalized = v.strip().lower()
|
|
485
|
+
mapping: dict[str, ReviewVerdict] = {
|
|
486
|
+
"accept": ReviewVerdict.passed,
|
|
487
|
+
"passed": ReviewVerdict.passed,
|
|
488
|
+
"reject": ReviewVerdict.failed,
|
|
489
|
+
"failed": ReviewVerdict.failed,
|
|
490
|
+
"dismiss": ReviewVerdict.waived,
|
|
491
|
+
"waived": ReviewVerdict.waived,
|
|
492
|
+
}
|
|
493
|
+
if normalized in mapping:
|
|
494
|
+
return mapping[normalized]
|
|
495
|
+
return v
|
|
496
|
+
|
|
497
|
+
@model_validator(mode="after")
|
|
498
|
+
def _waived_requires_reason(self) -> "ReviewVerdictItem":
|
|
499
|
+
if self.verdict == ReviewVerdict.waived and not (self.reason and self.reason.strip()):
|
|
500
|
+
raise ValueError("verdict 'waived' requires non-empty reason (WaivedReason, 50-1000 chars)")
|
|
501
|
+
return self
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
class ReviewInvariantCheck(BaseModel):
|
|
505
|
+
item_id: uuid.UUID
|
|
506
|
+
verified: bool
|
|
507
|
+
note: str | None = Field(default=None, max_length=1000)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
class ReviewVerdictFile(BaseModel):
|
|
511
|
+
review_id: uuid.UUID
|
|
512
|
+
verdicts: list[ReviewVerdictItem] = Field(default_factory=list)
|
|
513
|
+
invariants: list[ReviewInvariantCheck] = Field(default_factory=list)
|
|
514
|
+
|
|
515
|
+
@field_validator("verdicts")
|
|
516
|
+
@classmethod
|
|
517
|
+
def _no_duplicate_item_ids(cls, v: list[ReviewVerdictItem]) -> list[ReviewVerdictItem]:
|
|
518
|
+
seen: set[uuid.UUID] = set()
|
|
519
|
+
for item in v:
|
|
520
|
+
if item.item_id in seen:
|
|
521
|
+
raise ValueError(f"Duplicate item_id in verdicts: {item.item_id}")
|
|
522
|
+
seen.add(item.item_id)
|
|
523
|
+
return v
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# --- Result decision -------------------------------------------------------
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class ExperimentResultDecision(BaseModel):
|
|
530
|
+
summary: str = Field(min_length=1, max_length=1024)
|
|
531
|
+
content_md: str = Field(min_length=1)
|
|
532
|
+
metadata: dict | None = None
|
|
533
|
+
verdict_file: ReviewVerdictFile | None = Field(
|
|
534
|
+
default=None,
|
|
535
|
+
description=(
|
|
536
|
+
"Optional structured verdict file (CLI: --review-verdict-file). "
|
|
537
|
+
"When provided, server validates item_id.review_id ownership and "
|
|
538
|
+
"records pre_schema_accept_result='false' + verdict breakdown in "
|
|
539
|
+
"log metadata. Omit (legacy) → pre_schema_accept_result='true' with "
|
|
540
|
+
"an info-level warning."
|
|
541
|
+
),
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
class ExperimentBundleRead(BaseModel):
|
|
546
|
+
"""Aggregated experiment page payload (detail + plans + reviews + comment tree + logs)."""
|
|
547
|
+
|
|
548
|
+
experiment: ExperimentDetailRead
|
|
549
|
+
plans: list[PlanVersionRead] = Field(default_factory=list)
|
|
550
|
+
reviews: list[ReviewRead] = Field(default_factory=list)
|
|
551
|
+
comments: list[CommentTreeNode] = Field(default_factory=list)
|
|
552
|
+
logs: list[ExperimentLogRead] = Field(default_factory=list)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
# --- Status ---
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
class GlobalStatusRead(BaseModel):
|
|
559
|
+
total_experiments_by_phase: dict[str, int]
|
|
560
|
+
projects: list[ProjectStatusRead]
|
|
561
|
+
recent_experiments: list[ExperimentSummaryRead]
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class AgentRead(ORMModel):
|
|
565
|
+
id: uuid.UUID
|
|
566
|
+
name: str
|
|
567
|
+
role: AgentRole
|
|
568
|
+
project_id: uuid.UUID | None
|
|
569
|
+
project_key: str | None = None
|
|
570
|
+
created_at: datetime
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
class AgentCreateResponse(AgentRead):
|
|
574
|
+
api_token: str
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
class AgentCreate(BaseModel):
|
|
578
|
+
"""Body for ``POST /api/v1/agents`` (cleanup experiment f12a5638 P2 #5).
|
|
579
|
+
|
|
580
|
+
Replaces the legacy query-param creation contract so the endpoint
|
|
581
|
+
follows the same body-driven pattern as every other write endpoint
|
|
582
|
+
in the API. ``project_id`` and ``project_key`` are both accepted for
|
|
583
|
+
caller convenience — at most one must resolve to a project for
|
|
584
|
+
``role=agent`` (admin-only). When both are omitted the service layer
|
|
585
|
+
raises ``ValueError`` which maps to 400.
|
|
586
|
+
"""
|
|
587
|
+
|
|
588
|
+
name: str = Field(min_length=1, max_length=128)
|
|
589
|
+
role: AgentRole = AgentRole.agent
|
|
590
|
+
project_id: uuid.UUID | None = None
|
|
591
|
+
project_key: str | None = None
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
# --- Topic ---
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
class TopicCreate(BaseModel):
|
|
598
|
+
title: str = Field(min_length=1, max_length=512)
|
|
599
|
+
description: str | None = None
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
class TopicUpdate(BaseModel):
|
|
603
|
+
title: str | None = Field(default=None, min_length=1, max_length=512)
|
|
604
|
+
description: str | None = None
|
|
605
|
+
pinned: bool | None = None
|
|
606
|
+
archived: bool | None = None
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
class TopicAdvanceRound(BaseModel):
|
|
610
|
+
increment_summary: bool = True
|
|
611
|
+
acknowledged_by: list[uuid.UUID] = Field(default_factory=list)
|
|
612
|
+
ack: Literal["accept", "reject", "dismiss"] | None = None
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class TopicActionItemCreate(BaseModel):
|
|
616
|
+
title: str = Field(min_length=1, max_length=512)
|
|
617
|
+
description: str | None = None
|
|
618
|
+
owner_agent_id: uuid.UUID | None = None
|
|
619
|
+
due_at: datetime | None = None
|
|
620
|
+
linked_experiment_id: uuid.UUID | None = None
|
|
621
|
+
category: ActionItemCategory | None = None
|
|
622
|
+
# Optional id enables resolve-time upsert. Service layer preserves status when id
|
|
623
|
+
# matches an existing item; new ids are inserted as open. None means "always insert".
|
|
624
|
+
id: uuid.UUID | None = None
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
# Minimum reason length enforced per-category; mirrors server-side validation so the
|
|
628
|
+
# CLI and API stay consistent (A1 acceptance A1-3 + A1-4).
|
|
629
|
+
_CANCEL_REASON_MIN_LENGTH: dict[ActionItemCategory | None, int] = {
|
|
630
|
+
None: 8,
|
|
631
|
+
ActionItemCategory.unspecified: 8,
|
|
632
|
+
ActionItemCategory.implementation: 8,
|
|
633
|
+
ActionItemCategory.decision: 16,
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _cancel_reason_min_length(category: ActionItemCategory | None) -> int:
|
|
638
|
+
return _CANCEL_REASON_MIN_LENGTH.get(category, 8)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
class ActionItemCancel(BaseModel):
|
|
642
|
+
"""Body of ``POST /api/v1/action-items/{id}/cancel`` and ``map action cancel``."""
|
|
643
|
+
|
|
644
|
+
reason: str = Field(min_length=1)
|
|
645
|
+
category: ActionItemCategory | None = None
|
|
646
|
+
|
|
647
|
+
@model_validator(mode="after")
|
|
648
|
+
def validate_reason_length(self) -> "ActionItemCancel":
|
|
649
|
+
threshold = _cancel_reason_min_length(self.category)
|
|
650
|
+
if len(self.reason.strip()) < threshold:
|
|
651
|
+
raise ValueError(
|
|
652
|
+
f"reason too short: {len(self.reason.strip())} < {threshold} "
|
|
653
|
+
f"(category={self.category.value if self.category else 'unspecified'})"
|
|
654
|
+
)
|
|
655
|
+
return self
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
class TopicResolve(BaseModel):
|
|
659
|
+
decision: str | None = Field(default=None, min_length=1)
|
|
660
|
+
rationale: str | None = None
|
|
661
|
+
rejected_options: str | None = None
|
|
662
|
+
open_questions: str | None = None
|
|
663
|
+
no_decision_reason: str | None = Field(default=None, min_length=1)
|
|
664
|
+
action_items: list[TopicActionItemCreate] = Field(default_factory=list)
|
|
665
|
+
|
|
666
|
+
@field_validator("decision", "rationale", "rejected_options", "open_questions", "no_decision_reason", mode="before")
|
|
667
|
+
@classmethod
|
|
668
|
+
def normalize_blank_text(cls, value):
|
|
669
|
+
if value is None:
|
|
670
|
+
return None
|
|
671
|
+
if not isinstance(value, str):
|
|
672
|
+
return value
|
|
673
|
+
text = value.strip()
|
|
674
|
+
return text or None
|
|
675
|
+
|
|
676
|
+
@model_validator(mode="after")
|
|
677
|
+
def validate_resolution_content(self) -> "TopicResolve":
|
|
678
|
+
if not self.decision and not self.no_decision_reason:
|
|
679
|
+
raise ValueError("decision or no_decision_reason is required")
|
|
680
|
+
return self
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
class TopicActionItemRead(BaseModel):
|
|
684
|
+
id: uuid.UUID
|
|
685
|
+
decision_id: uuid.UUID
|
|
686
|
+
project_id: uuid.UUID
|
|
687
|
+
topic_id: uuid.UUID
|
|
688
|
+
title: str
|
|
689
|
+
description: str | None = None
|
|
690
|
+
owner_agent_id: uuid.UUID | None = None
|
|
691
|
+
owner_name: str | None = None
|
|
692
|
+
status: TopicActionItemStatus
|
|
693
|
+
due_at: datetime | None = None
|
|
694
|
+
linked_experiment_id: uuid.UUID | None = None
|
|
695
|
+
# 当前关联实验所处阶段。当 action_item 仍为 open 但实验已进入
|
|
696
|
+
# result_review 时,用户可据此判断"在等 reviewer 审批"而非 host 待办。
|
|
697
|
+
linked_experiment_phase: ExperimentPhase | None = None
|
|
698
|
+
category: ActionItemCategory | None = None
|
|
699
|
+
cancel_reason: str | None = None
|
|
700
|
+
suggested_linked_experiment_id: uuid.UUID | None = None
|
|
701
|
+
suggested_linked_experiment_title: str | None = None
|
|
702
|
+
# Wake / stale escalation fields (experiment B, plan §2 §3). Surfaced so
|
|
703
|
+
# the runtime-waker can run should_wake_action_item() against the same
|
|
704
|
+
# payload it gets from the todos endpoint, avoiding a second fetch.
|
|
705
|
+
wake_count: int = 0
|
|
706
|
+
first_open_at: datetime | None = None
|
|
707
|
+
last_woken_at: datetime | None = None
|
|
708
|
+
stale_at: datetime | None = None
|
|
709
|
+
created_at: datetime
|
|
710
|
+
updated_at: datetime
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
class TopicDecisionRead(BaseModel):
|
|
714
|
+
id: uuid.UUID
|
|
715
|
+
project_id: uuid.UUID
|
|
716
|
+
topic_id: uuid.UUID
|
|
717
|
+
topic_title: str | None = None
|
|
718
|
+
author_agent_id: uuid.UUID
|
|
719
|
+
author_name: str | None = None
|
|
720
|
+
decision: str | None = None
|
|
721
|
+
rationale: str | None = None
|
|
722
|
+
rejected_options: str | None = None
|
|
723
|
+
open_questions: str | None = None
|
|
724
|
+
no_decision_reason: str | None = None
|
|
725
|
+
action_items: list[TopicActionItemRead] = Field(default_factory=list)
|
|
726
|
+
created_at: datetime
|
|
727
|
+
updated_at: datetime
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
class TopicSummaryRead(BaseModel):
|
|
731
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
732
|
+
|
|
733
|
+
id: uuid.UUID
|
|
734
|
+
project_id: uuid.UUID
|
|
735
|
+
creator_agent_id: uuid.UUID
|
|
736
|
+
creator_name: str | None = None
|
|
737
|
+
title: str
|
|
738
|
+
description: str | None
|
|
739
|
+
status: TopicStatus
|
|
740
|
+
pinned: bool = False
|
|
741
|
+
discussion_round: TopicDiscussionRound = TopicDiscussionRound.round1
|
|
742
|
+
round_summary_count: int = 0
|
|
743
|
+
comment_count: int = 0
|
|
744
|
+
experiment_count: int = 0
|
|
745
|
+
last_comment_id: uuid.UUID | None = None
|
|
746
|
+
last_comment_author_agent_id: uuid.UUID | None = None
|
|
747
|
+
last_comment_author_name: str | None = None
|
|
748
|
+
last_comment_excerpt: str | None = None
|
|
749
|
+
my_comment_count: int | None = None
|
|
750
|
+
dismissed_at: datetime | None = None
|
|
751
|
+
stale_since: datetime | None = Field(
|
|
752
|
+
default=None,
|
|
753
|
+
validation_alias=AliasChoices("stale_since", "advance_round_pending_since"),
|
|
754
|
+
description="When this topic's pending action started waiting (round ack, reply, etc.). Renamed from advance_round_pending_since in N=2; old name remains readable until N=2.",
|
|
755
|
+
)
|
|
756
|
+
created_at: datetime
|
|
757
|
+
updated_at: datetime
|
|
758
|
+
archived_at: datetime | None = None
|
|
759
|
+
|
|
760
|
+
@computed_field # type: ignore[prop-decorator]
|
|
761
|
+
@property
|
|
762
|
+
def advance_round_pending_since(self) -> datetime | None:
|
|
763
|
+
"""DEPRECATED alias for stale_since — kept readable for clients still using the old name."""
|
|
764
|
+
return self.stale_since
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
class TopicCommentCreate(BaseModel):
|
|
768
|
+
body: str = Field(min_length=1)
|
|
769
|
+
parent_id: uuid.UUID | None = None
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
class TopicCommentRead(ORMModel):
|
|
773
|
+
id: uuid.UUID
|
|
774
|
+
topic_id: uuid.UUID
|
|
775
|
+
author_agent_id: uuid.UUID
|
|
776
|
+
author_name: str | None = None
|
|
777
|
+
parent_comment_id: uuid.UUID | None
|
|
778
|
+
body: str
|
|
779
|
+
kind: TopicCommentKind = TopicCommentKind.user
|
|
780
|
+
comment_seq: int
|
|
781
|
+
created_at: datetime
|
|
782
|
+
unresolved_mentions: list[str] = Field(default_factory=list)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
class TopicCommentTreeNode(TopicCommentRead):
|
|
786
|
+
children: list["TopicCommentTreeNode"] = Field(default_factory=list)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
class TopicRead(TopicSummaryRead):
|
|
790
|
+
experiments: list[ExperimentSummaryRead] = Field(default_factory=list)
|
|
791
|
+
comments: list[TopicCommentTreeNode] = Field(default_factory=list)
|
|
792
|
+
decision: TopicDecisionRead | None = None
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
class TopicProgressCommentRead(ORMModel):
|
|
796
|
+
id: uuid.UUID
|
|
797
|
+
author_agent_id: uuid.UUID
|
|
798
|
+
author_name: str | None = None
|
|
799
|
+
parent_comment_id: uuid.UUID | None
|
|
800
|
+
body: str
|
|
801
|
+
excerpt: str
|
|
802
|
+
created_at: datetime
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
class TopicWorkItemRead(BaseModel):
|
|
806
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
807
|
+
|
|
808
|
+
kind: str
|
|
809
|
+
priority: str
|
|
810
|
+
topic_id: uuid.UUID
|
|
811
|
+
topic_title: str
|
|
812
|
+
source_comment_id: uuid.UUID | None = None
|
|
813
|
+
thread_root_id: uuid.UUID | None = None
|
|
814
|
+
required_agent_id: uuid.UUID
|
|
815
|
+
reason: str
|
|
816
|
+
idempotency_key: str
|
|
817
|
+
clear_action: str
|
|
818
|
+
excerpt: str
|
|
819
|
+
created_at: datetime
|
|
820
|
+
discussion_round: str | None = None
|
|
821
|
+
stale_since: datetime | None = Field(
|
|
822
|
+
default=None,
|
|
823
|
+
validation_alias=AliasChoices("stale_since", "advance_round_pending_since"),
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
@computed_field # type: ignore[prop-decorator]
|
|
827
|
+
@property
|
|
828
|
+
def advance_round_pending_since(self) -> datetime | None:
|
|
829
|
+
"""DEPRECATED alias for stale_since."""
|
|
830
|
+
return self.stale_since
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
class TopicProgressItemRead(BaseModel):
|
|
834
|
+
topic_id: uuid.UUID
|
|
835
|
+
topic_title: str
|
|
836
|
+
discussion_round: TopicDiscussionRound
|
|
837
|
+
last_comment_author_agent_id: uuid.UUID | None = None
|
|
838
|
+
last_comment_author_name: str | None = None
|
|
839
|
+
my_last_comment_id: uuid.UUID | None = None
|
|
840
|
+
new_comments: list[TopicProgressCommentRead] = Field(default_factory=list)
|
|
841
|
+
new_comment_count: int = 0
|
|
842
|
+
work_items: list[TopicWorkItemRead] = Field(default_factory=list)
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
class TopicProgressListRead(BaseModel):
|
|
846
|
+
items: list[TopicProgressItemRead] = Field(default_factory=list)
|
|
847
|
+
total: int = 0
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
class PendingReplyRead(BaseModel):
|
|
851
|
+
item_id: uuid.UUID
|
|
852
|
+
experiment_id: uuid.UUID
|
|
853
|
+
experiment_title: str
|
|
854
|
+
content: str
|
|
855
|
+
status: ReviewItemStatus
|
|
856
|
+
updated_at: datetime
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
class PendingPlanRevisionRead(BaseModel):
|
|
860
|
+
experiment_id: uuid.UUID
|
|
861
|
+
experiment_title: str
|
|
862
|
+
current_plan_version: int
|
|
863
|
+
open_unreasonable_count: int
|
|
864
|
+
blocked_on: str = "open_unreasonable_item"
|
|
865
|
+
actions: list[str] = Field(default_factory=lambda: ["plan_revise"])
|
|
866
|
+
updated_at: datetime
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
class PendingTopicReplyTodoRead(BaseModel):
|
|
870
|
+
topic_id: uuid.UUID
|
|
871
|
+
topic_title: str
|
|
872
|
+
comment_id: uuid.UUID
|
|
873
|
+
parent_comment_id: uuid.UUID | None = None
|
|
874
|
+
thread_root_id: uuid.UUID
|
|
875
|
+
author_agent_id: uuid.UUID
|
|
876
|
+
author_name: str | None = None
|
|
877
|
+
excerpt: str
|
|
878
|
+
created_at: datetime
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
class PendingRoundAckTodoRead(BaseModel):
|
|
882
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
883
|
+
|
|
884
|
+
topic_id: uuid.UUID
|
|
885
|
+
topic_title: str
|
|
886
|
+
discussion_round: TopicDiscussionRound
|
|
887
|
+
round_summary_count: int = 0
|
|
888
|
+
summary_comment_id: uuid.UUID | None = None
|
|
889
|
+
summary_excerpt: str | None = None
|
|
890
|
+
stale_since: datetime | None = Field(
|
|
891
|
+
default=None,
|
|
892
|
+
validation_alias=AliasChoices("stale_since", "advance_round_pending_since"),
|
|
893
|
+
)
|
|
894
|
+
updated_at: datetime
|
|
895
|
+
|
|
896
|
+
@computed_field # type: ignore[prop-decorator]
|
|
897
|
+
@property
|
|
898
|
+
def advance_round_pending_since(self) -> datetime | None:
|
|
899
|
+
"""DEPRECATED alias for stale_since."""
|
|
900
|
+
return self.stale_since
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
class PendingAdvanceRoundTodoRead(BaseModel):
|
|
904
|
+
"""Host-owned topics where participant acks are complete and advance-round is due."""
|
|
905
|
+
|
|
906
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
907
|
+
|
|
908
|
+
topic_id: uuid.UUID
|
|
909
|
+
topic_title: str
|
|
910
|
+
discussion_round: TopicDiscussionRound
|
|
911
|
+
round_summary_count: int = 0
|
|
912
|
+
stale_since: datetime | None = Field(
|
|
913
|
+
default=None,
|
|
914
|
+
validation_alias=AliasChoices("stale_since", "advance_round_pending_since"),
|
|
915
|
+
)
|
|
916
|
+
updated_at: datetime
|
|
917
|
+
|
|
918
|
+
@computed_field # type: ignore[prop-decorator]
|
|
919
|
+
@property
|
|
920
|
+
def advance_round_pending_since(self) -> datetime | None:
|
|
921
|
+
"""DEPRECATED alias for stale_since."""
|
|
922
|
+
return self.stale_since
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
class StaleOpenTopicTodoRead(BaseModel):
|
|
926
|
+
"""Host-owned open topic that has had no activity for the stale threshold."""
|
|
927
|
+
|
|
928
|
+
topic_id: uuid.UUID
|
|
929
|
+
topic_title: str
|
|
930
|
+
discussion_round: TopicDiscussionRound
|
|
931
|
+
round_summary_count: int = 0
|
|
932
|
+
stale_since: datetime
|
|
933
|
+
updated_at: datetime
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
class MentionTodoRead(BaseModel):
|
|
937
|
+
id: uuid.UUID
|
|
938
|
+
mentioned_agent_id: uuid.UUID
|
|
939
|
+
author_agent_id: uuid.UUID
|
|
940
|
+
author_name: str | None = None
|
|
941
|
+
source_type: str
|
|
942
|
+
source_id: uuid.UUID
|
|
943
|
+
project_id: uuid.UUID
|
|
944
|
+
experiment_id: uuid.UUID | None
|
|
945
|
+
topic_id: uuid.UUID | None
|
|
946
|
+
excerpt: str
|
|
947
|
+
created_at: datetime
|
|
948
|
+
dismissed_at: datetime | None = None
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
class DismissMentionResultRead(BaseModel):
|
|
952
|
+
id: uuid.UUID
|
|
953
|
+
dismissed_at: datetime
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
class DismissAllMentionsResultRead(BaseModel):
|
|
957
|
+
dismissed: int
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
class TopicReadCursorRead(BaseModel):
|
|
961
|
+
topic_id: uuid.UUID
|
|
962
|
+
agent_id: uuid.UUID
|
|
963
|
+
last_read_comment_seq: int
|
|
964
|
+
updated_at: datetime
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
class TopicActionItemTodoRead(BaseModel):
|
|
968
|
+
id: uuid.UUID
|
|
969
|
+
decision_id: uuid.UUID
|
|
970
|
+
project_id: uuid.UUID
|
|
971
|
+
topic_id: uuid.UUID
|
|
972
|
+
topic_title: str
|
|
973
|
+
title: str
|
|
974
|
+
description: str | None = None
|
|
975
|
+
status: TopicActionItemStatus
|
|
976
|
+
due_at: datetime | None = None
|
|
977
|
+
linked_experiment_id: uuid.UUID | None = None
|
|
978
|
+
# 同 TopicActionItemRead.linked_experiment_phase:让 todos 消费者(CLI /
|
|
979
|
+
# waker / Web)能区分"等 reviewer 审批"与"host 自身待办"。
|
|
980
|
+
linked_experiment_phase: ExperimentPhase | None = None
|
|
981
|
+
# Wake / stale escalation fields (experiment B). The waker applies
|
|
982
|
+
# should_wake_action_item() directly to this payload — see
|
|
983
|
+
# cli/action_item_escalation.py scan_pending_action_items() for the caller.
|
|
984
|
+
wake_count: int = 0
|
|
985
|
+
first_open_at: datetime | None = None
|
|
986
|
+
last_woken_at: datetime | None = None
|
|
987
|
+
stale_at: datetime | None = None
|
|
988
|
+
created_at: datetime
|
|
989
|
+
updated_at: datetime
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
class ExperimentReviewInformationalRead(BaseModel):
|
|
993
|
+
"""Read-only experiment review snapshot for non-reviewer personas."""
|
|
994
|
+
|
|
995
|
+
experiment_title: str
|
|
996
|
+
phase: ExperimentPhase
|
|
997
|
+
updated_at: datetime
|
|
998
|
+
review_progress: str
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
class TodoRead(BaseModel):
|
|
1002
|
+
my_open_experiments: list[ExperimentSummaryRead] = Field(default_factory=list)
|
|
1003
|
+
pending_reviews: list[ExperimentSummaryRead] = Field(default_factory=list)
|
|
1004
|
+
pending_result_reviews: list[ExperimentSummaryRead] = Field(default_factory=list)
|
|
1005
|
+
experiment_review_informational: list[ExperimentReviewInformationalRead] = Field(
|
|
1006
|
+
default_factory=list
|
|
1007
|
+
)
|
|
1008
|
+
pending_replies: list[PendingReplyRead] = Field(default_factory=list)
|
|
1009
|
+
pending_plan_revisions: list[PendingPlanRevisionRead] = Field(default_factory=list)
|
|
1010
|
+
pending_topic_replies: list[PendingTopicReplyTodoRead] = Field(default_factory=list)
|
|
1011
|
+
pending_round_acks: list[PendingRoundAckTodoRead] = Field(default_factory=list)
|
|
1012
|
+
pending_advance_rounds: list[PendingAdvanceRoundTodoRead] = Field(default_factory=list)
|
|
1013
|
+
stale_open_topics: list[StaleOpenTopicTodoRead] = Field(default_factory=list)
|
|
1014
|
+
my_open_topics: list[TopicSummaryRead] = Field(default_factory=list)
|
|
1015
|
+
mentions: list[MentionTodoRead] = Field(default_factory=list)
|
|
1016
|
+
action_items: list[TopicActionItemTodoRead] = Field(default_factory=list)
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
# --- Notification ---
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
class NotificationRead(ORMModel):
|
|
1023
|
+
id: uuid.UUID
|
|
1024
|
+
recipient_agent_id: uuid.UUID
|
|
1025
|
+
project_id: uuid.UUID | None
|
|
1026
|
+
event: str
|
|
1027
|
+
summary: str
|
|
1028
|
+
target_type: str
|
|
1029
|
+
target_id: uuid.UUID | None
|
|
1030
|
+
payload_json: dict | None
|
|
1031
|
+
category: NotificationCategory = NotificationCategory.digest
|
|
1032
|
+
group_key: str | None = None
|
|
1033
|
+
wake_version: int = 1
|
|
1034
|
+
fingerprint_version: NotificationFingerprintVersion = NotificationFingerprintVersion.v2
|
|
1035
|
+
event_count: int = 1
|
|
1036
|
+
first_event_at: datetime | None = None
|
|
1037
|
+
last_event_at: datetime | None = None
|
|
1038
|
+
read_at: datetime | None
|
|
1039
|
+
created_at: datetime
|
|
1040
|
+
updated_at: datetime | None = None
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
class NotificationListRead(BaseModel):
|
|
1044
|
+
items: list[NotificationRead]
|
|
1045
|
+
total: int
|
|
1046
|
+
unread_count: int
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
class AgentWorkRead(BaseModel):
|
|
1050
|
+
"""Unified work snapshot for Web 待办, waker, and CLI ``map work``."""
|
|
1051
|
+
|
|
1052
|
+
agent: AgentRead
|
|
1053
|
+
topic_progress: TopicProgressListRead
|
|
1054
|
+
todos: TodoRead
|
|
1055
|
+
notifications: NotificationListRead
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
BucketVisibility = Literal["all", "host_only", "reviewer_only", "participant_only"]
|
|
1059
|
+
SummaryBucketKind = Literal[
|
|
1060
|
+
"mention",
|
|
1061
|
+
"round_ack",
|
|
1062
|
+
"pending_reply",
|
|
1063
|
+
"explicit_only",
|
|
1064
|
+
"informational_only",
|
|
1065
|
+
"action_items",
|
|
1066
|
+
]
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
class SummaryBucketItem(BaseModel):
|
|
1070
|
+
"""A short summary of one item inside a bucket. The full item is in
|
|
1071
|
+
``map work``'s ``todos`` and ``topic_progress``; this is the slice
|
|
1072
|
+
rendered on the /work summary card.
|
|
1073
|
+
"""
|
|
1074
|
+
|
|
1075
|
+
kind: SummaryBucketKind
|
|
1076
|
+
topic_id: uuid.UUID | None = None
|
|
1077
|
+
topic_title: str | None = None
|
|
1078
|
+
excerpt: str | None = None
|
|
1079
|
+
updated_at: datetime | None = None
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
class SummaryBucket(BaseModel):
|
|
1083
|
+
"""One of the 6 summary buckets exposed by ``map work --summary``.
|
|
1084
|
+
|
|
1085
|
+
A bucket is a kind-based aggregation of action items / context items so the
|
|
1086
|
+
UI can render a top-of-page card without scanning the full partition
|
|
1087
|
+
list.
|
|
1088
|
+
"""
|
|
1089
|
+
|
|
1090
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
1091
|
+
|
|
1092
|
+
kind: SummaryBucketKind
|
|
1093
|
+
count: int
|
|
1094
|
+
visibility: BucketVisibility = "all"
|
|
1095
|
+
items: list[SummaryBucketItem] = Field(default_factory=list)
|
|
1096
|
+
top_excerpt: str | None = None
|
|
1097
|
+
|
|
1098
|
+
@computed_field # type: ignore[prop-decorator]
|
|
1099
|
+
@property
|
|
1100
|
+
def partition_visibility(self) -> BucketVisibility:
|
|
1101
|
+
"""DEPRECATED alias for visibility — kept readable for clients still using the old name."""
|
|
1102
|
+
return self.visibility
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
class AgentWorkSummaryRead(BaseModel):
|
|
1106
|
+
"""6-bucket by_kind summary returned by ``map work --summary``.
|
|
1107
|
+
|
|
1108
|
+
Compact view intended for waker quick-scans and the Web /work top card.
|
|
1109
|
+
Full per-partition detail stays in ``AgentWorkRead``.
|
|
1110
|
+
"""
|
|
1111
|
+
|
|
1112
|
+
agent: AgentRead
|
|
1113
|
+
buckets: list[SummaryBucket] = Field(default_factory=list)
|
|
1114
|
+
topics_needing_attention: int = 0
|
|
1115
|
+
experiments_needing_attention: int = 0
|
|
1116
|
+
# f873c287 I1(e): per-phase_owner breakdown of the experiment attention
|
|
1117
|
+
# counter so host can see "3 are mine, 2 are waiting on reviewer" at a
|
|
1118
|
+
# glance. Keys are ``PhaseOwner.value`` strings ("host" / "reviewer" /
|
|
1119
|
+
# "participant" / "admin"). Visibility-filtered: under
|
|
1120
|
+
# ``visibility_filter_applied=True`` the ``host`` key is dropped because
|
|
1121
|
+
# the underlying bucket is host_only.
|
|
1122
|
+
experiments_needing_attention_by_owner: dict[str, int] = Field(
|
|
1123
|
+
default_factory=dict
|
|
1124
|
+
)
|
|
1125
|
+
topics_truncated: int = 0
|
|
1126
|
+
experiments_truncated: int = 0
|
|
1127
|
+
visibility_filter_applied: bool = True
|
|
1128
|
+
topics_limit: int = 10
|
|
1129
|
+
experiments_limit: int = 5
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
# --- InboundEvent ---
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
class InboundEventCreate(BaseModel):
|
|
1136
|
+
"""Waker-side record of a notification it intends to act on.
|
|
1137
|
+
|
|
1138
|
+
``fingerprint`` is the dedup key — server enforces ``UNIQUE(fingerprint)``
|
|
1139
|
+
and returns 409 Conflict on replay. ``event_id`` should match the upstream
|
|
1140
|
+
``notification.id`` so the three audit layers stay joinable.
|
|
1141
|
+
"""
|
|
1142
|
+
|
|
1143
|
+
event_id: uuid.UUID
|
|
1144
|
+
event_type: str = Field(min_length=1, max_length=64)
|
|
1145
|
+
source: InboundEventSource = InboundEventSource.polling
|
|
1146
|
+
fingerprint: str = Field(min_length=1, max_length=128)
|
|
1147
|
+
payload: dict | None = None
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
class InboundEventRead(ORMModel):
|
|
1151
|
+
id: uuid.UUID
|
|
1152
|
+
agent_id: uuid.UUID
|
|
1153
|
+
event_id: uuid.UUID
|
|
1154
|
+
event_type: str
|
|
1155
|
+
source: InboundEventSource
|
|
1156
|
+
fingerprint: str
|
|
1157
|
+
payload: dict | None
|
|
1158
|
+
received_at: datetime
|
|
1159
|
+
acked_at: datetime | None
|
|
1160
|
+
rejection_count: int = 0
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
class InboundEventRecordResult(BaseModel):
|
|
1164
|
+
"""Return shape for ``POST /me/inbound-events``.
|
|
1165
|
+
|
|
1166
|
+
``status="recorded"`` → first time, waker may proceed.
|
|
1167
|
+
``status="duplicate"`` → fingerprint already existed; treat as already woken
|
|
1168
|
+
(Phase 1 server gate; see plan D6).
|
|
1169
|
+
``status="rejected_v1"`` → legacy v1 fingerprint (``inbound:<event_id>``);
|
|
1170
|
+
inbound_event row is persisted (or upserted) with ``rejection_count`` bumped
|
|
1171
|
+
so the audit table still records the sighting, but the waker MUST NOT
|
|
1172
|
+
resume — see plan I2 / M30A acceptance #4.
|
|
1173
|
+
"""
|
|
1174
|
+
|
|
1175
|
+
status: Literal["recorded", "duplicate", "rejected_v1"]
|
|
1176
|
+
event: InboundEventRead
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
# --- Webhook ---
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
class WebhookCreate(BaseModel):
|
|
1183
|
+
url: str = Field(min_length=1, max_length=2048)
|
|
1184
|
+
events: list[str] = Field(default_factory=list)
|
|
1185
|
+
project_id: uuid.UUID | None = None
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
class WebhookUpdate(BaseModel):
|
|
1189
|
+
url: str | None = Field(default=None, min_length=1, max_length=2048)
|
|
1190
|
+
events: list[str] | None = None
|
|
1191
|
+
active: bool | None = None
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
class WebhookRead(ORMModel):
|
|
1195
|
+
id: uuid.UUID
|
|
1196
|
+
project_id: uuid.UUID | None
|
|
1197
|
+
url: str
|
|
1198
|
+
events: list[str]
|
|
1199
|
+
active: bool
|
|
1200
|
+
created_at: datetime
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
class WebhookCreateResponse(WebhookRead):
|
|
1204
|
+
secret: str
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
class WebhookDeliveryRead(ORMModel):
|
|
1208
|
+
id: uuid.UUID
|
|
1209
|
+
webhook_id: uuid.UUID
|
|
1210
|
+
event: str
|
|
1211
|
+
payload: dict
|
|
1212
|
+
status_code: int | None
|
|
1213
|
+
attempts: int
|
|
1214
|
+
success: bool
|
|
1215
|
+
last_attempt_at: datetime | None
|
|
1216
|
+
last_error: str | None
|
|
1217
|
+
created_at: datetime
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
# --- Audit ---
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
class AuditLogRead(ORMModel):
|
|
1224
|
+
id: uuid.UUID
|
|
1225
|
+
agent_id: uuid.UUID | None
|
|
1226
|
+
project_id: uuid.UUID | None
|
|
1227
|
+
action: str
|
|
1228
|
+
target_type: str
|
|
1229
|
+
target_id: uuid.UUID | None
|
|
1230
|
+
summary: str | None
|
|
1231
|
+
payload_json: dict | None
|
|
1232
|
+
created_at: datetime
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
# 0db51e10 I2(5e): request body for ``POST /experiments/{id}/cross-persona-call``.
|
|
1236
|
+
# Persisted in audit_logs.payload_json alongside caller_agent_id /
|
|
1237
|
+
# target_experiment_id / timestamp (created_at column).
|
|
1238
|
+
class CrossPersonaCallRecord(BaseModel):
|
|
1239
|
+
visibility_diff: dict[str, Any] = Field(default_factory=dict)
|
|
1240
|
+
result_partition_count: int = Field(default=0, ge=0)
|
|
1241
|
+
diff_size: int = Field(default=0, ge=0)
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
# --- Action Item audit payloads (B I2: waker escalation 三段式) ---
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
ACTION_ITEM_WAKE_SENT = "action_item.wake_sent"
|
|
1248
|
+
ACTION_ITEM_STALE = "action_item.stale"
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
class ActionItemWakeSentPayload(BaseModel):
|
|
1252
|
+
"""Payload of ``action_item.wake_sent`` audit event.
|
|
1253
|
+
|
|
1254
|
+
Fired by ``runtime-waker.scan_pending_action_items`` whenever ``should_wake_action_item``
|
|
1255
|
+
decides to wake the assignee (T+24h / T+72h / every 7d up to 4 times). Pairs with the
|
|
1256
|
+
``action_item.stale`` event but is a distinct, lower-severity audit signal.
|
|
1257
|
+
"""
|
|
1258
|
+
|
|
1259
|
+
action_item_id: uuid.UUID
|
|
1260
|
+
owner_agent_id: uuid.UUID
|
|
1261
|
+
topic_id: uuid.UUID
|
|
1262
|
+
decision_id: uuid.UUID | None = None
|
|
1263
|
+
linked_experiment_id: uuid.UUID | None = None
|
|
1264
|
+
wake_count: int
|
|
1265
|
+
last_woken_at: datetime
|
|
1266
|
+
first_open_at: datetime
|
|
1267
|
+
elapsed_since_first_open_seconds: int
|
|
1268
|
+
triggered_by: str = "waker.scan_pending_action_items"
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
class ActionItemStalePayload(BaseModel):
|
|
1272
|
+
"""Payload of ``action_item.stale`` audit event.
|
|
1273
|
+
|
|
1274
|
+
Fired after the 4th unanswered wake at the next 7d boundary. Pairs with admin
|
|
1275
|
+
notification + creator audit-only mark; the runtime-waker stops waking the
|
|
1276
|
+
assignee once ``stale_at`` is set. The event itself is independent of any
|
|
1277
|
+
state transition (unlike ``action_item.completed`` / ``action_item.cancelled``):
|
|
1278
|
+
it is a diagnostic signal that the open item has not progressed.
|
|
1279
|
+
"""
|
|
1280
|
+
|
|
1281
|
+
action_item_id: uuid.UUID
|
|
1282
|
+
owner_agent_id: uuid.UUID
|
|
1283
|
+
topic_id: uuid.UUID
|
|
1284
|
+
decision_id: uuid.UUID | None = None
|
|
1285
|
+
linked_experiment_id: uuid.UUID | None = None
|
|
1286
|
+
last_woken_at: datetime | None
|
|
1287
|
+
wake_count: int
|
|
1288
|
+
stale_after_attempt: int
|
|
1289
|
+
admin_notified: bool
|
|
1290
|
+
creator_audit_only: bool
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
# --- Platform Feedback ---
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
class PlatformFeedbackCreate(BaseModel):
|
|
1297
|
+
"""A free-text feedback entry any authenticated agent may submit.
|
|
1298
|
+
|
|
1299
|
+
`project_id` is an optional source-context hint (the project the agent was
|
|
1300
|
+
working in); it is NOT an access boundary — feedback is platform-wide.
|
|
1301
|
+
"""
|
|
1302
|
+
|
|
1303
|
+
body: str = Field(min_length=1, max_length=20000)
|
|
1304
|
+
project_id: uuid.UUID | None = None
|
|
1305
|
+
category: FeedbackCategory | None = None
|
|
1306
|
+
metadata: dict | None = None
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
class PlatformFeedbackUpdate(BaseModel):
|
|
1310
|
+
"""Admin-only triage fields."""
|
|
1311
|
+
|
|
1312
|
+
status: FeedbackStatus | None = None
|
|
1313
|
+
category: FeedbackCategory | None = None
|
|
1314
|
+
archived: bool | None = None
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
class PlatformFeedbackRead(ORMModel):
|
|
1318
|
+
id: uuid.UUID
|
|
1319
|
+
author_agent_id: uuid.UUID
|
|
1320
|
+
author_name: str | None = None
|
|
1321
|
+
project_id: uuid.UUID | None
|
|
1322
|
+
body: str
|
|
1323
|
+
category: FeedbackCategory | None
|
|
1324
|
+
status: FeedbackStatus
|
|
1325
|
+
metadata_json: dict | None
|
|
1326
|
+
created_at: datetime
|
|
1327
|
+
updated_at: datetime
|
|
1328
|
+
archived_at: datetime | None
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
# --- STATE_MACHINE.* escalation contact (experiment 156172e9 I1(c)) ---
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
class EscalationTargetRead(ORMModel):
|
|
1335
|
+
"""Resolution of a STATE_MACHINE.* error's escalation contact.
|
|
1336
|
+
|
|
1337
|
+
Returned by ``GET /api/v1/agents/me/escalation-target?experiment_id=...``.
|
|
1338
|
+
The CLI uses this on ``MAPHTTPError`` with a STATE_MACHINE.* error_code
|
|
1339
|
+
so the user knows who to ping about a state-machine refusal.
|
|
1340
|
+
"""
|
|
1341
|
+
|
|
1342
|
+
experiment_id: uuid.UUID | None
|
|
1343
|
+
escalation_target_id: uuid.UUID | None
|
|
1344
|
+
escalation_label: str # "@name" or "<no escalation contact>" placeholder
|
|
1345
|
+
tier: str # "experiment_override" | "current_caller" | "same_role_active" | "admin" | "none"
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
ProjectStatusRead.model_rebuild()
|
|
1349
|
+
CommentTreeNode.model_rebuild()
|
|
1350
|
+
TopicRead.model_rebuild()
|
|
1351
|
+
TopicCommentTreeNode.model_rebuild()
|