fuli-context 0.7.4-beta.3 → 0.7.6

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 (130) hide show
  1. package/README.md +77 -6
  2. package/README.zh-CN.md +69 -6
  3. package/compose.graphiti.yml +6 -6
  4. package/dist/web/assets/AboutPage-sLV9WChi.js +1 -0
  5. package/dist/web/assets/ConnectionsPage-BWn4ywGJ.js +1 -0
  6. package/dist/web/assets/KnowledgeEditDialog-CZ5ziPrD.js +1 -0
  7. package/dist/web/assets/KnowledgeInspector-CRQWOaos.js +1 -0
  8. package/dist/web/assets/KnowledgeOrganizerPage-Dfj3vIpU.js +1 -0
  9. package/dist/web/assets/KnowledgePage-BRYC9S5Y.js +1 -0
  10. package/dist/web/assets/KnowledgeWorkspace-ChEB5Bwk.js +4 -0
  11. package/dist/web/assets/OverviewPage-Cd66M0RT.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-RCFHL0Wz.js +1 -0
  13. package/dist/web/assets/PersonalProjectsPage-DEh8XWby.js +1 -0
  14. package/dist/web/assets/ProjectAgentsPage-Crh7PrXa.css +1 -0
  15. package/dist/web/assets/ProjectAgentsPage-DPnaH-TR.js +11 -0
  16. package/dist/web/assets/PublicProjectsPage-mstPEjHe.js +1 -0
  17. package/dist/web/assets/ReviewPage-6Y1SmfZy.js +1 -0
  18. package/dist/web/assets/SearchableSelect-D3HM8YF8.js +1 -0
  19. package/dist/web/assets/SettingsPage--LdTMUm7.js +1 -0
  20. package/dist/web/assets/SettingsPage-CwZp2ka4.css +1 -0
  21. package/dist/web/assets/WritingTastePage-CIpKswvs.js +1 -0
  22. package/dist/web/assets/{console-D_2zC4lm.js → console-BT1P2Db4.js} +1 -1
  23. package/dist/web/assets/{conversation-launcher-settings-DQmMH2j7.js → conversation-launcher-settings-DrJEotdf.js} +1 -1
  24. package/dist/web/assets/i18n-js6gfc7j.js +9 -0
  25. package/dist/web/assets/{identity-DmGcY3Y6.js → identity-CfDfanpa.js} +1 -1
  26. package/dist/web/assets/index-BnH5mcZx.css +1 -0
  27. package/dist/web/assets/index-GqJf1xqg.js +2 -0
  28. package/dist/web/assets/{model-CVoOdRsL.js → model-DJkLAJsg.js} +1 -1
  29. package/dist/web/assets/{useApi-CROJJdhE-CBNg4CdH.js → useApi-CROJJdhE-BdSti7Y2.js} +1 -1
  30. package/dist/web/assets/vue-router-Kk67EaCP.js +1 -0
  31. package/dist/web/index.html +6 -6
  32. package/graph-provider/fuli_graph/app.py +8 -0
  33. package/graph-provider/fuli_graph/collaboration_context.py +69 -6
  34. package/graph-provider/fuli_graph/knowledge_search.py +84 -5
  35. package/graph-provider/fuli_graph/model_validation.py +130 -0
  36. package/graph-provider/fuli_graph/models.py +24 -5
  37. package/graph-provider/fuli_graph/project_agent_access.py +54 -0
  38. package/graph-provider/fuli_graph/project_agent_coordination_models.py +22 -0
  39. package/graph-provider/fuli_graph/project_agent_executor_models.py +635 -0
  40. package/graph-provider/fuli_graph/project_agent_models.py +278 -0
  41. package/graph-provider/fuli_graph/project_agent_routes.py +626 -0
  42. package/graph-provider/fuli_graph/project_agent_task_models.py +545 -0
  43. package/graph-provider/fuli_graph/runtime.py +259 -0
  44. package/graph-provider/fuli_graph/search_projection.py +3 -0
  45. package/graph-provider/fuli_graph/store.py +6 -0
  46. package/graph-provider/fuli_graph/store_knowledge.py +62 -9
  47. package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +122 -0
  48. package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +756 -0
  49. package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +601 -0
  50. package/graph-provider/fuli_graph/store_project_agent_executors.py +1087 -0
  51. package/graph-provider/fuli_graph/store_project_agent_task_reads.py +458 -0
  52. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +705 -0
  53. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +586 -0
  54. package/graph-provider/fuli_graph/store_project_agent_tasks.py +1151 -0
  55. package/graph-provider/fuli_graph/store_project_agents.py +965 -0
  56. package/graph-provider/pyproject.toml +2 -1
  57. package/npm-shrinkwrap.json +2 -2
  58. package/package.json +1 -1
  59. package/skills/capturing-session-knowledge/SKILL.md +39 -0
  60. package/src/adaptive-runtime/executor-pool.js +127 -0
  61. package/src/adaptive-runtime/graph-services.js +166 -0
  62. package/src/adaptive-runtime/lease-client.js +335 -0
  63. package/src/adaptive-runtime/runtime-broker.js +318 -0
  64. package/src/adaptive-runtime/settings.js +111 -0
  65. package/src/adaptive-runtime/state.js +74 -0
  66. package/src/agent-tools/graph-definitions.js +419 -0
  67. package/src/agent-tools/graph-handlers.js +65 -0
  68. package/src/agent-tools/project-agent-definitions.js +307 -0
  69. package/src/backup/graph-backup.js +289 -0
  70. package/src/backup/identity-reconciliation.js +208 -0
  71. package/src/backup/runtime-adapters.js +269 -0
  72. package/src/cli/command-registry.js +6 -2
  73. package/src/cli/graph-data-command.js +112 -0
  74. package/src/cli/local-runtime-command.js +16 -2
  75. package/src/cli/setup-command.js +19 -3
  76. package/src/cli/update-command.js +4 -0
  77. package/src/cli.js +6 -0
  78. package/src/graphiti/collaboration-preference-projection.js +29 -1
  79. package/src/graphiti/collaboration-preference-workflow.js +121 -0
  80. package/src/graphiti/federated-application.js +53 -90
  81. package/src/graphiti/project-agent-control-plane.js +275 -0
  82. package/src/graphiti/project-agent-mapping.js +1024 -0
  83. package/src/graphiti/project-agent-workflows.js +576 -0
  84. package/src/graphiti/provider-client.js +282 -1
  85. package/src/http/api-router.js +63 -9
  86. package/src/http/graph-api-router.js +290 -0
  87. package/src/local-runtime/lifecycle.js +46 -10
  88. package/src/mcp/context-resources.js +15 -6
  89. package/src/mcp/create-mcp-server.js +51 -16
  90. package/src/mcp/instructions.js +7 -5
  91. package/src/mcp/runtime.js +17 -5
  92. package/src/mcp/session-id.js +38 -1
  93. package/src/mcp/tool-annotations.js +71 -0
  94. package/src/mcp-server.js +23 -6
  95. package/src/native-runtime/runtime.js +717 -0
  96. package/src/server.js +6 -2
  97. package/src/setup/agent-installation-status.js +7 -1
  98. package/src/setup/agents.js +3 -1
  99. package/src/setup/claude-code-config.js +3 -1
  100. package/src/setup/codex-bootstrap.js +11 -10
  101. package/src/setup/codex-config.js +7 -1
  102. package/src/setup/cursor-config.js +3 -1
  103. package/src/setup/graph-runtime.js +158 -13
  104. package/src/setup/neo4j-memory-profile.js +43 -0
  105. package/src/setup/options.js +38 -2
  106. package/src/setup/paths.js +14 -0
  107. package/src/setup/setup.js +37 -1
  108. package/src/system/resource-monitor.js +181 -16
  109. package/src/system/runtime-settings.js +10 -1
  110. package/src/system/system-service.js +27 -2
  111. package/dist/web/assets/AboutPage-Dpf5fRWv.js +0 -1
  112. package/dist/web/assets/ConnectionsPage-D21aUDCP.js +0 -1
  113. package/dist/web/assets/KnowledgeEditDialog-u7lpfN3j.js +0 -1
  114. package/dist/web/assets/KnowledgeInspector-k4xnHtXd.js +0 -1
  115. package/dist/web/assets/KnowledgeOrganizerPage-Bdj7Rn84.js +0 -1
  116. package/dist/web/assets/KnowledgePage-UHVAbC1W.js +0 -1
  117. package/dist/web/assets/KnowledgeWorkspace-Lq3d-MeC.js +0 -4
  118. package/dist/web/assets/OverviewPage-CycAgA1V.js +0 -1
  119. package/dist/web/assets/PersonalProfilePage-U9oIV5FT.js +0 -1
  120. package/dist/web/assets/PersonalProjectsPage-C5RpXKcA.js +0 -1
  121. package/dist/web/assets/PublicProjectsPage-DiWxm35E.js +0 -1
  122. package/dist/web/assets/ReviewPage-jxRfGSJK.js +0 -1
  123. package/dist/web/assets/SearchableSelect-B3-5IPup.js +0 -1
  124. package/dist/web/assets/SettingsPage-BqfGDOho.js +0 -1
  125. package/dist/web/assets/SettingsPage-CAfz4qiV.css +0 -1
  126. package/dist/web/assets/WritingTastePage-BJxhMqJR.js +0 -1
  127. package/dist/web/assets/i18n-CwqXDI1C.js +0 -5
  128. package/dist/web/assets/index-IUGLt4lG.js +0 -2
  129. package/dist/web/assets/index-X2j29j8i.css +0 -1
  130. package/dist/web/assets/vue-router-CWSsUUio.js +0 -1
@@ -0,0 +1,635 @@
1
+ """Durable executor-directory and routing contracts for Project Agents.
2
+
3
+ The executor layer deliberately stores capabilities and observed availability,
4
+ not a hard-coded mapping from a work kind to a tool or vendor. A coordinator
5
+ can only select an executor after the directory, workspace permission,
6
+ preflight, capability, and (when required) health checks all pass.
7
+ """
8
+
9
+ import hashlib
10
+ import json
11
+ from datetime import datetime
12
+ from typing import Literal
13
+
14
+ from pydantic import Field, field_validator, model_validator
15
+
16
+ from .model_validation import reject_credentials
17
+ from .models import SourceApplication, StrictModel
18
+ from .project_agent_models import (
19
+ ProjectAgentExecutorPolicy,
20
+ ProjectAgentModelStrategy,
21
+ )
22
+
23
+
24
+ ProjectAgentExecutorRegistrationStatus = Literal[
25
+ 'registered',
26
+ 'disabled',
27
+ 'revoked',
28
+ ]
29
+ ProjectAgentExecutorPermissionStatus = Literal[
30
+ 'pending',
31
+ 'authorized',
32
+ 'denied',
33
+ 'revoked',
34
+ ]
35
+ ProjectAgentExecutorPreflightStatus = Literal[
36
+ 'not_run',
37
+ 'passed',
38
+ 'failed',
39
+ 'expired',
40
+ ]
41
+ ProjectAgentExecutorHealthStatus = Literal[
42
+ 'unknown',
43
+ 'healthy',
44
+ 'degraded',
45
+ 'unhealthy',
46
+ ]
47
+ ProjectAgentExecutorScope = Literal['global', 'space', 'project', 'task']
48
+ ProjectAgentExecutorRuleStatus = Literal['active', 'disabled', 'ended']
49
+ ProjectAgentExecutorSelectionStatus = Literal[
50
+ 'selected',
51
+ 'fallback',
52
+ 'blocked',
53
+ 'unavailable',
54
+ ]
55
+ ProjectAgentExecutorFallbackOutcome = Literal[
56
+ 'not_needed',
57
+ 'same_rule_candidate',
58
+ 'global_priority',
59
+ 'blocked_locked',
60
+ 'blocked_no_candidate',
61
+ ]
62
+ ProjectAgentExecutorEvidenceKind = Literal[
63
+ 'terminal_outcome',
64
+ 'rework_requested',
65
+ 'repeated_negative_feedback',
66
+ 'explicit_praise',
67
+ 'test_passed',
68
+ 'test_failed',
69
+ 'acceptance_passed',
70
+ 'acceptance_failed',
71
+ 'explicit_rating',
72
+ ]
73
+ ProjectAgentExecutorEvidenceSource = Literal[
74
+ 'system_terminal',
75
+ 'user_explicit',
76
+ 'test_fact',
77
+ ]
78
+ ProjectAgentExecutorTerminalOutcome = Literal[
79
+ 'completed',
80
+ 'failed',
81
+ 'cancelled',
82
+ ]
83
+
84
+
85
+ def project_agent_model_strategy_key(
86
+ strategy: ProjectAgentModelStrategy,
87
+ ) -> str:
88
+ """Return the canonical, provider-neutral key for a model strategy."""
89
+
90
+ payload = strategy.model_dump(mode='json')
91
+ # Capability hints are a set of requirements, not an ordered preference.
92
+ # Canonicalizing them prevents equivalent provider-neutral strategies from
93
+ # creating separate evidence/aggregate buckets.
94
+ payload['capability_hints'] = sorted(
95
+ (value.casefold() for value in payload.get('capability_hints', [])),
96
+ )
97
+ encoded = json.dumps(
98
+ payload,
99
+ ensure_ascii=False,
100
+ sort_keys=True,
101
+ separators=(',', ':'),
102
+ ).encode('utf-8')
103
+ return hashlib.sha256(encoded).hexdigest()
104
+
105
+
106
+ def _unique_text(value: list[str], label: str, *, maximum: int = 512) -> list[str]:
107
+ normalized = [item.strip() for item in value]
108
+ if any(not item or len(item) > maximum for item in normalized):
109
+ raise ValueError(f'{label} must contain 1 to {maximum} characters')
110
+ if len({item.casefold() for item in normalized}) != len(normalized):
111
+ raise ValueError(f'{label} must be unique')
112
+ return normalized
113
+
114
+
115
+ def _unique_ids(value: list[str], label: str = 'IDs') -> list[str]:
116
+ return _unique_text(value, label, maximum=128)
117
+
118
+
119
+ class ProjectAgentExecutorModelRecord(StrictModel):
120
+ """A model actually advertised by an executor preflight.
121
+
122
+ ``provider`` and ``model`` are observations from the executor. Routing
123
+ continues to use the provider-neutral ``ProjectAgentModelStrategy`` and
124
+ these fields are only selected after a preflight reports them available.
125
+ """
126
+
127
+ provider: str = Field(min_length=1, max_length=128)
128
+ model: str = Field(min_length=1, max_length=256)
129
+ capabilities: list[str] = Field(default_factory=list, max_length=32)
130
+ strategy_modes: list[Literal['adaptive', 'fast', 'balanced', 'deep']] = Field(
131
+ default_factory=list,
132
+ max_length=4,
133
+ )
134
+ reasoning_efforts: list[Literal['default', 'low', 'medium', 'high']] = Field(
135
+ default_factory=list,
136
+ max_length=4,
137
+ )
138
+ available: bool = True
139
+ observed_at: datetime | None = None
140
+ source_application: SourceApplication | None = None
141
+ unavailable_reason: str | None = Field(default=None, min_length=1, max_length=1024)
142
+
143
+ @field_validator('capabilities')
144
+ @classmethod
145
+ def validate_capabilities(cls, value: list[str]) -> list[str]:
146
+ return _unique_text(value, 'executor model capabilities')
147
+
148
+ @model_validator(mode='after')
149
+ def validate_availability(self):
150
+ if self.available and self.unavailable_reason:
151
+ raise ValueError('available model cannot have an unavailable reason')
152
+ if not self.available and not self.unavailable_reason:
153
+ raise ValueError('unavailable model requires a reason')
154
+ reject_credentials(self, 'executor model report')
155
+ return self
156
+
157
+
158
+ class ProjectAgentExecutorRegistration(StrictModel):
159
+ personal_space_id: str = Field(min_length=1, max_length=128)
160
+ executor_id: str = Field(min_length=1, max_length=128)
161
+ display_name: str = Field(min_length=1, max_length=160)
162
+ executor_kind: str = Field(default='external', min_length=1, max_length=128)
163
+ capabilities: list[str] = Field(default_factory=list, max_length=64)
164
+ advertised_models: list[ProjectAgentExecutorModelRecord] = Field(
165
+ default_factory=list,
166
+ max_length=64,
167
+ )
168
+ global_priority: int = Field(default=100, ge=1, le=1_000_000)
169
+ health_required: bool = False
170
+ idempotency_key: str = Field(min_length=8, max_length=256)
171
+ source_application: SourceApplication | None = None
172
+ source_session_id: str | None = Field(default=None, min_length=1, max_length=256)
173
+ test_source: str | None = Field(default=None, min_length=1, max_length=256)
174
+ cleanup_eligible: bool = False
175
+ expected_revision: int | None = Field(default=None, ge=0)
176
+
177
+ @field_validator('capabilities')
178
+ @classmethod
179
+ def validate_capabilities(cls, value: list[str]) -> list[str]:
180
+ return _unique_text(value, 'executor capabilities')
181
+
182
+ @model_validator(mode='after')
183
+ def reject_sensitive_values(self):
184
+ reject_credentials(self, 'executor registration')
185
+ return self
186
+
187
+
188
+ class ProjectAgentExecutorAuthorization(StrictModel):
189
+ personal_space_id: str = Field(min_length=1, max_length=128)
190
+ executor_id: str = Field(min_length=1, max_length=128)
191
+ status: ProjectAgentExecutorPermissionStatus
192
+ reason: str = Field(min_length=1, max_length=2048)
193
+ expected_revision: int | None = Field(default=None, ge=0)
194
+ idempotency_key: str = Field(min_length=8, max_length=256)
195
+
196
+ @model_validator(mode='after')
197
+ def reject_sensitive_values(self):
198
+ reject_credentials(self, 'executor workspace authorization')
199
+ return self
200
+
201
+
202
+ class ProjectAgentExecutorPreflightReport(StrictModel):
203
+ personal_space_id: str = Field(min_length=1, max_length=128)
204
+ executor_id: str = Field(min_length=1, max_length=128)
205
+ status: Literal['passed', 'failed']
206
+ workspace_permission: bool
207
+ capabilities: list[str] = Field(default_factory=list, max_length=64)
208
+ available_models: list[ProjectAgentExecutorModelRecord] = Field(
209
+ default_factory=list,
210
+ max_length=64,
211
+ )
212
+ reason: str | None = Field(default=None, min_length=1, max_length=2048)
213
+ checked_at: datetime
214
+ idempotency_key: str = Field(min_length=8, max_length=256)
215
+ source_application: SourceApplication | None = None
216
+ source_session_id: str | None = Field(default=None, min_length=1, max_length=256)
217
+
218
+ @field_validator('capabilities')
219
+ @classmethod
220
+ def validate_capabilities(cls, value: list[str]) -> list[str]:
221
+ return _unique_text(value, 'preflight capabilities')
222
+
223
+ @model_validator(mode='after')
224
+ def validate_report(self):
225
+ if self.status == 'passed':
226
+ if not self.workspace_permission:
227
+ raise ValueError('passed preflight requires workspace permission')
228
+ if not any(model.available for model in self.available_models):
229
+ raise ValueError('passed preflight requires an available model')
230
+ if self.status == 'failed' and not self.reason:
231
+ raise ValueError('failed preflight requires a reason')
232
+ reject_credentials(self, 'executor preflight report')
233
+ return self
234
+
235
+
236
+ class ProjectAgentExecutorHealthReport(StrictModel):
237
+ personal_space_id: str = Field(min_length=1, max_length=128)
238
+ executor_id: str = Field(min_length=1, max_length=128)
239
+ status: ProjectAgentExecutorHealthStatus
240
+ reason: str | None = Field(default=None, min_length=1, max_length=2048)
241
+ checked_at: datetime
242
+ idempotency_key: str = Field(min_length=8, max_length=256)
243
+ source_application: SourceApplication | None = None
244
+ source_session_id: str | None = Field(default=None, min_length=1, max_length=256)
245
+
246
+ @model_validator(mode='after')
247
+ def validate_report(self):
248
+ if self.status in {'degraded', 'unhealthy'} and not self.reason:
249
+ raise ValueError('degraded or unhealthy executor requires a reason')
250
+ reject_credentials(self, 'executor health report')
251
+ return self
252
+
253
+
254
+ class ProjectAgentExecutorRecord(StrictModel):
255
+ executor_id: str
256
+ display_name: str
257
+ executor_kind: str
258
+ registration_status: ProjectAgentExecutorRegistrationStatus
259
+ permission_status: ProjectAgentExecutorPermissionStatus
260
+ preflight_status: ProjectAgentExecutorPreflightStatus
261
+ health_status: ProjectAgentExecutorHealthStatus
262
+ health_required: bool = False
263
+ workspace_permission: bool = False
264
+ capabilities: list[str] = Field(default_factory=list)
265
+ available_models: list[ProjectAgentExecutorModelRecord] = Field(
266
+ default_factory=list,
267
+ )
268
+ global_priority: int = Field(ge=1)
269
+ revision: int = Field(default=0, ge=0)
270
+ permission_revision: int = Field(default=0, ge=0)
271
+ preflight_at: datetime | None = None
272
+ health_checked_at: datetime | None = None
273
+ registered_at: datetime
274
+ updated_at: datetime
275
+ test_source: str | None = None
276
+ cleanup_eligible: bool = False
277
+
278
+
279
+ class ProjectAgentExecutorPriorityUpdate(StrictModel):
280
+ executor_id: str = Field(min_length=1, max_length=128)
281
+ global_priority: int = Field(ge=1, le=1_000_000)
282
+ expected_revision: int = Field(ge=0)
283
+ idempotency_key: str = Field(min_length=8, max_length=256)
284
+ reason: str = Field(min_length=1, max_length=2048)
285
+
286
+ @model_validator(mode='after')
287
+ def reject_sensitive_values(self):
288
+ reject_credentials(self, 'executor priority update')
289
+ return self
290
+
291
+
292
+ class ProjectAgentExecutorRoutingRuleCreate(StrictModel):
293
+ scope: ProjectAgentExecutorScope
294
+ personal_space_id: str | None = Field(default=None, min_length=1, max_length=128)
295
+ personal_project_id: str | None = Field(default=None, min_length=1, max_length=128)
296
+ task_id: str | None = Field(default=None, min_length=1, max_length=128)
297
+ work_kind: str = Field(min_length=1, max_length=128)
298
+ required_capabilities: list[str] = Field(default_factory=list, max_length=32)
299
+ executor_ids: list[str] = Field(min_length=1, max_length=32)
300
+ model_strategy: ProjectAgentModelStrategy | None = None
301
+ priority: int = Field(default=100, ge=1, le=1_000_000)
302
+ reason: str = Field(min_length=1, max_length=2048)
303
+ idempotency_key: str = Field(min_length=8, max_length=256)
304
+
305
+ @field_validator('required_capabilities')
306
+ @classmethod
307
+ def validate_capabilities(cls, value: list[str]) -> list[str]:
308
+ return _unique_text(value, 'routing rule capabilities')
309
+
310
+ @field_validator('executor_ids')
311
+ @classmethod
312
+ def validate_executor_ids(cls, value: list[str]) -> list[str]:
313
+ return _unique_ids(value, 'routing rule executor IDs')
314
+
315
+ @model_validator(mode='after')
316
+ def validate_scope(self):
317
+ expected = {
318
+ 'global': (False, False, False),
319
+ 'space': (True, False, False),
320
+ 'project': (True, True, False),
321
+ 'task': (True, True, True),
322
+ }[self.scope]
323
+ actual = (
324
+ bool(self.personal_space_id),
325
+ bool(self.personal_project_id),
326
+ bool(self.task_id),
327
+ )
328
+ if actual != expected:
329
+ raise ValueError(
330
+ f'{self.scope} routing rule requires exactly the matching scope IDs'
331
+ )
332
+ reject_credentials(self, 'executor routing rule')
333
+ return self
334
+
335
+
336
+ class ProjectAgentExecutorRoutingRuleRecord(
337
+ ProjectAgentExecutorRoutingRuleCreate
338
+ ):
339
+ rule_id: str
340
+ status: ProjectAgentExecutorRuleStatus = 'active'
341
+ revision: int = Field(default=0, ge=0)
342
+ created_at: datetime
343
+ updated_at: datetime
344
+
345
+
346
+ class ProjectAgentExecutorRoutingRuleUpdate(StrictModel):
347
+ personal_space_id: str = Field(min_length=1, max_length=128)
348
+ rule_id: str = Field(min_length=1, max_length=128)
349
+ expected_revision: int = Field(ge=0)
350
+ status: Literal['active', 'disabled', 'ended']
351
+ reason: str = Field(min_length=1, max_length=2048)
352
+ idempotency_key: str = Field(min_length=8, max_length=256)
353
+
354
+ @model_validator(mode='after')
355
+ def reject_sensitive_values(self):
356
+ reject_credentials(self, 'executor routing rule update')
357
+ return self
358
+
359
+
360
+ class ProjectAgentExecutorSelectionRequest(StrictModel):
361
+ personal_space_id: str = Field(min_length=1, max_length=128)
362
+ personal_project_id: str = Field(min_length=1, max_length=128)
363
+ task_id: str = Field(min_length=1, max_length=128)
364
+ agent_id: str = Field(min_length=1, max_length=128)
365
+ work_kind: str = Field(min_length=1, max_length=128)
366
+ required_capabilities: list[str] = Field(default_factory=list, max_length=32)
367
+ model_strategy: ProjectAgentModelStrategy = Field(
368
+ default_factory=ProjectAgentModelStrategy
369
+ )
370
+ executor_policy: ProjectAgentExecutorPolicy | None = None
371
+ idempotency_key: str = Field(min_length=8, max_length=256)
372
+ source_application: SourceApplication | None = None
373
+ source_session_id: str | None = Field(default=None, min_length=1, max_length=256)
374
+
375
+ @field_validator('required_capabilities')
376
+ @classmethod
377
+ def validate_capabilities(cls, value: list[str]) -> list[str]:
378
+ return _unique_text(value, 'required executor capabilities')
379
+
380
+ @model_validator(mode='after')
381
+ def reject_sensitive_values(self):
382
+ reject_credentials(self, 'executor selection request')
383
+ return self
384
+
385
+
386
+ class ProjectAgentExecutorSelection(StrictModel):
387
+ selection_id: str
388
+ task_id: str
389
+ personal_space_id: str
390
+ personal_project_id: str
391
+ agent_id: str
392
+ status: ProjectAgentExecutorSelectionStatus
393
+ outcome: ProjectAgentExecutorSelectionStatus
394
+ selected_executor_id: str | None = None
395
+ # ``executor_id`` is a compatibility alias consumed by the task router.
396
+ executor_id: str | None = None
397
+ selected_provider: str | None = None
398
+ selected_model: str | None = None
399
+ matched_rule_id: str | None = None
400
+ rule_id: str | None = None
401
+ matched_rule_scope: ProjectAgentExecutorScope | None = None
402
+ candidate_executor_ids: list[str] = Field(default_factory=list)
403
+ selection_reason: str
404
+ reason: str
405
+ fallback_outcome: ProjectAgentExecutorFallbackOutcome = 'not_needed'
406
+ fallback_from_executor_id: str | None = None
407
+ fallback_reason: str | None = None
408
+ blocked_reason: str | None = None
409
+ model_strategy: ProjectAgentModelStrategy
410
+ model_strategy_key: str = Field(min_length=64, max_length=64)
411
+ model_strategy_source: Literal[
412
+ 'task',
413
+ 'assignment',
414
+ 'agent',
415
+ 'routing_rule',
416
+ 'coordinator',
417
+ ] = 'agent'
418
+ executor_policy: ProjectAgentExecutorPolicy = Field(
419
+ default_factory=ProjectAgentExecutorPolicy
420
+ )
421
+ idempotency_key: str | None = Field(default=None, min_length=8, max_length=256)
422
+ payload_fingerprint: str | None = Field(default=None, min_length=16, max_length=128)
423
+ created_at: datetime
424
+
425
+ @model_validator(mode='after')
426
+ def validate_strategy_key(self):
427
+ if self.model_strategy_key != project_agent_model_strategy_key(
428
+ self.model_strategy
429
+ ):
430
+ raise ValueError('model strategy key does not match model strategy')
431
+ return self
432
+
433
+
434
+ class ProjectAgentExecutorActualReport(StrictModel):
435
+ personal_space_id: str = Field(min_length=1, max_length=128)
436
+ personal_project_id: str = Field(min_length=1, max_length=128)
437
+ task_id: str = Field(min_length=1, max_length=128)
438
+ run_id: str = Field(min_length=1, max_length=128)
439
+ agent_id: str = Field(min_length=1, max_length=128)
440
+ executor_id: str = Field(min_length=1, max_length=128)
441
+ provider: str = Field(min_length=1, max_length=128)
442
+ model: str = Field(min_length=1, max_length=256)
443
+ model_strategy: ProjectAgentModelStrategy = Field(
444
+ default_factory=ProjectAgentModelStrategy
445
+ )
446
+ model_strategy_source: Literal[
447
+ 'task',
448
+ 'assignment',
449
+ 'agent',
450
+ 'routing_rule',
451
+ 'coordinator',
452
+ ] = 'agent'
453
+ matched_rule_id: str | None = Field(default=None, min_length=1, max_length=128)
454
+ fallback_reason: str | None = Field(default=None, min_length=1, max_length=2048)
455
+ idempotency_key: str = Field(min_length=8, max_length=256)
456
+ occurred_at: datetime
457
+ source_application: SourceApplication | None = None
458
+ source_session_id: str | None = Field(default=None, min_length=1, max_length=256)
459
+
460
+ @model_validator(mode='after')
461
+ def reject_sensitive_values(self):
462
+ reject_credentials(self, 'actual executor report')
463
+ return self
464
+
465
+
466
+ class ProjectAgentExecutorOutcomeEvidenceCreate(StrictModel):
467
+ personal_space_id: str = Field(min_length=1, max_length=128)
468
+ personal_project_id: str = Field(min_length=1, max_length=128)
469
+ work_kind: str = Field(min_length=1, max_length=128)
470
+ agent_id: str = Field(min_length=1, max_length=128)
471
+ executor_id: str = Field(min_length=1, max_length=128)
472
+ task_id: str = Field(min_length=1, max_length=128)
473
+ run_id: str | None = Field(default=None, min_length=1, max_length=128)
474
+ model_strategy: ProjectAgentModelStrategy = Field(
475
+ default_factory=ProjectAgentModelStrategy
476
+ )
477
+ model_strategy_key: str | None = Field(
478
+ default=None,
479
+ min_length=64,
480
+ max_length=64,
481
+ )
482
+ evidence_kind: ProjectAgentExecutorEvidenceKind
483
+ source: ProjectAgentExecutorEvidenceSource
484
+ terminal_outcome: ProjectAgentExecutorTerminalOutcome | None = None
485
+ rating: int | None = Field(default=None, ge=1, le=5)
486
+ reference_ids: list[str] = Field(default_factory=list, max_length=16)
487
+ note: str | None = Field(default=None, min_length=1, max_length=2048)
488
+ idempotency_key: str = Field(min_length=8, max_length=256)
489
+ occurred_at: datetime
490
+
491
+ @field_validator('reference_ids')
492
+ @classmethod
493
+ def validate_reference_ids(cls, value: list[str]) -> list[str]:
494
+ return _unique_ids(value, 'evidence reference IDs')
495
+
496
+ @model_validator(mode='after')
497
+ def validate_evidence(self):
498
+ expected_strategy_key = project_agent_model_strategy_key(self.model_strategy)
499
+ if self.model_strategy_key and self.model_strategy_key != expected_strategy_key:
500
+ raise ValueError('model strategy key does not match model strategy')
501
+ system_kinds = {'terminal_outcome'}
502
+ test_kinds = {'test_passed', 'test_failed'}
503
+ explicit_kinds = {
504
+ 'rework_requested',
505
+ 'repeated_negative_feedback',
506
+ 'explicit_praise',
507
+ 'acceptance_passed',
508
+ 'acceptance_failed',
509
+ 'explicit_rating',
510
+ }
511
+ if self.evidence_kind in system_kinds and self.source != 'system_terminal':
512
+ raise ValueError('terminal outcome evidence must come from the system')
513
+ if self.evidence_kind in test_kinds and self.source != 'test_fact':
514
+ raise ValueError('test evidence must be recorded as a test fact')
515
+ if self.evidence_kind in explicit_kinds and self.source != 'user_explicit':
516
+ raise ValueError('feedback evidence must be explicitly supplied by a user')
517
+ if self.evidence_kind == 'terminal_outcome' and not self.terminal_outcome:
518
+ raise ValueError('terminal outcome evidence requires a terminal outcome')
519
+ if self.evidence_kind in system_kinds | test_kinds and not self.reference_ids:
520
+ raise ValueError('system or test evidence requires an evidence reference')
521
+ if self.evidence_kind != 'terminal_outcome' and self.terminal_outcome:
522
+ raise ValueError('terminal outcome is only valid for terminal evidence')
523
+ if self.evidence_kind == 'explicit_rating' and self.rating is None:
524
+ raise ValueError('explicit rating evidence requires a rating')
525
+ if self.evidence_kind != 'explicit_rating' and self.rating is not None:
526
+ raise ValueError('rating is only valid for explicit rating evidence')
527
+ if self.evidence_kind in explicit_kinds and not self.reference_ids and not self.note:
528
+ raise ValueError('explicit feedback requires a reference or note')
529
+ reject_credentials(self, 'executor outcome evidence')
530
+ return self
531
+
532
+
533
+ class ProjectAgentExecutorOutcomeEvidenceRecord(
534
+ ProjectAgentExecutorOutcomeEvidenceCreate
535
+ ):
536
+ # Stored evidence always exposes the canonical bucket key. The create
537
+ # contract keeps this optional for backwards-compatible callers; the store
538
+ # computes it before returning a durable record.
539
+ model_strategy_key: str = Field(min_length=64, max_length=64)
540
+ evidence_id: str
541
+ ignored: bool = False
542
+ ignored_reason: str | None = None
543
+ created_at: datetime
544
+
545
+
546
+ class ProjectAgentExecutorEvidenceContribution(StrictModel):
547
+ evidence_id: str
548
+ evidence_kind: ProjectAgentExecutorEvidenceKind
549
+ signal: Literal['success', 'failure', 'rework', 'rating', 'neutral']
550
+ value: float
551
+ decay_weight: float = Field(ge=0, le=1)
552
+ occurred_at: datetime
553
+ reference_ids: list[str] = Field(default_factory=list)
554
+
555
+
556
+ class ProjectAgentExecutorOutcomeAggregate(StrictModel):
557
+ personal_space_id: str
558
+ personal_project_id: str
559
+ work_kind: str
560
+ agent_id: str
561
+ executor_id: str
562
+ model_strategy: ProjectAgentModelStrategy
563
+ model_strategy_key: str = Field(min_length=64, max_length=64)
564
+ sample_count: int = Field(ge=0)
565
+ recent_count: int = Field(ge=0)
566
+ success_count: int = Field(ge=0)
567
+ rework_count: int = Field(ge=0)
568
+ failure_count: int = Field(ge=0)
569
+ rating_count: int = Field(ge=0)
570
+ average_rating: float | None = Field(default=None, ge=1, le=5)
571
+ neutral_due_to_insufficient_evidence: bool = False
572
+ ignored: bool = False
573
+ reset_at: datetime | None = None
574
+ as_of: datetime
575
+ decay_half_life_days: float = Field(gt=0)
576
+ weighted_success: float = Field(ge=0)
577
+ weighted_failure: float = Field(ge=0)
578
+ evidence_refs: list[str] = Field(default_factory=list)
579
+ evidence_contributions: list[ProjectAgentExecutorEvidenceContribution] = Field(
580
+ default_factory=list
581
+ )
582
+
583
+ @model_validator(mode='after')
584
+ def validate_strategy_key(self):
585
+ if self.model_strategy_key != project_agent_model_strategy_key(
586
+ self.model_strategy
587
+ ):
588
+ raise ValueError('model strategy key does not match model strategy')
589
+ return self
590
+
591
+
592
+ class ProjectAgentExecutorEvidenceIgnore(StrictModel):
593
+ personal_space_id: str = Field(min_length=1, max_length=128)
594
+ personal_project_id: str = Field(min_length=1, max_length=128)
595
+ agent_id: str = Field(min_length=1, max_length=128)
596
+ evidence_id: str = Field(min_length=1, max_length=128)
597
+ reason: str = Field(min_length=1, max_length=2048)
598
+ idempotency_key: str = Field(min_length=8, max_length=256)
599
+
600
+ @model_validator(mode='after')
601
+ def reject_sensitive_values(self):
602
+ reject_credentials(self, 'executor evidence ignore')
603
+ return self
604
+
605
+
606
+ class ProjectAgentExecutorOutcomeReset(StrictModel):
607
+ personal_space_id: str = Field(min_length=1, max_length=128)
608
+ personal_project_id: str = Field(min_length=1, max_length=128)
609
+ work_kind: str = Field(min_length=1, max_length=128)
610
+ agent_id: str = Field(min_length=1, max_length=128)
611
+ executor_id: str = Field(min_length=1, max_length=128)
612
+ model_strategy: ProjectAgentModelStrategy = Field(
613
+ default_factory=ProjectAgentModelStrategy
614
+ )
615
+ model_strategy_key: str | None = Field(
616
+ default=None,
617
+ min_length=64,
618
+ max_length=64,
619
+ )
620
+ reason: str = Field(min_length=1, max_length=2048)
621
+ idempotency_key: str = Field(min_length=8, max_length=256)
622
+ reset_at: datetime
623
+
624
+ @model_validator(mode='after')
625
+ def reject_sensitive_values(self):
626
+ expected_strategy_key = project_agent_model_strategy_key(self.model_strategy)
627
+ if self.model_strategy_key and self.model_strategy_key != expected_strategy_key:
628
+ raise ValueError('model strategy key does not match model strategy')
629
+ reject_credentials(self, 'executor evidence reset')
630
+ return self
631
+
632
+
633
+ # Names used by the task store and by older integration code.
634
+ ProjectAgentExecutorDecision = ProjectAgentExecutorSelection
635
+ ProjectAgentExecutorRouteResult = ProjectAgentExecutorSelection