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
@@ -9,26 +9,24 @@
9
9
  />
10
10
  <link rel="icon" type="image/png" href="/assets/fuli-logo-ER1wQ8Kk.png" />
11
11
  <title>复利 · Context Graph</title>
12
- <script type="module" crossorigin src="/assets/index-ulISYH02.js"></script>
13
- <link rel="modulepreload" crossorigin href="/assets/i18n-D2CJkEYK.js">
14
- <link rel="modulepreload" crossorigin href="/assets/client-B2cRl-7c.js">
15
- <link rel="modulepreload" crossorigin href="/assets/console-BtPqo79j.js">
12
+ <script type="module" crossorigin src="/assets/index-D1OHIz4c.js"></script>
13
+ <link rel="modulepreload" crossorigin href="/assets/i18n-b14l2pQA.js">
16
14
  <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-BDNMzG2s.js">
17
- <link rel="modulepreload" crossorigin href="/assets/GrowthLoading-KTjE2e50.js">
18
- <link rel="modulepreload" crossorigin href="/assets/useApi-CROJJdhE-BtFMXFjw.js">
19
- <link rel="modulepreload" crossorigin href="/assets/vue-router-DkaTtPn0.js">
15
+ <link rel="modulepreload" crossorigin href="/assets/client-DLS5FZBI.js">
16
+ <link rel="modulepreload" crossorigin href="/assets/console-CqzwSo7a.js">
17
+ <link rel="modulepreload" crossorigin href="/assets/UiDialog-Q7fsJUoO.js">
18
+ <link rel="modulepreload" crossorigin href="/assets/useApi-CROJJdhE-CEQAEuug.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/vue-router-B6ZjK-Br.js">
20
20
  <link rel="modulepreload" crossorigin href="/assets/fuli-logo-Bu9WBuQe.js">
21
- <link rel="modulepreload" crossorigin href="/assets/copy-D2LqwtPZ.js">
22
- <link rel="modulepreload" crossorigin href="/assets/catalog-DN72452f.js">
23
- <link rel="modulepreload" crossorigin href="/assets/attention-store-Dk-_P4Oi.js">
24
- <link rel="modulepreload" crossorigin href="/assets/AgentHand-eGsXoGSq.js">
25
- <link rel="modulepreload" crossorigin href="/assets/profile-model-BQO4zNs9.js">
26
- <link rel="modulepreload" crossorigin href="/assets/useModalDialog-D8tiJfbc.js">
27
- <link rel="modulepreload" crossorigin href="/assets/UiDisclosure-CWlC91Zt.js">
21
+ <link rel="modulepreload" crossorigin href="/assets/UiSelect-DcGpPQQo.js">
22
+ <link rel="modulepreload" crossorigin href="/assets/attention-store-D-qIFRIb.js">
23
+ <link rel="modulepreload" crossorigin href="/assets/AgentHand-DkyF4vgk.js">
24
+ <link rel="modulepreload" crossorigin href="/assets/UiDisclosure-BS-muK5j.js">
28
25
  <link rel="modulepreload" crossorigin href="/assets/paths-CDREouhi.js">
29
- <link rel="stylesheet" crossorigin href="/assets/GrowthLoading-CIuQ1icy.css">
26
+ <link rel="stylesheet" crossorigin href="/assets/client-CIuQ1icy.css">
27
+ <link rel="stylesheet" crossorigin href="/assets/UiSelect-Bv6R0yr1.css">
30
28
  <link rel="stylesheet" crossorigin href="/assets/AgentHand-Y83Oo36U.css">
31
- <link rel="stylesheet" crossorigin href="/assets/index-CNp0tRNp.css">
29
+ <link rel="stylesheet" crossorigin href="/assets/index-kPB0Cf8Q.css">
32
30
  </head>
33
31
  <body>
34
32
  <div id="app"></div>
@@ -7,15 +7,15 @@ Neo4j Provider 的独立日志中;工作记忆继续保存经过整理的决
7
7
 
8
8
  | 设置 | 默认 | 含义 |
9
9
  | --- | --- | --- |
10
- | 不活跃归档 | 7 天 | 按最后一条新事件计算;重试不延长活跃期 |
11
- | 自动恢复预算 | 2000 UTF-8 字节 | 保守内容上限,不是计费 token;可配 512–16000 |
10
+ | 压缩阈值 | 64 KB | 未折叠的消息原文超过阈值时,较早的消息折叠进摘要;可配 8–1024 |
11
+ | 自动恢复预算 | 4000 UTF-8 字节 | 保守内容上限,不是计费 token;可配 512–16000 |
12
12
  | 自动恢复 | 摘要 + 最近消息 | 新 chat 恢复同一 Agent、同一项目的最近上下文 |
13
- | 归档恢复 | 摘要 | 原始记录仍在数据库,可分页补读 |
14
- | 原始记录保存 | gzip 压缩 | 归档不删除原文,也不调用模型生成摘要 |
13
+ | 摘要内容 | 最近 12 个任务结果 + 最近 40 条请求要点 | 请求要点取每条用户消息的首行 |
14
+ | 原始记录保存 | gzip 压缩 | 压缩不删除原文,也不调用模型生成摘要 |
15
15
 
16
- 7 天内已成功采集的记录保持可读,不把 7 天的原文全部塞进每次提示词。摘要复用任务结束时
17
- 已经生成的工作结果;工具输出不自动注入恢复上下文。没有提交摘要的历史明确保留
18
- 空摘要,需要时补读,不生成虚构摘要。当前任务提示及宿主可见消息各有来源记录。
16
+ 压缩只看对话量,不看时间:闲置多久都不会丢失上下文。每个任务结束时的工作结果写入摘要;
17
+ 工具输出不自动注入恢复上下文。折叠后的原文仍在数据库,需要细节时用
18
+ `read_agent_conversation` 分页补读,不生成虚构摘要。当前任务提示及宿主可见消息各有来源记录。
19
19
  敏感凭据命中检测时整条内容替换为省略标记,因此“原始记录”是已过滤的可见记录。
20
20
 
21
21
  入口中的偏好保留完整指令,去掉重复证据包装。长期记忆只返回预览;更新前必须
@@ -120,7 +120,7 @@ JSONL transcript。只支持各适配器明确识别的可见文本、工具调
120
120
  ## 界面
121
121
 
122
122
  默认突出负责人、状态和当前工作;对话按需展开。执行器、路由、学习和技术明细
123
- 收在高级区域。记忆设置提供归档天数、恢复预算及采集开关,读取失败可重试。
123
+ 收在高级区域。记忆设置提供压缩阈值、恢复预算及采集开关,读取失败可重试。
124
124
 
125
125
  ## 设计参考与验证
126
126
 
@@ -128,5 +128,5 @@ JSONL transcript。只支持各适配器明确识别的可见文本、工具调
128
128
  - [Letta Agent File](https://github.com/letta-ai/agent-file):分开历史记录与送入模型的上下文。
129
129
  - [OpenHands event store](https://github.com/OpenHands/software-agent-sdk/blob/main/openhands-sdk/openhands/sdk/conversation/event_store.py):事件日志与恢复。
130
130
 
131
- 当前实现的 7 天、字节预算、能力档位、两次失败和 15 秒期限是产品配置选择,
131
+ 当前实现的压缩阈值、字节预算、能力档位、两次失败和 15 秒期限是产品配置选择,
132
132
  不是以上项目的默认配置。数据库集成测试只使用一次性本机数据库和合成数据。
@@ -0,0 +1,77 @@
1
+ # Agent Roundtable · 圆桌
2
+
3
+ Agents talk to each other directly, the way a lead asks a teammate or a sub-agent.
4
+ A Codex Agent can ask the Agent behind a specific Claude Code conversation, a Claude
5
+ Code Agent can ask a FULI Agent that has never run yet, and so on. Nobody creates a
6
+ room or invites anyone: an Agent sends a message, the recipient answers, and Fuli
7
+ records the exchange.
8
+
9
+ ## How Agents use it
10
+
11
+ All tools are on the normal `fuli` MCP server.
12
+
13
+ | Tool | Purpose |
14
+ | --- | --- |
15
+ | `find_agents` | Active FULI Agents, their clients and recent Claude Code / Codex conversations |
16
+ | `message_agent` | Ask an Agent; returns its answer, or queues the message |
17
+ | `read_agent_messages` | Messages waiting for the current task's Agent |
18
+ | `reply_agent_message` | Answer one of them |
19
+ | `read_agent_thread` | Every message of one thread |
20
+
21
+ `message_agent` takes `to` (Agent ID, exact name, or employee number such as
22
+ `FLA 000001`), `body`, the caller's
23
+ `taskContextToken`, `projectPath` (the current directory), and optionally `threadId`
24
+ to continue a thread or `conversation` to choose which of the recipient's
25
+ conversations answers (`auto` by default, `new`, or a session ID from `find_agents`).
26
+
27
+ Each Fuli Agent (FLA) has a permanent employee number within its personal space.
28
+ Numbers start at `000001`, expand beyond six digits when needed, and are never
29
+ reused after archival. Moving between projects or clients preserves the number;
30
+ existing IDs and profile links remain valid. `find_agents` can search by number.
31
+
32
+ Every project has one accountable lead. User requests for a member go through
33
+ that lead. A recorded lead-to-member roundtable delivery restores the member's
34
+ own isolated context and returns its answer to the lead for the final report.
35
+ Changing, deactivating, or archiving a project lead requires a handoff first.
36
+ Delivery requires an active task context. The Provider issues a short-lived
37
+ capability that Fuli passes only to the receiving process, binds to its session,
38
+ and revokes after each attempt. Public message headers do not grant access.
39
+ An answer is attributed to the recipient only after that session redeems the
40
+ capability. Members send their results through the project lead.
41
+
42
+ ## Delivery
43
+
44
+ 1. **Wake.** By default Fuli wakes the recipient in its own client, headlessly and
45
+ read-only, and waits for its final answer (default 5 minutes, at most 15):
46
+ - Claude Code resumes the conversation with `--resume <id> --fork-session`, so the
47
+ answer has that conversation's full context while the original stays untouched.
48
+ - Codex runs `codex exec resume <id>` with a read-only sandbox. The question and
49
+ answer are recorded in that conversation, so it shows up there next time it is
50
+ opened. Codex refuses while the conversation is open in the app; Fuli then asks
51
+ the same Agent in a new session instead.
52
+ - Without a resumable conversation, Fuli starts a new session in the caller's
53
+ project directory with `@{agent}`, which loads that Agent's memory and role.
54
+ - If that client is not installed or not logged in on this machine, Fuli tries the
55
+ Agent's next allowed client. A timeout stops there instead of starting another run.
56
+ 2. **Inbox.** If no allowed client can answer (none installed or logged in, or the
57
+ wake timed out), the message waits. The next time that Agent starts a task,
58
+ task entry includes it under `agent_messages`, and the Agent answers with
59
+ `reply_agent_message`.
60
+
61
+ A thread holds at most 24 messages and at most 3 asks may be waiting on each other
62
+ inside one thread, so Agents cannot loop forever. Woken Agents run read-only.
63
+
64
+ ## What people see
65
+
66
+ The console's **圆桌 / Roundtable** page lists every thread with its participants and
67
+ shows the full timeline: who asked whom, through which client, the exact message and
68
+ answer, and how it was delivered. People watch; they do not need to act.
69
+
70
+ Threads are stored locally in `agent-roundtable.sqlite` in the Fuli data directory.
71
+
72
+ ## Clients
73
+
74
+ Waking needs the client's CLI on this machine: `claude` for Claude Code, and `codex`
75
+ (or the CLI bundled with the Codex app) for Codex. Override the paths with
76
+ `FULI_CLAUDE_BIN` / `FULI_CODEX_BIN`. A recipient is only woken in clients its Agent
77
+ profile allows.
@@ -1,10 +1,10 @@
1
1
  # Specialist Agents
2
2
 
3
- An employee template is a reusable role with an optional FULI-hosted or native workbench. Recruitment reuses the existing durable Project Agent identity and assignment model. The built-in specialists are **Jefa**, the project manager, and **Bole**, the fixed system HR Agent. Bole is created with every personal space, cannot be archived or replaced by another `hr` identity, and does not require a separate runtime package.
3
+ An employee template is a reusable role with an optional FULI-hosted or native workbench. Recruitment reuses the existing durable Project Agent identity and assignment model. Two roles are fixed: **Jefa**, the project manager, and **Bole**, the HR Agent. Both exist in every personal space from the start, manage all projects by default, cannot be archived or replaced by another coordinator or `hr` identity, and are never offered for hire. Neither requires a separate runtime package.
4
4
 
5
5
  ## User flow
6
6
 
7
- Open **Project Agents → Hire an Agent**, select a template and its responsibility policy, then hire. The custom project picker supports multiple selection, individual exclusions, search, and **Invert selection**. A newly recruited Jefa defaults to **All projects**, including future active projects unless explicitly excluded. **Selected projects** keeps a fixed selection; selecting none in that mode hires without a project. The Agent directory reflects the persistent identity; **Specialist Agents → Jefa** in the sidebar opens its installed board directly. Hiring alone does not create a project. Register a lasting project from the personal-project directory, or explicitly coordinate a one-off task to use the temporary scope described below.
7
+ Open **Project Agents → Hire an Agent**, select a template and its responsibility policy, then hire. Only non-fixed templates appear there; to narrow what Jefa or Bole manage, use **Manage assigned projects** on their card. The custom project picker supports multiple selection, individual exclusions, search, and **Invert selection**. **All projects** includes future active projects unless explicitly excluded. **Selected projects** keeps a fixed selection; selecting none in that mode hires without a project. The Agent directory reflects the persistent identity; **Specialist Agents → Jefa** in the sidebar opens its installed board directly. Hiring alone does not create a project. Register a lasting project from the personal-project directory, or explicitly coordinate a one-off task to use the temporary scope described below.
8
8
 
9
9
  Specialist workspaces use a compact host toolbar rather than the generic console heading. They keep the workspace's own navigation and content, plus the host's project switcher and responsibility editor. The selected project is reflected in the URL; an unauthorized explicit deep link is never silently redirected to a different project. Empty assignments, unavailable templates and catalog errors each have an actionable state. The `employee` CLI/API/manifest names are unchanged for compatibility.
10
10
 
@@ -128,7 +128,7 @@ MCP 上下文有长度上限。被截断时必须显式补读完整记忆后再
128
128
  `FULI_TEST_NEO4J_URI`。这些测试全部使用合成角色和数据;协议测试、真实数据库测试
129
129
  与真实模型客户端验收属于不同证据,不能互相替代。
130
130
 
131
- 可见对话的 7 天归档、跨客户端接续、借调与验证升级见
131
+ 可见对话的按量压缩、跨客户端接续、借调与验证升级见
132
132
  [对话与协作](agent-conversations-and-collaboration.md)。
133
133
 
134
134
  ## 必需的回复身份回执
@@ -6,10 +6,19 @@ from .model_validation import reject_credentials
6
6
 
7
7
 
8
8
  class ConversationPolicy(StrictModel):
9
- idle_days: int = Field(default=7, ge=1, le=365)
10
- context_budget: int = Field(default=2000, ge=512, le=16000)
9
+ # Fold older messages into the digest once this much unfolded text accumulates.
10
+ compact_after_kb: int = Field(default=64, ge=8, le=1024)
11
+ context_budget: int = Field(default=4000, ge=512, le=16000)
11
12
  enabled: bool = True
12
13
 
14
+ @model_validator(mode='before')
15
+ @classmethod
16
+ def drop_retired_idle_days(cls, value):
17
+ # Policies saved before size-based compaction carried an inactivity window.
18
+ if isinstance(value, dict) and 'idle_days' in value:
19
+ value = {key: item for key, item in value.items() if key != 'idle_days'}
20
+ return value
21
+
13
22
 
14
23
  class ConversationScope(StrictModel):
15
24
  personal_space_id: str = Field(min_length=1, max_length=128)
@@ -1,12 +1,43 @@
1
- """Deterministic recovery: no model calls, conservative UTF-8 byte accounting."""
1
+ """Deterministic conversation compaction and recovery: no model calls.
2
+
3
+ Older messages fold into a bounded digest once the unfolded text passes a size
4
+ threshold; raw events are always retained. Sizes are conservative UTF-8 bytes.
5
+ """
2
6
  import json
3
- from datetime import datetime, timezone
7
+
8
+ DIGEST_TASKS = 12
9
+ DIGEST_NOTES = 40
10
+ NOTE_CHARS = 160
11
+
12
+
13
+ def message_bytes(event):
14
+ if event['role'] not in ('user', 'assistant') or event.get('kind', 'message') != 'message':
15
+ return 0
16
+ return len(event['content'].encode())
17
+
18
+
19
+ def request_note(content):
20
+ """The first line of a user request, bounded; enough to recall what was asked."""
21
+ line = next((part.strip() for part in content.splitlines() if part.strip()), '')
22
+ return line if len(line) <= NOTE_CHARS else line[:NOTE_CHARS - 1] + '…'
23
+
24
+
25
+ def add_task_result(digest, at, status, summary):
26
+ tasks = [*digest.get('tasks', []), {'at': at, 'status': status, 'summary': summary}]
27
+ return {**digest, 'tasks': tasks[-DIGEST_TASKS:]}
28
+
29
+
30
+ def fold_messages(digest, events):
31
+ notes = [request_note(event['content']) for event in events
32
+ if event['role'] == 'user' and event.get('kind', 'message') == 'message']
33
+ return {**digest, 'notes': [*digest.get('notes', []), *filter(None, notes)][-DIGEST_NOTES:]}
4
34
 
5
35
 
6
- def archived(last_activity, idle_days, *, now=None):
7
- current = datetime.fromisoformat(now) if now else datetime.now(timezone.utc)
8
- previous = datetime.fromisoformat(last_activity)
9
- return (current - previous).total_seconds() >= idle_days * 86400
36
+ def render_digest(digest):
37
+ lines = [f"[{task['at'][:10]} {task['status']}] {task['summary']}" for task in digest.get('tasks', [])]
38
+ if digest.get('notes'):
39
+ lines += ['Earlier requests:', *(f'- {note}' for note in digest['notes'])]
40
+ return '\n'.join(lines)
10
41
 
11
42
 
12
43
  def pack_context(summary, events, budget):
@@ -14,12 +45,12 @@ def pack_context(summary, events, budget):
14
45
  def size():
15
46
  return len(json.dumps(result, ensure_ascii=False, separators=(',', ':')).encode())
16
47
  result['summary'] = summary or ''
17
- # Reserve half for recent messages; Unicode slicing never splits codepoints.
48
+ # Reserve half for recent messages; keep the newest digest lines.
18
49
  while len(result['summary'].encode()) > budget // 2:
19
- result['summary'] = result['summary'][:max(0, len(result['summary']) - 64)]
50
+ result['summary'] = result['summary'][64:]
20
51
  result['truncated'] = True
21
52
  for event in reversed(events):
22
- if event['role'] not in ('user', 'assistant') or event.get('kind', 'message') != 'message':
53
+ if not message_bytes(event):
23
54
  continue
24
55
  item = {'role': event['role'], 'content': event['content']}
25
56
  result['messages'].insert(0, item)
@@ -0,0 +1,88 @@
1
+ """Every task in a registered project gets a FULI Agent.
2
+
3
+ When a project has no team lead, Bole hires one through an ordinary staffing
4
+ task. The recruitment stays auditable and the new lead joins the project team.
5
+ """
6
+
7
+ from fastapi import HTTPException
8
+ from pydantic import Field
9
+
10
+ from .model_base import StrictModel
11
+ from .models import SourceApplication
12
+ from .project_agent_context_models import ProjectAgentContextResolution
13
+ from .project_agent_models import ProjectAgentProfile
14
+ from .project_agent_task_models import ProjectAgentTaskActivityCreate, ProjectAgentTaskSubmit
15
+
16
+
17
+ class DefaultProjectLeadRequest(StrictModel):
18
+ personal_space_id: str = Field(min_length=1, max_length=128)
19
+ personal_project_id: str = Field(min_length=1, max_length=128)
20
+ source_application: SourceApplication = 'other'
21
+
22
+
23
+ def default_lead_profile(project_name):
24
+ return ProjectAgentProfile(
25
+ name=f'{project_name} 负责人'[:160],
26
+ responsibility=f'负责 {project_name} 的开发、修复、测试与交付,并延续项目上下文。'[:4096],
27
+ work_kinds=['implementation', 'project_context', 'code_review', 'test_validation', 'architecture'],
28
+ capabilities=['代码实现', '问题修复', '测试验证', '代码审查', '项目上下文'],
29
+ initial_preferences=['先理解现有实现与约定,再修改。', '结论附带可复现的验证证据。'],
30
+ )
31
+
32
+
33
+ class StoreDefaultProjectLead:
34
+ async def staff_default_project_lead(self, actor, request: DefaultProjectLeadRequest):
35
+ self._require_personal()
36
+ project = await self.get_personal_project(
37
+ actor, request.personal_space_id, request.personal_project_id,
38
+ )
39
+ policy = await self.get_project_agent_coordination_policy(
40
+ actor, request.personal_space_id, request.personal_project_id,
41
+ )
42
+ if policy.team_lead_agent_id:
43
+ # An existing lead that could not take this task is a user decision.
44
+ return ProjectAgentContextResolution(status='unassigned', reason='team_lead_unavailable')
45
+ submit = ProjectAgentTaskSubmit(
46
+ personal_space_id=request.personal_space_id,
47
+ personal_project_id=request.personal_project_id,
48
+ idempotency_key=f'default-lead:{request.personal_project_id}:{policy.updated_at or "initial"}',
49
+ title='接任项目负责人',
50
+ objective='为项目配置默认负责人,承接后续任务并延续项目上下文。',
51
+ work_kind='project_context',
52
+ duration='one_off',
53
+ staffing_intent='default_lead',
54
+ source_application=request.source_application,
55
+ routing_reason='Every task needs a FULI Agent; this project had no team lead.',
56
+ recruitment_profile=default_lead_profile(project.profile.name),
57
+ )
58
+ routed = await self.submit_project_agent_task(actor, submit)
59
+ lead = routed.assigned_agent
60
+ if not lead:
61
+ return ProjectAgentContextResolution(
62
+ status='unassigned', reason=routed.task.routing_reason or 'staffing_unavailable',
63
+ )
64
+ task = routed.task
65
+ for status, summary in (('running', '接任项目负责人'), ('completed', f'{lead.profile.name} 已接任项目负责人')):
66
+ if task.status in {'completed', 'cancelled', 'failed'}:
67
+ break
68
+ try:
69
+ task = await self.record_project_agent_task_activity(actor, ProjectAgentTaskActivityCreate(
70
+ personal_space_id=request.personal_space_id,
71
+ personal_project_id=request.personal_project_id,
72
+ task_id=task.task_id,
73
+ idempotency_key=f'{submit.idempotency_key}:{status}',
74
+ status=status,
75
+ summary=summary,
76
+ agent_id=lead.agent_id,
77
+ source_application=request.source_application,
78
+ ))
79
+ except HTTPException as error:
80
+ # The hire already succeeded; a closed staffing record is cosmetic.
81
+ if error.status_code != 409:
82
+ raise
83
+ break
84
+ return ProjectAgentContextResolution(
85
+ status='ready', reason='default_project_lead',
86
+ match_basis=['HR hired the project lead for its first task'],
87
+ candidate_count=1, agent=lead,
88
+ )
@@ -1,4 +1,4 @@
1
- """Read-only task-entry selection; never submits a task or spawns a worker."""
1
+ """Task-entry selection and optional invocation claim; never spawns a worker."""
2
2
 
3
3
  from typing import Literal
4
4
 
@@ -13,6 +13,8 @@ class ProjectAgentContextRequest(StrictModel):
13
13
  personal_space_id: str = Field(min_length=1, max_length=128)
14
14
  personal_project_id: str = Field(min_length=1, max_length=128)
15
15
  agent_id: str | None = Field(default=None, min_length=1, max_length=128)
16
+ delegation_token: str | None = Field(default=None, min_length=32, max_length=128)
17
+ delegation_session_id: str | None = Field(default=None, min_length=1, max_length=256)
16
18
  session_id: str | None = Field(default=None, min_length=1, max_length=256)
17
19
  turn_id: str | None = Field(default=None, min_length=1, max_length=256)
18
20
  work_kind: str = Field(default='project_context', min_length=1, max_length=128)
@@ -26,4 +28,6 @@ class ProjectAgentContextResolution(StrictModel):
26
28
  match_basis: list[str] = Field(default_factory=list)
27
29
  candidate_count: int = 0
28
30
  agent: ProjectAgentRecord | None = None
31
+ requested_agent_id: str | None = None
32
+ reporting_lead_agent_id: str | None = None
29
33
  worker_started: Literal[False] = False
@@ -0,0 +1,118 @@
1
+ """Short-lived, session-bound capabilities for a real roundtable invocation."""
2
+
3
+ from hashlib import sha256
4
+ from secrets import token_urlsafe
5
+
6
+ from fastapi import HTTPException
7
+ from pydantic import Field
8
+
9
+ from .models import SourceApplication, StrictModel
10
+ from .personal_project_access import authorize_personal_project
11
+
12
+
13
+ class AgentDelegationIssue(StrictModel):
14
+ personal_space_id: str = Field(min_length=1, max_length=128)
15
+ task_context_token: str = Field(min_length=1, max_length=256)
16
+ source_application: SourceApplication
17
+ agent_id: str = Field(min_length=1, max_length=128)
18
+ target_application: SourceApplication
19
+ target_session_id: str | None = Field(default=None, min_length=1, max_length=256)
20
+ lifetime_seconds: int = Field(default=300, ge=30, le=900)
21
+
22
+
23
+ class AgentDelegationCheck(StrictModel):
24
+ personal_space_id: str = Field(min_length=1, max_length=128)
25
+ token: str = Field(min_length=32, max_length=128)
26
+ session_id: str | None = Field(default=None, min_length=1, max_length=256)
27
+
28
+
29
+ def token_hash(token):
30
+ return sha256(token.encode()).hexdigest()
31
+
32
+
33
+ class StoreAgentDelegations:
34
+ async def issue_agent_delegation(self, actor, request):
35
+ self._require_personal()
36
+ space = await self.authorize(actor, request.personal_space_id, 'maintainer')
37
+ context = await self.get_task_context(actor, request.personal_space_id,
38
+ request.task_context_token, request.source_application)
39
+ project_id = context.get('personal_project_id')
40
+ if not project_id or (context.get('checkpoint') or {}).get('phase') == 'complete':
41
+ raise HTTPException(409, 'Delegation requires an active project task')
42
+ await authorize_personal_project(self, actor, space, project_id)
43
+ policy = await self.get_project_agent_coordination_policy(actor, request.personal_space_id, project_id)
44
+ lead = policy.team_lead_agent_id
45
+ sender = context.get('project_agent_id')
46
+ if not lead or not sender or (sender != lead and request.agent_id != lead):
47
+ raise HTTPException(403, 'Members must communicate through the project lead')
48
+ target = await self.get_project_agent(actor, request.personal_space_id, project_id, request.agent_id)
49
+ if (target.profile.status != 'active'
50
+ or request.target_application not in target.profile.allowed_clients):
51
+ raise HTTPException(403, 'The receiving Agent is unavailable in this client')
52
+ token = token_urlsafe(32)
53
+ # Only hashes are stored. A crash leaves a bounded lifetime, never a
54
+ # reusable authority derived from a public message or thread identifier.
55
+ await self.runtime.driver.execute_query(
56
+ '''MATCH (old:FuliAgentDelegation {space_id: $space_id})
57
+ WHERE old.expires_at <= datetime() DELETE old''', space_id=request.personal_space_id)
58
+ await self.runtime.driver.execute_query(
59
+ '''CREATE (:FuliAgentDelegation {token_hash: $token_hash, space_id: $space_id,
60
+ project_id: $project_id, agent_id: $agent_id, source_application: $application,
61
+ session_id: $session_id, sender_token: $sender_token, lead_agent_id: $lead,
62
+ expires_at: datetime() + duration({seconds: $lifetime}), redeemed: false})''',
63
+ token_hash=token_hash(token), space_id=request.personal_space_id, project_id=project_id,
64
+ agent_id=request.agent_id, application=request.target_application,
65
+ session_id=request.target_session_id, sender_token=request.task_context_token,
66
+ lead=lead, lifetime=request.lifetime_seconds)
67
+ return {'token': token}
68
+
69
+ async def redeem_agent_delegation(self, request, lead):
70
+ if not request.delegation_token or not request.delegation_session_id or not request.agent_id:
71
+ return False
72
+ # Lock before reading the session binding: two different sessions cannot
73
+ # both claim a fresh capability. Same-session hook/MCP retries are safe.
74
+ rows, _, _ = await self.runtime.driver.execute_query(
75
+ '''MATCH (:FuliSpace {id: $space_id})-[:CONTAINS_PROJECT]->
76
+ (:FuliPersonalProject {project_id: $project_id})-
77
+ [:HAS_PROJECT_AGENT_COORDINATION_POLICY]->(policy:FuliProjectAgentCoordinationPolicy)
78
+ SET policy._fuli_write_lock = true
79
+ REMOVE policy._fuli_write_lock
80
+ WITH policy WHERE policy.team_lead_agent_id = $lead
81
+ MATCH (grant:FuliAgentDelegation {token_hash: $token_hash, space_id: $space_id,
82
+ project_id: $project_id, agent_id: $agent_id, source_application: $application,
83
+ lead_agent_id: $lead})
84
+ SET grant.write_serial = coalesce(grant.write_serial, 0) + 1
85
+ WITH grant
86
+ WHERE grant.expires_at > datetime()
87
+ AND (grant.session_id IS NULL OR grant.session_id = $session_id)
88
+ MATCH (session:FuliTaskContextSession)-[:HAS_CONTEXT]->(task:FuliTaskContext)
89
+ WHERE task.token = grant.sender_token AND session.current_token = task.token
90
+ AND NOT task.completed AND task.personal_space_id = grant.space_id
91
+ AND task.personal_project_id = grant.project_id
92
+ AND (task.project_agent_id = grant.lead_agent_id OR grant.agent_id = grant.lead_agent_id)
93
+ SET grant.session_id = $session_id, grant.redeemed = true
94
+ RETURN grant.token_hash AS token_hash''',
95
+ token_hash=token_hash(request.delegation_token), space_id=request.personal_space_id,
96
+ project_id=request.personal_project_id, agent_id=request.agent_id,
97
+ application=request.source_application, lead=lead, session_id=request.delegation_session_id)
98
+ return bool(rows)
99
+
100
+ async def verify_agent_delegation(self, actor, request):
101
+ self._require_personal()
102
+ await self.authorize(actor, request.personal_space_id, 'reader')
103
+ rows, _, _ = await self.runtime.driver.execute_query(
104
+ '''MATCH (grant:FuliAgentDelegation {token_hash: $token_hash, space_id: $space_id,
105
+ session_id: $session_id, redeemed: true})
106
+ WHERE grant.expires_at > datetime()
107
+ RETURN grant.token_hash AS token_hash''',
108
+ token_hash=token_hash(request.token), space_id=request.personal_space_id,
109
+ session_id=request.session_id)
110
+ return {'redeemed': bool(rows)}
111
+
112
+ async def revoke_agent_delegation(self, actor, request):
113
+ self._require_personal()
114
+ await self.authorize(actor, request.personal_space_id, 'maintainer')
115
+ await self.runtime.driver.execute_query(
116
+ '''MATCH (grant:FuliAgentDelegation {token_hash: $token_hash, space_id: $space_id})
117
+ DELETE grant''', token_hash=token_hash(request.token), space_id=request.personal_space_id)
118
+ return {'revoked': True}
@@ -0,0 +1,41 @@
1
+ """Space-wide FLA employee numbers, assigned in the identity write transaction.
2
+
3
+ Both identity creation paths take the space lock before touching an Agent.
4
+ The high-water mark survives archives and identity aliases; numbers are never
5
+ reused. Existing identities are backfilled in creation order on upgrade.
6
+ """
7
+
8
+ EMPLOYEE_NUMBER_LOCK = '''
9
+ SET space._employee_number_lock = true
10
+ REMOVE space._employee_number_lock
11
+ WITH space
12
+ '''
13
+
14
+ # Keep outer variables (notably ``agent``) intact. The subquery always returns
15
+ # one row, including when the space has no missing numbers.
16
+ ASSIGN_EMPLOYEE_NUMBERS = '''
17
+ WITH space, agent
18
+ CALL {
19
+ WITH space
20
+ MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(employee:FuliProjectAgent)
21
+ WITH DISTINCT space, employee
22
+ ORDER BY employee.created_at, employee.agent_id
23
+ WITH space, collect(employee) AS employees
24
+ WITH space, employees,
25
+ [employee IN employees WHERE employee.employee_number IS NULL] AS missing,
26
+ reduce(high = coalesce(space.fla_employee_sequence, 0), employee IN employees |
27
+ CASE WHEN coalesce(employee.employee_number, 0) > high
28
+ THEN employee.employee_number ELSE high END) AS high
29
+ SET space.fla_employee_sequence = high + size(missing)
30
+ FOREACH (index IN range(0, size(missing) - 1) |
31
+ FOREACH (employee IN [missing[index]] |
32
+ SET employee.employee_number = high + index + 1))
33
+ FOREACH (employee IN [item IN employees WHERE item.employee_number_space_id IS NULL] |
34
+ SET employee.employee_number_space_id = space.id)
35
+ RETURN space.fla_employee_sequence AS employee_sequence
36
+ }
37
+ '''
38
+
39
+
40
+ def employee_number_label(number: int | None) -> str | None:
41
+ return f'{number:06d}' if number is not None else None
@@ -13,7 +13,9 @@ from .project_agent_context_models import (
13
13
  ProjectAgentContextRequest, ProjectAgentContextResolution,
14
14
  )
15
15
  from .agent_conversation_routes import register_agent_conversation_routes
16
+ from .default_project_lead import DefaultProjectLeadRequest
16
17
  from .task_context_routes import register_task_context_routes
18
+ from .project_agent_delegations import AgentDelegationIssue, AgentDelegationCheck
17
19
 
18
20
 
19
21
  def register_project_agent_memory_routes(application, store, Actor):
@@ -25,6 +27,24 @@ def register_project_agent_memory_routes(application, store, Actor):
25
27
  async def resolve_context(request: ProjectAgentContextRequest, actor: Actor):
26
28
  return await store.resolve_project_agent_context(actor, request)
27
29
 
30
+ @application.post('/v1/project-agent-context/delegations')
31
+ async def issue_delegation(request: AgentDelegationIssue, actor: Actor):
32
+ return await store.issue_agent_delegation(actor, request)
33
+
34
+ @application.post('/v1/project-agent-context/delegations/verify')
35
+ async def verify_delegation(request: AgentDelegationCheck, actor: Actor):
36
+ return await store.verify_agent_delegation(actor, request)
37
+
38
+ @application.post('/v1/project-agent-context/delegations/revoke')
39
+ async def revoke_delegation(request: AgentDelegationCheck, actor: Actor):
40
+ return await store.revoke_agent_delegation(actor, request)
41
+
42
+ @application.post(
43
+ '/v1/project-agent-context/default-lead', response_model=ProjectAgentContextResolution,
44
+ )
45
+ async def staff_default_lead(request: DefaultProjectLeadRequest, actor: Actor):
46
+ return await store.staff_default_project_lead(actor, request)
47
+
28
48
  @application.get(
29
49
  '/v1/project-agents/{agent_id}/memory', response_model=ProjectAgentMemoryView,
30
50
  )
@@ -277,6 +277,7 @@ class ProjectAgentAssignmentReplaceResult(StrictModel):
277
277
 
278
278
  class ProjectAgentRecord(StrictModel):
279
279
  agent_id: str
280
+ employee_number: str | None = None
280
281
  legacy_agent_ids: list[str] = Field(default_factory=list)
281
282
  personal_space_id: str
282
283
  personal_project_id: str | None = None
@@ -20,6 +20,9 @@ from .project_agent_models import (
20
20
  ProjectAgentTaskDuration = Literal['ongoing', 'one_off']
21
21
  ProjectAgentStaffingIntent = Literal[
22
22
  'reuse_preferred',
23
+ # Like reuse_preferred, but a project without a team lead hires its first
24
+ # lead through HR without waiting for confirmation.
25
+ 'default_lead',
23
26
  'new_durable',
24
27
  'temporary',
25
28
  'unassigned',
@@ -33,8 +33,8 @@ async def enroll_recruited_team_member(store, recruitment, profile):
33
33
  policy.team_lead_agent_id IS NULL AS needs_lead,
34
34
  coalesce(policy.team_lead_agent_id = agent.agent_id, false)
35
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
36
+ WITH *, NOT already_member
37
+ AND (needs_lead OR (enabled AND size(members) < 32)) AS changed
38
38
  FOREACH (_ IN CASE WHEN changed THEN [1] ELSE [] END |
39
39
  SET policy.team_lead_agent_id = CASE WHEN needs_lead
40
40
  THEN agent.agent_id ELSE policy.team_lead_agent_id END,