fuli-context 0.7.10-beta.0 → 0.8.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 +5 -2
- package/README.zh-CN.md +5 -3
- package/dist/web/assets/{AboutPage-BvXH4hsO.js → AboutPage-BjMmpwJB.js} +1 -1
- package/dist/web/assets/AboutPage-vYIBifKw.css +1 -0
- package/dist/web/assets/AgentHand-BfLYPGjg.js +1 -0
- package/dist/web/assets/AgentHand-D9sQu2vc.css +1 -0
- package/dist/web/assets/ConnectionsPage-BKGxFDos.js +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-B59X3rOi.css +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-D-W043v4.js +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-Cz7y-IFD.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-HHQPxdqI.js +3 -0
- package/dist/web/assets/GrowthLoading-B_Hh0CRS.css +1 -0
- package/dist/web/assets/GrowthLoading-WXCj8b0r.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-Bkie08ZA.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-D9NuRHvp.css +1 -0
- package/dist/web/assets/KnowledgeInspector-Cez-Z_95.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-BYmTPA_8.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-h2iKvQpO.css +1 -0
- package/dist/web/assets/KnowledgePage-Ck7tIBDn.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-DKij2_B4.js +4 -0
- package/dist/web/assets/KnowledgeWorkspace-sb8OJ97I.css +1 -0
- package/dist/web/assets/NotFoundPage-HGV-q-bh.js +1 -0
- package/dist/web/assets/{OverviewPage-D4vLhIFD.js → OverviewPage-DE29_F_Y.js} +1 -1
- package/dist/web/assets/PersonalProfilePage-B95DFc7x.js +1 -0
- package/dist/web/assets/PersonalProfilePage-L8T5xMEC.css +1 -0
- package/dist/web/assets/PersonalProjectsPage-Bt7tFF5Z.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-BioDR9OW.css +1 -0
- package/dist/web/assets/ProjectAgentsPage-CiiPNe_G.js +12 -0
- package/dist/web/assets/ProjectScopePicker-BvaGTMFx.css +1 -0
- package/dist/web/assets/ProjectScopePicker-Cu5PdxF6.js +1 -0
- package/dist/web/assets/PublicProjectsPage-BY4B3_t3.js +1 -0
- package/dist/web/assets/ReviewPage-C9vCesNu.js +1 -0
- package/dist/web/assets/{SearchableSelect-DY-TWC2i.js → SearchableSelect-214TvfYg.js} +1 -1
- package/dist/web/assets/SettingsPage-Brl-SL33.css +1 -0
- package/dist/web/assets/SettingsPage-D9TnzcFc.js +1 -0
- package/dist/web/assets/SharedJefaReportPage-CdSGI_OE.js +1 -0
- package/dist/web/assets/SharedJefaReportPage-DXbj_JhO.css +1 -0
- package/dist/web/assets/WritingTastePage-Bzg6AvnB.css +1 -0
- package/dist/web/assets/WritingTastePage-CUE1dU8f.js +1 -0
- package/dist/web/assets/bole-avatar-CVIr-bU_.webp +0 -0
- package/dist/web/assets/client-Bl4oiW3E.js +1 -0
- package/dist/web/assets/console-BAIVNC-N.js +1 -0
- package/dist/web/assets/conversation-launcher-settings-DpRI_Jp2.js +1 -0
- package/dist/web/assets/employee-dialog-Bh5ccIbK.css +1 -0
- package/dist/web/assets/{i18n-C7r_00q2.js → i18n-CVFADb_M.js} +3 -3
- package/dist/web/assets/{identity-COvBmUF4.js → identity-Dt0WZgqo.js} +1 -1
- package/dist/web/assets/index-BUMq5zNh.js +2 -0
- package/dist/web/assets/index-C0ci8dnU.css +1 -0
- package/dist/web/assets/{model-CL1JILt4.js → model-B8sOxih0.js} +1 -1
- package/dist/web/assets/personal-profile-graph-aAqex7ty.js +1 -0
- package/dist/web/assets/shared-report-BQD6l_cS.js +1 -0
- package/dist/web/assets/shared-report-ezeb8dW4.css +1 -0
- package/dist/web/assets/{useApi-CROJJdhE-DERnOR_k.js → useApi-CROJJdhE-CuJ16v2S.js} +1 -1
- package/dist/web/assets/useModalDialog-Ddv2XJQi.js +1 -0
- package/dist/web/assets/{vue-router-DJ6C6-8v.js → vue-router-C8KuEZ6a.js} +1 -1
- package/dist/web/index.html +14 -8
- package/docs/agent-interface-architecture.md +97 -0
- package/docs/employee-agents.md +58 -2
- package/docs/project-agent-memory.md +69 -449
- package/graph-provider/fuli_graph/app.py +2 -0
- package/graph-provider/fuli_graph/collaboration_context.py +2 -1
- package/graph-provider/fuli_graph/graph_models.py +5 -3
- package/graph-provider/fuli_graph/graph_query.py +4 -0
- package/graph-provider/fuli_graph/knowledge_search.py +2 -1
- package/graph-provider/fuli_graph/preference_conflicts.py +4 -1
- package/graph-provider/fuli_graph/project_agent_access.py +2 -0
- package/graph-provider/fuli_graph/project_agent_attention_models.py +64 -0
- package/graph-provider/fuli_graph/project_agent_attention_routes.py +38 -0
- package/graph-provider/fuli_graph/project_agent_coordination_models.py +23 -1
- package/graph-provider/fuli_graph/project_agent_memory_models.py +11 -0
- package/graph-provider/fuli_graph/project_agent_models.py +2 -0
- package/graph-provider/fuli_graph/project_agent_names.py +33 -0
- package/graph-provider/fuli_graph/project_agent_routes.py +12 -0
- package/graph-provider/fuli_graph/project_agent_team_growth.py +56 -0
- package/graph-provider/fuli_graph/project_agent_work_log.py +34 -0
- package/graph-provider/fuli_graph/runtime.py +49 -20
- package/graph-provider/fuli_graph/store.py +2 -0
- package/graph-provider/fuli_graph/store_knowledge.py +4 -2
- package/graph-provider/fuli_graph/store_project_agent_attention.py +138 -0
- package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +56 -2
- package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +8 -0
- package/graph-provider/fuli_graph/store_project_agent_memory.py +43 -13
- package/graph-provider/fuli_graph/store_project_agent_task_activity.py +1 -1
- package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +6 -1
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +21 -3
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +4 -0
- package/graph-provider/fuli_graph/store_project_agents.py +109 -0
- package/graph-provider/fuli_graph/store_task_contexts.py +12 -2
- package/graph-provider/fuli_graph/system_hr_identity.py +218 -0
- package/graph-provider/fuli_graph/task_context_adoption.py +65 -0
- package/graph-provider/fuli_graph/task_context_models.py +23 -1
- package/graph-provider/fuli_graph/task_context_routes.py +5 -1
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +52 -60
- package/package.json +2 -2
- package/skills/operating-fuli/SKILL.md +47 -0
- package/skills/operating-fuli/agents/openai.yaml +4 -0
- package/skills/operating-fuli/references/workflow-recipes.md +33 -0
- package/src/agent-tools/agent-attention-definitions.js +33 -0
- package/src/agent-tools/graph-definitions.js +22 -7
- package/src/agent-tools/graph-handlers.js +41 -0
- package/src/agent-tools/interface-catalog.js +101 -0
- package/src/agent-tools/interface-definitions.js +184 -0
- package/src/agent-tools/project-agent-definitions.js +2 -0
- package/src/agents/codex/lifecycle-hook.js +1 -0
- package/src/cli/local-runtime-command.js +2 -1
- package/src/cli/setup-command.js +1 -1
- package/src/employees/catalog/bole.json +19 -0
- package/src/employees/install-package.js +3 -0
- package/src/employees/manifest.js +4 -0
- package/src/employees/package-registry.js +2 -0
- package/src/employees/recruitment.js +3 -0
- package/src/employees/service.js +10 -0
- package/src/external-knowledge/registry.js +24 -0
- package/src/external-knowledge/service.js +34 -1
- package/src/graphiti/agent-attention.js +26 -0
- package/src/graphiti/agent-knowledge-workflows.js +14 -4
- package/src/graphiti/project-agent-control-plane.js +30 -0
- package/src/graphiti/project-agent-mapping.js +18 -1
- package/src/graphiti/project-agent-memory.js +10 -2
- package/src/graphiti/project-agent-task-entry.js +4 -1
- package/src/graphiti/project-agent-team-context.js +21 -0
- package/src/graphiti/project-agent-workflows.js +18 -0
- package/src/graphiti/provider-client.js +20 -1
- package/src/graphiti/workspace-provider-client.js +9 -1
- package/src/graphiti/writing-taste-profile-workflow.js +62 -9
- package/src/graphiti/writing-taste-profile.js +43 -16
- package/src/http/agent-attention-api-router.js +28 -0
- package/src/http/error-mapping.js +3 -0
- package/src/http/graph-api-router.js +11 -0
- package/src/local-runtime/lifecycle.js +8 -2
- package/src/mcp/create-mcp-server.js +15 -4
- package/src/mcp/instructions.js +3 -3
- package/src/mcp/provider-task-context-registry.js +11 -1
- package/src/mcp/task-context-registry.js +2 -1
- package/src/mcp/tool-annotations.js +31 -0
- package/src/mcp/tool-result.js +11 -7
- package/src/setup/agent-installation-status.js +2 -1
- package/src/setup/agents.js +4 -0
- package/src/setup/graph-runtime.js +4 -0
- package/src/setup/paths.js +2 -1
- package/src/setup/setup.js +4 -2
- package/src/setup/uninstall.js +4 -2
- package/src/system/resource-monitor.js +11 -1
- package/src/system/runtime-settings.js +7 -0
- package/dist/web/assets/AboutPage-DXh7Fwxt.css +0 -1
- package/dist/web/assets/ConnectionsPage-WVc6Srbd.js +0 -1
- package/dist/web/assets/EmployeeRecruitDialog-CnA7rOnB.css +0 -1
- package/dist/web/assets/EmployeeRecruitDialog-Dal5wy8a.js +0 -1
- package/dist/web/assets/EmployeeWorkbenchPage-BDJvrpcn.css +0 -1
- package/dist/web/assets/EmployeeWorkbenchPage-CW5pu_Yb.js +0 -1
- package/dist/web/assets/KnowledgeEditDialog-BBvm74ln.js +0 -1
- package/dist/web/assets/KnowledgeEditDialog-iv-dgUeD.css +0 -1
- package/dist/web/assets/KnowledgeInspector-CE9ADmtT.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-BRtizGUS.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +0 -1
- package/dist/web/assets/KnowledgePage-D5clXGCw.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-CKLH6cIf.js +0 -4
- package/dist/web/assets/PersonalProfilePage-7w8rAzkg.js +0 -1
- package/dist/web/assets/PersonalProfilePage-B7T09bow.css +0 -1
- package/dist/web/assets/PersonalProjectsPage-BY0B1t24.js +0 -1
- package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +0 -1
- package/dist/web/assets/ProjectAgentsPage-LHPOuLWw.js +0 -11
- package/dist/web/assets/PublicProjectsPage-PSp6RzVZ.js +0 -1
- package/dist/web/assets/ReviewPage-DPP23xLF.js +0 -1
- package/dist/web/assets/SettingsPage-C59OPsNP.js +0 -1
- package/dist/web/assets/SettingsPage-CwZp2ka4.css +0 -1
- package/dist/web/assets/WritingTastePage-DNXhCdgn.js +0 -1
- package/dist/web/assets/WritingTastePage-DSgvTH6x.css +0 -1
- package/dist/web/assets/avatars-BfZueqlm.js +0 -1
- package/dist/web/assets/console-CYXvhGCR.js +0 -1
- package/dist/web/assets/conversation-launcher-settings-SwmUP91W.js +0 -1
- package/dist/web/assets/index-B-rXoqYP.js +0 -2
- package/dist/web/assets/index-Ch1jmkyO.css +0 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from fastapi import HTTPException
|
|
5
|
+
|
|
6
|
+
from .personal_project_access import authorize_personal_project
|
|
7
|
+
from .project_agent_access import authorize_project_agent
|
|
8
|
+
from .project_agent_attention_models import (
|
|
9
|
+
AgentAttentionCreate, AgentAttentionDecision, AgentAttentionList,
|
|
10
|
+
AgentAttentionRecord,
|
|
11
|
+
)
|
|
12
|
+
from .provider_values import native_datetime, now_utc, stable_uuid
|
|
13
|
+
from .system_hr_identity import resolve_hr_alias
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StoreProjectAgentAttention:
|
|
17
|
+
"""Explicit human requests; never infer attention from worker/task status."""
|
|
18
|
+
|
|
19
|
+
async def create_agent_attention(self, actor, request: AgentAttentionCreate):
|
|
20
|
+
self._require_personal()
|
|
21
|
+
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
22
|
+
agent_id = await resolve_hr_alias(self, request.personal_space_id, request.agent_id)
|
|
23
|
+
await authorize_project_agent(
|
|
24
|
+
self, actor, space, request.personal_project_id, agent_id,
|
|
25
|
+
require_active=True,
|
|
26
|
+
allow_unassigned=agent_id in {
|
|
27
|
+
'employee.bole', 'employee.jefa', 'fuli-project-coordinator',
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
if request.task_id:
|
|
31
|
+
task = await self._find_task_row(
|
|
32
|
+
request.personal_space_id, request.task_id,
|
|
33
|
+
request.personal_project_id,
|
|
34
|
+
)
|
|
35
|
+
if not task:
|
|
36
|
+
raise HTTPException(status_code=404, detail='project Agent task not found')
|
|
37
|
+
payload = request.model_dump(exclude={'idempotency_key'})
|
|
38
|
+
fingerprint = hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
|
|
39
|
+
payload['agent_id'] = agent_id
|
|
40
|
+
request_id = stable_uuid(
|
|
41
|
+
self.settings.provider_id, request.personal_space_id,
|
|
42
|
+
request.personal_project_id, 'agent-attention', request.idempotency_key,
|
|
43
|
+
)
|
|
44
|
+
timestamp = now_utc()
|
|
45
|
+
rows, _, _ = await self.runtime.driver.execute_query(
|
|
46
|
+
'''
|
|
47
|
+
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})
|
|
48
|
+
MERGE (request:FuliAgentAttention {id: $request_id})
|
|
49
|
+
ON CREATE SET request += $payload,
|
|
50
|
+
request.request_id = $request_id,
|
|
51
|
+
request.fingerprint = $fingerprint,
|
|
52
|
+
request.status = 'open', request.revision = 0,
|
|
53
|
+
request.created_at = $timestamp,
|
|
54
|
+
request.updated_at = $timestamp
|
|
55
|
+
MERGE (space)-[:HAS_AGENT_ATTENTION]->(request)
|
|
56
|
+
RETURN request
|
|
57
|
+
''',
|
|
58
|
+
personal_space_id=request.personal_space_id, request_id=request_id,
|
|
59
|
+
payload=payload, fingerprint=fingerprint, timestamp=timestamp,
|
|
60
|
+
)
|
|
61
|
+
raw = dict(rows[0]['request'])
|
|
62
|
+
if raw['fingerprint'] != fingerprint:
|
|
63
|
+
raise HTTPException(status_code=409, detail='attention idempotency key conflict')
|
|
64
|
+
return attention_record(raw)
|
|
65
|
+
|
|
66
|
+
async def list_agent_attention(
|
|
67
|
+
self, actor, personal_space_id, personal_project_id=None,
|
|
68
|
+
agent_id=None, status='open', limit=100, offset=0,
|
|
69
|
+
):
|
|
70
|
+
self._require_personal()
|
|
71
|
+
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
72
|
+
agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
|
|
73
|
+
if personal_project_id:
|
|
74
|
+
await authorize_personal_project(self, actor, space, personal_project_id)
|
|
75
|
+
rows, _, _ = await self.runtime.driver.execute_query(
|
|
76
|
+
'''
|
|
77
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
78
|
+
[:HAS_AGENT_ATTENTION]->(request:FuliAgentAttention)
|
|
79
|
+
WHERE ($project_id IS NULL OR request.personal_project_id = $project_id)
|
|
80
|
+
AND ($agent_id IS NULL OR request.agent_id = $agent_id)
|
|
81
|
+
AND ($status IS NULL OR request.status = $status)
|
|
82
|
+
WITH request ORDER BY request.created_at DESC, request.request_id
|
|
83
|
+
WITH collect(request) AS requests
|
|
84
|
+
RETURN requests[$offset..($offset + $limit)] AS items,
|
|
85
|
+
size(requests) AS total,
|
|
86
|
+
[r IN requests WHERE r.status = 'open' | r.agent_id] AS open_agents
|
|
87
|
+
''', space_id=personal_space_id, project_id=personal_project_id,
|
|
88
|
+
agent_id=agent_id, status=status, limit=limit, offset=offset, routing_='r',
|
|
89
|
+
)
|
|
90
|
+
row = rows[0] if rows else {'items': [], 'total': 0, 'open_agents': []}
|
|
91
|
+
counts = {}
|
|
92
|
+
for agent in row['open_agents']:
|
|
93
|
+
counts[agent] = counts.get(agent, 0) + 1
|
|
94
|
+
return AgentAttentionList(
|
|
95
|
+
items=[attention_record(dict(item)) for item in row['items']],
|
|
96
|
+
total=row['total'], counts=counts,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
async def decide_agent_attention(
|
|
100
|
+
self, actor, request: AgentAttentionDecision, *, human: bool,
|
|
101
|
+
):
|
|
102
|
+
self._require_personal()
|
|
103
|
+
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
104
|
+
await authorize_personal_project(self, actor, space, request.personal_project_id)
|
|
105
|
+
timestamp = now_utc()
|
|
106
|
+
# Increment obtains a write lock before the revision predicate is checked.
|
|
107
|
+
# A stale caller cannot overwrite the response recorded by another client.
|
|
108
|
+
rows, _, _ = await self.runtime.driver.execute_query(
|
|
109
|
+
'''
|
|
110
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
111
|
+
[:HAS_AGENT_ATTENTION]->(request:FuliAgentAttention {
|
|
112
|
+
request_id: $request_id, personal_project_id: $project_id
|
|
113
|
+
})
|
|
114
|
+
SET request.lock_version = coalesce(request.lock_version, 0) + 1
|
|
115
|
+
WITH request
|
|
116
|
+
WHERE request.revision = $revision AND request.status = 'open'
|
|
117
|
+
SET request.status = $status, request.response = $response,
|
|
118
|
+
request.responded_by = $responded_by,
|
|
119
|
+
request.revision = request.revision + 1,
|
|
120
|
+
request.updated_at = $timestamp
|
|
121
|
+
RETURN request
|
|
122
|
+
''', space_id=request.personal_space_id,
|
|
123
|
+
project_id=request.personal_project_id, request_id=request.request_id,
|
|
124
|
+
revision=request.expected_revision, response=request.response,
|
|
125
|
+
status='resolved' if human else 'cancelled',
|
|
126
|
+
responded_by='human' if human else 'agent', timestamp=timestamp,
|
|
127
|
+
)
|
|
128
|
+
if not rows:
|
|
129
|
+
raise HTTPException(status_code=409, detail='attention changed or is no longer open; reload it')
|
|
130
|
+
return attention_record(dict(rows[0]['request']))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def attention_record(raw):
|
|
134
|
+
return AgentAttentionRecord(**{
|
|
135
|
+
key: native_datetime(raw.get(key)) if key in {'created_at', 'updated_at'} else raw.get(key)
|
|
136
|
+
for key in AgentAttentionRecord.model_fields
|
|
137
|
+
if key in raw
|
|
138
|
+
})
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Project-local Agent continuity and recruitment authorization."""
|
|
2
2
|
|
|
3
|
+
from fastapi import HTTPException
|
|
4
|
+
|
|
3
5
|
from .personal_project_access import authorize_personal_project
|
|
4
6
|
from .project_agent_coordination_models import (
|
|
5
7
|
ProjectAgentCoordinationPolicyRecord,
|
|
@@ -9,7 +11,7 @@ from .provider_values import native_datetime, now_utc, stable_uuid
|
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class StoreProjectAgentCoordinationPolicy:
|
|
12
|
-
"""Persist
|
|
14
|
+
"""Persist project automation and a stable team without absorbing peer roles."""
|
|
13
15
|
|
|
14
16
|
async def get_project_agent_coordination_policy(
|
|
15
17
|
self,
|
|
@@ -53,6 +55,9 @@ class StoreProjectAgentCoordinationPolicy:
|
|
|
53
55
|
'auto_reuse_previous_agent',
|
|
54
56
|
True,
|
|
55
57
|
),
|
|
58
|
+
team_lead_agent_id=raw.get('team_lead_agent_id'),
|
|
59
|
+
auto_grow_team=raw.get('auto_grow_team', True),
|
|
60
|
+
team_member_agent_ids=raw.get('team_member_agent_ids') or [],
|
|
56
61
|
updated_at=native_datetime(raw.get('updated_at')),
|
|
57
62
|
)
|
|
58
63
|
|
|
@@ -77,8 +82,38 @@ class StoreProjectAgentCoordinationPolicy:
|
|
|
77
82
|
request.personal_space_id,
|
|
78
83
|
request.personal_project_id,
|
|
79
84
|
)
|
|
85
|
+
current = await self.get_project_agent_coordination_policy(
|
|
86
|
+
actor, request.personal_space_id, request.personal_project_id,
|
|
87
|
+
)
|
|
88
|
+
if ('expected_updated_at' in request.model_fields_set
|
|
89
|
+
and request.expected_updated_at != current.updated_at):
|
|
90
|
+
raise HTTPException(status_code=409, detail='coordination policy changed; reload before saving')
|
|
91
|
+
lead = (request.team_lead_agent_id if 'team_lead_agent_id' in request.model_fields_set
|
|
92
|
+
else current.team_lead_agent_id)
|
|
93
|
+
members = (request.team_member_agent_ids if 'team_member_agent_ids' in request.model_fields_set
|
|
94
|
+
else current.team_member_agent_ids)
|
|
95
|
+
auto_grow = current.auto_grow_team if request.auto_grow_team is None else request.auto_grow_team
|
|
96
|
+
if members and not lead:
|
|
97
|
+
raise HTTPException(status_code=422, detail='select a team lead before adding members')
|
|
98
|
+
if lead in members:
|
|
99
|
+
raise HTTPException(status_code=422, detail='the team lead cannot also be a team member')
|
|
100
|
+
if 'team_lead_agent_id' in request.model_fields_set or 'team_member_agent_ids' in request.model_fields_set:
|
|
101
|
+
for agent_id in ([lead] if lead else []) + members:
|
|
102
|
+
agent = await self.get_project_agent(
|
|
103
|
+
actor, request.personal_space_id, request.personal_project_id, agent_id,
|
|
104
|
+
)
|
|
105
|
+
profile = agent.profile
|
|
106
|
+
active_assignment = any(
|
|
107
|
+
assignment.status == 'active'
|
|
108
|
+
and assignment.personal_project_id == request.personal_project_id
|
|
109
|
+
for assignment in agent.assignments
|
|
110
|
+
)
|
|
111
|
+
if (profile.status != 'active' or not active_assignment
|
|
112
|
+
or profile.agent_type != 'durable'
|
|
113
|
+
or any(cap.startswith('fuli.employee:') for cap in profile.capabilities)):
|
|
114
|
+
raise HTTPException(status_code=422, detail='team roles require active durable project assignments; HR and project managers remain peers')
|
|
80
115
|
updated_at = now_utc()
|
|
81
|
-
await self.runtime.driver.execute_query(
|
|
116
|
+
written, _, _ = await self.runtime.driver.execute_query(
|
|
82
117
|
'''
|
|
83
118
|
MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
84
119
|
[:CONTAINS_PROJECT]->
|
|
@@ -89,23 +124,42 @@ class StoreProjectAgentCoordinationPolicy:
|
|
|
89
124
|
ON CREATE SET policy.personal_space_id = $personal_space_id,
|
|
90
125
|
policy.personal_project_id = $personal_project_id,
|
|
91
126
|
policy.created_at = $updated_at
|
|
127
|
+
SET policy._fuli_write_lock = true
|
|
128
|
+
REMOVE policy._fuli_write_lock
|
|
129
|
+
WITH project, policy
|
|
130
|
+
WHERE ($expected_updated_at IS NULL AND policy.updated_at IS NULL)
|
|
131
|
+
OR (policy.updated_at.epochSeconds = $expected_updated_at.epochSeconds
|
|
132
|
+
AND policy.updated_at.nanosecond = $expected_updated_at.nanosecond)
|
|
92
133
|
SET policy.ask_before_recruitment = $ask_before_recruitment,
|
|
93
134
|
policy.auto_reuse_previous_agent = $auto_reuse_previous_agent,
|
|
135
|
+
policy.auto_grow_team = $auto_grow_team,
|
|
136
|
+
policy.team_lead_agent_id = $team_lead_agent_id,
|
|
137
|
+
policy.team_member_agent_ids = $team_member_agent_ids,
|
|
94
138
|
policy.updated_at = $updated_at
|
|
95
139
|
MERGE (project)-[:HAS_PROJECT_AGENT_COORDINATION_POLICY]->(policy)
|
|
140
|
+
RETURN policy
|
|
96
141
|
''',
|
|
97
142
|
policy_id=policy_id,
|
|
98
143
|
personal_space_id=request.personal_space_id,
|
|
99
144
|
personal_project_id=request.personal_project_id,
|
|
100
145
|
ask_before_recruitment=request.ask_before_recruitment,
|
|
101
146
|
auto_reuse_previous_agent=request.auto_reuse_previous_agent,
|
|
147
|
+
auto_grow_team=auto_grow,
|
|
148
|
+
team_lead_agent_id=lead,
|
|
149
|
+
team_member_agent_ids=members,
|
|
150
|
+
expected_updated_at=current.updated_at,
|
|
102
151
|
updated_at=updated_at,
|
|
103
152
|
)
|
|
153
|
+
if not written:
|
|
154
|
+
raise HTTPException(status_code=409, detail='coordination policy changed; reload before saving')
|
|
104
155
|
return ProjectAgentCoordinationPolicyRecord(
|
|
105
156
|
personal_space_id=request.personal_space_id,
|
|
106
157
|
personal_project_id=request.personal_project_id,
|
|
107
158
|
ask_before_recruitment=request.ask_before_recruitment,
|
|
108
159
|
auto_reuse_previous_agent=request.auto_reuse_previous_agent,
|
|
160
|
+
auto_grow_team=auto_grow,
|
|
161
|
+
team_lead_agent_id=lead,
|
|
162
|
+
team_member_agent_ids=members,
|
|
109
163
|
updated_at=updated_at,
|
|
110
164
|
)
|
|
111
165
|
|
|
@@ -22,6 +22,7 @@ from .project_agent_executor_models import (
|
|
|
22
22
|
from .project_agent_models import ProjectAgentModelStrategy
|
|
23
23
|
from .provider_values import native_datetime, now_utc, stable_uuid
|
|
24
24
|
from .store_transactions import query_store_transaction
|
|
25
|
+
from .system_hr_identity import resolve_hr_alias
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
_TERMINAL_NEUTRAL = {'cancelled'}
|
|
@@ -276,6 +277,7 @@ class StoreProjectAgentExecutorLearning:
|
|
|
276
277
|
) -> list[ProjectAgentExecutorOutcomeEvidenceRecord]:
|
|
277
278
|
self._require_personal()
|
|
278
279
|
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
280
|
+
agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
|
|
279
281
|
await authorize_personal_project(
|
|
280
282
|
self,
|
|
281
283
|
actor,
|
|
@@ -334,6 +336,7 @@ class StoreProjectAgentExecutorLearning:
|
|
|
334
336
|
|
|
335
337
|
self._require_personal()
|
|
336
338
|
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
339
|
+
agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
|
|
337
340
|
if personal_project_id is not None:
|
|
338
341
|
await authorize_personal_project(
|
|
339
342
|
self,
|
|
@@ -367,12 +370,17 @@ class StoreProjectAgentExecutorLearning:
|
|
|
367
370
|
aggregates: list[ProjectAgentExecutorOutcomeAggregate] = []
|
|
368
371
|
for row in records:
|
|
369
372
|
raw = dict(row.get('aggregate') or {})
|
|
373
|
+
identity = raw.get('agent_id')
|
|
370
374
|
aggregate_json = raw.get('aggregate_json')
|
|
371
375
|
if aggregate_json:
|
|
372
376
|
try:
|
|
373
377
|
raw.update(json.loads(aggregate_json))
|
|
374
378
|
except (TypeError, ValueError):
|
|
375
379
|
pass
|
|
380
|
+
if raw.get('hr_identity_original_refs_json'):
|
|
381
|
+
raw['agent_id'] = identity
|
|
382
|
+
raw.pop('hr_identity_original_refs_json', None)
|
|
383
|
+
raw.pop('hr_identity_original_id', None)
|
|
376
384
|
# Storage identities are intentionally internal: the public model
|
|
377
385
|
# remains the provider-neutral bucket contract.
|
|
378
386
|
raw.pop('id', None)
|
|
@@ -16,6 +16,8 @@ from .project_agent_memory_models import (
|
|
|
16
16
|
ProjectAgentMemoryWrite,
|
|
17
17
|
)
|
|
18
18
|
from .provider_values import now_utc, stable_uuid
|
|
19
|
+
from .project_agent_work_log import read_employee_work_log
|
|
20
|
+
from .system_hr_identity import LEGACY_HR_AGENT_ID, SYSTEM_HR_AGENT_ID, resolve_hr_alias
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
class StoreProjectAgentMemory:
|
|
@@ -24,7 +26,7 @@ class StoreProjectAgentMemory:
|
|
|
24
26
|
) -> ProjectAgentMemoryView:
|
|
25
27
|
self._require_personal()
|
|
26
28
|
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
27
|
-
await authorize_project_agent(
|
|
29
|
+
agent = await authorize_project_agent(
|
|
28
30
|
self, actor, space, personal_project_id, agent_id, require_memory=True,
|
|
29
31
|
)
|
|
30
32
|
records, _, _ = await self.runtime.driver.execute_query(
|
|
@@ -34,24 +36,26 @@ class StoreProjectAgentMemory:
|
|
|
34
36
|
RETURN checkpoint.record_json AS record_json
|
|
35
37
|
ORDER BY checkpoint.revision DESC LIMIT $limit
|
|
36
38
|
''',
|
|
37
|
-
memory_id=self.
|
|
38
|
-
personal_space_id, personal_project_id, agent_id,
|
|
39
|
-
),
|
|
39
|
+
memory_id=await self._resolved_agent_memory_id(
|
|
40
|
+
personal_space_id, personal_project_id, agent['agent_id']),
|
|
40
41
|
limit=max(1, min(limit, 10)),
|
|
41
42
|
routing_='r',
|
|
42
43
|
)
|
|
43
44
|
history = [
|
|
44
|
-
ProjectAgentMemoryRecord.model_validate_json(row['record_json'])
|
|
45
|
+
ProjectAgentMemoryRecord.model_validate_json(row['record_json']).model_copy(
|
|
46
|
+
update={'agent_id': agent['agent_id']})
|
|
45
47
|
for row in records
|
|
46
48
|
]
|
|
47
49
|
current = history[0] if history else None
|
|
48
50
|
return ProjectAgentMemoryView(
|
|
49
51
|
personal_space_id=personal_space_id,
|
|
50
52
|
personal_project_id=personal_project_id,
|
|
51
|
-
agent_id=agent_id,
|
|
53
|
+
agent_id=agent['agent_id'],
|
|
52
54
|
revision=current.revision if current else 0,
|
|
53
55
|
current=current,
|
|
54
56
|
history=history,
|
|
57
|
+
work_log=await read_employee_work_log(self, personal_space_id,
|
|
58
|
+
personal_project_id, agent['agent_id']),
|
|
55
59
|
)
|
|
56
60
|
|
|
57
61
|
async def write_project_agent_memory(
|
|
@@ -78,14 +82,14 @@ class StoreProjectAgentMemory:
|
|
|
78
82
|
''',
|
|
79
83
|
space_id=request.personal_space_id,
|
|
80
84
|
project_id=request.personal_project_id,
|
|
81
|
-
agent_id=
|
|
85
|
+
agent_id=agent['agent_id'],
|
|
82
86
|
task_id=request.task_id,
|
|
83
87
|
routing_='r',
|
|
84
88
|
)
|
|
85
89
|
if not task_rows:
|
|
86
90
|
raise HTTPException(status_code=404, detail='Agent memory task not found')
|
|
87
|
-
memory_id = self.
|
|
88
|
-
request.personal_space_id, request.personal_project_id,
|
|
91
|
+
memory_id = await self._resolved_agent_memory_id(
|
|
92
|
+
request.personal_space_id, request.personal_project_id, agent['agent_id'],
|
|
89
93
|
)
|
|
90
94
|
checkpoint_id = stable_uuid(memory_id, request.idempotency_key)
|
|
91
95
|
payload = request.model_dump(mode='json', exclude={
|
|
@@ -98,7 +102,7 @@ class StoreProjectAgentMemory:
|
|
|
98
102
|
checkpoint_id=checkpoint_id,
|
|
99
103
|
personal_space_id=request.personal_space_id,
|
|
100
104
|
personal_project_id=request.personal_project_id,
|
|
101
|
-
agent_id=
|
|
105
|
+
agent_id=agent['agent_id'],
|
|
102
106
|
revision=request.expected_revision + 1,
|
|
103
107
|
memory=request.memory,
|
|
104
108
|
source_application=request.source_application,
|
|
@@ -147,7 +151,7 @@ class StoreProjectAgentMemory:
|
|
|
147
151
|
''',
|
|
148
152
|
space_id=request.personal_space_id,
|
|
149
153
|
project_id=request.personal_project_id,
|
|
150
|
-
agent_id=
|
|
154
|
+
agent_id=agent['agent_id'],
|
|
151
155
|
memory_id=memory_id,
|
|
152
156
|
checkpoint_id=checkpoint_id,
|
|
153
157
|
expected_revision=request.expected_revision,
|
|
@@ -164,9 +168,35 @@ class StoreProjectAgentMemory:
|
|
|
164
168
|
status_code=409,
|
|
165
169
|
detail='Agent memory changed; reload its latest revision before merging notes',
|
|
166
170
|
)
|
|
167
|
-
|
|
171
|
+
compatible_hashes = {payload_hash}
|
|
172
|
+
if LEGACY_HR_AGENT_ID in agent.get('legacy_agent_ids', []):
|
|
173
|
+
# Old checkpoints keep their exact IDs/JSON/hashes. A canonical
|
|
174
|
+
# retry of the same input may differ only in the identity spelling.
|
|
175
|
+
for identity in (LEGACY_HR_AGENT_ID, SYSTEM_HR_AGENT_ID):
|
|
176
|
+
compatible_hashes.add(hashlib.sha256(json.dumps(
|
|
177
|
+
{**payload, 'agent_id': identity}, sort_keys=True,
|
|
178
|
+
ensure_ascii=False, separators=(',', ':'),
|
|
179
|
+
).encode()).hexdigest())
|
|
180
|
+
if row['payload_hash'] not in compatible_hashes:
|
|
168
181
|
raise HTTPException(status_code=409, detail='memory idempotency key has different input')
|
|
169
|
-
return ProjectAgentMemoryRecord.model_validate_json(row['record_json'])
|
|
182
|
+
return ProjectAgentMemoryRecord.model_validate_json(row['record_json']).model_copy(
|
|
183
|
+
update={'agent_id': agent['agent_id']})
|
|
184
|
+
|
|
185
|
+
async def _resolved_agent_memory_id(self, space_id, project_id, agent_id):
|
|
186
|
+
agent_id = await resolve_hr_alias(self, space_id, agent_id)
|
|
187
|
+
if agent_id == SYSTEM_HR_AGENT_ID:
|
|
188
|
+
rows, _, _ = await self.runtime.driver.execute_query(
|
|
189
|
+
'''MATCH (:FuliSpace {id: $space_id})-[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
190
|
+
(:FuliProjectAgent {agent_id: $agent_id})-[:HAS_WORKING_MEMORY]->
|
|
191
|
+
(head:FuliProjectAgentMemory {personal_space_id: $space_id,
|
|
192
|
+
personal_project_id: $project_id})
|
|
193
|
+
RETURN DISTINCT head.id AS id''',
|
|
194
|
+
space_id=space_id, project_id=project_id, agent_id=agent_id, routing_='r')
|
|
195
|
+
if len(rows) > 1:
|
|
196
|
+
raise HTTPException(409, 'Agent has conflicting memory heads')
|
|
197
|
+
if rows:
|
|
198
|
+
return rows[0]['id']
|
|
199
|
+
return self._agent_memory_id(space_id, project_id, agent_id)
|
|
170
200
|
|
|
171
201
|
def _agent_memory_id(self, space_id, project_id, agent_id):
|
|
172
202
|
return stable_uuid(
|
|
@@ -266,7 +266,7 @@ class StoreProjectAgentTaskActivity:
|
|
|
266
266
|
THEN [1] ELSE [] END |
|
|
267
267
|
SET task.status = $status,
|
|
268
268
|
task.result_summary = CASE
|
|
269
|
-
WHEN $status
|
|
269
|
+
WHEN $status IN ['completed', 'awaiting_review', 'failed', 'blocked', 'cancelled'] THEN $summary
|
|
270
270
|
ELSE task.result_summary END,
|
|
271
271
|
task.failure_reason = CASE
|
|
272
272
|
WHEN $status = 'failed' THEN $summary
|
|
@@ -24,7 +24,9 @@ from .project_agent_task_models import (
|
|
|
24
24
|
ProjectAgentRoutingDecisionRecord,
|
|
25
25
|
)
|
|
26
26
|
from .provider_values import native_datetime, now_utc, stable_uuid
|
|
27
|
+
from .store_project_agents import SYSTEM_HR_AGENT_ID
|
|
27
28
|
from .store_transactions import query_store_transaction
|
|
29
|
+
from .project_agent_team_growth import enroll_recruited_team_member
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
RECRUITMENT_CLAIM_TTL = timedelta(minutes=2)
|
|
@@ -744,9 +746,11 @@ class StoreProjectAgentTaskRecruitment:
|
|
|
744
746
|
MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
745
747
|
[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
746
748
|
(hr:FuliProjectAgent {agent_type: 'hr', status: 'active'})
|
|
747
|
-
RETURN hr ORDER BY
|
|
749
|
+
RETURN hr ORDER BY CASE WHEN hr.agent_id = $system_hr_agent_id
|
|
750
|
+
THEN 0 ELSE 1 END, hr.created_at, hr.agent_id LIMIT 1
|
|
748
751
|
''',
|
|
749
752
|
personal_space_id=request.personal_space_id,
|
|
753
|
+
system_hr_agent_id=SYSTEM_HR_AGENT_ID,
|
|
750
754
|
routing_='r',
|
|
751
755
|
)
|
|
752
756
|
hr_agent_id = (
|
|
@@ -1233,6 +1237,7 @@ class StoreProjectAgentTaskRecruitment:
|
|
|
1233
1237
|
include_temporary=True,
|
|
1234
1238
|
)
|
|
1235
1239
|
selected = next(item for item in assignment_rows if item['agent_id'] == agent_id)
|
|
1240
|
+
await enroll_recruited_team_member(self, raw, profile)
|
|
1236
1241
|
return selected
|
|
1237
1242
|
|
|
1238
1243
|
async def _get_recruitment(self, personal_space_id, recruitment_id):
|
|
@@ -7,6 +7,7 @@ from fastapi import HTTPException
|
|
|
7
7
|
from .project_agent_access import authorize_project_agent
|
|
8
8
|
from .project_agent_models import ProjectAgentProfile
|
|
9
9
|
from .project_agent_task_models import ProjectAgentParallelPlan
|
|
10
|
+
from .store_project_agents import SYSTEM_HR_AGENT_ID
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class StoreProjectAgentTaskStaffing:
|
|
@@ -61,6 +62,18 @@ class StoreProjectAgentTaskStaffing:
|
|
|
61
62
|
return [], rows, 'manual_agent_selection', [
|
|
62
63
|
'project policy requires an explicit @Agent selection',
|
|
63
64
|
]
|
|
65
|
+
team_lead = getattr(policy, 'team_lead_agent_id', None)
|
|
66
|
+
if team_lead:
|
|
67
|
+
lead = next((row for row in rows if row['agent_id'] == team_lead), None)
|
|
68
|
+
if lead is None:
|
|
69
|
+
return [], rows, 'agent_unavailable', ['configured team lead is unavailable; select or replace the lead']
|
|
70
|
+
member_ids = set(getattr(policy, 'team_member_agent_ids', []))
|
|
71
|
+
members = [row for row in rows if row['agent_id'] in member_ids]
|
|
72
|
+
return [lead], [lead, *members], 'project_team_lead', [
|
|
73
|
+
'project team lead is the stable conversation owner',
|
|
74
|
+
f'{len(members)} active team member(s) available for explicit workstreams',
|
|
75
|
+
'HR and project manager remain peer roles',
|
|
76
|
+
]
|
|
64
77
|
normalized_work_kind = request.work_kind.casefold()
|
|
65
78
|
required = {item.casefold() for item in request.required_capabilities}
|
|
66
79
|
eligible = [row for row in rows if required.issubset({
|
|
@@ -505,16 +518,19 @@ class StoreProjectAgentTaskStaffing:
|
|
|
505
518
|
request,
|
|
506
519
|
candidates,
|
|
507
520
|
participants,
|
|
521
|
+
*,
|
|
522
|
+
team_only=False,
|
|
508
523
|
):
|
|
509
524
|
capacity = len(request.parallel_plan.workstream_boundaries) - len(participants)
|
|
510
525
|
if capacity <= 0:
|
|
511
526
|
return []
|
|
512
527
|
pool = list(candidates)
|
|
513
528
|
known_ids = {item['agent_id'] for item in pool}
|
|
514
|
-
|
|
529
|
+
assignments = [] if team_only else await self._assignment_candidates(
|
|
515
530
|
request.personal_space_id,
|
|
516
531
|
request.personal_project_id,
|
|
517
|
-
)
|
|
532
|
+
)
|
|
533
|
+
for row in assignments:
|
|
518
534
|
if row['agent_id'] not in known_ids:
|
|
519
535
|
pool.append(row)
|
|
520
536
|
known_ids.add(row['agent_id'])
|
|
@@ -634,9 +650,11 @@ class StoreProjectAgentTaskStaffing:
|
|
|
634
650
|
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
635
651
|
[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
636
652
|
(hr:FuliProjectAgent {agent_type: 'hr', status: 'active'})
|
|
637
|
-
RETURN hr
|
|
653
|
+
RETURN hr ORDER BY CASE WHEN hr.agent_id = $system_hr_agent_id
|
|
654
|
+
THEN 0 ELSE 1 END, hr.created_at, hr.agent_id LIMIT 1
|
|
638
655
|
''',
|
|
639
656
|
personal_space_id=request.personal_space_id,
|
|
657
|
+
system_hr_agent_id=SYSTEM_HR_AGENT_ID,
|
|
640
658
|
routing_='r',
|
|
641
659
|
)
|
|
642
660
|
if not hr_records:
|
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
from collections import defaultdict
|
|
3
3
|
from datetime import date
|
|
4
4
|
from fastapi import HTTPException
|
|
5
|
+
from .system_hr_identity import resolve_hr_alias
|
|
5
6
|
from .personal_project_access import authorize_personal_project
|
|
6
7
|
from .project_agent_access import authorize_project_agent
|
|
7
8
|
from .project_agent_models import ProjectAgentModelStrategy, ProjectAgentProfile
|
|
@@ -146,6 +147,7 @@ class StoreProjectAgentTasks(
|
|
|
146
147
|
request,
|
|
147
148
|
candidates,
|
|
148
149
|
participants,
|
|
150
|
+
**({'team_only': True} if reason == 'project_team_lead' else {}),
|
|
149
151
|
)
|
|
150
152
|
candidate_ids = {item['agent_id'] for item in candidates}
|
|
151
153
|
candidates.extend(
|
|
@@ -470,6 +472,7 @@ class StoreProjectAgentTasks(
|
|
|
470
472
|
) -> list[ProjectAgentTaskRecord]:
|
|
471
473
|
self._require_personal()
|
|
472
474
|
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
475
|
+
agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
|
|
473
476
|
if personal_project_id:
|
|
474
477
|
await authorize_personal_project(
|
|
475
478
|
self,
|
|
@@ -510,6 +513,7 @@ class StoreProjectAgentTasks(
|
|
|
510
513
|
) -> ProjectAgentActivityResult:
|
|
511
514
|
self._require_personal()
|
|
512
515
|
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
516
|
+
agent_id = await resolve_hr_alias(self, personal_space_id, agent_id)
|
|
513
517
|
if personal_project_id:
|
|
514
518
|
await authorize_personal_project(
|
|
515
519
|
self,
|