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.
- package/README.md +69 -13
- package/README.zh-CN.md +60 -14
- package/dist/web/assets/{AboutPage-sLV9WChi.js → AboutPage-9CVX91tr.js} +1 -1
- package/dist/web/assets/{ConnectionsPage-BWn4ywGJ.js → ConnectionsPage-CWbCVFdv.js} +1 -1
- package/dist/web/assets/EmployeeRecruitDialog-CQgNpGJ_.js +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-CfFlyKSh.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-CWg4WJhr.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-CwU4qBgD.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-B6TGAcFH.js +1 -0
- package/dist/web/assets/{KnowledgeEditDialog-DWUV_NPp.css → KnowledgeEditDialog-iv-dgUeD.css} +1 -1
- package/dist/web/assets/{KnowledgeInspector-CRQWOaos.js → KnowledgeInspector-M2pLY0bR.js} +1 -1
- package/dist/web/assets/{KnowledgeOrganizerPage-Dfj3vIpU.js → KnowledgeOrganizerPage-DzeAlk9b.js} +1 -1
- package/dist/web/assets/KnowledgePage-BJu9w0fF.js +1 -0
- package/dist/web/assets/{KnowledgeWorkspace-ChEB5Bwk.js → KnowledgeWorkspace-CCt8rnBb.js} +1 -1
- package/dist/web/assets/{OverviewPage-Cd66M0RT.js → OverviewPage-CZswVqui.js} +1 -1
- package/dist/web/assets/{PersonalProfilePage-RCFHL0Wz.js → PersonalProfilePage-f1nSF6kx.js} +1 -1
- package/dist/web/assets/PersonalProjectsPage-D__eJf0t.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +1 -0
- package/dist/web/assets/ProjectAgentsPage-LrwagMGj.js +11 -0
- package/dist/web/assets/{PublicProjectsPage-mstPEjHe.js → PublicProjectsPage-BdC2Qk4n.js} +1 -1
- package/dist/web/assets/{ReviewPage-6Y1SmfZy.js → ReviewPage-Cbgl8_Si.js} +1 -1
- package/dist/web/assets/SearchableSelect-ERm-k4KI.js +1 -0
- package/dist/web/assets/{SettingsPage--LdTMUm7.js → SettingsPage-DWso4x29.js} +1 -1
- package/dist/web/assets/{WritingTastePage-CIpKswvs.js → WritingTastePage-CFpCjmIY.js} +1 -1
- package/dist/web/assets/catalog-WGLn7ERJ.js +1 -0
- package/dist/web/assets/{console-BT1P2Db4.js → console-DQgu0JIA.js} +1 -1
- package/dist/web/assets/conversation-launcher-settings-0KtlG_LV.js +1 -0
- package/dist/web/assets/{i18n-js6gfc7j.js → i18n-ByRiA3pJ.js} +3 -3
- package/dist/web/assets/{identity-CfDfanpa.js → identity-CxdPRMwH.js} +1 -1
- package/dist/web/assets/index-JAZe6lfX.js +2 -0
- package/dist/web/assets/index-wcXzzggp.css +1 -0
- package/dist/web/assets/{model-DJkLAJsg.js → model-Db4-KefQ.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-BdSti7Y2.js → useApi-CROJJdhE-CePMjc08.js} +1 -1
- package/dist/web/assets/{vue-router-Kk67EaCP.js → vue-router-CzX-hn3P.js} +1 -1
- package/dist/web/index.html +7 -6
- package/docs/employee-agents.md +99 -0
- package/docs/project-agent-memory.md +479 -0
- package/graph-provider/fuli_graph/knowledge_audit_models.py +171 -0
- package/graph-provider/fuli_graph/model_base.py +5 -0
- package/graph-provider/fuli_graph/model_validation.py +32 -0
- package/graph-provider/fuli_graph/models.py +26 -170
- package/graph-provider/fuli_graph/project_agent_context_models.py +29 -0
- package/graph-provider/fuli_graph/project_agent_executor_models.py +35 -7
- package/graph-provider/fuli_graph/project_agent_memory_models.py +79 -0
- package/graph-provider/fuli_graph/project_agent_memory_routes.py +46 -0
- package/graph-provider/fuli_graph/project_agent_models.py +9 -0
- package/graph-provider/fuli_graph/project_agent_recruitment_values.py +48 -0
- package/graph-provider/fuli_graph/project_agent_routes.py +21 -1
- package/graph-provider/fuli_graph/project_agent_task_models.py +217 -29
- package/graph-provider/fuli_graph/runtime.py +37 -1
- package/graph-provider/fuli_graph/store.py +6 -0
- package/graph-provider/fuli_graph/store_project_agent_context.py +65 -0
- package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +175 -11
- package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +33 -10
- package/graph-provider/fuli_graph/store_project_agent_executors.py +181 -14
- package/graph-provider/fuli_graph/store_project_agent_memory.py +174 -0
- package/graph-provider/fuli_graph/store_project_agent_task_activity.py +423 -0
- package/graph-provider/fuli_graph/store_project_agent_task_persistence.py +152 -0
- package/graph-provider/fuli_graph/store_project_agent_task_reads.py +187 -21
- package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +748 -162
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +230 -18
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +261 -468
- package/graph-provider/fuli_graph/store_project_agents.py +214 -46
- package/graph-provider/fuli_graph/store_task_contexts.py +217 -0
- package/graph-provider/fuli_graph/store_transactions.py +40 -0
- package/graph-provider/fuli_graph/task_context_models.py +50 -0
- package/graph-provider/fuli_graph/task_context_routes.py +31 -0
- package/graph-provider/fuli_graph/workflow_observation_models.py +1 -1
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +8 -8
- package/package.json +5 -2
- package/skills/capturing-session-knowledge/SKILL.md +18 -6
- package/src/adaptive-runtime/executor-pool.js +46 -5
- package/src/adaptive-runtime/graph-services.js +10 -3
- package/src/adaptive-runtime/lease-client.js +66 -14
- package/src/adaptive-runtime/runtime-broker.js +24 -9
- package/src/agent-tools/employee-definitions.js +43 -0
- package/src/agent-tools/graph-definitions.js +25 -6
- package/src/agent-tools/graph-handlers.js +11 -0
- package/src/agent-tools/project-agent-definitions.js +35 -4
- package/src/agent-tools/project-agent-memory-definitions.js +36 -0
- package/src/agent-tools.js +6 -2
- package/src/agents/codex/lifecycle-hook.js +107 -0
- package/src/agents/codex/lifecycle-hooks.js +231 -0
- package/src/agents/cursor/lifecycle-hook.js +133 -0
- package/src/agents/cursor/lifecycle-hooks.js +50 -0
- package/src/agents/lifecycle-message.js +19 -0
- package/src/app/agent-request-context.js +23 -0
- package/src/cli/command-registry.js +2 -0
- package/src/cli/employee-command.js +17 -0
- package/src/cli/remote-mcp-command.js +282 -0
- package/src/cli/setup-command.js +5 -0
- package/src/cli/update-command.js +6 -43
- package/src/cli.js +10 -0
- package/src/employees/catalog/jefa.json +21 -0
- package/src/employees/install-package.js +77 -0
- package/src/employees/management-policy.js +88 -0
- package/src/employees/manifest.js +53 -0
- package/src/employees/package-registry.js +107 -0
- package/src/employees/project-selection.js +50 -0
- package/src/employees/recruitment.js +199 -0
- package/src/employees/service.js +83 -0
- package/src/employees/task-entry.js +39 -0
- package/src/graphiti/agent-knowledge-workflows.js +135 -25
- package/src/graphiti/federated-application.js +78 -35
- package/src/graphiti/project-agent-control-plane.js +20 -3
- package/src/graphiti/project-agent-mapping.js +155 -5
- package/src/graphiti/project-agent-memory.js +115 -0
- package/src/graphiti/project-agent-task-entry.js +126 -0
- package/src/graphiti/project-agent-workflows.js +30 -14
- package/src/graphiti/provider-client.js +66 -4
- package/src/http/api-router.js +30 -5
- package/src/http/employee-api-router.js +64 -0
- package/src/http/error-mapping.js +4 -0
- package/src/http/graph-api-router.js +2 -0
- package/src/http/request-policy.js +6 -3
- package/src/http/static-handler.js +4 -3
- package/src/mcp/create-mcp-server.js +98 -37
- package/src/mcp/direct-call.js +38 -0
- package/src/mcp/instructions.js +8 -7
- package/src/mcp/provider-task-context-registry.js +74 -0
- package/src/mcp/remote-http-server.js +761 -0
- package/src/mcp/session-id.js +29 -3
- package/src/mcp/task-context-registry.js +88 -29
- package/src/mcp/tool-annotations.js +10 -2
- package/src/mcp/tool-result.js +8 -3
- package/src/mcp-server.js +20 -8
- package/src/native-runtime/runtime.js +58 -8
- package/src/semantic-version.js +42 -0
- package/src/server.js +76 -28
- package/src/setup/agent-installation-status.js +38 -6
- package/src/setup/agents.js +20 -8
- package/src/setup/claude-code-config.js +55 -13
- package/src/setup/codex-bootstrap.js +11 -10
- package/src/setup/codex-config.js +74 -7
- package/src/setup/config-backup.js +18 -2
- package/src/setup/cursor-config.js +22 -5
- package/src/setup/setup.js +30 -22
- package/src/storage/json-file.js +11 -3
- package/src/system/package-version-checker.js +72 -0
- package/src/system/system-service.js +3 -0
- package/dist/web/assets/KnowledgeEditDialog-CZ5ziPrD.js +0 -1
- package/dist/web/assets/KnowledgePage-BRYC9S5Y.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-DEh8XWby.js +0 -1
- package/dist/web/assets/ProjectAgentsPage-Crh7PrXa.css +0 -1
- package/dist/web/assets/ProjectAgentsPage-DPnaH-TR.js +0 -11
- package/dist/web/assets/SearchableSelect-D3HM8YF8.js +0 -1
- package/dist/web/assets/conversation-launcher-settings-DrJEotdf.js +0 -1
- package/dist/web/assets/index-BnH5mcZx.css +0 -1
- 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
|
-
|
|
85
|
+
continuity_pool,
|
|
66
86
|
request,
|
|
67
87
|
)
|
|
68
88
|
if continuity:
|
|
69
89
|
selected = continuity[0]
|
|
70
|
-
|
|
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(
|
|
101
|
-
return [
|
|
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
|
|
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
|
|
171
|
-
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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=
|
|
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
|
)
|