fuli-context 0.7.10-beta.0 → 0.8.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.
Files changed (174) hide show
  1. package/README.md +5 -2
  2. package/README.zh-CN.md +5 -3
  3. package/dist/web/assets/{AboutPage-BvXH4hsO.js → AboutPage-BjMmpwJB.js} +1 -1
  4. package/dist/web/assets/AboutPage-vYIBifKw.css +1 -0
  5. package/dist/web/assets/AgentHand-BfLYPGjg.js +1 -0
  6. package/dist/web/assets/AgentHand-D9sQu2vc.css +1 -0
  7. package/dist/web/assets/ConnectionsPage-BKGxFDos.js +1 -0
  8. package/dist/web/assets/EmployeeRecruitDialog-B59X3rOi.css +1 -0
  9. package/dist/web/assets/EmployeeRecruitDialog-D-W043v4.js +1 -0
  10. package/dist/web/assets/EmployeeWorkbenchPage-Cz7y-IFD.css +1 -0
  11. package/dist/web/assets/EmployeeWorkbenchPage-HHQPxdqI.js +3 -0
  12. package/dist/web/assets/GrowthLoading-B_Hh0CRS.css +1 -0
  13. package/dist/web/assets/GrowthLoading-WXCj8b0r.js +1 -0
  14. package/dist/web/assets/KnowledgeEditDialog-Bkie08ZA.js +1 -0
  15. package/dist/web/assets/KnowledgeEditDialog-D9NuRHvp.css +1 -0
  16. package/dist/web/assets/KnowledgeInspector-Cez-Z_95.js +1 -0
  17. package/dist/web/assets/KnowledgeOrganizerPage-BYmTPA_8.js +1 -0
  18. package/dist/web/assets/KnowledgeOrganizerPage-h2iKvQpO.css +1 -0
  19. package/dist/web/assets/KnowledgePage-Ck7tIBDn.js +1 -0
  20. package/dist/web/assets/KnowledgeWorkspace-DKij2_B4.js +4 -0
  21. package/dist/web/assets/KnowledgeWorkspace-sb8OJ97I.css +1 -0
  22. package/dist/web/assets/NotFoundPage-HGV-q-bh.js +1 -0
  23. package/dist/web/assets/{OverviewPage-D4vLhIFD.js → OverviewPage-DE29_F_Y.js} +1 -1
  24. package/dist/web/assets/PersonalProfilePage-B95DFc7x.js +1 -0
  25. package/dist/web/assets/PersonalProfilePage-L8T5xMEC.css +1 -0
  26. package/dist/web/assets/PersonalProjectsPage-Bt7tFF5Z.js +1 -0
  27. package/dist/web/assets/ProjectAgentsPage-BioDR9OW.css +1 -0
  28. package/dist/web/assets/ProjectAgentsPage-CiiPNe_G.js +12 -0
  29. package/dist/web/assets/ProjectScopePicker-BvaGTMFx.css +1 -0
  30. package/dist/web/assets/ProjectScopePicker-Cu5PdxF6.js +1 -0
  31. package/dist/web/assets/PublicProjectsPage-BY4B3_t3.js +1 -0
  32. package/dist/web/assets/ReviewPage-C9vCesNu.js +1 -0
  33. package/dist/web/assets/{SearchableSelect-DY-TWC2i.js → SearchableSelect-214TvfYg.js} +1 -1
  34. package/dist/web/assets/SettingsPage-Brl-SL33.css +1 -0
  35. package/dist/web/assets/SettingsPage-D9TnzcFc.js +1 -0
  36. package/dist/web/assets/SharedJefaReportPage-CdSGI_OE.js +1 -0
  37. package/dist/web/assets/SharedJefaReportPage-DXbj_JhO.css +1 -0
  38. package/dist/web/assets/WritingTastePage-Bzg6AvnB.css +1 -0
  39. package/dist/web/assets/WritingTastePage-CUE1dU8f.js +1 -0
  40. package/dist/web/assets/bole-avatar-CVIr-bU_.webp +0 -0
  41. package/dist/web/assets/client-Bl4oiW3E.js +1 -0
  42. package/dist/web/assets/console-BAIVNC-N.js +1 -0
  43. package/dist/web/assets/conversation-launcher-settings-DpRI_Jp2.js +1 -0
  44. package/dist/web/assets/employee-dialog-Bh5ccIbK.css +1 -0
  45. package/dist/web/assets/{i18n-C7r_00q2.js → i18n-CVFADb_M.js} +3 -3
  46. package/dist/web/assets/{identity-COvBmUF4.js → identity-Dt0WZgqo.js} +1 -1
  47. package/dist/web/assets/index-BUMq5zNh.js +2 -0
  48. package/dist/web/assets/index-C0ci8dnU.css +1 -0
  49. package/dist/web/assets/{model-CL1JILt4.js → model-B8sOxih0.js} +1 -1
  50. package/dist/web/assets/personal-profile-graph-aAqex7ty.js +1 -0
  51. package/dist/web/assets/shared-report-BQD6l_cS.js +1 -0
  52. package/dist/web/assets/shared-report-ezeb8dW4.css +1 -0
  53. package/dist/web/assets/{useApi-CROJJdhE-DERnOR_k.js → useApi-CROJJdhE-CuJ16v2S.js} +1 -1
  54. package/dist/web/assets/useModalDialog-Ddv2XJQi.js +1 -0
  55. package/dist/web/assets/{vue-router-DJ6C6-8v.js → vue-router-C8KuEZ6a.js} +1 -1
  56. package/dist/web/index.html +14 -8
  57. package/docs/agent-interface-architecture.md +97 -0
  58. package/docs/employee-agents.md +58 -2
  59. package/docs/project-agent-memory.md +69 -449
  60. package/graph-provider/fuli_graph/app.py +2 -0
  61. package/graph-provider/fuli_graph/collaboration_context.py +2 -1
  62. package/graph-provider/fuli_graph/graph_models.py +5 -3
  63. package/graph-provider/fuli_graph/graph_query.py +4 -0
  64. package/graph-provider/fuli_graph/knowledge_search.py +2 -1
  65. package/graph-provider/fuli_graph/preference_conflicts.py +4 -1
  66. package/graph-provider/fuli_graph/project_agent_access.py +2 -0
  67. package/graph-provider/fuli_graph/project_agent_attention_models.py +64 -0
  68. package/graph-provider/fuli_graph/project_agent_attention_routes.py +38 -0
  69. package/graph-provider/fuli_graph/project_agent_coordination_models.py +23 -1
  70. package/graph-provider/fuli_graph/project_agent_memory_models.py +11 -0
  71. package/graph-provider/fuli_graph/project_agent_models.py +2 -0
  72. package/graph-provider/fuli_graph/project_agent_names.py +33 -0
  73. package/graph-provider/fuli_graph/project_agent_routes.py +12 -0
  74. package/graph-provider/fuli_graph/project_agent_team_growth.py +56 -0
  75. package/graph-provider/fuli_graph/project_agent_work_log.py +34 -0
  76. package/graph-provider/fuli_graph/runtime.py +49 -20
  77. package/graph-provider/fuli_graph/store.py +2 -0
  78. package/graph-provider/fuli_graph/store_knowledge.py +4 -2
  79. package/graph-provider/fuli_graph/store_project_agent_attention.py +138 -0
  80. package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +56 -2
  81. package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +8 -0
  82. package/graph-provider/fuli_graph/store_project_agent_memory.py +43 -13
  83. package/graph-provider/fuli_graph/store_project_agent_task_activity.py +1 -1
  84. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +6 -1
  85. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +21 -3
  86. package/graph-provider/fuli_graph/store_project_agent_tasks.py +4 -0
  87. package/graph-provider/fuli_graph/store_project_agents.py +109 -0
  88. package/graph-provider/fuli_graph/store_task_contexts.py +12 -2
  89. package/graph-provider/fuli_graph/system_hr_identity.py +218 -0
  90. package/graph-provider/fuli_graph/task_context_adoption.py +65 -0
  91. package/graph-provider/fuli_graph/task_context_models.py +23 -1
  92. package/graph-provider/fuli_graph/task_context_routes.py +5 -1
  93. package/graph-provider/pyproject.toml +1 -1
  94. package/npm-shrinkwrap.json +52 -60
  95. package/package.json +2 -2
  96. package/skills/operating-fuli/SKILL.md +47 -0
  97. package/skills/operating-fuli/agents/openai.yaml +4 -0
  98. package/skills/operating-fuli/references/workflow-recipes.md +33 -0
  99. package/src/agent-tools/agent-attention-definitions.js +33 -0
  100. package/src/agent-tools/graph-definitions.js +22 -7
  101. package/src/agent-tools/graph-handlers.js +41 -0
  102. package/src/agent-tools/interface-catalog.js +101 -0
  103. package/src/agent-tools/interface-definitions.js +184 -0
  104. package/src/agent-tools/project-agent-definitions.js +2 -0
  105. package/src/agents/codex/lifecycle-hook.js +1 -0
  106. package/src/cli/local-runtime-command.js +2 -1
  107. package/src/cli/setup-command.js +1 -1
  108. package/src/employees/catalog/bole.json +19 -0
  109. package/src/employees/install-package.js +3 -0
  110. package/src/employees/manifest.js +4 -0
  111. package/src/employees/package-registry.js +2 -0
  112. package/src/employees/recruitment.js +3 -0
  113. package/src/employees/service.js +10 -0
  114. package/src/external-knowledge/registry.js +24 -0
  115. package/src/external-knowledge/service.js +34 -1
  116. package/src/graphiti/agent-attention.js +26 -0
  117. package/src/graphiti/agent-knowledge-workflows.js +14 -4
  118. package/src/graphiti/project-agent-control-plane.js +30 -0
  119. package/src/graphiti/project-agent-mapping.js +18 -1
  120. package/src/graphiti/project-agent-memory.js +10 -2
  121. package/src/graphiti/project-agent-task-entry.js +4 -1
  122. package/src/graphiti/project-agent-team-context.js +21 -0
  123. package/src/graphiti/project-agent-workflows.js +18 -0
  124. package/src/graphiti/provider-client.js +20 -1
  125. package/src/graphiti/workspace-provider-client.js +9 -1
  126. package/src/graphiti/writing-taste-profile-workflow.js +62 -9
  127. package/src/graphiti/writing-taste-profile.js +43 -16
  128. package/src/http/agent-attention-api-router.js +28 -0
  129. package/src/http/error-mapping.js +3 -0
  130. package/src/http/graph-api-router.js +11 -0
  131. package/src/local-runtime/lifecycle.js +8 -2
  132. package/src/mcp/create-mcp-server.js +15 -4
  133. package/src/mcp/instructions.js +3 -3
  134. package/src/mcp/provider-task-context-registry.js +11 -1
  135. package/src/mcp/task-context-registry.js +2 -1
  136. package/src/mcp/tool-annotations.js +31 -0
  137. package/src/mcp/tool-result.js +11 -7
  138. package/src/setup/agent-installation-status.js +2 -1
  139. package/src/setup/agents.js +4 -0
  140. package/src/setup/graph-runtime.js +4 -0
  141. package/src/setup/paths.js +2 -1
  142. package/src/setup/setup.js +4 -2
  143. package/src/setup/uninstall.js +4 -2
  144. package/src/system/resource-monitor.js +11 -1
  145. package/src/system/runtime-settings.js +7 -0
  146. package/dist/web/assets/AboutPage-DXh7Fwxt.css +0 -1
  147. package/dist/web/assets/ConnectionsPage-WVc6Srbd.js +0 -1
  148. package/dist/web/assets/EmployeeRecruitDialog-CnA7rOnB.css +0 -1
  149. package/dist/web/assets/EmployeeRecruitDialog-Dal5wy8a.js +0 -1
  150. package/dist/web/assets/EmployeeWorkbenchPage-BDJvrpcn.css +0 -1
  151. package/dist/web/assets/EmployeeWorkbenchPage-CW5pu_Yb.js +0 -1
  152. package/dist/web/assets/KnowledgeEditDialog-BBvm74ln.js +0 -1
  153. package/dist/web/assets/KnowledgeEditDialog-iv-dgUeD.css +0 -1
  154. package/dist/web/assets/KnowledgeInspector-CE9ADmtT.js +0 -1
  155. package/dist/web/assets/KnowledgeOrganizerPage-BRtizGUS.js +0 -1
  156. package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +0 -1
  157. package/dist/web/assets/KnowledgePage-D5clXGCw.js +0 -1
  158. package/dist/web/assets/KnowledgeWorkspace-CKLH6cIf.js +0 -4
  159. package/dist/web/assets/PersonalProfilePage-7w8rAzkg.js +0 -1
  160. package/dist/web/assets/PersonalProfilePage-B7T09bow.css +0 -1
  161. package/dist/web/assets/PersonalProjectsPage-BY0B1t24.js +0 -1
  162. package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +0 -1
  163. package/dist/web/assets/ProjectAgentsPage-LHPOuLWw.js +0 -11
  164. package/dist/web/assets/PublicProjectsPage-PSp6RzVZ.js +0 -1
  165. package/dist/web/assets/ReviewPage-DPP23xLF.js +0 -1
  166. package/dist/web/assets/SettingsPage-C59OPsNP.js +0 -1
  167. package/dist/web/assets/SettingsPage-CwZp2ka4.css +0 -1
  168. package/dist/web/assets/WritingTastePage-DNXhCdgn.js +0 -1
  169. package/dist/web/assets/WritingTastePage-DSgvTH6x.css +0 -1
  170. package/dist/web/assets/avatars-BfZueqlm.js +0 -1
  171. package/dist/web/assets/console-CYXvhGCR.js +0 -1
  172. package/dist/web/assets/conversation-launcher-settings-SwmUP91W.js +0 -1
  173. package/dist/web/assets/index-B-rXoqYP.js +0 -2
  174. package/dist/web/assets/index-Ch1jmkyO.css +0 -1
@@ -216,6 +216,7 @@ def _node_query(project_scoped: bool, paginated: bool = False) -> str:
216
216
  ELSE coalesce(node.fuli_preference_scope, 'global') END
217
217
  AS preference_scope,
218
218
  node.fuli_preference_project_id AS preference_project_id,
219
+ node.fuli_preference_agent_id AS preference_agent_id,
219
220
  coalesce(node.fuli_inheritance_mode, 'local_only')
220
221
  AS inheritance_mode,
221
222
  coalesce(node.fuli_inherited_project_ids, [])
@@ -331,6 +332,7 @@ def _edge_query(project_scoped: bool, paginated: bool = False) -> str:
331
332
  ELSE coalesce(edge.fuli_preference_scope, 'global') END
332
333
  AS preference_scope,
333
334
  edge.fuli_preference_project_id AS preference_project_id,
335
+ edge.fuli_preference_agent_id AS preference_agent_id,
334
336
  coalesce(edge.fuli_inheritance_mode, 'local_only')
335
337
  AS inheritance_mode,
336
338
  coalesce(edge.fuli_inherited_project_ids, [])
@@ -499,6 +501,7 @@ def _graph_node(
499
501
  profile_aspect=record.get('profile_aspect'),
500
502
  preference_scope=record.get('preference_scope'),
501
503
  preference_project_id=record.get('preference_project_id'),
504
+ preference_agent_id=record.get('preference_agent_id'),
502
505
  inheritance_mode=record.get('inheritance_mode') or 'local_only',
503
506
  inherited_project_ids=record.get('inherited_project_ids') or [],
504
507
  human_edited=record.get('human_edited') is True,
@@ -580,6 +583,7 @@ def _graph_edge(
580
583
  profile_aspect=record.get('profile_aspect'),
581
584
  preference_scope=record.get('preference_scope'),
582
585
  preference_project_id=record.get('preference_project_id'),
586
+ preference_agent_id=record.get('preference_agent_id'),
583
587
  inheritance_mode=record.get('inheritance_mode') or 'local_only',
584
588
  inherited_project_ids=record.get('inherited_project_ids') or [],
585
589
  human_edited=record.get('human_edited') is True,
@@ -317,7 +317,7 @@ async def _personal_search_space(store, actor, spaces, request):
317
317
  for project_id in request.personal_project_ids:
318
318
  await authorize_personal_project(store, actor, space, project_id)
319
319
  if request.project_agent_id:
320
- await authorize_project_agent(
320
+ agent = await authorize_project_agent(
321
321
  store,
322
322
  actor,
323
323
  space,
@@ -326,6 +326,7 @@ async def _personal_search_space(store, actor, spaces, request):
326
326
  require_active=True,
327
327
  require_memory=True,
328
328
  )
329
+ request.project_agent_id = agent['agent_id']
329
330
  return space
330
331
 
331
332
 
@@ -92,6 +92,7 @@ async def list_preference_conflicts(
92
92
  *,
93
93
  status: str | None = None,
94
94
  limit: int = 500,
95
+ offset: int = 0,
95
96
  ) -> list[PreferenceConflictRecord]:
96
97
  await _authorize_personal_space(
97
98
  store,
@@ -105,12 +106,14 @@ async def list_preference_conflicts(
105
106
  [:HAS_PREFERENCE_CONFLICT]->(conflict:FuliPreferenceConflict)
106
107
  WHERE $status IS NULL OR conflict.status = $status
107
108
  RETURN conflict
108
- ORDER BY conflict.updated_at DESC
109
+ ORDER BY conflict.updated_at DESC, conflict.id ASC
110
+ SKIP $offset
109
111
  LIMIT $limit
110
112
  ''',
111
113
  space_id=personal_space_id,
112
114
  status=status,
113
115
  limit=min(max(limit, 1), 1000),
116
+ offset=max(offset, 0),
114
117
  routing_='r',
115
118
  )
116
119
  return [_record(record['conflict']) for record in records]
@@ -1,6 +1,7 @@
1
1
  from fastapi import HTTPException
2
2
 
3
3
  from .personal_project_access import authorize_personal_project
4
+ from .system_hr_identity import resolve_hr_alias
4
5
 
5
6
 
6
7
  async def authorize_project_agent(
@@ -20,6 +21,7 @@ async def authorize_project_agent(
20
21
  detail='project Agent access requires a personal project',
21
22
  )
22
23
  await authorize_personal_project(store, actor, space, project_id)
24
+ agent_id = await resolve_hr_alias(store, space['id'], agent_id)
23
25
  records, _, _ = await store.runtime.driver.execute_query(
24
26
  '''
25
27
  MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})-
@@ -0,0 +1,64 @@
1
+ from datetime import datetime
2
+ from typing import Literal
3
+
4
+ from pydantic import Field, model_validator
5
+
6
+ from .model_base import StrictModel
7
+ from .model_validation import reject_credentials
8
+
9
+
10
+ AttentionStatus = Literal['open', 'resolved', 'cancelled']
11
+
12
+
13
+ class AgentAttentionCreate(StrictModel):
14
+ personal_space_id: str = Field(min_length=1, max_length=128)
15
+ personal_project_id: str = Field(min_length=1, max_length=128)
16
+ agent_id: str = Field(min_length=1, max_length=128)
17
+ task_id: str | None = Field(default=None, min_length=1, max_length=128)
18
+ idempotency_key: str = Field(min_length=8, max_length=256)
19
+ kind: Literal['question', 'approval', 'review', 'permission', 'blocked']
20
+ title: str = Field(min_length=1, max_length=160)
21
+ detail: str = Field(min_length=1, max_length=4096)
22
+ requested_action: str = Field(min_length=1, max_length=2048)
23
+
24
+ @model_validator(mode='after')
25
+ def reject_sensitive_values(self):
26
+ reject_credentials(self, 'Agent attention request')
27
+ return self
28
+
29
+
30
+ class AgentAttentionDecision(StrictModel):
31
+ personal_space_id: str = Field(min_length=1, max_length=128)
32
+ personal_project_id: str = Field(min_length=1, max_length=128)
33
+ request_id: str = Field(min_length=1, max_length=128)
34
+ expected_revision: int = Field(ge=0)
35
+ response: str = Field(min_length=1, max_length=4096)
36
+
37
+ @model_validator(mode='after')
38
+ def reject_sensitive_values(self):
39
+ reject_credentials(self, 'Agent attention response')
40
+ return self
41
+
42
+
43
+ class AgentAttentionRecord(StrictModel):
44
+ request_id: str
45
+ personal_space_id: str
46
+ personal_project_id: str
47
+ agent_id: str
48
+ task_id: str | None = None
49
+ kind: str
50
+ title: str
51
+ detail: str
52
+ requested_action: str
53
+ status: AttentionStatus = 'open'
54
+ revision: int = 0
55
+ response: str | None = None
56
+ responded_by: Literal['human', 'agent'] | None = None
57
+ created_at: datetime
58
+ updated_at: datetime
59
+
60
+
61
+ class AgentAttentionList(StrictModel):
62
+ items: list[AgentAttentionRecord]
63
+ total: int
64
+ counts: dict[str, int]
@@ -0,0 +1,38 @@
1
+ from typing import Annotated, Any
2
+
3
+ from fastapi import Query
4
+
5
+ from .project_agent_attention_models import (
6
+ AgentAttentionCreate, AgentAttentionDecision, AgentAttentionList,
7
+ AgentAttentionRecord, AttentionStatus,
8
+ )
9
+
10
+
11
+ def register_project_agent_attention_routes(application, store, Actor: Any):
12
+ @application.post('/v1/agent-attention', response_model=AgentAttentionRecord)
13
+ async def create(request: AgentAttentionCreate, actor: Actor):
14
+ return await store.create_agent_attention(actor, request)
15
+
16
+ @application.get('/v1/agent-attention', response_model=AgentAttentionList)
17
+ async def list_requests(
18
+ actor: Actor,
19
+ personal_space_id: Annotated[str, Query(min_length=1, max_length=128)],
20
+ personal_project_id: Annotated[str | None, Query(min_length=1, max_length=128)] = None,
21
+ agent_id: Annotated[str | None, Query(min_length=1, max_length=128)] = None,
22
+ status: AttentionStatus | None = 'open',
23
+ limit: Annotated[int, Query(ge=1, le=200)] = 100,
24
+ offset: Annotated[int, Query(ge=0)] = 0,
25
+ ):
26
+ return await store.list_agent_attention(
27
+ actor, personal_space_id, personal_project_id, agent_id, status, limit, offset,
28
+ )
29
+
30
+ @application.post('/v1/agent-attention/cancel', response_model=AgentAttentionRecord)
31
+ async def cancel(request: AgentAttentionDecision, actor: Actor):
32
+ return await store.decide_agent_attention(actor, request, human=False)
33
+
34
+ @application.post('/v1/agent-attention/respond', response_model=AgentAttentionRecord)
35
+ async def respond(request: AgentAttentionDecision, actor: Actor):
36
+ # Only the trusted local-user HTTP workflow exposes this operation.
37
+ # A reply does not execute an approval, permission change or acceptance.
38
+ return await store.decide_agent_attention(actor, request, human=True)
@@ -1,6 +1,6 @@
1
1
  from datetime import datetime
2
2
 
3
- from pydantic import Field
3
+ from pydantic import Field, field_validator, model_validator
4
4
 
5
5
  from .models import StrictModel
6
6
 
@@ -12,6 +12,9 @@ class ProjectAgentCoordinationPolicyRecord(StrictModel):
12
12
  personal_project_id: str
13
13
  ask_before_recruitment: bool = True
14
14
  auto_reuse_previous_agent: bool = True
15
+ auto_grow_team: bool = True
16
+ team_lead_agent_id: str | None = None
17
+ team_member_agent_ids: list[str] = Field(default_factory=list)
15
18
  updated_at: datetime | None = None
16
19
 
17
20
 
@@ -20,3 +23,22 @@ class ProjectAgentCoordinationPolicyUpdate(StrictModel):
20
23
  personal_project_id: str = Field(min_length=1, max_length=128)
21
24
  ask_before_recruitment: bool = True
22
25
  auto_reuse_previous_agent: bool = True
26
+ auto_grow_team: bool | None = None
27
+ team_lead_agent_id: str | None = Field(default=None, min_length=1, max_length=128)
28
+ team_member_agent_ids: list[str] = Field(default_factory=list, max_length=32)
29
+ expected_updated_at: datetime | None = None
30
+
31
+ @field_validator('team_member_agent_ids')
32
+ @classmethod
33
+ def validate_members(cls, value):
34
+ if any(not item.strip() or len(item) > 128 for item in value):
35
+ raise ValueError('team member IDs must contain 1 to 128 characters')
36
+ if len(set(value)) != len(value):
37
+ raise ValueError('team member IDs must be unique')
38
+ return value
39
+
40
+ @model_validator(mode='after')
41
+ def validate_team(self):
42
+ if self.team_lead_agent_id in self.team_member_agent_ids:
43
+ raise ValueError('the team lead cannot also be a team member')
44
+ return self
@@ -65,6 +65,16 @@ class ProjectAgentMemoryRecord(StrictModel):
65
65
  created_at: datetime
66
66
 
67
67
 
68
+ class EmployeeWorkLogRecord(StrictModel):
69
+ task_context_token: str
70
+ source_application: SourceApplication
71
+ session_id: str
72
+ created_at: datetime
73
+ status: Literal['reported', 'completed', 'incomplete', 'failed', 'no_change', 'unreported', 'running']
74
+ summary: str = Field(min_length=1, max_length=2000)
75
+ memory_updated: bool
76
+
77
+
68
78
  class ProjectAgentMemoryView(StrictModel):
69
79
  personal_space_id: str
70
80
  personal_project_id: str
@@ -77,3 +87,4 @@ class ProjectAgentMemoryView(StrictModel):
77
87
  revision: int = Field(default=0, ge=0)
78
88
  current: ProjectAgentMemoryRecord | None = None
79
89
  history: list[ProjectAgentMemoryRecord] = Field(default_factory=list)
90
+ work_log: list[EmployeeWorkLogRecord] = Field(default_factory=list, max_length=6)
@@ -90,6 +90,7 @@ class ProjectAgentExecutorPolicy(StrictModel):
90
90
 
91
91
  class ProjectAgentProfile(StrictModel):
92
92
  name: str = Field(min_length=1, max_length=160)
93
+ display_name: str | None = Field(default=None, min_length=1, max_length=160)
93
94
  # Keep the visual occupation marker as first-class profile data. It is
94
95
  # intentionally independent from ``name`` so clients can update either
95
96
  # value without rewriting the other and old profiles can omit it.
@@ -266,6 +267,7 @@ class ProjectAgentAssignmentReplaceResult(StrictModel):
266
267
 
267
268
  class ProjectAgentRecord(StrictModel):
268
269
  agent_id: str
270
+ legacy_agent_ids: list[str] = Field(default_factory=list)
269
271
  personal_space_id: str
270
272
  personal_project_id: str | None = None
271
273
  profile: ProjectAgentProfile
@@ -0,0 +1,33 @@
1
+ """Stable product nicknames, independent of identity, assignment and work status."""
2
+ import hashlib
3
+ import re
4
+
5
+
6
+ FIRST_NAMES = (
7
+ 'Milo', 'Nova', 'Arlo', 'Nora', 'Theo', 'Iris', 'Remy', 'Lena',
8
+ 'Finn', 'Cleo', 'Ezra', 'Mira', 'Jude', 'Lyra', 'Owen', 'Cora',
9
+ 'Alex', 'Robin', 'Jamie', 'Rowan', 'Casey', 'Avery', 'Morgan', 'Riley',
10
+ 'Ellis', 'Quinn', 'Sage', 'Blair', 'Reese', 'Emery', 'Drew', 'Sky',
11
+ )
12
+ LAST_NAMES = (
13
+ 'Reed', 'Lane', 'Vale', 'Brook', 'Wren', 'Stone', 'Park', 'Finch',
14
+ 'Lake', 'Ash', 'Birch', 'Hill', 'West', 'Woods', 'Dale', 'Hart',
15
+ 'Gray', 'Bell', 'Hayes', 'Blake', 'Lowe', 'Frost', 'Green', 'Page',
16
+ 'Wells', 'Shaw', 'Fox', 'Snow', 'Field', 'North', 'Ray', 'Cole',
17
+ )
18
+ ROLE_NAME = re.compile(
19
+ r'(工程师|协调人|开发者|设计师|架构师|审计员|项目经理|负责人|专员|助手|智能体|验证员|测试员|记录员)'
20
+ r'|\b(agent|engineer|coordinator|specialist|developer|reviewer|tester|verifier|auditor|designer|architect|implementer)\b',
21
+ re.IGNORECASE,
22
+ )
23
+
24
+
25
+ def agent_display_name(agent_id: str, name: str, display_name: str | None = None):
26
+ if display_name:
27
+ return display_name
28
+ if agent_id == 'fuli-project-coordinator' and name == '项目协调人':
29
+ return 'Orion'
30
+ if not ROLE_NAME.search(name):
31
+ return name
32
+ digest = hashlib.sha256(agent_id.encode()).digest()
33
+ return f'{FIRST_NAMES[digest[0] % len(FIRST_NAMES)]} {LAST_NAMES[digest[1] % len(LAST_NAMES)]}'
@@ -60,6 +60,8 @@ def register_project_agent_routes(
60
60
  """Attach Project Agent routes while keeping ``create_app`` orchestration small."""
61
61
 
62
62
  register_project_agent_memory_routes(application, store, Actor)
63
+ from .project_agent_attention_routes import register_project_agent_attention_routes
64
+ register_project_agent_attention_routes(application, store, Actor)
63
65
 
64
66
  @application.put('/v1/project-agents', response_model=ProjectAgentRecord)
65
67
  async def upsert_project_agent(
@@ -137,6 +139,16 @@ def register_project_agent_routes(
137
139
  ) -> ProjectAgentRecord:
138
140
  return await store.ensure_system_project_coordinator(actor, personal_space_id)
139
141
 
142
+ @application.post(
143
+ '/v1/project-agents/system-hr',
144
+ response_model=ProjectAgentRecord,
145
+ )
146
+ async def ensure_project_agent_hr(
147
+ actor: Actor,
148
+ personal_space_id: Annotated[str, Query(min_length=1, max_length=128)],
149
+ ) -> ProjectAgentRecord:
150
+ return await store.ensure_system_project_hr(actor, personal_space_id)
151
+
140
152
  @application.post(
141
153
  '/v1/project-agent-assignments',
142
154
  response_model=ProjectAgentAssignmentRecord,
@@ -0,0 +1,56 @@
1
+ """Grow a project team inside the recruitment transaction, without changing authority."""
2
+
3
+ from fastapi import HTTPException
4
+
5
+ from .provider_values import now_utc
6
+
7
+
8
+ async def enroll_recruited_team_member(store, recruitment, profile):
9
+ # Temporary workers and system employees do not become permanent teammates.
10
+ if (profile.agent_type != 'durable' or profile.status != 'active'
11
+ or any(cap.startswith('fuli.employee:') for cap in profile.capabilities)):
12
+ return
13
+ space_id = recruitment['personal_space_id']
14
+ project_id = recruitment['personal_project_id']
15
+ rows, _, _ = await store.runtime.driver.execute_query(
16
+ '''
17
+ MATCH (:FuliSpace {id: $space_id})-[:CONTAINS_PROJECT]->
18
+ (project:FuliPersonalProject {project_id: $project_id})
19
+ MATCH (project)-[:HAS_PROJECT_AGENT_ASSIGNMENT]->
20
+ (:FuliProjectAgentAssignment {status: 'active'})-[:ASSIGNED_AGENT]->
21
+ (agent:FuliProjectAgent {agent_id: $agent_id, status: 'active'})
22
+ WITH DISTINCT project, agent
23
+ MERGE (policy:FuliProjectAgentCoordinationPolicy {policy_id: $policy_id})
24
+ ON CREATE SET policy.personal_space_id = $space_id,
25
+ policy.personal_project_id = $project_id,
26
+ policy.created_at = $updated_at
27
+ SET policy._fuli_write_lock = true
28
+ REMOVE policy._fuli_write_lock
29
+ MERGE (project)-[:HAS_PROJECT_AGENT_COORDINATION_POLICY]->(policy)
30
+ WITH policy, agent, coalesce(policy.auto_grow_team, true) AS enabled,
31
+ coalesce(policy.team_member_agent_ids, []) AS members
32
+ WITH policy, agent, members, enabled,
33
+ policy.team_lead_agent_id IS NULL AS needs_lead,
34
+ coalesce(policy.team_lead_agent_id = agent.agent_id, false)
35
+ OR agent.agent_id IN members AS already_member
36
+ WITH *, enabled AND NOT already_member
37
+ AND (needs_lead OR size(members) < 32) AS changed
38
+ FOREACH (_ IN CASE WHEN changed THEN [1] ELSE [] END |
39
+ SET policy.team_lead_agent_id = CASE WHEN needs_lead
40
+ THEN agent.agent_id ELSE policy.team_lead_agent_id END,
41
+ policy.team_member_agent_ids = CASE WHEN needs_lead
42
+ THEN members ELSE members + agent.agent_id END,
43
+ policy.updated_at = $updated_at
44
+ )
45
+ RETURN enabled AND NOT already_member AND NOT needs_lead
46
+ AND size(members) >= 32 AS full
47
+ ''',
48
+ space_id=space_id, project_id=project_id,
49
+ agent_id=recruitment['proposed_agent_id'],
50
+ policy_id=store._project_agent_coordination_policy_id(space_id, project_id),
51
+ updated_at=now_utc(),
52
+ )
53
+ if not rows:
54
+ raise HTTPException(409, 'recruited Agent has no active project assignment')
55
+ if rows[0]['full']:
56
+ raise HTTPException(409, 'project team is full; change the team before recruiting')
@@ -0,0 +1,34 @@
1
+ """Recover bounded employee work records independently of distilled memory updates."""
2
+
3
+ import json
4
+
5
+
6
+ async def read_employee_work_log(store, space_id, project_id, agent_id):
7
+ rows, _, _ = await store.runtime.driver.execute_query(
8
+ '''
9
+ MATCH (:FuliSpace {id: $space_id})-[:HAS_TASK_CONTEXT_SESSION]->
10
+ (session:FuliTaskContextSession)-[:HAS_CONTEXT]->(task:FuliTaskContext)
11
+ WHERE task.personal_project_id = $project_id AND task.project_agent_id = $agent_id
12
+ RETURN task.record_json AS record_json, task.completed AS completed,
13
+ task.agent_memory_claimed AS memory_updated,
14
+ task.token = session.current_token AS current
15
+ ORDER BY task.created_at DESC, task.token LIMIT 6
16
+ ''', space_id=space_id, project_id=project_id, agent_id=agent_id, routing_='r',
17
+ )
18
+ result = []
19
+ for row in rows:
20
+ record = json.loads(row['record_json'])
21
+ checkpoint = record.get('checkpoint') or {}
22
+ log = checkpoint.get('work_log') or {}
23
+ result.append({
24
+ 'task_context_token': record['token'],
25
+ 'source_application': record['source_application'],
26
+ 'session_id': record['session_id'],
27
+ 'created_at': record['created_at'],
28
+ 'status': (log.get('status', 'unreported') if row['completed']
29
+ else 'running' if row.get('current') else 'incomplete'),
30
+ 'summary': log.get('summary') or checkpoint.get('reason')
31
+ or 'The employee has not submitted a work summary for this task.',
32
+ 'memory_updated': bool(row.get('memory_updated')),
33
+ })
34
+ return result
@@ -271,6 +271,8 @@ class GraphitiRuntime:
271
271
  for query in (
272
272
  'CREATE CONSTRAINT fuli_project_agent_id IF NOT EXISTS '
273
273
  'FOR (n:FuliProjectAgent) REQUIRE n.id IS UNIQUE',
274
+ 'CREATE CONSTRAINT fuli_agent_attention_id IF NOT EXISTS '
275
+ 'FOR (n:FuliAgentAttention) REQUIRE n.id IS UNIQUE',
274
276
  'CREATE CONSTRAINT fuli_project_agent_assignment_id IF NOT EXISTS '
275
277
  'FOR (n:FuliProjectAgentAssignment) REQUIRE n.id IS UNIQUE',
276
278
  'CREATE CONSTRAINT fuli_project_agent_memory_id IF NOT EXISTS '
@@ -516,7 +518,7 @@ class GraphitiRuntime:
516
518
  from .project_agent_models import ProjectAgentProfile
517
519
  from .provider_values import now_utc, stable_uuid
518
520
 
519
- profile = ProjectAgentProfile(
521
+ coordinator_profile = ProjectAgentProfile(
520
522
  name='项目协调人',
521
523
  responsibility='评估任务、应用用户配置并路由已有 Agent。',
522
524
  agent_type='coordinator',
@@ -525,26 +527,50 @@ class GraphitiRuntime:
525
527
  initial_preferences=['质量与可验收完成优先于成本和时间'],
526
528
  status='active',
527
529
  )
530
+ hr_profile = ProjectAgentProfile(
531
+ name='Bole',
532
+ occupation_emoji='🔎',
533
+ responsibility=(
534
+ '维护 Agent 人员分布、当前工作与可审计招募记录,'
535
+ '并在需要新角色时执行受策略约束的招募。'
536
+ ),
537
+ agent_type='hr',
538
+ work_kinds=['agent-recruitment', 'staffing-review'],
539
+ capabilities=[
540
+ 'Agent 招募', '人员分布', '工作状态', '招募审计',
541
+ 'fuli.employee:bole',
542
+ ],
543
+ initial_preferences=[
544
+ '每次招募保留任务、原因、触发来源与时间线。',
545
+ '只展示 Provider 已记录的人员状态,不推测未上报的工作。',
546
+ ],
547
+ status='active',
548
+ )
528
549
  updated_at = now_utc()
529
550
  for record in records:
530
551
  space_id = record['space_id']
531
- node_id = stable_uuid(
532
- self.settings.provider_id,
533
- space_id,
534
- 'project-agent',
535
- 'fuli-project-coordinator',
536
- )
537
- await self.driver.execute_query(
538
- '''
552
+ for agent_id, profile in (
553
+ ('fuli-project-coordinator', coordinator_profile),
554
+ ('employee.bole', hr_profile),
555
+ ):
556
+ node_id = stable_uuid(
557
+ self.settings.provider_id,
558
+ space_id,
559
+ 'project-agent',
560
+ agent_id,
561
+ )
562
+ await self.driver.execute_query(
563
+ '''
539
564
  MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
540
565
  MERGE (agent:FuliProjectAgent {id: $id})
541
- ON CREATE SET agent.agent_id = 'fuli-project-coordinator',
566
+ ON CREATE SET agent.agent_id = $agent_id,
542
567
  agent.profile_json = $profile_json,
543
568
  agent.name = $name,
569
+ agent.occupation_emoji = $occupation_emoji,
544
570
  agent.responsibility = $responsibility,
545
571
  agent.capabilities = $capabilities,
546
572
  agent.work_kinds = $work_kinds,
547
- agent.agent_type = 'coordinator',
573
+ agent.agent_type = $agent_type,
548
574
  agent.memory_scope = 'reviewed_agent',
549
575
  agent.status = 'active',
550
576
  agent.system_managed = true,
@@ -552,15 +578,18 @@ class GraphitiRuntime:
552
578
  agent.updated_at = $updated_at
553
579
  MERGE (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(agent)
554
580
  ''',
555
- space_id=space_id,
556
- id=node_id,
557
- profile_json=profile.model_dump_json(),
558
- name=profile.name,
559
- responsibility=profile.responsibility,
560
- capabilities=profile.capabilities,
561
- work_kinds=profile.work_kinds,
562
- updated_at=updated_at,
563
- )
581
+ space_id=space_id,
582
+ id=node_id,
583
+ agent_id=agent_id,
584
+ profile_json=profile.model_dump_json(),
585
+ name=profile.name,
586
+ occupation_emoji=profile.occupation_emoji,
587
+ responsibility=profile.responsibility,
588
+ capabilities=profile.capabilities,
589
+ work_kinds=profile.work_kinds,
590
+ agent_type=profile.agent_type,
591
+ updated_at=updated_at,
592
+ )
564
593
 
565
594
  async def _migrate_personal_project_relation_review_defaults(self) -> None:
566
595
  await self.driver.execute_query(
@@ -7,6 +7,7 @@ from .store_project_agent_executors import StoreProjectAgentExecutors
7
7
  from .store_project_agent_memory import StoreProjectAgentMemory
8
8
  from .store_project_agent_context import StoreProjectAgentContext
9
9
  from .store_project_agents import StoreProjectAgents
10
+ from .store_project_agent_attention import StoreProjectAgentAttention
10
11
  from .store_project_agent_tasks import StoreProjectAgentTasks
11
12
  from .store_projects import StoreProjects
12
13
  from .store_publication import StorePublication
@@ -18,6 +19,7 @@ class GraphStore(
18
19
  StoreIdentity,
19
20
  StoreProjects,
20
21
  StoreProjectAgents,
22
+ StoreProjectAgentAttention,
21
23
  StoreProjectAgentMemory,
22
24
  StoreProjectAgentContext,
23
25
  StoreTaskContexts,
@@ -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,