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
|
@@ -134,13 +134,19 @@ class StoreProjectAgents:
|
|
|
134
134
|
if request.profile.agent_type == 'temporary'
|
|
135
135
|
else 'reviewed_agent'
|
|
136
136
|
)
|
|
137
|
-
await self.runtime.driver.execute_query(
|
|
137
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
138
138
|
'''
|
|
139
139
|
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})
|
|
140
140
|
MERGE (agent:FuliProjectAgent {id: $id})
|
|
141
141
|
ON CREATE SET agent.agent_id = $agent_id,
|
|
142
142
|
agent.created_at = $updated_at,
|
|
143
143
|
agent.system_managed = false
|
|
144
|
+
// Acquire the node write lock before reading lifecycle status so a
|
|
145
|
+
// concurrent archive cannot be lost to this compatibility upsert.
|
|
146
|
+
SET agent._profile_upsert_lock = true
|
|
147
|
+
REMOVE agent._profile_upsert_lock
|
|
148
|
+
WITH space, agent
|
|
149
|
+
WHERE agent.status IS NULL OR agent.status <> 'archived'
|
|
144
150
|
SET agent.profile_json = $profile_json,
|
|
145
151
|
agent.name = $name,
|
|
146
152
|
agent.occupation_emoji = $occupation_emoji,
|
|
@@ -175,20 +181,37 @@ class StoreProjectAgents:
|
|
|
175
181
|
recruitment_id=recruitment_id,
|
|
176
182
|
updated_at=updated_at,
|
|
177
183
|
)
|
|
184
|
+
if not records:
|
|
185
|
+
raise HTTPException(
|
|
186
|
+
status_code=409,
|
|
187
|
+
detail=(
|
|
188
|
+
'archived Project Agents require a dedicated restore operation'
|
|
189
|
+
),
|
|
190
|
+
)
|
|
178
191
|
if request.personal_project_id:
|
|
179
|
-
await self._upsert_legacy_assignment(request, updated_at)
|
|
192
|
+
if not await self._upsert_legacy_assignment(request, updated_at):
|
|
193
|
+
raise HTTPException(
|
|
194
|
+
status_code=409,
|
|
195
|
+
detail=(
|
|
196
|
+
'archived Project Agents require a dedicated '
|
|
197
|
+
'restore operation'
|
|
198
|
+
),
|
|
199
|
+
)
|
|
180
200
|
return await self.get_project_agent(
|
|
181
201
|
actor,
|
|
182
202
|
request.personal_space_id,
|
|
183
203
|
request.personal_project_id,
|
|
184
204
|
request.agent_id,
|
|
205
|
+
include_inactive_project_assignment=bool(
|
|
206
|
+
request.personal_project_id
|
|
207
|
+
),
|
|
185
208
|
)
|
|
186
209
|
|
|
187
210
|
async def _upsert_legacy_assignment(
|
|
188
211
|
self,
|
|
189
212
|
request: ProjectAgentUpsert,
|
|
190
213
|
updated_at,
|
|
191
|
-
) ->
|
|
214
|
+
) -> bool:
|
|
192
215
|
assignment_id = stable_uuid(
|
|
193
216
|
self.settings.provider_id,
|
|
194
217
|
request.personal_space_id,
|
|
@@ -197,7 +220,7 @@ class StoreProjectAgents:
|
|
|
197
220
|
request.personal_project_id,
|
|
198
221
|
request.agent_id,
|
|
199
222
|
)
|
|
200
|
-
await self.runtime.driver.execute_query(
|
|
223
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
201
224
|
'''
|
|
202
225
|
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
203
226
|
[:CONTAINS_PROJECT]->
|
|
@@ -205,20 +228,62 @@ class StoreProjectAgents:
|
|
|
205
228
|
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
206
229
|
[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
207
230
|
(agent:FuliProjectAgent {agent_id: $agent_id})
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
231
|
+
SET agent._task_lifecycle_lock = true
|
|
232
|
+
REMOVE agent._task_lifecycle_lock
|
|
233
|
+
WITH project, agent
|
|
234
|
+
WHERE agent.status <> 'archived'
|
|
235
|
+
OPTIONAL MATCH (project)-[:HAS_PROJECT_AGENT_ASSIGNMENT]->
|
|
236
|
+
(active_assignment:FuliProjectAgentAssignment)-
|
|
237
|
+
[:ASSIGNED_AGENT]->(agent)
|
|
238
|
+
WHERE active_assignment.status = 'active'
|
|
239
|
+
OPTIONAL MATCH (project)-[:HAS_PROJECT_AGENT_ASSIGNMENT]->
|
|
240
|
+
(handed_off_assignment:FuliProjectAgentAssignment)-
|
|
241
|
+
[:ASSIGNED_AGENT]->(agent)
|
|
242
|
+
WHERE handed_off_assignment.status = 'ended'
|
|
243
|
+
AND handed_off_assignment.replaced_by_assignment_id IS NOT NULL
|
|
244
|
+
WITH project, agent,
|
|
245
|
+
count(DISTINCT active_assignment) AS active_assignment_count,
|
|
246
|
+
count(DISTINCT CASE
|
|
247
|
+
WHEN active_assignment.id = $assignment_id
|
|
248
|
+
THEN active_assignment
|
|
249
|
+
END) AS legacy_assignment_count,
|
|
250
|
+
count(DISTINCT handed_off_assignment)
|
|
251
|
+
AS handed_off_assignment_count
|
|
252
|
+
// An audited handoff is terminal for the backwards-compatible
|
|
253
|
+
// upsert path. Reassignment must use the explicit assignment API.
|
|
254
|
+
// An already-active legacy assignment may still refresh its
|
|
255
|
+
// profile-backed fields even when another historical role moved.
|
|
256
|
+
FOREACH (_ IN CASE
|
|
257
|
+
WHEN legacy_assignment_count > 0 OR (
|
|
258
|
+
active_assignment_count = 0
|
|
259
|
+
AND handed_off_assignment_count = 0
|
|
260
|
+
)
|
|
261
|
+
THEN [1] ELSE [] END |
|
|
262
|
+
MERGE (assignment:FuliProjectAgentAssignment {id: $assignment_id})
|
|
263
|
+
ON CREATE SET assignment.assignment_id = $assignment_id,
|
|
264
|
+
assignment.assigned_at = $updated_at,
|
|
265
|
+
assignment.revision = 0,
|
|
266
|
+
assignment.reason = 'legacy project Agent profile'
|
|
267
|
+
SET assignment.revision = CASE
|
|
268
|
+
WHEN assignment.status IS NOT NULL
|
|
269
|
+
AND assignment.status <> 'active'
|
|
270
|
+
THEN coalesce(assignment.revision, 0) + 1
|
|
271
|
+
ELSE coalesce(assignment.revision, 0)
|
|
272
|
+
END,
|
|
273
|
+
assignment.ended_at = NULL,
|
|
274
|
+
assignment.end_reason = NULL,
|
|
275
|
+
assignment.replaced_by_assignment_id = NULL
|
|
276
|
+
SET assignment.responsibility = $responsibility,
|
|
277
|
+
assignment.work_kinds = $work_kinds,
|
|
278
|
+
assignment.capabilities = $capabilities,
|
|
279
|
+
assignment.model_strategy_json = $model_strategy_json,
|
|
280
|
+
assignment.executor_policy_json = $executor_policy_json,
|
|
281
|
+
assignment.status = 'active',
|
|
282
|
+
assignment.updated_at = $updated_at
|
|
283
|
+
MERGE (project)-[:HAS_PROJECT_AGENT_ASSIGNMENT]->(assignment)
|
|
284
|
+
MERGE (assignment)-[:ASSIGNED_AGENT]->(agent)
|
|
285
|
+
)
|
|
286
|
+
RETURN agent.agent_id AS agent_id
|
|
222
287
|
''',
|
|
223
288
|
personal_space_id=request.personal_space_id,
|
|
224
289
|
personal_project_id=request.personal_project_id,
|
|
@@ -236,6 +301,7 @@ class StoreProjectAgents:
|
|
|
236
301
|
),
|
|
237
302
|
updated_at=updated_at,
|
|
238
303
|
)
|
|
304
|
+
return bool(records)
|
|
239
305
|
|
|
240
306
|
async def list_project_agents(
|
|
241
307
|
self,
|
|
@@ -273,6 +339,8 @@ class StoreProjectAgents:
|
|
|
273
339
|
personal_space_id: str,
|
|
274
340
|
personal_project_id: str | None,
|
|
275
341
|
agent_id: str,
|
|
342
|
+
*,
|
|
343
|
+
include_inactive_project_assignment: bool = False,
|
|
276
344
|
) -> ProjectAgentRecord:
|
|
277
345
|
self._require_personal()
|
|
278
346
|
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
@@ -285,13 +353,22 @@ class StoreProjectAgents:
|
|
|
285
353
|
)
|
|
286
354
|
records = await self._project_agent_rows(
|
|
287
355
|
personal_space_id,
|
|
288
|
-
personal_project_id=
|
|
356
|
+
personal_project_id=(
|
|
357
|
+
None
|
|
358
|
+
if include_inactive_project_assignment
|
|
359
|
+
else personal_project_id
|
|
360
|
+
),
|
|
289
361
|
agent_id=agent_id,
|
|
290
362
|
status=None,
|
|
291
363
|
capability=None,
|
|
292
364
|
)
|
|
293
365
|
if not records:
|
|
294
366
|
raise HTTPException(status_code=404, detail='project Agent not found')
|
|
367
|
+
if personal_project_id and not any(
|
|
368
|
+
dict(item).get('personal_project_id') == personal_project_id
|
|
369
|
+
for item in records[0].get('assignment_rows') or []
|
|
370
|
+
):
|
|
371
|
+
raise HTTPException(status_code=404, detail='project Agent not found')
|
|
295
372
|
return self._project_agent_from_row(
|
|
296
373
|
records[0],
|
|
297
374
|
personal_space_id,
|
|
@@ -322,6 +399,11 @@ class StoreProjectAgents:
|
|
|
322
399
|
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
323
400
|
[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
324
401
|
(agent:FuliProjectAgent {agent_id: $agent_id})
|
|
402
|
+
// Serialize task-participant creation with lifecycle termination.
|
|
403
|
+
// The lock must be taken before the open-work snapshot is read.
|
|
404
|
+
SET agent._task_lifecycle_lock = true
|
|
405
|
+
REMOVE agent._task_lifecycle_lock
|
|
406
|
+
WITH space, agent
|
|
325
407
|
OPTIONAL MATCH (space)-[:HAS_PROJECT_AGENT_TASK]->
|
|
326
408
|
(task:FuliProjectAgentTask)-
|
|
327
409
|
[participant:HAS_PARTICIPANT]->(agent)
|
|
@@ -337,7 +419,7 @@ class StoreProjectAgents:
|
|
|
337
419
|
]] AS open_tasks
|
|
338
420
|
WHERE agent.status = 'archived' OR size(open_tasks) = 0
|
|
339
421
|
SET agent.status = 'archived',
|
|
340
|
-
agent.archive_reason = $reason,
|
|
422
|
+
agent.archive_reason = coalesce(agent.archive_reason, $reason),
|
|
341
423
|
agent.archived_at = coalesce(agent.archived_at, $updated_at),
|
|
342
424
|
agent.updated_at = $updated_at
|
|
343
425
|
WITH space, agent
|
|
@@ -355,7 +437,7 @@ class StoreProjectAgents:
|
|
|
355
437
|
''',
|
|
356
438
|
personal_space_id=personal_space_id,
|
|
357
439
|
agent_id=agent_id,
|
|
358
|
-
reason=reason,
|
|
440
|
+
reason=reason.strip(),
|
|
359
441
|
updated_at=now_utc(),
|
|
360
442
|
)
|
|
361
443
|
if not records:
|
|
@@ -420,30 +502,64 @@ class StoreProjectAgents:
|
|
|
420
502
|
WITH space, agent, collect(DISTINCT {
|
|
421
503
|
assignment: assignment,
|
|
422
504
|
personal_project_id: project.project_id
|
|
423
|
-
}) AS
|
|
505
|
+
}) AS all_assignment_rows
|
|
506
|
+
WITH space, agent,
|
|
507
|
+
[item IN all_assignment_rows
|
|
508
|
+
WHERE $personal_project_id IS NULL
|
|
509
|
+
OR item.personal_project_id = $personal_project_id]
|
|
510
|
+
AS assignment_rows
|
|
424
511
|
OPTIONAL MATCH (space)-[:HAS_PROJECT_AGENT_TASK]->
|
|
425
512
|
(task:FuliProjectAgentTask)-
|
|
426
513
|
[participant:HAS_PARTICIPANT]->(agent)
|
|
427
514
|
WITH space, agent, assignment_rows,
|
|
428
515
|
collect(DISTINCT {
|
|
429
516
|
task_id: task.task_id,
|
|
517
|
+
personal_project_id: task.personal_project_id,
|
|
430
518
|
status: participant.status,
|
|
431
519
|
updated_at: participant.updated_at,
|
|
432
520
|
source_application: task.source_application
|
|
433
|
-
}) AS
|
|
434
|
-
collect(DISTINCT
|
|
521
|
+
}) AS all_task_rows,
|
|
522
|
+
collect(DISTINCT {
|
|
523
|
+
personal_project_id: task.personal_project_id,
|
|
524
|
+
source_application: task.source_application
|
|
525
|
+
}) AS all_task_client_rows
|
|
526
|
+
WITH space, agent, assignment_rows,
|
|
527
|
+
[item IN all_task_rows
|
|
528
|
+
WHERE $personal_project_id IS NULL
|
|
529
|
+
OR item.personal_project_id = $personal_project_id]
|
|
530
|
+
AS task_rows,
|
|
531
|
+
[item IN all_task_client_rows
|
|
532
|
+
WHERE ($personal_project_id IS NULL
|
|
533
|
+
OR item.personal_project_id = $personal_project_id)
|
|
534
|
+
AND item.source_application IS NOT NULL]
|
|
535
|
+
AS task_client_rows
|
|
435
536
|
OPTIONAL MATCH (space)-[:HAS_PROJECT_AGENT_TASK]->
|
|
436
|
-
(:FuliProjectAgentTask)-[:HAS_TASK_EVENT]->
|
|
537
|
+
(event_task:FuliProjectAgentTask)-[:HAS_TASK_EVENT]->
|
|
437
538
|
(event:FuliProjectAgentTaskEvent)-[:EVENT_AGENT]->(agent)
|
|
438
539
|
WITH agent,
|
|
439
540
|
assignment_rows,
|
|
440
541
|
task_rows,
|
|
441
|
-
|
|
442
|
-
collect(DISTINCT
|
|
542
|
+
task_client_rows,
|
|
543
|
+
collect(DISTINCT {
|
|
544
|
+
personal_project_id: event_task.personal_project_id,
|
|
545
|
+
source_application: event.source_application
|
|
546
|
+
}) AS all_event_client_rows
|
|
547
|
+
WITH agent,
|
|
548
|
+
assignment_rows,
|
|
549
|
+
task_rows,
|
|
550
|
+
task_client_rows,
|
|
551
|
+
[item IN all_event_client_rows
|
|
552
|
+
WHERE ($personal_project_id IS NULL
|
|
553
|
+
OR item.personal_project_id = $personal_project_id)
|
|
554
|
+
AND item.source_application IS NOT NULL]
|
|
555
|
+
AS event_client_rows
|
|
443
556
|
RETURN agent,
|
|
444
557
|
assignment_rows,
|
|
445
558
|
task_rows,
|
|
446
|
-
|
|
559
|
+
[item IN task_client_rows | item.source_application]
|
|
560
|
+
+ [item IN event_client_rows | item.source_application]
|
|
561
|
+
AS observed_clients,
|
|
562
|
+
task_client_rows + event_client_rows AS observed_client_rows
|
|
447
563
|
ORDER BY CASE agent.agent_type
|
|
448
564
|
WHEN 'coordinator' THEN 0
|
|
449
565
|
WHEN 'hr' THEN 1
|
|
@@ -500,6 +616,10 @@ class StoreProjectAgents:
|
|
|
500
616
|
MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
501
617
|
[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
502
618
|
(agent:FuliProjectAgent {agent_id: $agent_id})
|
|
619
|
+
SET agent._task_lifecycle_lock = true
|
|
620
|
+
REMOVE agent._task_lifecycle_lock
|
|
621
|
+
WITH project, agent
|
|
622
|
+
WHERE agent.status = 'active'
|
|
503
623
|
MERGE (assignment:FuliProjectAgentAssignment {id: $assignment_id})
|
|
504
624
|
ON CREATE SET assignment.assignment_id = $assignment_id,
|
|
505
625
|
assignment.payload_hash = $payload_hash,
|
|
@@ -548,7 +668,10 @@ class StoreProjectAgents:
|
|
|
548
668
|
updated_at=updated_at,
|
|
549
669
|
)
|
|
550
670
|
if not records:
|
|
551
|
-
raise HTTPException(
|
|
671
|
+
raise HTTPException(
|
|
672
|
+
status_code=409,
|
|
673
|
+
detail='project Agent is not active or is unavailable',
|
|
674
|
+
)
|
|
552
675
|
assignment = dict(records[0]['assignment'])
|
|
553
676
|
if assignment.get('payload_hash') != payload_hash:
|
|
554
677
|
raise HTTPException(
|
|
@@ -634,13 +757,16 @@ class StoreProjectAgents:
|
|
|
634
757
|
(assignment:FuliProjectAgentAssignment {
|
|
635
758
|
assignment_id: $assignment_id
|
|
636
759
|
})-[:ASSIGNED_AGENT]->(agent:FuliProjectAgent)
|
|
760
|
+
SET agent._task_lifecycle_lock = true
|
|
761
|
+
REMOVE agent._task_lifecycle_lock
|
|
762
|
+
WITH project, assignment, agent
|
|
637
763
|
WHERE assignment.status = 'active'
|
|
638
|
-
AND assignment.revision = $expected_revision
|
|
764
|
+
AND coalesce(assignment.revision, 0) = $expected_revision
|
|
639
765
|
SET assignment.status = 'ended',
|
|
640
766
|
assignment.end_reason = $reason,
|
|
641
767
|
assignment.ended_at = $updated_at,
|
|
642
768
|
assignment.updated_at = $updated_at,
|
|
643
|
-
assignment.revision = assignment.revision + 1
|
|
769
|
+
assignment.revision = coalesce(assignment.revision, 0) + 1
|
|
644
770
|
RETURN assignment, project.project_id AS personal_project_id,
|
|
645
771
|
agent.agent_id AS agent_id
|
|
646
772
|
''',
|
|
@@ -683,7 +809,7 @@ class StoreProjectAgents:
|
|
|
683
809
|
space,
|
|
684
810
|
request.personal_project_id,
|
|
685
811
|
request.replacement_agent_id,
|
|
686
|
-
require_active=
|
|
812
|
+
require_active=False,
|
|
687
813
|
allow_unassigned=True,
|
|
688
814
|
)
|
|
689
815
|
replacement_id = stable_uuid(
|
|
@@ -708,16 +834,29 @@ class StoreProjectAgents:
|
|
|
708
834
|
(replacement_agent:FuliProjectAgent {
|
|
709
835
|
agent_id: $replacement_agent_id
|
|
710
836
|
})
|
|
837
|
+
WITH project, ended, ended_agent, replacement_agent,
|
|
838
|
+
CASE
|
|
839
|
+
WHEN ended_agent.agent_id <= replacement_agent.agent_id
|
|
840
|
+
THEN [ended_agent, replacement_agent]
|
|
841
|
+
ELSE [replacement_agent, ended_agent]
|
|
842
|
+
END AS lifecycle_agents
|
|
843
|
+
UNWIND lifecycle_agents AS lifecycle_agent
|
|
844
|
+
SET lifecycle_agent._task_lifecycle_lock = true
|
|
845
|
+
REMOVE lifecycle_agent._task_lifecycle_lock
|
|
846
|
+
WITH project, ended, ended_agent, replacement_agent,
|
|
847
|
+
collect(lifecycle_agent) AS locked_agents
|
|
711
848
|
OPTIONAL MATCH (existing_replacement:FuliProjectAgentAssignment {
|
|
712
849
|
id: $replacement_id
|
|
713
850
|
})
|
|
714
851
|
WITH project, ended, ended_agent, replacement_agent,
|
|
715
852
|
existing_replacement
|
|
716
853
|
WHERE existing_replacement IS NOT NULL
|
|
717
|
-
OR (
|
|
718
|
-
AND
|
|
854
|
+
OR (replacement_agent.status = 'active'
|
|
855
|
+
AND ended_agent.status = 'active'
|
|
856
|
+
AND ended.status = 'active'
|
|
857
|
+
AND coalesce(ended.revision, 0) = $expected_revision)
|
|
719
858
|
OR (ended.status = 'ended'
|
|
720
|
-
AND ended.revision = $expected_revision + 1
|
|
859
|
+
AND coalesce(ended.revision, 0) = $expected_revision + 1
|
|
721
860
|
AND ended.replaced_by_assignment_id = $replacement_id)
|
|
722
861
|
MERGE (replacement:FuliProjectAgentAssignment {id: $replacement_id})
|
|
723
862
|
ON CREATE SET replacement.assignment_id = $replacement_id,
|
|
@@ -737,10 +876,13 @@ class StoreProjectAgents:
|
|
|
737
876
|
replacement.updated_at = $updated_at
|
|
738
877
|
WITH project, ended, ended_agent, replacement_agent, replacement,
|
|
739
878
|
replacement.payload_hash = $payload_hash AS payload_matches,
|
|
740
|
-
(
|
|
741
|
-
AND
|
|
879
|
+
(replacement_agent.status = 'active'
|
|
880
|
+
AND ended_agent.status = 'active'
|
|
881
|
+
AND ended.status = 'active'
|
|
882
|
+
AND coalesce(ended.revision, 0) = $expected_revision)
|
|
883
|
+
AS can_apply,
|
|
742
884
|
(ended.status = 'ended'
|
|
743
|
-
AND ended.revision = $expected_revision + 1
|
|
885
|
+
AND coalesce(ended.revision, 0) = $expected_revision + 1
|
|
744
886
|
AND ended.replaced_by_assignment_id = $replacement_id)
|
|
745
887
|
AS exact_replay
|
|
746
888
|
FOREACH (_ IN CASE
|
|
@@ -752,7 +894,7 @@ class StoreProjectAgents:
|
|
|
752
894
|
ended.ended_at = $updated_at,
|
|
753
895
|
ended.updated_at = $updated_at,
|
|
754
896
|
ended.replaced_by_assignment_id = $replacement_id,
|
|
755
|
-
ended.revision = ended.revision + 1
|
|
897
|
+
ended.revision = coalesce(ended.revision, 0) + 1
|
|
756
898
|
)
|
|
757
899
|
RETURN ended, replacement,
|
|
758
900
|
ended_agent.agent_id AS ended_agent_id,
|
|
@@ -849,7 +991,14 @@ class StoreProjectAgents:
|
|
|
849
991
|
value = dict(item)
|
|
850
992
|
assignment_raw = value.get('assignment')
|
|
851
993
|
project_id = value.get('personal_project_id')
|
|
852
|
-
if
|
|
994
|
+
if (
|
|
995
|
+
assignment_raw is None
|
|
996
|
+
or not project_id
|
|
997
|
+
or (
|
|
998
|
+
projection_project_id is not None
|
|
999
|
+
and project_id != projection_project_id
|
|
1000
|
+
)
|
|
1001
|
+
):
|
|
853
1002
|
continue
|
|
854
1003
|
assignments.append(
|
|
855
1004
|
self._assignment(
|
|
@@ -871,7 +1020,13 @@ class StoreProjectAgents:
|
|
|
871
1020
|
tasks = [
|
|
872
1021
|
dict(value)
|
|
873
1022
|
for value in (row.get('task_rows') or [])
|
|
874
|
-
if value
|
|
1023
|
+
if value
|
|
1024
|
+
and value.get('task_id')
|
|
1025
|
+
and value.get('status') in nonterminal
|
|
1026
|
+
and (
|
|
1027
|
+
projection_project_id is None
|
|
1028
|
+
or value.get('personal_project_id') == projection_project_id
|
|
1029
|
+
)
|
|
875
1030
|
]
|
|
876
1031
|
priority = {
|
|
877
1032
|
'running': 0,
|
|
@@ -888,13 +1043,26 @@ class StoreProjectAgents:
|
|
|
888
1043
|
if item.get('updated_at') else 0),
|
|
889
1044
|
)
|
|
890
1045
|
)
|
|
891
|
-
work_status =
|
|
892
|
-
|
|
893
|
-
|
|
1046
|
+
work_status = (
|
|
1047
|
+
'ended'
|
|
1048
|
+
if profile.status == 'archived'
|
|
1049
|
+
else (tasks[0]['status'] if tasks else 'idle')
|
|
1050
|
+
)
|
|
1051
|
+
observed_client_rows = row.get('observed_client_rows')
|
|
1052
|
+
if projection_project_id is not None:
|
|
1053
|
+
observed_values = [
|
|
1054
|
+
dict(value).get('source_application')
|
|
1055
|
+
for value in observed_client_rows or []
|
|
1056
|
+
if dict(value).get('personal_project_id') == projection_project_id
|
|
1057
|
+
]
|
|
1058
|
+
else:
|
|
1059
|
+
observed_values = row.get('observed_clients') or []
|
|
894
1060
|
observed_clients = sorted({
|
|
895
1061
|
value
|
|
896
|
-
for value in
|
|
897
|
-
if value in {
|
|
1062
|
+
for value in observed_values
|
|
1063
|
+
if value in {
|
|
1064
|
+
'codex', 'claude', 'claude_code', 'cursor', 'kiro', 'other'
|
|
1065
|
+
}
|
|
898
1066
|
})
|
|
899
1067
|
return ProjectAgentRecord(
|
|
900
1068
|
agent_id=raw['agent_id'],
|