fuli-context 0.7.9 → 0.7.10-beta.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.
Files changed (150) hide show
  1. package/README.md +5 -2
  2. package/README.zh-CN.md +5 -3
  3. package/dist/web/assets/{AboutPage-Bp-Xz6yo.js → AboutPage-EtWPOSpz.js} +1 -1
  4. package/dist/web/assets/AboutPage-vYIBifKw.css +1 -0
  5. package/dist/web/assets/AgentHand-D5pOgVjJ.js +1 -0
  6. package/dist/web/assets/AgentHand-DvdNtJ3_.css +1 -0
  7. package/dist/web/assets/ConnectionsPage-DXkvMuyK.js +1 -0
  8. package/dist/web/assets/EmployeeRecruitDialog-B59X3rOi.css +1 -0
  9. package/dist/web/assets/EmployeeRecruitDialog-DONxXnd2.js +1 -0
  10. package/dist/web/assets/EmployeeWorkbenchPage-Cd4hd4bf.js +3 -0
  11. package/dist/web/assets/EmployeeWorkbenchPage-Cz7y-IFD.css +1 -0
  12. package/dist/web/assets/GrowthLoading-B_Hh0CRS.css +1 -0
  13. package/dist/web/assets/GrowthLoading-CCYPgE2f.js +1 -0
  14. package/dist/web/assets/KnowledgeEditDialog-BUqMfa0a.js +1 -0
  15. package/dist/web/assets/KnowledgeEditDialog-D9NuRHvp.css +1 -0
  16. package/dist/web/assets/KnowledgeInspector-DlVPDGP3.js +1 -0
  17. package/dist/web/assets/KnowledgeOrganizerPage-DhthZGKZ.js +1 -0
  18. package/dist/web/assets/KnowledgeOrganizerPage-h2iKvQpO.css +1 -0
  19. package/dist/web/assets/KnowledgePage-B6RDVxq-.js +1 -0
  20. package/dist/web/assets/KnowledgeWorkspace-mVFHQcxr.js +4 -0
  21. package/dist/web/assets/KnowledgeWorkspace-sb8OJ97I.css +1 -0
  22. package/dist/web/assets/NotFoundPage-CCeEdBy9.js +1 -0
  23. package/dist/web/assets/{OverviewPage-HwoSW-5z.js → OverviewPage-CnYWoZ3r.js} +1 -1
  24. package/dist/web/assets/PersonalProfilePage-Bz4zpTPC.js +1 -0
  25. package/dist/web/assets/PersonalProfilePage-yQRCBude.css +1 -0
  26. package/dist/web/assets/PersonalProjectsPage-2u1o6dvA.js +1 -0
  27. package/dist/web/assets/ProjectAgentsPage-8tci631q.js +11 -0
  28. package/dist/web/assets/ProjectAgentsPage-BN4k3ubu.css +1 -0
  29. package/dist/web/assets/ProjectScopePicker-BcPtIcv8.js +1 -0
  30. package/dist/web/assets/ProjectScopePicker-BvaGTMFx.css +1 -0
  31. package/dist/web/assets/PublicProjectsPage-BmBHfnud.js +1 -0
  32. package/dist/web/assets/ReviewPage-tvt2eqhw.js +1 -0
  33. package/dist/web/assets/{SearchableSelect-CFZNdtlY.js → SearchableSelect-BjBDBVuZ.js} +1 -1
  34. package/dist/web/assets/SettingsPage-Brl-SL33.css +1 -0
  35. package/dist/web/assets/SettingsPage-D9mNzBg-.js +1 -0
  36. package/dist/web/assets/SharedJefaReportPage-DXbj_JhO.css +1 -0
  37. package/dist/web/assets/SharedJefaReportPage-Dn_HBgaa.js +1 -0
  38. package/dist/web/assets/WritingTastePage-BCwimXC-.js +1 -0
  39. package/dist/web/assets/WritingTastePage-tb_pgude.css +1 -0
  40. package/dist/web/assets/bole-avatar-CVIr-bU_.webp +0 -0
  41. package/dist/web/assets/client-v8XKgQji.js +1 -0
  42. package/dist/web/assets/console-C9HS-zbd.js +1 -0
  43. package/dist/web/assets/conversation-launcher-settings-DMDCpf_N.js +1 -0
  44. package/dist/web/assets/employee-dialog-Bh5ccIbK.css +1 -0
  45. package/dist/web/assets/{i18n-DQH9D35M.js → i18n-BkJjUsz_.js} +3 -3
  46. package/dist/web/assets/{identity-JjZG6aS5.js → identity-74yGiMzO.js} +1 -1
  47. package/dist/web/assets/index-CwjxeHGk.css +1 -0
  48. package/dist/web/assets/index-Dioqmmvf.js +2 -0
  49. package/dist/web/assets/{model-zL97NJ-U.js → model-86tZgY0r.js} +1 -1
  50. package/dist/web/assets/shared-report-CLg82y4D.js +1 -0
  51. package/dist/web/assets/shared-report-ezeb8dW4.css +1 -0
  52. package/dist/web/assets/{useApi-CROJJdhE-CElDxEZI.js → useApi-CROJJdhE-BBUO_YEu.js} +1 -1
  53. package/dist/web/assets/useModalDialog-DrLhqtjR.js +1 -0
  54. package/dist/web/assets/{vue-router-F7MnkVlW.js → vue-router-Cw4QZxM1.js} +1 -1
  55. package/dist/web/index.html +14 -8
  56. package/docs/agent-interface-architecture.md +97 -0
  57. package/docs/employee-agents.md +58 -2
  58. package/graph-provider/fuli_graph/collaboration_context.py +2 -1
  59. package/graph-provider/fuli_graph/graph_models.py +5 -3
  60. package/graph-provider/fuli_graph/graph_query.py +4 -0
  61. package/graph-provider/fuli_graph/knowledge_search.py +2 -1
  62. package/graph-provider/fuli_graph/project_agent_access.py +2 -0
  63. package/graph-provider/fuli_graph/project_agent_attention_models.py +64 -0
  64. package/graph-provider/fuli_graph/project_agent_attention_routes.py +38 -0
  65. package/graph-provider/fuli_graph/project_agent_models.py +2 -0
  66. package/graph-provider/fuli_graph/project_agent_names.py +33 -0
  67. package/graph-provider/fuli_graph/project_agent_routes.py +12 -0
  68. package/graph-provider/fuli_graph/runtime.py +49 -20
  69. package/graph-provider/fuli_graph/store.py +2 -0
  70. package/graph-provider/fuli_graph/store_knowledge.py +4 -2
  71. package/graph-provider/fuli_graph/store_project_agent_attention.py +138 -0
  72. package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +8 -0
  73. package/graph-provider/fuli_graph/store_project_agent_memory.py +40 -13
  74. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +4 -1
  75. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +4 -1
  76. package/graph-provider/fuli_graph/store_project_agent_tasks.py +3 -0
  77. package/graph-provider/fuli_graph/store_project_agents.py +109 -0
  78. package/graph-provider/fuli_graph/store_task_contexts.py +1 -1
  79. package/graph-provider/fuli_graph/system_hr_identity.py +218 -0
  80. package/graph-provider/pyproject.toml +1 -1
  81. package/npm-shrinkwrap.json +52 -60
  82. package/package.json +2 -2
  83. package/skills/operating-fuli/SKILL.md +47 -0
  84. package/skills/operating-fuli/agents/openai.yaml +4 -0
  85. package/skills/operating-fuli/references/workflow-recipes.md +33 -0
  86. package/src/agent-tools/agent-attention-definitions.js +33 -0
  87. package/src/agent-tools/graph-definitions.js +8 -2
  88. package/src/agent-tools/graph-handlers.js +41 -0
  89. package/src/agent-tools/interface-catalog.js +101 -0
  90. package/src/agent-tools/interface-definitions.js +184 -0
  91. package/src/agent-tools/project-agent-definitions.js +1 -0
  92. package/src/cli/local-runtime-command.js +2 -1
  93. package/src/employees/catalog/bole.json +19 -0
  94. package/src/employees/install-package.js +3 -0
  95. package/src/employees/manifest.js +4 -0
  96. package/src/employees/package-registry.js +2 -0
  97. package/src/employees/recruitment.js +3 -0
  98. package/src/employees/service.js +10 -0
  99. package/src/external-knowledge/registry.js +24 -0
  100. package/src/external-knowledge/service.js +34 -1
  101. package/src/graphiti/agent-attention.js +26 -0
  102. package/src/graphiti/agent-knowledge-workflows.js +1 -0
  103. package/src/graphiti/project-agent-control-plane.js +21 -0
  104. package/src/graphiti/project-agent-mapping.js +6 -0
  105. package/src/graphiti/provider-client.js +13 -0
  106. package/src/graphiti/workspace-provider-client.js +9 -1
  107. package/src/http/agent-attention-api-router.js +28 -0
  108. package/src/http/error-mapping.js +3 -0
  109. package/src/http/graph-api-router.js +2 -0
  110. package/src/local-runtime/lifecycle.js +8 -2
  111. package/src/mcp/create-mcp-server.js +10 -1
  112. package/src/mcp/tool-annotations.js +31 -0
  113. package/src/mcp/tool-result.js +4 -4
  114. package/src/setup/agent-installation-status.js +2 -1
  115. package/src/setup/agents.js +4 -0
  116. package/src/setup/graph-runtime.js +4 -0
  117. package/src/setup/paths.js +2 -1
  118. package/src/setup/setup.js +4 -2
  119. package/src/setup/uninstall.js +4 -2
  120. package/src/system/resource-monitor.js +11 -1
  121. package/src/system/runtime-settings.js +7 -0
  122. package/dist/web/assets/AboutPage-DXh7Fwxt.css +0 -1
  123. package/dist/web/assets/ConnectionsPage-BMpkYeSE.js +0 -1
  124. package/dist/web/assets/EmployeeRecruitDialog-CnA7rOnB.css +0 -1
  125. package/dist/web/assets/EmployeeRecruitDialog-DbMVuLAI.js +0 -1
  126. package/dist/web/assets/EmployeeWorkbenchPage-BpkqpEs7.css +0 -1
  127. package/dist/web/assets/EmployeeWorkbenchPage-DtXSJOpq.js +0 -1
  128. package/dist/web/assets/KnowledgeEditDialog-DADXj_BT.js +0 -1
  129. package/dist/web/assets/KnowledgeEditDialog-iv-dgUeD.css +0 -1
  130. package/dist/web/assets/KnowledgeInspector-Xs4VCUg8.js +0 -1
  131. package/dist/web/assets/KnowledgeOrganizerPage-B_Ct4bB2.js +0 -1
  132. package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +0 -1
  133. package/dist/web/assets/KnowledgePage-RG9Ttnrp.js +0 -1
  134. package/dist/web/assets/KnowledgeWorkspace-DI8YqTVI.js +0 -4
  135. package/dist/web/assets/PersonalProfilePage-B7T09bow.css +0 -1
  136. package/dist/web/assets/PersonalProfilePage-DCoG52SW.js +0 -1
  137. package/dist/web/assets/PersonalProjectsPage-CNb9mlbp.js +0 -1
  138. package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +0 -1
  139. package/dist/web/assets/ProjectAgentsPage-zQ9BZW9v.js +0 -11
  140. package/dist/web/assets/PublicProjectsPage-BuLqkZ8g.js +0 -1
  141. package/dist/web/assets/ReviewPage-8A7N5pJs.js +0 -1
  142. package/dist/web/assets/SettingsPage-4ah1u906.js +0 -1
  143. package/dist/web/assets/SettingsPage-CwZp2ka4.css +0 -1
  144. package/dist/web/assets/WritingTastePage-DSgvTH6x.css +0 -1
  145. package/dist/web/assets/WritingTastePage-DztRj6MK.js +0 -1
  146. package/dist/web/assets/avatars-LvVKbDF7.js +0 -1
  147. package/dist/web/assets/console-B7dWYAoU.js +0 -1
  148. package/dist/web/assets/conversation-launcher-settings-D1xhW1S5.js +0 -1
  149. package/dist/web/assets/index-Ch1jmkyO.css +0 -1
  150. package/dist/web/assets/index-DRd-2Tii.js +0 -2
@@ -32,7 +32,7 @@ class StoreKnowledge:
32
32
  self, actor, space, request.personal_project_id
33
33
  )
34
34
  if request.project_agent_id:
35
- await authorize_project_agent(
35
+ agent = await authorize_project_agent(
36
36
  self,
37
37
  actor,
38
38
  space,
@@ -41,6 +41,7 @@ class StoreKnowledge:
41
41
  require_active=True,
42
42
  require_memory=True,
43
43
  )
44
+ request = request.model_copy(update={'project_agent_id': agent['agent_id']})
44
45
  return await self._commit_episode(
45
46
  space,
46
47
  request.episode,
@@ -65,7 +66,7 @@ class StoreKnowledge:
65
66
  self, actor, space, request.personal_project_id
66
67
  )
67
68
  if request.project_agent_id:
68
- await authorize_project_agent(
69
+ agent = await authorize_project_agent(
69
70
  self,
70
71
  actor,
71
72
  space,
@@ -74,6 +75,7 @@ class StoreKnowledge:
74
75
  require_active=True,
75
76
  require_memory=True,
76
77
  )
78
+ request = request.model_copy(update={'project_agent_id': agent['agent_id']})
77
79
  return await self._commit_episode(
78
80
  space,
79
81
  request.episode,
@@ -0,0 +1,138 @@
1
+ import hashlib
2
+ import json
3
+
4
+ from fastapi import HTTPException
5
+
6
+ from .personal_project_access import authorize_personal_project
7
+ from .project_agent_access import authorize_project_agent
8
+ from .project_agent_attention_models import (
9
+ AgentAttentionCreate, AgentAttentionDecision, AgentAttentionList,
10
+ AgentAttentionRecord,
11
+ )
12
+ from .provider_values import native_datetime, now_utc, stable_uuid
13
+ from .system_hr_identity import resolve_hr_alias
14
+
15
+
16
+ class StoreProjectAgentAttention:
17
+ """Explicit human requests; never infer attention from worker/task status."""
18
+
19
+ async def create_agent_attention(self, actor, request: AgentAttentionCreate):
20
+ self._require_personal()
21
+ space = await self.authorize(actor, request.personal_space_id, 'maintainer')
22
+ agent_id = await resolve_hr_alias(self, request.personal_space_id, request.agent_id)
23
+ await authorize_project_agent(
24
+ self, actor, space, request.personal_project_id, agent_id,
25
+ require_active=True,
26
+ allow_unassigned=agent_id in {
27
+ 'employee.bole', 'employee.jefa', 'fuli-project-coordinator',
28
+ },
29
+ )
30
+ if request.task_id:
31
+ task = await self._find_task_row(
32
+ request.personal_space_id, request.task_id,
33
+ request.personal_project_id,
34
+ )
35
+ if not task:
36
+ raise HTTPException(status_code=404, detail='project Agent task not found')
37
+ payload = request.model_dump(exclude={'idempotency_key'})
38
+ fingerprint = hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
39
+ payload['agent_id'] = agent_id
40
+ request_id = stable_uuid(
41
+ self.settings.provider_id, request.personal_space_id,
42
+ request.personal_project_id, 'agent-attention', request.idempotency_key,
43
+ )
44
+ timestamp = now_utc()
45
+ rows, _, _ = await self.runtime.driver.execute_query(
46
+ '''
47
+ MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})
48
+ MERGE (request:FuliAgentAttention {id: $request_id})
49
+ ON CREATE SET request += $payload,
50
+ request.request_id = $request_id,
51
+ request.fingerprint = $fingerprint,
52
+ request.status = 'open', request.revision = 0,
53
+ request.created_at = $timestamp,
54
+ request.updated_at = $timestamp
55
+ MERGE (space)-[:HAS_AGENT_ATTENTION]->(request)
56
+ RETURN request
57
+ ''',
58
+ personal_space_id=request.personal_space_id, request_id=request_id,
59
+ payload=payload, fingerprint=fingerprint, timestamp=timestamp,
60
+ )
61
+ raw = dict(rows[0]['request'])
62
+ if raw['fingerprint'] != fingerprint:
63
+ raise HTTPException(status_code=409, detail='attention idempotency key conflict')
64
+ return attention_record(raw)
65
+
66
+ async def list_agent_attention(
67
+ self, actor, personal_space_id, personal_project_id=None,
68
+ agent_id=None, status='open', limit=100, offset=0,
69
+ ):
70
+ self._require_personal()
71
+ space = await self.authorize(actor, personal_space_id, 'reader')
72
+ agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
73
+ if personal_project_id:
74
+ await authorize_personal_project(self, actor, space, personal_project_id)
75
+ rows, _, _ = await self.runtime.driver.execute_query(
76
+ '''
77
+ MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
78
+ [:HAS_AGENT_ATTENTION]->(request:FuliAgentAttention)
79
+ WHERE ($project_id IS NULL OR request.personal_project_id = $project_id)
80
+ AND ($agent_id IS NULL OR request.agent_id = $agent_id)
81
+ AND ($status IS NULL OR request.status = $status)
82
+ WITH request ORDER BY request.created_at DESC, request.request_id
83
+ WITH collect(request) AS requests
84
+ RETURN requests[$offset..($offset + $limit)] AS items,
85
+ size(requests) AS total,
86
+ [r IN requests WHERE r.status = 'open' | r.agent_id] AS open_agents
87
+ ''', space_id=personal_space_id, project_id=personal_project_id,
88
+ agent_id=agent_id, status=status, limit=limit, offset=offset, routing_='r',
89
+ )
90
+ row = rows[0] if rows else {'items': [], 'total': 0, 'open_agents': []}
91
+ counts = {}
92
+ for agent in row['open_agents']:
93
+ counts[agent] = counts.get(agent, 0) + 1
94
+ return AgentAttentionList(
95
+ items=[attention_record(dict(item)) for item in row['items']],
96
+ total=row['total'], counts=counts,
97
+ )
98
+
99
+ async def decide_agent_attention(
100
+ self, actor, request: AgentAttentionDecision, *, human: bool,
101
+ ):
102
+ self._require_personal()
103
+ space = await self.authorize(actor, request.personal_space_id, 'maintainer')
104
+ await authorize_personal_project(self, actor, space, request.personal_project_id)
105
+ timestamp = now_utc()
106
+ # Increment obtains a write lock before the revision predicate is checked.
107
+ # A stale caller cannot overwrite the response recorded by another client.
108
+ rows, _, _ = await self.runtime.driver.execute_query(
109
+ '''
110
+ MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
111
+ [:HAS_AGENT_ATTENTION]->(request:FuliAgentAttention {
112
+ request_id: $request_id, personal_project_id: $project_id
113
+ })
114
+ SET request.lock_version = coalesce(request.lock_version, 0) + 1
115
+ WITH request
116
+ WHERE request.revision = $revision AND request.status = 'open'
117
+ SET request.status = $status, request.response = $response,
118
+ request.responded_by = $responded_by,
119
+ request.revision = request.revision + 1,
120
+ request.updated_at = $timestamp
121
+ RETURN request
122
+ ''', space_id=request.personal_space_id,
123
+ project_id=request.personal_project_id, request_id=request.request_id,
124
+ revision=request.expected_revision, response=request.response,
125
+ status='resolved' if human else 'cancelled',
126
+ responded_by='human' if human else 'agent', timestamp=timestamp,
127
+ )
128
+ if not rows:
129
+ raise HTTPException(status_code=409, detail='attention changed or is no longer open; reload it')
130
+ return attention_record(dict(rows[0]['request']))
131
+
132
+
133
+ def attention_record(raw):
134
+ return AgentAttentionRecord(**{
135
+ key: native_datetime(raw.get(key)) if key in {'created_at', 'updated_at'} else raw.get(key)
136
+ for key in AgentAttentionRecord.model_fields
137
+ if key in raw
138
+ })
@@ -22,6 +22,7 @@ from .project_agent_executor_models import (
22
22
  from .project_agent_models import ProjectAgentModelStrategy
23
23
  from .provider_values import native_datetime, now_utc, stable_uuid
24
24
  from .store_transactions import query_store_transaction
25
+ from .system_hr_identity import resolve_hr_alias
25
26
 
26
27
 
27
28
  _TERMINAL_NEUTRAL = {'cancelled'}
@@ -276,6 +277,7 @@ class StoreProjectAgentExecutorLearning:
276
277
  ) -> list[ProjectAgentExecutorOutcomeEvidenceRecord]:
277
278
  self._require_personal()
278
279
  space = await self.authorize(actor, personal_space_id, 'reader')
280
+ agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
279
281
  await authorize_personal_project(
280
282
  self,
281
283
  actor,
@@ -334,6 +336,7 @@ class StoreProjectAgentExecutorLearning:
334
336
 
335
337
  self._require_personal()
336
338
  space = await self.authorize(actor, personal_space_id, 'reader')
339
+ agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
337
340
  if personal_project_id is not None:
338
341
  await authorize_personal_project(
339
342
  self,
@@ -367,12 +370,17 @@ class StoreProjectAgentExecutorLearning:
367
370
  aggregates: list[ProjectAgentExecutorOutcomeAggregate] = []
368
371
  for row in records:
369
372
  raw = dict(row.get('aggregate') or {})
373
+ identity = raw.get('agent_id')
370
374
  aggregate_json = raw.get('aggregate_json')
371
375
  if aggregate_json:
372
376
  try:
373
377
  raw.update(json.loads(aggregate_json))
374
378
  except (TypeError, ValueError):
375
379
  pass
380
+ if raw.get('hr_identity_original_refs_json'):
381
+ raw['agent_id'] = identity
382
+ raw.pop('hr_identity_original_refs_json', None)
383
+ raw.pop('hr_identity_original_id', None)
376
384
  # Storage identities are intentionally internal: the public model
377
385
  # remains the provider-neutral bucket contract.
378
386
  raw.pop('id', None)
@@ -16,6 +16,7 @@ from .project_agent_memory_models import (
16
16
  ProjectAgentMemoryWrite,
17
17
  )
18
18
  from .provider_values import now_utc, stable_uuid
19
+ from .system_hr_identity import LEGACY_HR_AGENT_ID, SYSTEM_HR_AGENT_ID, resolve_hr_alias
19
20
 
20
21
 
21
22
  class StoreProjectAgentMemory:
@@ -24,7 +25,7 @@ class StoreProjectAgentMemory:
24
25
  ) -> ProjectAgentMemoryView:
25
26
  self._require_personal()
26
27
  space = await self.authorize(actor, personal_space_id, 'reader')
27
- await authorize_project_agent(
28
+ agent = await authorize_project_agent(
28
29
  self, actor, space, personal_project_id, agent_id, require_memory=True,
29
30
  )
30
31
  records, _, _ = await self.runtime.driver.execute_query(
@@ -34,21 +35,21 @@ class StoreProjectAgentMemory:
34
35
  RETURN checkpoint.record_json AS record_json
35
36
  ORDER BY checkpoint.revision DESC LIMIT $limit
36
37
  ''',
37
- memory_id=self._agent_memory_id(
38
- personal_space_id, personal_project_id, agent_id,
39
- ),
38
+ memory_id=await self._resolved_agent_memory_id(
39
+ personal_space_id, personal_project_id, agent['agent_id']),
40
40
  limit=max(1, min(limit, 10)),
41
41
  routing_='r',
42
42
  )
43
43
  history = [
44
- ProjectAgentMemoryRecord.model_validate_json(row['record_json'])
44
+ ProjectAgentMemoryRecord.model_validate_json(row['record_json']).model_copy(
45
+ update={'agent_id': agent['agent_id']})
45
46
  for row in records
46
47
  ]
47
48
  current = history[0] if history else None
48
49
  return ProjectAgentMemoryView(
49
50
  personal_space_id=personal_space_id,
50
51
  personal_project_id=personal_project_id,
51
- agent_id=agent_id,
52
+ agent_id=agent['agent_id'],
52
53
  revision=current.revision if current else 0,
53
54
  current=current,
54
55
  history=history,
@@ -78,14 +79,14 @@ class StoreProjectAgentMemory:
78
79
  ''',
79
80
  space_id=request.personal_space_id,
80
81
  project_id=request.personal_project_id,
81
- agent_id=request.agent_id,
82
+ agent_id=agent['agent_id'],
82
83
  task_id=request.task_id,
83
84
  routing_='r',
84
85
  )
85
86
  if not task_rows:
86
87
  raise HTTPException(status_code=404, detail='Agent memory task not found')
87
- memory_id = self._agent_memory_id(
88
- request.personal_space_id, request.personal_project_id, request.agent_id,
88
+ memory_id = await self._resolved_agent_memory_id(
89
+ request.personal_space_id, request.personal_project_id, agent['agent_id'],
89
90
  )
90
91
  checkpoint_id = stable_uuid(memory_id, request.idempotency_key)
91
92
  payload = request.model_dump(mode='json', exclude={
@@ -98,7 +99,7 @@ class StoreProjectAgentMemory:
98
99
  checkpoint_id=checkpoint_id,
99
100
  personal_space_id=request.personal_space_id,
100
101
  personal_project_id=request.personal_project_id,
101
- agent_id=request.agent_id,
102
+ agent_id=agent['agent_id'],
102
103
  revision=request.expected_revision + 1,
103
104
  memory=request.memory,
104
105
  source_application=request.source_application,
@@ -147,7 +148,7 @@ class StoreProjectAgentMemory:
147
148
  ''',
148
149
  space_id=request.personal_space_id,
149
150
  project_id=request.personal_project_id,
150
- agent_id=request.agent_id,
151
+ agent_id=agent['agent_id'],
151
152
  memory_id=memory_id,
152
153
  checkpoint_id=checkpoint_id,
153
154
  expected_revision=request.expected_revision,
@@ -164,9 +165,35 @@ class StoreProjectAgentMemory:
164
165
  status_code=409,
165
166
  detail='Agent memory changed; reload its latest revision before merging notes',
166
167
  )
167
- if row['payload_hash'] != payload_hash:
168
+ compatible_hashes = {payload_hash}
169
+ if LEGACY_HR_AGENT_ID in agent.get('legacy_agent_ids', []):
170
+ # Old checkpoints keep their exact IDs/JSON/hashes. A canonical
171
+ # retry of the same input may differ only in the identity spelling.
172
+ for identity in (LEGACY_HR_AGENT_ID, SYSTEM_HR_AGENT_ID):
173
+ compatible_hashes.add(hashlib.sha256(json.dumps(
174
+ {**payload, 'agent_id': identity}, sort_keys=True,
175
+ ensure_ascii=False, separators=(',', ':'),
176
+ ).encode()).hexdigest())
177
+ if row['payload_hash'] not in compatible_hashes:
168
178
  raise HTTPException(status_code=409, detail='memory idempotency key has different input')
169
- return ProjectAgentMemoryRecord.model_validate_json(row['record_json'])
179
+ return ProjectAgentMemoryRecord.model_validate_json(row['record_json']).model_copy(
180
+ update={'agent_id': agent['agent_id']})
181
+
182
+ async def _resolved_agent_memory_id(self, space_id, project_id, agent_id):
183
+ agent_id = await resolve_hr_alias(self, space_id, agent_id)
184
+ if agent_id == SYSTEM_HR_AGENT_ID:
185
+ rows, _, _ = await self.runtime.driver.execute_query(
186
+ '''MATCH (:FuliSpace {id: $space_id})-[:HAS_PROJECT_AGENT_IDENTITY]->
187
+ (:FuliProjectAgent {agent_id: $agent_id})-[:HAS_WORKING_MEMORY]->
188
+ (head:FuliProjectAgentMemory {personal_space_id: $space_id,
189
+ personal_project_id: $project_id})
190
+ RETURN DISTINCT head.id AS id''',
191
+ space_id=space_id, project_id=project_id, agent_id=agent_id, routing_='r')
192
+ if len(rows) > 1:
193
+ raise HTTPException(409, 'Agent has conflicting memory heads')
194
+ if rows:
195
+ return rows[0]['id']
196
+ return self._agent_memory_id(space_id, project_id, agent_id)
170
197
 
171
198
  def _agent_memory_id(self, space_id, project_id, agent_id):
172
199
  return stable_uuid(
@@ -24,6 +24,7 @@ from .project_agent_task_models import (
24
24
  ProjectAgentRoutingDecisionRecord,
25
25
  )
26
26
  from .provider_values import native_datetime, now_utc, stable_uuid
27
+ from .store_project_agents import SYSTEM_HR_AGENT_ID
27
28
  from .store_transactions import query_store_transaction
28
29
 
29
30
 
@@ -744,9 +745,11 @@ class StoreProjectAgentTaskRecruitment:
744
745
  MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})-
745
746
  [:HAS_PROJECT_AGENT_IDENTITY]->
746
747
  (hr:FuliProjectAgent {agent_type: 'hr', status: 'active'})
747
- RETURN hr ORDER BY hr.created_at, hr.agent_id LIMIT 1
748
+ RETURN hr ORDER BY CASE WHEN hr.agent_id = $system_hr_agent_id
749
+ THEN 0 ELSE 1 END, hr.created_at, hr.agent_id LIMIT 1
748
750
  ''',
749
751
  personal_space_id=request.personal_space_id,
752
+ system_hr_agent_id=SYSTEM_HR_AGENT_ID,
750
753
  routing_='r',
751
754
  )
752
755
  hr_agent_id = (
@@ -7,6 +7,7 @@ from fastapi import HTTPException
7
7
  from .project_agent_access import authorize_project_agent
8
8
  from .project_agent_models import ProjectAgentProfile
9
9
  from .project_agent_task_models import ProjectAgentParallelPlan
10
+ from .store_project_agents import SYSTEM_HR_AGENT_ID
10
11
 
11
12
 
12
13
  class StoreProjectAgentTaskStaffing:
@@ -634,9 +635,11 @@ class StoreProjectAgentTaskStaffing:
634
635
  MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})-
635
636
  [:HAS_PROJECT_AGENT_IDENTITY]->
636
637
  (hr:FuliProjectAgent {agent_type: 'hr', status: 'active'})
637
- RETURN hr LIMIT 1
638
+ RETURN hr ORDER BY CASE WHEN hr.agent_id = $system_hr_agent_id
639
+ THEN 0 ELSE 1 END, hr.created_at, hr.agent_id LIMIT 1
638
640
  ''',
639
641
  personal_space_id=request.personal_space_id,
642
+ system_hr_agent_id=SYSTEM_HR_AGENT_ID,
640
643
  routing_='r',
641
644
  )
642
645
  if not hr_records:
@@ -2,6 +2,7 @@ import json
2
2
  from collections import defaultdict
3
3
  from datetime import date
4
4
  from fastapi import HTTPException
5
+ from .system_hr_identity import resolve_hr_alias
5
6
  from .personal_project_access import authorize_personal_project
6
7
  from .project_agent_access import authorize_project_agent
7
8
  from .project_agent_models import ProjectAgentModelStrategy, ProjectAgentProfile
@@ -470,6 +471,7 @@ class StoreProjectAgentTasks(
470
471
  ) -> list[ProjectAgentTaskRecord]:
471
472
  self._require_personal()
472
473
  space = await self.authorize(actor, personal_space_id, 'reader')
474
+ agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
473
475
  if personal_project_id:
474
476
  await authorize_personal_project(
475
477
  self,
@@ -510,6 +512,7 @@ class StoreProjectAgentTasks(
510
512
  ) -> ProjectAgentActivityResult:
511
513
  self._require_personal()
512
514
  space = await self.authorize(actor, personal_space_id, 'reader')
515
+ agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
513
516
  if personal_project_id:
514
517
  await authorize_personal_project(
515
518
  self,
@@ -18,12 +18,87 @@ from .project_agent_models import (
18
18
  ProjectAgentUpsert,
19
19
  )
20
20
  from .provider_values import native_datetime, now_utc, stable_uuid
21
+ from .project_agent_names import agent_display_name
22
+ from .store_transactions import query_store_transaction
23
+ from .system_hr_identity import LEGACY_HR_AGENT_ID, merge_legacy_hr, resolve_hr_alias
21
24
 
22
25
 
23
26
  SYSTEM_COORDINATOR_AGENT_ID = 'fuli-project-coordinator'
27
+ SYSTEM_HR_AGENT_ID = 'employee.bole'
24
28
 
25
29
 
26
30
  class StoreProjectAgents:
31
+ async def ensure_system_project_hr(
32
+ self,
33
+ actor: dict,
34
+ personal_space_id: str,
35
+ ) -> ProjectAgentRecord:
36
+ self._require_personal()
37
+ await self.authorize(actor, personal_space_id, 'maintainer')
38
+ async with query_store_transaction(self) as scoped:
39
+ await scoped._ensure_system_project_hr_identity(personal_space_id)
40
+ await merge_legacy_hr(scoped, personal_space_id)
41
+ return await self.get_project_agent(actor, personal_space_id, None, SYSTEM_HR_AGENT_ID)
42
+
43
+ async def _ensure_system_project_hr_identity(self, personal_space_id):
44
+ profile = ProjectAgentProfile(
45
+ name='Bole',
46
+ occupation_emoji='🔎',
47
+ responsibility=(
48
+ '维护 Agent 人员分布、当前工作与可审计招募记录,'
49
+ '并在需要新角色时执行受策略约束的招募。'
50
+ ),
51
+ agent_type='hr',
52
+ work_kinds=['agent-recruitment', 'staffing-review'],
53
+ capabilities=[
54
+ 'Agent 招募', '人员分布', '工作状态', '招募审计',
55
+ 'fuli.employee:bole',
56
+ ],
57
+ initial_preferences=[
58
+ '每次招募保留任务、原因、触发来源与时间线。',
59
+ '只展示 Provider 已记录的人员状态,不推测未上报的工作。',
60
+ ],
61
+ status='active',
62
+ )
63
+ updated_at = now_utc()
64
+ node_id = stable_uuid(
65
+ self.settings.provider_id,
66
+ personal_space_id,
67
+ 'project-agent',
68
+ SYSTEM_HR_AGENT_ID,
69
+ )
70
+ await self.runtime.driver.execute_query(
71
+ '''
72
+ MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})
73
+ MERGE (agent:FuliProjectAgent {id: $id})
74
+ ON CREATE SET agent.agent_id = $agent_id,
75
+ agent.created_at = $updated_at,
76
+ agent.system_managed = true
77
+ ON CREATE SET agent.profile_json = $profile_json,
78
+ agent.name = $name,
79
+ agent.occupation_emoji = $occupation_emoji,
80
+ agent.responsibility = $responsibility,
81
+ agent.capabilities = $capabilities,
82
+ agent.work_kinds = $work_kinds,
83
+ agent.agent_type = 'hr',
84
+ agent.memory_scope = 'reviewed_agent',
85
+ agent.status = 'active',
86
+ agent.updated_at = $updated_at
87
+ MERGE (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(agent)
88
+ RETURN agent
89
+ ''',
90
+ personal_space_id=personal_space_id,
91
+ id=node_id,
92
+ agent_id=SYSTEM_HR_AGENT_ID,
93
+ profile_json=profile.model_dump_json(),
94
+ name=profile.name,
95
+ occupation_emoji=profile.occupation_emoji,
96
+ responsibility=profile.responsibility,
97
+ capabilities=profile.capabilities,
98
+ work_kinds=profile.work_kinds,
99
+ updated_at=updated_at,
100
+ )
101
+
27
102
  async def ensure_system_project_coordinator(
28
103
  self,
29
104
  actor: dict,
@@ -93,6 +168,8 @@ class StoreProjectAgents:
93
168
  recruitment_id: str | None = None,
94
169
  ) -> ProjectAgentRecord:
95
170
  self._require_personal()
171
+ if request.agent_id == LEGACY_HR_AGENT_ID:
172
+ raise HTTPException(409, 'This HR identity has moved to employee.bole')
96
173
  if request.profile.agent_type == 'temporary' and not recruitment_id:
97
174
  raise HTTPException(
98
175
  status_code=422,
@@ -106,6 +183,14 @@ class StoreProjectAgents:
106
183
  status_code=422,
107
184
  detail='only the system-managed identity may use coordinator type',
108
185
  )
186
+ if (
187
+ request.profile.agent_type == 'hr'
188
+ and request.agent_id != SYSTEM_HR_AGENT_ID
189
+ ):
190
+ raise HTTPException(
191
+ status_code=422,
192
+ detail='only the system-managed Bole identity may use HR type',
193
+ )
109
194
  if (
110
195
  request.agent_id == SYSTEM_COORDINATOR_AGENT_ID
111
196
  and request.profile.agent_type != 'coordinator'
@@ -114,6 +199,19 @@ class StoreProjectAgents:
114
199
  status_code=422,
115
200
  detail='the system coordinator identity cannot change Agent type',
116
201
  )
202
+ if (
203
+ request.agent_id == SYSTEM_HR_AGENT_ID
204
+ and request.profile.agent_type != 'hr'
205
+ ):
206
+ raise HTTPException(
207
+ status_code=422,
208
+ detail='the system HR identity cannot change Agent type',
209
+ )
210
+ if request.agent_id == SYSTEM_HR_AGENT_ID and request.profile.status != 'active':
211
+ raise HTTPException(
212
+ status_code=422,
213
+ detail='the system HR must remain active',
214
+ )
117
215
  space = await self.authorize(actor, request.personal_space_id, 'maintainer')
118
216
  if request.personal_project_id:
119
217
  await authorize_personal_project(
@@ -344,6 +442,7 @@ class StoreProjectAgents:
344
442
  ) -> ProjectAgentRecord:
345
443
  self._require_personal()
346
444
  space = await self.authorize(actor, personal_space_id, 'reader')
445
+ agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
347
446
  if personal_project_id:
348
447
  await authorize_personal_project(
349
448
  self,
@@ -391,6 +490,11 @@ class StoreProjectAgents:
391
490
  status_code=422,
392
491
  detail='the system coordinator cannot be archived',
393
492
  )
493
+ if agent_id == SYSTEM_HR_AGENT_ID:
494
+ raise HTTPException(
495
+ status_code=422,
496
+ detail='the system HR cannot be archived',
497
+ )
394
498
  await self.authorize(actor, personal_space_id, 'maintainer')
395
499
  if not reason.strip():
396
500
  raise HTTPException(status_code=422, detail='archive reason is required')
@@ -696,6 +800,7 @@ class StoreProjectAgents:
696
800
  ) -> list[ProjectAgentAssignmentRecord]:
697
801
  self._require_personal()
698
802
  space = await self.authorize(actor, personal_space_id, 'reader')
803
+ agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
699
804
  if personal_project_id:
700
805
  await authorize_personal_project(
701
806
  self,
@@ -974,6 +1079,9 @@ class StoreProjectAgents:
974
1079
  ) -> ProjectAgentRecord:
975
1080
  raw = dict(row['agent'])
976
1081
  profile = ProjectAgentProfile.model_validate_json(raw['profile_json'])
1082
+ profile = profile.model_copy(update={
1083
+ 'display_name': agent_display_name(raw['agent_id'], profile.name, profile.display_name),
1084
+ })
977
1085
  # ``occupation_emoji`` was introduced after the original profile JSON
978
1086
  # contract. Prefer the durable profile value, but tolerate an older
979
1087
  # JSON payload while the explicit node property is being backfilled.
@@ -1066,6 +1174,7 @@ class StoreProjectAgents:
1066
1174
  })
1067
1175
  return ProjectAgentRecord(
1068
1176
  agent_id=raw['agent_id'],
1177
+ legacy_agent_ids=list(raw.get('legacy_agent_ids') or []),
1069
1178
  personal_space_id=personal_space_id,
1070
1179
  personal_project_id=projection_project_id,
1071
1180
  profile=profile,
@@ -143,7 +143,7 @@ class StoreTaskContexts:
143
143
  # The existing checkpoint lets pre-upgrade claims retry without new writes.
144
144
  memory_checkpoint_id = None
145
145
  if record['project_agent_id'] and record['personal_project_id']:
146
- memory_checkpoint_id = stable_uuid(self._agent_memory_id(
146
+ memory_checkpoint_id = stable_uuid(await self._resolved_agent_memory_id(
147
147
  request.personal_space_id, record['personal_project_id'],
148
148
  record['project_agent_id'],
149
149
  ), f'{token}:memory')