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.
- package/README.md +27 -30
- package/README.zh-CN.md +22 -26
- package/dist/web/assets/AboutPage-cW9hDghN.js +1 -0
- package/dist/web/assets/AgentConversations-CGznTWuV.js +2 -0
- package/dist/web/assets/AgentConversations-XqNEBY_2.css +1 -0
- package/dist/web/assets/AgentHand-DkyF4vgk.js +1 -0
- package/dist/web/assets/AgentName-C1mAVr-v.js +1 -0
- package/dist/web/assets/AgentProfilePage-pKUR0rfj.js +1 -0
- package/dist/web/assets/AgentsDirectoryPage-CUlbT2hD.js +1 -0
- package/dist/web/assets/AgentsDirectoryPage-yRWktiin.css +1 -0
- package/dist/web/assets/ConnectionsPage-CM9C-zEI.css +1 -0
- package/dist/web/assets/ConnectionsPage-CuDHIJXu.js +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-CVbqCzHb.css +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-D9QfGipd.js +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-D-c_Jm2z.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-DAtvGney.js +3 -0
- package/dist/web/assets/KnowledgeEditDialog-a-5rasG0.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-hT9znkoe.css +1 -0
- package/dist/web/assets/KnowledgeInspector-BGsqjNY2.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-BndgUsHS.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-DeL7Jp34.css +1 -0
- package/dist/web/assets/KnowledgePage-CQweO0YJ.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-BtUjKTLb.js +4 -0
- package/dist/web/assets/KnowledgeWorkspace-D4a7FcAb.css +1 -0
- package/dist/web/assets/NotFoundPage-BmQTTqQn.js +1 -0
- package/dist/web/assets/OverviewPage-By-fUzSG.css +1 -0
- package/dist/web/assets/OverviewPage-C73HyTsM.js +1 -0
- package/dist/web/assets/PersonalProfilePage-BMuOrppx.js +1 -0
- package/dist/web/assets/PersonalProfilePage-DrMDAAdy.css +1 -0
- package/dist/web/assets/PersonalProjectsPage-Cq9_kFvs.js +1 -0
- package/dist/web/assets/ProjectAgentFirstTask-Bi1ZsTC1.css +1 -0
- package/dist/web/assets/ProjectAgentFirstTask-CaHvOjkp.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-DWGkgIsr.css +1 -0
- package/dist/web/assets/ProjectAgentsPage-WVcNdzVi.js +12 -0
- package/dist/web/assets/ProjectScopePicker-zAgiaZKL.js +1 -0
- package/dist/web/assets/PublicProjectsPage-DKamXDV9.css +1 -0
- package/dist/web/assets/PublicProjectsPage-DL093f65.js +1 -0
- package/dist/web/assets/ReviewPage-D_NIhPx8.js +1 -0
- package/dist/web/assets/RoundtablesPage-Cb0R4GaM.css +1 -0
- package/dist/web/assets/RoundtablesPage-CdVVyZE-.js +1 -0
- package/dist/web/assets/SettingsPage-BmYkLul4.css +1 -0
- package/dist/web/assets/SettingsPage-CbLaau8Q.js +1 -0
- package/dist/web/assets/SharedJefaReportPage-aygd6wvb.js +1 -0
- package/dist/web/assets/UiDialog-Q7fsJUoO.js +1 -0
- package/dist/web/assets/UiDisclosure-BS-muK5j.js +1 -0
- package/dist/web/assets/UiSegmented-CT28bxqw.js +1 -0
- package/dist/web/assets/UiSelect-Bv6R0yr1.css +1 -0
- package/dist/web/assets/UiSelect-DcGpPQQo.js +1 -0
- package/dist/web/assets/WritingTastePage-B_zYD1df.css +1 -0
- package/dist/web/assets/WritingTastePage-CYlwAFqV.js +1 -0
- package/dist/web/assets/agent-profile-BZj3ezWe.css +1 -0
- package/dist/web/assets/{attention-store-Dk-_P4Oi.js → attention-store-D-qIFRIb.js} +1 -1
- package/dist/web/assets/{catalog-DN72452f.js → catalog-UDv854JN.js} +1 -1
- package/dist/web/assets/client-DLS5FZBI.js +1 -0
- package/dist/web/assets/{console-BtPqo79j.js → console-CqzwSo7a.js} +1 -1
- package/dist/web/assets/{conversation-launcher-settings-jkHRmWai.js → conversation-launcher-settings-DRoIz42V.js} +1 -1
- package/dist/web/assets/{i18n-D2CJkEYK.js → i18n-b14l2pQA.js} +3 -3
- package/dist/web/assets/{identity-B3zEw-1u.js → identity-BWbPmZZf.js} +1 -1
- package/dist/web/assets/index-D1OHIz4c.js +5 -0
- package/dist/web/assets/index-kPB0Cf8Q.css +1 -0
- package/dist/web/assets/{model-S7ehAjky.js → model-DupZy8Qb.js} +1 -1
- package/dist/web/assets/{personal-profile-graph--GG3Mizo.js → personal-profile-graph-BPTtMJTZ.js} +1 -1
- package/dist/web/assets/shared-report-BYnLhmCZ.js +1 -0
- package/dist/web/assets/{useAgentRoster-BAPKWZ9y.js → useAgentRoster-ep7SpKv1.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-BtFMXFjw.js → useApi-CROJJdhE-CEQAEuug.js} +1 -1
- package/dist/web/assets/{vue-router-DkaTtPn0.js → vue-router-B6ZjK-Br.js} +1 -1
- package/dist/web/index.html +14 -16
- package/docs/agent-conversations-and-collaboration.md +9 -9
- package/docs/agent-roundtable.md +77 -0
- package/docs/employee-agents.md +2 -2
- package/docs/project-agent-memory.md +1 -1
- package/graph-provider/fuli_graph/agent_conversation_models.py +11 -2
- package/graph-provider/fuli_graph/conversation_context.py +40 -9
- package/graph-provider/fuli_graph/default_project_lead.py +88 -0
- package/graph-provider/fuli_graph/project_agent_context_models.py +5 -1
- package/graph-provider/fuli_graph/project_agent_delegations.py +118 -0
- package/graph-provider/fuli_graph/project_agent_employee_numbers.py +41 -0
- package/graph-provider/fuli_graph/project_agent_memory_routes.py +20 -0
- package/graph-provider/fuli_graph/project_agent_models.py +1 -0
- package/graph-provider/fuli_graph/project_agent_task_models.py +3 -0
- package/graph-provider/fuli_graph/project_agent_team_growth.py +2 -2
- package/graph-provider/fuli_graph/runtime.py +10 -76
- package/graph-provider/fuli_graph/store.py +2 -0
- package/graph-provider/fuli_graph/store_agent_conversations.py +60 -8
- package/graph-provider/fuli_graph/store_project_agent_context.py +52 -4
- package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +16 -1
- package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +3 -1
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +3 -2
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +12 -8
- package/graph-provider/fuli_graph/store_project_agents.py +56 -129
- package/graph-provider/fuli_graph/store_task_contexts.py +26 -0
- package/graph-provider/fuli_graph/system_agents.py +130 -0
- package/graph-provider/fuli_graph/system_hr_identity.py +1 -1
- package/graph-provider/fuli_graph/task_context_routes.py +10 -0
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -2
- package/src/agent-roundtable/http.js +14 -0
- package/src/agent-roundtable/service.js +247 -0
- package/src/agent-roundtable/store.js +86 -0
- package/src/agent-roundtable/tools.js +61 -0
- package/src/agent-roundtable/wake.js +100 -0
- package/src/agent-tools/agent-conversation-definitions.js +3 -3
- package/src/agent-tools/graph-definitions.js +3 -2
- package/src/agent-tools/interface-catalog.js +0 -5
- package/src/agent-tools.js +2 -2
- package/src/agents/claude-code/wake.js +37 -0
- package/src/agents/codex/wake.js +47 -0
- package/src/agents/identity-receipt.js +35 -7
- package/src/agents/session-log.js +34 -0
- package/src/agents/wake-registry.js +14 -0
- package/src/cli/command-registry.js +0 -2
- package/src/cli/setup-command.js +2 -5
- package/src/cli/update-command.js +13 -2
- package/src/cli.js +0 -5
- package/src/employees/management-policy.js +4 -2
- package/src/employees/manifest.js +3 -0
- package/src/employees/recruitment.js +3 -2
- package/src/graphiti/agent-conversations.js +11 -9
- package/src/graphiti/agent-knowledge-workflows.js +34 -7
- package/src/graphiti/federated-application.js +17 -5
- package/src/graphiti/project-agent-mapping.js +2 -0
- package/src/graphiti/project-agent-task-entry.js +38 -8
- package/src/graphiti/project-agent-team-context.js +4 -1
- package/src/graphiti/project-path-context.js +7 -2
- package/src/graphiti/provider-client.js +17 -0
- package/src/graphiti/repository-project-registration.js +17 -0
- package/src/graphiti/task-entry-preferences.js +17 -4
- package/src/graphiti/task-entry-project-scope.js +67 -0
- package/src/http/api-router.js +5 -3
- package/src/http/error-mapping.js +0 -2
- package/src/mcp/instructions.js +5 -5
- package/src/mcp/session-id.js +1 -1
- package/src/mcp/tool-annotations.js +3 -4
- package/src/setup/agents.js +1 -41
- package/src/setup/graph-runtime.js +15 -2
- package/src/setup/session-skill.js +22 -2
- package/dist/web/assets/AboutPage-D-GyJNv1.js +0 -1
- package/dist/web/assets/AgentConversations-DOuZgUeA.js +0 -2
- package/dist/web/assets/AgentConversations-DS8h3yKn.css +0 -1
- package/dist/web/assets/AgentHand-eGsXoGSq.js +0 -1
- package/dist/web/assets/AgentName-Dtmu9Ff5.js +0 -1
- package/dist/web/assets/AgentProfilePage-D8Tvayn7.js +0 -1
- package/dist/web/assets/AgentsDirectoryPage-BTEfKZjF.js +0 -1
- package/dist/web/assets/ConnectionsPage-C1w99yN3.css +0 -1
- package/dist/web/assets/ConnectionsPage-DwOIcvl9.js +0 -1
- package/dist/web/assets/EmployeeRecruitDialog-BrIMRblq.css +0 -1
- package/dist/web/assets/EmployeeRecruitDialog-D11blfwU.js +0 -1
- package/dist/web/assets/EmployeeWorkbenchPage-BcEzYt65.css +0 -1
- package/dist/web/assets/EmployeeWorkbenchPage-CK2BAoGt.js +0 -3
- package/dist/web/assets/GrowthLoading-KTjE2e50.js +0 -1
- package/dist/web/assets/KnowledgeEditDialog-B3x8vs3c.css +0 -1
- package/dist/web/assets/KnowledgeEditDialog-n9trHQ5d.js +0 -1
- package/dist/web/assets/KnowledgeInspector-BcNjgw9H.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-BzgxP-nG.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-DMBpGxLr.js +0 -1
- package/dist/web/assets/KnowledgePage-POkCMW8f.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-2FvQadhB.js +0 -4
- package/dist/web/assets/KnowledgeWorkspace-sb8OJ97I.css +0 -1
- package/dist/web/assets/NotFoundPage-BCMZbeB8.js +0 -1
- package/dist/web/assets/OverviewPage-0CFWCG9-.css +0 -1
- package/dist/web/assets/OverviewPage-0mYifpjp.js +0 -1
- package/dist/web/assets/PersonalProfilePage-Chv2a2pi.css +0 -1
- package/dist/web/assets/PersonalProfilePage-JW6Muix6.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-CRbi64JP.js +0 -1
- package/dist/web/assets/ProjectAgentFirstTask-6afJ8HUe.css +0 -1
- package/dist/web/assets/ProjectAgentFirstTask-DJ0wTbh8.js +0 -1
- package/dist/web/assets/ProjectAgentsPage-CPgKVLgg.js +0 -12
- package/dist/web/assets/ProjectAgentsPage-DeuRkMp1.css +0 -1
- package/dist/web/assets/ProjectScopePicker-BwB7gK-7.js +0 -1
- package/dist/web/assets/PublicProjectsPage-CB272Sq-.js +0 -1
- package/dist/web/assets/ReviewPage-CelTZU4C.js +0 -1
- package/dist/web/assets/RoundtablesPage-C9dXfWCl.js +0 -2
- package/dist/web/assets/RoundtablesPage-DCbIBnBq.css +0 -1
- package/dist/web/assets/SearchableSelect-f7EHbyDL.js +0 -1
- package/dist/web/assets/SettingsPage-5g7mRN2F.css +0 -1
- package/dist/web/assets/SettingsPage-nAijmm5f.js +0 -1
- package/dist/web/assets/SharedJefaReportPage-CjpJ9bkk.js +0 -1
- package/dist/web/assets/UiDisclosure-CWlC91Zt.js +0 -1
- package/dist/web/assets/WritingTastePage-DaHWY61P.js +0 -1
- package/dist/web/assets/WritingTastePage-SMGF39mf.css +0 -1
- package/dist/web/assets/agent-profile-CBvJHGjt.css +0 -1
- package/dist/web/assets/client-B2cRl-7c.js +0 -1
- package/dist/web/assets/copy-D2LqwtPZ.js +0 -1
- package/dist/web/assets/index-CNp0tRNp.css +0 -1
- package/dist/web/assets/index-ulISYH02.js +0 -5
- package/dist/web/assets/shared-report-DEK9wvhl.js +0 -1
- package/dist/web/assets/useModalDialog-D8tiJfbc.js +0 -1
- package/docs/roundtable.md +0 -46
- package/src/agents/a2a/roundtable-participant.js +0 -374
- package/src/agents/claude-code/roundtable-participant.js +0 -24
- package/src/agents/codex/roundtable-config.js +0 -32
- package/src/agents/codex/roundtable-participant.js +0 -34
- package/src/agents/grok/roundtable-participant.js +0 -185
- package/src/agents/pi/roundtable-participant.js +0 -193
- package/src/agents/pi/roundtable-result-format.js +0 -183
- package/src/cli/roundtable-command.js +0 -79
- package/src/roundtables/application.js +0 -69
- package/src/roundtables/domain.js +0 -181
- package/src/roundtables/http-router.js +0 -58
- package/src/roundtables/mcp-server.js +0 -55
- package/src/roundtables/participant-registry.js +0 -12
- package/src/roundtables/process.js +0 -70
- package/src/roundtables/profiles.js +0 -84
- package/src/roundtables/result-contract.js +0 -47
- package/src/roundtables/server.js +0 -52
- package/src/roundtables/service.js +0 -480
- package/src/roundtables/store.js +0 -63
- package/src/roundtables/tool-contract.js +0 -58
- package/src/roundtables/turn-prompt.js +0 -24
- package/src/roundtables/worker.js +0 -85
- /package/dist/web/assets/{GrowthLoading-CIuQ1icy.css → client-CIuQ1icy.css} +0 -0
package/dist/web/index.html
CHANGED
|
@@ -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-
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/i18n-
|
|
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/
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
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/
|
|
22
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
23
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
24
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
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/
|
|
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-
|
|
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
|
-
|
|
|
11
|
-
| 自动恢复预算 |
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
package/docs/employee-agents.md
CHANGED
|
@@ -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.
|
|
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**.
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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;
|
|
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'][:
|
|
50
|
+
result['summary'] = result['summary'][64:]
|
|
20
51
|
result['truncated'] = True
|
|
21
52
|
for event in reversed(events):
|
|
22
|
-
if
|
|
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
|
-
"""
|
|
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 *,
|
|
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,
|