fuli-context 0.7.6 → 0.7.8

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 (150) 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-9CVX91tr.js} +1 -1
  4. package/dist/web/assets/{ConnectionsPage-BWn4ywGJ.js → ConnectionsPage-CWbCVFdv.js} +1 -1
  5. package/dist/web/assets/EmployeeRecruitDialog-CQgNpGJ_.js +1 -0
  6. package/dist/web/assets/EmployeeRecruitDialog-CfFlyKSh.css +1 -0
  7. package/dist/web/assets/EmployeeWorkbenchPage-CWg4WJhr.css +1 -0
  8. package/dist/web/assets/EmployeeWorkbenchPage-CwU4qBgD.js +1 -0
  9. package/dist/web/assets/KnowledgeEditDialog-B6TGAcFH.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-M2pLY0bR.js} +1 -1
  12. package/dist/web/assets/{KnowledgeOrganizerPage-Dfj3vIpU.js → KnowledgeOrganizerPage-DzeAlk9b.js} +1 -1
  13. package/dist/web/assets/KnowledgePage-BJu9w0fF.js +1 -0
  14. package/dist/web/assets/{KnowledgeWorkspace-ChEB5Bwk.js → KnowledgeWorkspace-CCt8rnBb.js} +1 -1
  15. package/dist/web/assets/{OverviewPage-Cd66M0RT.js → OverviewPage-CZswVqui.js} +1 -1
  16. package/dist/web/assets/{PersonalProfilePage-RCFHL0Wz.js → PersonalProfilePage-f1nSF6kx.js} +1 -1
  17. package/dist/web/assets/PersonalProjectsPage-D__eJf0t.js +1 -0
  18. package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +1 -0
  19. package/dist/web/assets/ProjectAgentsPage-LrwagMGj.js +11 -0
  20. package/dist/web/assets/{PublicProjectsPage-mstPEjHe.js → PublicProjectsPage-BdC2Qk4n.js} +1 -1
  21. package/dist/web/assets/{ReviewPage-6Y1SmfZy.js → ReviewPage-Cbgl8_Si.js} +1 -1
  22. package/dist/web/assets/SearchableSelect-ERm-k4KI.js +1 -0
  23. package/dist/web/assets/{SettingsPage--LdTMUm7.js → SettingsPage-DWso4x29.js} +1 -1
  24. package/dist/web/assets/{WritingTastePage-CIpKswvs.js → WritingTastePage-CFpCjmIY.js} +1 -1
  25. package/dist/web/assets/catalog-WGLn7ERJ.js +1 -0
  26. package/dist/web/assets/{console-BT1P2Db4.js → console-DQgu0JIA.js} +1 -1
  27. package/dist/web/assets/conversation-launcher-settings-0KtlG_LV.js +1 -0
  28. package/dist/web/assets/{i18n-js6gfc7j.js → i18n-ByRiA3pJ.js} +3 -3
  29. package/dist/web/assets/{identity-CfDfanpa.js → identity-CxdPRMwH.js} +1 -1
  30. package/dist/web/assets/index-JAZe6lfX.js +2 -0
  31. package/dist/web/assets/index-wcXzzggp.css +1 -0
  32. package/dist/web/assets/{model-DJkLAJsg.js → model-Db4-KefQ.js} +1 -1
  33. package/dist/web/assets/{useApi-CROJJdhE-BdSti7Y2.js → useApi-CROJJdhE-CePMjc08.js} +1 -1
  34. package/dist/web/assets/{vue-router-Kk67EaCP.js → vue-router-CzX-hn3P.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/update-command.js +6 -43
  94. package/src/cli.js +10 -0
  95. package/src/employees/catalog/jefa.json +21 -0
  96. package/src/employees/install-package.js +77 -0
  97. package/src/employees/management-policy.js +88 -0
  98. package/src/employees/manifest.js +53 -0
  99. package/src/employees/package-registry.js +107 -0
  100. package/src/employees/project-selection.js +50 -0
  101. package/src/employees/recruitment.js +199 -0
  102. package/src/employees/service.js +83 -0
  103. package/src/employees/task-entry.js +39 -0
  104. package/src/graphiti/agent-knowledge-workflows.js +135 -25
  105. package/src/graphiti/federated-application.js +78 -35
  106. package/src/graphiti/project-agent-control-plane.js +20 -3
  107. package/src/graphiti/project-agent-mapping.js +155 -5
  108. package/src/graphiti/project-agent-memory.js +115 -0
  109. package/src/graphiti/project-agent-task-entry.js +126 -0
  110. package/src/graphiti/project-agent-workflows.js +30 -14
  111. package/src/graphiti/provider-client.js +66 -4
  112. package/src/http/api-router.js +30 -5
  113. package/src/http/employee-api-router.js +64 -0
  114. package/src/http/error-mapping.js +4 -0
  115. package/src/http/graph-api-router.js +2 -0
  116. package/src/http/request-policy.js +6 -3
  117. package/src/http/static-handler.js +4 -3
  118. package/src/mcp/create-mcp-server.js +98 -37
  119. package/src/mcp/direct-call.js +38 -0
  120. package/src/mcp/instructions.js +8 -7
  121. package/src/mcp/provider-task-context-registry.js +74 -0
  122. package/src/mcp/remote-http-server.js +761 -0
  123. package/src/mcp/session-id.js +29 -3
  124. package/src/mcp/task-context-registry.js +88 -29
  125. package/src/mcp/tool-annotations.js +10 -2
  126. package/src/mcp/tool-result.js +8 -3
  127. package/src/mcp-server.js +20 -8
  128. package/src/native-runtime/runtime.js +58 -8
  129. package/src/semantic-version.js +42 -0
  130. package/src/server.js +76 -28
  131. package/src/setup/agent-installation-status.js +38 -6
  132. package/src/setup/agents.js +20 -8
  133. package/src/setup/claude-code-config.js +55 -13
  134. package/src/setup/codex-bootstrap.js +11 -10
  135. package/src/setup/codex-config.js +74 -7
  136. package/src/setup/config-backup.js +18 -2
  137. package/src/setup/cursor-config.js +22 -5
  138. package/src/setup/setup.js +30 -22
  139. package/src/storage/json-file.js +11 -3
  140. package/src/system/package-version-checker.js +72 -0
  141. package/src/system/system-service.js +3 -0
  142. package/dist/web/assets/KnowledgeEditDialog-CZ5ziPrD.js +0 -1
  143. package/dist/web/assets/KnowledgePage-BRYC9S5Y.js +0 -1
  144. package/dist/web/assets/PersonalProjectsPage-DEh8XWby.js +0 -1
  145. package/dist/web/assets/ProjectAgentsPage-Crh7PrXa.css +0 -1
  146. package/dist/web/assets/ProjectAgentsPage-DPnaH-TR.js +0 -11
  147. package/dist/web/assets/SearchableSelect-D3HM8YF8.js +0 -1
  148. package/dist/web/assets/conversation-launcher-settings-DrJEotdf.js +0 -1
  149. package/dist/web/assets/index-BnH5mcZx.css +0 -1
  150. package/dist/web/assets/index-GqJf1xqg.js +0 -2
@@ -61,20 +61,39 @@ class StoreProjectAgentTaskStaffing:
61
61
  return [], rows, 'manual_agent_selection', [
62
62
  'project policy requires an explicit @Agent selection',
63
63
  ]
64
+ normalized_work_kind = request.work_kind.casefold()
65
+ required = {item.casefold() for item in request.required_capabilities}
66
+ eligible = [row for row in rows if required.issubset({
67
+ item.casefold() for item in row.get('capabilities', [])
68
+ })]
69
+ candidates, history = await self._rank_agent_candidates(eligible, request)
70
+ # Continuity may break a tie between qualified specialists; it must
71
+ # never override a capability requirement or a stronger work-kind fit.
72
+ continuity_pool = eligible
73
+ if candidates:
74
+ best_work_match = normalized_work_kind in {
75
+ item.casefold() for item in candidates[0].get('work_kinds', [])
76
+ }
77
+ continuity_pool = [row for row in candidates if (
78
+ normalized_work_kind in {item.casefold() for item in row.get('work_kinds', [])}
79
+ ) == best_work_match]
80
+ if continuity_pool:
81
+ least_busy = min(self._metric_int(row.get('active_task_count')) for row in continuity_pool)
82
+ continuity_pool = [row for row in continuity_pool
83
+ if self._metric_int(row.get('active_task_count')) == least_busy]
64
84
  continuity, project_history = await self._rank_project_continuity(
65
- rows,
85
+ continuity_pool,
66
86
  request,
67
87
  )
68
88
  if continuity:
69
89
  selected = continuity[0]
70
- return [selected], continuity, 'project_continuity', [
90
+ remaining = [row for row in candidates or eligible if row['agent_id'] != selected['agent_id']]
91
+ return [selected], [selected, *remaining], 'project_continuity', [
71
92
  self._project_history_match_basis(
72
93
  project_history.get(selected['agent_id']),
73
94
  ),
95
+ *self._automatic_selection_basis(selected, request),
74
96
  ]
75
- normalized_work_kind = request.work_kind.casefold()
76
- required = {item.casefold() for item in request.required_capabilities}
77
- candidates, history = await self._rank_agent_candidates(rows, request)
78
97
  if not candidates:
79
98
  unavailable_match = any(
80
99
  not self._agent_client_allowed(row, request.source_application)
@@ -97,9 +116,20 @@ class StoreProjectAgentTaskStaffing:
97
116
  'matching Agent is unavailable to source client: '
98
117
  f'{request.source_application or "other"}',
99
118
  ]
100
- if len(rows) == 1:
101
- return [rows[0]], rows, 'sole_active_assignment', [
119
+ if len(eligible) == 1:
120
+ return [eligible[0]], eligible, 'sole_active_assignment', [
102
121
  'the project has one active Agent assignment',
122
+ *self._automatic_selection_basis(eligible[0], request),
123
+ ]
124
+ if eligible and normalized_work_kind == 'project_context' and not required:
125
+ eligible.sort(key=lambda row: (
126
+ self._metric_int(row.get('active_task_count')),
127
+ -int(self._metric_int(row.get('memory_revision')) > 0),
128
+ row['assigned_at'], row['agent_id'],
129
+ ))
130
+ return eligible[:1], eligible, 'project_default', [
131
+ f'{len(eligible)} eligible project roles; working-memory continuity and stable tie-break',
132
+ *self._automatic_selection_basis(eligible[0], request),
103
133
  ]
104
134
  return [], rows, 'no_match', ['no active assignment matched exactly']
105
135
  selected = candidates[0]
@@ -119,8 +149,24 @@ class StoreProjectAgentTaskStaffing:
119
149
  )
120
150
  if history_basis:
121
151
  match_basis.append(history_basis)
152
+ match_basis.extend(item for item in self._automatic_selection_basis(selected, request)
153
+ if item not in match_basis)
122
154
  return [selected], candidates, reason, match_basis
123
155
 
156
+ def _automatic_selection_basis(self, selected, request):
157
+ work_kind = request.work_kind.casefold()
158
+ if work_kind == 'project_context':
159
+ work_basis = 'general project context: continuity only, not semantic task matching'
160
+ elif work_kind in {item.casefold() for item in selected.get('work_kinds', [])}:
161
+ work_basis = f'exact work kind: {request.work_kind}'
162
+ else:
163
+ match = ('explicit capabilities matched' if request.required_capabilities
164
+ else 'continuity only, not semantic task matching')
165
+ work_basis = f'no exact work-kind match: {request.work_kind}; {match}'
166
+ return [work_basis,
167
+ f'selected active task count: {self._metric_int(selected.get("active_task_count"))}; '
168
+ 'work-kind fit and load precede history']
169
+
124
170
  async def _rank_agent_candidates(self, rows, request):
125
171
  """Rank already source-eligible assignments with task-history continuity."""
126
172
 
@@ -132,6 +178,8 @@ class StoreProjectAgentTaskStaffing:
132
178
  capabilities = {item.casefold() for item in row.get('capabilities', [])}
133
179
  work_match = normalized_work_kind in work_kinds
134
180
  capability_match = required.issubset(capabilities)
181
+ if not capability_match:
182
+ continue
135
183
  if not work_match and not (required and capability_match):
136
184
  continue
137
185
  score = (2 if work_match else 0) + (1 if capability_match else 0)
@@ -147,7 +195,9 @@ class StoreProjectAgentTaskStaffing:
147
195
  scored.sort(
148
196
  key=lambda item: (
149
197
  -item[0],
198
+ self._metric_int(item[1].get('active_task_count')),
150
199
  *self._history_sort_key(history.get(item[1]['agent_id'])),
200
+ -int(self._metric_int(item[1].get('memory_revision')) > 0),
151
201
  item[1]['assigned_at'],
152
202
  item[1]['agent_id'],
153
203
  )
@@ -155,7 +205,7 @@ class StoreProjectAgentTaskStaffing:
155
205
  return [item[1] for item in scored], history
156
206
 
157
207
  async def _rank_project_continuity(self, rows, request):
158
- """Prefer the last effective project lead before task-type matching."""
208
+ """Prefer an effective previous lead among equally qualified available roles."""
159
209
 
160
210
  if not rows:
161
211
  return [], {}
@@ -167,8 +217,15 @@ class StoreProjectAgentTaskStaffing:
167
217
  candidates = [
168
218
  row
169
219
  for row in rows
170
- if self._metric_int(
171
- (history.get(row['agent_id']) or {}).get('participation_count')
220
+ if sum(
221
+ self._metric_int(
222
+ (history.get(row['agent_id']) or {}).get(metric)
223
+ )
224
+ for metric in (
225
+ 'completed_count',
226
+ 'failed_count',
227
+ 'cancelled_count',
228
+ )
172
229
  ) > 0
173
230
  ]
174
231
  candidates.sort(
@@ -209,7 +266,13 @@ class StoreProjectAgentTaskStaffing:
209
266
  OPTIONAL MATCH (task)-[:HAS_TASK_EVENT]->
210
267
  (event:FuliProjectAgentTaskEvent)-[:EVENT_AGENT]->(agent)
211
268
  RETURN agent.agent_id AS agent_id,
212
- count(DISTINCT task.task_id) AS participation_count,
269
+ count(DISTINCT CASE
270
+ WHEN participant.status IN
271
+ ['completed', 'failed', 'cancelled']
272
+ OR event.status IN
273
+ ['completed', 'failed', 'cancelled']
274
+ THEN task.task_id END
275
+ ) AS participation_count,
213
276
  count(DISTINCT CASE
214
277
  WHEN participant.status = 'completed'
215
278
  OR event.status = 'completed'
@@ -225,7 +288,13 @@ class StoreProjectAgentTaskStaffing:
225
288
  OR event.status = 'cancelled'
226
289
  THEN task.task_id END
227
290
  ) AS cancelled_count,
228
- max(task.updated_at) AS last_task_at,
291
+ max(CASE
292
+ WHEN participant.status IN
293
+ ['completed', 'failed', 'cancelled']
294
+ OR event.status IN
295
+ ['completed', 'failed', 'cancelled']
296
+ THEN coalesce(event.created_at, task.updated_at) END
297
+ ) AS last_task_at,
229
298
  max(CASE
230
299
  WHEN participant.status = 'completed'
231
300
  OR event.status = 'completed'
@@ -283,9 +352,14 @@ class StoreProjectAgentTaskStaffing:
283
352
  AND toLower(task.work_kind) = toLower($work_kind)
284
353
  OPTIONAL MATCH (task)-[:HAS_TASK_EVENT]->
285
354
  (event:FuliProjectAgentTaskEvent)-[:EVENT_AGENT]->(agent)
286
- WHERE event.status IN ['completed', 'failed', 'cancelled']
287
355
  RETURN agent.agent_id AS agent_id,
288
- count(DISTINCT task.task_id) AS participation_count,
356
+ count(DISTINCT CASE
357
+ WHEN participant.status IN
358
+ ['completed', 'failed', 'cancelled']
359
+ OR event.status IN
360
+ ['completed', 'failed', 'cancelled']
361
+ THEN task.task_id END
362
+ ) AS participation_count,
289
363
  count(DISTINCT CASE
290
364
  WHEN participant.status = 'completed'
291
365
  OR event.status = 'completed'
@@ -301,7 +375,13 @@ class StoreProjectAgentTaskStaffing:
301
375
  OR event.status = 'cancelled'
302
376
  THEN task.task_id END
303
377
  ) AS cancelled_count,
304
- max(event.created_at) AS last_outcome_at
378
+ max(CASE
379
+ WHEN participant.status IN
380
+ ['completed', 'failed', 'cancelled']
381
+ OR event.status IN
382
+ ['completed', 'failed', 'cancelled']
383
+ THEN coalesce(event.created_at, task.updated_at) END
384
+ ) AS last_outcome_at
305
385
  ''',
306
386
  personal_space_id=personal_space_id,
307
387
  personal_project_id=personal_project_id,
@@ -474,6 +554,111 @@ class StoreProjectAgentTaskStaffing:
474
554
  added.append(candidate)
475
555
  return added
476
556
 
557
+ async def _parallel_plan_participant_projection(
558
+ self,
559
+ actor,
560
+ request,
561
+ *,
562
+ lead,
563
+ participants,
564
+ routing_reason,
565
+ recruitment_slots=None,
566
+ ):
567
+ """Project every Agent automatic recruitment will add before writes.
568
+
569
+ Parallel-plan validation runs before recruitment because recruitment is
570
+ itself durable (and automatic recruitment provisions an Agent and an
571
+ assignment). Confirmation-mode recruitment does not create an Agent yet
572
+ and must therefore not count as an active participant.
573
+ """
574
+
575
+ projected = list(participants)
576
+ slots = recruitment_slots
577
+ if slots is None:
578
+ slots = await self._automatic_parallel_recruitment_slots(
579
+ actor,
580
+ request,
581
+ lead=lead,
582
+ participants=participants,
583
+ routing_reason=routing_reason,
584
+ )
585
+ for slot in slots:
586
+ participant = {
587
+ 'agent_id': slot['placeholder_agent_id'],
588
+ 'role': slot['participant_role'],
589
+ }
590
+ if slot['participant_role'] == 'lead':
591
+ projected.insert(0, participant)
592
+ else:
593
+ projected.append(participant)
594
+ return projected
595
+
596
+ async def _automatic_parallel_recruitment_slots(
597
+ self,
598
+ actor,
599
+ request,
600
+ *,
601
+ lead,
602
+ participants,
603
+ routing_reason,
604
+ ):
605
+ plan = request.parallel_plan
606
+ if not plan.enabled:
607
+ return []
608
+ if (
609
+ request.staffing_intent == 'unassigned'
610
+ or routing_reason == 'manual_agent_selection'
611
+ ):
612
+ return []
613
+
614
+ roles = []
615
+ projected_count = len(participants)
616
+ if not lead:
617
+ roles.append('lead')
618
+ projected_count += 1
619
+ while projected_count < 2:
620
+ roles.append('collaborator')
621
+ projected_count += 1
622
+ if not roles:
623
+ return []
624
+
625
+ policy = await self.get_project_agent_coordination_policy(
626
+ actor,
627
+ request.personal_space_id,
628
+ request.personal_project_id,
629
+ )
630
+ if policy.ask_before_recruitment:
631
+ return []
632
+ hr_records, _, _ = await self.runtime.driver.execute_query(
633
+ '''
634
+ MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})-
635
+ [:HAS_PROJECT_AGENT_IDENTITY]->
636
+ (hr:FuliProjectAgent {agent_type: 'hr', status: 'active'})
637
+ RETURN hr LIMIT 1
638
+ ''',
639
+ personal_space_id=request.personal_space_id,
640
+ routing_='r',
641
+ )
642
+ if not hr_records:
643
+ return []
644
+
645
+ collaborator_index = 0
646
+ slots = []
647
+ for role in roles:
648
+ if role == 'lead':
649
+ slot = 'lead'
650
+ else:
651
+ collaborator_index += 1
652
+ slot = f'collaborator-{collaborator_index}'
653
+ slots.append({
654
+ 'participant_role': role,
655
+ 'recruitment_slot': slot,
656
+ 'placeholder_agent_id': (
657
+ f"__recruited_{slot.replace('-', '_')}__"
658
+ ),
659
+ })
660
+ return slots
661
+
477
662
  async def _assignment_candidates(
478
663
  self,
479
664
  personal_space_id,
@@ -483,7 +668,7 @@ class StoreProjectAgentTaskStaffing:
483
668
  ):
484
669
  records, _, _ = await self.runtime.driver.execute_query(
485
670
  '''
486
- MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})-
671
+ MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})-
487
672
  [:CONTAINS_PROJECT]->
488
673
  (:FuliPersonalProject {project_id: $personal_project_id})-
489
674
  [:HAS_PROJECT_AGENT_ASSIGNMENT]->
@@ -491,7 +676,27 @@ class StoreProjectAgentTaskStaffing:
491
676
  [:ASSIGNED_AGENT]->(agent:FuliProjectAgent {status: 'active'})
492
677
  WHERE agent.agent_type = 'durable'
493
678
  OR ($include_temporary = true AND agent.agent_type = 'temporary')
494
- RETURN assignment, agent
679
+ OPTIONAL MATCH (agent)-[:HAS_WORKING_MEMORY]->(memory:FuliProjectAgentMemory {
680
+ personal_space_id: $personal_space_id, personal_project_id: $personal_project_id
681
+ })
682
+ WITH space, assignment, agent, coalesce(max(memory.revision), 0) AS memory_revision
683
+ OPTIONAL MATCH (space)-[:HAS_PROJECT_AGENT_TASK]->(active_task:FuliProjectAgentTask)
684
+ -[participant:HAS_PARTICIPANT]->(agent)
685
+ WHERE active_task.personal_project_id = $personal_project_id
686
+ AND active_task.status IN ['queued', 'running', 'awaiting_review']
687
+ AND participant.status IN ['queued', 'running', 'awaiting_review']
688
+ WITH space, assignment, agent, memory_revision,
689
+ count(DISTINCT active_task) AS task_count
690
+ OPTIONAL MATCH (session:FuliTaskContextSession)-[:HAS_CONTEXT]->
691
+ (context:FuliTaskContext {personal_space_id: $personal_space_id,
692
+ personal_project_id: $personal_project_id,
693
+ project_agent_id: agent.agent_id, completed: false})
694
+ WHERE session.current_token = context.token
695
+ AND context.created_at > datetime() - duration('PT2H')
696
+ WITH assignment, agent, memory_revision, task_count,
697
+ count(DISTINCT context) AS context_count
698
+ RETURN assignment, agent, memory_revision,
699
+ task_count + context_count AS active_task_count
495
700
  ORDER BY assignment.assigned_at, agent.agent_id
496
701
  ''',
497
702
  personal_space_id=personal_space_id,
@@ -520,6 +725,8 @@ class StoreProjectAgentTaskStaffing:
520
725
  ),
521
726
  'profile': profile,
522
727
  'assigned_at': str(assignment.get('assigned_at') or ''),
728
+ 'active_task_count': row.get('active_task_count') or 0,
729
+ 'memory_revision': row.get('memory_revision') or 0,
523
730
  })
524
731
  return result
525
732
 
@@ -582,5 +789,10 @@ class StoreProjectAgentTaskStaffing:
582
789
  if plan.enabled and len(participants) < 2:
583
790
  raise HTTPException(
584
791
  status_code=422,
585
- detail='parallel work requires at least two active Agents',
792
+ detail=(
793
+ 'parallel work requires at least two active Agents; '
794
+ 'automatic recruitment needs an active HR Agent, otherwise '
795
+ 'recruit another qualified collaborator or assign enough '
796
+ 'existing Agents before enabling parallel work'
797
+ ),
586
798
  )