fuli-context 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -58
- package/README.zh-CN.md +24 -54
- package/dist/web/assets/AboutPage-CKq4IRHd.js +1 -0
- package/dist/web/assets/AgentConversations-DfXiFvBN.js +2 -0
- package/dist/web/assets/AgentConversations-XqNEBY_2.css +1 -0
- package/dist/web/assets/AgentHand-BBM-sKh2.js +1 -0
- package/dist/web/assets/AgentName-C_6R80nP.js +1 -0
- package/dist/web/assets/AgentProfilePage-Cz-i1fRD.js +1 -0
- package/dist/web/assets/AgentsDirectoryPage-BI0EFz6y.js +1 -0
- package/dist/web/assets/AgentsDirectoryPage-yRWktiin.css +1 -0
- package/dist/web/assets/ConnectionsPage-BoC6kXgs.js +1 -0
- package/dist/web/assets/ConnectionsPage-CM9C-zEI.css +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-BVyclQAJ.js +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-CVbqCzHb.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-D-c_Jm2z.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-D2GvYraI.js +3 -0
- package/dist/web/assets/KnowledgeEditDialog-a0AKWics.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-hT9znkoe.css +1 -0
- package/dist/web/assets/KnowledgeInspector-DMg9yJZ2.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-BgsHM3Qw.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-DeL7Jp34.css +1 -0
- package/dist/web/assets/KnowledgePage-kn8MqUCO.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-BtRq9fvC.js +4 -0
- package/dist/web/assets/KnowledgeWorkspace-D4a7FcAb.css +1 -0
- package/dist/web/assets/NotFoundPage-D-7jLz3o.js +1 -0
- package/dist/web/assets/OverviewPage-BHPc-XxY.js +1 -0
- package/dist/web/assets/OverviewPage-By-fUzSG.css +1 -0
- package/dist/web/assets/PersonalProfilePage-C00iV0Kx.js +1 -0
- package/dist/web/assets/PersonalProfilePage-DrMDAAdy.css +1 -0
- package/dist/web/assets/PersonalProjectsPage-CFNkdHSo.js +1 -0
- package/dist/web/assets/ProjectAgentFirstTask-Bi1ZsTC1.css +1 -0
- package/dist/web/assets/ProjectAgentFirstTask-CrCWEYgn.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-BzztLMH-.js +12 -0
- package/dist/web/assets/ProjectAgentsPage-DNLNjYp-.css +1 -0
- package/dist/web/assets/ProjectScopePicker-CMxn3txv.js +1 -0
- package/dist/web/assets/PublicProjectsPage-DKamXDV9.css +1 -0
- package/dist/web/assets/PublicProjectsPage-DQVtvhOS.js +1 -0
- package/dist/web/assets/ReviewPage-BZhvyDWc.js +1 -0
- package/dist/web/assets/RoundtablesPage-CVebTXQa.js +1 -0
- package/dist/web/assets/RoundtablesPage-Cb0R4GaM.css +1 -0
- package/dist/web/assets/SettingsPage-1knvfOW6.js +1 -0
- package/dist/web/assets/SettingsPage-BmYkLul4.css +1 -0
- package/dist/web/assets/SharedJefaReportPage-BCVq8u0P.js +1 -0
- package/dist/web/assets/UiDialog-Da1P10f4.js +1 -0
- package/dist/web/assets/UiDisclosure-8wE8YuE4.js +1 -0
- package/dist/web/assets/UiSegmented-BKZXc8QF.js +1 -0
- package/dist/web/assets/UiSelect-Bv6R0yr1.css +1 -0
- package/dist/web/assets/UiSelect-D5Xj5Ec4.js +1 -0
- package/dist/web/assets/WritingTastePage-B8-etOBn.js +1 -0
- package/dist/web/assets/WritingTastePage-B_zYD1df.css +1 -0
- package/dist/web/assets/agent-profile-BZj3ezWe.css +1 -0
- package/dist/web/assets/{attention-store-Dk-_P4Oi.js → attention-store-BfxtL9EV.js} +1 -1
- package/dist/web/assets/{catalog-DN72452f.js → catalog-6SN9i3WR.js} +1 -1
- package/dist/web/assets/client-CUTkF3t4.js +1 -0
- package/dist/web/assets/{console-BtPqo79j.js → console-CG8tdtSs.js} +1 -1
- package/dist/web/assets/{conversation-launcher-settings-jkHRmWai.js → conversation-launcher-settings-CyZkLJg7.js} +1 -1
- package/dist/web/assets/{i18n-D2CJkEYK.js → i18n-CCMR4gDS.js} +3 -3
- package/dist/web/assets/{identity-B3zEw-1u.js → identity-C1eN-FSB.js} +1 -1
- package/dist/web/assets/index-kPB0Cf8Q.css +1 -0
- package/dist/web/assets/index-sLDKI6R-.js +5 -0
- package/dist/web/assets/{model-S7ehAjky.js → model-l-sAqtW_.js} +1 -1
- package/dist/web/assets/{personal-profile-graph--GG3Mizo.js → personal-profile-graph-BD1Y6TDS.js} +1 -1
- package/dist/web/assets/shared-report-B46dxlCR.js +1 -0
- package/dist/web/assets/{useAgentRoster-BAPKWZ9y.js → useAgentRoster-DdxoKDEE.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-BtFMXFjw.js → useApi-CROJJdhE-DlOiu8lP.js} +1 -1
- package/dist/web/assets/{vue-router-DkaTtPn0.js → vue-router-DjqEU4W2.js} +1 -1
- package/dist/web/index.html +14 -16
- package/docs/agent-conversations-and-collaboration.md +9 -9
- package/docs/agent-roundtable.md +61 -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_memory_routes.py +7 -0
- package/graph-provider/fuli_graph/project_agent_task_models.py +3 -0
- package/graph-provider/fuli_graph/runtime.py +3 -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_task_recruitment.py +3 -1
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +12 -8
- package/graph-provider/fuli_graph/store_project_agents.py +25 -124
- package/graph-provider/fuli_graph/store_task_contexts.py +26 -0
- package/graph-provider/fuli_graph/system_agents.py +128 -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 -4
- package/skills/capturing-session-knowledge/SKILL.md +1 -1
- package/src/agent-roundtable/http.js +14 -0
- package/src/agent-roundtable/service.js +218 -0
- package/src/agent-roundtable/store.js +86 -0
- package/src/agent-roundtable/tools.js +61 -0
- package/src/agent-roundtable/wake.js +99 -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/lifecycle-hook.js +6 -19
- package/src/agents/claude-code/wake.js +37 -0
- package/src/agents/codex/lifecycle-hook.js +51 -27
- package/src/agents/codex/token-usage.js +235 -0
- package/src/agents/codex/wake.js +47 -0
- package/src/agents/cursor/lifecycle-hook.js +3 -9
- package/src/agents/identity-receipt.js +16 -6
- package/src/agents/session-log.js +34 -0
- package/src/agents/token-usage-resolver.js +147 -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 +18 -6
- package/src/graphiti/project-agent-mapping.js +1 -1
- package/src/graphiti/project-agent-task-entry.js +19 -7
- package/src/graphiti/project-agent-workflows.js +6 -1
- package/src/graphiti/project-path-context.js +7 -2
- package/src/graphiti/provider-client.js +8 -0
- package/src/graphiti/repository-project-registration.js +17 -0
- package/src/graphiti/task-entry-preferences.js +5 -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 -2
- 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-BgQLIpxg.css +0 -1
- package/dist/web/assets/ProjectAgentsPage-CQemHFxy.js +0 -12
- 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-ByVeZlgC.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-UygXwZDZ.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-beta-testing.md +0 -102
- package/docs/roundtable-beta-validation.md +0 -133
- package/docs/roundtable-research.md +0 -142
- 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 -56
- package/src/roundtables/mcp-server.js +0 -48
- package/src/roundtables/participant-registry.js +0 -12
- package/src/roundtables/process.js +0 -70
- package/src/roundtables/result-contract.js +0 -47
- package/src/roundtables/server.js +0 -52
- package/src/roundtables/service.js +0 -396
- package/src/roundtables/store.js +0 -59
- package/src/roundtables/tool-contract.js +0 -41
- 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-sLDKI6R-.js"></script>
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/i18n-CCMR4gDS.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-CUTkF3t4.js">
|
|
16
|
+
<link rel="modulepreload" crossorigin href="/assets/console-CG8tdtSs.js">
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/assets/UiDialog-Da1P10f4.js">
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/assets/useApi-CROJJdhE-DlOiu8lP.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/assets/vue-router-DjqEU4W2.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-D5Xj5Ec4.js">
|
|
22
|
+
<link rel="modulepreload" crossorigin href="/assets/attention-store-BfxtL9EV.js">
|
|
23
|
+
<link rel="modulepreload" crossorigin href="/assets/AgentHand-BBM-sKh2.js">
|
|
24
|
+
<link rel="modulepreload" crossorigin href="/assets/UiDisclosure-8wE8YuE4.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,61 @@
|
|
|
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 or exact name), `body`, the caller's
|
|
22
|
+
`taskContextToken`, `projectPath` (the current directory), and optionally `threadId`
|
|
23
|
+
to continue a thread or `conversation` to choose which of the recipient's
|
|
24
|
+
conversations answers (`auto` by default, `new`, or a session ID from `find_agents`).
|
|
25
|
+
|
|
26
|
+
## Delivery
|
|
27
|
+
|
|
28
|
+
1. **Wake.** By default Fuli wakes the recipient in its own client, headlessly and
|
|
29
|
+
read-only, and waits for its final answer (default 5 minutes, at most 15):
|
|
30
|
+
- Claude Code resumes the conversation with `--resume <id> --fork-session`, so the
|
|
31
|
+
answer has that conversation's full context while the original stays untouched.
|
|
32
|
+
- Codex runs `codex exec resume <id>` with a read-only sandbox. The question and
|
|
33
|
+
answer are recorded in that conversation, so it shows up there next time it is
|
|
34
|
+
opened. Codex refuses while the conversation is open in the app; Fuli then asks
|
|
35
|
+
the same Agent in a new session instead.
|
|
36
|
+
- Without a resumable conversation, Fuli starts a new session in the caller's
|
|
37
|
+
project directory with `@{agent}`, which loads that Agent's memory and role.
|
|
38
|
+
- If that client is not installed or not logged in on this machine, Fuli tries the
|
|
39
|
+
Agent's next allowed client. A timeout stops there instead of starting another run.
|
|
40
|
+
2. **Inbox.** If no allowed client can answer (none installed or logged in, or the
|
|
41
|
+
wake timed out), the message waits. The next time that Agent starts a task,
|
|
42
|
+
task entry includes it under `agent_messages`, and the Agent answers with
|
|
43
|
+
`reply_agent_message`.
|
|
44
|
+
|
|
45
|
+
A thread holds at most 24 messages and at most 3 asks may be waiting on each other
|
|
46
|
+
inside one thread, so Agents cannot loop forever. Woken Agents run read-only.
|
|
47
|
+
|
|
48
|
+
## What people see
|
|
49
|
+
|
|
50
|
+
The console's **圆桌 / Roundtable** page lists every thread with its participants and
|
|
51
|
+
shows the full timeline: who asked whom, through which client, the exact message and
|
|
52
|
+
answer, and how it was delivered. People watch; they do not need to act.
|
|
53
|
+
|
|
54
|
+
Threads are stored locally in `agent-roundtable.sqlite` in the Fuli data directory.
|
|
55
|
+
|
|
56
|
+
## Clients
|
|
57
|
+
|
|
58
|
+
Waking needs the client's CLI on this machine: `claude` for Claude Code, and `codex`
|
|
59
|
+
(or the CLI bundled with the Codex app) for Codex. Override the paths with
|
|
60
|
+
`FULI_CLAUDE_BIN` / `FULI_CODEX_BIN`. A recipient is only woken in clients its Agent
|
|
61
|
+
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
|
+
)
|
|
@@ -13,6 +13,7 @@ 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
|
|
17
18
|
|
|
18
19
|
|
|
@@ -25,6 +26,12 @@ def register_project_agent_memory_routes(application, store, Actor):
|
|
|
25
26
|
async def resolve_context(request: ProjectAgentContextRequest, actor: Actor):
|
|
26
27
|
return await store.resolve_project_agent_context(actor, request)
|
|
27
28
|
|
|
29
|
+
@application.post(
|
|
30
|
+
'/v1/project-agent-context/default-lead', response_model=ProjectAgentContextResolution,
|
|
31
|
+
)
|
|
32
|
+
async def staff_default_lead(request: DefaultProjectLeadRequest, actor: Actor):
|
|
33
|
+
return await store.staff_default_project_lead(actor, request)
|
|
34
|
+
|
|
28
35
|
@application.get(
|
|
29
36
|
'/v1/project-agents/{agent_id}/memory', response_model=ProjectAgentMemoryView,
|
|
30
37
|
)
|
|
@@ -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',
|
|
@@ -527,83 +527,10 @@ class GraphitiRuntime:
|
|
|
527
527
|
"MATCH (space:FuliSpace {kind: 'personal'}) RETURN space.id AS space_id",
|
|
528
528
|
routing_='r',
|
|
529
529
|
)
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
from .project_agent_models import ProjectAgentProfile
|
|
533
|
-
from .provider_values import now_utc, stable_uuid
|
|
534
|
-
|
|
535
|
-
coordinator_profile = ProjectAgentProfile(
|
|
536
|
-
name='项目协调人',
|
|
537
|
-
responsibility='评估任务、应用用户配置并路由已有 Agent。',
|
|
538
|
-
agent_type='coordinator',
|
|
539
|
-
work_kinds=['task-coordination'],
|
|
540
|
-
capabilities=['任务评估', '策略应用', 'Agent 路由'],
|
|
541
|
-
initial_preferences=['质量与可验收完成优先于成本和时间'],
|
|
542
|
-
status='active',
|
|
543
|
-
)
|
|
544
|
-
hr_profile = ProjectAgentProfile(
|
|
545
|
-
name='Bole',
|
|
546
|
-
occupation_emoji='🔎',
|
|
547
|
-
responsibility=(
|
|
548
|
-
'维护 Agent 人员分布、当前工作与可审计招募记录,'
|
|
549
|
-
'并在需要新角色时执行受策略约束的招募。'
|
|
550
|
-
),
|
|
551
|
-
agent_type='hr',
|
|
552
|
-
work_kinds=['agent-recruitment', 'staffing-review'],
|
|
553
|
-
capabilities=[
|
|
554
|
-
'Agent 招募', '人员分布', '工作状态', '招募审计',
|
|
555
|
-
'fuli.employee:bole',
|
|
556
|
-
],
|
|
557
|
-
initial_preferences=[
|
|
558
|
-
'每次招募保留任务、原因、触发来源与时间线。',
|
|
559
|
-
'只展示 Provider 已记录的人员状态,不推测未上报的工作。',
|
|
560
|
-
],
|
|
561
|
-
status='active',
|
|
562
|
-
)
|
|
563
|
-
updated_at = now_utc()
|
|
530
|
+
from .system_agents import ensure_system_agents
|
|
531
|
+
|
|
564
532
|
for record in records:
|
|
565
|
-
|
|
566
|
-
for agent_id, profile in (
|
|
567
|
-
('fuli-project-coordinator', coordinator_profile),
|
|
568
|
-
('employee.bole', hr_profile),
|
|
569
|
-
):
|
|
570
|
-
node_id = stable_uuid(
|
|
571
|
-
self.settings.provider_id,
|
|
572
|
-
space_id,
|
|
573
|
-
'project-agent',
|
|
574
|
-
agent_id,
|
|
575
|
-
)
|
|
576
|
-
await self.driver.execute_query(
|
|
577
|
-
'''
|
|
578
|
-
MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
|
|
579
|
-
MERGE (agent:FuliProjectAgent {id: $id})
|
|
580
|
-
ON CREATE SET agent.agent_id = $agent_id,
|
|
581
|
-
agent.profile_json = $profile_json,
|
|
582
|
-
agent.name = $name,
|
|
583
|
-
agent.occupation_emoji = $occupation_emoji,
|
|
584
|
-
agent.responsibility = $responsibility,
|
|
585
|
-
agent.capabilities = $capabilities,
|
|
586
|
-
agent.work_kinds = $work_kinds,
|
|
587
|
-
agent.agent_type = $agent_type,
|
|
588
|
-
agent.memory_scope = 'reviewed_agent',
|
|
589
|
-
agent.status = 'active',
|
|
590
|
-
agent.system_managed = true,
|
|
591
|
-
agent.created_at = $updated_at,
|
|
592
|
-
agent.updated_at = $updated_at
|
|
593
|
-
MERGE (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(agent)
|
|
594
|
-
''',
|
|
595
|
-
space_id=space_id,
|
|
596
|
-
id=node_id,
|
|
597
|
-
agent_id=agent_id,
|
|
598
|
-
profile_json=profile.model_dump_json(),
|
|
599
|
-
name=profile.name,
|
|
600
|
-
occupation_emoji=profile.occupation_emoji,
|
|
601
|
-
responsibility=profile.responsibility,
|
|
602
|
-
capabilities=profile.capabilities,
|
|
603
|
-
work_kinds=profile.work_kinds,
|
|
604
|
-
agent_type=profile.agent_type,
|
|
605
|
-
updated_at=updated_at,
|
|
606
|
-
)
|
|
533
|
+
await ensure_system_agents(self.driver, self.settings.provider_id, record['space_id'])
|
|
607
534
|
|
|
608
535
|
async def _migrate_personal_project_relation_review_defaults(self) -> None:
|
|
609
536
|
await self.driver.execute_query(
|
|
@@ -9,6 +9,7 @@ from .store_knowledge import StoreKnowledge
|
|
|
9
9
|
from .store_project_agent_executors import StoreProjectAgentExecutors
|
|
10
10
|
from .store_project_agent_memory import StoreProjectAgentMemory
|
|
11
11
|
from .store_project_agent_context import StoreProjectAgentContext
|
|
12
|
+
from .default_project_lead import StoreDefaultProjectLead
|
|
12
13
|
from .store_project_agents import StoreProjectAgents
|
|
13
14
|
from .store_project_agent_attention import StoreProjectAgentAttention
|
|
14
15
|
from .store_project_agent_tasks import StoreProjectAgentTasks
|
|
@@ -28,6 +29,7 @@ class GraphStore(
|
|
|
28
29
|
StoreProjectAgentAttention,
|
|
29
30
|
StoreProjectAgentMemory,
|
|
30
31
|
StoreProjectAgentContext,
|
|
32
|
+
StoreDefaultProjectLead,
|
|
31
33
|
StoreTaskContexts,
|
|
32
34
|
StoreProjectAgentExecutors,
|
|
33
35
|
StoreProjectAgentTasks,
|
|
@@ -5,7 +5,9 @@ import hashlib
|
|
|
5
5
|
import json
|
|
6
6
|
from fastapi import HTTPException
|
|
7
7
|
from .agent_conversation_models import ConversationPolicy
|
|
8
|
-
from .conversation_context import
|
|
8
|
+
from .conversation_context import (
|
|
9
|
+
add_task_result, fold_messages, message_bytes, pack_context, render_digest,
|
|
10
|
+
)
|
|
9
11
|
from .project_agent_access import authorize_project_agent
|
|
10
12
|
from .provider_values import now_utc, stable_uuid
|
|
11
13
|
from .store_transactions import query_store_transaction
|
|
@@ -77,13 +79,13 @@ class StoreAgentConversations:
|
|
|
77
79
|
raise HTTPException(404, 'Conversation not found in this Agent and project')
|
|
78
80
|
record = dict(rows[0]['c'])
|
|
79
81
|
view = self._conversation_view(record, policy)
|
|
80
|
-
if request.mode == 'context' and view['archived']:
|
|
81
|
-
return {**view, 'context': pack_context(record.get('summary', ''), [], policy.context_budget)}
|
|
82
82
|
order = 'DESC' if request.mode == 'context' else 'ASC'
|
|
83
|
+
# Recovery starts after the folded range; reading history still sees everything.
|
|
84
|
+
after = max(request.after, record.get('compacted_through') or 0) if request.mode == 'context' else request.after
|
|
83
85
|
events, _, _ = await self.runtime.driver.execute_query(
|
|
84
86
|
'MATCH (c:FuliConversation {id: $id})-[:HAS_EVENT]->(e:FuliConversationEvent) '
|
|
85
87
|
'WHERE e.sequence > $after RETURN e.payload AS payload, e.sequence AS sequence '
|
|
86
|
-
f'ORDER BY e.sequence {order} LIMIT $limit', id=record['id'], after=
|
|
88
|
+
f'ORDER BY e.sequence {order} LIMIT $limit', id=record['id'], after=after,
|
|
87
89
|
limit=20 if request.mode == 'context' else request.limit)
|
|
88
90
|
decoded = []
|
|
89
91
|
total = 0
|
|
@@ -94,15 +96,52 @@ class StoreAgentConversations:
|
|
|
94
96
|
break
|
|
95
97
|
decoded.append(value)
|
|
96
98
|
if request.mode == 'context':
|
|
97
|
-
return {**view, 'context': pack_context(
|
|
99
|
+
return {**view, 'context': pack_context(self._digest_text(record), list(reversed(decoded)), policy.context_budget)}
|
|
98
100
|
cursor = decoded[-1]['sequence'] if decoded else request.after
|
|
99
101
|
return {**view, 'events': decoded, 'next_cursor': cursor, 'has_more': cursor < record['revision']}
|
|
100
102
|
|
|
101
103
|
def _conversation_view(self, record, policy):
|
|
102
104
|
return {key: record.get(key) for key in ('id', 'summary', 'status', 'revision', 'last_activity')} | {
|
|
103
|
-
'
|
|
105
|
+
'compacted_through': record.get('compacted_through') or 0,
|
|
106
|
+
'unfolded_bytes': record.get('unfolded_bytes') or 0,
|
|
104
107
|
'policy': policy.model_dump(), 'raw_retained': True}
|
|
105
108
|
|
|
109
|
+
@staticmethod
|
|
110
|
+
def _digest_text(record):
|
|
111
|
+
if record.get('digest'):
|
|
112
|
+
return render_digest(json.loads(record['digest']))
|
|
113
|
+
return record.get('summary') or ''
|
|
114
|
+
|
|
115
|
+
async def _compact_conversation(self, conversation_id, policy):
|
|
116
|
+
"""Fold older messages into the digest, keeping recent ones verbatim."""
|
|
117
|
+
rows, _, _ = await self.runtime.driver.execute_query(
|
|
118
|
+
'MATCH (c:FuliConversation {id: $id}) RETURN c', id=conversation_id)
|
|
119
|
+
record = dict(rows[0]['c'])
|
|
120
|
+
threshold = policy.compact_after_kb * 1024
|
|
121
|
+
if (record.get('unfolded_bytes') or 0) <= threshold:
|
|
122
|
+
return
|
|
123
|
+
events, _, _ = await self.runtime.driver.execute_query(
|
|
124
|
+
'MATCH (c:FuliConversation {id: $id})-[:HAS_EVENT]->(e:FuliConversationEvent) '
|
|
125
|
+
'WHERE e.sequence > $after RETURN e.payload AS payload, e.sequence AS sequence '
|
|
126
|
+
'ORDER BY e.sequence', id=conversation_id, after=record.get('compacted_through') or 0)
|
|
127
|
+
decoded = [decode_event(event) for event in events]
|
|
128
|
+
if not decoded:
|
|
129
|
+
return
|
|
130
|
+
# The newest event always stays verbatim, then as much as fits a quarter.
|
|
131
|
+
keep_bytes, cut = threshold // 4, len(decoded) - 1
|
|
132
|
+
kept = message_bytes(decoded[cut])
|
|
133
|
+
while cut and kept + message_bytes(decoded[cut - 1]) <= keep_bytes:
|
|
134
|
+
cut -= 1
|
|
135
|
+
kept += message_bytes(decoded[cut])
|
|
136
|
+
if not cut:
|
|
137
|
+
return
|
|
138
|
+
digest = json.loads(record['digest']) if record.get('digest') else {}
|
|
139
|
+
await self.runtime.driver.execute_query(
|
|
140
|
+
'MATCH (c:FuliConversation {id: $id}) '
|
|
141
|
+
'SET c.digest=$digest, c.compacted_through=$through, c.unfolded_bytes=$kept',
|
|
142
|
+
id=conversation_id, digest=json.dumps(fold_messages(digest, decoded[:cut]), ensure_ascii=False),
|
|
143
|
+
through=decoded[cut - 1]['sequence'], kept=kept)
|
|
144
|
+
|
|
106
145
|
async def append_conversation(self, actor, request):
|
|
107
146
|
if request.initialize_cursor:
|
|
108
147
|
raise HTTPException(422, 'Initialize transcripts only at a verified task boundary')
|
|
@@ -137,6 +176,7 @@ class StoreAgentConversations:
|
|
|
137
176
|
"c.last_activity=$now, c.summary='', c.status='reported' "
|
|
138
177
|
'SET c.serial=coalesce(c.serial,0)+1', id=conversation_id, scope=scope, now=now)
|
|
139
178
|
new_summary = False
|
|
179
|
+
unfolded = 0
|
|
140
180
|
for event in request.events:
|
|
141
181
|
digest, payload = encode_event(event)
|
|
142
182
|
event_id = stable_uuid(conversation_id, request.source_application, request.session_id, event.event_id)
|
|
@@ -148,6 +188,7 @@ class StoreAgentConversations:
|
|
|
148
188
|
continue
|
|
149
189
|
if event.kind == 'checkpoint':
|
|
150
190
|
new_summary = True
|
|
191
|
+
unfolded += message_bytes(event.model_dump())
|
|
151
192
|
await store.runtime.driver.execute_query(
|
|
152
193
|
'MATCH (c:FuliConversation {id: $id}) SET c.revision=c.revision+1, c.last_activity=$now '
|
|
153
194
|
'CREATE (e:FuliConversationEvent {id: $event_id, digest: $digest, payload: $payload, '
|
|
@@ -155,10 +196,21 @@ class StoreAgentConversations:
|
|
|
155
196
|
'CREATE (c)-[:HAS_EVENT]->(e)', id=conversation_id, event_id=event_id,
|
|
156
197
|
digest=digest, payload=payload, now=now, source=request.source_application,
|
|
157
198
|
task=request.task_context_token)
|
|
199
|
+
if unfolded:
|
|
200
|
+
await store.runtime.driver.execute_query(
|
|
201
|
+
'MATCH (c:FuliConversation {id: $id}) '
|
|
202
|
+
'SET c.unfolded_bytes=coalesce(c.unfolded_bytes,0)+$bytes',
|
|
203
|
+
id=conversation_id, bytes=unfolded)
|
|
158
204
|
if request.summary is not None and new_summary:
|
|
205
|
+
rows, _, _ = await store.runtime.driver.execute_query(
|
|
206
|
+
'MATCH (c:FuliConversation {id: $id}) RETURN c.digest AS digest', id=conversation_id)
|
|
207
|
+
digest = json.loads(rows[0]['digest']) if rows[0]['digest'] else {}
|
|
208
|
+
digest = add_task_result(digest, now, request.status or 'reported', request.summary)
|
|
159
209
|
await store.runtime.driver.execute_query(
|
|
160
|
-
'MATCH (c:FuliConversation {id: $id}) SET c.summary=$summary, c.status=$status',
|
|
161
|
-
id=conversation_id, summary=request.summary, status=request.status or 'reported'
|
|
210
|
+
'MATCH (c:FuliConversation {id: $id}) SET c.summary=$summary, c.status=$status, c.digest=$digest',
|
|
211
|
+
id=conversation_id, summary=request.summary, status=request.status or 'reported',
|
|
212
|
+
digest=json.dumps(digest, ensure_ascii=False))
|
|
213
|
+
await store._compact_conversation(conversation_id, policy)
|
|
162
214
|
if request.cursor is not None:
|
|
163
215
|
await store.runtime.driver.execute_query(
|
|
164
216
|
'MATCH (s:FuliTranscriptCursor {id: $id}) SET s.cursor=$cursor, s.owner_scope=$scope', id=stream, cursor=request.cursor, scope=scope)
|
|
@@ -761,9 +761,11 @@ class StoreProjectAgentTaskRecruitment:
|
|
|
761
761
|
request.personal_space_id,
|
|
762
762
|
request.personal_project_id,
|
|
763
763
|
)
|
|
764
|
+
first_lead = (request.staffing_intent == 'default_lead' and participant_role == 'lead'
|
|
765
|
+
and coordination_policy.team_lead_agent_id is None)
|
|
764
766
|
confirmation_mode = (
|
|
765
767
|
'require_confirmation'
|
|
766
|
-
if coordination_policy.ask_before_recruitment
|
|
768
|
+
if coordination_policy.ask_before_recruitment and not first_lead
|
|
767
769
|
else 'automatic'
|
|
768
770
|
)
|
|
769
771
|
if not hr_agent_id:
|