fuli-context 0.12.0 → 0.12.2

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 (160) hide show
  1. package/README.md +14 -6
  2. package/README.zh-CN.md +9 -4
  3. package/dist/web/assets/{AboutPage-BAD0iPVX.js → AboutPage-Dized8Uz.js} +1 -1
  4. package/dist/web/assets/{AgentConversations-1iGTd4sz.js → AgentConversations-Ccy41mEA.js} +1 -1
  5. package/dist/web/assets/{AgentHand-D_RafKOM.js → AgentHand-UzVhbKUC.js} +1 -1
  6. package/dist/web/assets/{AgentName-B5LKQ09q.js → AgentName-DJpvkub2.js} +1 -1
  7. package/dist/web/assets/AgentProfilePage-CFViQ8kj.js +1 -0
  8. package/dist/web/assets/AgentsDirectoryPage-CBFTg_IE.js +1 -0
  9. package/dist/web/assets/AgentsDirectoryPage-Cv-vxw2C.css +1 -0
  10. package/dist/web/assets/{ConnectionsPage-Tr2rRVOZ.js → ConnectionsPage-uXcabab_.js} +1 -1
  11. package/dist/web/assets/EmployeeRecruitDialog-D8Hvhc-M.js +1 -0
  12. package/dist/web/assets/EmployeeWorkbenchPage-TBl_HNj6.js +3 -0
  13. package/dist/web/assets/KnowledgeEditDialog-HmiqLC-Y.js +1 -0
  14. package/dist/web/assets/KnowledgeInspector-Cj2yK4Qh.js +1 -0
  15. package/dist/web/assets/KnowledgeOrganizerPage-DT3BROZm.js +1 -0
  16. package/dist/web/assets/KnowledgePage-C4J4VueL.js +1 -0
  17. package/dist/web/assets/KnowledgeWorkspace-DA_dqVRG.js +4 -0
  18. package/dist/web/assets/{NotFoundPage-fetSnp0N.js → NotFoundPage-x_Yd59mm.js} +1 -1
  19. package/dist/web/assets/{OverviewPage-BD4yY2Xz.js → OverviewPage-3eZjQ26v.js} +1 -1
  20. package/dist/web/assets/OverviewPage-8LBN8Shi.css +1 -0
  21. package/dist/web/assets/PersonalProfilePage-CDQk096L.js +1 -0
  22. package/dist/web/assets/PersonalProjectsPage-CJxp4Wyz.js +1 -0
  23. package/dist/web/assets/{ProjectAgentFirstTask-BcxqmKXQ.js → ProjectAgentFirstTask-BkRJ0zwO.js} +1 -1
  24. package/dist/web/assets/{ProjectAgentsPage-BWDJBESb.js → ProjectAgentsPage-BuNNqSEy.js} +8 -8
  25. package/dist/web/assets/{ProjectScopePicker-yodM7lF_.js → ProjectScopePicker-CiDxM2Yx.js} +1 -1
  26. package/dist/web/assets/PublicProjectsPage-ByOT559r.js +1 -0
  27. package/dist/web/assets/{ReviewPage-Cy0Rt4CO.js → ReviewPage-C0Vdr9uq.js} +1 -1
  28. package/dist/web/assets/RoundtablesPage-Drou-B81.css +1 -0
  29. package/dist/web/assets/RoundtablesPage-L3ShEWS3.js +1 -0
  30. package/dist/web/assets/SettingsPage-BE8pWbI6.css +1 -0
  31. package/dist/web/assets/SettingsPage-McYuinGz.js +1 -0
  32. package/dist/web/assets/{SharedJefaReportPage-C56kPciB.js → SharedJefaReportPage-Bg6lIk3E.js} +1 -1
  33. package/dist/web/assets/UiButton-x8hiHNRM.js +1 -0
  34. package/dist/web/assets/UiDialog-mjletK0y.js +1 -0
  35. package/dist/web/assets/{UiDisclosure-D0iMTplF.js → UiDisclosure-_IxeiexW.js} +1 -1
  36. package/dist/web/assets/{UiSegmented-DIJW6wv3.js → UiSegmented-WFfz9Evl.js} +1 -1
  37. package/dist/web/assets/{UiSelect-CMR0QqF6.js → UiSelect-K29jnV-1.js} +1 -1
  38. package/dist/web/assets/{WritingTastePage-QgNzssfF.js → WritingTastePage-DCULB-d2.js} +1 -1
  39. package/dist/web/assets/agent-pins-DS4mNsNt.js +1 -0
  40. package/dist/web/assets/agent-profile-Bh3MJGvb.css +1 -0
  41. package/dist/web/assets/{attention-store-B1DI8wmd.js → attention-store-CA5y8j1R.js} +1 -1
  42. package/dist/web/assets/catalog-D0VEfOAV.js +1 -0
  43. package/dist/web/assets/{client-CR-W5Wzp.js → client-DnwnUBQV.js} +1 -1
  44. package/dist/web/assets/{console-D7GHOMEC.js → console-Cw7TzOSC.js} +1 -1
  45. package/dist/web/assets/conversation-launcher-settings-Yc13viwr.js +1 -0
  46. package/dist/web/assets/{i18n-CVFhT_k7.js → i18n-oJFlt8hW.js} +3 -3
  47. package/dist/web/assets/{identity-CM6Dmy00.js → identity-DSGS4CZc.js} +1 -1
  48. package/dist/web/assets/index-BfHCcUL2.css +1 -0
  49. package/dist/web/assets/index-CxNER7Oo.js +5 -0
  50. package/dist/web/assets/{judgment-types-CJBEyJs1.js → judgment-types-DR931cZ7.js} +1 -1
  51. package/dist/web/assets/{model-WFhQZ3v_.js → model-BJ-UF4cs.js} +1 -1
  52. package/dist/web/assets/{personal-profile-graph-BwCh2M9G.js → personal-profile-graph-SOK0BR0w.js} +1 -1
  53. package/dist/web/assets/project-team-api-C_0qJk_B.js +1 -0
  54. package/dist/web/assets/{shared-report-CeIvrL9a.js → shared-report-CiX64ZiD.js} +1 -1
  55. package/dist/web/assets/useAgentRoster-B84mzZIg.js +1 -0
  56. package/dist/web/assets/{useApi-CROJJdhE-BYhbjaZB.js → useApi-CROJJdhE-7gt0QyQ5.js} +1 -1
  57. package/dist/web/assets/{vue-router-DaxrQwX3.js → vue-router-CCvPGaLD.js} +1 -1
  58. package/dist/web/index.html +15 -14
  59. package/dist/web/licenses/ant-design-icons.txt +22 -0
  60. package/docs/agent-conversations-and-collaboration.md +3 -3
  61. package/docs/agent-roundtable.md +33 -0
  62. package/docs/client-connections.md +108 -0
  63. package/docs/employee-agents.md +3 -3
  64. package/docs/project-agent-memory.md +6 -6
  65. package/graph-provider/fuli_graph/default_project_lead.py +1 -1
  66. package/graph-provider/fuli_graph/project_agent_delegations.py +70 -2
  67. package/graph-provider/fuli_graph/project_agent_memory_routes.py +18 -1
  68. package/graph-provider/fuli_graph/project_agent_remote_origins.py +154 -0
  69. package/graph-provider/fuli_graph/project_agent_task_models.py +1 -1
  70. package/graph-provider/fuli_graph/runtime.py +7 -0
  71. package/graph-provider/fuli_graph/store_project_agent_context.py +3 -2
  72. package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +1 -1
  73. package/graph-provider/fuli_graph/store_project_agent_task_reads.py +1 -1
  74. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +5 -5
  75. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +2 -2
  76. package/graph-provider/fuli_graph/store_project_agent_tasks.py +2 -2
  77. package/graph-provider/fuli_graph/store_project_agents.py +6 -6
  78. package/graph-provider/fuli_graph/system_agents.py +12 -2
  79. package/graph-provider/fuli_graph/system_hr_identity.py +2 -2
  80. package/graph-provider/pyproject.toml +2 -1
  81. package/npm-shrinkwrap.json +280 -5
  82. package/package.json +6 -2
  83. package/skills/operating-fuli/SKILL.md +3 -3
  84. package/src/agent-peer/coordinator-http.js +117 -0
  85. package/src/agent-peer/coordinator-service.js +258 -0
  86. package/src/agent-peer/coordinator-store.js +95 -0
  87. package/src/agent-peer/device-identity.js +70 -0
  88. package/src/agent-peer/http.js +47 -0
  89. package/src/agent-peer/node-store.js +127 -0
  90. package/src/agent-peer/origin-proof.js +56 -0
  91. package/src/agent-peer/peer-port.js +140 -0
  92. package/src/agent-peer/peer-runtime.js +298 -0
  93. package/src/agent-peer/peer-transport.js +78 -0
  94. package/src/agent-peer/protocol.js +161 -0
  95. package/src/agent-peer/runner.js +251 -0
  96. package/src/agent-roundtable/http.js +17 -3
  97. package/src/agent-roundtable/service.js +272 -43
  98. package/src/agent-roundtable/tools.js +15 -1
  99. package/src/agent-roundtable/wake.js +12 -3
  100. package/src/agent-tools/graph-definitions.js +14 -5
  101. package/src/agent-tools/graph-handlers.js +1 -0
  102. package/src/agent-tools/interface-catalog.js +10 -1
  103. package/src/agent-tools/project-agent-definitions.js +1 -1
  104. package/src/agents/identity-receipt.js +1 -1
  105. package/src/cli/command-registry.js +3 -0
  106. package/src/cli/connect-command.js +31 -0
  107. package/src/cli/mcp-command.js +101 -0
  108. package/src/cli/peer-command.js +100 -0
  109. package/src/cli/setup-agent-selection.js +2 -2
  110. package/src/cli/setup-command.js +12 -5
  111. package/src/cli.js +16 -0
  112. package/src/employees/catalog/bole.json +2 -2
  113. package/src/graphiti/federated-application.js +4 -1
  114. package/src/graphiti/project-agent-control-plane.js +7 -0
  115. package/src/graphiti/project-agent-task-entry.js +4 -4
  116. package/src/graphiti/project-agent-team-context.js +16 -18
  117. package/src/graphiti/project-team-view.js +82 -0
  118. package/src/graphiti/provider-client.js +9 -0
  119. package/src/graphiti/task-entry-preferences.js +1 -1
  120. package/src/http/api-router.js +10 -1
  121. package/src/http/graph-api-router.js +7 -0
  122. package/src/mcp/runtime.js +2 -1
  123. package/src/mcp/tool-annotations.js +4 -3
  124. package/src/mcp/tool-profiles.js +36 -0
  125. package/src/mcp-server.js +2 -100
  126. package/src/server.js +14 -2
  127. package/src/setup/agent-installation-status.js +5 -3
  128. package/src/setup/agents.js +23 -3
  129. package/src/setup/claude-desktop-config.js +60 -0
  130. package/src/setup/client-connection.js +25 -0
  131. package/src/setup/paths.js +3 -1
  132. package/src/setup/setup.js +1 -1
  133. package/src/setup/stable-package-root.js +29 -0
  134. package/src/system/system-service.js +5 -0
  135. package/dist/web/assets/AgentProfilePage-CioOdkBM.js +0 -1
  136. package/dist/web/assets/AgentsDirectoryPage--1A9nvNA.js +0 -1
  137. package/dist/web/assets/AgentsDirectoryPage-C7OUQH9v.css +0 -1
  138. package/dist/web/assets/EmployeeRecruitDialog-1QevD4Qv.js +0 -1
  139. package/dist/web/assets/EmployeeWorkbenchPage-B0vQIgwA.js +0 -3
  140. package/dist/web/assets/KnowledgeEditDialog-ClnrfPZi.js +0 -1
  141. package/dist/web/assets/KnowledgeInspector-Te1hJZbx.js +0 -1
  142. package/dist/web/assets/KnowledgeOrganizerPage-CCCrWl6Y.js +0 -1
  143. package/dist/web/assets/KnowledgePage-DGOcBwr8.js +0 -1
  144. package/dist/web/assets/KnowledgeWorkspace-cKn1NCRe.js +0 -4
  145. package/dist/web/assets/OverviewPage-By-fUzSG.css +0 -1
  146. package/dist/web/assets/PersonalProfilePage-DGJVP_VB.js +0 -1
  147. package/dist/web/assets/PersonalProjectsPage-D25HYMgD.js +0 -1
  148. package/dist/web/assets/PublicProjectsPage-CyNxVzW3.js +0 -1
  149. package/dist/web/assets/RoundtablesPage-BjVhvwMc.js +0 -1
  150. package/dist/web/assets/RoundtablesPage-Cb0R4GaM.css +0 -1
  151. package/dist/web/assets/SettingsPage-CCZqZtnH.js +0 -1
  152. package/dist/web/assets/SettingsPage-DYz5bTwa.css +0 -1
  153. package/dist/web/assets/UiDialog-CvDxg3B1.js +0 -1
  154. package/dist/web/assets/agent-pins-BcJ_qr8n.js +0 -1
  155. package/dist/web/assets/agent-profile-BZj3ezWe.css +0 -1
  156. package/dist/web/assets/catalog-DbXv0_bR.js +0 -1
  157. package/dist/web/assets/conversation-launcher-settings-Dj2GMzCI.js +0 -1
  158. package/dist/web/assets/index-CJ6Jmok9.css +0 -1
  159. package/dist/web/assets/index-CRa3GnYi.js +0 -5
  160. package/dist/web/assets/useAgentRoster-DCArHlxK.js +0 -1
@@ -0,0 +1,154 @@
1
+ """Origin proof for asks this installation sends to Agents on other devices.
2
+
3
+ A device signature only proves which installation sent a message. The
4
+ receiving device also needs proof that this authority, not merely the device,
5
+ let the current project lead's live task send exactly that message. This
6
+ authority signs that statement with a key that never leaves it; the receiver
7
+ checks it with the public key it bound when the owners paired the devices.
8
+ """
9
+
10
+ import json
11
+ from base64 import urlsafe_b64decode, urlsafe_b64encode
12
+ from datetime import datetime, timedelta, timezone
13
+ from hashlib import sha256
14
+
15
+ from fastapi import HTTPException
16
+ from nacl.signing import SigningKey
17
+ from neo4j.exceptions import ConstraintError
18
+ from pydantic import Field
19
+
20
+ from .models import SourceApplication, StrictModel
21
+ from .personal_project_access import authorize_personal_project
22
+
23
+ OPAQUE = r'^[A-Za-z0-9][A-Za-z0-9._:-]*$'
24
+ DIGEST = r'^[0-9a-f]{64}$'
25
+ CANONICAL_TIME = r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$'
26
+ PROOF_VERSION = 'fuli-remote-origin/1'
27
+ MAX_LIFETIME = timedelta(minutes=30)
28
+
29
+
30
+ class AgentRemoteOriginIssue(StrictModel):
31
+ personal_space_id: str = Field(min_length=1, max_length=128)
32
+ task_context_token: str = Field(min_length=1, max_length=256)
33
+ source_application: SourceApplication
34
+ message_id: str = Field(min_length=1, max_length=128, pattern=OPAQUE)
35
+ digest: str = Field(pattern=DIGEST)
36
+ target_node: str = Field(pattern=DIGEST)
37
+ binding_id: str = Field(min_length=1, max_length=128, pattern=OPAQUE)
38
+ expires_at: str = Field(pattern=CANONICAL_TIME)
39
+
40
+
41
+ def _encode(value):
42
+ return urlsafe_b64encode(value).rstrip(b'=').decode()
43
+
44
+
45
+ def _decode(value):
46
+ return urlsafe_b64decode(value + '=' * (-len(value) % 4))
47
+
48
+
49
+ def canonical_time(value):
50
+ return value.astimezone(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.') + f'{value.microsecond // 1000:03d}Z'
51
+
52
+
53
+ class StoreRemoteAgentOrigins:
54
+ async def remote_origin_authority(self, actor, personal_space_id):
55
+ """Public half of this space's origin key; created once and never rotated silently."""
56
+ self._require_personal()
57
+ await self.authorize(actor, personal_space_id, 'maintainer')
58
+ key = await self._remote_origin_key(personal_space_id)
59
+ return {'algorithm': 'Ed25519', 'key_id': key['key_id'], 'public_key': key['public_key']}
60
+
61
+ async def _remote_origin_key(self, space_id):
62
+ candidate = SigningKey.generate()
63
+ public = bytes(candidate.verify_key)
64
+ try:
65
+ rows, _, _ = await self.runtime.driver.execute_query(
66
+ '''MERGE (key:FuliRemoteOriginKey {space_id: $space_id})
67
+ ON CREATE SET key.private_key = $private_key, key.public_key = $public_key,
68
+ key.key_id = $key_id, key.created_at = datetime()
69
+ RETURN key.private_key AS private_key, key.public_key AS public_key, key.key_id AS key_id''',
70
+ space_id=space_id, private_key=_encode(bytes(candidate)),
71
+ public_key=_encode(public), key_id=sha256(public).hexdigest()[:32])
72
+ except ConstraintError:
73
+ # A concurrent first use created the key; everyone must use that one.
74
+ rows, _, _ = await self.runtime.driver.execute_query(
75
+ '''MATCH (key:FuliRemoteOriginKey {space_id: $space_id})
76
+ RETURN key.private_key AS private_key, key.public_key AS public_key, key.key_id AS key_id''',
77
+ space_id=space_id)
78
+ return rows[0]
79
+
80
+ async def issue_remote_agent_origin(self, actor, request):
81
+ self._require_personal()
82
+ space = await self.authorize(actor, request.personal_space_id, 'maintainer')
83
+ expires = datetime.fromisoformat(request.expires_at.replace('Z', '+00:00'))
84
+ now = datetime.now(timezone.utc)
85
+ if expires <= now or expires - now > MAX_LIFETIME:
86
+ raise HTTPException(422, 'A remote ask needs a lifetime of at most 30 minutes')
87
+ context = await self.get_task_context(actor, request.personal_space_id,
88
+ request.task_context_token, request.source_application)
89
+ agent_id = context.get('project_agent_id')
90
+ project_id = context.get('personal_project_id')
91
+ if not agent_id or not project_id or (context.get('checkpoint') or {}).get('phase') == 'complete':
92
+ raise HTTPException(409, 'A remote ask requires an active project task of a FULI Agent')
93
+ await authorize_personal_project(self, actor, space, project_id)
94
+ policy = await self.get_project_agent_coordination_policy(actor, request.personal_space_id, project_id)
95
+ # Members reach other devices through their own lead, never directly.
96
+ if not policy.team_lead_agent_id or agent_id != policy.team_lead_agent_id:
97
+ raise HTTPException(403, 'Only the project lead can ask Agents on other devices')
98
+ agent = await self.get_project_agent(actor, request.personal_space_id, project_id, agent_id)
99
+ if agent.profile.status != 'active':
100
+ raise HTTPException(403, 'The sending Agent is not active')
101
+ task_hash = sha256(request.task_context_token.encode()).hexdigest()
102
+ # The checks above can be overtaken by a new task turn or a lead handoff.
103
+ # Those writers lock the session, the role and the policy; this takes the
104
+ # same locks and re-checks under them before recording the origin.
105
+ rows, _, _ = await self.runtime.driver.execute_query(
106
+ '''MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})-[:HAS_TASK_CONTEXT_SESSION]->
107
+ (session:FuliTaskContextSession)-[:HAS_CONTEXT]->(task:FuliTaskContext {token: $token})
108
+ SET session._fuli_write_lock = true REMOVE session._fuli_write_lock
109
+ WITH space, session, task
110
+ MATCH (space)-[:CONTAINS_PROJECT]->(project:FuliPersonalProject {project_id: $project_id})
111
+ MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(role:FuliProjectAgent {agent_id: $agent_id})
112
+ SET role._task_lifecycle_lock = true REMOVE role._task_lifecycle_lock
113
+ WITH space, session, task, project, role
114
+ MATCH (project)-[:HAS_PROJECT_AGENT_COORDINATION_POLICY]->(policy:FuliProjectAgentCoordinationPolicy)
115
+ SET policy._fuli_write_lock = true REMOVE policy._fuli_write_lock
116
+ WITH session.current_token = task.token AND NOT coalesce(task.completed, false)
117
+ AND task.personal_project_id = $project_id AND task.project_agent_id = $agent_id AS live_task,
118
+ policy.team_lead_agent_id = $agent_id AS lead,
119
+ role.status = 'active' AND EXISTS {
120
+ MATCH (project)-[:HAS_PROJECT_AGENT_ASSIGNMENT]->
121
+ (:FuliProjectAgentAssignment {status: 'active'})-[:ASSIGNED_AGENT]->(role)
122
+ } AS assigned
123
+ WITH live_task, lead, assigned, live_task AND lead AND assigned AS allowed
124
+ FOREACH (ignored IN CASE WHEN allowed THEN [1] ELSE [] END |
125
+ MERGE (origin:FuliRemoteAgentOrigin {space_id: $space_id, message_id: $message_id})
126
+ ON CREATE SET origin.digest = $digest, origin.agent_id = $agent_id, origin.project_id = $project_id,
127
+ origin.target_node = $target_node, origin.binding_id = $binding_id, origin.task_hash = $task_hash,
128
+ origin.issued_at = datetime(), origin.expires_at = datetime($expires_at))
129
+ WITH live_task, lead, assigned, allowed
130
+ OPTIONAL MATCH (origin:FuliRemoteAgentOrigin {space_id: $space_id, message_id: $message_id})
131
+ WITH live_task, lead, assigned,
132
+ allowed AND origin.digest = $digest AND origin.agent_id = $agent_id AND origin.task_hash = $task_hash
133
+ AND origin.target_node = $target_node AND origin.binding_id = $binding_id AS same
134
+ RETURN live_task, lead, assigned, same''',
135
+ space_id=request.personal_space_id, token=request.task_context_token, message_id=request.message_id,
136
+ digest=request.digest, agent_id=agent_id, project_id=project_id, target_node=request.target_node,
137
+ binding_id=request.binding_id, task_hash=task_hash, expires_at=request.expires_at)
138
+ row = rows[0] if rows else None
139
+ if not row or not row['live_task']:
140
+ raise HTTPException(404, 'The sending task is no longer current')
141
+ if not row['lead'] or not row['assigned']:
142
+ raise HTTPException(403, 'Only the project lead can ask Agents on other devices')
143
+ if not row['same']:
144
+ raise HTTPException(409, 'This message ID already belongs to a different remote ask')
145
+ key = await self._remote_origin_key(request.personal_space_id)
146
+ payload = json.dumps({
147
+ 'version': PROOF_VERSION, 'key_id': key['key_id'], 'space_id': request.personal_space_id,
148
+ 'project_id': project_id, 'agent_id': agent_id, 'task_hash': task_hash,
149
+ 'message_id': request.message_id, 'digest': request.digest, 'target_node': request.target_node,
150
+ 'binding_id': request.binding_id, 'issued_at': canonical_time(now), 'expires_at': request.expires_at,
151
+ }, sort_keys=True, separators=(',', ':')).encode()
152
+ signature = SigningKey(_decode(key['private_key'])).sign(payload).signature
153
+ return {'agent_id': agent_id, 'agent_name': agent.profile.display_name or agent.profile.name or agent_id,
154
+ 'proof': {'payload': _encode(payload), 'signature': _encode(signature)}}
@@ -21,7 +21,7 @@ ProjectAgentTaskDuration = Literal['ongoing', 'one_off']
21
21
  ProjectAgentStaffingIntent = Literal[
22
22
  'reuse_preferred',
23
23
  # Like reuse_preferred, but a project without a team lead hires its first
24
- # lead through HR without waiting for confirmation.
24
+ # lead through AR without waiting for confirmation.
25
25
  'default_lead',
26
26
  'new_durable',
27
27
  'temporary',
@@ -331,6 +331,13 @@ class GraphitiRuntime:
331
331
  'FOR (n:FuliProjectAgentExecutorOutcomeReset) REQUIRE n.id IS UNIQUE',
332
332
  'CREATE CONSTRAINT fuli_project_agent_executor_observation_id IF NOT EXISTS '
333
333
  'FOR (n:FuliProjectAgentExecutorObservation) REQUIRE n.id IS UNIQUE',
334
+ 'CREATE CONSTRAINT fuli_remote_origin_key_space IF NOT EXISTS '
335
+ 'FOR (n:FuliRemoteOriginKey) REQUIRE n.space_id IS UNIQUE',
336
+ 'CREATE CONSTRAINT fuli_remote_agent_origin_message IF NOT EXISTS '
337
+ 'FOR (n:FuliRemoteAgentOrigin) REQUIRE (n.space_id, n.message_id) IS UNIQUE',
338
+ 'CREATE CONSTRAINT fuli_remote_delegation_attempt IF NOT EXISTS '
339
+ 'FOR (n:FuliRemoteDelegationAttempt) '
340
+ 'REQUIRE (n.space_id, n.origin_node, n.origin_message_id, n.origin_attempt_id) IS UNIQUE',
334
341
  ):
335
342
  await self.driver.execute_query(query)
336
343
  for query in (
@@ -5,9 +5,10 @@ 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
7
  from .project_agent_delegations import StoreAgentDelegations
8
+ from .project_agent_remote_origins import StoreRemoteAgentOrigins
8
9
 
9
10
 
10
- class StoreProjectAgentContext(StoreAgentDelegations):
11
+ class StoreProjectAgentContext(StoreAgentDelegations, StoreRemoteAgentOrigins):
11
12
  async def resolve_project_agent_context(self, actor, request):
12
13
  self._require_personal()
13
14
  space = await self.authorize(actor, request.personal_space_id, 'reader')
@@ -68,7 +69,7 @@ class StoreProjectAgentContext(StoreAgentDelegations):
68
69
  )
69
70
  if selected and not self._implicit_owner_allowed(selected[0], fallback_selection):
70
71
  selected, reason, basis = [], 'no_match', [
71
- 'management or HR peer cannot own unrelated project context implicitly',
72
+ 'management or AR peer cannot own unrelated project context implicitly',
72
73
  ]
73
74
  if selected:
74
75
  reason = 'project_context_fallback'
@@ -113,7 +113,7 @@ class StoreProjectAgentCoordinationPolicy:
113
113
  if (profile.status != 'active' or not active_assignment
114
114
  or profile.agent_type != 'durable'
115
115
  or any(cap.startswith('fuli.employee:') for cap in profile.capabilities)):
116
- raise HTTPException(status_code=422, detail='team roles require active durable project assignments; HR and project managers remain peers')
116
+ raise HTTPException(status_code=422, detail='team roles require active durable project assignments; AR and project managers remain peers')
117
117
  updated_at = now_utc()
118
118
  written, _, _ = await self.runtime.driver.execute_query(
119
119
  '''
@@ -618,7 +618,7 @@ class StoreProjectAgentTaskReads:
618
618
  if must_disclose:
619
619
  notice = ' '.join(
620
620
  (
621
- f'HR {item.hr_agent_id} recruited '
621
+ f'AR {item.hr_agent_id} recruited '
622
622
  f'{item.proposed_profile.name} as a '
623
623
  f'{item.position_kind} {item.participant_role} Agent for '
624
624
  f'{item.proposed_profile.responsibility}. '
@@ -33,7 +33,7 @@ RECRUITMENT_CLAIM_TTL = timedelta(minutes=2)
33
33
 
34
34
 
35
35
  class StoreProjectAgentTaskRecruitment:
36
- """Own the audited HR lifecycle independently from task routing mechanics."""
36
+ """Own the audited AR lifecycle independently from task routing mechanics."""
37
37
 
38
38
  async def get_project_agent_recruitment_policy(
39
39
  self,
@@ -202,7 +202,7 @@ class StoreProjectAgentTaskRecruitment:
202
202
  if not raw.get('hr_agent_id'):
203
203
  raise HTTPException(
204
204
  status_code=409,
205
- detail='recruitment cannot proceed without an active HR Agent',
205
+ detail='recruitment cannot proceed without an active AR Agent',
206
206
  )
207
207
  raw['_guarded_task_id'] = raw['task_id']
208
208
  selected = await self._provision_recruitment(actor, raw)
@@ -321,7 +321,7 @@ class StoreProjectAgentTaskRecruitment:
321
321
  model_strategy_source=model_source,
322
322
  outcome=outcome,
323
323
  reason=recruitment['reason_code'],
324
- match_basis=['HR recruitment approved and Agent assigned'],
324
+ match_basis=['AR recruitment approved and Agent assigned'],
325
325
  candidate_agent_ids=[routing_selected['agent_id']],
326
326
  created_at=now,
327
327
  **self._decision_executor_fields(executor_decision),
@@ -333,7 +333,7 @@ class StoreProjectAgentTaskRecruitment:
333
333
  )
334
334
  explanation = self._routing_explanation(
335
335
  recruitment['reason_code'],
336
- ['HR recruitment approved and Agent assigned'],
336
+ ['AR recruitment approved and Agent assigned'],
337
337
  executor_decision,
338
338
  )
339
339
  event_id = stable_uuid(
@@ -445,7 +445,7 @@ class StoreProjectAgentTaskRecruitment:
445
445
  decision_id=decision_id,
446
446
  decision_json=decision.model_dump_json(),
447
447
  event_id=event_id,
448
- event_summary=f'HR recruitment approved: {approval_reason}',
448
+ event_summary=f'AR recruitment approved: {approval_reason}',
449
449
  expected_task_revision=expected_task_revision,
450
450
  updated_at=now,
451
451
  )
@@ -84,7 +84,7 @@ class StoreProjectAgentTaskStaffing:
84
84
  return [lead], [lead, *members], 'project_team_lead', [
85
85
  'project team lead is the stable conversation owner',
86
86
  f'{len(members)} active team member(s) available for explicit workstreams',
87
- 'HR and project manager remain peer roles',
87
+ 'AR and project manager remain peer roles',
88
88
  ]
89
89
  normalized_work_kind = request.work_kind.casefold()
90
90
  required = {item.casefold() for item in request.required_capabilities}
@@ -886,7 +886,7 @@ class StoreProjectAgentTaskStaffing:
886
886
  status_code=422,
887
887
  detail=(
888
888
  'parallel work requires at least two active Agents; '
889
- 'automatic recruitment needs an active HR Agent, otherwise '
889
+ 'automatic recruitment needs an active AR Agent, otherwise '
890
890
  'recruit another qualified collaborator or assign enough '
891
891
  'existing Agents before enabling parallel work'
892
892
  ),
@@ -272,7 +272,7 @@ class StoreProjectAgentTasks(
272
272
  *match_basis,
273
273
  'parallel automatic recruitment: filled '
274
274
  f'{recruited_count} missing participant slot(s)',
275
- 'HR recruitment recorded before Agent creation',
275
+ 'AR recruitment recorded before Agent creation',
276
276
  ]
277
277
  if recruited_lead:
278
278
  model_strategy, model_source = self._effective_model_strategy(
@@ -322,7 +322,7 @@ class StoreProjectAgentTasks(
322
322
  else 'no_match'
323
323
  )
324
324
  match_basis = [
325
- 'HR recruitment recorded before Agent creation',
325
+ 'AR recruitment recorded before Agent creation',
326
326
  *(
327
327
  [
328
328
  'parallel auto-staffing: added '
@@ -68,11 +68,11 @@ class StoreProjectAgents:
68
68
  ) -> ProjectAgentRecord:
69
69
  self._require_personal()
70
70
  if request.agent_id == LEGACY_HR_AGENT_ID:
71
- raise HTTPException(409, 'This HR identity has moved to employee.bole')
71
+ raise HTTPException(409, 'This AR identity has moved to employee.bole')
72
72
  if request.profile.agent_type == 'temporary' and not recruitment_id:
73
73
  raise HTTPException(
74
74
  status_code=422,
75
- detail='temporary Agents must be created by an audited HR recruitment',
75
+ detail='temporary Agents must be created by an audited AR recruitment',
76
76
  )
77
77
  if (
78
78
  request.profile.agent_type == 'coordinator'
@@ -88,7 +88,7 @@ class StoreProjectAgents:
88
88
  ):
89
89
  raise HTTPException(
90
90
  status_code=422,
91
- detail='only the system-managed Bole identity may use HR type',
91
+ detail='only the system-managed Bole identity may use AR type',
92
92
  )
93
93
  if (
94
94
  request.agent_id == SYSTEM_COORDINATOR_AGENT_ID
@@ -104,7 +104,7 @@ class StoreProjectAgents:
104
104
  ):
105
105
  raise HTTPException(
106
106
  status_code=422,
107
- detail='the system HR identity cannot change Agent type',
107
+ detail='the system AR identity cannot change Agent type',
108
108
  )
109
109
  if (
110
110
  request.agent_id in (SYSTEM_HR_AGENT_ID, SYSTEM_COORDINATOR_AGENT_ID)
@@ -112,7 +112,7 @@ class StoreProjectAgents:
112
112
  ):
113
113
  raise HTTPException(
114
114
  status_code=422,
115
- detail='the project manager and HR must remain active',
115
+ detail='the project manager and AR must remain active',
116
116
  )
117
117
  space = await self.authorize(actor, request.personal_space_id, 'maintainer')
118
118
  if request.personal_project_id:
@@ -402,7 +402,7 @@ class StoreProjectAgents:
402
402
  if agent_id == SYSTEM_HR_AGENT_ID:
403
403
  raise HTTPException(
404
404
  status_code=422,
405
- detail='the system HR cannot be archived',
405
+ detail='the system AR cannot be archived',
406
406
  )
407
407
  await self.authorize(actor, personal_space_id, 'maintainer')
408
408
  if not reason.strip():
@@ -1,4 +1,4 @@
1
- """Fixed roles: Jefa (project manager), Bole (HR), and Tonborg (judgment).
1
+ """Fixed roles: Jefa (project manager), Bole (AR), 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
@@ -15,6 +15,8 @@ SYSTEM_COORDINATOR_AGENT_ID = 'employee.jefa'
15
15
  LEGACY_COORDINATOR_AGENT_ID = 'fuli-project-coordinator'
16
16
  SYSTEM_HR_AGENT_ID = 'employee.bole'
17
17
  SYSTEM_JUDGMENT_AGENT_ID = 'employee.tonborg'
18
+ # Upgrade only the former built-in copy; custom responsibilities are user data.
19
+ LEGACY_HR_RESPONSIBILITY = 'HR:维护 Agent 人员分布与工作状态,按需招募并保留招募记录。'
18
20
 
19
21
 
20
22
  def coordinator_profile() -> ProjectAgentProfile:
@@ -37,7 +39,7 @@ def hr_profile() -> ProjectAgentProfile:
37
39
  return ProjectAgentProfile(
38
40
  name='Bole',
39
41
  occupation_emoji='🔎',
40
- responsibility='HR:维护 Agent 人员分布与工作状态,按需招募并保留招募记录。',
42
+ responsibility='AR:维护 Agent 人员分布与工作状态,按需招募并保留招募记录。',
41
43
  agent_type='hr',
42
44
  work_kinds=['agent-recruitment', 'staffing-review'],
43
45
  capabilities=['Agent 招募', '人员分布', '工作状态', '招募审计', 'fuli.employee:bole'],
@@ -86,8 +88,13 @@ async def ensure_system_identity(driver, provider_id, space_id, agent_id):
86
88
  id=node_id,
87
89
  )
88
90
  profile = default
91
+ upgrade_responsibility = False
89
92
  if rows and rows[0]['profile_json']:
90
93
  stored = json.loads(rows[0]['profile_json'])
94
+ upgrade_responsibility = (agent_id == SYSTEM_HR_AGENT_ID
95
+ and stored.get('responsibility') == LEGACY_HR_RESPONSIBILITY)
96
+ if upgrade_responsibility:
97
+ stored['responsibility'] = default.responsibility
91
98
  profile = ProjectAgentProfile.model_validate(
92
99
  {**stored, 'agent_type': default.agent_type, 'status': 'active'}
93
100
  )
@@ -107,6 +114,8 @@ async def ensure_system_identity(driver, provider_id, space_id, agent_id):
107
114
  agent.created_at = $now,
108
115
  agent.updated_at = $now
109
116
  SET agent.profile_json = $profile_json,
117
+ agent.responsibility = CASE WHEN $upgrade_responsibility
118
+ THEN $responsibility ELSE agent.responsibility END,
110
119
  agent.agent_type = $agent_type,
111
120
  agent.status = 'active',
112
121
  agent.system_managed = true
@@ -119,6 +128,7 @@ async def ensure_system_identity(driver, provider_id, space_id, agent_id):
119
128
  name=profile.name,
120
129
  occupation_emoji=profile.occupation_emoji,
121
130
  responsibility=profile.responsibility,
131
+ upgrade_responsibility=upgrade_responsibility,
122
132
  capabilities=profile.capabilities,
123
133
  work_kinds=profile.work_kinds,
124
134
  agent_type=profile.agent_type,
@@ -1,4 +1,4 @@
1
- """Lossless, exact-ID upgrade of the original HR into the built-in Bole.
1
+ """Lossless, exact-ID upgrade of the original AR into the built-in Bole.
2
2
 
3
3
  The old node and its original edges remain an audit source, not another active
4
4
  Agent. Only this reserved legacy ID is eligible; names never establish identity.
@@ -103,7 +103,7 @@ async def merge_legacy_hr(store, space_id):
103
103
  for edge in edges:
104
104
  kind = edge['kind']
105
105
  if not re.fullmatch(r'[A-Z][A-Z0-9_]*', kind):
106
- raise HTTPException(409, 'Unsupported HR relationship; no data was changed')
106
+ raise HTTPException(409, 'Unsupported AR relationship; no data was changed')
107
107
  start_id = row['canonical_element'] if edge['start_id'] == row['legacy_element'] else edge['start_id']
108
108
  end_id = row['canonical_element'] if edge['end_id'] == row['legacy_element'] else edge['end_id']
109
109
  copied, _, _ = await driver.execute_query(
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fuli-graph-provider"
3
- version = "0.12.0"
3
+ version = "0.12.2"
4
4
  description = "Graphiti-backed personal and shared project provider for Fuli."
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
@@ -8,6 +8,7 @@ dependencies = [
8
8
  "graphiti-core==0.29.2",
9
9
  "openai>=1.91,<2",
10
10
  "pydantic-settings==2.10.1",
11
+ "pynacl==1.6.2",
11
12
  "uvicorn[standard]==0.35.0",
12
13
  ]
13
14