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
|
@@ -226,6 +226,10 @@ class GraphitiRuntime:
|
|
|
226
226
|
'CREATE CONSTRAINT fuli_workflow_review_preview_token IF NOT EXISTS '
|
|
227
227
|
'FOR (n:FuliWorkflowReviewPreview) REQUIRE n.token_hash IS UNIQUE'
|
|
228
228
|
)
|
|
229
|
+
await self.driver.execute_query(
|
|
230
|
+
'CREATE CONSTRAINT fuli_agent_delegation_token IF NOT EXISTS '
|
|
231
|
+
'FOR (n:FuliAgentDelegation) REQUIRE n.token_hash IS UNIQUE'
|
|
232
|
+
)
|
|
229
233
|
await self.driver.execute_query(
|
|
230
234
|
'CREATE CONSTRAINT fuli_workflow_rule_id IF NOT EXISTS '
|
|
231
235
|
'FOR (n:FuliWorkflowRule) REQUIRE n.id IS UNIQUE'
|
|
@@ -285,6 +289,9 @@ class GraphitiRuntime:
|
|
|
285
289
|
'FOR (n:FuliConversationPolicy) REQUIRE n.id IS UNIQUE',
|
|
286
290
|
'CREATE CONSTRAINT fuli_project_agent_id IF NOT EXISTS '
|
|
287
291
|
'FOR (n:FuliProjectAgent) REQUIRE n.id IS UNIQUE',
|
|
292
|
+
'CREATE CONSTRAINT fuli_project_agent_employee_number IF NOT EXISTS '
|
|
293
|
+
'FOR (n:FuliProjectAgent) '
|
|
294
|
+
'REQUIRE (n.employee_number_space_id, n.employee_number) IS UNIQUE',
|
|
288
295
|
'CREATE CONSTRAINT fuli_agent_attention_id IF NOT EXISTS '
|
|
289
296
|
'FOR (n:FuliAgentAttention) REQUIRE n.id IS UNIQUE',
|
|
290
297
|
'CREATE CONSTRAINT fuli_project_agent_assignment_id IF NOT EXISTS '
|
|
@@ -527,83 +534,10 @@ class GraphitiRuntime:
|
|
|
527
534
|
"MATCH (space:FuliSpace {kind: 'personal'}) RETURN space.id AS space_id",
|
|
528
535
|
routing_='r',
|
|
529
536
|
)
|
|
530
|
-
|
|
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()
|
|
537
|
+
from .system_agents import ensure_system_agents
|
|
538
|
+
|
|
564
539
|
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
|
-
)
|
|
540
|
+
await ensure_system_agents(self.driver, self.settings.provider_id, record['space_id'])
|
|
607
541
|
|
|
608
542
|
async def _migrate_personal_project_relation_review_defaults(self) -> None:
|
|
609
543
|
await self.driver.execute_query(
|
|
@@ -9,6 +9,7 @@ from .store_knowledge import StoreKnowledge
|
|
|
9
9
|
from .store_project_agent_executors import StoreProjectAgentExecutors
|
|
10
10
|
from .store_project_agent_memory import StoreProjectAgentMemory
|
|
11
11
|
from .store_project_agent_context import StoreProjectAgentContext
|
|
12
|
+
from .default_project_lead import StoreDefaultProjectLead
|
|
12
13
|
from .store_project_agents import StoreProjectAgents
|
|
13
14
|
from .store_project_agent_attention import StoreProjectAgentAttention
|
|
14
15
|
from .store_project_agent_tasks import StoreProjectAgentTasks
|
|
@@ -28,6 +29,7 @@ class GraphStore(
|
|
|
28
29
|
StoreProjectAgentAttention,
|
|
29
30
|
StoreProjectAgentMemory,
|
|
30
31
|
StoreProjectAgentContext,
|
|
32
|
+
StoreDefaultProjectLead,
|
|
31
33
|
StoreTaskContexts,
|
|
32
34
|
StoreProjectAgentExecutors,
|
|
33
35
|
StoreProjectAgentTasks,
|
|
@@ -5,7 +5,9 @@ import hashlib
|
|
|
5
5
|
import json
|
|
6
6
|
from fastapi import HTTPException
|
|
7
7
|
from .agent_conversation_models import ConversationPolicy
|
|
8
|
-
from .conversation_context import
|
|
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)
|
|
@@ -4,16 +4,21 @@ from .personal_project_access import authorize_personal_project
|
|
|
4
4
|
from .project_agent_context_models import ProjectAgentContextResolution
|
|
5
5
|
from .project_agent_task_models import ProjectAgentTaskSubmit
|
|
6
6
|
from .task_context_temporary_owner import temporary_task_owner
|
|
7
|
+
from .project_agent_delegations import StoreAgentDelegations
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
class StoreProjectAgentContext:
|
|
10
|
+
class StoreProjectAgentContext(StoreAgentDelegations):
|
|
10
11
|
async def resolve_project_agent_context(self, actor, request):
|
|
11
12
|
self._require_personal()
|
|
12
13
|
space = await self.authorize(actor, request.personal_space_id, 'reader')
|
|
13
14
|
await authorize_personal_project(self, actor, space, request.personal_project_id)
|
|
14
|
-
temporary_owner = await temporary_task_owner(self, actor, request)
|
|
15
|
+
temporary_owner = await temporary_task_owner(self, actor, request) if not request.delegation_token else None
|
|
15
16
|
if temporary_owner is not None:
|
|
16
17
|
return temporary_owner
|
|
18
|
+
policy = await self.get_project_agent_coordination_policy(
|
|
19
|
+
actor, request.personal_space_id, request.personal_project_id,
|
|
20
|
+
)
|
|
21
|
+
project_lead = getattr(policy, 'team_lead_agent_id', None)
|
|
17
22
|
owner = None
|
|
18
23
|
if request.session_id and not request.agent_id:
|
|
19
24
|
rows, _, _ = await self.runtime.driver.execute_query(
|
|
@@ -39,7 +44,8 @@ class StoreProjectAgentContext:
|
|
|
39
44
|
idempotency_key='read-only-task-context',
|
|
40
45
|
title='Resolve task context', objective='Select one existing durable role.',
|
|
41
46
|
work_kind=request.work_kind, required_capabilities=request.required_capabilities,
|
|
42
|
-
lead_agent_id=request.agent_id or owner
|
|
47
|
+
lead_agent_id=request.agent_id or (project_lead if owner else None),
|
|
48
|
+
source_application=request.source_application,
|
|
43
49
|
routing_reason='Read-only task-entry context recovery',
|
|
44
50
|
)
|
|
45
51
|
if owner and not request.agent_id:
|
|
@@ -71,16 +77,58 @@ class StoreProjectAgentContext:
|
|
|
71
77
|
'using project_context for continuity only',
|
|
72
78
|
*basis,
|
|
73
79
|
]
|
|
74
|
-
|
|
80
|
+
requested_agent_id = None
|
|
81
|
+
reporting_lead_agent_id = None
|
|
82
|
+
peer_request = bool(request.agent_id and selected and self._is_implicit_peer(selected[0]))
|
|
83
|
+
delegated = bool(request.delegation_token)
|
|
84
|
+
if delegated and (not selected or selected[0]['agent_id'] != request.agent_id):
|
|
85
|
+
return ProjectAgentContextResolution(status='agent_unavailable', reason='delegation_invalid')
|
|
86
|
+
if not peer_request:
|
|
87
|
+
if not project_lead:
|
|
88
|
+
if delegated:
|
|
89
|
+
return ProjectAgentContextResolution(status='agent_unavailable', reason='delegation_invalid')
|
|
90
|
+
return ProjectAgentContextResolution(
|
|
91
|
+
status='unassigned', reason='project_lead_required',
|
|
92
|
+
requested_agent_id=request.agent_id,
|
|
93
|
+
match_basis=['Every project requires one accountable lead before work begins.'],
|
|
94
|
+
)
|
|
95
|
+
if selected and selected[0]['agent_id'] != project_lead:
|
|
96
|
+
# Validate the named specialist first, then route accountability
|
|
97
|
+
# through the lead. Never load the specialist's private memory here.
|
|
98
|
+
rows = await self._assignment_candidates(request.personal_space_id, request.personal_project_id)
|
|
99
|
+
lead = next((row for row in rows if row['agent_id'] == project_lead
|
|
100
|
+
and (delegated or self._agent_client_allowed(row, request.source_application))), None)
|
|
101
|
+
if not lead:
|
|
102
|
+
return ProjectAgentContextResolution(status='agent_unavailable',
|
|
103
|
+
reason='team_lead_unavailable', match_basis=['The configured project lead is unavailable.'])
|
|
104
|
+
if delegated:
|
|
105
|
+
# A real lead-to-member worker invocation keeps the member's
|
|
106
|
+
# isolated identity; its result returns to the project lead.
|
|
107
|
+
reporting_lead_agent_id = project_lead
|
|
108
|
+
reason = 'delegated_member'
|
|
109
|
+
basis = ['This member worker reports to the configured project lead.']
|
|
110
|
+
else:
|
|
111
|
+
requested_agent_id = request.agent_id
|
|
112
|
+
selected = [lead]
|
|
113
|
+
reason = 'project_lead_reporting'
|
|
114
|
+
basis = ['The project lead owns the task; requested members report results to this lead.']
|
|
115
|
+
if owner and selected and owner == selected[0]['agent_id']:
|
|
75
116
|
reason, basis = 'active_task_owner', ['reuse the owner of this host session task']
|
|
76
117
|
agent = await self.get_project_agent(
|
|
77
118
|
actor, request.personal_space_id, request.personal_project_id,
|
|
78
119
|
selected[0]['agent_id'],
|
|
79
120
|
) if selected else None
|
|
121
|
+
# A reporting lead does not execute in the member's client. Acknowledge
|
|
122
|
+
# the capability only after the executing role passes selection, so a
|
|
123
|
+
# failed resolution cannot be accepted as that member's answer.
|
|
124
|
+
if delegated and (not agent or not await self.redeem_agent_delegation(request, project_lead)):
|
|
125
|
+
return ProjectAgentContextResolution(status='agent_unavailable', reason='delegation_invalid')
|
|
80
126
|
return ProjectAgentContextResolution(
|
|
81
127
|
status='ready' if agent else (
|
|
82
128
|
'manual_selection' if reason == 'manual_agent_selection'
|
|
83
129
|
else 'agent_unavailable' if reason == 'agent_unavailable' else 'unassigned'
|
|
84
130
|
),
|
|
85
131
|
agent=agent, reason=reason, match_basis=basis, candidate_count=len(candidates),
|
|
132
|
+
requested_agent_id=requested_agent_id,
|
|
133
|
+
reporting_lead_agent_id=reporting_lead_agent_id,
|
|
86
134
|
)
|
|
@@ -93,6 +93,8 @@ class StoreProjectAgentCoordinationPolicy:
|
|
|
93
93
|
members = (request.team_member_agent_ids if 'team_member_agent_ids' in request.model_fields_set
|
|
94
94
|
else current.team_member_agent_ids)
|
|
95
95
|
auto_grow = current.auto_grow_team if request.auto_grow_team is None else request.auto_grow_team
|
|
96
|
+
if 'team_lead_agent_id' in request.model_fields_set and not lead:
|
|
97
|
+
raise HTTPException(status_code=422, detail='each project requires a lead; select a replacement instead of clearing the lead')
|
|
96
98
|
if members and not lead:
|
|
97
99
|
raise HTTPException(status_code=422, detail='select a team lead before adding members')
|
|
98
100
|
if lead in members:
|
|
@@ -115,9 +117,21 @@ class StoreProjectAgentCoordinationPolicy:
|
|
|
115
117
|
updated_at = now_utc()
|
|
116
118
|
written, _, _ = await self.runtime.driver.execute_query(
|
|
117
119
|
'''
|
|
118
|
-
MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
120
|
+
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
119
121
|
[:CONTAINS_PROJECT]->
|
|
120
122
|
(project:FuliPersonalProject {project_id: $personal_project_id})
|
|
123
|
+
OPTIONAL MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(role:FuliProjectAgent)
|
|
124
|
+
WHERE role.agent_id IN $team_agent_ids
|
|
125
|
+
WITH project, role ORDER BY role.agent_id
|
|
126
|
+
FOREACH (locked IN CASE WHEN role IS NULL THEN [] ELSE [role] END |
|
|
127
|
+
SET locked._task_lifecycle_lock = true
|
|
128
|
+
REMOVE locked._task_lifecycle_lock)
|
|
129
|
+
WITH project, collect(role) AS roles
|
|
130
|
+
WHERE size(roles) = size($team_agent_ids)
|
|
131
|
+
AND all(role IN roles WHERE role.status = 'active' AND EXISTS {
|
|
132
|
+
MATCH (project)-[:HAS_PROJECT_AGENT_ASSIGNMENT]->
|
|
133
|
+
(:FuliProjectAgentAssignment {status: 'active'})-[:ASSIGNED_AGENT]->(role)
|
|
134
|
+
})
|
|
121
135
|
MERGE (policy:FuliProjectAgentCoordinationPolicy {
|
|
122
136
|
policy_id: $policy_id
|
|
123
137
|
})
|
|
@@ -147,6 +161,7 @@ class StoreProjectAgentCoordinationPolicy:
|
|
|
147
161
|
auto_grow_team=auto_grow,
|
|
148
162
|
team_lead_agent_id=lead,
|
|
149
163
|
team_member_agent_ids=members,
|
|
164
|
+
team_agent_ids=([lead] if lead else []) + members,
|
|
150
165
|
expected_updated_at=current.updated_at,
|
|
151
166
|
updated_at=updated_at,
|
|
152
167
|
)
|
|
@@ -761,9 +761,11 @@ class StoreProjectAgentTaskRecruitment:
|
|
|
761
761
|
request.personal_space_id,
|
|
762
762
|
request.personal_project_id,
|
|
763
763
|
)
|
|
764
|
+
first_lead = (request.staffing_intent == 'default_lead' and participant_role == 'lead'
|
|
765
|
+
and coordination_policy.team_lead_agent_id is None)
|
|
764
766
|
confirmation_mode = (
|
|
765
767
|
'require_confirmation'
|
|
766
|
-
if coordination_policy.ask_before_recruitment
|
|
768
|
+
if coordination_policy.ask_before_recruitment and not first_lead
|
|
767
769
|
else 'automatic'
|
|
768
770
|
)
|
|
769
771
|
if not hr_agent_id:
|
|
@@ -54,14 +54,15 @@ class StoreProjectAgentTaskStaffing:
|
|
|
54
54
|
row for row in assignment_rows
|
|
55
55
|
if self._agent_client_allowed(row, request.source_application)
|
|
56
56
|
]
|
|
57
|
-
if request.staffing_intent in {'new_durable', 'temporary'}:
|
|
57
|
+
if request.staffing_intent in {'new_durable', 'temporary', 'default_lead'}:
|
|
58
58
|
return (
|
|
59
59
|
[],
|
|
60
60
|
rows,
|
|
61
61
|
'explicit_temporary_agent'
|
|
62
62
|
if request.staffing_intent == 'temporary'
|
|
63
63
|
else 'explicit_new_agent',
|
|
64
|
-
['
|
|
64
|
+
['project requires an accountable lead' if request.staffing_intent == 'default_lead'
|
|
65
|
+
else 'user explicitly requested recruitment'],
|
|
65
66
|
)
|
|
66
67
|
policy = await self.get_project_agent_coordination_policy(
|
|
67
68
|
actor,
|
|
@@ -347,13 +347,16 @@ class StoreProjectAgentTasks(
|
|
|
347
347
|
reason = 'hr_unavailable'
|
|
348
348
|
self._verify_parallel_plan(request.parallel_plan, participants)
|
|
349
349
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
350
|
+
# Staffing a default lead executes nothing, so it needs no executor.
|
|
351
|
+
executor_decision = None if request.staffing_intent == 'default_lead' else (
|
|
352
|
+
await self._resolve_executor_if_available(
|
|
353
|
+
actor,
|
|
354
|
+
request,
|
|
355
|
+
lead,
|
|
356
|
+
model_strategy,
|
|
357
|
+
model_source,
|
|
358
|
+
task_id,
|
|
359
|
+
)
|
|
357
360
|
)
|
|
358
361
|
if lead and executor_decision and self._executor_decision_blocked(executor_decision):
|
|
359
362
|
routing_outcome = 'blocked'
|
|
@@ -904,7 +907,8 @@ class StoreProjectAgentTasks(
|
|
|
904
907
|
lead_agent_id=lead_agent_id,
|
|
905
908
|
complexity=complexity,
|
|
906
909
|
complexity_basis=complexity_basis,
|
|
907
|
-
|
|
910
|
+
# A staffing record has no artifact to verify.
|
|
911
|
+
verification_required=request.staffing_intent != 'default_lead',
|
|
908
912
|
model_strategy_json=model_strategy.model_dump_json() if model_strategy else None,
|
|
909
913
|
task_model_strategy_override_json=(
|
|
910
914
|
request.model_strategy_override.model_dump_json()
|