fuli-context 0.7.6 → 0.7.7

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 (146) hide show
  1. package/README.md +69 -13
  2. package/README.zh-CN.md +60 -14
  3. package/dist/web/assets/{AboutPage-sLV9WChi.js → AboutPage-BcZeFcHe.js} +1 -1
  4. package/dist/web/assets/{ConnectionsPage-BWn4ywGJ.js → ConnectionsPage-xAQYl1SQ.js} +1 -1
  5. package/dist/web/assets/EmployeeRecruitDialog-CbPAmhy7.js +1 -0
  6. package/dist/web/assets/EmployeeRecruitDialog-CfFlyKSh.css +1 -0
  7. package/dist/web/assets/EmployeeWorkbenchPage-BQvTLoc8.css +1 -0
  8. package/dist/web/assets/EmployeeWorkbenchPage-DF3EL0Ui.js +1 -0
  9. package/dist/web/assets/KnowledgeEditDialog-UnnBDt0I.js +1 -0
  10. package/dist/web/assets/{KnowledgeEditDialog-DWUV_NPp.css → KnowledgeEditDialog-iv-dgUeD.css} +1 -1
  11. package/dist/web/assets/{KnowledgeInspector-CRQWOaos.js → KnowledgeInspector-BqPJH5Rr.js} +1 -1
  12. package/dist/web/assets/{KnowledgeOrganizerPage-Dfj3vIpU.js → KnowledgeOrganizerPage-CyvRflXM.js} +1 -1
  13. package/dist/web/assets/KnowledgePage-59eczTqe.js +1 -0
  14. package/dist/web/assets/{KnowledgeWorkspace-ChEB5Bwk.js → KnowledgeWorkspace-CfIjhOog.js} +1 -1
  15. package/dist/web/assets/{OverviewPage-Cd66M0RT.js → OverviewPage-Xyfm5xkZ.js} +1 -1
  16. package/dist/web/assets/{PersonalProfilePage-RCFHL0Wz.js → PersonalProfilePage-DB9mByB3.js} +1 -1
  17. package/dist/web/assets/PersonalProjectsPage-_-YXVi5O.js +1 -0
  18. package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +1 -0
  19. package/dist/web/assets/ProjectAgentsPage-DeeQByPX.js +11 -0
  20. package/dist/web/assets/{PublicProjectsPage-mstPEjHe.js → PublicProjectsPage-BEZ1YPs3.js} +1 -1
  21. package/dist/web/assets/{ReviewPage-6Y1SmfZy.js → ReviewPage-CQYGwFBH.js} +1 -1
  22. package/dist/web/assets/SearchableSelect-D27H0MoP.js +1 -0
  23. package/dist/web/assets/{SettingsPage--LdTMUm7.js → SettingsPage-BlEZ03Yi.js} +1 -1
  24. package/dist/web/assets/{WritingTastePage-CIpKswvs.js → WritingTastePage-DvRt_nmP.js} +1 -1
  25. package/dist/web/assets/catalog-DFvIaHVS.js +1 -0
  26. package/dist/web/assets/{console-BT1P2Db4.js → console-tOxOl8Vq.js} +1 -1
  27. package/dist/web/assets/conversation-launcher-settings-DuFSpPAo.js +1 -0
  28. package/dist/web/assets/{i18n-js6gfc7j.js → i18n-CgBHZthI.js} +3 -3
  29. package/dist/web/assets/{identity-CfDfanpa.js → identity-CFo_Uw1H.js} +1 -1
  30. package/dist/web/assets/index-CJc5ppXH.css +1 -0
  31. package/dist/web/assets/index-yCi3cz2l.js +2 -0
  32. package/dist/web/assets/{model-DJkLAJsg.js → model-BtnVr7fX.js} +1 -1
  33. package/dist/web/assets/{useApi-CROJJdhE-BdSti7Y2.js → useApi-CROJJdhE-B6MHm_Av.js} +1 -1
  34. package/dist/web/assets/{vue-router-Kk67EaCP.js → vue-router-CzHm-y-x.js} +1 -1
  35. package/dist/web/index.html +7 -6
  36. package/docs/employee-agents.md +99 -0
  37. package/docs/project-agent-memory.md +479 -0
  38. package/graph-provider/fuli_graph/knowledge_audit_models.py +171 -0
  39. package/graph-provider/fuli_graph/model_base.py +5 -0
  40. package/graph-provider/fuli_graph/model_validation.py +32 -0
  41. package/graph-provider/fuli_graph/models.py +26 -170
  42. package/graph-provider/fuli_graph/project_agent_context_models.py +29 -0
  43. package/graph-provider/fuli_graph/project_agent_executor_models.py +35 -7
  44. package/graph-provider/fuli_graph/project_agent_memory_models.py +79 -0
  45. package/graph-provider/fuli_graph/project_agent_memory_routes.py +46 -0
  46. package/graph-provider/fuli_graph/project_agent_models.py +9 -0
  47. package/graph-provider/fuli_graph/project_agent_recruitment_values.py +48 -0
  48. package/graph-provider/fuli_graph/project_agent_routes.py +21 -1
  49. package/graph-provider/fuli_graph/project_agent_task_models.py +217 -29
  50. package/graph-provider/fuli_graph/runtime.py +37 -1
  51. package/graph-provider/fuli_graph/store.py +6 -0
  52. package/graph-provider/fuli_graph/store_project_agent_context.py +65 -0
  53. package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +175 -11
  54. package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +33 -10
  55. package/graph-provider/fuli_graph/store_project_agent_executors.py +181 -14
  56. package/graph-provider/fuli_graph/store_project_agent_memory.py +174 -0
  57. package/graph-provider/fuli_graph/store_project_agent_task_activity.py +423 -0
  58. package/graph-provider/fuli_graph/store_project_agent_task_persistence.py +152 -0
  59. package/graph-provider/fuli_graph/store_project_agent_task_reads.py +187 -21
  60. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +748 -162
  61. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +230 -18
  62. package/graph-provider/fuli_graph/store_project_agent_tasks.py +261 -468
  63. package/graph-provider/fuli_graph/store_project_agents.py +214 -46
  64. package/graph-provider/fuli_graph/store_task_contexts.py +217 -0
  65. package/graph-provider/fuli_graph/store_transactions.py +40 -0
  66. package/graph-provider/fuli_graph/task_context_models.py +50 -0
  67. package/graph-provider/fuli_graph/task_context_routes.py +31 -0
  68. package/graph-provider/fuli_graph/workflow_observation_models.py +1 -1
  69. package/graph-provider/pyproject.toml +1 -1
  70. package/npm-shrinkwrap.json +8 -8
  71. package/package.json +5 -2
  72. package/skills/capturing-session-knowledge/SKILL.md +18 -6
  73. package/src/adaptive-runtime/executor-pool.js +46 -5
  74. package/src/adaptive-runtime/graph-services.js +10 -3
  75. package/src/adaptive-runtime/lease-client.js +66 -14
  76. package/src/adaptive-runtime/runtime-broker.js +24 -9
  77. package/src/agent-tools/employee-definitions.js +43 -0
  78. package/src/agent-tools/graph-definitions.js +25 -6
  79. package/src/agent-tools/graph-handlers.js +11 -0
  80. package/src/agent-tools/project-agent-definitions.js +35 -4
  81. package/src/agent-tools/project-agent-memory-definitions.js +36 -0
  82. package/src/agent-tools.js +6 -2
  83. package/src/agents/codex/lifecycle-hook.js +107 -0
  84. package/src/agents/codex/lifecycle-hooks.js +231 -0
  85. package/src/agents/cursor/lifecycle-hook.js +133 -0
  86. package/src/agents/cursor/lifecycle-hooks.js +50 -0
  87. package/src/agents/lifecycle-message.js +19 -0
  88. package/src/app/agent-request-context.js +23 -0
  89. package/src/cli/command-registry.js +2 -0
  90. package/src/cli/employee-command.js +17 -0
  91. package/src/cli/remote-mcp-command.js +282 -0
  92. package/src/cli/setup-command.js +5 -0
  93. package/src/cli.js +10 -0
  94. package/src/employees/catalog/jefa.json +21 -0
  95. package/src/employees/install-package.js +77 -0
  96. package/src/employees/management-policy.js +88 -0
  97. package/src/employees/manifest.js +53 -0
  98. package/src/employees/package-registry.js +107 -0
  99. package/src/employees/project-selection.js +50 -0
  100. package/src/employees/recruitment.js +199 -0
  101. package/src/employees/service.js +83 -0
  102. package/src/employees/task-entry.js +39 -0
  103. package/src/graphiti/agent-knowledge-workflows.js +135 -25
  104. package/src/graphiti/federated-application.js +78 -35
  105. package/src/graphiti/project-agent-control-plane.js +20 -3
  106. package/src/graphiti/project-agent-mapping.js +155 -5
  107. package/src/graphiti/project-agent-memory.js +115 -0
  108. package/src/graphiti/project-agent-task-entry.js +126 -0
  109. package/src/graphiti/project-agent-workflows.js +30 -14
  110. package/src/graphiti/provider-client.js +66 -4
  111. package/src/http/api-router.js +25 -5
  112. package/src/http/employee-api-router.js +64 -0
  113. package/src/http/error-mapping.js +4 -0
  114. package/src/http/graph-api-router.js +2 -0
  115. package/src/http/request-policy.js +6 -3
  116. package/src/http/static-handler.js +4 -3
  117. package/src/mcp/create-mcp-server.js +98 -37
  118. package/src/mcp/direct-call.js +38 -0
  119. package/src/mcp/instructions.js +8 -7
  120. package/src/mcp/provider-task-context-registry.js +74 -0
  121. package/src/mcp/remote-http-server.js +761 -0
  122. package/src/mcp/session-id.js +29 -3
  123. package/src/mcp/task-context-registry.js +88 -29
  124. package/src/mcp/tool-annotations.js +10 -2
  125. package/src/mcp/tool-result.js +8 -3
  126. package/src/mcp-server.js +20 -8
  127. package/src/native-runtime/runtime.js +58 -8
  128. package/src/server.js +76 -28
  129. package/src/setup/agent-installation-status.js +38 -6
  130. package/src/setup/agents.js +20 -8
  131. package/src/setup/claude-code-config.js +55 -13
  132. package/src/setup/codex-bootstrap.js +11 -10
  133. package/src/setup/codex-config.js +74 -7
  134. package/src/setup/config-backup.js +18 -2
  135. package/src/setup/cursor-config.js +22 -5
  136. package/src/setup/setup.js +30 -22
  137. package/src/storage/json-file.js +11 -3
  138. package/dist/web/assets/KnowledgeEditDialog-CZ5ziPrD.js +0 -1
  139. package/dist/web/assets/KnowledgePage-BRYC9S5Y.js +0 -1
  140. package/dist/web/assets/PersonalProjectsPage-DEh8XWby.js +0 -1
  141. package/dist/web/assets/ProjectAgentsPage-Crh7PrXa.css +0 -1
  142. package/dist/web/assets/ProjectAgentsPage-DPnaH-TR.js +0 -11
  143. package/dist/web/assets/SearchableSelect-D3HM8YF8.js +0 -1
  144. package/dist/web/assets/conversation-launcher-settings-DrJEotdf.js +0 -1
  145. package/dist/web/assets/index-BnH5mcZx.css +0 -1
  146. package/dist/web/assets/index-GqJf1xqg.js +0 -2
@@ -1,25 +1,15 @@
1
1
  import json
2
2
  from collections import defaultdict
3
3
  from datetime import date
4
-
5
4
  from fastapi import HTTPException
6
-
7
5
  from .personal_project_access import authorize_personal_project
8
6
  from .project_agent_access import authorize_project_agent
9
- from .project_agent_executor_models import (
10
- ProjectAgentExecutorActualReport,
11
- ProjectAgentExecutorOutcomeEvidenceCreate,
12
- )
13
- from .project_agent_models import (
14
- ProjectAgentModelStrategy,
15
- ProjectAgentProfile,
16
- )
7
+ from .project_agent_models import ProjectAgentModelStrategy, ProjectAgentProfile
17
8
  from .project_agent_task_models import (
18
9
  ProjectAgentActivityDay,
19
10
  ProjectAgentActivityResult,
20
11
  ProjectAgentActivityTask,
21
12
  ProjectAgentRoutingDecisionRecord,
22
- ProjectAgentTaskActivityCreate,
23
13
  ProjectAgentTaskRecord,
24
14
  ProjectAgentTaskRouteResult,
25
15
  ProjectAgentTaskSubmit,
@@ -27,35 +17,32 @@ from .project_agent_task_models import (
27
17
  from .provider_values import native_datetime, now_utc, stable_uuid
28
18
  from .store_project_agents import SYSTEM_COORDINATOR_AGENT_ID
29
19
  from .store_project_agent_task_reads import StoreProjectAgentTaskReads
20
+ from .store_project_agent_task_persistence import StoreProjectAgentTaskPersistence
30
21
  from .store_project_agent_task_recruitment import StoreProjectAgentTaskRecruitment
31
22
  from .store_project_agent_task_staffing import StoreProjectAgentTaskStaffing
32
- from .store_project_agent_coordination_policy import (
33
- StoreProjectAgentCoordinationPolicy,
34
- )
35
-
36
-
37
- TERMINAL_TASK_STATUSES = {'completed', 'failed', 'cancelled'}
38
-
39
-
23
+ from .store_project_agent_coordination_policy import StoreProjectAgentCoordinationPolicy
24
+ from .store_project_agent_task_activity import StoreProjectAgentTaskActivity, TERMINAL_TASK_STATUSES
25
+ from .store_transactions import TransactionQueryDriver, query_store_transaction
40
26
  class StoreProjectAgentTasks(
27
+ StoreProjectAgentTaskActivity,
41
28
  StoreProjectAgentCoordinationPolicy,
42
29
  StoreProjectAgentTaskRecruitment,
43
30
  StoreProjectAgentTaskReads,
44
- # Keep the pre-existing base order ahead of the extracted implementation.
31
+ StoreProjectAgentTaskPersistence,
45
32
  StoreProjectAgentTaskStaffing,
46
33
  ):
47
34
  """Durable task, routing, recruitment, and verified activity control plane.
48
35
 
49
- This is deliberately not a worker or scheduler. Registered clients submit
50
- tasks and append truthful state events to the same persistent Agent/task
51
- history. No task is shown as running until a client records that event.
36
+ Clients append real state events; this control plane does not run workers.
52
37
  """
53
-
54
38
  async def submit_project_agent_task(
55
39
  self,
56
40
  actor: dict,
57
41
  request: ProjectAgentTaskSubmit,
58
42
  ) -> ProjectAgentTaskRouteResult:
43
+ if not isinstance(self.runtime.driver, TransactionQueryDriver):
44
+ async with query_store_transaction(self) as scoped:
45
+ return await scoped.submit_project_agent_task(actor, request)
59
46
  self._require_personal()
60
47
  space = await self.authorize(actor, request.personal_space_id, 'maintainer')
61
48
  await authorize_personal_project(
@@ -79,11 +66,32 @@ class StoreProjectAgentTasks(
79
66
  status_code=409,
80
67
  detail='task idempotency key was used with different input',
81
68
  )
69
+ await self._repair_task_persistence(request, raw)
70
+ existing = await self._find_task_row(
71
+ request.personal_space_id,
72
+ task_id,
73
+ )
82
74
  task = self._task_from_row(existing)
83
- recruitment = await self._task_recruitment(
75
+ recruitments = await self._task_recruitments(
84
76
  request.personal_space_id,
85
77
  task.task_id,
86
78
  )
79
+ if await self._recover_recruitment_lifecycle(
80
+ actor,
81
+ request,
82
+ task,
83
+ recruitments,
84
+ ):
85
+ existing = await self._find_task_row(
86
+ request.personal_space_id,
87
+ task_id,
88
+ )
89
+ task = self._task_from_row(existing)
90
+ recruitments = await self._task_recruitments(
91
+ request.personal_space_id,
92
+ task.task_id,
93
+ )
94
+ recruitment = recruitments[0] if recruitments else None
87
95
  assigned_agent = await self._route_assigned_agent(
88
96
  actor,
89
97
  request,
@@ -93,13 +101,9 @@ class StoreProjectAgentTasks(
93
101
  task,
94
102
  recruitment,
95
103
  assigned_agent=assigned_agent,
104
+ recruitments=recruitments,
96
105
  )
97
106
 
98
- coordinator = await self.ensure_system_project_coordinator(
99
- actor,
100
- request.personal_space_id,
101
- )
102
- coordinator_agent_id = request.coordinator_agent_id or coordinator.agent_id
103
107
  if request.coordinator_agent_id:
104
108
  await authorize_project_agent(
105
109
  self,
@@ -162,12 +166,53 @@ class StoreProjectAgentTasks(
162
166
  'qualified durable Agent candidates'
163
167
  )
164
168
 
169
+ parallel_recruitment_slots = (
170
+ await self._automatic_parallel_recruitment_slots(
171
+ actor,
172
+ request,
173
+ lead=lead,
174
+ participants=participants,
175
+ routing_reason=reason,
176
+ )
177
+ )
178
+ projected_participants = await self._parallel_plan_participant_projection(
179
+ actor,
180
+ request,
181
+ lead=lead,
182
+ participants=participants,
183
+ routing_reason=reason,
184
+ recruitment_slots=parallel_recruitment_slots,
185
+ )
186
+ self._verify_parallel_plan(request.parallel_plan, projected_participants)
187
+
188
+ will_open_recruitment = (
189
+ request.staffing_intent != 'unassigned'
190
+ and reason != 'manual_agent_selection'
191
+ and (
192
+ bool(parallel_recruitment_slots) or not lead
193
+ or request.staffing_intent in {'new_durable', 'temporary'}
194
+ )
195
+ )
196
+ if will_open_recruitment:
197
+ _, recruitment_profile = self._normalized_recruitment_profile(request)
198
+ self._require_agent_client_allowed(
199
+ recruitment_profile,
200
+ request.source_application,
201
+ )
202
+
203
+ coordinator = await self.ensure_system_project_coordinator(
204
+ actor,
205
+ request.personal_space_id,
206
+ )
207
+ coordinator_agent_id = request.coordinator_agent_id or coordinator.agent_id
208
+
165
209
  model_strategy, model_source = self._effective_model_strategy(
166
210
  request,
167
211
  lead,
168
212
  coordinator.profile.default_model_strategy,
169
213
  )
170
214
  recruitment = None
215
+ recruitments = []
171
216
  routing_outcome = 'assigned_existing' if lead else 'unassigned'
172
217
  task_status = 'queued' if lead else 'blocked'
173
218
  if request.staffing_intent == 'unassigned':
@@ -177,6 +222,72 @@ class StoreProjectAgentTasks(
177
222
  elif reason == 'manual_agent_selection':
178
223
  routing_outcome = 'unassigned'
179
224
  task_status = 'blocked'
225
+ elif parallel_recruitment_slots:
226
+ recruited_count = 0
227
+ recruited_lead = False
228
+ for slot in parallel_recruitment_slots:
229
+ slot_recruitment, recruited = await self._open_recruitment(
230
+ actor,
231
+ space,
232
+ request,
233
+ task_id,
234
+ coordinator_agent_id,
235
+ reason,
236
+ participant_role=slot['participant_role'],
237
+ recruitment_slot=slot['recruitment_slot'],
238
+ )
239
+ if slot_recruitment:
240
+ recruitments.append(slot_recruitment)
241
+ if slot['participant_role'] == 'lead':
242
+ recruitment = slot_recruitment
243
+ if not recruited:
244
+ continue
245
+
246
+ recruited_count += 1
247
+ participant = {
248
+ 'agent_id': recruited['agent_id'],
249
+ 'role': slot['participant_role'],
250
+ 'assignment_summary': recruited.get('responsibility'),
251
+ }
252
+ if not any(
253
+ item['agent_id'] == recruited['agent_id']
254
+ for item in participants
255
+ ):
256
+ if slot['participant_role'] == 'lead':
257
+ participants.insert(0, participant)
258
+ else:
259
+ participants.append(participant)
260
+ if slot['participant_role'] == 'lead':
261
+ lead = recruited
262
+ recruited_lead = True
263
+
264
+ if recruitment is None and recruitments:
265
+ recruitment = recruitments[0]
266
+ if recruited_count == len(parallel_recruitment_slots):
267
+ routing_outcome = 'recruited'
268
+ task_status = 'queued'
269
+ match_basis = [
270
+ *match_basis,
271
+ 'parallel automatic recruitment: filled '
272
+ f'{recruited_count} missing participant slot(s)',
273
+ 'HR recruitment recorded before Agent creation',
274
+ ]
275
+ if recruited_lead:
276
+ model_strategy, model_source = self._effective_model_strategy(
277
+ request,
278
+ lead,
279
+ coordinator.profile.default_model_strategy,
280
+ )
281
+ elif any(
282
+ item.status == 'awaiting_confirmation'
283
+ for item in recruitments
284
+ ):
285
+ routing_outcome = 'recruitment_required'
286
+ task_status = 'awaiting_recruitment'
287
+ else:
288
+ routing_outcome = 'blocked'
289
+ task_status = 'blocked'
290
+ reason = 'hr_unavailable'
180
291
  elif not lead or request.staffing_intent in {'new_durable', 'temporary'}:
181
292
  recruitment, recruited = await self._open_recruitment(
182
293
  actor,
@@ -186,6 +297,8 @@ class StoreProjectAgentTasks(
186
297
  coordinator_agent_id,
187
298
  reason,
188
299
  )
300
+ if recruitment:
301
+ recruitments.append(recruitment)
189
302
  if recruited:
190
303
  lead = recruited
191
304
  if not any(
@@ -290,6 +403,7 @@ class StoreProjectAgentTasks(
290
403
  participants=participants,
291
404
  decision=decision,
292
405
  recruitment=recruitment,
406
+ recruitments=recruitments,
293
407
  executor_decision=executor_decision,
294
408
  created_at=now,
295
409
  )
@@ -307,6 +421,7 @@ class StoreProjectAgentTasks(
307
421
  task,
308
422
  recruitment,
309
423
  assigned_agent=assigned_agent,
424
+ recruitments=recruitments,
310
425
  )
311
426
 
312
427
  async def get_project_agent_task(
@@ -314,19 +429,33 @@ class StoreProjectAgentTasks(
314
429
  actor: dict,
315
430
  personal_space_id: str,
316
431
  task_id: str,
432
+ *,
433
+ personal_project_id: str | None = None,
317
434
  ) -> ProjectAgentTaskRecord:
318
435
  self._require_personal()
319
436
  space = await self.authorize(actor, personal_space_id, 'reader')
320
- row = await self._find_task_row(personal_space_id, task_id)
437
+ if personal_project_id:
438
+ await authorize_personal_project(
439
+ self,
440
+ actor,
441
+ space,
442
+ personal_project_id,
443
+ )
444
+ row = await self._find_task_row(
445
+ personal_space_id,
446
+ task_id,
447
+ personal_project_id,
448
+ )
321
449
  if not row:
322
450
  raise HTTPException(status_code=404, detail='project Agent task not found')
323
451
  task = dict(row['task'])
324
- await authorize_personal_project(
325
- self,
326
- actor,
327
- space,
328
- task['personal_project_id'],
329
- )
452
+ if not personal_project_id:
453
+ await authorize_personal_project(
454
+ self,
455
+ actor,
456
+ space,
457
+ task['personal_project_id'],
458
+ )
330
459
  return self._task_from_row(row)
331
460
 
332
461
  async def list_project_agent_tasks(
@@ -369,349 +498,6 @@ class StoreProjectAgentTasks(
369
498
  )
370
499
  return [self._task_from_row(row) for row in records]
371
500
 
372
- async def record_project_agent_task_activity(
373
- self,
374
- actor: dict,
375
- request: ProjectAgentTaskActivityCreate,
376
- ) -> ProjectAgentTaskRecord:
377
- self._require_personal()
378
- space = await self.authorize(actor, request.personal_space_id, 'maintainer')
379
- await authorize_personal_project(
380
- self,
381
- actor,
382
- space,
383
- request.personal_project_id,
384
- )
385
- row = await self._find_task_row(request.personal_space_id, request.task_id)
386
- if not row:
387
- raise HTTPException(status_code=404, detail='project Agent task not found')
388
- raw_task = dict(row['task'])
389
- if raw_task['personal_project_id'] != request.personal_project_id:
390
- raise HTTPException(status_code=404, detail='project Agent task not found')
391
- event_id = stable_uuid(
392
- self.settings.provider_id,
393
- request.personal_space_id,
394
- 'project-agent-task-event',
395
- request.task_id,
396
- request.idempotency_key,
397
- )
398
- payload_hash = self._payload_hash(request)
399
- existing_event = next(
400
- (
401
- dict(value)
402
- for value in (row.get('event_rows') or [])
403
- if value
404
- and (value.get('event_id') or value.get('id')) == event_id
405
- ),
406
- None,
407
- )
408
- if existing_event:
409
- if existing_event.get('payload_hash') != payload_hash:
410
- raise HTTPException(
411
- status_code=409,
412
- detail=(
413
- 'task event idempotency key was used with different input'
414
- ),
415
- )
416
- return self._task_from_row(row)
417
- self._validate_task_transition(
418
- raw_task.get('status'),
419
- request.status,
420
- has_actual_executor=bool(request.actual_executor_id),
421
- )
422
- expected_revision = getattr(request, 'expected_revision', None)
423
- if expected_revision is None:
424
- expected_revision = raw_task.get('revision', 0)
425
- if request.agent_id:
426
- participant = next(
427
- (
428
- item for item in self._participant_maps(row)
429
- if item['agent_id'] == request.agent_id
430
- ),
431
- None,
432
- )
433
- if not participant:
434
- raise HTTPException(
435
- status_code=409,
436
- detail='Agent is not assigned to this task',
437
- )
438
- if participant.get('profile_json'):
439
- self._require_agent_client_allowed(
440
- {
441
- 'agent_id': request.agent_id,
442
- 'profile': ProjectAgentProfile.model_validate_json(
443
- participant['profile_json']
444
- ),
445
- },
446
- request.source_application,
447
- )
448
- now = now_utc()
449
- terminal_at = now if request.status in TERMINAL_TASK_STATUSES else None
450
- actual_executor_id = getattr(request, 'actual_executor_id', None)
451
- matched_executor_rule_id = getattr(request, 'matched_executor_rule_id', None)
452
- executor_selection_reason = getattr(request, 'executor_selection_reason', None)
453
- executor_fallback_reason = getattr(request, 'executor_fallback_reason', None)
454
- executor_blocked_reason = getattr(request, 'executor_blocked_reason', None)
455
- worker_id = getattr(request, 'worker_id', None)
456
- worker_label = getattr(request, 'worker_label', None)
457
- worker_occupation_emoji = getattr(
458
- request,
459
- 'worker_occupation_emoji',
460
- None,
461
- )
462
- worker_status = getattr(request, 'worker_status', None)
463
- actual_recorder = None
464
- actual_report = None
465
- if actual_executor_id:
466
- actual_recorder = getattr(
467
- self,
468
- 'record_project_agent_executor_actual',
469
- None,
470
- )
471
- if not callable(actual_recorder):
472
- raise HTTPException(
473
- status_code=503,
474
- detail='executor evidence control plane is unavailable',
475
- )
476
- strategy = (
477
- ProjectAgentModelStrategy.model_validate_json(
478
- raw_task['effective_model_strategy_json']
479
- )
480
- if raw_task.get('effective_model_strategy_json')
481
- else ProjectAgentModelStrategy()
482
- )
483
- actual_report = ProjectAgentExecutorActualReport(
484
- personal_space_id=request.personal_space_id,
485
- personal_project_id=request.personal_project_id,
486
- task_id=request.task_id,
487
- run_id=event_id,
488
- agent_id=request.agent_id,
489
- executor_id=actual_executor_id,
490
- provider=request.actual_model_provider,
491
- model=request.actual_model,
492
- model_strategy=strategy,
493
- model_strategy_source=(
494
- raw_task.get('model_strategy_source') or 'coordinator'
495
- ),
496
- matched_rule_id=matched_executor_rule_id,
497
- fallback_reason=executor_fallback_reason,
498
- idempotency_key=f'actual:{event_id}',
499
- occurred_at=now,
500
- source_application=request.source_application,
501
- source_session_id=request.source_session_id,
502
- )
503
- records, _, _ = await self.runtime.driver.execute_query(
504
- '''
505
- MATCH (space:FuliSpace {
506
- id: $personal_space_id, kind: 'personal'
507
- })-[:HAS_PROJECT_AGENT_TASK]->
508
- (task:FuliProjectAgentTask {
509
- personal_space_id: $personal_space_id,
510
- task_id: $task_id
511
- })
512
- WHERE task.revision = $expected_revision
513
- OR EXISTS {
514
- MATCH (task)-[:HAS_TASK_EVENT]->
515
- (:FuliProjectAgentTaskEvent {id: $event_id})
516
- }
517
- OPTIONAL MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->
518
- (agent:FuliProjectAgent {agent_id: $agent_id})
519
- WHERE $agent_id IS NULL OR EXISTS {
520
- MATCH (task)-[:HAS_PARTICIPANT]->(agent)
521
- }
522
- MERGE (event:FuliProjectAgentTaskEvent {id: $event_id})
523
- ON CREATE SET event.event_id = $event_id,
524
- event.task_id = $task_id,
525
- event.agent_id = $agent_id,
526
- event.status = $status,
527
- event.actor_kind = $actor_kind,
528
- event.summary = $summary,
529
- event.payload_hash = $payload_hash,
530
- event.source_application = $source_application,
531
- event.source_session_id = $source_session_id,
532
- event.actual_model_provider = $actual_model_provider,
533
- event.actual_model = $actual_model,
534
- event.actual_executor_id = $actual_executor_id,
535
- event.matched_executor_rule_id =
536
- $matched_executor_rule_id,
537
- event.executor_selection_reason =
538
- $executor_selection_reason,
539
- event.executor_fallback_reason =
540
- $executor_fallback_reason,
541
- event.executor_blocked_reason =
542
- $executor_blocked_reason,
543
- event.worker_id = $worker_id,
544
- event.worker_label = $worker_label,
545
- event.worker_occupation_emoji = $worker_occupation_emoji,
546
- event.worker_status = $worker_status,
547
- event.activity_date = CASE
548
- WHEN $terminal_at IS NULL THEN null
549
- ELSE date($terminal_at) END,
550
- event.created_at = $updated_at
551
- WITH task, event, agent,
552
- event.payload_hash = $payload_hash AS same_payload,
553
- task.revision = $expected_revision AS applied_transition
554
- FOREACH (_ IN CASE
555
- WHEN same_payload AND applied_transition
556
- THEN [1] ELSE [] END |
557
- MERGE (task)-[:HAS_TASK_EVENT]->(event)
558
- )
559
- FOREACH (_ IN CASE
560
- WHEN same_payload AND applied_transition AND agent IS NOT NULL
561
- THEN [1] ELSE [] END |
562
- MERGE (event)-[:EVENT_AGENT]->(agent)
563
- )
564
- FOREACH (_ IN CASE
565
- WHEN same_payload AND applied_transition
566
- THEN [1] ELSE [] END |
567
- SET task.status = $status,
568
- task.result_summary = CASE
569
- WHEN $status = 'completed' THEN $summary
570
- ELSE task.result_summary END,
571
- task.failure_reason = CASE
572
- WHEN $status = 'failed' THEN $summary
573
- ELSE task.failure_reason END,
574
- task.actual_executor_id = coalesce(
575
- $actual_executor_id, task.actual_executor_id),
576
- task.actual_model_provider = coalesce(
577
- $actual_model_provider, task.actual_model_provider),
578
- task.actual_model = coalesce($actual_model, task.actual_model),
579
- task.matched_executor_rule_id = coalesce(
580
- $matched_executor_rule_id,
581
- task.matched_executor_rule_id),
582
- task.executor_selection_reason = coalesce(
583
- $executor_selection_reason,
584
- task.executor_selection_reason),
585
- task.executor_fallback_reason = coalesce(
586
- $executor_fallback_reason,
587
- task.executor_fallback_reason),
588
- task.executor_blocked_reason = coalesce(
589
- $executor_blocked_reason,
590
- task.executor_blocked_reason),
591
- task.completed_at = $terminal_at,
592
- task.updated_at = $updated_at,
593
- task.revision = task.revision + 1
594
- )
595
- FOREACH (_ IN CASE
596
- WHEN same_payload AND applied_transition AND agent IS NOT NULL
597
- THEN [1] ELSE [] END |
598
- SET task.lead_agent_id = CASE
599
- WHEN task.lead_agent_id IS NULL AND $status = 'running'
600
- THEN agent.agent_id ELSE task.lead_agent_id END
601
- )
602
- WITH task, event, same_payload, applied_transition
603
- OPTIONAL MATCH (task)-[participant:HAS_PARTICIPANT]->
604
- (participant_agent:FuliProjectAgent)
605
- FOREACH (_ IN CASE
606
- WHEN same_payload
607
- AND applied_transition
608
- AND participant IS NOT NULL
609
- AND NOT (coalesce(participant.status, '') IN
610
- ['completed', 'failed', 'cancelled'])
611
- AND (
612
- $terminal_at IS NOT NULL
613
- OR $agent_id IS NULL
614
- OR participant_agent.agent_id = $agent_id
615
- )
616
- THEN [1] ELSE [] END |
617
- SET participant.status = $status,
618
- participant.updated_at = $updated_at,
619
- participant.ended_at = $terminal_at
620
- )
621
- RETURN DISTINCT task, event, same_payload, applied_transition
622
- ''',
623
- personal_space_id=request.personal_space_id,
624
- task_id=request.task_id,
625
- expected_revision=expected_revision,
626
- event_id=event_id,
627
- agent_id=request.agent_id,
628
- status=request.status,
629
- actor_kind=request.actor_kind,
630
- summary=request.summary,
631
- payload_hash=payload_hash,
632
- source_application=request.source_application,
633
- source_session_id=request.source_session_id,
634
- actual_model_provider=request.actual_model_provider,
635
- actual_model=request.actual_model,
636
- actual_executor_id=actual_executor_id,
637
- matched_executor_rule_id=matched_executor_rule_id,
638
- executor_selection_reason=executor_selection_reason,
639
- executor_fallback_reason=executor_fallback_reason,
640
- executor_blocked_reason=executor_blocked_reason,
641
- worker_id=worker_id,
642
- worker_label=worker_label,
643
- worker_occupation_emoji=worker_occupation_emoji,
644
- worker_status=worker_status,
645
- terminal_at=terminal_at,
646
- updated_at=now,
647
- )
648
- if not records:
649
- raise HTTPException(
650
- status_code=409,
651
- detail='task revision is stale or participant is invalid',
652
- )
653
- if not records[0]['same_payload']:
654
- raise HTTPException(
655
- status_code=409,
656
- detail='task event idempotency key was used with different input',
657
- )
658
- if not records[0].get('applied_transition'):
659
- return await self.get_project_agent_task(
660
- actor,
661
- request.personal_space_id,
662
- request.task_id,
663
- )
664
- if actual_report:
665
- await actual_recorder(actor, actual_report)
666
- evidence_status = (
667
- request.status
668
- if request.status in TERMINAL_TASK_STATUSES
669
- else worker_status
670
- )
671
- if (
672
- evidence_status in TERMINAL_TASK_STATUSES
673
- and actual_executor_id
674
- and request.agent_id
675
- ):
676
- evidence_recorder = getattr(
677
- self,
678
- 'record_project_agent_executor_outcome_evidence',
679
- None,
680
- )
681
- if callable(evidence_recorder):
682
- strategy = (
683
- ProjectAgentModelStrategy.model_validate_json(
684
- raw_task['effective_model_strategy_json']
685
- )
686
- if raw_task.get('effective_model_strategy_json')
687
- else ProjectAgentModelStrategy()
688
- )
689
- await evidence_recorder(
690
- actor,
691
- ProjectAgentExecutorOutcomeEvidenceCreate(
692
- personal_space_id=request.personal_space_id,
693
- personal_project_id=request.personal_project_id,
694
- work_kind=raw_task['work_kind'],
695
- agent_id=request.agent_id,
696
- executor_id=actual_executor_id,
697
- task_id=request.task_id,
698
- run_id=event_id,
699
- model_strategy=strategy,
700
- evidence_kind='terminal_outcome',
701
- source='system_terminal',
702
- terminal_outcome=evidence_status,
703
- reference_ids=[event_id],
704
- idempotency_key=f'terminal:{event_id}',
705
- occurred_at=now,
706
- ),
707
- )
708
- await self._archive_finished_temporary_agent(request, raw_task, now)
709
- return await self.get_project_agent_task(
710
- actor,
711
- request.personal_space_id,
712
- request.task_id,
713
- )
714
-
715
501
  async def get_project_agent_activity(
716
502
  self,
717
503
  actor: dict,
@@ -719,9 +505,18 @@ class StoreProjectAgentTasks(
719
505
  agent_id: str,
720
506
  from_date: date,
721
507
  to_date: date,
508
+ *,
509
+ personal_project_id: str | None = None,
722
510
  ) -> ProjectAgentActivityResult:
723
511
  self._require_personal()
724
- await self.authorize(actor, personal_space_id, 'reader')
512
+ space = await self.authorize(actor, personal_space_id, 'reader')
513
+ if personal_project_id:
514
+ await authorize_personal_project(
515
+ self,
516
+ actor,
517
+ space,
518
+ personal_project_id,
519
+ )
725
520
  if from_date > to_date:
726
521
  raise HTTPException(status_code=422, detail='from date must not exceed to date')
727
522
  records, _, _ = await self.runtime.driver.execute_query(
@@ -733,12 +528,18 @@ class StoreProjectAgentTasks(
733
528
  (task:FuliProjectAgentTask)-[:HAS_TASK_EVENT]->
734
529
  (event:FuliProjectAgentTaskEvent)-[:EVENT_AGENT]->(agent)
735
530
  WHERE event.status IN ['completed', 'failed', 'cancelled']
531
+ AND ($personal_project_id IS NULL
532
+ OR task.personal_project_id = $personal_project_id)
736
533
  AND event.activity_date >= date($from_date)
737
534
  AND event.activity_date <= date($to_date)
738
- RETURN event, task.title AS title
535
+ WITH task, event, agent
536
+ OPTIONAL MATCH (task)-[:HAS_TASK_EVENT]->(evidence_event:FuliProjectAgentTaskEvent)-[:EVENT_AGENT]->(agent)
537
+ WHERE evidence_event.created_at <= event.created_at
538
+ RETURN event, task.title AS title, collect(evidence_event) AS evidence_events
739
539
  ORDER BY event.created_at DESC, event.event_id
740
540
  ''',
741
541
  personal_space_id=personal_space_id,
542
+ personal_project_id=personal_project_id,
742
543
  agent_id=agent_id,
743
544
  from_date=from_date.isoformat(),
744
545
  to_date=to_date.isoformat(),
@@ -746,7 +547,9 @@ class StoreProjectAgentTasks(
746
547
  )
747
548
  grouped = defaultdict(list)
748
549
  for row in records:
749
- event = dict(row['event'])
550
+ event = self._event_with_latest_execution_evidence(
551
+ row['event'], row.get('evidence_events') or []
552
+ )
750
553
  occurred_at = native_datetime(event['created_at'])
751
554
  grouped[occurred_at.date()].append(ProjectAgentActivityTask(
752
555
  task_id=event['task_id'],
@@ -784,7 +587,6 @@ class StoreProjectAgentTasks(
784
587
 
785
588
  async def _route_assigned_agent(self, actor, request, task):
786
589
  """Project the selected durable identity into the route response."""
787
-
788
590
  agent_id = getattr(task, 'lead_agent_id', None)
789
591
  getter = getattr(self, 'get_project_agent', None)
790
592
  if not agent_id or not callable(getter):
@@ -898,7 +700,7 @@ class StoreProjectAgentTasks(
898
700
  task_id=task_id,
899
701
  agent_id=selected['agent_id'],
900
702
  work_kind=request.work_kind,
901
- required_capabilities=request.required_capabilities,
703
+ required_capabilities=request.executor_capability_hints,
902
704
  model_strategy=model_strategy,
903
705
  task_override=(
904
706
  getattr(request, 'executor_policy_override', None)
@@ -919,9 +721,6 @@ class StoreProjectAgentTasks(
919
721
  return {}
920
722
  raw = decision.model_dump(mode='json') if hasattr(decision, 'model_dump') else dict(decision)
921
723
  aliases = {
922
- # The routing decision audit field is JSON data. Keep the
923
- # provider-neutral snapshot instead of leaking a Pydantic model
924
- # into the parent record (which fails validation in real routing).
925
724
  'executor_decision': raw,
926
725
  'selected_executor_id': raw.get('selected_executor_id') or raw.get('executor_id'),
927
726
  'matched_executor_rule_id': raw.get('matched_rule_id') or raw.get('rule_id'),
@@ -935,26 +734,15 @@ class StoreProjectAgentTasks(
935
734
  if key in ProjectAgentRoutingDecisionRecord.model_fields
936
735
  }
937
736
 
938
- @staticmethod
939
- def _activity_executor_fields(event):
940
- return {
941
- key: value
942
- for key, value in {
943
- 'actual_executor_id': event.get('actual_executor_id'),
944
- 'matched_executor_rule_id': event.get('matched_executor_rule_id'),
945
- 'worker_id': event.get('worker_id'),
946
- 'worker_label': event.get('worker_label'),
947
- 'worker_occupation_emoji': event.get(
948
- 'worker_occupation_emoji'
949
- ),
950
- 'worker_status': event.get('worker_status'),
951
- }.items()
952
- if key in ProjectAgentActivityTask.model_fields
953
- }
737
+ async def _persist_task(self, request, **values):
738
+ # The task must not become visible before all of its planned edges exist.
739
+ async with self.runtime.driver.transaction() as transaction:
740
+ await self._persist_task_transaction(request, transaction, **values)
954
741
 
955
- async def _persist_task(
742
+ async def _persist_task_transaction(
956
743
  self,
957
744
  request,
745
+ transaction,
958
746
  *,
959
747
  task_id,
960
748
  payload_hash,
@@ -974,7 +762,24 @@ class StoreProjectAgentTasks(
974
762
  recruitment,
975
763
  executor_decision,
976
764
  created_at,
765
+ recruitments=None,
977
766
  ):
767
+ linked_recruitments = list(recruitments or [])
768
+ if recruitment and not any(
769
+ item.recruitment_id == recruitment.recruitment_id
770
+ for item in linked_recruitments
771
+ ):
772
+ linked_recruitments.insert(0, recruitment)
773
+ recruitment_links = [
774
+ {
775
+ 'recruitment_id': item.recruitment_id,
776
+ 'is_primary': bool(
777
+ recruitment
778
+ and item.recruitment_id == recruitment.recruitment_id
779
+ ),
780
+ }
781
+ for item in linked_recruitments
782
+ ]
978
783
  decision_raw = decision.model_dump(mode='json')
979
784
  executor_raw = (
980
785
  executor_decision.model_dump(mode='json')
@@ -988,15 +793,15 @@ class StoreProjectAgentTasks(
988
793
  task_id,
989
794
  'created',
990
795
  )
991
- await self.runtime.driver.execute_query(
796
+ result = await transaction.run(
992
797
  '''
993
798
  MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})
994
799
  MATCH (space)-[:CONTAINS_PROJECT]->
995
800
  (project:FuliPersonalProject {project_id: $personal_project_id})
996
801
  MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->
997
802
  (coordinator:FuliProjectAgent {agent_id: $coordinator_agent_id})
998
- CREATE (task:FuliProjectAgentTask {
999
- id: $task_id,
803
+ MERGE (task:FuliProjectAgentTask {id: $task_id})
804
+ ON CREATE SET task += {
1000
805
  task_id: $task_id,
1001
806
  personal_space_id: $personal_space_id,
1002
807
  personal_project_id: $personal_project_id,
@@ -1005,6 +810,7 @@ class StoreProjectAgentTasks(
1005
810
  objective: $objective,
1006
811
  work_kind: $work_kind,
1007
812
  required_capabilities: $required_capabilities,
813
+ executor_capability_hints: $executor_capability_hints,
1008
814
  duration: $duration,
1009
815
  staffing_intent: $staffing_intent,
1010
816
  status: $status,
@@ -1028,32 +834,39 @@ class StoreProjectAgentTasks(
1028
834
  executor_decision_json: $executor_decision_json,
1029
835
  executor_policy_json: $executor_policy_json,
1030
836
  selected_executor_id: $selected_executor_id,
837
+ participant_plan_json: $participant_plan_json,
838
+ recruitment_plan_json: $recruitment_plan_json,
1031
839
  created_at: $created_at,
1032
840
  updated_at: $created_at
841
+ }
842
+ WITH space, project, coordinator, task
843
+ WHERE task.payload_hash = $payload_hash
844
+ MERGE (decision_node:FuliProjectAgentRoutingDecision {
845
+ id: $decision_id
1033
846
  })
1034
- CREATE (decision_node:FuliProjectAgentRoutingDecision {
1035
- id: $decision_id,
1036
- decision_id: $decision_id,
1037
- task_id: $task_id,
1038
- decision_json: $decision_json,
1039
- created_at: $created_at
1040
- })
1041
- CREATE (event:FuliProjectAgentTaskEvent {
1042
- id: $initial_event_id,
1043
- event_id: $initial_event_id,
1044
- task_id: $task_id,
1045
- status: $status,
1046
- actor_kind: 'system',
1047
- summary: $routing_explanation,
1048
- source_application: $source_application,
1049
- source_session_id: $source_session_id,
1050
- created_at: $created_at
1051
- })
847
+ ON CREATE SET decision_node.decision_id = $decision_id,
848
+ decision_node.task_id = $task_id,
849
+ decision_node.decision_json = $decision_json,
850
+ decision_node.created_at = $created_at
851
+ MERGE (event:FuliProjectAgentTaskEvent {id: $initial_event_id})
852
+ ON CREATE SET event.event_id = $initial_event_id,
853
+ event.task_id = $task_id,
854
+ event.status = $status,
855
+ event.actor_kind = 'system',
856
+ event.summary = $routing_explanation,
857
+ event.source_application = $source_application,
858
+ event.source_session_id = $source_session_id,
859
+ event.created_at = $created_at
1052
860
  MERGE (space)-[:HAS_PROJECT_AGENT_TASK]->(task)
1053
861
  MERGE (project)-[:HAS_PROJECT_AGENT_TASK]->(task)
1054
862
  MERGE (task)-[:COORDINATED_BY]->(coordinator)
1055
863
  MERGE (task)-[:ROUTED_BY]->(decision_node)
1056
864
  MERGE (task)-[:HAS_TASK_EVENT]->(event)
865
+ RETURN true AS payload_matches,
866
+ task.participant_plan_json AS participant_plan_json,
867
+ task.recruitment_plan_json AS recruitment_plan_json,
868
+ task.status AS persisted_status,
869
+ task.created_at AS persisted_created_at
1057
870
  ''',
1058
871
  task_id=task_id,
1059
872
  personal_space_id=request.personal_space_id,
@@ -1063,6 +876,7 @@ class StoreProjectAgentTasks(
1063
876
  objective=request.objective,
1064
877
  work_kind=request.work_kind,
1065
878
  required_capabilities=request.required_capabilities,
879
+ executor_capability_hints=request.executor_capability_hints,
1066
880
  duration=request.duration,
1067
881
  staffing_intent=request.staffing_intent,
1068
882
  status=status,
@@ -1101,51 +915,30 @@ class StoreProjectAgentTasks(
1101
915
  (executor_raw or {}).get('selected_executor_id')
1102
916
  or (executor_raw or {}).get('executor_id')
1103
917
  ),
918
+ participant_plan_json=json.dumps(participants, sort_keys=True),
919
+ recruitment_plan_json=json.dumps(recruitment_links, sort_keys=True),
1104
920
  created_at=created_at,
1105
921
  decision_id=decision.decision_id,
1106
922
  decision_json=json.dumps(decision_raw, sort_keys=True),
1107
923
  initial_event_id=initial_event_id,
1108
924
  )
1109
- for participant in participants:
1110
- await self.runtime.driver.execute_query(
1111
- '''
1112
- MATCH (space:FuliSpace {
1113
- id: $personal_space_id, kind: 'personal'
1114
- })-[:HAS_PROJECT_AGENT_TASK]->
1115
- (task:FuliProjectAgentTask {task_id: $task_id})
1116
- MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->
1117
- (agent:FuliProjectAgent {agent_id: $agent_id})
1118
- MERGE (task)-[participant:HAS_PARTICIPANT]->(agent)
1119
- SET participant.role = $role,
1120
- participant.status = $status,
1121
- participant.assignment_summary = $assignment_summary,
1122
- participant.joined_at = $created_at,
1123
- participant.updated_at = $created_at
1124
- ''',
1125
- personal_space_id=request.personal_space_id,
1126
- task_id=task_id,
1127
- agent_id=participant['agent_id'],
1128
- role=participant['role'],
1129
- status=status,
1130
- assignment_summary=participant.get('assignment_summary'),
1131
- created_at=created_at,
1132
- )
1133
- if recruitment:
1134
- await self.runtime.driver.execute_query(
1135
- '''
1136
- MATCH (space:FuliSpace {
1137
- id: $personal_space_id, kind: 'personal'
1138
- })-[:HAS_PROJECT_AGENT_TASK]->
1139
- (task:FuliProjectAgentTask {task_id: $task_id})
1140
- MATCH (space)-[:HAS_PROJECT_AGENT_RECRUITMENT]->
1141
- (recruitment:FuliProjectAgentRecruitment {
1142
- recruitment_id: $recruitment_id
1143
- })
1144
- SET task.recruitment_id = $recruitment_id,
1145
- task.hr_agent_id = recruitment.hr_agent_id
1146
- MERGE (task)-[:TRIGGERED_RECRUITMENT]->(recruitment)
1147
- ''',
1148
- personal_space_id=request.personal_space_id,
1149
- task_id=task_id,
1150
- recruitment_id=recruitment.recruitment_id,
925
+ records = [record async for record in result]
926
+ if not records or not dict(records[0]).get('payload_matches'):
927
+ raise HTTPException(
928
+ status_code=409,
929
+ detail='task idempotency key was used with different input',
1151
930
  )
931
+ persisted = dict(records[0])
932
+ await self._persist_task_links(
933
+ request,
934
+ transaction=transaction,
935
+ task_id=task_id,
936
+ participants=self._task_link_plan(
937
+ persisted.get('participant_plan_json')
938
+ ),
939
+ recruitment_links=self._task_link_plan(
940
+ persisted.get('recruitment_plan_json')
941
+ ),
942
+ status=persisted.get('persisted_status') or status,
943
+ created_at=persisted.get('persisted_created_at') or created_at,
944
+ )