fuli-context 0.7.4-beta.3 → 0.7.5
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 +77 -6
- package/README.zh-CN.md +69 -6
- package/compose.graphiti.yml +6 -6
- package/dist/web/assets/AboutPage-ByCUor-j.js +1 -0
- package/dist/web/assets/ConnectionsPage-DkdkZ40Q.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-DSe3Gwep.js +1 -0
- package/dist/web/assets/KnowledgeInspector-CYAjAb9J.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CV4F54R_.js +1 -0
- package/dist/web/assets/KnowledgePage-vYYXipOi.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-z4u5mXOG.js +4 -0
- package/dist/web/assets/OverviewPage-BWOfeS6k.js +1 -0
- package/dist/web/assets/PersonalProfilePage-c3w4-h7a.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-B7AlLEwf.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-CgftpIPB.css +1 -0
- package/dist/web/assets/ProjectAgentsPage-DTHcL_ox.js +11 -0
- package/dist/web/assets/PublicProjectsPage-DdH0oGB3.js +1 -0
- package/dist/web/assets/ReviewPage-BYIpcIHG.js +1 -0
- package/dist/web/assets/SearchableSelect-Dz13ClQb.js +1 -0
- package/dist/web/assets/SettingsPage-2VkbXgZi.js +1 -0
- package/dist/web/assets/SettingsPage-CwZp2ka4.css +1 -0
- package/dist/web/assets/WritingTastePage-BOPQZuva.js +1 -0
- package/dist/web/assets/{console-D_2zC4lm.js → console-BlAhujsH.js} +1 -1
- package/dist/web/assets/{conversation-launcher-settings-DQmMH2j7.js → conversation-launcher-settings-DTYxiLJt.js} +1 -1
- package/dist/web/assets/i18n-Ci3heuiL.js +9 -0
- package/dist/web/assets/{identity-DmGcY3Y6.js → identity-4_-v6cB7.js} +1 -1
- package/dist/web/assets/index-BnH5mcZx.css +1 -0
- package/dist/web/assets/index-C67Q0JY2.js +2 -0
- package/dist/web/assets/{model-CVoOdRsL.js → model-yG0YqUyZ.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-CBNg4CdH.js → useApi-CROJJdhE-twhdZArA.js} +1 -1
- package/dist/web/assets/vue-router-C-wot_Di.js +1 -0
- package/dist/web/index.html +6 -6
- package/graph-provider/fuli_graph/app.py +8 -0
- package/graph-provider/fuli_graph/collaboration_context.py +69 -6
- package/graph-provider/fuli_graph/knowledge_search.py +84 -5
- package/graph-provider/fuli_graph/model_validation.py +130 -0
- package/graph-provider/fuli_graph/models.py +24 -5
- package/graph-provider/fuli_graph/project_agent_access.py +54 -0
- package/graph-provider/fuli_graph/project_agent_executor_models.py +635 -0
- package/graph-provider/fuli_graph/project_agent_models.py +278 -0
- package/graph-provider/fuli_graph/project_agent_routes.py +597 -0
- package/graph-provider/fuli_graph/project_agent_task_models.py +542 -0
- package/graph-provider/fuli_graph/runtime.py +259 -0
- package/graph-provider/fuli_graph/search_projection.py +3 -0
- package/graph-provider/fuli_graph/store.py +6 -0
- package/graph-provider/fuli_graph/store_knowledge.py +62 -9
- package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +756 -0
- package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +601 -0
- package/graph-provider/fuli_graph/store_project_agent_executors.py +1087 -0
- package/graph-provider/fuli_graph/store_project_agent_task_reads.py +458 -0
- package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +699 -0
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +409 -0
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +1144 -0
- package/graph-provider/fuli_graph/store_project_agents.py +965 -0
- package/graph-provider/pyproject.toml +2 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/capturing-session-knowledge/SKILL.md +39 -0
- package/src/adaptive-runtime/executor-pool.js +127 -0
- package/src/adaptive-runtime/graph-services.js +166 -0
- package/src/adaptive-runtime/lease-client.js +335 -0
- package/src/adaptive-runtime/runtime-broker.js +318 -0
- package/src/adaptive-runtime/settings.js +111 -0
- package/src/adaptive-runtime/state.js +74 -0
- package/src/agent-tools/graph-definitions.js +396 -0
- package/src/agent-tools/graph-handlers.js +61 -0
- package/src/agent-tools/project-agent-definitions.js +307 -0
- package/src/backup/graph-backup.js +289 -0
- package/src/backup/identity-reconciliation.js +208 -0
- package/src/backup/runtime-adapters.js +269 -0
- package/src/cli/command-registry.js +6 -2
- package/src/cli/graph-data-command.js +112 -0
- package/src/cli/local-runtime-command.js +16 -2
- package/src/cli/setup-command.js +19 -3
- package/src/cli/update-command.js +4 -0
- package/src/cli.js +6 -0
- package/src/graphiti/collaboration-preference-projection.js +29 -1
- package/src/graphiti/collaboration-preference-workflow.js +121 -0
- package/src/graphiti/federated-application.js +53 -90
- package/src/graphiti/project-agent-control-plane.js +263 -0
- package/src/graphiti/project-agent-mapping.js +1003 -0
- package/src/graphiti/project-agent-workflows.js +557 -0
- package/src/graphiti/provider-client.js +270 -1
- package/src/http/api-router.js +63 -9
- package/src/http/graph-api-router.js +278 -0
- package/src/local-runtime/lifecycle.js +46 -10
- package/src/mcp/context-resources.js +15 -6
- package/src/mcp/create-mcp-server.js +51 -16
- package/src/mcp/instructions.js +7 -5
- package/src/mcp/runtime.js +17 -5
- package/src/mcp/session-id.js +38 -1
- package/src/mcp/tool-annotations.js +69 -0
- package/src/mcp-server.js +23 -6
- package/src/native-runtime/runtime.js +717 -0
- package/src/server.js +6 -2
- package/src/setup/agent-installation-status.js +7 -1
- package/src/setup/agents.js +3 -1
- package/src/setup/claude-code-config.js +3 -1
- package/src/setup/codex-bootstrap.js +11 -10
- package/src/setup/codex-config.js +7 -1
- package/src/setup/cursor-config.js +3 -1
- package/src/setup/graph-runtime.js +158 -13
- package/src/setup/neo4j-memory-profile.js +43 -0
- package/src/setup/options.js +38 -2
- package/src/setup/paths.js +14 -0
- package/src/setup/setup.js +37 -1
- package/src/system/resource-monitor.js +181 -16
- package/src/system/runtime-settings.js +10 -1
- package/src/system/system-service.js +27 -2
- package/dist/web/assets/AboutPage-Dpf5fRWv.js +0 -1
- package/dist/web/assets/ConnectionsPage-D21aUDCP.js +0 -1
- package/dist/web/assets/KnowledgeEditDialog-u7lpfN3j.js +0 -1
- package/dist/web/assets/KnowledgeInspector-k4xnHtXd.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-Bdj7Rn84.js +0 -1
- package/dist/web/assets/KnowledgePage-UHVAbC1W.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-Lq3d-MeC.js +0 -4
- package/dist/web/assets/OverviewPage-CycAgA1V.js +0 -1
- package/dist/web/assets/PersonalProfilePage-U9oIV5FT.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-C5RpXKcA.js +0 -1
- package/dist/web/assets/PublicProjectsPage-DiWxm35E.js +0 -1
- package/dist/web/assets/ReviewPage-jxRfGSJK.js +0 -1
- package/dist/web/assets/SearchableSelect-B3-5IPup.js +0 -1
- package/dist/web/assets/SettingsPage-BqfGDOho.js +0 -1
- package/dist/web/assets/SettingsPage-CAfz4qiV.css +0 -1
- package/dist/web/assets/WritingTastePage-BJxhMqJR.js +0 -1
- package/dist/web/assets/i18n-CwqXDI1C.js +0 -5
- package/dist/web/assets/index-IUGLt4lG.js +0 -2
- package/dist/web/assets/index-X2j29j8i.css +0 -1
- package/dist/web/assets/vue-router-CWSsUUio.js +0 -1
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"""Agent assignment selection, history continuity, and parallel staffing."""
|
|
2
|
+
|
|
3
|
+
from fastapi import HTTPException
|
|
4
|
+
|
|
5
|
+
from .project_agent_access import authorize_project_agent
|
|
6
|
+
from .project_agent_models import ProjectAgentProfile
|
|
7
|
+
from .project_agent_task_models import ProjectAgentParallelPlan
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StoreProjectAgentTaskStaffing:
|
|
11
|
+
"""Keep task routing's staffing mechanics separate from task persistence."""
|
|
12
|
+
|
|
13
|
+
async def _select_agents(self, actor, space, request):
|
|
14
|
+
if request.staffing_intent == 'unassigned':
|
|
15
|
+
return [], [], 'explicit_unassigned', ['user explicitly left task unassigned']
|
|
16
|
+
assignment_rows = await self._assignment_candidates(
|
|
17
|
+
request.personal_space_id,
|
|
18
|
+
request.personal_project_id,
|
|
19
|
+
)
|
|
20
|
+
if request.lead_agent_id:
|
|
21
|
+
await authorize_project_agent(
|
|
22
|
+
self,
|
|
23
|
+
actor,
|
|
24
|
+
space,
|
|
25
|
+
request.personal_project_id,
|
|
26
|
+
request.lead_agent_id,
|
|
27
|
+
require_active=True,
|
|
28
|
+
)
|
|
29
|
+
matched = [
|
|
30
|
+
row for row in assignment_rows
|
|
31
|
+
if row['agent_id'] == request.lead_agent_id
|
|
32
|
+
]
|
|
33
|
+
if not matched:
|
|
34
|
+
raise HTTPException(status_code=409, detail='lead Agent assignment is inactive')
|
|
35
|
+
self._require_agent_client_allowed(
|
|
36
|
+
matched[0],
|
|
37
|
+
request.source_application,
|
|
38
|
+
)
|
|
39
|
+
return matched[:1], matched, 'explicit_agent', ['explicit Agent selected']
|
|
40
|
+
rows = [
|
|
41
|
+
row for row in assignment_rows
|
|
42
|
+
if self._agent_client_allowed(row, request.source_application)
|
|
43
|
+
]
|
|
44
|
+
if request.staffing_intent in {'new_durable', 'temporary'}:
|
|
45
|
+
return (
|
|
46
|
+
[],
|
|
47
|
+
rows,
|
|
48
|
+
'explicit_temporary_agent'
|
|
49
|
+
if request.staffing_intent == 'temporary'
|
|
50
|
+
else 'explicit_new_agent',
|
|
51
|
+
['user explicitly requested recruitment'],
|
|
52
|
+
)
|
|
53
|
+
normalized_work_kind = request.work_kind.casefold()
|
|
54
|
+
required = {item.casefold() for item in request.required_capabilities}
|
|
55
|
+
candidates, history = await self._rank_agent_candidates(rows, request)
|
|
56
|
+
if not candidates:
|
|
57
|
+
unavailable_match = any(
|
|
58
|
+
not self._agent_client_allowed(row, request.source_application)
|
|
59
|
+
and (
|
|
60
|
+
normalized_work_kind in {
|
|
61
|
+
item.casefold() for item in row.get('work_kinds', [])
|
|
62
|
+
}
|
|
63
|
+
or (
|
|
64
|
+
required
|
|
65
|
+
and required.issubset({
|
|
66
|
+
item.casefold()
|
|
67
|
+
for item in row.get('capabilities', [])
|
|
68
|
+
})
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
for row in assignment_rows
|
|
72
|
+
)
|
|
73
|
+
if unavailable_match:
|
|
74
|
+
return [], rows, 'agent_unavailable', [
|
|
75
|
+
'matching Agent is unavailable to source client: '
|
|
76
|
+
f'{request.source_application or "other"}',
|
|
77
|
+
]
|
|
78
|
+
return [], rows, 'no_match', ['no active assignment matched exactly']
|
|
79
|
+
selected = candidates[0]
|
|
80
|
+
if normalized_work_kind in {
|
|
81
|
+
item.casefold() for item in selected.get('work_kinds', [])
|
|
82
|
+
}:
|
|
83
|
+
match_basis = [
|
|
84
|
+
f'exact work kind: {request.work_kind}',
|
|
85
|
+
]
|
|
86
|
+
reason = 'exact_work_kind'
|
|
87
|
+
else:
|
|
88
|
+
match_basis = ['all required capabilities matched exactly']
|
|
89
|
+
reason = 'exact_capability'
|
|
90
|
+
history_basis = self._history_match_basis(
|
|
91
|
+
history.get(selected['agent_id']),
|
|
92
|
+
request.work_kind,
|
|
93
|
+
)
|
|
94
|
+
if history_basis:
|
|
95
|
+
match_basis.append(history_basis)
|
|
96
|
+
return [selected], candidates, reason, match_basis
|
|
97
|
+
|
|
98
|
+
async def _rank_agent_candidates(self, rows, request):
|
|
99
|
+
"""Rank already source-eligible assignments with task-history continuity."""
|
|
100
|
+
|
|
101
|
+
normalized_work_kind = request.work_kind.casefold()
|
|
102
|
+
required = {item.casefold() for item in request.required_capabilities}
|
|
103
|
+
scored = []
|
|
104
|
+
for row in rows:
|
|
105
|
+
work_kinds = {item.casefold() for item in row.get('work_kinds', [])}
|
|
106
|
+
capabilities = {item.casefold() for item in row.get('capabilities', [])}
|
|
107
|
+
work_match = normalized_work_kind in work_kinds
|
|
108
|
+
capability_match = required.issubset(capabilities)
|
|
109
|
+
if not work_match and not (required and capability_match):
|
|
110
|
+
continue
|
|
111
|
+
score = (2 if work_match else 0) + (1 if capability_match else 0)
|
|
112
|
+
scored.append((score, row))
|
|
113
|
+
if not scored:
|
|
114
|
+
return [], {}
|
|
115
|
+
history = await self._historical_agent_outcomes(
|
|
116
|
+
request.personal_space_id,
|
|
117
|
+
request.personal_project_id,
|
|
118
|
+
request.work_kind,
|
|
119
|
+
[row['agent_id'] for _, row in scored],
|
|
120
|
+
)
|
|
121
|
+
scored.sort(
|
|
122
|
+
key=lambda item: (
|
|
123
|
+
-item[0],
|
|
124
|
+
*self._history_sort_key(history.get(item[1]['agent_id'])),
|
|
125
|
+
item[1]['assigned_at'],
|
|
126
|
+
item[1]['agent_id'],
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
return [item[1] for item in scored], history
|
|
130
|
+
|
|
131
|
+
async def _historical_agent_outcomes(
|
|
132
|
+
self,
|
|
133
|
+
personal_space_id,
|
|
134
|
+
personal_project_id,
|
|
135
|
+
work_kind,
|
|
136
|
+
agent_ids,
|
|
137
|
+
):
|
|
138
|
+
"""Read project/work-kind participation without making history mandatory."""
|
|
139
|
+
|
|
140
|
+
if not agent_ids:
|
|
141
|
+
return {}
|
|
142
|
+
runtime = getattr(self, 'runtime', None)
|
|
143
|
+
driver = getattr(runtime, 'driver', None)
|
|
144
|
+
if driver is None:
|
|
145
|
+
return {}
|
|
146
|
+
records, _, _ = await driver.execute_query(
|
|
147
|
+
'''
|
|
148
|
+
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
149
|
+
[:HAS_PROJECT_AGENT_TASK]->
|
|
150
|
+
(task:FuliProjectAgentTask {
|
|
151
|
+
personal_space_id: $personal_space_id,
|
|
152
|
+
personal_project_id: $personal_project_id
|
|
153
|
+
})-[participant:HAS_PARTICIPANT]->
|
|
154
|
+
(agent:FuliProjectAgent)
|
|
155
|
+
WHERE agent.agent_id IN $agent_ids
|
|
156
|
+
AND toLower(task.work_kind) = toLower($work_kind)
|
|
157
|
+
OPTIONAL MATCH (task)-[:HAS_TASK_EVENT]->
|
|
158
|
+
(event:FuliProjectAgentTaskEvent)-[:EVENT_AGENT]->(agent)
|
|
159
|
+
WHERE event.status IN ['completed', 'failed', 'cancelled']
|
|
160
|
+
RETURN agent.agent_id AS agent_id,
|
|
161
|
+
count(DISTINCT task.task_id) AS participation_count,
|
|
162
|
+
count(DISTINCT CASE
|
|
163
|
+
WHEN participant.status = 'completed'
|
|
164
|
+
OR event.status = 'completed'
|
|
165
|
+
THEN task.task_id END
|
|
166
|
+
) AS completed_count,
|
|
167
|
+
count(DISTINCT CASE
|
|
168
|
+
WHEN participant.status = 'failed'
|
|
169
|
+
OR event.status = 'failed'
|
|
170
|
+
THEN task.task_id END
|
|
171
|
+
) AS failed_count,
|
|
172
|
+
count(DISTINCT CASE
|
|
173
|
+
WHEN participant.status = 'cancelled'
|
|
174
|
+
OR event.status = 'cancelled'
|
|
175
|
+
THEN task.task_id END
|
|
176
|
+
) AS cancelled_count,
|
|
177
|
+
max(event.created_at) AS last_outcome_at
|
|
178
|
+
''',
|
|
179
|
+
personal_space_id=personal_space_id,
|
|
180
|
+
personal_project_id=personal_project_id,
|
|
181
|
+
work_kind=work_kind,
|
|
182
|
+
agent_ids=list(agent_ids),
|
|
183
|
+
routing_='r',
|
|
184
|
+
)
|
|
185
|
+
result = {}
|
|
186
|
+
for row in records or []:
|
|
187
|
+
raw = dict(row)
|
|
188
|
+
agent_id = raw.get('agent_id')
|
|
189
|
+
if not agent_id:
|
|
190
|
+
continue
|
|
191
|
+
result[agent_id] = {
|
|
192
|
+
'participation_count': self._metric_int(
|
|
193
|
+
raw.get('participation_count')
|
|
194
|
+
),
|
|
195
|
+
'completed_count': self._metric_int(raw.get('completed_count')),
|
|
196
|
+
'failed_count': self._metric_int(raw.get('failed_count')),
|
|
197
|
+
'cancelled_count': self._metric_int(
|
|
198
|
+
raw.get('cancelled_count')
|
|
199
|
+
),
|
|
200
|
+
'last_outcome_at': raw.get('last_outcome_at'),
|
|
201
|
+
}
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def _metric_int(value):
|
|
206
|
+
try:
|
|
207
|
+
return max(0, int(value or 0))
|
|
208
|
+
except (TypeError, ValueError):
|
|
209
|
+
return 0
|
|
210
|
+
|
|
211
|
+
@classmethod
|
|
212
|
+
def _history_sort_key(cls, history):
|
|
213
|
+
history = history or {}
|
|
214
|
+
completed = cls._metric_int(history.get('completed_count'))
|
|
215
|
+
failed = cls._metric_int(history.get('failed_count'))
|
|
216
|
+
cancelled = cls._metric_int(history.get('cancelled_count'))
|
|
217
|
+
participation = cls._metric_int(history.get('participation_count'))
|
|
218
|
+
# A successful prior task is the strongest continuity signal. Failed
|
|
219
|
+
# and cancelled outcomes lower the signal, while participation remains
|
|
220
|
+
# a deterministic tie-breaker for otherwise equivalent outcomes.
|
|
221
|
+
outcome_score = completed * 3 - failed * 2 - cancelled
|
|
222
|
+
return (
|
|
223
|
+
-outcome_score,
|
|
224
|
+
-completed,
|
|
225
|
+
failed,
|
|
226
|
+
cancelled,
|
|
227
|
+
-participation,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
@classmethod
|
|
231
|
+
def _history_match_basis(cls, history, work_kind):
|
|
232
|
+
if not history:
|
|
233
|
+
return None
|
|
234
|
+
participation = cls._metric_int(history.get('participation_count'))
|
|
235
|
+
if participation <= 0:
|
|
236
|
+
return None
|
|
237
|
+
completed = cls._metric_int(history.get('completed_count'))
|
|
238
|
+
failed = cls._metric_int(history.get('failed_count'))
|
|
239
|
+
cancelled = cls._metric_int(history.get('cancelled_count'))
|
|
240
|
+
return (
|
|
241
|
+
f'historical continuity for {work_kind}: '
|
|
242
|
+
f'{participation} prior task(s), '
|
|
243
|
+
f'{completed} completed, {failed} failed, {cancelled} cancelled'
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
async def _parallel_staffing_candidates(
|
|
247
|
+
self,
|
|
248
|
+
request,
|
|
249
|
+
candidates,
|
|
250
|
+
participants,
|
|
251
|
+
):
|
|
252
|
+
capacity = len(request.parallel_plan.workstream_boundaries) - len(participants)
|
|
253
|
+
if capacity <= 0:
|
|
254
|
+
return []
|
|
255
|
+
pool = list(candidates)
|
|
256
|
+
known_ids = {item['agent_id'] for item in pool}
|
|
257
|
+
for row in await self._assignment_candidates(
|
|
258
|
+
request.personal_space_id,
|
|
259
|
+
request.personal_project_id,
|
|
260
|
+
):
|
|
261
|
+
if row['agent_id'] not in known_ids:
|
|
262
|
+
pool.append(row)
|
|
263
|
+
known_ids.add(row['agent_id'])
|
|
264
|
+
eligible = [
|
|
265
|
+
row
|
|
266
|
+
for row in pool
|
|
267
|
+
if self._agent_client_allowed(row, request.source_application)
|
|
268
|
+
]
|
|
269
|
+
ranked, _ = await self._rank_agent_candidates(eligible, request)
|
|
270
|
+
participant_ids = {item['agent_id'] for item in participants}
|
|
271
|
+
return [
|
|
272
|
+
row
|
|
273
|
+
for row in ranked
|
|
274
|
+
if row['agent_id'] not in participant_ids
|
|
275
|
+
][:capacity]
|
|
276
|
+
|
|
277
|
+
@staticmethod
|
|
278
|
+
def _add_parallel_collaborators(plan, participants, candidates):
|
|
279
|
+
if not plan.enabled:
|
|
280
|
+
return []
|
|
281
|
+
limit = len(plan.workstream_boundaries)
|
|
282
|
+
participant_ids = {item['agent_id'] for item in participants}
|
|
283
|
+
added = []
|
|
284
|
+
for candidate in candidates:
|
|
285
|
+
if len(participants) >= limit:
|
|
286
|
+
break
|
|
287
|
+
agent_id = candidate['agent_id']
|
|
288
|
+
if agent_id in participant_ids:
|
|
289
|
+
continue
|
|
290
|
+
participant = {
|
|
291
|
+
'agent_id': agent_id,
|
|
292
|
+
'role': 'collaborator',
|
|
293
|
+
'assignment_summary': candidate.get('responsibility'),
|
|
294
|
+
}
|
|
295
|
+
participants.append(participant)
|
|
296
|
+
participant_ids.add(agent_id)
|
|
297
|
+
added.append(candidate)
|
|
298
|
+
return added
|
|
299
|
+
|
|
300
|
+
async def _assignment_candidates(
|
|
301
|
+
self,
|
|
302
|
+
personal_space_id,
|
|
303
|
+
personal_project_id,
|
|
304
|
+
*,
|
|
305
|
+
include_temporary=False,
|
|
306
|
+
):
|
|
307
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
308
|
+
'''
|
|
309
|
+
MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
310
|
+
[:CONTAINS_PROJECT]->
|
|
311
|
+
(:FuliPersonalProject {project_id: $personal_project_id})-
|
|
312
|
+
[:HAS_PROJECT_AGENT_ASSIGNMENT]->
|
|
313
|
+
(assignment:FuliProjectAgentAssignment {status: 'active'})-
|
|
314
|
+
[:ASSIGNED_AGENT]->(agent:FuliProjectAgent {status: 'active'})
|
|
315
|
+
WHERE agent.agent_type = 'durable'
|
|
316
|
+
OR ($include_temporary = true AND agent.agent_type = 'temporary')
|
|
317
|
+
RETURN assignment, agent
|
|
318
|
+
ORDER BY assignment.assigned_at, agent.agent_id
|
|
319
|
+
''',
|
|
320
|
+
personal_space_id=personal_space_id,
|
|
321
|
+
personal_project_id=personal_project_id,
|
|
322
|
+
include_temporary=include_temporary,
|
|
323
|
+
routing_='r',
|
|
324
|
+
)
|
|
325
|
+
result = []
|
|
326
|
+
for row in records:
|
|
327
|
+
assignment = dict(row['assignment'])
|
|
328
|
+
agent = dict(row['agent'])
|
|
329
|
+
profile = ProjectAgentProfile.model_validate_json(agent['profile_json'])
|
|
330
|
+
result.append({
|
|
331
|
+
'agent_id': agent['agent_id'],
|
|
332
|
+
'assignment_id': assignment.get('assignment_id') or assignment['id'],
|
|
333
|
+
'responsibility': assignment.get('responsibility'),
|
|
334
|
+
'work_kinds': list(
|
|
335
|
+
assignment.get('work_kinds') or profile.work_kinds
|
|
336
|
+
),
|
|
337
|
+
'capabilities': list(
|
|
338
|
+
assignment.get('capabilities') or profile.capabilities
|
|
339
|
+
),
|
|
340
|
+
'model_strategy_override': assignment.get('model_strategy_json'),
|
|
341
|
+
'executor_policy_override': assignment.get(
|
|
342
|
+
'executor_policy_json'
|
|
343
|
+
),
|
|
344
|
+
'profile': profile,
|
|
345
|
+
'assigned_at': str(assignment.get('assigned_at') or ''),
|
|
346
|
+
})
|
|
347
|
+
return result
|
|
348
|
+
|
|
349
|
+
async def _explicit_collaborators(self, actor, space, request):
|
|
350
|
+
if not request.collaborator_agent_ids:
|
|
351
|
+
return []
|
|
352
|
+
rows = await self._assignment_candidates(
|
|
353
|
+
request.personal_space_id,
|
|
354
|
+
request.personal_project_id,
|
|
355
|
+
)
|
|
356
|
+
result = []
|
|
357
|
+
for agent_id in request.collaborator_agent_ids:
|
|
358
|
+
await authorize_project_agent(
|
|
359
|
+
self,
|
|
360
|
+
actor,
|
|
361
|
+
space,
|
|
362
|
+
request.personal_project_id,
|
|
363
|
+
agent_id,
|
|
364
|
+
require_active=True,
|
|
365
|
+
)
|
|
366
|
+
row = next((item for item in rows if item['agent_id'] == agent_id), None)
|
|
367
|
+
if not row:
|
|
368
|
+
raise HTTPException(
|
|
369
|
+
status_code=409,
|
|
370
|
+
detail=f'collaborator Agent assignment is inactive: {agent_id}',
|
|
371
|
+
)
|
|
372
|
+
self._require_agent_client_allowed(
|
|
373
|
+
row,
|
|
374
|
+
request.source_application,
|
|
375
|
+
)
|
|
376
|
+
result.append(row)
|
|
377
|
+
return result
|
|
378
|
+
|
|
379
|
+
@staticmethod
|
|
380
|
+
def _agent_client_allowed(candidate, source_application):
|
|
381
|
+
profile = candidate.get('profile') if isinstance(candidate, dict) else candidate
|
|
382
|
+
allowed_clients = set(profile.allowed_clients)
|
|
383
|
+
return (source_application or 'other') in allowed_clients
|
|
384
|
+
|
|
385
|
+
@classmethod
|
|
386
|
+
def _require_agent_client_allowed(cls, candidate, source_application):
|
|
387
|
+
if cls._agent_client_allowed(candidate, source_application):
|
|
388
|
+
return
|
|
389
|
+
agent_id = candidate.get('agent_id') if isinstance(candidate, dict) else None
|
|
390
|
+
raise HTTPException(
|
|
391
|
+
status_code=403,
|
|
392
|
+
detail=(
|
|
393
|
+
f'project Agent is not available to this source client: '
|
|
394
|
+
f'{agent_id or "selected Agent"}'
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
@staticmethod
|
|
399
|
+
def _verify_parallel_plan(plan: ProjectAgentParallelPlan, participants: list[dict]):
|
|
400
|
+
if plan.enabled and len(participants) > len(plan.workstream_boundaries):
|
|
401
|
+
raise HTTPException(
|
|
402
|
+
status_code=422,
|
|
403
|
+
detail='parallel work exceeds declared workstream boundaries',
|
|
404
|
+
)
|
|
405
|
+
if plan.enabled and len(participants) < 2:
|
|
406
|
+
raise HTTPException(
|
|
407
|
+
status_code=422,
|
|
408
|
+
detail='parallel work requires at least two active Agents',
|
|
409
|
+
)
|