fuli-context 0.7.5 → 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.
- package/README.md +69 -13
- package/README.zh-CN.md +60 -14
- package/dist/web/assets/{AboutPage-ByCUor-j.js → AboutPage-BcZeFcHe.js} +1 -1
- package/dist/web/assets/{ConnectionsPage-DkdkZ40Q.js → ConnectionsPage-xAQYl1SQ.js} +1 -1
- package/dist/web/assets/EmployeeRecruitDialog-CbPAmhy7.js +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-CfFlyKSh.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-BQvTLoc8.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-DF3EL0Ui.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-UnnBDt0I.js +1 -0
- package/dist/web/assets/{KnowledgeEditDialog-DWUV_NPp.css → KnowledgeEditDialog-iv-dgUeD.css} +1 -1
- package/dist/web/assets/{KnowledgeInspector-CYAjAb9J.js → KnowledgeInspector-BqPJH5Rr.js} +1 -1
- package/dist/web/assets/{KnowledgeOrganizerPage-CV4F54R_.js → KnowledgeOrganizerPage-CyvRflXM.js} +1 -1
- package/dist/web/assets/KnowledgePage-59eczTqe.js +1 -0
- package/dist/web/assets/{KnowledgeWorkspace-z4u5mXOG.js → KnowledgeWorkspace-CfIjhOog.js} +1 -1
- package/dist/web/assets/{OverviewPage-BWOfeS6k.js → OverviewPage-Xyfm5xkZ.js} +1 -1
- package/dist/web/assets/{PersonalProfilePage-c3w4-h7a.js → PersonalProfilePage-DB9mByB3.js} +1 -1
- package/dist/web/assets/PersonalProjectsPage-_-YXVi5O.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +1 -0
- package/dist/web/assets/ProjectAgentsPage-DeeQByPX.js +11 -0
- package/dist/web/assets/{PublicProjectsPage-DdH0oGB3.js → PublicProjectsPage-BEZ1YPs3.js} +1 -1
- package/dist/web/assets/{ReviewPage-BYIpcIHG.js → ReviewPage-CQYGwFBH.js} +1 -1
- package/dist/web/assets/SearchableSelect-D27H0MoP.js +1 -0
- package/dist/web/assets/{SettingsPage-2VkbXgZi.js → SettingsPage-BlEZ03Yi.js} +1 -1
- package/dist/web/assets/{WritingTastePage-BOPQZuva.js → WritingTastePage-DvRt_nmP.js} +1 -1
- package/dist/web/assets/catalog-DFvIaHVS.js +1 -0
- package/dist/web/assets/{console-BlAhujsH.js → console-tOxOl8Vq.js} +1 -1
- package/dist/web/assets/conversation-launcher-settings-DuFSpPAo.js +1 -0
- package/dist/web/assets/{i18n-Ci3heuiL.js → i18n-CgBHZthI.js} +3 -3
- package/dist/web/assets/{identity-4_-v6cB7.js → identity-CFo_Uw1H.js} +1 -1
- package/dist/web/assets/index-CJc5ppXH.css +1 -0
- package/dist/web/assets/index-yCi3cz2l.js +2 -0
- package/dist/web/assets/{model-yG0YqUyZ.js → model-BtnVr7fX.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-twhdZArA.js → useApi-CROJJdhE-B6MHm_Av.js} +1 -1
- package/dist/web/assets/{vue-router-C-wot_Di.js → vue-router-CzHm-y-x.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_coordination_models.py +22 -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 +50 -1
- package/graph-provider/fuli_graph/project_agent_task_models.py +220 -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_coordination_policy.py +122 -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 +756 -164
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +396 -7
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +265 -465
- 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 +48 -6
- package/src/agent-tools/graph-handlers.js +15 -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.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 +32 -3
- package/src/graphiti/project-agent-mapping.js +175 -4
- 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 +49 -14
- package/src/graphiti/provider-client.js +78 -4
- package/src/http/api-router.js +25 -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 +14 -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 +12 -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/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/dist/web/assets/KnowledgeEditDialog-DSe3Gwep.js +0 -1
- package/dist/web/assets/KnowledgePage-vYYXipOi.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-B7AlLEwf.js +0 -1
- package/dist/web/assets/ProjectAgentsPage-CgftpIPB.css +0 -1
- package/dist/web/assets/ProjectAgentsPage-DTHcL_ox.js +0 -11
- package/dist/web/assets/SearchableSelect-Dz13ClQb.js +0 -1
- package/dist/web/assets/conversation-launcher-settings-DTYxiLJt.js +0 -1
- package/dist/web/assets/index-BnH5mcZx.css +0 -1
- package/dist/web/assets/index-C67Q0JY2.js +0 -2
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Agent assignment selection, history continuity, and parallel staffing."""
|
|
2
2
|
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
3
5
|
from fastapi import HTTPException
|
|
4
6
|
|
|
5
7
|
from .project_agent_access import authorize_project_agent
|
|
@@ -50,9 +52,48 @@ class StoreProjectAgentTaskStaffing:
|
|
|
50
52
|
else 'explicit_new_agent',
|
|
51
53
|
['user explicitly requested recruitment'],
|
|
52
54
|
)
|
|
55
|
+
policy = await self.get_project_agent_coordination_policy(
|
|
56
|
+
actor,
|
|
57
|
+
request.personal_space_id,
|
|
58
|
+
request.personal_project_id,
|
|
59
|
+
)
|
|
60
|
+
if not policy.auto_reuse_previous_agent:
|
|
61
|
+
return [], rows, 'manual_agent_selection', [
|
|
62
|
+
'project policy requires an explicit @Agent selection',
|
|
63
|
+
]
|
|
53
64
|
normalized_work_kind = request.work_kind.casefold()
|
|
54
65
|
required = {item.casefold() for item in request.required_capabilities}
|
|
55
|
-
|
|
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]
|
|
84
|
+
continuity, project_history = await self._rank_project_continuity(
|
|
85
|
+
continuity_pool,
|
|
86
|
+
request,
|
|
87
|
+
)
|
|
88
|
+
if continuity:
|
|
89
|
+
selected = continuity[0]
|
|
90
|
+
remaining = [row for row in candidates or eligible if row['agent_id'] != selected['agent_id']]
|
|
91
|
+
return [selected], [selected, *remaining], 'project_continuity', [
|
|
92
|
+
self._project_history_match_basis(
|
|
93
|
+
project_history.get(selected['agent_id']),
|
|
94
|
+
),
|
|
95
|
+
*self._automatic_selection_basis(selected, request),
|
|
96
|
+
]
|
|
56
97
|
if not candidates:
|
|
57
98
|
unavailable_match = any(
|
|
58
99
|
not self._agent_client_allowed(row, request.source_application)
|
|
@@ -75,6 +116,21 @@ class StoreProjectAgentTaskStaffing:
|
|
|
75
116
|
'matching Agent is unavailable to source client: '
|
|
76
117
|
f'{request.source_application or "other"}',
|
|
77
118
|
]
|
|
119
|
+
if len(eligible) == 1:
|
|
120
|
+
return [eligible[0]], eligible, 'sole_active_assignment', [
|
|
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),
|
|
133
|
+
]
|
|
78
134
|
return [], rows, 'no_match', ['no active assignment matched exactly']
|
|
79
135
|
selected = candidates[0]
|
|
80
136
|
if normalized_work_kind in {
|
|
@@ -93,8 +149,24 @@ class StoreProjectAgentTaskStaffing:
|
|
|
93
149
|
)
|
|
94
150
|
if history_basis:
|
|
95
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)
|
|
96
154
|
return [selected], candidates, reason, match_basis
|
|
97
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
|
+
|
|
98
170
|
async def _rank_agent_candidates(self, rows, request):
|
|
99
171
|
"""Rank already source-eligible assignments with task-history continuity."""
|
|
100
172
|
|
|
@@ -106,6 +178,8 @@ class StoreProjectAgentTaskStaffing:
|
|
|
106
178
|
capabilities = {item.casefold() for item in row.get('capabilities', [])}
|
|
107
179
|
work_match = normalized_work_kind in work_kinds
|
|
108
180
|
capability_match = required.issubset(capabilities)
|
|
181
|
+
if not capability_match:
|
|
182
|
+
continue
|
|
109
183
|
if not work_match and not (required and capability_match):
|
|
110
184
|
continue
|
|
111
185
|
score = (2 if work_match else 0) + (1 if capability_match else 0)
|
|
@@ -121,13 +195,135 @@ class StoreProjectAgentTaskStaffing:
|
|
|
121
195
|
scored.sort(
|
|
122
196
|
key=lambda item: (
|
|
123
197
|
-item[0],
|
|
198
|
+
self._metric_int(item[1].get('active_task_count')),
|
|
124
199
|
*self._history_sort_key(history.get(item[1]['agent_id'])),
|
|
200
|
+
-int(self._metric_int(item[1].get('memory_revision')) > 0),
|
|
125
201
|
item[1]['assigned_at'],
|
|
126
202
|
item[1]['agent_id'],
|
|
127
203
|
)
|
|
128
204
|
)
|
|
129
205
|
return [item[1] for item in scored], history
|
|
130
206
|
|
|
207
|
+
async def _rank_project_continuity(self, rows, request):
|
|
208
|
+
"""Prefer an effective previous lead among equally qualified available roles."""
|
|
209
|
+
|
|
210
|
+
if not rows:
|
|
211
|
+
return [], {}
|
|
212
|
+
history = await self._historical_project_agent_outcomes(
|
|
213
|
+
request.personal_space_id,
|
|
214
|
+
request.personal_project_id,
|
|
215
|
+
[row['agent_id'] for row in rows],
|
|
216
|
+
)
|
|
217
|
+
candidates = [
|
|
218
|
+
row
|
|
219
|
+
for row in rows
|
|
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
|
+
)
|
|
229
|
+
) > 0
|
|
230
|
+
]
|
|
231
|
+
candidates.sort(
|
|
232
|
+
key=lambda row: (
|
|
233
|
+
*self._project_history_sort_key(history.get(row['agent_id'])),
|
|
234
|
+
row['assigned_at'],
|
|
235
|
+
row['agent_id'],
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
return candidates, history
|
|
239
|
+
|
|
240
|
+
async def _historical_project_agent_outcomes(
|
|
241
|
+
self,
|
|
242
|
+
personal_space_id,
|
|
243
|
+
personal_project_id,
|
|
244
|
+
agent_ids,
|
|
245
|
+
):
|
|
246
|
+
"""Read cross-work-kind lead history for sticky project continuity."""
|
|
247
|
+
|
|
248
|
+
if not agent_ids:
|
|
249
|
+
return {}
|
|
250
|
+
runtime = getattr(self, 'runtime', None)
|
|
251
|
+
driver = getattr(runtime, 'driver', None)
|
|
252
|
+
if driver is None:
|
|
253
|
+
return {}
|
|
254
|
+
records, _, _ = await driver.execute_query(
|
|
255
|
+
'''
|
|
256
|
+
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
257
|
+
[:HAS_PROJECT_AGENT_TASK]->
|
|
258
|
+
(task:FuliProjectAgentTask {
|
|
259
|
+
personal_space_id: $personal_space_id,
|
|
260
|
+
personal_project_id: $personal_project_id
|
|
261
|
+
})-[participant:HAS_PARTICIPANT]->
|
|
262
|
+
(agent:FuliProjectAgent)
|
|
263
|
+
WHERE agent.agent_id IN $agent_ids
|
|
264
|
+
AND (participant.role = 'lead'
|
|
265
|
+
OR task.lead_agent_id = agent.agent_id)
|
|
266
|
+
OPTIONAL MATCH (task)-[:HAS_TASK_EVENT]->
|
|
267
|
+
(event:FuliProjectAgentTaskEvent)-[:EVENT_AGENT]->(agent)
|
|
268
|
+
RETURN agent.agent_id AS agent_id,
|
|
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,
|
|
276
|
+
count(DISTINCT CASE
|
|
277
|
+
WHEN participant.status = 'completed'
|
|
278
|
+
OR event.status = 'completed'
|
|
279
|
+
THEN task.task_id END
|
|
280
|
+
) AS completed_count,
|
|
281
|
+
count(DISTINCT CASE
|
|
282
|
+
WHEN participant.status = 'failed'
|
|
283
|
+
OR event.status = 'failed'
|
|
284
|
+
THEN task.task_id END
|
|
285
|
+
) AS failed_count,
|
|
286
|
+
count(DISTINCT CASE
|
|
287
|
+
WHEN participant.status = 'cancelled'
|
|
288
|
+
OR event.status = 'cancelled'
|
|
289
|
+
THEN task.task_id END
|
|
290
|
+
) AS cancelled_count,
|
|
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,
|
|
298
|
+
max(CASE
|
|
299
|
+
WHEN participant.status = 'completed'
|
|
300
|
+
OR event.status = 'completed'
|
|
301
|
+
THEN coalesce(event.created_at, task.updated_at) END
|
|
302
|
+
) AS last_completed_at
|
|
303
|
+
''',
|
|
304
|
+
personal_space_id=personal_space_id,
|
|
305
|
+
personal_project_id=personal_project_id,
|
|
306
|
+
agent_ids=list(agent_ids),
|
|
307
|
+
routing_='r',
|
|
308
|
+
)
|
|
309
|
+
result = {}
|
|
310
|
+
for row in records or []:
|
|
311
|
+
raw = dict(row)
|
|
312
|
+
agent_id = raw.get('agent_id')
|
|
313
|
+
if not agent_id:
|
|
314
|
+
continue
|
|
315
|
+
result[agent_id] = {
|
|
316
|
+
'participation_count': self._metric_int(
|
|
317
|
+
raw.get('participation_count')
|
|
318
|
+
),
|
|
319
|
+
'completed_count': self._metric_int(raw.get('completed_count')),
|
|
320
|
+
'failed_count': self._metric_int(raw.get('failed_count')),
|
|
321
|
+
'cancelled_count': self._metric_int(raw.get('cancelled_count')),
|
|
322
|
+
'last_task_at': raw.get('last_task_at'),
|
|
323
|
+
'last_completed_at': raw.get('last_completed_at'),
|
|
324
|
+
}
|
|
325
|
+
return result
|
|
326
|
+
|
|
131
327
|
async def _historical_agent_outcomes(
|
|
132
328
|
self,
|
|
133
329
|
personal_space_id,
|
|
@@ -156,9 +352,14 @@ class StoreProjectAgentTaskStaffing:
|
|
|
156
352
|
AND toLower(task.work_kind) = toLower($work_kind)
|
|
157
353
|
OPTIONAL MATCH (task)-[:HAS_TASK_EVENT]->
|
|
158
354
|
(event:FuliProjectAgentTaskEvent)-[:EVENT_AGENT]->(agent)
|
|
159
|
-
WHERE event.status IN ['completed', 'failed', 'cancelled']
|
|
160
355
|
RETURN agent.agent_id AS agent_id,
|
|
161
|
-
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,
|
|
162
363
|
count(DISTINCT CASE
|
|
163
364
|
WHEN participant.status = 'completed'
|
|
164
365
|
OR event.status = 'completed'
|
|
@@ -174,7 +375,13 @@ class StoreProjectAgentTaskStaffing:
|
|
|
174
375
|
OR event.status = 'cancelled'
|
|
175
376
|
THEN task.task_id END
|
|
176
377
|
) AS cancelled_count,
|
|
177
|
-
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
|
|
178
385
|
''',
|
|
179
386
|
personal_space_id=personal_space_id,
|
|
180
387
|
personal_project_id=personal_project_id,
|
|
@@ -227,6 +434,56 @@ class StoreProjectAgentTaskStaffing:
|
|
|
227
434
|
-participation,
|
|
228
435
|
)
|
|
229
436
|
|
|
437
|
+
@classmethod
|
|
438
|
+
def _project_history_sort_key(cls, history):
|
|
439
|
+
history = history or {}
|
|
440
|
+
completed = cls._metric_int(history.get('completed_count'))
|
|
441
|
+
failed = cls._metric_int(history.get('failed_count'))
|
|
442
|
+
cancelled = cls._metric_int(history.get('cancelled_count'))
|
|
443
|
+
participation = cls._metric_int(history.get('participation_count'))
|
|
444
|
+
effective_time = (
|
|
445
|
+
history.get('last_completed_at')
|
|
446
|
+
if completed > 0
|
|
447
|
+
else history.get('last_task_at')
|
|
448
|
+
)
|
|
449
|
+
outcome_score = completed * 3 - failed * 2 - cancelled
|
|
450
|
+
return (
|
|
451
|
+
0 if completed > 0 else 1,
|
|
452
|
+
-cls._history_timestamp(effective_time),
|
|
453
|
+
-outcome_score,
|
|
454
|
+
-participation,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
@staticmethod
|
|
458
|
+
def _history_timestamp(value):
|
|
459
|
+
if value is None:
|
|
460
|
+
return 0.0
|
|
461
|
+
if hasattr(value, 'to_native'):
|
|
462
|
+
value = value.to_native()
|
|
463
|
+
if hasattr(value, 'timestamp'):
|
|
464
|
+
try:
|
|
465
|
+
return float(value.timestamp())
|
|
466
|
+
except (TypeError, ValueError, OSError):
|
|
467
|
+
return 0.0
|
|
468
|
+
try:
|
|
469
|
+
return datetime.fromisoformat(
|
|
470
|
+
str(value).replace('Z', '+00:00')
|
|
471
|
+
).timestamp()
|
|
472
|
+
except (TypeError, ValueError, OSError):
|
|
473
|
+
return 0.0
|
|
474
|
+
|
|
475
|
+
@classmethod
|
|
476
|
+
def _project_history_match_basis(cls, history):
|
|
477
|
+
history = history or {}
|
|
478
|
+
participation = cls._metric_int(history.get('participation_count'))
|
|
479
|
+
completed = cls._metric_int(history.get('completed_count'))
|
|
480
|
+
if completed:
|
|
481
|
+
return (
|
|
482
|
+
'last successful project lead continuity: '
|
|
483
|
+
f'{participation} prior task(s), {completed} completed'
|
|
484
|
+
)
|
|
485
|
+
return f'most recent project lead continuity: {participation} prior task(s)'
|
|
486
|
+
|
|
230
487
|
@classmethod
|
|
231
488
|
def _history_match_basis(cls, history, work_kind):
|
|
232
489
|
if not history:
|
|
@@ -297,6 +554,111 @@ class StoreProjectAgentTaskStaffing:
|
|
|
297
554
|
added.append(candidate)
|
|
298
555
|
return added
|
|
299
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
|
+
|
|
300
662
|
async def _assignment_candidates(
|
|
301
663
|
self,
|
|
302
664
|
personal_space_id,
|
|
@@ -306,7 +668,7 @@ class StoreProjectAgentTaskStaffing:
|
|
|
306
668
|
):
|
|
307
669
|
records, _, _ = await self.runtime.driver.execute_query(
|
|
308
670
|
'''
|
|
309
|
-
MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
671
|
+
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
310
672
|
[:CONTAINS_PROJECT]->
|
|
311
673
|
(:FuliPersonalProject {project_id: $personal_project_id})-
|
|
312
674
|
[:HAS_PROJECT_AGENT_ASSIGNMENT]->
|
|
@@ -314,7 +676,27 @@ class StoreProjectAgentTaskStaffing:
|
|
|
314
676
|
[:ASSIGNED_AGENT]->(agent:FuliProjectAgent {status: 'active'})
|
|
315
677
|
WHERE agent.agent_type = 'durable'
|
|
316
678
|
OR ($include_temporary = true AND agent.agent_type = 'temporary')
|
|
317
|
-
|
|
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
|
|
318
700
|
ORDER BY assignment.assigned_at, agent.agent_id
|
|
319
701
|
''',
|
|
320
702
|
personal_space_id=personal_space_id,
|
|
@@ -343,6 +725,8 @@ class StoreProjectAgentTaskStaffing:
|
|
|
343
725
|
),
|
|
344
726
|
'profile': profile,
|
|
345
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,
|
|
346
730
|
})
|
|
347
731
|
return result
|
|
348
732
|
|
|
@@ -405,5 +789,10 @@ class StoreProjectAgentTaskStaffing:
|
|
|
405
789
|
if plan.enabled and len(participants) < 2:
|
|
406
790
|
raise HTTPException(
|
|
407
791
|
status_code=422,
|
|
408
|
-
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
|
+
),
|
|
409
798
|
)
|