fuli-context 0.10.0 → 0.11.1

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 (212) hide show
  1. package/README.md +27 -30
  2. package/README.zh-CN.md +22 -26
  3. package/dist/web/assets/AboutPage-cW9hDghN.js +1 -0
  4. package/dist/web/assets/AgentConversations-CGznTWuV.js +2 -0
  5. package/dist/web/assets/AgentConversations-XqNEBY_2.css +1 -0
  6. package/dist/web/assets/AgentHand-DkyF4vgk.js +1 -0
  7. package/dist/web/assets/AgentName-C1mAVr-v.js +1 -0
  8. package/dist/web/assets/AgentProfilePage-pKUR0rfj.js +1 -0
  9. package/dist/web/assets/AgentsDirectoryPage-CUlbT2hD.js +1 -0
  10. package/dist/web/assets/AgentsDirectoryPage-yRWktiin.css +1 -0
  11. package/dist/web/assets/ConnectionsPage-CM9C-zEI.css +1 -0
  12. package/dist/web/assets/ConnectionsPage-CuDHIJXu.js +1 -0
  13. package/dist/web/assets/EmployeeRecruitDialog-CVbqCzHb.css +1 -0
  14. package/dist/web/assets/EmployeeRecruitDialog-D9QfGipd.js +1 -0
  15. package/dist/web/assets/EmployeeWorkbenchPage-D-c_Jm2z.css +1 -0
  16. package/dist/web/assets/EmployeeWorkbenchPage-DAtvGney.js +3 -0
  17. package/dist/web/assets/KnowledgeEditDialog-a-5rasG0.js +1 -0
  18. package/dist/web/assets/KnowledgeEditDialog-hT9znkoe.css +1 -0
  19. package/dist/web/assets/KnowledgeInspector-BGsqjNY2.js +1 -0
  20. package/dist/web/assets/KnowledgeOrganizerPage-BndgUsHS.js +1 -0
  21. package/dist/web/assets/KnowledgeOrganizerPage-DeL7Jp34.css +1 -0
  22. package/dist/web/assets/KnowledgePage-CQweO0YJ.js +1 -0
  23. package/dist/web/assets/KnowledgeWorkspace-BtUjKTLb.js +4 -0
  24. package/dist/web/assets/KnowledgeWorkspace-D4a7FcAb.css +1 -0
  25. package/dist/web/assets/NotFoundPage-BmQTTqQn.js +1 -0
  26. package/dist/web/assets/OverviewPage-By-fUzSG.css +1 -0
  27. package/dist/web/assets/OverviewPage-C73HyTsM.js +1 -0
  28. package/dist/web/assets/PersonalProfilePage-BMuOrppx.js +1 -0
  29. package/dist/web/assets/PersonalProfilePage-DrMDAAdy.css +1 -0
  30. package/dist/web/assets/PersonalProjectsPage-Cq9_kFvs.js +1 -0
  31. package/dist/web/assets/ProjectAgentFirstTask-Bi1ZsTC1.css +1 -0
  32. package/dist/web/assets/ProjectAgentFirstTask-CaHvOjkp.js +1 -0
  33. package/dist/web/assets/ProjectAgentsPage-DWGkgIsr.css +1 -0
  34. package/dist/web/assets/ProjectAgentsPage-WVcNdzVi.js +12 -0
  35. package/dist/web/assets/ProjectScopePicker-zAgiaZKL.js +1 -0
  36. package/dist/web/assets/PublicProjectsPage-DKamXDV9.css +1 -0
  37. package/dist/web/assets/PublicProjectsPage-DL093f65.js +1 -0
  38. package/dist/web/assets/ReviewPage-D_NIhPx8.js +1 -0
  39. package/dist/web/assets/RoundtablesPage-Cb0R4GaM.css +1 -0
  40. package/dist/web/assets/RoundtablesPage-CdVVyZE-.js +1 -0
  41. package/dist/web/assets/SettingsPage-BmYkLul4.css +1 -0
  42. package/dist/web/assets/SettingsPage-CbLaau8Q.js +1 -0
  43. package/dist/web/assets/SharedJefaReportPage-aygd6wvb.js +1 -0
  44. package/dist/web/assets/UiDialog-Q7fsJUoO.js +1 -0
  45. package/dist/web/assets/UiDisclosure-BS-muK5j.js +1 -0
  46. package/dist/web/assets/UiSegmented-CT28bxqw.js +1 -0
  47. package/dist/web/assets/UiSelect-Bv6R0yr1.css +1 -0
  48. package/dist/web/assets/UiSelect-DcGpPQQo.js +1 -0
  49. package/dist/web/assets/WritingTastePage-B_zYD1df.css +1 -0
  50. package/dist/web/assets/WritingTastePage-CYlwAFqV.js +1 -0
  51. package/dist/web/assets/agent-profile-BZj3ezWe.css +1 -0
  52. package/dist/web/assets/{attention-store-Dk-_P4Oi.js → attention-store-D-qIFRIb.js} +1 -1
  53. package/dist/web/assets/{catalog-DN72452f.js → catalog-UDv854JN.js} +1 -1
  54. package/dist/web/assets/client-DLS5FZBI.js +1 -0
  55. package/dist/web/assets/{console-BtPqo79j.js → console-CqzwSo7a.js} +1 -1
  56. package/dist/web/assets/{conversation-launcher-settings-jkHRmWai.js → conversation-launcher-settings-DRoIz42V.js} +1 -1
  57. package/dist/web/assets/{i18n-D2CJkEYK.js → i18n-b14l2pQA.js} +3 -3
  58. package/dist/web/assets/{identity-B3zEw-1u.js → identity-BWbPmZZf.js} +1 -1
  59. package/dist/web/assets/index-D1OHIz4c.js +5 -0
  60. package/dist/web/assets/index-kPB0Cf8Q.css +1 -0
  61. package/dist/web/assets/{model-S7ehAjky.js → model-DupZy8Qb.js} +1 -1
  62. package/dist/web/assets/{personal-profile-graph--GG3Mizo.js → personal-profile-graph-BPTtMJTZ.js} +1 -1
  63. package/dist/web/assets/shared-report-BYnLhmCZ.js +1 -0
  64. package/dist/web/assets/{useAgentRoster-BAPKWZ9y.js → useAgentRoster-ep7SpKv1.js} +1 -1
  65. package/dist/web/assets/{useApi-CROJJdhE-BtFMXFjw.js → useApi-CROJJdhE-CEQAEuug.js} +1 -1
  66. package/dist/web/assets/{vue-router-DkaTtPn0.js → vue-router-B6ZjK-Br.js} +1 -1
  67. package/dist/web/index.html +14 -16
  68. package/docs/agent-conversations-and-collaboration.md +9 -9
  69. package/docs/agent-roundtable.md +77 -0
  70. package/docs/employee-agents.md +2 -2
  71. package/docs/project-agent-memory.md +1 -1
  72. package/graph-provider/fuli_graph/agent_conversation_models.py +11 -2
  73. package/graph-provider/fuli_graph/conversation_context.py +40 -9
  74. package/graph-provider/fuli_graph/default_project_lead.py +88 -0
  75. package/graph-provider/fuli_graph/project_agent_context_models.py +5 -1
  76. package/graph-provider/fuli_graph/project_agent_delegations.py +118 -0
  77. package/graph-provider/fuli_graph/project_agent_employee_numbers.py +41 -0
  78. package/graph-provider/fuli_graph/project_agent_memory_routes.py +20 -0
  79. package/graph-provider/fuli_graph/project_agent_models.py +1 -0
  80. package/graph-provider/fuli_graph/project_agent_task_models.py +3 -0
  81. package/graph-provider/fuli_graph/project_agent_team_growth.py +2 -2
  82. package/graph-provider/fuli_graph/runtime.py +10 -76
  83. package/graph-provider/fuli_graph/store.py +2 -0
  84. package/graph-provider/fuli_graph/store_agent_conversations.py +60 -8
  85. package/graph-provider/fuli_graph/store_project_agent_context.py +52 -4
  86. package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +16 -1
  87. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +3 -1
  88. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +3 -2
  89. package/graph-provider/fuli_graph/store_project_agent_tasks.py +12 -8
  90. package/graph-provider/fuli_graph/store_project_agents.py +56 -129
  91. package/graph-provider/fuli_graph/store_task_contexts.py +26 -0
  92. package/graph-provider/fuli_graph/system_agents.py +130 -0
  93. package/graph-provider/fuli_graph/system_hr_identity.py +1 -1
  94. package/graph-provider/fuli_graph/task_context_routes.py +10 -0
  95. package/graph-provider/pyproject.toml +1 -1
  96. package/npm-shrinkwrap.json +2 -2
  97. package/package.json +2 -2
  98. package/src/agent-roundtable/http.js +14 -0
  99. package/src/agent-roundtable/service.js +247 -0
  100. package/src/agent-roundtable/store.js +86 -0
  101. package/src/agent-roundtable/tools.js +61 -0
  102. package/src/agent-roundtable/wake.js +100 -0
  103. package/src/agent-tools/agent-conversation-definitions.js +3 -3
  104. package/src/agent-tools/graph-definitions.js +3 -2
  105. package/src/agent-tools/interface-catalog.js +0 -5
  106. package/src/agent-tools.js +2 -2
  107. package/src/agents/claude-code/wake.js +37 -0
  108. package/src/agents/codex/wake.js +47 -0
  109. package/src/agents/identity-receipt.js +35 -7
  110. package/src/agents/session-log.js +34 -0
  111. package/src/agents/wake-registry.js +14 -0
  112. package/src/cli/command-registry.js +0 -2
  113. package/src/cli/setup-command.js +2 -5
  114. package/src/cli/update-command.js +13 -2
  115. package/src/cli.js +0 -5
  116. package/src/employees/management-policy.js +4 -2
  117. package/src/employees/manifest.js +3 -0
  118. package/src/employees/recruitment.js +3 -2
  119. package/src/graphiti/agent-conversations.js +11 -9
  120. package/src/graphiti/agent-knowledge-workflows.js +34 -7
  121. package/src/graphiti/federated-application.js +17 -5
  122. package/src/graphiti/project-agent-mapping.js +2 -0
  123. package/src/graphiti/project-agent-task-entry.js +38 -8
  124. package/src/graphiti/project-agent-team-context.js +4 -1
  125. package/src/graphiti/project-path-context.js +7 -2
  126. package/src/graphiti/provider-client.js +17 -0
  127. package/src/graphiti/repository-project-registration.js +17 -0
  128. package/src/graphiti/task-entry-preferences.js +17 -4
  129. package/src/graphiti/task-entry-project-scope.js +67 -0
  130. package/src/http/api-router.js +5 -3
  131. package/src/http/error-mapping.js +0 -2
  132. package/src/mcp/instructions.js +5 -5
  133. package/src/mcp/session-id.js +1 -1
  134. package/src/mcp/tool-annotations.js +3 -4
  135. package/src/setup/agents.js +1 -41
  136. package/src/setup/graph-runtime.js +15 -2
  137. package/src/setup/session-skill.js +22 -2
  138. package/dist/web/assets/AboutPage-D-GyJNv1.js +0 -1
  139. package/dist/web/assets/AgentConversations-DOuZgUeA.js +0 -2
  140. package/dist/web/assets/AgentConversations-DS8h3yKn.css +0 -1
  141. package/dist/web/assets/AgentHand-eGsXoGSq.js +0 -1
  142. package/dist/web/assets/AgentName-Dtmu9Ff5.js +0 -1
  143. package/dist/web/assets/AgentProfilePage-D8Tvayn7.js +0 -1
  144. package/dist/web/assets/AgentsDirectoryPage-BTEfKZjF.js +0 -1
  145. package/dist/web/assets/ConnectionsPage-C1w99yN3.css +0 -1
  146. package/dist/web/assets/ConnectionsPage-DwOIcvl9.js +0 -1
  147. package/dist/web/assets/EmployeeRecruitDialog-BrIMRblq.css +0 -1
  148. package/dist/web/assets/EmployeeRecruitDialog-D11blfwU.js +0 -1
  149. package/dist/web/assets/EmployeeWorkbenchPage-BcEzYt65.css +0 -1
  150. package/dist/web/assets/EmployeeWorkbenchPage-CK2BAoGt.js +0 -3
  151. package/dist/web/assets/GrowthLoading-KTjE2e50.js +0 -1
  152. package/dist/web/assets/KnowledgeEditDialog-B3x8vs3c.css +0 -1
  153. package/dist/web/assets/KnowledgeEditDialog-n9trHQ5d.js +0 -1
  154. package/dist/web/assets/KnowledgeInspector-BcNjgw9H.js +0 -1
  155. package/dist/web/assets/KnowledgeOrganizerPage-BzgxP-nG.css +0 -1
  156. package/dist/web/assets/KnowledgeOrganizerPage-DMBpGxLr.js +0 -1
  157. package/dist/web/assets/KnowledgePage-POkCMW8f.js +0 -1
  158. package/dist/web/assets/KnowledgeWorkspace-2FvQadhB.js +0 -4
  159. package/dist/web/assets/KnowledgeWorkspace-sb8OJ97I.css +0 -1
  160. package/dist/web/assets/NotFoundPage-BCMZbeB8.js +0 -1
  161. package/dist/web/assets/OverviewPage-0CFWCG9-.css +0 -1
  162. package/dist/web/assets/OverviewPage-0mYifpjp.js +0 -1
  163. package/dist/web/assets/PersonalProfilePage-Chv2a2pi.css +0 -1
  164. package/dist/web/assets/PersonalProfilePage-JW6Muix6.js +0 -1
  165. package/dist/web/assets/PersonalProjectsPage-CRbi64JP.js +0 -1
  166. package/dist/web/assets/ProjectAgentFirstTask-6afJ8HUe.css +0 -1
  167. package/dist/web/assets/ProjectAgentFirstTask-DJ0wTbh8.js +0 -1
  168. package/dist/web/assets/ProjectAgentsPage-CPgKVLgg.js +0 -12
  169. package/dist/web/assets/ProjectAgentsPage-DeuRkMp1.css +0 -1
  170. package/dist/web/assets/ProjectScopePicker-BwB7gK-7.js +0 -1
  171. package/dist/web/assets/PublicProjectsPage-CB272Sq-.js +0 -1
  172. package/dist/web/assets/ReviewPage-CelTZU4C.js +0 -1
  173. package/dist/web/assets/RoundtablesPage-C9dXfWCl.js +0 -2
  174. package/dist/web/assets/RoundtablesPage-DCbIBnBq.css +0 -1
  175. package/dist/web/assets/SearchableSelect-f7EHbyDL.js +0 -1
  176. package/dist/web/assets/SettingsPage-5g7mRN2F.css +0 -1
  177. package/dist/web/assets/SettingsPage-nAijmm5f.js +0 -1
  178. package/dist/web/assets/SharedJefaReportPage-CjpJ9bkk.js +0 -1
  179. package/dist/web/assets/UiDisclosure-CWlC91Zt.js +0 -1
  180. package/dist/web/assets/WritingTastePage-DaHWY61P.js +0 -1
  181. package/dist/web/assets/WritingTastePage-SMGF39mf.css +0 -1
  182. package/dist/web/assets/agent-profile-CBvJHGjt.css +0 -1
  183. package/dist/web/assets/client-B2cRl-7c.js +0 -1
  184. package/dist/web/assets/copy-D2LqwtPZ.js +0 -1
  185. package/dist/web/assets/index-CNp0tRNp.css +0 -1
  186. package/dist/web/assets/index-ulISYH02.js +0 -5
  187. package/dist/web/assets/shared-report-DEK9wvhl.js +0 -1
  188. package/dist/web/assets/useModalDialog-D8tiJfbc.js +0 -1
  189. package/docs/roundtable.md +0 -46
  190. package/src/agents/a2a/roundtable-participant.js +0 -374
  191. package/src/agents/claude-code/roundtable-participant.js +0 -24
  192. package/src/agents/codex/roundtable-config.js +0 -32
  193. package/src/agents/codex/roundtable-participant.js +0 -34
  194. package/src/agents/grok/roundtable-participant.js +0 -185
  195. package/src/agents/pi/roundtable-participant.js +0 -193
  196. package/src/agents/pi/roundtable-result-format.js +0 -183
  197. package/src/cli/roundtable-command.js +0 -79
  198. package/src/roundtables/application.js +0 -69
  199. package/src/roundtables/domain.js +0 -181
  200. package/src/roundtables/http-router.js +0 -58
  201. package/src/roundtables/mcp-server.js +0 -55
  202. package/src/roundtables/participant-registry.js +0 -12
  203. package/src/roundtables/process.js +0 -70
  204. package/src/roundtables/profiles.js +0 -84
  205. package/src/roundtables/result-contract.js +0 -47
  206. package/src/roundtables/server.js +0 -52
  207. package/src/roundtables/service.js +0 -480
  208. package/src/roundtables/store.js +0 -63
  209. package/src/roundtables/tool-contract.js +0 -58
  210. package/src/roundtables/turn-prompt.js +0 -24
  211. package/src/roundtables/worker.js +0 -85
  212. /package/dist/web/assets/{GrowthLoading-CIuQ1icy.css → client-CIuQ1icy.css} +0 -0
@@ -226,6 +226,10 @@ class GraphitiRuntime:
226
226
  'CREATE CONSTRAINT fuli_workflow_review_preview_token IF NOT EXISTS '
227
227
  'FOR (n:FuliWorkflowReviewPreview) REQUIRE n.token_hash IS UNIQUE'
228
228
  )
229
+ await self.driver.execute_query(
230
+ 'CREATE CONSTRAINT fuli_agent_delegation_token IF NOT EXISTS '
231
+ 'FOR (n:FuliAgentDelegation) REQUIRE n.token_hash IS UNIQUE'
232
+ )
229
233
  await self.driver.execute_query(
230
234
  'CREATE CONSTRAINT fuli_workflow_rule_id IF NOT EXISTS '
231
235
  'FOR (n:FuliWorkflowRule) REQUIRE n.id IS UNIQUE'
@@ -285,6 +289,9 @@ class GraphitiRuntime:
285
289
  'FOR (n:FuliConversationPolicy) REQUIRE n.id IS UNIQUE',
286
290
  'CREATE CONSTRAINT fuli_project_agent_id IF NOT EXISTS '
287
291
  'FOR (n:FuliProjectAgent) REQUIRE n.id IS UNIQUE',
292
+ 'CREATE CONSTRAINT fuli_project_agent_employee_number IF NOT EXISTS '
293
+ 'FOR (n:FuliProjectAgent) '
294
+ 'REQUIRE (n.employee_number_space_id, n.employee_number) IS UNIQUE',
288
295
  'CREATE CONSTRAINT fuli_agent_attention_id IF NOT EXISTS '
289
296
  'FOR (n:FuliAgentAttention) REQUIRE n.id IS UNIQUE',
290
297
  'CREATE CONSTRAINT fuli_project_agent_assignment_id IF NOT EXISTS '
@@ -527,83 +534,10 @@ class GraphitiRuntime:
527
534
  "MATCH (space:FuliSpace {kind: 'personal'}) RETURN space.id AS space_id",
528
535
  routing_='r',
529
536
  )
530
- if not records:
531
- return
532
- from .project_agent_models import ProjectAgentProfile
533
- from .provider_values import now_utc, stable_uuid
534
-
535
- coordinator_profile = ProjectAgentProfile(
536
- name='项目协调人',
537
- responsibility='评估任务、应用用户配置并路由已有 Agent。',
538
- agent_type='coordinator',
539
- work_kinds=['task-coordination'],
540
- capabilities=['任务评估', '策略应用', 'Agent 路由'],
541
- initial_preferences=['质量与可验收完成优先于成本和时间'],
542
- status='active',
543
- )
544
- hr_profile = ProjectAgentProfile(
545
- name='Bole',
546
- occupation_emoji='🔎',
547
- responsibility=(
548
- '维护 Agent 人员分布、当前工作与可审计招募记录,'
549
- '并在需要新角色时执行受策略约束的招募。'
550
- ),
551
- agent_type='hr',
552
- work_kinds=['agent-recruitment', 'staffing-review'],
553
- capabilities=[
554
- 'Agent 招募', '人员分布', '工作状态', '招募审计',
555
- 'fuli.employee:bole',
556
- ],
557
- initial_preferences=[
558
- '每次招募保留任务、原因、触发来源与时间线。',
559
- '只展示 Provider 已记录的人员状态,不推测未上报的工作。',
560
- ],
561
- status='active',
562
- )
563
- updated_at = now_utc()
537
+ from .system_agents import ensure_system_agents
538
+
564
539
  for record in records:
565
- space_id = record['space_id']
566
- for agent_id, profile in (
567
- ('fuli-project-coordinator', coordinator_profile),
568
- ('employee.bole', hr_profile),
569
- ):
570
- node_id = stable_uuid(
571
- self.settings.provider_id,
572
- space_id,
573
- 'project-agent',
574
- agent_id,
575
- )
576
- await self.driver.execute_query(
577
- '''
578
- MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
579
- MERGE (agent:FuliProjectAgent {id: $id})
580
- ON CREATE SET agent.agent_id = $agent_id,
581
- agent.profile_json = $profile_json,
582
- agent.name = $name,
583
- agent.occupation_emoji = $occupation_emoji,
584
- agent.responsibility = $responsibility,
585
- agent.capabilities = $capabilities,
586
- agent.work_kinds = $work_kinds,
587
- agent.agent_type = $agent_type,
588
- agent.memory_scope = 'reviewed_agent',
589
- agent.status = 'active',
590
- agent.system_managed = true,
591
- agent.created_at = $updated_at,
592
- agent.updated_at = $updated_at
593
- MERGE (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(agent)
594
- ''',
595
- space_id=space_id,
596
- id=node_id,
597
- agent_id=agent_id,
598
- profile_json=profile.model_dump_json(),
599
- name=profile.name,
600
- occupation_emoji=profile.occupation_emoji,
601
- responsibility=profile.responsibility,
602
- capabilities=profile.capabilities,
603
- work_kinds=profile.work_kinds,
604
- agent_type=profile.agent_type,
605
- updated_at=updated_at,
606
- )
540
+ await ensure_system_agents(self.driver, self.settings.provider_id, record['space_id'])
607
541
 
608
542
  async def _migrate_personal_project_relation_review_defaults(self) -> None:
609
543
  await self.driver.execute_query(
@@ -9,6 +9,7 @@ from .store_knowledge import StoreKnowledge
9
9
  from .store_project_agent_executors import StoreProjectAgentExecutors
10
10
  from .store_project_agent_memory import StoreProjectAgentMemory
11
11
  from .store_project_agent_context import StoreProjectAgentContext
12
+ from .default_project_lead import StoreDefaultProjectLead
12
13
  from .store_project_agents import StoreProjectAgents
13
14
  from .store_project_agent_attention import StoreProjectAgentAttention
14
15
  from .store_project_agent_tasks import StoreProjectAgentTasks
@@ -28,6 +29,7 @@ class GraphStore(
28
29
  StoreProjectAgentAttention,
29
30
  StoreProjectAgentMemory,
30
31
  StoreProjectAgentContext,
32
+ StoreDefaultProjectLead,
31
33
  StoreTaskContexts,
32
34
  StoreProjectAgentExecutors,
33
35
  StoreProjectAgentTasks,
@@ -5,7 +5,9 @@ import hashlib
5
5
  import json
6
6
  from fastapi import HTTPException
7
7
  from .agent_conversation_models import ConversationPolicy
8
- from .conversation_context import archived, pack_context
8
+ from .conversation_context import (
9
+ add_task_result, fold_messages, message_bytes, pack_context, render_digest,
10
+ )
9
11
  from .project_agent_access import authorize_project_agent
10
12
  from .provider_values import now_utc, stable_uuid
11
13
  from .store_transactions import query_store_transaction
@@ -77,13 +79,13 @@ class StoreAgentConversations:
77
79
  raise HTTPException(404, 'Conversation not found in this Agent and project')
78
80
  record = dict(rows[0]['c'])
79
81
  view = self._conversation_view(record, policy)
80
- if request.mode == 'context' and view['archived']:
81
- return {**view, 'context': pack_context(record.get('summary', ''), [], policy.context_budget)}
82
82
  order = 'DESC' if request.mode == 'context' else 'ASC'
83
+ # Recovery starts after the folded range; reading history still sees everything.
84
+ after = max(request.after, record.get('compacted_through') or 0) if request.mode == 'context' else request.after
83
85
  events, _, _ = await self.runtime.driver.execute_query(
84
86
  'MATCH (c:FuliConversation {id: $id})-[:HAS_EVENT]->(e:FuliConversationEvent) '
85
87
  'WHERE e.sequence > $after RETURN e.payload AS payload, e.sequence AS sequence '
86
- f'ORDER BY e.sequence {order} LIMIT $limit', id=record['id'], after=request.after,
88
+ f'ORDER BY e.sequence {order} LIMIT $limit', id=record['id'], after=after,
87
89
  limit=20 if request.mode == 'context' else request.limit)
88
90
  decoded = []
89
91
  total = 0
@@ -94,15 +96,52 @@ class StoreAgentConversations:
94
96
  break
95
97
  decoded.append(value)
96
98
  if request.mode == 'context':
97
- return {**view, 'context': pack_context(record.get('summary', ''), list(reversed(decoded)), policy.context_budget)}
99
+ return {**view, 'context': pack_context(self._digest_text(record), list(reversed(decoded)), policy.context_budget)}
98
100
  cursor = decoded[-1]['sequence'] if decoded else request.after
99
101
  return {**view, 'events': decoded, 'next_cursor': cursor, 'has_more': cursor < record['revision']}
100
102
 
101
103
  def _conversation_view(self, record, policy):
102
104
  return {key: record.get(key) for key in ('id', 'summary', 'status', 'revision', 'last_activity')} | {
103
- 'archived': archived(record['last_activity'], policy.idle_days),
105
+ 'compacted_through': record.get('compacted_through') or 0,
106
+ 'unfolded_bytes': record.get('unfolded_bytes') or 0,
104
107
  'policy': policy.model_dump(), 'raw_retained': True}
105
108
 
109
+ @staticmethod
110
+ def _digest_text(record):
111
+ if record.get('digest'):
112
+ return render_digest(json.loads(record['digest']))
113
+ return record.get('summary') or ''
114
+
115
+ async def _compact_conversation(self, conversation_id, policy):
116
+ """Fold older messages into the digest, keeping recent ones verbatim."""
117
+ rows, _, _ = await self.runtime.driver.execute_query(
118
+ 'MATCH (c:FuliConversation {id: $id}) RETURN c', id=conversation_id)
119
+ record = dict(rows[0]['c'])
120
+ threshold = policy.compact_after_kb * 1024
121
+ if (record.get('unfolded_bytes') or 0) <= threshold:
122
+ return
123
+ events, _, _ = await self.runtime.driver.execute_query(
124
+ 'MATCH (c:FuliConversation {id: $id})-[:HAS_EVENT]->(e:FuliConversationEvent) '
125
+ 'WHERE e.sequence > $after RETURN e.payload AS payload, e.sequence AS sequence '
126
+ 'ORDER BY e.sequence', id=conversation_id, after=record.get('compacted_through') or 0)
127
+ decoded = [decode_event(event) for event in events]
128
+ if not decoded:
129
+ return
130
+ # The newest event always stays verbatim, then as much as fits a quarter.
131
+ keep_bytes, cut = threshold // 4, len(decoded) - 1
132
+ kept = message_bytes(decoded[cut])
133
+ while cut and kept + message_bytes(decoded[cut - 1]) <= keep_bytes:
134
+ cut -= 1
135
+ kept += message_bytes(decoded[cut])
136
+ if not cut:
137
+ return
138
+ digest = json.loads(record['digest']) if record.get('digest') else {}
139
+ await self.runtime.driver.execute_query(
140
+ 'MATCH (c:FuliConversation {id: $id}) '
141
+ 'SET c.digest=$digest, c.compacted_through=$through, c.unfolded_bytes=$kept',
142
+ id=conversation_id, digest=json.dumps(fold_messages(digest, decoded[:cut]), ensure_ascii=False),
143
+ through=decoded[cut - 1]['sequence'], kept=kept)
144
+
106
145
  async def append_conversation(self, actor, request):
107
146
  if request.initialize_cursor:
108
147
  raise HTTPException(422, 'Initialize transcripts only at a verified task boundary')
@@ -137,6 +176,7 @@ class StoreAgentConversations:
137
176
  "c.last_activity=$now, c.summary='', c.status='reported' "
138
177
  'SET c.serial=coalesce(c.serial,0)+1', id=conversation_id, scope=scope, now=now)
139
178
  new_summary = False
179
+ unfolded = 0
140
180
  for event in request.events:
141
181
  digest, payload = encode_event(event)
142
182
  event_id = stable_uuid(conversation_id, request.source_application, request.session_id, event.event_id)
@@ -148,6 +188,7 @@ class StoreAgentConversations:
148
188
  continue
149
189
  if event.kind == 'checkpoint':
150
190
  new_summary = True
191
+ unfolded += message_bytes(event.model_dump())
151
192
  await store.runtime.driver.execute_query(
152
193
  'MATCH (c:FuliConversation {id: $id}) SET c.revision=c.revision+1, c.last_activity=$now '
153
194
  'CREATE (e:FuliConversationEvent {id: $event_id, digest: $digest, payload: $payload, '
@@ -155,10 +196,21 @@ class StoreAgentConversations:
155
196
  'CREATE (c)-[:HAS_EVENT]->(e)', id=conversation_id, event_id=event_id,
156
197
  digest=digest, payload=payload, now=now, source=request.source_application,
157
198
  task=request.task_context_token)
199
+ if unfolded:
200
+ await store.runtime.driver.execute_query(
201
+ 'MATCH (c:FuliConversation {id: $id}) '
202
+ 'SET c.unfolded_bytes=coalesce(c.unfolded_bytes,0)+$bytes',
203
+ id=conversation_id, bytes=unfolded)
158
204
  if request.summary is not None and new_summary:
205
+ rows, _, _ = await store.runtime.driver.execute_query(
206
+ 'MATCH (c:FuliConversation {id: $id}) RETURN c.digest AS digest', id=conversation_id)
207
+ digest = json.loads(rows[0]['digest']) if rows[0]['digest'] else {}
208
+ digest = add_task_result(digest, now, request.status or 'reported', request.summary)
159
209
  await store.runtime.driver.execute_query(
160
- 'MATCH (c:FuliConversation {id: $id}) SET c.summary=$summary, c.status=$status',
161
- id=conversation_id, summary=request.summary, status=request.status or 'reported')
210
+ 'MATCH (c:FuliConversation {id: $id}) SET c.summary=$summary, c.status=$status, c.digest=$digest',
211
+ id=conversation_id, summary=request.summary, status=request.status or 'reported',
212
+ digest=json.dumps(digest, ensure_ascii=False))
213
+ await store._compact_conversation(conversation_id, policy)
162
214
  if request.cursor is not None:
163
215
  await store.runtime.driver.execute_query(
164
216
  'MATCH (s:FuliTranscriptCursor {id: $id}) SET s.cursor=$cursor, s.owner_scope=$scope', id=stream, cursor=request.cursor, scope=scope)
@@ -4,16 +4,21 @@ from .personal_project_access import authorize_personal_project
4
4
  from .project_agent_context_models import ProjectAgentContextResolution
5
5
  from .project_agent_task_models import ProjectAgentTaskSubmit
6
6
  from .task_context_temporary_owner import temporary_task_owner
7
+ from .project_agent_delegations import StoreAgentDelegations
7
8
 
8
9
 
9
- class StoreProjectAgentContext:
10
+ class StoreProjectAgentContext(StoreAgentDelegations):
10
11
  async def resolve_project_agent_context(self, actor, request):
11
12
  self._require_personal()
12
13
  space = await self.authorize(actor, request.personal_space_id, 'reader')
13
14
  await authorize_personal_project(self, actor, space, request.personal_project_id)
14
- temporary_owner = await temporary_task_owner(self, actor, request)
15
+ temporary_owner = await temporary_task_owner(self, actor, request) if not request.delegation_token else None
15
16
  if temporary_owner is not None:
16
17
  return temporary_owner
18
+ policy = await self.get_project_agent_coordination_policy(
19
+ actor, request.personal_space_id, request.personal_project_id,
20
+ )
21
+ project_lead = getattr(policy, 'team_lead_agent_id', None)
17
22
  owner = None
18
23
  if request.session_id and not request.agent_id:
19
24
  rows, _, _ = await self.runtime.driver.execute_query(
@@ -39,7 +44,8 @@ class StoreProjectAgentContext:
39
44
  idempotency_key='read-only-task-context',
40
45
  title='Resolve task context', objective='Select one existing durable role.',
41
46
  work_kind=request.work_kind, required_capabilities=request.required_capabilities,
42
- lead_agent_id=request.agent_id or owner, source_application=request.source_application,
47
+ lead_agent_id=request.agent_id or (project_lead if owner else None),
48
+ source_application=request.source_application,
43
49
  routing_reason='Read-only task-entry context recovery',
44
50
  )
45
51
  if owner and not request.agent_id:
@@ -71,16 +77,58 @@ class StoreProjectAgentContext:
71
77
  'using project_context for continuity only',
72
78
  *basis,
73
79
  ]
74
- if owner and selected:
80
+ requested_agent_id = None
81
+ reporting_lead_agent_id = None
82
+ peer_request = bool(request.agent_id and selected and self._is_implicit_peer(selected[0]))
83
+ delegated = bool(request.delegation_token)
84
+ if delegated and (not selected or selected[0]['agent_id'] != request.agent_id):
85
+ return ProjectAgentContextResolution(status='agent_unavailable', reason='delegation_invalid')
86
+ if not peer_request:
87
+ if not project_lead:
88
+ if delegated:
89
+ return ProjectAgentContextResolution(status='agent_unavailable', reason='delegation_invalid')
90
+ return ProjectAgentContextResolution(
91
+ status='unassigned', reason='project_lead_required',
92
+ requested_agent_id=request.agent_id,
93
+ match_basis=['Every project requires one accountable lead before work begins.'],
94
+ )
95
+ if selected and selected[0]['agent_id'] != project_lead:
96
+ # Validate the named specialist first, then route accountability
97
+ # through the lead. Never load the specialist's private memory here.
98
+ rows = await self._assignment_candidates(request.personal_space_id, request.personal_project_id)
99
+ lead = next((row for row in rows if row['agent_id'] == project_lead
100
+ and (delegated or self._agent_client_allowed(row, request.source_application))), None)
101
+ if not lead:
102
+ return ProjectAgentContextResolution(status='agent_unavailable',
103
+ reason='team_lead_unavailable', match_basis=['The configured project lead is unavailable.'])
104
+ if delegated:
105
+ # A real lead-to-member worker invocation keeps the member's
106
+ # isolated identity; its result returns to the project lead.
107
+ reporting_lead_agent_id = project_lead
108
+ reason = 'delegated_member'
109
+ basis = ['This member worker reports to the configured project lead.']
110
+ else:
111
+ requested_agent_id = request.agent_id
112
+ selected = [lead]
113
+ reason = 'project_lead_reporting'
114
+ basis = ['The project lead owns the task; requested members report results to this lead.']
115
+ if owner and selected and owner == selected[0]['agent_id']:
75
116
  reason, basis = 'active_task_owner', ['reuse the owner of this host session task']
76
117
  agent = await self.get_project_agent(
77
118
  actor, request.personal_space_id, request.personal_project_id,
78
119
  selected[0]['agent_id'],
79
120
  ) if selected else None
121
+ # A reporting lead does not execute in the member's client. Acknowledge
122
+ # the capability only after the executing role passes selection, so a
123
+ # failed resolution cannot be accepted as that member's answer.
124
+ if delegated and (not agent or not await self.redeem_agent_delegation(request, project_lead)):
125
+ return ProjectAgentContextResolution(status='agent_unavailable', reason='delegation_invalid')
80
126
  return ProjectAgentContextResolution(
81
127
  status='ready' if agent else (
82
128
  'manual_selection' if reason == 'manual_agent_selection'
83
129
  else 'agent_unavailable' if reason == 'agent_unavailable' else 'unassigned'
84
130
  ),
85
131
  agent=agent, reason=reason, match_basis=basis, candidate_count=len(candidates),
132
+ requested_agent_id=requested_agent_id,
133
+ reporting_lead_agent_id=reporting_lead_agent_id,
86
134
  )
@@ -93,6 +93,8 @@ class StoreProjectAgentCoordinationPolicy:
93
93
  members = (request.team_member_agent_ids if 'team_member_agent_ids' in request.model_fields_set
94
94
  else current.team_member_agent_ids)
95
95
  auto_grow = current.auto_grow_team if request.auto_grow_team is None else request.auto_grow_team
96
+ if 'team_lead_agent_id' in request.model_fields_set and not lead:
97
+ raise HTTPException(status_code=422, detail='each project requires a lead; select a replacement instead of clearing the lead')
96
98
  if members and not lead:
97
99
  raise HTTPException(status_code=422, detail='select a team lead before adding members')
98
100
  if lead in members:
@@ -115,9 +117,21 @@ class StoreProjectAgentCoordinationPolicy:
115
117
  updated_at = now_utc()
116
118
  written, _, _ = await self.runtime.driver.execute_query(
117
119
  '''
118
- MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})-
120
+ MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})-
119
121
  [:CONTAINS_PROJECT]->
120
122
  (project:FuliPersonalProject {project_id: $personal_project_id})
123
+ OPTIONAL MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(role:FuliProjectAgent)
124
+ WHERE role.agent_id IN $team_agent_ids
125
+ WITH project, role ORDER BY role.agent_id
126
+ FOREACH (locked IN CASE WHEN role IS NULL THEN [] ELSE [role] END |
127
+ SET locked._task_lifecycle_lock = true
128
+ REMOVE locked._task_lifecycle_lock)
129
+ WITH project, collect(role) AS roles
130
+ WHERE size(roles) = size($team_agent_ids)
131
+ AND all(role IN roles WHERE role.status = 'active' AND EXISTS {
132
+ MATCH (project)-[:HAS_PROJECT_AGENT_ASSIGNMENT]->
133
+ (:FuliProjectAgentAssignment {status: 'active'})-[:ASSIGNED_AGENT]->(role)
134
+ })
121
135
  MERGE (policy:FuliProjectAgentCoordinationPolicy {
122
136
  policy_id: $policy_id
123
137
  })
@@ -147,6 +161,7 @@ class StoreProjectAgentCoordinationPolicy:
147
161
  auto_grow_team=auto_grow,
148
162
  team_lead_agent_id=lead,
149
163
  team_member_agent_ids=members,
164
+ team_agent_ids=([lead] if lead else []) + members,
150
165
  expected_updated_at=current.updated_at,
151
166
  updated_at=updated_at,
152
167
  )
@@ -761,9 +761,11 @@ class StoreProjectAgentTaskRecruitment:
761
761
  request.personal_space_id,
762
762
  request.personal_project_id,
763
763
  )
764
+ first_lead = (request.staffing_intent == 'default_lead' and participant_role == 'lead'
765
+ and coordination_policy.team_lead_agent_id is None)
764
766
  confirmation_mode = (
765
767
  'require_confirmation'
766
- if coordination_policy.ask_before_recruitment
768
+ if coordination_policy.ask_before_recruitment and not first_lead
767
769
  else 'automatic'
768
770
  )
769
771
  if not hr_agent_id:
@@ -54,14 +54,15 @@ class StoreProjectAgentTaskStaffing:
54
54
  row for row in assignment_rows
55
55
  if self._agent_client_allowed(row, request.source_application)
56
56
  ]
57
- if request.staffing_intent in {'new_durable', 'temporary'}:
57
+ if request.staffing_intent in {'new_durable', 'temporary', 'default_lead'}:
58
58
  return (
59
59
  [],
60
60
  rows,
61
61
  'explicit_temporary_agent'
62
62
  if request.staffing_intent == 'temporary'
63
63
  else 'explicit_new_agent',
64
- ['user explicitly requested recruitment'],
64
+ ['project requires an accountable lead' if request.staffing_intent == 'default_lead'
65
+ else 'user explicitly requested recruitment'],
65
66
  )
66
67
  policy = await self.get_project_agent_coordination_policy(
67
68
  actor,
@@ -347,13 +347,16 @@ class StoreProjectAgentTasks(
347
347
  reason = 'hr_unavailable'
348
348
  self._verify_parallel_plan(request.parallel_plan, participants)
349
349
 
350
- executor_decision = await self._resolve_executor_if_available(
351
- actor,
352
- request,
353
- lead,
354
- model_strategy,
355
- model_source,
356
- task_id,
350
+ # Staffing a default lead executes nothing, so it needs no executor.
351
+ executor_decision = None if request.staffing_intent == 'default_lead' else (
352
+ await self._resolve_executor_if_available(
353
+ actor,
354
+ request,
355
+ lead,
356
+ model_strategy,
357
+ model_source,
358
+ task_id,
359
+ )
357
360
  )
358
361
  if lead and executor_decision and self._executor_decision_blocked(executor_decision):
359
362
  routing_outcome = 'blocked'
@@ -904,7 +907,8 @@ class StoreProjectAgentTasks(
904
907
  lead_agent_id=lead_agent_id,
905
908
  complexity=complexity,
906
909
  complexity_basis=complexity_basis,
907
- verification_required=request.verification_required,
910
+ # A staffing record has no artifact to verify.
911
+ verification_required=request.staffing_intent != 'default_lead',
908
912
  model_strategy_json=model_strategy.model_dump_json() if model_strategy else None,
909
913
  task_model_strategy_override_json=(
910
914
  request.model_strategy_override.model_dump_json()