fuli-context 0.11.0 → 0.12.0
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/dist/web/assets/AboutPage-BAD0iPVX.js +1 -0
- package/dist/web/assets/AgentConversations-1iGTd4sz.js +2 -0
- package/dist/web/assets/AgentHand-D_RafKOM.js +1 -0
- package/dist/web/assets/AgentName-B5LKQ09q.js +1 -0
- package/dist/web/assets/AgentProfilePage-CioOdkBM.js +1 -0
- package/dist/web/assets/AgentsDirectoryPage--1A9nvNA.js +1 -0
- package/dist/web/assets/AgentsDirectoryPage-C7OUQH9v.css +1 -0
- package/dist/web/assets/ConnectionsPage-Tr2rRVOZ.js +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-1QevD4Qv.js +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-B0vQIgwA.js +3 -0
- package/dist/web/assets/EmployeeWorkbenchPage-BQes4L1f.css +1 -0
- package/dist/web/assets/KnowledgeEditDialog-ClnrfPZi.js +1 -0
- package/dist/web/assets/KnowledgeInspector-Te1hJZbx.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CCCrWl6Y.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-ccgscrPd.css +1 -0
- package/dist/web/assets/KnowledgePage-DGOcBwr8.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-cKn1NCRe.js +4 -0
- package/dist/web/assets/NotFoundPage-fetSnp0N.js +1 -0
- package/dist/web/assets/OverviewPage-BD4yY2Xz.js +1 -0
- package/dist/web/assets/PersonalProfilePage-DGJVP_VB.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-D25HYMgD.js +1 -0
- package/dist/web/assets/ProjectAgentFirstTask-BcxqmKXQ.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-BWDJBESb.js +12 -0
- package/dist/web/assets/{ProjectAgentsPage-DNLNjYp-.css → ProjectAgentsPage-DWGkgIsr.css} +1 -1
- package/dist/web/assets/ProjectScopePicker-yodM7lF_.js +1 -0
- package/dist/web/assets/PublicProjectsPage-CyNxVzW3.js +1 -0
- package/dist/web/assets/ReviewPage-Cy0Rt4CO.js +1 -0
- package/dist/web/assets/RoundtablesPage-BjVhvwMc.js +1 -0
- package/dist/web/assets/SettingsPage-CCZqZtnH.js +1 -0
- package/dist/web/assets/SettingsPage-DYz5bTwa.css +1 -0
- package/dist/web/assets/SharedJefaReportPage-C56kPciB.js +1 -0
- package/dist/web/assets/UiDialog-CvDxg3B1.js +1 -0
- package/dist/web/assets/UiDisclosure-D0iMTplF.js +1 -0
- package/dist/web/assets/UiSegmented-DIJW6wv3.js +1 -0
- package/dist/web/assets/UiSelect-CMR0QqF6.js +1 -0
- package/dist/web/assets/WritingTastePage-QgNzssfF.js +1 -0
- package/dist/web/assets/agent-pins-BcJ_qr8n.js +1 -0
- package/dist/web/assets/agent-work-summary-C-yW7x7A.js +1 -0
- package/dist/web/assets/{attention-store-BfxtL9EV.js → attention-store-B1DI8wmd.js} +1 -1
- package/dist/web/assets/{catalog-6SN9i3WR.js → catalog-DbXv0_bR.js} +1 -1
- package/dist/web/assets/client-CR-W5Wzp.js +1 -0
- package/dist/web/assets/console-D7GHOMEC.js +1 -0
- package/dist/web/assets/conversation-launcher-settings-Dj2GMzCI.js +1 -0
- package/dist/web/assets/{i18n-CCMR4gDS.js → i18n-CVFhT_k7.js} +3 -3
- package/dist/web/assets/{identity-C1eN-FSB.js → identity-CM6Dmy00.js} +1 -1
- package/dist/web/assets/index-CJ6Jmok9.css +1 -0
- package/dist/web/assets/index-CRa3GnYi.js +5 -0
- package/dist/web/assets/judgment-types-CJBEyJs1.js +1 -0
- package/dist/web/assets/model-WFhQZ3v_.js +1 -0
- package/dist/web/assets/{personal-profile-graph-BD1Y6TDS.js → personal-profile-graph-BwCh2M9G.js} +1 -1
- package/dist/web/assets/shared-report-CeIvrL9a.js +1 -0
- package/dist/web/assets/{agent-work-summary-LeW-Al_S.js → task-evidence-DTiPCs7r.js} +1 -1
- package/dist/web/assets/useAgentRoster-DCArHlxK.js +1 -0
- package/dist/web/assets/{useApi-CROJJdhE-DlOiu8lP.js → useApi-CROJJdhE-BYhbjaZB.js} +1 -1
- package/dist/web/assets/vue-router-DaxrQwX3.js +1 -0
- package/dist/web/index.html +16 -12
- package/docs/agent-roundtable.md +17 -1
- package/docs/employee-agents.md +1 -1
- package/docs/judgment-and-autonomy.md +33 -0
- package/graph-provider/fuli_graph/ai_review.py +154 -0
- package/graph-provider/fuli_graph/common_knowledge.py +12 -0
- package/graph-provider/fuli_graph/graph_models.py +14 -0
- package/graph-provider/fuli_graph/graph_query.py +217 -0
- package/graph-provider/fuli_graph/knowledge_audit.py +6 -0
- package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +6 -0
- package/graph-provider/fuli_graph/knowledge_management.py +23 -0
- package/graph-provider/fuli_graph/knowledge_review.py +383 -4
- package/graph-provider/fuli_graph/knowledge_review_models.py +61 -2
- package/graph-provider/fuli_graph/personal_global_preferences.py +5 -1
- package/graph-provider/fuli_graph/preference_conflicts.py +91 -1
- package/graph-provider/fuli_graph/project_agent_context_models.py +5 -1
- package/graph-provider/fuli_graph/project_agent_delegations.py +118 -0
- package/graph-provider/fuli_graph/project_agent_employee_numbers.py +41 -0
- package/graph-provider/fuli_graph/project_agent_memory_routes.py +13 -0
- package/graph-provider/fuli_graph/project_agent_models.py +1 -0
- package/graph-provider/fuli_graph/project_agent_task_models.py +2 -0
- package/graph-provider/fuli_graph/project_agent_team_growth.py +2 -2
- package/graph-provider/fuli_graph/project_knowledge.py +41 -0
- package/graph-provider/fuli_graph/runtime.py +15 -0
- package/graph-provider/fuli_graph/store_agent_verification.py +12 -5
- package/graph-provider/fuli_graph/store_knowledge.py +98 -58
- package/graph-provider/fuli_graph/store_project_agent_attention.py +1 -1
- package/graph-provider/fuli_graph/store_project_agent_context.py +52 -4
- package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +16 -1
- package/graph-provider/fuli_graph/store_project_agent_task_activity.py +26 -0
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +5 -3
- package/graph-provider/fuli_graph/store_project_agents.py +31 -5
- package/graph-provider/fuli_graph/system_agents.py +23 -2
- package/graph-provider/fuli_graph/system_hr_identity.py +19 -5
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +3 -2
- package/src/agent-roundtable/service.js +34 -5
- package/src/agent-roundtable/wake.js +2 -1
- package/src/agent-tools/graph-definitions.js +2 -0
- package/src/agent-tools/graph-handlers.js +7 -0
- package/src/agent-tools/interface-catalog.js +6 -0
- package/src/agent-tools/judgment-definitions.js +12 -0
- package/src/agents/claude-code/wake.js +7 -0
- package/src/agents/codex/wake.js +7 -0
- package/src/agents/identity-receipt.js +20 -2
- package/src/employees/catalog/tonborg.json +20 -0
- package/src/employees/manifest.js +2 -2
- package/src/employees/recruitment.js +20 -1
- package/src/graphiti/agent-collaboration.js +8 -2
- package/src/graphiti/federated-application.js +11 -2
- package/src/graphiti/knowledge-review.js +7 -3
- package/src/graphiti/project-agent-mapping.js +4 -0
- package/src/graphiti/project-agent-task-entry.js +23 -5
- package/src/graphiti/project-agent-team-context.js +4 -1
- package/src/graphiti/provider-client.js +9 -0
- package/src/graphiti/task-entry-preferences.js +17 -4
- package/src/graphiti/task-entry-project-scope.js +67 -0
- package/src/http/api-router.js +2 -0
- package/src/judgment/acceptance.js +91 -0
- package/src/judgment/engine.js +168 -0
- package/src/judgment/http.js +24 -0
- package/src/judgment/model.js +24 -0
- package/src/judgment/routing.js +96 -0
- package/src/judgment/scheduler.js +25 -0
- package/src/judgment/service.js +66 -0
- package/src/judgment/store.js +123 -0
- package/src/mcp/session-id.js +1 -0
- package/src/mcp/tool-annotations.js +4 -2
- package/src/server.js +9 -2
- package/dist/web/assets/AboutPage-CKq4IRHd.js +0 -1
- package/dist/web/assets/AgentConversations-DfXiFvBN.js +0 -2
- package/dist/web/assets/AgentHand-BBM-sKh2.js +0 -1
- package/dist/web/assets/AgentName-C_6R80nP.js +0 -1
- package/dist/web/assets/AgentProfilePage-Cz-i1fRD.js +0 -1
- package/dist/web/assets/AgentsDirectoryPage-BI0EFz6y.js +0 -1
- package/dist/web/assets/AgentsDirectoryPage-yRWktiin.css +0 -1
- package/dist/web/assets/ConnectionsPage-BoC6kXgs.js +0 -1
- package/dist/web/assets/EmployeeRecruitDialog-BVyclQAJ.js +0 -1
- package/dist/web/assets/EmployeeWorkbenchPage-D-c_Jm2z.css +0 -1
- package/dist/web/assets/EmployeeWorkbenchPage-D2GvYraI.js +0 -3
- package/dist/web/assets/KnowledgeEditDialog-a0AKWics.js +0 -1
- package/dist/web/assets/KnowledgeInspector-DMg9yJZ2.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-BgsHM3Qw.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-DeL7Jp34.css +0 -1
- package/dist/web/assets/KnowledgePage-kn8MqUCO.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-BtRq9fvC.js +0 -4
- package/dist/web/assets/NotFoundPage-D-7jLz3o.js +0 -1
- package/dist/web/assets/OverviewPage-BHPc-XxY.js +0 -1
- package/dist/web/assets/PersonalProfilePage-C00iV0Kx.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-CFNkdHSo.js +0 -1
- package/dist/web/assets/ProjectAgentFirstTask-CrCWEYgn.js +0 -1
- package/dist/web/assets/ProjectAgentsPage-BzztLMH-.js +0 -12
- package/dist/web/assets/ProjectScopePicker-CMxn3txv.js +0 -1
- package/dist/web/assets/PublicProjectsPage-DQVtvhOS.js +0 -1
- package/dist/web/assets/ReviewPage-BZhvyDWc.js +0 -1
- package/dist/web/assets/RoundtablesPage-CVebTXQa.js +0 -1
- package/dist/web/assets/SettingsPage-1knvfOW6.js +0 -1
- package/dist/web/assets/SettingsPage-BmYkLul4.css +0 -1
- package/dist/web/assets/SharedJefaReportPage-BCVq8u0P.js +0 -1
- package/dist/web/assets/UiDialog-Da1P10f4.js +0 -1
- package/dist/web/assets/UiDisclosure-8wE8YuE4.js +0 -1
- package/dist/web/assets/UiSegmented-BKZXc8QF.js +0 -1
- package/dist/web/assets/UiSelect-D5Xj5Ec4.js +0 -1
- package/dist/web/assets/WritingTastePage-B8-etOBn.js +0 -1
- package/dist/web/assets/client-CUTkF3t4.js +0 -1
- package/dist/web/assets/console-CG8tdtSs.js +0 -1
- package/dist/web/assets/conversation-launcher-settings-CyZkLJg7.js +0 -1
- package/dist/web/assets/index-kPB0Cf8Q.css +0 -1
- package/dist/web/assets/index-sLDKI6R-.js +0 -5
- package/dist/web/assets/model-l-sAqtW_.js +0 -1
- package/dist/web/assets/shared-report-B46dxlCR.js +0 -1
- package/dist/web/assets/useAgentRoster-DdxoKDEE.js +0 -1
- package/dist/web/assets/vue-router-DjqEU4W2.js +0 -1
|
@@ -43,7 +43,7 @@ class StoreAgentVerification:
|
|
|
43
43
|
task = await self._quality_task(actor, request)
|
|
44
44
|
rows, _, _ = await self.runtime.driver.execute_query(
|
|
45
45
|
'MATCH (t:FuliProjectAgentTask {task_id:$task, personal_space_id:$space})-[:HAS_VERIFICATION]->(v:FuliAgentVerification) '
|
|
46
|
-
'RETURN v.record_json AS record_json ORDER BY v.sequence', task=request.task_id, space=request.personal_space_id)
|
|
46
|
+
'RETURN v.record_json AS record_json, v.run_id AS run_id ORDER BY v.sequence', task=request.task_id, space=request.personal_space_id)
|
|
47
47
|
models = await self._quality_models(actor, request, task)
|
|
48
48
|
result = evaluate_quality_policy(complexity=task.get('complexity') or 'standard',
|
|
49
49
|
current_strategy=json.loads(task.get('effective_model_strategy_json') or '{"mode":"adaptive"}'),
|
|
@@ -53,7 +53,12 @@ class StoreAgentVerification:
|
|
|
53
53
|
if selected:
|
|
54
54
|
selected['executor_id'] = next(model['executor_id'] for model in models
|
|
55
55
|
if all(model.get(key) == selected[key] for key in ('provider', 'model', 'capability_tier', 'cost_rank')))
|
|
56
|
-
|
|
56
|
+
verified = next((row for row in rows if json.loads(row['record_json']).get('attempt_id') == result['verified_attempt_id']), None)
|
|
57
|
+
attempt = json.loads(verified['record_json']) if verified else None
|
|
58
|
+
if attempt:
|
|
59
|
+
attempt = {key: attempt.get(key) for key in ('attempt_id', 'artifact_revision', 'outcome', 'evidence_refs', 'verifier_id', 'executor_id', 'provider', 'model')}
|
|
60
|
+
attempt['run_id'] = verified.get('run_id')
|
|
61
|
+
return {**result, 'verified_attempt': attempt, 'evidence_authority': 'host_reported_test_evidence',
|
|
57
62
|
'execution_started': False, 'guidance': 'Only complete the task after passed for the current artifact revision. Blocked means no authorized preflighted model proves the required capability tier. Model selection does not prove execution.'}
|
|
58
63
|
|
|
59
64
|
async def record_agent_verification(self, actor, request):
|
|
@@ -92,7 +97,8 @@ class StoreAgentVerification:
|
|
|
92
97
|
raise HTTPException(409, 'An execution run cannot count as two verification attempts')
|
|
93
98
|
identifier = stable_uuid('verification', request.personal_space_id, request.task_id, request.attempt_id)
|
|
94
99
|
record = dict(attempt_id=request.attempt_id, outcome=request.outcome, artifact_revision=request.artifact_revision,
|
|
95
|
-
evidence_refs=request.evidence_refs, capability_tier=observed['capability_tier'], verifier_id=context['project_agent_id']
|
|
100
|
+
evidence_refs=request.evidence_refs, capability_tier=observed['capability_tier'], verifier_id=context['project_agent_id'],
|
|
101
|
+
executor_id=request.executor_id, provider=request.provider, model=request.model)
|
|
96
102
|
fingerprint = store._payload_hash(request)
|
|
97
103
|
rows, _, _ = await store.runtime.driver.execute_query(
|
|
98
104
|
'MATCH (t:FuliProjectAgentTask {task_id:$task, personal_space_id:$space}) '
|
|
@@ -106,6 +112,7 @@ class StoreAgentVerification:
|
|
|
106
112
|
result = await store.query_agent_quality(actor, request)
|
|
107
113
|
await store.runtime.driver.execute_query(
|
|
108
114
|
'MATCH (t:FuliProjectAgentTask {task_id:$task, personal_space_id:$space}) '
|
|
109
|
-
'SET t.quality_gate=$status, t.verified_artifact_revision=$revision',
|
|
110
|
-
task=request.task_id, space=request.personal_space_id, status=result['status'], revision=request.artifact_revision
|
|
115
|
+
'SET t.quality_gate=$status, t.verified_artifact_revision=$revision, t.verified_attempt_id=$attempt',
|
|
116
|
+
task=request.task_id, space=request.personal_space_id, status=result['status'], revision=request.artifact_revision,
|
|
117
|
+
attempt=result.get('verified_attempt_id'))
|
|
111
118
|
return result
|
|
@@ -325,6 +325,8 @@ class StoreKnowledge:
|
|
|
325
325
|
await self.runtime.driver.execute_query(
|
|
326
326
|
'''
|
|
327
327
|
UNWIND $entities AS row
|
|
328
|
+
WITH row
|
|
329
|
+
ORDER BY 'entity:' + row.uuid
|
|
328
330
|
MERGE (entity:Entity {uuid: row.uuid})
|
|
329
331
|
ON CREATE SET entity.group_id = $group_id,
|
|
330
332
|
entity.name = row.name,
|
|
@@ -365,6 +367,8 @@ class StoreKnowledge:
|
|
|
365
367
|
entity.fuli_preference_project_id = row.preference_project_id,
|
|
366
368
|
entity.fuli_preference_agent_id = row.preference_agent_id,
|
|
367
369
|
entity.fuli_attributes_json = row.attributes_json
|
|
370
|
+
SET entity.fuli_ai_review_lock_version =
|
|
371
|
+
coalesce(entity.fuli_ai_review_lock_version, 0) + 1
|
|
368
372
|
WITH count(entity) AS entity_count
|
|
369
373
|
CREATE (episode:Episodic {
|
|
370
374
|
uuid: $episode_id,
|
|
@@ -392,12 +396,95 @@ class StoreKnowledge:
|
|
|
392
396
|
})
|
|
393
397
|
WITH episode
|
|
394
398
|
UNWIND $entities AS row
|
|
399
|
+
WITH episode, row
|
|
400
|
+
ORDER BY 'entity:' + row.uuid
|
|
395
401
|
MATCH (entity:Entity {uuid: row.uuid})
|
|
396
402
|
MERGE (episode)-[mention:MENTIONS {uuid: row.mention_uuid}]->(entity)
|
|
397
403
|
SET mention.group_id = $group_id,
|
|
398
404
|
mention.created_at = $created_at
|
|
399
405
|
WITH episode
|
|
406
|
+
CALL {
|
|
407
|
+
WITH episode
|
|
408
|
+
OPTIONAL MATCH ()-[old:RELATES_TO {group_id: $group_id}]->()
|
|
409
|
+
WITH collect(old) AS candidate_olds
|
|
410
|
+
UNWIND candidate_olds AS old
|
|
411
|
+
WITH old
|
|
412
|
+
WHERE old IS NOT NULL
|
|
413
|
+
AND old.invalid_at IS NULL
|
|
414
|
+
AND NOT (old.uuid IN $relationship_ids)
|
|
415
|
+
AND (
|
|
416
|
+
EXISTS {
|
|
417
|
+
MATCH (:FuliSpace {id: $space_id})-
|
|
418
|
+
[:HAS_KNOWLEDGE_ASSIGNMENT]->
|
|
419
|
+
(assignment:FuliKnowledgeAssignment {
|
|
420
|
+
item_kind: 'relationship',
|
|
421
|
+
item_id: old.uuid
|
|
422
|
+
})
|
|
423
|
+
WHERE assignment.project_id = $personal_project_id
|
|
424
|
+
}
|
|
425
|
+
OR (
|
|
426
|
+
NOT EXISTS {
|
|
427
|
+
MATCH (:FuliSpace {id: $space_id})-
|
|
428
|
+
[:HAS_KNOWLEDGE_ASSIGNMENT]->
|
|
429
|
+
(:FuliKnowledgeAssignment {
|
|
430
|
+
item_kind: 'relationship',
|
|
431
|
+
item_id: old.uuid
|
|
432
|
+
})
|
|
433
|
+
}
|
|
434
|
+
AND EXISTS {
|
|
435
|
+
MATCH (old_episode:Episodic {group_id: $group_id})
|
|
436
|
+
WHERE old_episode.uuid IN coalesce(old.episodes, [])
|
|
437
|
+
AND (
|
|
438
|
+
old_episode.fuli_personal_project_id =
|
|
439
|
+
$personal_project_id
|
|
440
|
+
OR (
|
|
441
|
+
old_episode.fuli_personal_project_id IS NULL
|
|
442
|
+
AND $personal_project_id IS NULL
|
|
443
|
+
)
|
|
444
|
+
)
|
|
445
|
+
}
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
AND EXISTS {
|
|
449
|
+
MATCH (agent_episode:Episodic {group_id: $group_id})
|
|
450
|
+
WHERE agent_episode.uuid IN coalesce(old.episodes, [])
|
|
451
|
+
AND (
|
|
452
|
+
agent_episode.fuli_project_agent_id = $project_agent_id
|
|
453
|
+
OR (
|
|
454
|
+
agent_episode.fuli_project_agent_id IS NULL
|
|
455
|
+
AND $project_agent_id IS NULL
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
AND any(
|
|
460
|
+
replacement IN $superseded_relationships
|
|
461
|
+
WHERE replacement.key = old.fuli_key
|
|
462
|
+
)
|
|
463
|
+
WITH collect(old) AS superseded_edges
|
|
464
|
+
OPTIONAL MATCH ()-[existing:RELATES_TO {
|
|
465
|
+
group_id: $group_id
|
|
466
|
+
}]->()
|
|
467
|
+
WITH superseded_edges, collect(existing) AS candidate_existing
|
|
468
|
+
RETURN superseded_edges,
|
|
469
|
+
[edge IN candidate_existing
|
|
470
|
+
WHERE edge IS NOT NULL
|
|
471
|
+
AND edge.uuid IN $relationship_ids]
|
|
472
|
+
AS existing_edges
|
|
473
|
+
}
|
|
474
|
+
CALL {
|
|
475
|
+
WITH superseded_edges, existing_edges
|
|
476
|
+
UNWIND superseded_edges + existing_edges AS lock_edge
|
|
477
|
+
WITH lock_edge
|
|
478
|
+
WHERE lock_edge IS NOT NULL
|
|
479
|
+
ORDER BY 'relationship:' + lock_edge.uuid
|
|
480
|
+
SET lock_edge.fuli_ai_review_lock_version =
|
|
481
|
+
coalesce(lock_edge.fuli_ai_review_lock_version, 0) + 1
|
|
482
|
+
RETURN count(*) AS _locked_relationship_count
|
|
483
|
+
}
|
|
484
|
+
WITH episode, superseded_edges
|
|
400
485
|
UNWIND $relationships AS row
|
|
486
|
+
WITH episode, superseded_edges, row
|
|
487
|
+
ORDER BY 'relationship:' + row.uuid
|
|
401
488
|
MATCH (source:Entity {uuid: row.source_uuid})
|
|
402
489
|
MATCH (target:Entity {uuid: row.target_uuid})
|
|
403
490
|
MERGE (source)-[edge:RELATES_TO {uuid: row.uuid}]->(target)
|
|
@@ -435,73 +522,26 @@ class StoreKnowledge:
|
|
|
435
522
|
edge.fuli_workflow_confirmation_authority =
|
|
436
523
|
row.workflow_confirmation_authority,
|
|
437
524
|
edge.fuli_workflow_session_authority =
|
|
438
|
-
row.workflow_session_authority
|
|
525
|
+
row.workflow_session_authority,
|
|
526
|
+
edge.fuli_ai_review_lock_version = 1
|
|
439
527
|
SET edge.episodes =
|
|
440
528
|
CASE WHEN $episode_id IN coalesce(edge.episodes, [])
|
|
441
529
|
THEN edge.episodes
|
|
442
530
|
ELSE coalesce(edge.episodes, []) + $episode_id END
|
|
443
|
-
WITH count(edge) AS edge_count
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
EXISTS {
|
|
449
|
-
MATCH (:FuliSpace {id: $space_id})-
|
|
450
|
-
[:HAS_KNOWLEDGE_ASSIGNMENT]->
|
|
451
|
-
(assignment:FuliKnowledgeAssignment {
|
|
452
|
-
item_kind: 'relationship',
|
|
453
|
-
item_id: old.uuid
|
|
454
|
-
})
|
|
455
|
-
WHERE assignment.project_id = $personal_project_id
|
|
456
|
-
}
|
|
457
|
-
OR (
|
|
458
|
-
NOT EXISTS {
|
|
459
|
-
MATCH (:FuliSpace {id: $space_id})-
|
|
460
|
-
[:HAS_KNOWLEDGE_ASSIGNMENT]->
|
|
461
|
-
(:FuliKnowledgeAssignment {
|
|
462
|
-
item_kind: 'relationship',
|
|
463
|
-
item_id: old.uuid
|
|
464
|
-
})
|
|
465
|
-
}
|
|
466
|
-
AND EXISTS {
|
|
467
|
-
MATCH (old_episode:Episodic {group_id: $group_id})
|
|
468
|
-
WHERE old_episode.uuid IN coalesce(old.episodes, [])
|
|
469
|
-
AND (
|
|
470
|
-
old_episode.fuli_personal_project_id =
|
|
471
|
-
$personal_project_id
|
|
472
|
-
OR (
|
|
473
|
-
old_episode.fuli_personal_project_id IS NULL
|
|
474
|
-
AND $personal_project_id IS NULL
|
|
475
|
-
)
|
|
476
|
-
)
|
|
477
|
-
}
|
|
478
|
-
)
|
|
479
|
-
)
|
|
480
|
-
AND EXISTS {
|
|
481
|
-
MATCH (agent_episode:Episodic {group_id: $group_id})
|
|
482
|
-
WHERE agent_episode.uuid IN coalesce(old.episodes, [])
|
|
483
|
-
AND (
|
|
484
|
-
agent_episode.fuli_project_agent_id = $project_agent_id
|
|
485
|
-
OR (
|
|
486
|
-
agent_episode.fuli_project_agent_id IS NULL
|
|
487
|
-
AND $project_agent_id IS NULL
|
|
488
|
-
)
|
|
489
|
-
)
|
|
490
|
-
}
|
|
491
|
-
AND any(
|
|
531
|
+
WITH episode, superseded_edges, count(edge) AS edge_count
|
|
532
|
+
CALL {
|
|
533
|
+
WITH superseded_edges
|
|
534
|
+
UNWIND superseded_edges AS old
|
|
535
|
+
WITH old, head([
|
|
492
536
|
replacement IN $superseded_relationships
|
|
493
|
-
WHERE replacement.key = old.fuli_key
|
|
494
|
-
)
|
|
495
|
-
WITH edge_count, old, head([
|
|
496
|
-
replacement IN $superseded_relationships
|
|
497
|
-
WHERE replacement.key = old.fuli_key | replacement
|
|
498
|
-
]) AS replacement
|
|
499
|
-
FOREACH (_ IN CASE WHEN old IS NULL THEN [] ELSE [1] END |
|
|
537
|
+
WHERE replacement.key = old.fuli_key | replacement
|
|
538
|
+
]) AS replacement
|
|
500
539
|
SET old.invalid_at = $reference_time,
|
|
501
540
|
old.expired_at = $created_at,
|
|
502
541
|
old.fuli_replaced_by_item_id = replacement.replacement_id,
|
|
503
542
|
old.fuli_replaced_by_item_kind = 'relationship'
|
|
504
|
-
|
|
543
|
+
RETURN count(*) AS _superseded_relationship_count
|
|
544
|
+
}
|
|
505
545
|
RETURN edge_count
|
|
506
546
|
''',
|
|
507
547
|
entities=[
|
|
@@ -24,7 +24,7 @@ class StoreProjectAgentAttention:
|
|
|
24
24
|
self, actor, space, request.personal_project_id, agent_id,
|
|
25
25
|
require_active=True,
|
|
26
26
|
allow_unassigned=agent_id in {
|
|
27
|
-
'employee.bole', 'employee.jefa', 'fuli-project-coordinator',
|
|
27
|
+
'employee.bole', 'employee.jefa', 'employee.tonborg', 'fuli-project-coordinator',
|
|
28
28
|
},
|
|
29
29
|
)
|
|
30
30
|
if request.task_id:
|
|
@@ -4,16 +4,21 @@ from .personal_project_access import authorize_personal_project
|
|
|
4
4
|
from .project_agent_context_models import ProjectAgentContextResolution
|
|
5
5
|
from .project_agent_task_models import ProjectAgentTaskSubmit
|
|
6
6
|
from .task_context_temporary_owner import temporary_task_owner
|
|
7
|
+
from .project_agent_delegations import StoreAgentDelegations
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
class StoreProjectAgentContext:
|
|
10
|
+
class StoreProjectAgentContext(StoreAgentDelegations):
|
|
10
11
|
async def resolve_project_agent_context(self, actor, request):
|
|
11
12
|
self._require_personal()
|
|
12
13
|
space = await self.authorize(actor, request.personal_space_id, 'reader')
|
|
13
14
|
await authorize_personal_project(self, actor, space, request.personal_project_id)
|
|
14
|
-
temporary_owner = await temporary_task_owner(self, actor, request)
|
|
15
|
+
temporary_owner = await temporary_task_owner(self, actor, request) if not request.delegation_token else None
|
|
15
16
|
if temporary_owner is not None:
|
|
16
17
|
return temporary_owner
|
|
18
|
+
policy = await self.get_project_agent_coordination_policy(
|
|
19
|
+
actor, request.personal_space_id, request.personal_project_id,
|
|
20
|
+
)
|
|
21
|
+
project_lead = getattr(policy, 'team_lead_agent_id', None)
|
|
17
22
|
owner = None
|
|
18
23
|
if request.session_id and not request.agent_id:
|
|
19
24
|
rows, _, _ = await self.runtime.driver.execute_query(
|
|
@@ -39,7 +44,8 @@ class StoreProjectAgentContext:
|
|
|
39
44
|
idempotency_key='read-only-task-context',
|
|
40
45
|
title='Resolve task context', objective='Select one existing durable role.',
|
|
41
46
|
work_kind=request.work_kind, required_capabilities=request.required_capabilities,
|
|
42
|
-
lead_agent_id=request.agent_id or owner
|
|
47
|
+
lead_agent_id=request.agent_id or (project_lead if owner else None),
|
|
48
|
+
source_application=request.source_application,
|
|
43
49
|
routing_reason='Read-only task-entry context recovery',
|
|
44
50
|
)
|
|
45
51
|
if owner and not request.agent_id:
|
|
@@ -71,16 +77,58 @@ class StoreProjectAgentContext:
|
|
|
71
77
|
'using project_context for continuity only',
|
|
72
78
|
*basis,
|
|
73
79
|
]
|
|
74
|
-
|
|
80
|
+
requested_agent_id = None
|
|
81
|
+
reporting_lead_agent_id = None
|
|
82
|
+
peer_request = bool(request.agent_id and selected and self._is_implicit_peer(selected[0]))
|
|
83
|
+
delegated = bool(request.delegation_token)
|
|
84
|
+
if delegated and (not selected or selected[0]['agent_id'] != request.agent_id):
|
|
85
|
+
return ProjectAgentContextResolution(status='agent_unavailable', reason='delegation_invalid')
|
|
86
|
+
if not peer_request:
|
|
87
|
+
if not project_lead:
|
|
88
|
+
if delegated:
|
|
89
|
+
return ProjectAgentContextResolution(status='agent_unavailable', reason='delegation_invalid')
|
|
90
|
+
return ProjectAgentContextResolution(
|
|
91
|
+
status='unassigned', reason='project_lead_required',
|
|
92
|
+
requested_agent_id=request.agent_id,
|
|
93
|
+
match_basis=['Every project requires one accountable lead before work begins.'],
|
|
94
|
+
)
|
|
95
|
+
if selected and selected[0]['agent_id'] != project_lead:
|
|
96
|
+
# Validate the named specialist first, then route accountability
|
|
97
|
+
# through the lead. Never load the specialist's private memory here.
|
|
98
|
+
rows = await self._assignment_candidates(request.personal_space_id, request.personal_project_id)
|
|
99
|
+
lead = next((row for row in rows if row['agent_id'] == project_lead
|
|
100
|
+
and (delegated or self._agent_client_allowed(row, request.source_application))), None)
|
|
101
|
+
if not lead:
|
|
102
|
+
return ProjectAgentContextResolution(status='agent_unavailable',
|
|
103
|
+
reason='team_lead_unavailable', match_basis=['The configured project lead is unavailable.'])
|
|
104
|
+
if delegated:
|
|
105
|
+
# A real lead-to-member worker invocation keeps the member's
|
|
106
|
+
# isolated identity; its result returns to the project lead.
|
|
107
|
+
reporting_lead_agent_id = project_lead
|
|
108
|
+
reason = 'delegated_member'
|
|
109
|
+
basis = ['This member worker reports to the configured project lead.']
|
|
110
|
+
else:
|
|
111
|
+
requested_agent_id = request.agent_id
|
|
112
|
+
selected = [lead]
|
|
113
|
+
reason = 'project_lead_reporting'
|
|
114
|
+
basis = ['The project lead owns the task; requested members report results to this lead.']
|
|
115
|
+
if owner and selected and owner == selected[0]['agent_id']:
|
|
75
116
|
reason, basis = 'active_task_owner', ['reuse the owner of this host session task']
|
|
76
117
|
agent = await self.get_project_agent(
|
|
77
118
|
actor, request.personal_space_id, request.personal_project_id,
|
|
78
119
|
selected[0]['agent_id'],
|
|
79
120
|
) if selected else None
|
|
121
|
+
# A reporting lead does not execute in the member's client. Acknowledge
|
|
122
|
+
# the capability only after the executing role passes selection, so a
|
|
123
|
+
# failed resolution cannot be accepted as that member's answer.
|
|
124
|
+
if delegated and (not agent or not await self.redeem_agent_delegation(request, project_lead)):
|
|
125
|
+
return ProjectAgentContextResolution(status='agent_unavailable', reason='delegation_invalid')
|
|
80
126
|
return ProjectAgentContextResolution(
|
|
81
127
|
status='ready' if agent else (
|
|
82
128
|
'manual_selection' if reason == 'manual_agent_selection'
|
|
83
129
|
else 'agent_unavailable' if reason == 'agent_unavailable' else 'unassigned'
|
|
84
130
|
),
|
|
85
131
|
agent=agent, reason=reason, match_basis=basis, candidate_count=len(candidates),
|
|
132
|
+
requested_agent_id=requested_agent_id,
|
|
133
|
+
reporting_lead_agent_id=reporting_lead_agent_id,
|
|
86
134
|
)
|
|
@@ -93,6 +93,8 @@ class StoreProjectAgentCoordinationPolicy:
|
|
|
93
93
|
members = (request.team_member_agent_ids if 'team_member_agent_ids' in request.model_fields_set
|
|
94
94
|
else current.team_member_agent_ids)
|
|
95
95
|
auto_grow = current.auto_grow_team if request.auto_grow_team is None else request.auto_grow_team
|
|
96
|
+
if 'team_lead_agent_id' in request.model_fields_set and not lead:
|
|
97
|
+
raise HTTPException(status_code=422, detail='each project requires a lead; select a replacement instead of clearing the lead')
|
|
96
98
|
if members and not lead:
|
|
97
99
|
raise HTTPException(status_code=422, detail='select a team lead before adding members')
|
|
98
100
|
if lead in members:
|
|
@@ -115,9 +117,21 @@ class StoreProjectAgentCoordinationPolicy:
|
|
|
115
117
|
updated_at = now_utc()
|
|
116
118
|
written, _, _ = await self.runtime.driver.execute_query(
|
|
117
119
|
'''
|
|
118
|
-
MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
120
|
+
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
119
121
|
[:CONTAINS_PROJECT]->
|
|
120
122
|
(project:FuliPersonalProject {project_id: $personal_project_id})
|
|
123
|
+
OPTIONAL MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(role:FuliProjectAgent)
|
|
124
|
+
WHERE role.agent_id IN $team_agent_ids
|
|
125
|
+
WITH project, role ORDER BY role.agent_id
|
|
126
|
+
FOREACH (locked IN CASE WHEN role IS NULL THEN [] ELSE [role] END |
|
|
127
|
+
SET locked._task_lifecycle_lock = true
|
|
128
|
+
REMOVE locked._task_lifecycle_lock)
|
|
129
|
+
WITH project, collect(role) AS roles
|
|
130
|
+
WHERE size(roles) = size($team_agent_ids)
|
|
131
|
+
AND all(role IN roles WHERE role.status = 'active' AND EXISTS {
|
|
132
|
+
MATCH (project)-[:HAS_PROJECT_AGENT_ASSIGNMENT]->
|
|
133
|
+
(:FuliProjectAgentAssignment {status: 'active'})-[:ASSIGNED_AGENT]->(role)
|
|
134
|
+
})
|
|
121
135
|
MERGE (policy:FuliProjectAgentCoordinationPolicy {
|
|
122
136
|
policy_id: $policy_id
|
|
123
137
|
})
|
|
@@ -147,6 +161,7 @@ class StoreProjectAgentCoordinationPolicy:
|
|
|
147
161
|
auto_grow_team=auto_grow,
|
|
148
162
|
team_lead_agent_id=lead,
|
|
149
163
|
team_member_agent_ids=members,
|
|
164
|
+
team_agent_ids=([lead] if lead else []) + members,
|
|
150
165
|
expected_updated_at=current.updated_at,
|
|
151
166
|
updated_at=updated_at,
|
|
152
167
|
)
|
|
@@ -41,6 +41,19 @@ class StoreProjectAgentTaskActivity:
|
|
|
41
41
|
space,
|
|
42
42
|
request.personal_project_id,
|
|
43
43
|
)
|
|
44
|
+
if request.judgment_task_context_token:
|
|
45
|
+
# Match the task-context lifecycle lock order: session, then task.
|
|
46
|
+
# A new host turn cannot supersede the checked token before commit.
|
|
47
|
+
contexts, _, _ = await self.runtime.driver.execute_query(
|
|
48
|
+
'''
|
|
49
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
50
|
+
[:HAS_TASK_CONTEXT_SESSION]->(session:FuliTaskContextSession)-
|
|
51
|
+
[:HAS_CONTEXT]->(context:FuliTaskContext {token: $token})
|
|
52
|
+
SET session.write_serial = coalesce(session.write_serial, 0) + 1
|
|
53
|
+
RETURN context.token AS token
|
|
54
|
+
''', space_id=request.personal_space_id, token=request.judgment_task_context_token)
|
|
55
|
+
if not contexts:
|
|
56
|
+
raise HTTPException(403, 'Judgment acceptance context is unavailable')
|
|
44
57
|
# Acquire the task's write lock before reading its revision, transition
|
|
45
58
|
# state or idempotency events. A transaction alone permits two concurrent
|
|
46
59
|
# readers to validate the same revision before either obtains a lock.
|
|
@@ -96,6 +109,19 @@ class StoreProjectAgentTaskActivity:
|
|
|
96
109
|
if (not request.artifact_revision or raw_task.get('quality_gate') != 'passed'
|
|
97
110
|
or raw_task.get('verified_artifact_revision') != request.artifact_revision):
|
|
98
111
|
raise HTTPException(409, 'Verify this exact artifact revision before completing the task')
|
|
112
|
+
if request.expected_verification_attempt_id is not None:
|
|
113
|
+
if (request.status != 'completed' or raw_task.get('quality_gate') != 'passed'
|
|
114
|
+
or raw_task.get('verified_attempt_id') != request.expected_verification_attempt_id
|
|
115
|
+
or raw_task.get('verified_artifact_revision') != request.artifact_revision):
|
|
116
|
+
raise HTTPException(409, 'Verification evidence changed before task acceptance')
|
|
117
|
+
if not request.judgment_task_context_token or not request.source_application:
|
|
118
|
+
raise HTTPException(403, 'Judgment acceptance requires a current lead context')
|
|
119
|
+
context = await self.get_task_context(actor, request.personal_space_id,
|
|
120
|
+
request.judgment_task_context_token, request.source_application)
|
|
121
|
+
if (context.get('personal_project_id') != request.personal_project_id
|
|
122
|
+
or context.get('project_agent_id') != raw_task.get('lead_agent_id')
|
|
123
|
+
or request.agent_id != raw_task.get('lead_agent_id')):
|
|
124
|
+
raise HTTPException(403, 'Judgment acceptance requires the current accountable lead')
|
|
99
125
|
self._validate_task_transition(
|
|
100
126
|
raw_task.get('status'),
|
|
101
127
|
request.status,
|
|
@@ -13,10 +13,11 @@ from .system_hr_identity import LEGACY_HR_AGENT_ID
|
|
|
13
13
|
|
|
14
14
|
IMPLICIT_PEER_AGENT_IDS = frozenset({
|
|
15
15
|
'employee.jefa',
|
|
16
|
+
'employee.tonborg',
|
|
16
17
|
SYSTEM_HR_AGENT_ID.casefold(),
|
|
17
18
|
LEGACY_HR_AGENT_ID.casefold(),
|
|
18
19
|
})
|
|
19
|
-
IMPLICIT_PEER_CAPABILITIES = frozenset({'fuli.employee:jefa', 'fuli.employee:bole'})
|
|
20
|
+
IMPLICIT_PEER_CAPABILITIES = frozenset({'fuli.employee:jefa', 'fuli.employee:bole', 'fuli.employee:tonborg'})
|
|
20
21
|
IMPLICIT_PEER_AGENT_TYPES = frozenset({'hr'})
|
|
21
22
|
|
|
22
23
|
|
|
@@ -54,14 +55,15 @@ class StoreProjectAgentTaskStaffing:
|
|
|
54
55
|
row for row in assignment_rows
|
|
55
56
|
if self._agent_client_allowed(row, request.source_application)
|
|
56
57
|
]
|
|
57
|
-
if request.staffing_intent in {'new_durable', 'temporary'}:
|
|
58
|
+
if request.staffing_intent in {'new_durable', 'temporary', 'default_lead'}:
|
|
58
59
|
return (
|
|
59
60
|
[],
|
|
60
61
|
rows,
|
|
61
62
|
'explicit_temporary_agent'
|
|
62
63
|
if request.staffing_intent == 'temporary'
|
|
63
64
|
else 'explicit_new_agent',
|
|
64
|
-
['
|
|
65
|
+
['project requires an accountable lead' if request.staffing_intent == 'default_lead'
|
|
66
|
+
else 'user explicitly requested recruitment'],
|
|
65
67
|
)
|
|
66
68
|
policy = await self.get_project_agent_coordination_policy(
|
|
67
69
|
actor,
|
|
@@ -5,6 +5,9 @@ from fastapi import HTTPException
|
|
|
5
5
|
|
|
6
6
|
from .personal_project_access import authorize_personal_project
|
|
7
7
|
from .project_agent_access import authorize_project_agent
|
|
8
|
+
from .project_agent_employee_numbers import (
|
|
9
|
+
ASSIGN_EMPLOYEE_NUMBERS, EMPLOYEE_NUMBER_LOCK, employee_number_label,
|
|
10
|
+
)
|
|
8
11
|
from .project_agent_models import (
|
|
9
12
|
ProjectAgentAssignmentCreate,
|
|
10
13
|
ProjectAgentAssignmentEnd,
|
|
@@ -134,6 +137,7 @@ class StoreProjectAgents:
|
|
|
134
137
|
records, _, _ = await self.runtime.driver.execute_query(
|
|
135
138
|
'''
|
|
136
139
|
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})
|
|
140
|
+
''' + EMPLOYEE_NUMBER_LOCK + '''
|
|
137
141
|
MERGE (agent:FuliProjectAgent {id: $id})
|
|
138
142
|
ON CREATE SET agent.agent_id = $agent_id,
|
|
139
143
|
agent.created_at = $updated_at,
|
|
@@ -143,7 +147,12 @@ class StoreProjectAgents:
|
|
|
143
147
|
SET agent._profile_upsert_lock = true
|
|
144
148
|
REMOVE agent._profile_upsert_lock
|
|
145
149
|
WITH space, agent
|
|
146
|
-
WHERE agent.status IS NULL OR agent.status <> 'archived'
|
|
150
|
+
WHERE (agent.status IS NULL OR agent.status <> 'archived')
|
|
151
|
+
AND ($status = 'active' OR NOT EXISTS {
|
|
152
|
+
MATCH (space)-[:CONTAINS_PROJECT]->(:FuliPersonalProject)-
|
|
153
|
+
[:HAS_PROJECT_AGENT_COORDINATION_POLICY]->(policy:FuliProjectAgentCoordinationPolicy)
|
|
154
|
+
WHERE policy.team_lead_agent_id = agent.agent_id
|
|
155
|
+
})
|
|
147
156
|
SET agent.profile_json = $profile_json,
|
|
148
157
|
agent.name = $name,
|
|
149
158
|
agent.occupation_emoji = $occupation_emoji,
|
|
@@ -159,6 +168,7 @@ class StoreProjectAgents:
|
|
|
159
168
|
agent.recruitment_id, $recruitment_id),
|
|
160
169
|
agent.updated_at = $updated_at
|
|
161
170
|
MERGE (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(agent)
|
|
171
|
+
''' + ASSIGN_EMPLOYEE_NUMBERS + '''
|
|
162
172
|
RETURN agent
|
|
163
173
|
''',
|
|
164
174
|
personal_space_id=request.personal_space_id,
|
|
@@ -182,7 +192,7 @@ class StoreProjectAgents:
|
|
|
182
192
|
raise HTTPException(
|
|
183
193
|
status_code=409,
|
|
184
194
|
detail=(
|
|
185
|
-
'archived
|
|
195
|
+
'archived Agents require a dedicated restore operation; project leads must hand off before deactivation'
|
|
186
196
|
),
|
|
187
197
|
)
|
|
188
198
|
if request.personal_project_id:
|
|
@@ -420,7 +430,12 @@ class StoreProjectAgents:
|
|
|
420
430
|
'awaiting_recruitment', 'queued', 'running',
|
|
421
431
|
'paused', 'blocked', 'awaiting_review'
|
|
422
432
|
]] AS open_tasks
|
|
423
|
-
WHERE agent.status = 'archived' OR size(open_tasks) = 0
|
|
433
|
+
WHERE (agent.status = 'archived' OR size(open_tasks) = 0)
|
|
434
|
+
AND NOT EXISTS {
|
|
435
|
+
MATCH (space)-[:CONTAINS_PROJECT]->(:FuliPersonalProject)-
|
|
436
|
+
[:HAS_PROJECT_AGENT_COORDINATION_POLICY]->(policy:FuliProjectAgentCoordinationPolicy)
|
|
437
|
+
WHERE policy.team_lead_agent_id = agent.agent_id
|
|
438
|
+
}
|
|
424
439
|
SET agent.status = 'archived',
|
|
425
440
|
agent.archive_reason = coalesce(agent.archive_reason, $reason),
|
|
426
441
|
agent.archived_at = coalesce(agent.archived_at, $updated_at),
|
|
@@ -446,7 +461,7 @@ class StoreProjectAgents:
|
|
|
446
461
|
if not records:
|
|
447
462
|
raise HTTPException(
|
|
448
463
|
status_code=409,
|
|
449
|
-
detail='Agent is missing
|
|
464
|
+
detail='Agent is missing, has non-terminal task work, or is a project lead; hand off before archiving',
|
|
450
465
|
)
|
|
451
466
|
return await self.get_project_agent(
|
|
452
467
|
actor,
|
|
@@ -768,6 +783,11 @@ class StoreProjectAgents:
|
|
|
768
783
|
WITH project, assignment, agent
|
|
769
784
|
WHERE assignment.status = 'active'
|
|
770
785
|
AND coalesce(assignment.revision, 0) = $expected_revision
|
|
786
|
+
AND NOT EXISTS {
|
|
787
|
+
MATCH (project)-[:HAS_PROJECT_AGENT_COORDINATION_POLICY]->
|
|
788
|
+
(policy:FuliProjectAgentCoordinationPolicy)
|
|
789
|
+
WHERE policy.team_lead_agent_id = agent.agent_id
|
|
790
|
+
}
|
|
771
791
|
SET assignment.status = 'ended',
|
|
772
792
|
assignment.end_reason = $reason,
|
|
773
793
|
assignment.ended_at = $updated_at,
|
|
@@ -786,7 +806,7 @@ class StoreProjectAgents:
|
|
|
786
806
|
if not records:
|
|
787
807
|
raise HTTPException(
|
|
788
808
|
status_code=409,
|
|
789
|
-
detail='assignment is not active
|
|
809
|
+
detail='assignment is not active, revision is stale, or project leadership requires a handoff',
|
|
790
810
|
)
|
|
791
811
|
row = records[0]
|
|
792
812
|
return self._assignment(
|
|
@@ -851,6 +871,11 @@ class StoreProjectAgents:
|
|
|
851
871
|
REMOVE lifecycle_agent._task_lifecycle_lock
|
|
852
872
|
WITH project, ended, ended_agent, replacement_agent,
|
|
853
873
|
collect(lifecycle_agent) AS locked_agents
|
|
874
|
+
WHERE ended.status <> 'active' OR NOT EXISTS {
|
|
875
|
+
MATCH (project)-[:HAS_PROJECT_AGENT_COORDINATION_POLICY]->
|
|
876
|
+
(policy:FuliProjectAgentCoordinationPolicy)
|
|
877
|
+
WHERE policy.team_lead_agent_id = ended_agent.agent_id
|
|
878
|
+
}
|
|
854
879
|
OPTIONAL MATCH (existing_replacement:FuliProjectAgentAssignment {
|
|
855
880
|
id: $replacement_id
|
|
856
881
|
})
|
|
@@ -1075,6 +1100,7 @@ class StoreProjectAgents:
|
|
|
1075
1100
|
})
|
|
1076
1101
|
return ProjectAgentRecord(
|
|
1077
1102
|
agent_id=raw['agent_id'],
|
|
1103
|
+
employee_number=employee_number_label(raw.get('employee_number')),
|
|
1078
1104
|
legacy_agent_ids=list(raw.get('legacy_agent_ids') or []),
|
|
1079
1105
|
personal_space_id=personal_space_id,
|
|
1080
1106
|
personal_project_id=projection_project_id,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Fixed roles: Jefa (project manager), Bole (HR), and Tonborg (judgment).
|
|
2
2
|
|
|
3
3
|
Jefa also coordinates task routing, replacing the earlier separate coordinator
|
|
4
4
|
identity. That legacy node is retired, not deleted, so old tasks keep their
|
|
@@ -8,11 +8,13 @@ original coordinator reference.
|
|
|
8
8
|
import json
|
|
9
9
|
|
|
10
10
|
from .project_agent_models import ProjectAgentProfile
|
|
11
|
+
from .project_agent_employee_numbers import ASSIGN_EMPLOYEE_NUMBERS, EMPLOYEE_NUMBER_LOCK
|
|
11
12
|
from .provider_values import now_utc, stable_uuid
|
|
12
13
|
|
|
13
14
|
SYSTEM_COORDINATOR_AGENT_ID = 'employee.jefa'
|
|
14
15
|
LEGACY_COORDINATOR_AGENT_ID = 'fuli-project-coordinator'
|
|
15
16
|
SYSTEM_HR_AGENT_ID = 'employee.bole'
|
|
17
|
+
SYSTEM_JUDGMENT_AGENT_ID = 'employee.tonborg'
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
def coordinator_profile() -> ProjectAgentProfile:
|
|
@@ -47,9 +49,27 @@ def hr_profile() -> ProjectAgentProfile:
|
|
|
47
49
|
)
|
|
48
50
|
|
|
49
51
|
|
|
52
|
+
def judgment_profile() -> ProjectAgentProfile:
|
|
53
|
+
return ProjectAgentProfile(
|
|
54
|
+
name='Tonborg',
|
|
55
|
+
occupation_emoji='⚖️',
|
|
56
|
+
responsibility='判断官:依照用户偏好审核日常事项、建议客户端和模型,保留判断依据与反馈。',
|
|
57
|
+
agent_type='durable',
|
|
58
|
+
work_kinds=['judgment', 'knowledge-review', 'executor-selection'],
|
|
59
|
+
capabilities=['判断审核', '执行器选择', '判断反馈', 'fuli.employee:tonborg'],
|
|
60
|
+
initial_preferences=[
|
|
61
|
+
'用户明确偏好优先;只把相关范围的历史反馈用于当前判断。',
|
|
62
|
+
'不把模型建议当作已执行,不把 AI 审核冒充用户确认。',
|
|
63
|
+
'保持项目总负责人与汇报关系。',
|
|
64
|
+
],
|
|
65
|
+
status='active',
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
50
69
|
SYSTEM_PROFILES = {
|
|
51
70
|
SYSTEM_COORDINATOR_AGENT_ID: coordinator_profile,
|
|
52
71
|
SYSTEM_HR_AGENT_ID: hr_profile,
|
|
72
|
+
SYSTEM_JUDGMENT_AGENT_ID: judgment_profile,
|
|
53
73
|
}
|
|
54
74
|
|
|
55
75
|
|
|
@@ -75,6 +95,7 @@ async def ensure_system_identity(driver, provider_id, space_id, agent_id):
|
|
|
75
95
|
await driver.execute_query(
|
|
76
96
|
'''
|
|
77
97
|
MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
|
|
98
|
+
''' + EMPLOYEE_NUMBER_LOCK + '''
|
|
78
99
|
MERGE (agent:FuliProjectAgent {id: $id})
|
|
79
100
|
ON CREATE SET agent.agent_id = $agent_id,
|
|
80
101
|
agent.name = $name,
|
|
@@ -90,7 +111,7 @@ async def ensure_system_identity(driver, provider_id, space_id, agent_id):
|
|
|
90
111
|
agent.status = 'active',
|
|
91
112
|
agent.system_managed = true
|
|
92
113
|
MERGE (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(agent)
|
|
93
|
-
''',
|
|
114
|
+
''' + ASSIGN_EMPLOYEE_NUMBERS + ' RETURN agent',
|
|
94
115
|
space_id=space_id,
|
|
95
116
|
id=node_id,
|
|
96
117
|
agent_id=agent_id,
|