fuli-context 0.7.4-beta.3 → 0.7.6
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-sLV9WChi.js +1 -0
- package/dist/web/assets/ConnectionsPage-BWn4ywGJ.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-CZ5ziPrD.js +1 -0
- package/dist/web/assets/KnowledgeInspector-CRQWOaos.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-Dfj3vIpU.js +1 -0
- package/dist/web/assets/KnowledgePage-BRYC9S5Y.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-ChEB5Bwk.js +4 -0
- package/dist/web/assets/OverviewPage-Cd66M0RT.js +1 -0
- package/dist/web/assets/PersonalProfilePage-RCFHL0Wz.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-DEh8XWby.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-Crh7PrXa.css +1 -0
- package/dist/web/assets/ProjectAgentsPage-DPnaH-TR.js +11 -0
- package/dist/web/assets/PublicProjectsPage-mstPEjHe.js +1 -0
- package/dist/web/assets/ReviewPage-6Y1SmfZy.js +1 -0
- package/dist/web/assets/SearchableSelect-D3HM8YF8.js +1 -0
- package/dist/web/assets/SettingsPage--LdTMUm7.js +1 -0
- package/dist/web/assets/SettingsPage-CwZp2ka4.css +1 -0
- package/dist/web/assets/WritingTastePage-CIpKswvs.js +1 -0
- package/dist/web/assets/{console-D_2zC4lm.js → console-BT1P2Db4.js} +1 -1
- package/dist/web/assets/{conversation-launcher-settings-DQmMH2j7.js → conversation-launcher-settings-DrJEotdf.js} +1 -1
- package/dist/web/assets/i18n-js6gfc7j.js +9 -0
- package/dist/web/assets/{identity-DmGcY3Y6.js → identity-CfDfanpa.js} +1 -1
- package/dist/web/assets/index-BnH5mcZx.css +1 -0
- package/dist/web/assets/index-GqJf1xqg.js +2 -0
- package/dist/web/assets/{model-CVoOdRsL.js → model-DJkLAJsg.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-CBNg4CdH.js → useApi-CROJJdhE-BdSti7Y2.js} +1 -1
- package/dist/web/assets/vue-router-Kk67EaCP.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_coordination_models.py +22 -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 +626 -0
- package/graph-provider/fuli_graph/project_agent_task_models.py +545 -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_coordination_policy.py +122 -0
- 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 +705 -0
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +586 -0
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +1151 -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 +419 -0
- package/src/agent-tools/graph-handlers.js +65 -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 +275 -0
- package/src/graphiti/project-agent-mapping.js +1024 -0
- package/src/graphiti/project-agent-workflows.js +576 -0
- package/src/graphiti/provider-client.js +282 -1
- package/src/http/api-router.js +63 -9
- package/src/http/graph-api-router.js +290 -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 +71 -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,1151 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from datetime import date
|
|
4
|
+
|
|
5
|
+
from fastapi import HTTPException
|
|
6
|
+
|
|
7
|
+
from .personal_project_access import authorize_personal_project
|
|
8
|
+
from .project_agent_access import authorize_project_agent
|
|
9
|
+
from .project_agent_executor_models import (
|
|
10
|
+
ProjectAgentExecutorActualReport,
|
|
11
|
+
ProjectAgentExecutorOutcomeEvidenceCreate,
|
|
12
|
+
)
|
|
13
|
+
from .project_agent_models import (
|
|
14
|
+
ProjectAgentModelStrategy,
|
|
15
|
+
ProjectAgentProfile,
|
|
16
|
+
)
|
|
17
|
+
from .project_agent_task_models import (
|
|
18
|
+
ProjectAgentActivityDay,
|
|
19
|
+
ProjectAgentActivityResult,
|
|
20
|
+
ProjectAgentActivityTask,
|
|
21
|
+
ProjectAgentRoutingDecisionRecord,
|
|
22
|
+
ProjectAgentTaskActivityCreate,
|
|
23
|
+
ProjectAgentTaskRecord,
|
|
24
|
+
ProjectAgentTaskRouteResult,
|
|
25
|
+
ProjectAgentTaskSubmit,
|
|
26
|
+
)
|
|
27
|
+
from .provider_values import native_datetime, now_utc, stable_uuid
|
|
28
|
+
from .store_project_agents import SYSTEM_COORDINATOR_AGENT_ID
|
|
29
|
+
from .store_project_agent_task_reads import StoreProjectAgentTaskReads
|
|
30
|
+
from .store_project_agent_task_recruitment import StoreProjectAgentTaskRecruitment
|
|
31
|
+
from .store_project_agent_task_staffing import StoreProjectAgentTaskStaffing
|
|
32
|
+
from .store_project_agent_coordination_policy import (
|
|
33
|
+
StoreProjectAgentCoordinationPolicy,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
TERMINAL_TASK_STATUSES = {'completed', 'failed', 'cancelled'}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class StoreProjectAgentTasks(
|
|
41
|
+
StoreProjectAgentCoordinationPolicy,
|
|
42
|
+
StoreProjectAgentTaskRecruitment,
|
|
43
|
+
StoreProjectAgentTaskReads,
|
|
44
|
+
# Keep the pre-existing base order ahead of the extracted implementation.
|
|
45
|
+
StoreProjectAgentTaskStaffing,
|
|
46
|
+
):
|
|
47
|
+
"""Durable task, routing, recruitment, and verified activity control plane.
|
|
48
|
+
|
|
49
|
+
This is deliberately not a worker or scheduler. Registered clients submit
|
|
50
|
+
tasks and append truthful state events to the same persistent Agent/task
|
|
51
|
+
history. No task is shown as running until a client records that event.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
async def submit_project_agent_task(
|
|
55
|
+
self,
|
|
56
|
+
actor: dict,
|
|
57
|
+
request: ProjectAgentTaskSubmit,
|
|
58
|
+
) -> ProjectAgentTaskRouteResult:
|
|
59
|
+
self._require_personal()
|
|
60
|
+
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
61
|
+
await authorize_personal_project(
|
|
62
|
+
self,
|
|
63
|
+
actor,
|
|
64
|
+
space,
|
|
65
|
+
request.personal_project_id,
|
|
66
|
+
)
|
|
67
|
+
task_id = stable_uuid(
|
|
68
|
+
self.settings.provider_id,
|
|
69
|
+
request.personal_space_id,
|
|
70
|
+
'project-agent-task',
|
|
71
|
+
request.idempotency_key,
|
|
72
|
+
)
|
|
73
|
+
payload_hash = self._payload_hash(request)
|
|
74
|
+
existing = await self._find_task_row(request.personal_space_id, task_id)
|
|
75
|
+
if existing:
|
|
76
|
+
raw = dict(existing['task'])
|
|
77
|
+
if raw.get('payload_hash') != payload_hash:
|
|
78
|
+
raise HTTPException(
|
|
79
|
+
status_code=409,
|
|
80
|
+
detail='task idempotency key was used with different input',
|
|
81
|
+
)
|
|
82
|
+
task = self._task_from_row(existing)
|
|
83
|
+
recruitment = await self._task_recruitment(
|
|
84
|
+
request.personal_space_id,
|
|
85
|
+
task.task_id,
|
|
86
|
+
)
|
|
87
|
+
assigned_agent = await self._route_assigned_agent(
|
|
88
|
+
actor,
|
|
89
|
+
request,
|
|
90
|
+
task,
|
|
91
|
+
)
|
|
92
|
+
return self._route_result(
|
|
93
|
+
task,
|
|
94
|
+
recruitment,
|
|
95
|
+
assigned_agent=assigned_agent,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
coordinator = await self.ensure_system_project_coordinator(
|
|
99
|
+
actor,
|
|
100
|
+
request.personal_space_id,
|
|
101
|
+
)
|
|
102
|
+
coordinator_agent_id = request.coordinator_agent_id or coordinator.agent_id
|
|
103
|
+
if request.coordinator_agent_id:
|
|
104
|
+
await authorize_project_agent(
|
|
105
|
+
self,
|
|
106
|
+
actor,
|
|
107
|
+
space,
|
|
108
|
+
request.personal_project_id,
|
|
109
|
+
request.coordinator_agent_id,
|
|
110
|
+
require_active=True,
|
|
111
|
+
allow_unassigned=True,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
complexity, complexity_basis = self._assess_complexity(request)
|
|
115
|
+
selected, candidates, reason, match_basis = await self._select_agents(
|
|
116
|
+
actor,
|
|
117
|
+
space,
|
|
118
|
+
request,
|
|
119
|
+
)
|
|
120
|
+
lead = selected[0] if selected else None
|
|
121
|
+
participants = []
|
|
122
|
+
if lead:
|
|
123
|
+
participants.append({
|
|
124
|
+
'agent_id': lead['agent_id'],
|
|
125
|
+
'role': 'lead',
|
|
126
|
+
'assignment_summary': lead.get('responsibility'),
|
|
127
|
+
})
|
|
128
|
+
for collaborator in await self._explicit_collaborators(actor, space, request):
|
|
129
|
+
if not any(item['agent_id'] == collaborator['agent_id'] for item in participants):
|
|
130
|
+
participants.append({
|
|
131
|
+
'agent_id': collaborator['agent_id'],
|
|
132
|
+
'role': 'collaborator',
|
|
133
|
+
'assignment_summary': collaborator.get('responsibility'),
|
|
134
|
+
})
|
|
135
|
+
parallel_candidates = []
|
|
136
|
+
auto_collaborators = []
|
|
137
|
+
if (
|
|
138
|
+
request.parallel_plan.enabled
|
|
139
|
+
and request.staffing_intent != 'unassigned'
|
|
140
|
+
):
|
|
141
|
+
parallel_candidates = await self._parallel_staffing_candidates(
|
|
142
|
+
request,
|
|
143
|
+
candidates,
|
|
144
|
+
participants,
|
|
145
|
+
)
|
|
146
|
+
candidate_ids = {item['agent_id'] for item in candidates}
|
|
147
|
+
candidates.extend(
|
|
148
|
+
item
|
|
149
|
+
for item in parallel_candidates
|
|
150
|
+
if item['agent_id'] not in candidate_ids
|
|
151
|
+
)
|
|
152
|
+
auto_collaborators = self._add_parallel_collaborators(
|
|
153
|
+
request.parallel_plan,
|
|
154
|
+
participants,
|
|
155
|
+
parallel_candidates,
|
|
156
|
+
)
|
|
157
|
+
if auto_collaborators:
|
|
158
|
+
match_basis = list(match_basis)
|
|
159
|
+
match_basis.append(
|
|
160
|
+
'parallel auto-staffing: added '
|
|
161
|
+
f'{len(auto_collaborators)} collaborator(s) from '
|
|
162
|
+
'qualified durable Agent candidates'
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
model_strategy, model_source = self._effective_model_strategy(
|
|
166
|
+
request,
|
|
167
|
+
lead,
|
|
168
|
+
coordinator.profile.default_model_strategy,
|
|
169
|
+
)
|
|
170
|
+
recruitment = None
|
|
171
|
+
routing_outcome = 'assigned_existing' if lead else 'unassigned'
|
|
172
|
+
task_status = 'queued' if lead else 'blocked'
|
|
173
|
+
if request.staffing_intent == 'unassigned':
|
|
174
|
+
reason = 'explicit_unassigned'
|
|
175
|
+
routing_outcome = 'unassigned'
|
|
176
|
+
task_status = 'blocked'
|
|
177
|
+
elif reason == 'manual_agent_selection':
|
|
178
|
+
routing_outcome = 'unassigned'
|
|
179
|
+
task_status = 'blocked'
|
|
180
|
+
elif not lead or request.staffing_intent in {'new_durable', 'temporary'}:
|
|
181
|
+
recruitment, recruited = await self._open_recruitment(
|
|
182
|
+
actor,
|
|
183
|
+
space,
|
|
184
|
+
request,
|
|
185
|
+
task_id,
|
|
186
|
+
coordinator_agent_id,
|
|
187
|
+
reason,
|
|
188
|
+
)
|
|
189
|
+
if recruited:
|
|
190
|
+
lead = recruited
|
|
191
|
+
if not any(
|
|
192
|
+
item['agent_id'] == recruited['agent_id']
|
|
193
|
+
for item in participants
|
|
194
|
+
):
|
|
195
|
+
participants.insert(0, {
|
|
196
|
+
'agent_id': recruited['agent_id'],
|
|
197
|
+
'role': 'lead',
|
|
198
|
+
'assignment_summary': recruited.get('responsibility'),
|
|
199
|
+
})
|
|
200
|
+
routing_outcome = 'recruited'
|
|
201
|
+
task_status = 'queued'
|
|
202
|
+
reason = (
|
|
203
|
+
'explicit_temporary_agent'
|
|
204
|
+
if request.staffing_intent == 'temporary'
|
|
205
|
+
else 'explicit_new_agent'
|
|
206
|
+
if request.staffing_intent == 'new_durable'
|
|
207
|
+
else 'no_match'
|
|
208
|
+
)
|
|
209
|
+
match_basis = [
|
|
210
|
+
'HR recruitment recorded before Agent creation',
|
|
211
|
+
*(
|
|
212
|
+
[
|
|
213
|
+
'parallel auto-staffing: added '
|
|
214
|
+
f'{len(auto_collaborators)} collaborator(s) from '
|
|
215
|
+
'qualified durable Agent candidates'
|
|
216
|
+
]
|
|
217
|
+
if auto_collaborators
|
|
218
|
+
else []
|
|
219
|
+
),
|
|
220
|
+
]
|
|
221
|
+
model_strategy, model_source = self._effective_model_strategy(
|
|
222
|
+
request,
|
|
223
|
+
recruited,
|
|
224
|
+
coordinator.profile.default_model_strategy,
|
|
225
|
+
)
|
|
226
|
+
elif recruitment and recruitment.status == 'awaiting_confirmation':
|
|
227
|
+
routing_outcome = 'recruitment_required'
|
|
228
|
+
task_status = 'awaiting_recruitment'
|
|
229
|
+
else:
|
|
230
|
+
routing_outcome = 'blocked'
|
|
231
|
+
task_status = 'blocked'
|
|
232
|
+
reason = 'hr_unavailable'
|
|
233
|
+
self._verify_parallel_plan(request.parallel_plan, participants)
|
|
234
|
+
|
|
235
|
+
executor_decision = await self._resolve_executor_if_available(
|
|
236
|
+
actor,
|
|
237
|
+
request,
|
|
238
|
+
lead,
|
|
239
|
+
model_strategy,
|
|
240
|
+
model_source,
|
|
241
|
+
task_id,
|
|
242
|
+
)
|
|
243
|
+
if lead and executor_decision and self._executor_decision_blocked(executor_decision):
|
|
244
|
+
routing_outcome = 'blocked'
|
|
245
|
+
task_status = 'blocked'
|
|
246
|
+
|
|
247
|
+
decision_id = stable_uuid(
|
|
248
|
+
self.settings.provider_id,
|
|
249
|
+
request.personal_space_id,
|
|
250
|
+
'project-agent-routing-decision',
|
|
251
|
+
task_id,
|
|
252
|
+
)
|
|
253
|
+
now = now_utc()
|
|
254
|
+
decision = ProjectAgentRoutingDecisionRecord(
|
|
255
|
+
decision_id=decision_id,
|
|
256
|
+
task_id=task_id,
|
|
257
|
+
coordinator_agent_id=coordinator_agent_id,
|
|
258
|
+
complexity=complexity,
|
|
259
|
+
complexity_basis=complexity_basis,
|
|
260
|
+
selected_model_strategy=model_strategy,
|
|
261
|
+
model_strategy_source=model_source,
|
|
262
|
+
outcome=routing_outcome,
|
|
263
|
+
reason=reason,
|
|
264
|
+
match_basis=match_basis,
|
|
265
|
+
candidate_agent_ids=[item['agent_id'] for item in candidates],
|
|
266
|
+
parallel_plan=request.parallel_plan,
|
|
267
|
+
created_at=now,
|
|
268
|
+
**self._decision_executor_fields(executor_decision),
|
|
269
|
+
)
|
|
270
|
+
routing_explanation = self._routing_explanation(
|
|
271
|
+
reason,
|
|
272
|
+
match_basis,
|
|
273
|
+
executor_decision,
|
|
274
|
+
)
|
|
275
|
+
await self._persist_task(
|
|
276
|
+
request,
|
|
277
|
+
task_id=task_id,
|
|
278
|
+
payload_hash=payload_hash,
|
|
279
|
+
coordinator_agent_id=coordinator_agent_id,
|
|
280
|
+
lead_agent_id=lead['agent_id'] if lead else None,
|
|
281
|
+
status=task_status,
|
|
282
|
+
routing_outcome=routing_outcome,
|
|
283
|
+
routing_reason=reason,
|
|
284
|
+
routing_explanation=routing_explanation,
|
|
285
|
+
match_basis=match_basis,
|
|
286
|
+
complexity=complexity,
|
|
287
|
+
complexity_basis=complexity_basis,
|
|
288
|
+
model_strategy=model_strategy,
|
|
289
|
+
model_source=model_source,
|
|
290
|
+
participants=participants,
|
|
291
|
+
decision=decision,
|
|
292
|
+
recruitment=recruitment,
|
|
293
|
+
executor_decision=executor_decision,
|
|
294
|
+
created_at=now,
|
|
295
|
+
)
|
|
296
|
+
task = await self.get_project_agent_task(
|
|
297
|
+
actor,
|
|
298
|
+
request.personal_space_id,
|
|
299
|
+
task_id,
|
|
300
|
+
)
|
|
301
|
+
assigned_agent = await self._route_assigned_agent(
|
|
302
|
+
actor,
|
|
303
|
+
request,
|
|
304
|
+
task,
|
|
305
|
+
)
|
|
306
|
+
return self._route_result(
|
|
307
|
+
task,
|
|
308
|
+
recruitment,
|
|
309
|
+
assigned_agent=assigned_agent,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
async def get_project_agent_task(
|
|
313
|
+
self,
|
|
314
|
+
actor: dict,
|
|
315
|
+
personal_space_id: str,
|
|
316
|
+
task_id: str,
|
|
317
|
+
) -> ProjectAgentTaskRecord:
|
|
318
|
+
self._require_personal()
|
|
319
|
+
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
320
|
+
row = await self._find_task_row(personal_space_id, task_id)
|
|
321
|
+
if not row:
|
|
322
|
+
raise HTTPException(status_code=404, detail='project Agent task not found')
|
|
323
|
+
task = dict(row['task'])
|
|
324
|
+
await authorize_personal_project(
|
|
325
|
+
self,
|
|
326
|
+
actor,
|
|
327
|
+
space,
|
|
328
|
+
task['personal_project_id'],
|
|
329
|
+
)
|
|
330
|
+
return self._task_from_row(row)
|
|
331
|
+
|
|
332
|
+
async def list_project_agent_tasks(
|
|
333
|
+
self,
|
|
334
|
+
actor: dict,
|
|
335
|
+
personal_space_id: str,
|
|
336
|
+
*,
|
|
337
|
+
personal_project_id: str | None = None,
|
|
338
|
+
agent_id: str | None = None,
|
|
339
|
+
status: str | None = None,
|
|
340
|
+
limit: int = 100,
|
|
341
|
+
) -> list[ProjectAgentTaskRecord]:
|
|
342
|
+
self._require_personal()
|
|
343
|
+
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
344
|
+
if personal_project_id:
|
|
345
|
+
await authorize_personal_project(
|
|
346
|
+
self,
|
|
347
|
+
actor,
|
|
348
|
+
space,
|
|
349
|
+
personal_project_id,
|
|
350
|
+
)
|
|
351
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
352
|
+
self._task_read_query(
|
|
353
|
+
'''
|
|
354
|
+
WHERE ($personal_project_id IS NULL
|
|
355
|
+
OR task.personal_project_id = $personal_project_id)
|
|
356
|
+
AND ($status IS NULL OR task.status = $status)
|
|
357
|
+
AND ($agent_id IS NULL OR EXISTS {
|
|
358
|
+
MATCH (task)-[:HAS_PARTICIPANT]->
|
|
359
|
+
(:FuliProjectAgent {agent_id: $agent_id})
|
|
360
|
+
})
|
|
361
|
+
'''
|
|
362
|
+
) + '\nORDER BY task.updated_at DESC, task.task_id LIMIT $limit',
|
|
363
|
+
personal_space_id=personal_space_id,
|
|
364
|
+
personal_project_id=personal_project_id,
|
|
365
|
+
agent_id=agent_id,
|
|
366
|
+
status=status,
|
|
367
|
+
limit=limit,
|
|
368
|
+
routing_='r',
|
|
369
|
+
)
|
|
370
|
+
return [self._task_from_row(row) for row in records]
|
|
371
|
+
|
|
372
|
+
async def record_project_agent_task_activity(
|
|
373
|
+
self,
|
|
374
|
+
actor: dict,
|
|
375
|
+
request: ProjectAgentTaskActivityCreate,
|
|
376
|
+
) -> ProjectAgentTaskRecord:
|
|
377
|
+
self._require_personal()
|
|
378
|
+
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
379
|
+
await authorize_personal_project(
|
|
380
|
+
self,
|
|
381
|
+
actor,
|
|
382
|
+
space,
|
|
383
|
+
request.personal_project_id,
|
|
384
|
+
)
|
|
385
|
+
row = await self._find_task_row(request.personal_space_id, request.task_id)
|
|
386
|
+
if not row:
|
|
387
|
+
raise HTTPException(status_code=404, detail='project Agent task not found')
|
|
388
|
+
raw_task = dict(row['task'])
|
|
389
|
+
if raw_task['personal_project_id'] != request.personal_project_id:
|
|
390
|
+
raise HTTPException(status_code=404, detail='project Agent task not found')
|
|
391
|
+
event_id = stable_uuid(
|
|
392
|
+
self.settings.provider_id,
|
|
393
|
+
request.personal_space_id,
|
|
394
|
+
'project-agent-task-event',
|
|
395
|
+
request.task_id,
|
|
396
|
+
request.idempotency_key,
|
|
397
|
+
)
|
|
398
|
+
payload_hash = self._payload_hash(request)
|
|
399
|
+
existing_event = next(
|
|
400
|
+
(
|
|
401
|
+
dict(value)
|
|
402
|
+
for value in (row.get('event_rows') or [])
|
|
403
|
+
if value
|
|
404
|
+
and (value.get('event_id') or value.get('id')) == event_id
|
|
405
|
+
),
|
|
406
|
+
None,
|
|
407
|
+
)
|
|
408
|
+
if existing_event:
|
|
409
|
+
if existing_event.get('payload_hash') != payload_hash:
|
|
410
|
+
raise HTTPException(
|
|
411
|
+
status_code=409,
|
|
412
|
+
detail=(
|
|
413
|
+
'task event idempotency key was used with different input'
|
|
414
|
+
),
|
|
415
|
+
)
|
|
416
|
+
return self._task_from_row(row)
|
|
417
|
+
self._validate_task_transition(
|
|
418
|
+
raw_task.get('status'),
|
|
419
|
+
request.status,
|
|
420
|
+
has_actual_executor=bool(request.actual_executor_id),
|
|
421
|
+
)
|
|
422
|
+
expected_revision = getattr(request, 'expected_revision', None)
|
|
423
|
+
if expected_revision is None:
|
|
424
|
+
expected_revision = raw_task.get('revision', 0)
|
|
425
|
+
if request.agent_id:
|
|
426
|
+
participant = next(
|
|
427
|
+
(
|
|
428
|
+
item for item in self._participant_maps(row)
|
|
429
|
+
if item['agent_id'] == request.agent_id
|
|
430
|
+
),
|
|
431
|
+
None,
|
|
432
|
+
)
|
|
433
|
+
if not participant:
|
|
434
|
+
raise HTTPException(
|
|
435
|
+
status_code=409,
|
|
436
|
+
detail='Agent is not assigned to this task',
|
|
437
|
+
)
|
|
438
|
+
if participant.get('profile_json'):
|
|
439
|
+
self._require_agent_client_allowed(
|
|
440
|
+
{
|
|
441
|
+
'agent_id': request.agent_id,
|
|
442
|
+
'profile': ProjectAgentProfile.model_validate_json(
|
|
443
|
+
participant['profile_json']
|
|
444
|
+
),
|
|
445
|
+
},
|
|
446
|
+
request.source_application,
|
|
447
|
+
)
|
|
448
|
+
now = now_utc()
|
|
449
|
+
terminal_at = now if request.status in TERMINAL_TASK_STATUSES else None
|
|
450
|
+
actual_executor_id = getattr(request, 'actual_executor_id', None)
|
|
451
|
+
matched_executor_rule_id = getattr(request, 'matched_executor_rule_id', None)
|
|
452
|
+
executor_selection_reason = getattr(request, 'executor_selection_reason', None)
|
|
453
|
+
executor_fallback_reason = getattr(request, 'executor_fallback_reason', None)
|
|
454
|
+
executor_blocked_reason = getattr(request, 'executor_blocked_reason', None)
|
|
455
|
+
worker_id = getattr(request, 'worker_id', None)
|
|
456
|
+
worker_label = getattr(request, 'worker_label', None)
|
|
457
|
+
worker_occupation_emoji = getattr(
|
|
458
|
+
request,
|
|
459
|
+
'worker_occupation_emoji',
|
|
460
|
+
None,
|
|
461
|
+
)
|
|
462
|
+
worker_status = getattr(request, 'worker_status', None)
|
|
463
|
+
actual_recorder = None
|
|
464
|
+
actual_report = None
|
|
465
|
+
if actual_executor_id:
|
|
466
|
+
actual_recorder = getattr(
|
|
467
|
+
self,
|
|
468
|
+
'record_project_agent_executor_actual',
|
|
469
|
+
None,
|
|
470
|
+
)
|
|
471
|
+
if not callable(actual_recorder):
|
|
472
|
+
raise HTTPException(
|
|
473
|
+
status_code=503,
|
|
474
|
+
detail='executor evidence control plane is unavailable',
|
|
475
|
+
)
|
|
476
|
+
strategy = (
|
|
477
|
+
ProjectAgentModelStrategy.model_validate_json(
|
|
478
|
+
raw_task['effective_model_strategy_json']
|
|
479
|
+
)
|
|
480
|
+
if raw_task.get('effective_model_strategy_json')
|
|
481
|
+
else ProjectAgentModelStrategy()
|
|
482
|
+
)
|
|
483
|
+
actual_report = ProjectAgentExecutorActualReport(
|
|
484
|
+
personal_space_id=request.personal_space_id,
|
|
485
|
+
personal_project_id=request.personal_project_id,
|
|
486
|
+
task_id=request.task_id,
|
|
487
|
+
run_id=event_id,
|
|
488
|
+
agent_id=request.agent_id,
|
|
489
|
+
executor_id=actual_executor_id,
|
|
490
|
+
provider=request.actual_model_provider,
|
|
491
|
+
model=request.actual_model,
|
|
492
|
+
model_strategy=strategy,
|
|
493
|
+
model_strategy_source=(
|
|
494
|
+
raw_task.get('model_strategy_source') or 'coordinator'
|
|
495
|
+
),
|
|
496
|
+
matched_rule_id=matched_executor_rule_id,
|
|
497
|
+
fallback_reason=executor_fallback_reason,
|
|
498
|
+
idempotency_key=f'actual:{event_id}',
|
|
499
|
+
occurred_at=now,
|
|
500
|
+
source_application=request.source_application,
|
|
501
|
+
source_session_id=request.source_session_id,
|
|
502
|
+
)
|
|
503
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
504
|
+
'''
|
|
505
|
+
MATCH (space:FuliSpace {
|
|
506
|
+
id: $personal_space_id, kind: 'personal'
|
|
507
|
+
})-[:HAS_PROJECT_AGENT_TASK]->
|
|
508
|
+
(task:FuliProjectAgentTask {
|
|
509
|
+
personal_space_id: $personal_space_id,
|
|
510
|
+
task_id: $task_id
|
|
511
|
+
})
|
|
512
|
+
WHERE task.revision = $expected_revision
|
|
513
|
+
OR EXISTS {
|
|
514
|
+
MATCH (task)-[:HAS_TASK_EVENT]->
|
|
515
|
+
(:FuliProjectAgentTaskEvent {id: $event_id})
|
|
516
|
+
}
|
|
517
|
+
OPTIONAL MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
518
|
+
(agent:FuliProjectAgent {agent_id: $agent_id})
|
|
519
|
+
WHERE $agent_id IS NULL OR EXISTS {
|
|
520
|
+
MATCH (task)-[:HAS_PARTICIPANT]->(agent)
|
|
521
|
+
}
|
|
522
|
+
MERGE (event:FuliProjectAgentTaskEvent {id: $event_id})
|
|
523
|
+
ON CREATE SET event.event_id = $event_id,
|
|
524
|
+
event.task_id = $task_id,
|
|
525
|
+
event.agent_id = $agent_id,
|
|
526
|
+
event.status = $status,
|
|
527
|
+
event.actor_kind = $actor_kind,
|
|
528
|
+
event.summary = $summary,
|
|
529
|
+
event.payload_hash = $payload_hash,
|
|
530
|
+
event.source_application = $source_application,
|
|
531
|
+
event.source_session_id = $source_session_id,
|
|
532
|
+
event.actual_model_provider = $actual_model_provider,
|
|
533
|
+
event.actual_model = $actual_model,
|
|
534
|
+
event.actual_executor_id = $actual_executor_id,
|
|
535
|
+
event.matched_executor_rule_id =
|
|
536
|
+
$matched_executor_rule_id,
|
|
537
|
+
event.executor_selection_reason =
|
|
538
|
+
$executor_selection_reason,
|
|
539
|
+
event.executor_fallback_reason =
|
|
540
|
+
$executor_fallback_reason,
|
|
541
|
+
event.executor_blocked_reason =
|
|
542
|
+
$executor_blocked_reason,
|
|
543
|
+
event.worker_id = $worker_id,
|
|
544
|
+
event.worker_label = $worker_label,
|
|
545
|
+
event.worker_occupation_emoji = $worker_occupation_emoji,
|
|
546
|
+
event.worker_status = $worker_status,
|
|
547
|
+
event.activity_date = CASE
|
|
548
|
+
WHEN $terminal_at IS NULL THEN null
|
|
549
|
+
ELSE date($terminal_at) END,
|
|
550
|
+
event.created_at = $updated_at
|
|
551
|
+
WITH task, event, agent,
|
|
552
|
+
event.payload_hash = $payload_hash AS same_payload,
|
|
553
|
+
task.revision = $expected_revision AS applied_transition
|
|
554
|
+
FOREACH (_ IN CASE
|
|
555
|
+
WHEN same_payload AND applied_transition
|
|
556
|
+
THEN [1] ELSE [] END |
|
|
557
|
+
MERGE (task)-[:HAS_TASK_EVENT]->(event)
|
|
558
|
+
)
|
|
559
|
+
FOREACH (_ IN CASE
|
|
560
|
+
WHEN same_payload AND applied_transition AND agent IS NOT NULL
|
|
561
|
+
THEN [1] ELSE [] END |
|
|
562
|
+
MERGE (event)-[:EVENT_AGENT]->(agent)
|
|
563
|
+
)
|
|
564
|
+
FOREACH (_ IN CASE
|
|
565
|
+
WHEN same_payload AND applied_transition
|
|
566
|
+
THEN [1] ELSE [] END |
|
|
567
|
+
SET task.status = $status,
|
|
568
|
+
task.result_summary = CASE
|
|
569
|
+
WHEN $status = 'completed' THEN $summary
|
|
570
|
+
ELSE task.result_summary END,
|
|
571
|
+
task.failure_reason = CASE
|
|
572
|
+
WHEN $status = 'failed' THEN $summary
|
|
573
|
+
ELSE task.failure_reason END,
|
|
574
|
+
task.actual_executor_id = coalesce(
|
|
575
|
+
$actual_executor_id, task.actual_executor_id),
|
|
576
|
+
task.actual_model_provider = coalesce(
|
|
577
|
+
$actual_model_provider, task.actual_model_provider),
|
|
578
|
+
task.actual_model = coalesce($actual_model, task.actual_model),
|
|
579
|
+
task.matched_executor_rule_id = coalesce(
|
|
580
|
+
$matched_executor_rule_id,
|
|
581
|
+
task.matched_executor_rule_id),
|
|
582
|
+
task.executor_selection_reason = coalesce(
|
|
583
|
+
$executor_selection_reason,
|
|
584
|
+
task.executor_selection_reason),
|
|
585
|
+
task.executor_fallback_reason = coalesce(
|
|
586
|
+
$executor_fallback_reason,
|
|
587
|
+
task.executor_fallback_reason),
|
|
588
|
+
task.executor_blocked_reason = coalesce(
|
|
589
|
+
$executor_blocked_reason,
|
|
590
|
+
task.executor_blocked_reason),
|
|
591
|
+
task.completed_at = $terminal_at,
|
|
592
|
+
task.updated_at = $updated_at,
|
|
593
|
+
task.revision = task.revision + 1
|
|
594
|
+
)
|
|
595
|
+
FOREACH (_ IN CASE
|
|
596
|
+
WHEN same_payload AND applied_transition AND agent IS NOT NULL
|
|
597
|
+
THEN [1] ELSE [] END |
|
|
598
|
+
SET task.lead_agent_id = CASE
|
|
599
|
+
WHEN task.lead_agent_id IS NULL AND $status = 'running'
|
|
600
|
+
THEN agent.agent_id ELSE task.lead_agent_id END
|
|
601
|
+
)
|
|
602
|
+
WITH task, event, same_payload, applied_transition
|
|
603
|
+
OPTIONAL MATCH (task)-[participant:HAS_PARTICIPANT]->
|
|
604
|
+
(participant_agent:FuliProjectAgent)
|
|
605
|
+
FOREACH (_ IN CASE
|
|
606
|
+
WHEN same_payload
|
|
607
|
+
AND applied_transition
|
|
608
|
+
AND participant IS NOT NULL
|
|
609
|
+
AND NOT (coalesce(participant.status, '') IN
|
|
610
|
+
['completed', 'failed', 'cancelled'])
|
|
611
|
+
AND (
|
|
612
|
+
$terminal_at IS NOT NULL
|
|
613
|
+
OR $agent_id IS NULL
|
|
614
|
+
OR participant_agent.agent_id = $agent_id
|
|
615
|
+
)
|
|
616
|
+
THEN [1] ELSE [] END |
|
|
617
|
+
SET participant.status = $status,
|
|
618
|
+
participant.updated_at = $updated_at,
|
|
619
|
+
participant.ended_at = $terminal_at
|
|
620
|
+
)
|
|
621
|
+
RETURN DISTINCT task, event, same_payload, applied_transition
|
|
622
|
+
''',
|
|
623
|
+
personal_space_id=request.personal_space_id,
|
|
624
|
+
task_id=request.task_id,
|
|
625
|
+
expected_revision=expected_revision,
|
|
626
|
+
event_id=event_id,
|
|
627
|
+
agent_id=request.agent_id,
|
|
628
|
+
status=request.status,
|
|
629
|
+
actor_kind=request.actor_kind,
|
|
630
|
+
summary=request.summary,
|
|
631
|
+
payload_hash=payload_hash,
|
|
632
|
+
source_application=request.source_application,
|
|
633
|
+
source_session_id=request.source_session_id,
|
|
634
|
+
actual_model_provider=request.actual_model_provider,
|
|
635
|
+
actual_model=request.actual_model,
|
|
636
|
+
actual_executor_id=actual_executor_id,
|
|
637
|
+
matched_executor_rule_id=matched_executor_rule_id,
|
|
638
|
+
executor_selection_reason=executor_selection_reason,
|
|
639
|
+
executor_fallback_reason=executor_fallback_reason,
|
|
640
|
+
executor_blocked_reason=executor_blocked_reason,
|
|
641
|
+
worker_id=worker_id,
|
|
642
|
+
worker_label=worker_label,
|
|
643
|
+
worker_occupation_emoji=worker_occupation_emoji,
|
|
644
|
+
worker_status=worker_status,
|
|
645
|
+
terminal_at=terminal_at,
|
|
646
|
+
updated_at=now,
|
|
647
|
+
)
|
|
648
|
+
if not records:
|
|
649
|
+
raise HTTPException(
|
|
650
|
+
status_code=409,
|
|
651
|
+
detail='task revision is stale or participant is invalid',
|
|
652
|
+
)
|
|
653
|
+
if not records[0]['same_payload']:
|
|
654
|
+
raise HTTPException(
|
|
655
|
+
status_code=409,
|
|
656
|
+
detail='task event idempotency key was used with different input',
|
|
657
|
+
)
|
|
658
|
+
if not records[0].get('applied_transition'):
|
|
659
|
+
return await self.get_project_agent_task(
|
|
660
|
+
actor,
|
|
661
|
+
request.personal_space_id,
|
|
662
|
+
request.task_id,
|
|
663
|
+
)
|
|
664
|
+
if actual_report:
|
|
665
|
+
await actual_recorder(actor, actual_report)
|
|
666
|
+
evidence_status = (
|
|
667
|
+
request.status
|
|
668
|
+
if request.status in TERMINAL_TASK_STATUSES
|
|
669
|
+
else worker_status
|
|
670
|
+
)
|
|
671
|
+
if (
|
|
672
|
+
evidence_status in TERMINAL_TASK_STATUSES
|
|
673
|
+
and actual_executor_id
|
|
674
|
+
and request.agent_id
|
|
675
|
+
):
|
|
676
|
+
evidence_recorder = getattr(
|
|
677
|
+
self,
|
|
678
|
+
'record_project_agent_executor_outcome_evidence',
|
|
679
|
+
None,
|
|
680
|
+
)
|
|
681
|
+
if callable(evidence_recorder):
|
|
682
|
+
strategy = (
|
|
683
|
+
ProjectAgentModelStrategy.model_validate_json(
|
|
684
|
+
raw_task['effective_model_strategy_json']
|
|
685
|
+
)
|
|
686
|
+
if raw_task.get('effective_model_strategy_json')
|
|
687
|
+
else ProjectAgentModelStrategy()
|
|
688
|
+
)
|
|
689
|
+
await evidence_recorder(
|
|
690
|
+
actor,
|
|
691
|
+
ProjectAgentExecutorOutcomeEvidenceCreate(
|
|
692
|
+
personal_space_id=request.personal_space_id,
|
|
693
|
+
personal_project_id=request.personal_project_id,
|
|
694
|
+
work_kind=raw_task['work_kind'],
|
|
695
|
+
agent_id=request.agent_id,
|
|
696
|
+
executor_id=actual_executor_id,
|
|
697
|
+
task_id=request.task_id,
|
|
698
|
+
run_id=event_id,
|
|
699
|
+
model_strategy=strategy,
|
|
700
|
+
evidence_kind='terminal_outcome',
|
|
701
|
+
source='system_terminal',
|
|
702
|
+
terminal_outcome=evidence_status,
|
|
703
|
+
reference_ids=[event_id],
|
|
704
|
+
idempotency_key=f'terminal:{event_id}',
|
|
705
|
+
occurred_at=now,
|
|
706
|
+
),
|
|
707
|
+
)
|
|
708
|
+
await self._archive_finished_temporary_agent(request, raw_task, now)
|
|
709
|
+
return await self.get_project_agent_task(
|
|
710
|
+
actor,
|
|
711
|
+
request.personal_space_id,
|
|
712
|
+
request.task_id,
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
async def get_project_agent_activity(
|
|
716
|
+
self,
|
|
717
|
+
actor: dict,
|
|
718
|
+
personal_space_id: str,
|
|
719
|
+
agent_id: str,
|
|
720
|
+
from_date: date,
|
|
721
|
+
to_date: date,
|
|
722
|
+
) -> ProjectAgentActivityResult:
|
|
723
|
+
self._require_personal()
|
|
724
|
+
await self.authorize(actor, personal_space_id, 'reader')
|
|
725
|
+
if from_date > to_date:
|
|
726
|
+
raise HTTPException(status_code=422, detail='from date must not exceed to date')
|
|
727
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
728
|
+
'''
|
|
729
|
+
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
730
|
+
[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
731
|
+
(agent:FuliProjectAgent {agent_id: $agent_id})
|
|
732
|
+
MATCH (space)-[:HAS_PROJECT_AGENT_TASK]->
|
|
733
|
+
(task:FuliProjectAgentTask)-[:HAS_TASK_EVENT]->
|
|
734
|
+
(event:FuliProjectAgentTaskEvent)-[:EVENT_AGENT]->(agent)
|
|
735
|
+
WHERE event.status IN ['completed', 'failed', 'cancelled']
|
|
736
|
+
AND event.activity_date >= date($from_date)
|
|
737
|
+
AND event.activity_date <= date($to_date)
|
|
738
|
+
RETURN event, task.title AS title
|
|
739
|
+
ORDER BY event.created_at DESC, event.event_id
|
|
740
|
+
''',
|
|
741
|
+
personal_space_id=personal_space_id,
|
|
742
|
+
agent_id=agent_id,
|
|
743
|
+
from_date=from_date.isoformat(),
|
|
744
|
+
to_date=to_date.isoformat(),
|
|
745
|
+
routing_='r',
|
|
746
|
+
)
|
|
747
|
+
grouped = defaultdict(list)
|
|
748
|
+
for row in records:
|
|
749
|
+
event = dict(row['event'])
|
|
750
|
+
occurred_at = native_datetime(event['created_at'])
|
|
751
|
+
grouped[occurred_at.date()].append(ProjectAgentActivityTask(
|
|
752
|
+
task_id=event['task_id'],
|
|
753
|
+
title=row['title'],
|
|
754
|
+
status=event['status'],
|
|
755
|
+
summary=event['summary'],
|
|
756
|
+
occurred_at=occurred_at,
|
|
757
|
+
source_application=event.get('source_application'),
|
|
758
|
+
actual_model_provider=event.get('actual_model_provider'),
|
|
759
|
+
actual_model=event.get('actual_model'),
|
|
760
|
+
**self._activity_executor_fields(event),
|
|
761
|
+
))
|
|
762
|
+
days = []
|
|
763
|
+
for activity_date in sorted(grouped):
|
|
764
|
+
tasks = grouped[activity_date]
|
|
765
|
+
counts = {
|
|
766
|
+
status: sum(item.status == status for item in tasks)
|
|
767
|
+
for status in TERMINAL_TASK_STATUSES
|
|
768
|
+
}
|
|
769
|
+
days.append(ProjectAgentActivityDay(
|
|
770
|
+
date=activity_date,
|
|
771
|
+
completed=counts['completed'],
|
|
772
|
+
failed=counts['failed'],
|
|
773
|
+
cancelled=counts['cancelled'],
|
|
774
|
+
total=len(tasks),
|
|
775
|
+
tasks=tasks,
|
|
776
|
+
))
|
|
777
|
+
return ProjectAgentActivityResult(
|
|
778
|
+
agent_id=agent_id,
|
|
779
|
+
personal_space_id=personal_space_id,
|
|
780
|
+
from_date=from_date,
|
|
781
|
+
to_date=to_date,
|
|
782
|
+
days=days,
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
async def _route_assigned_agent(self, actor, request, task):
|
|
786
|
+
"""Project the selected durable identity into the route response."""
|
|
787
|
+
|
|
788
|
+
agent_id = getattr(task, 'lead_agent_id', None)
|
|
789
|
+
getter = getattr(self, 'get_project_agent', None)
|
|
790
|
+
if not agent_id or not callable(getter):
|
|
791
|
+
return None
|
|
792
|
+
try:
|
|
793
|
+
return await getter(
|
|
794
|
+
actor,
|
|
795
|
+
request.personal_space_id,
|
|
796
|
+
request.personal_project_id,
|
|
797
|
+
agent_id,
|
|
798
|
+
)
|
|
799
|
+
except HTTPException as exc:
|
|
800
|
+
# A task remains readable if an Agent was archived immediately
|
|
801
|
+
# after persistence; the selected identity is then unavailable.
|
|
802
|
+
if exc.status_code == 404:
|
|
803
|
+
return None
|
|
804
|
+
raise
|
|
805
|
+
|
|
806
|
+
@staticmethod
|
|
807
|
+
def _validate_task_transition(current, requested, *, has_actual_executor=False):
|
|
808
|
+
if not current:
|
|
809
|
+
return
|
|
810
|
+
if current in TERMINAL_TASK_STATUSES:
|
|
811
|
+
raise HTTPException(
|
|
812
|
+
status_code=409,
|
|
813
|
+
detail=f'illegal project Agent task transition: {current} -> {requested}',
|
|
814
|
+
)
|
|
815
|
+
if current == requested:
|
|
816
|
+
return
|
|
817
|
+
allowed = {
|
|
818
|
+
'awaiting_recruitment': {'blocked', 'cancelled'},
|
|
819
|
+
'queued': {'running', 'paused', 'blocked', 'failed', 'cancelled'},
|
|
820
|
+
'running': {
|
|
821
|
+
'paused', 'blocked', 'awaiting_review',
|
|
822
|
+
'completed', 'failed', 'cancelled',
|
|
823
|
+
},
|
|
824
|
+
'paused': {'running', 'blocked', 'failed', 'cancelled'},
|
|
825
|
+
'awaiting_review': {'running', 'completed', 'failed', 'cancelled'},
|
|
826
|
+
'blocked': {'cancelled', 'failed'},
|
|
827
|
+
'completed': set(),
|
|
828
|
+
'failed': set(),
|
|
829
|
+
'cancelled': set(),
|
|
830
|
+
}
|
|
831
|
+
if current == 'blocked' and has_actual_executor:
|
|
832
|
+
allowed['blocked'] = {'queued', 'running', 'cancelled', 'failed'}
|
|
833
|
+
if requested not in allowed.get(current, set()):
|
|
834
|
+
raise HTTPException(
|
|
835
|
+
status_code=409,
|
|
836
|
+
detail=f'illegal project Agent task transition: {current} -> {requested}',
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
@staticmethod
|
|
840
|
+
def _assess_complexity(request):
|
|
841
|
+
if request.complexity_hint:
|
|
842
|
+
return request.complexity_hint, ['explicit task complexity hint']
|
|
843
|
+
basis = []
|
|
844
|
+
points = 0
|
|
845
|
+
if len(request.objective) > 1200:
|
|
846
|
+
points += 2
|
|
847
|
+
basis.append('long objective')
|
|
848
|
+
elif len(request.objective) > 400:
|
|
849
|
+
points += 1
|
|
850
|
+
basis.append('multi-part objective')
|
|
851
|
+
if len(request.required_capabilities) >= 4:
|
|
852
|
+
points += 2
|
|
853
|
+
basis.append('four or more required capabilities')
|
|
854
|
+
elif len(request.required_capabilities) >= 2:
|
|
855
|
+
points += 1
|
|
856
|
+
basis.append('multiple required capabilities')
|
|
857
|
+
if request.parallel_plan.enabled:
|
|
858
|
+
points += 2
|
|
859
|
+
basis.append('verified parallel work requested')
|
|
860
|
+
if points >= 4:
|
|
861
|
+
return 'complex', basis or ['complex task shape']
|
|
862
|
+
if points >= 2:
|
|
863
|
+
return 'standard', basis or ['standard task shape']
|
|
864
|
+
return 'simple', basis or ['bounded task shape']
|
|
865
|
+
|
|
866
|
+
@staticmethod
|
|
867
|
+
def _effective_model_strategy(request, selected, coordinator_strategy):
|
|
868
|
+
if request.model_strategy_override:
|
|
869
|
+
return request.model_strategy_override, 'task'
|
|
870
|
+
if selected and selected.get('model_strategy_override'):
|
|
871
|
+
value = selected['model_strategy_override']
|
|
872
|
+
return (
|
|
873
|
+
value
|
|
874
|
+
if isinstance(value, ProjectAgentModelStrategy)
|
|
875
|
+
else ProjectAgentModelStrategy.model_validate_json(value),
|
|
876
|
+
'assignment',
|
|
877
|
+
)
|
|
878
|
+
if selected and selected.get('profile'):
|
|
879
|
+
return selected['profile'].default_model_strategy, 'agent'
|
|
880
|
+
return coordinator_strategy, 'coordinator'
|
|
881
|
+
|
|
882
|
+
async def _resolve_executor_if_available(
|
|
883
|
+
self,
|
|
884
|
+
actor,
|
|
885
|
+
request,
|
|
886
|
+
selected,
|
|
887
|
+
model_strategy,
|
|
888
|
+
model_strategy_source,
|
|
889
|
+
task_id,
|
|
890
|
+
):
|
|
891
|
+
resolver = getattr(self, 'resolve_project_agent_executor', None)
|
|
892
|
+
if not callable(resolver) or not selected:
|
|
893
|
+
return None
|
|
894
|
+
return await resolver(
|
|
895
|
+
actor,
|
|
896
|
+
personal_space_id=request.personal_space_id,
|
|
897
|
+
personal_project_id=request.personal_project_id,
|
|
898
|
+
task_id=task_id,
|
|
899
|
+
agent_id=selected['agent_id'],
|
|
900
|
+
work_kind=request.work_kind,
|
|
901
|
+
required_capabilities=request.required_capabilities,
|
|
902
|
+
model_strategy=model_strategy,
|
|
903
|
+
task_override=(
|
|
904
|
+
getattr(request, 'executor_policy_override', None)
|
|
905
|
+
or getattr(request, 'executor_override', None)
|
|
906
|
+
),
|
|
907
|
+
assignment_id=selected.get('assignment_id'),
|
|
908
|
+
model_strategy_source=model_strategy_source,
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
@staticmethod
|
|
912
|
+
def _executor_decision_blocked(decision):
|
|
913
|
+
value = decision.model_dump(mode='json') if hasattr(decision, 'model_dump') else decision
|
|
914
|
+
return value.get('outcome') == 'blocked' or value.get('status') == 'blocked'
|
|
915
|
+
|
|
916
|
+
@staticmethod
|
|
917
|
+
def _decision_executor_fields(decision):
|
|
918
|
+
if not decision:
|
|
919
|
+
return {}
|
|
920
|
+
raw = decision.model_dump(mode='json') if hasattr(decision, 'model_dump') else dict(decision)
|
|
921
|
+
aliases = {
|
|
922
|
+
# The routing decision audit field is JSON data. Keep the
|
|
923
|
+
# provider-neutral snapshot instead of leaking a Pydantic model
|
|
924
|
+
# into the parent record (which fails validation in real routing).
|
|
925
|
+
'executor_decision': raw,
|
|
926
|
+
'selected_executor_id': raw.get('selected_executor_id') or raw.get('executor_id'),
|
|
927
|
+
'matched_executor_rule_id': raw.get('matched_rule_id') or raw.get('rule_id'),
|
|
928
|
+
'executor_selection_reason': raw.get('selection_reason') or raw.get('reason'),
|
|
929
|
+
'executor_fallback_outcome': raw.get('fallback_outcome'),
|
|
930
|
+
'executor_policy': raw.get('executor_policy'),
|
|
931
|
+
}
|
|
932
|
+
return {
|
|
933
|
+
key: value
|
|
934
|
+
for key, value in aliases.items()
|
|
935
|
+
if key in ProjectAgentRoutingDecisionRecord.model_fields
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
@staticmethod
|
|
939
|
+
def _activity_executor_fields(event):
|
|
940
|
+
return {
|
|
941
|
+
key: value
|
|
942
|
+
for key, value in {
|
|
943
|
+
'actual_executor_id': event.get('actual_executor_id'),
|
|
944
|
+
'matched_executor_rule_id': event.get('matched_executor_rule_id'),
|
|
945
|
+
'worker_id': event.get('worker_id'),
|
|
946
|
+
'worker_label': event.get('worker_label'),
|
|
947
|
+
'worker_occupation_emoji': event.get(
|
|
948
|
+
'worker_occupation_emoji'
|
|
949
|
+
),
|
|
950
|
+
'worker_status': event.get('worker_status'),
|
|
951
|
+
}.items()
|
|
952
|
+
if key in ProjectAgentActivityTask.model_fields
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
async def _persist_task(
|
|
956
|
+
self,
|
|
957
|
+
request,
|
|
958
|
+
*,
|
|
959
|
+
task_id,
|
|
960
|
+
payload_hash,
|
|
961
|
+
coordinator_agent_id,
|
|
962
|
+
lead_agent_id,
|
|
963
|
+
status,
|
|
964
|
+
routing_outcome,
|
|
965
|
+
routing_reason,
|
|
966
|
+
routing_explanation,
|
|
967
|
+
match_basis,
|
|
968
|
+
complexity,
|
|
969
|
+
complexity_basis,
|
|
970
|
+
model_strategy,
|
|
971
|
+
model_source,
|
|
972
|
+
participants,
|
|
973
|
+
decision,
|
|
974
|
+
recruitment,
|
|
975
|
+
executor_decision,
|
|
976
|
+
created_at,
|
|
977
|
+
):
|
|
978
|
+
decision_raw = decision.model_dump(mode='json')
|
|
979
|
+
executor_raw = (
|
|
980
|
+
executor_decision.model_dump(mode='json')
|
|
981
|
+
if hasattr(executor_decision, 'model_dump')
|
|
982
|
+
else executor_decision
|
|
983
|
+
)
|
|
984
|
+
initial_event_id = stable_uuid(
|
|
985
|
+
self.settings.provider_id,
|
|
986
|
+
request.personal_space_id,
|
|
987
|
+
'project-agent-task-event',
|
|
988
|
+
task_id,
|
|
989
|
+
'created',
|
|
990
|
+
)
|
|
991
|
+
await self.runtime.driver.execute_query(
|
|
992
|
+
'''
|
|
993
|
+
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})
|
|
994
|
+
MATCH (space)-[:CONTAINS_PROJECT]->
|
|
995
|
+
(project:FuliPersonalProject {project_id: $personal_project_id})
|
|
996
|
+
MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
997
|
+
(coordinator:FuliProjectAgent {agent_id: $coordinator_agent_id})
|
|
998
|
+
CREATE (task:FuliProjectAgentTask {
|
|
999
|
+
id: $task_id,
|
|
1000
|
+
task_id: $task_id,
|
|
1001
|
+
personal_space_id: $personal_space_id,
|
|
1002
|
+
personal_project_id: $personal_project_id,
|
|
1003
|
+
payload_hash: $payload_hash,
|
|
1004
|
+
title: $title,
|
|
1005
|
+
objective: $objective,
|
|
1006
|
+
work_kind: $work_kind,
|
|
1007
|
+
required_capabilities: $required_capabilities,
|
|
1008
|
+
duration: $duration,
|
|
1009
|
+
staffing_intent: $staffing_intent,
|
|
1010
|
+
status: $status,
|
|
1011
|
+
revision: 0,
|
|
1012
|
+
routing_outcome: $routing_outcome,
|
|
1013
|
+
routing_reason: $routing_reason,
|
|
1014
|
+
routing_explanation: $routing_explanation,
|
|
1015
|
+
match_basis: $match_basis,
|
|
1016
|
+
coordinator_agent_id: $coordinator_agent_id,
|
|
1017
|
+
lead_agent_id: $lead_agent_id,
|
|
1018
|
+
complexity: $complexity,
|
|
1019
|
+
complexity_basis: $complexity_basis,
|
|
1020
|
+
effective_model_strategy_json: $model_strategy_json,
|
|
1021
|
+
task_model_strategy_override_json:
|
|
1022
|
+
$task_model_strategy_override_json,
|
|
1023
|
+
task_executor_policy_override_json:
|
|
1024
|
+
$task_executor_policy_override_json,
|
|
1025
|
+
model_strategy_source: $model_source,
|
|
1026
|
+
source_application: $source_application,
|
|
1027
|
+
source_session_id: $source_session_id,
|
|
1028
|
+
executor_decision_json: $executor_decision_json,
|
|
1029
|
+
executor_policy_json: $executor_policy_json,
|
|
1030
|
+
selected_executor_id: $selected_executor_id,
|
|
1031
|
+
created_at: $created_at,
|
|
1032
|
+
updated_at: $created_at
|
|
1033
|
+
})
|
|
1034
|
+
CREATE (decision_node:FuliProjectAgentRoutingDecision {
|
|
1035
|
+
id: $decision_id,
|
|
1036
|
+
decision_id: $decision_id,
|
|
1037
|
+
task_id: $task_id,
|
|
1038
|
+
decision_json: $decision_json,
|
|
1039
|
+
created_at: $created_at
|
|
1040
|
+
})
|
|
1041
|
+
CREATE (event:FuliProjectAgentTaskEvent {
|
|
1042
|
+
id: $initial_event_id,
|
|
1043
|
+
event_id: $initial_event_id,
|
|
1044
|
+
task_id: $task_id,
|
|
1045
|
+
status: $status,
|
|
1046
|
+
actor_kind: 'system',
|
|
1047
|
+
summary: $routing_explanation,
|
|
1048
|
+
source_application: $source_application,
|
|
1049
|
+
source_session_id: $source_session_id,
|
|
1050
|
+
created_at: $created_at
|
|
1051
|
+
})
|
|
1052
|
+
MERGE (space)-[:HAS_PROJECT_AGENT_TASK]->(task)
|
|
1053
|
+
MERGE (project)-[:HAS_PROJECT_AGENT_TASK]->(task)
|
|
1054
|
+
MERGE (task)-[:COORDINATED_BY]->(coordinator)
|
|
1055
|
+
MERGE (task)-[:ROUTED_BY]->(decision_node)
|
|
1056
|
+
MERGE (task)-[:HAS_TASK_EVENT]->(event)
|
|
1057
|
+
''',
|
|
1058
|
+
task_id=task_id,
|
|
1059
|
+
personal_space_id=request.personal_space_id,
|
|
1060
|
+
personal_project_id=request.personal_project_id,
|
|
1061
|
+
payload_hash=payload_hash,
|
|
1062
|
+
title=request.title,
|
|
1063
|
+
objective=request.objective,
|
|
1064
|
+
work_kind=request.work_kind,
|
|
1065
|
+
required_capabilities=request.required_capabilities,
|
|
1066
|
+
duration=request.duration,
|
|
1067
|
+
staffing_intent=request.staffing_intent,
|
|
1068
|
+
status=status,
|
|
1069
|
+
routing_outcome=routing_outcome,
|
|
1070
|
+
routing_reason=routing_reason,
|
|
1071
|
+
routing_explanation=routing_explanation,
|
|
1072
|
+
match_basis=match_basis,
|
|
1073
|
+
coordinator_agent_id=coordinator_agent_id,
|
|
1074
|
+
lead_agent_id=lead_agent_id,
|
|
1075
|
+
complexity=complexity,
|
|
1076
|
+
complexity_basis=complexity_basis,
|
|
1077
|
+
model_strategy_json=model_strategy.model_dump_json() if model_strategy else None,
|
|
1078
|
+
task_model_strategy_override_json=(
|
|
1079
|
+
request.model_strategy_override.model_dump_json()
|
|
1080
|
+
if request.model_strategy_override else None
|
|
1081
|
+
),
|
|
1082
|
+
task_executor_policy_override_json=(
|
|
1083
|
+
(
|
|
1084
|
+
request.executor_policy_override
|
|
1085
|
+
or request.executor_override
|
|
1086
|
+
).model_dump_json()
|
|
1087
|
+
if request.executor_policy_override or request.executor_override
|
|
1088
|
+
else None
|
|
1089
|
+
),
|
|
1090
|
+
model_source=model_source,
|
|
1091
|
+
source_application=request.source_application,
|
|
1092
|
+
source_session_id=request.source_session_id,
|
|
1093
|
+
executor_decision_json=(
|
|
1094
|
+
json.dumps(executor_raw, sort_keys=True) if executor_raw else None
|
|
1095
|
+
),
|
|
1096
|
+
executor_policy_json=(
|
|
1097
|
+
json.dumps((executor_raw or {}).get('executor_policy'), sort_keys=True)
|
|
1098
|
+
if (executor_raw or {}).get('executor_policy') else None
|
|
1099
|
+
),
|
|
1100
|
+
selected_executor_id=(
|
|
1101
|
+
(executor_raw or {}).get('selected_executor_id')
|
|
1102
|
+
or (executor_raw or {}).get('executor_id')
|
|
1103
|
+
),
|
|
1104
|
+
created_at=created_at,
|
|
1105
|
+
decision_id=decision.decision_id,
|
|
1106
|
+
decision_json=json.dumps(decision_raw, sort_keys=True),
|
|
1107
|
+
initial_event_id=initial_event_id,
|
|
1108
|
+
)
|
|
1109
|
+
for participant in participants:
|
|
1110
|
+
await self.runtime.driver.execute_query(
|
|
1111
|
+
'''
|
|
1112
|
+
MATCH (space:FuliSpace {
|
|
1113
|
+
id: $personal_space_id, kind: 'personal'
|
|
1114
|
+
})-[:HAS_PROJECT_AGENT_TASK]->
|
|
1115
|
+
(task:FuliProjectAgentTask {task_id: $task_id})
|
|
1116
|
+
MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
1117
|
+
(agent:FuliProjectAgent {agent_id: $agent_id})
|
|
1118
|
+
MERGE (task)-[participant:HAS_PARTICIPANT]->(agent)
|
|
1119
|
+
SET participant.role = $role,
|
|
1120
|
+
participant.status = $status,
|
|
1121
|
+
participant.assignment_summary = $assignment_summary,
|
|
1122
|
+
participant.joined_at = $created_at,
|
|
1123
|
+
participant.updated_at = $created_at
|
|
1124
|
+
''',
|
|
1125
|
+
personal_space_id=request.personal_space_id,
|
|
1126
|
+
task_id=task_id,
|
|
1127
|
+
agent_id=participant['agent_id'],
|
|
1128
|
+
role=participant['role'],
|
|
1129
|
+
status=status,
|
|
1130
|
+
assignment_summary=participant.get('assignment_summary'),
|
|
1131
|
+
created_at=created_at,
|
|
1132
|
+
)
|
|
1133
|
+
if recruitment:
|
|
1134
|
+
await self.runtime.driver.execute_query(
|
|
1135
|
+
'''
|
|
1136
|
+
MATCH (space:FuliSpace {
|
|
1137
|
+
id: $personal_space_id, kind: 'personal'
|
|
1138
|
+
})-[:HAS_PROJECT_AGENT_TASK]->
|
|
1139
|
+
(task:FuliProjectAgentTask {task_id: $task_id})
|
|
1140
|
+
MATCH (space)-[:HAS_PROJECT_AGENT_RECRUITMENT]->
|
|
1141
|
+
(recruitment:FuliProjectAgentRecruitment {
|
|
1142
|
+
recruitment_id: $recruitment_id
|
|
1143
|
+
})
|
|
1144
|
+
SET task.recruitment_id = $recruitment_id,
|
|
1145
|
+
task.hr_agent_id = recruitment.hr_agent_id
|
|
1146
|
+
MERGE (task)-[:TRIGGERED_RECRUITMENT]->(recruitment)
|
|
1147
|
+
''',
|
|
1148
|
+
personal_space_id=request.personal_space_id,
|
|
1149
|
+
task_id=task_id,
|
|
1150
|
+
recruitment_id=recruitment.recruitment_id,
|
|
1151
|
+
)
|