fuli-context 0.7.5 → 0.7.7
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 +69 -13
- package/README.zh-CN.md +60 -14
- package/dist/web/assets/{AboutPage-ByCUor-j.js → AboutPage-BcZeFcHe.js} +1 -1
- package/dist/web/assets/{ConnectionsPage-DkdkZ40Q.js → ConnectionsPage-xAQYl1SQ.js} +1 -1
- package/dist/web/assets/EmployeeRecruitDialog-CbPAmhy7.js +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-CfFlyKSh.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-BQvTLoc8.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-DF3EL0Ui.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-UnnBDt0I.js +1 -0
- package/dist/web/assets/{KnowledgeEditDialog-DWUV_NPp.css → KnowledgeEditDialog-iv-dgUeD.css} +1 -1
- package/dist/web/assets/{KnowledgeInspector-CYAjAb9J.js → KnowledgeInspector-BqPJH5Rr.js} +1 -1
- package/dist/web/assets/{KnowledgeOrganizerPage-CV4F54R_.js → KnowledgeOrganizerPage-CyvRflXM.js} +1 -1
- package/dist/web/assets/KnowledgePage-59eczTqe.js +1 -0
- package/dist/web/assets/{KnowledgeWorkspace-z4u5mXOG.js → KnowledgeWorkspace-CfIjhOog.js} +1 -1
- package/dist/web/assets/{OverviewPage-BWOfeS6k.js → OverviewPage-Xyfm5xkZ.js} +1 -1
- package/dist/web/assets/{PersonalProfilePage-c3w4-h7a.js → PersonalProfilePage-DB9mByB3.js} +1 -1
- package/dist/web/assets/PersonalProjectsPage-_-YXVi5O.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +1 -0
- package/dist/web/assets/ProjectAgentsPage-DeeQByPX.js +11 -0
- package/dist/web/assets/{PublicProjectsPage-DdH0oGB3.js → PublicProjectsPage-BEZ1YPs3.js} +1 -1
- package/dist/web/assets/{ReviewPage-BYIpcIHG.js → ReviewPage-CQYGwFBH.js} +1 -1
- package/dist/web/assets/SearchableSelect-D27H0MoP.js +1 -0
- package/dist/web/assets/{SettingsPage-2VkbXgZi.js → SettingsPage-BlEZ03Yi.js} +1 -1
- package/dist/web/assets/{WritingTastePage-BOPQZuva.js → WritingTastePage-DvRt_nmP.js} +1 -1
- package/dist/web/assets/catalog-DFvIaHVS.js +1 -0
- package/dist/web/assets/{console-BlAhujsH.js → console-tOxOl8Vq.js} +1 -1
- package/dist/web/assets/conversation-launcher-settings-DuFSpPAo.js +1 -0
- package/dist/web/assets/{i18n-Ci3heuiL.js → i18n-CgBHZthI.js} +3 -3
- package/dist/web/assets/{identity-4_-v6cB7.js → identity-CFo_Uw1H.js} +1 -1
- package/dist/web/assets/index-CJc5ppXH.css +1 -0
- package/dist/web/assets/index-yCi3cz2l.js +2 -0
- package/dist/web/assets/{model-yG0YqUyZ.js → model-BtnVr7fX.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-twhdZArA.js → useApi-CROJJdhE-B6MHm_Av.js} +1 -1
- package/dist/web/assets/{vue-router-C-wot_Di.js → vue-router-CzHm-y-x.js} +1 -1
- package/dist/web/index.html +7 -6
- package/docs/employee-agents.md +99 -0
- package/docs/project-agent-memory.md +479 -0
- package/graph-provider/fuli_graph/knowledge_audit_models.py +171 -0
- package/graph-provider/fuli_graph/model_base.py +5 -0
- package/graph-provider/fuli_graph/model_validation.py +32 -0
- package/graph-provider/fuli_graph/models.py +26 -170
- package/graph-provider/fuli_graph/project_agent_context_models.py +29 -0
- package/graph-provider/fuli_graph/project_agent_coordination_models.py +22 -0
- package/graph-provider/fuli_graph/project_agent_executor_models.py +35 -7
- package/graph-provider/fuli_graph/project_agent_memory_models.py +79 -0
- package/graph-provider/fuli_graph/project_agent_memory_routes.py +46 -0
- package/graph-provider/fuli_graph/project_agent_models.py +9 -0
- package/graph-provider/fuli_graph/project_agent_recruitment_values.py +48 -0
- package/graph-provider/fuli_graph/project_agent_routes.py +50 -1
- package/graph-provider/fuli_graph/project_agent_task_models.py +220 -29
- package/graph-provider/fuli_graph/runtime.py +37 -1
- package/graph-provider/fuli_graph/store.py +6 -0
- package/graph-provider/fuli_graph/store_project_agent_context.py +65 -0
- package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +122 -0
- package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +175 -11
- package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +33 -10
- package/graph-provider/fuli_graph/store_project_agent_executors.py +181 -14
- package/graph-provider/fuli_graph/store_project_agent_memory.py +174 -0
- package/graph-provider/fuli_graph/store_project_agent_task_activity.py +423 -0
- package/graph-provider/fuli_graph/store_project_agent_task_persistence.py +152 -0
- package/graph-provider/fuli_graph/store_project_agent_task_reads.py +187 -21
- package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +756 -164
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +396 -7
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +265 -465
- package/graph-provider/fuli_graph/store_project_agents.py +214 -46
- package/graph-provider/fuli_graph/store_task_contexts.py +217 -0
- package/graph-provider/fuli_graph/store_transactions.py +40 -0
- package/graph-provider/fuli_graph/task_context_models.py +50 -0
- package/graph-provider/fuli_graph/task_context_routes.py +31 -0
- package/graph-provider/fuli_graph/workflow_observation_models.py +1 -1
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +8 -8
- package/package.json +5 -2
- package/skills/capturing-session-knowledge/SKILL.md +18 -6
- package/src/adaptive-runtime/executor-pool.js +46 -5
- package/src/adaptive-runtime/graph-services.js +10 -3
- package/src/adaptive-runtime/lease-client.js +66 -14
- package/src/adaptive-runtime/runtime-broker.js +24 -9
- package/src/agent-tools/employee-definitions.js +43 -0
- package/src/agent-tools/graph-definitions.js +48 -6
- package/src/agent-tools/graph-handlers.js +15 -0
- package/src/agent-tools/project-agent-definitions.js +35 -4
- package/src/agent-tools/project-agent-memory-definitions.js +36 -0
- package/src/agent-tools.js +6 -2
- package/src/agents/codex/lifecycle-hook.js +107 -0
- package/src/agents/codex/lifecycle-hooks.js +231 -0
- package/src/agents/cursor/lifecycle-hook.js +133 -0
- package/src/agents/cursor/lifecycle-hooks.js +50 -0
- package/src/agents/lifecycle-message.js +19 -0
- package/src/app/agent-request-context.js +23 -0
- package/src/cli/command-registry.js +2 -0
- package/src/cli/employee-command.js +17 -0
- package/src/cli/remote-mcp-command.js +282 -0
- package/src/cli/setup-command.js +5 -0
- package/src/cli.js +10 -0
- package/src/employees/catalog/jefa.json +21 -0
- package/src/employees/install-package.js +77 -0
- package/src/employees/management-policy.js +88 -0
- package/src/employees/manifest.js +53 -0
- package/src/employees/package-registry.js +107 -0
- package/src/employees/project-selection.js +50 -0
- package/src/employees/recruitment.js +199 -0
- package/src/employees/service.js +83 -0
- package/src/employees/task-entry.js +39 -0
- package/src/graphiti/agent-knowledge-workflows.js +135 -25
- package/src/graphiti/federated-application.js +78 -35
- package/src/graphiti/project-agent-control-plane.js +32 -3
- package/src/graphiti/project-agent-mapping.js +175 -4
- package/src/graphiti/project-agent-memory.js +115 -0
- package/src/graphiti/project-agent-task-entry.js +126 -0
- package/src/graphiti/project-agent-workflows.js +49 -14
- package/src/graphiti/provider-client.js +78 -4
- package/src/http/api-router.js +25 -5
- package/src/http/employee-api-router.js +64 -0
- package/src/http/error-mapping.js +4 -0
- package/src/http/graph-api-router.js +14 -0
- package/src/http/request-policy.js +6 -3
- package/src/http/static-handler.js +4 -3
- package/src/mcp/create-mcp-server.js +98 -37
- package/src/mcp/direct-call.js +38 -0
- package/src/mcp/instructions.js +8 -7
- package/src/mcp/provider-task-context-registry.js +74 -0
- package/src/mcp/remote-http-server.js +761 -0
- package/src/mcp/session-id.js +29 -3
- package/src/mcp/task-context-registry.js +88 -29
- package/src/mcp/tool-annotations.js +12 -2
- package/src/mcp/tool-result.js +8 -3
- package/src/mcp-server.js +20 -8
- package/src/native-runtime/runtime.js +58 -8
- package/src/server.js +76 -28
- package/src/setup/agent-installation-status.js +38 -6
- package/src/setup/agents.js +20 -8
- package/src/setup/claude-code-config.js +55 -13
- package/src/setup/codex-bootstrap.js +11 -10
- package/src/setup/codex-config.js +74 -7
- package/src/setup/config-backup.js +18 -2
- package/src/setup/cursor-config.js +22 -5
- package/src/setup/setup.js +30 -22
- package/src/storage/json-file.js +11 -3
- package/dist/web/assets/KnowledgeEditDialog-DSe3Gwep.js +0 -1
- package/dist/web/assets/KnowledgePage-vYYXipOi.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-B7AlLEwf.js +0 -1
- package/dist/web/assets/ProjectAgentsPage-CgftpIPB.css +0 -1
- package/dist/web/assets/ProjectAgentsPage-DTHcL_ox.js +0 -11
- package/dist/web/assets/SearchableSelect-Dz13ClQb.js +0 -1
- package/dist/web/assets/conversation-launcher-settings-DTYxiLJt.js +0 -1
- package/dist/web/assets/index-BnH5mcZx.css +0 -1
- package/dist/web/assets/index-C67Q0JY2.js +0 -2
|
@@ -1,25 +1,15 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from collections import defaultdict
|
|
3
3
|
from datetime import date
|
|
4
|
-
|
|
5
4
|
from fastapi import HTTPException
|
|
6
|
-
|
|
7
5
|
from .personal_project_access import authorize_personal_project
|
|
8
6
|
from .project_agent_access import authorize_project_agent
|
|
9
|
-
from .
|
|
10
|
-
ProjectAgentExecutorActualReport,
|
|
11
|
-
ProjectAgentExecutorOutcomeEvidenceCreate,
|
|
12
|
-
)
|
|
13
|
-
from .project_agent_models import (
|
|
14
|
-
ProjectAgentModelStrategy,
|
|
15
|
-
ProjectAgentProfile,
|
|
16
|
-
)
|
|
7
|
+
from .project_agent_models import ProjectAgentModelStrategy, ProjectAgentProfile
|
|
17
8
|
from .project_agent_task_models import (
|
|
18
9
|
ProjectAgentActivityDay,
|
|
19
10
|
ProjectAgentActivityResult,
|
|
20
11
|
ProjectAgentActivityTask,
|
|
21
12
|
ProjectAgentRoutingDecisionRecord,
|
|
22
|
-
ProjectAgentTaskActivityCreate,
|
|
23
13
|
ProjectAgentTaskRecord,
|
|
24
14
|
ProjectAgentTaskRouteResult,
|
|
25
15
|
ProjectAgentTaskSubmit,
|
|
@@ -27,31 +17,32 @@ from .project_agent_task_models import (
|
|
|
27
17
|
from .provider_values import native_datetime, now_utc, stable_uuid
|
|
28
18
|
from .store_project_agents import SYSTEM_COORDINATOR_AGENT_ID
|
|
29
19
|
from .store_project_agent_task_reads import StoreProjectAgentTaskReads
|
|
20
|
+
from .store_project_agent_task_persistence import StoreProjectAgentTaskPersistence
|
|
30
21
|
from .store_project_agent_task_recruitment import StoreProjectAgentTaskRecruitment
|
|
31
22
|
from .store_project_agent_task_staffing import StoreProjectAgentTaskStaffing
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
23
|
+
from .store_project_agent_coordination_policy import StoreProjectAgentCoordinationPolicy
|
|
24
|
+
from .store_project_agent_task_activity import StoreProjectAgentTaskActivity, TERMINAL_TASK_STATUSES
|
|
25
|
+
from .store_transactions import TransactionQueryDriver, query_store_transaction
|
|
37
26
|
class StoreProjectAgentTasks(
|
|
27
|
+
StoreProjectAgentTaskActivity,
|
|
28
|
+
StoreProjectAgentCoordinationPolicy,
|
|
38
29
|
StoreProjectAgentTaskRecruitment,
|
|
39
30
|
StoreProjectAgentTaskReads,
|
|
40
|
-
|
|
31
|
+
StoreProjectAgentTaskPersistence,
|
|
41
32
|
StoreProjectAgentTaskStaffing,
|
|
42
33
|
):
|
|
43
34
|
"""Durable task, routing, recruitment, and verified activity control plane.
|
|
44
35
|
|
|
45
|
-
|
|
46
|
-
tasks and append truthful state events to the same persistent Agent/task
|
|
47
|
-
history. No task is shown as running until a client records that event.
|
|
36
|
+
Clients append real state events; this control plane does not run workers.
|
|
48
37
|
"""
|
|
49
|
-
|
|
50
38
|
async def submit_project_agent_task(
|
|
51
39
|
self,
|
|
52
40
|
actor: dict,
|
|
53
41
|
request: ProjectAgentTaskSubmit,
|
|
54
42
|
) -> ProjectAgentTaskRouteResult:
|
|
43
|
+
if not isinstance(self.runtime.driver, TransactionQueryDriver):
|
|
44
|
+
async with query_store_transaction(self) as scoped:
|
|
45
|
+
return await scoped.submit_project_agent_task(actor, request)
|
|
55
46
|
self._require_personal()
|
|
56
47
|
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
57
48
|
await authorize_personal_project(
|
|
@@ -75,11 +66,32 @@ class StoreProjectAgentTasks(
|
|
|
75
66
|
status_code=409,
|
|
76
67
|
detail='task idempotency key was used with different input',
|
|
77
68
|
)
|
|
69
|
+
await self._repair_task_persistence(request, raw)
|
|
70
|
+
existing = await self._find_task_row(
|
|
71
|
+
request.personal_space_id,
|
|
72
|
+
task_id,
|
|
73
|
+
)
|
|
78
74
|
task = self._task_from_row(existing)
|
|
79
|
-
|
|
75
|
+
recruitments = await self._task_recruitments(
|
|
80
76
|
request.personal_space_id,
|
|
81
77
|
task.task_id,
|
|
82
78
|
)
|
|
79
|
+
if await self._recover_recruitment_lifecycle(
|
|
80
|
+
actor,
|
|
81
|
+
request,
|
|
82
|
+
task,
|
|
83
|
+
recruitments,
|
|
84
|
+
):
|
|
85
|
+
existing = await self._find_task_row(
|
|
86
|
+
request.personal_space_id,
|
|
87
|
+
task_id,
|
|
88
|
+
)
|
|
89
|
+
task = self._task_from_row(existing)
|
|
90
|
+
recruitments = await self._task_recruitments(
|
|
91
|
+
request.personal_space_id,
|
|
92
|
+
task.task_id,
|
|
93
|
+
)
|
|
94
|
+
recruitment = recruitments[0] if recruitments else None
|
|
83
95
|
assigned_agent = await self._route_assigned_agent(
|
|
84
96
|
actor,
|
|
85
97
|
request,
|
|
@@ -89,13 +101,9 @@ class StoreProjectAgentTasks(
|
|
|
89
101
|
task,
|
|
90
102
|
recruitment,
|
|
91
103
|
assigned_agent=assigned_agent,
|
|
104
|
+
recruitments=recruitments,
|
|
92
105
|
)
|
|
93
106
|
|
|
94
|
-
coordinator = await self.ensure_system_project_coordinator(
|
|
95
|
-
actor,
|
|
96
|
-
request.personal_space_id,
|
|
97
|
-
)
|
|
98
|
-
coordinator_agent_id = request.coordinator_agent_id or coordinator.agent_id
|
|
99
107
|
if request.coordinator_agent_id:
|
|
100
108
|
await authorize_project_agent(
|
|
101
109
|
self,
|
|
@@ -158,18 +166,128 @@ class StoreProjectAgentTasks(
|
|
|
158
166
|
'qualified durable Agent candidates'
|
|
159
167
|
)
|
|
160
168
|
|
|
169
|
+
parallel_recruitment_slots = (
|
|
170
|
+
await self._automatic_parallel_recruitment_slots(
|
|
171
|
+
actor,
|
|
172
|
+
request,
|
|
173
|
+
lead=lead,
|
|
174
|
+
participants=participants,
|
|
175
|
+
routing_reason=reason,
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
projected_participants = await self._parallel_plan_participant_projection(
|
|
179
|
+
actor,
|
|
180
|
+
request,
|
|
181
|
+
lead=lead,
|
|
182
|
+
participants=participants,
|
|
183
|
+
routing_reason=reason,
|
|
184
|
+
recruitment_slots=parallel_recruitment_slots,
|
|
185
|
+
)
|
|
186
|
+
self._verify_parallel_plan(request.parallel_plan, projected_participants)
|
|
187
|
+
|
|
188
|
+
will_open_recruitment = (
|
|
189
|
+
request.staffing_intent != 'unassigned'
|
|
190
|
+
and reason != 'manual_agent_selection'
|
|
191
|
+
and (
|
|
192
|
+
bool(parallel_recruitment_slots) or not lead
|
|
193
|
+
or request.staffing_intent in {'new_durable', 'temporary'}
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
if will_open_recruitment:
|
|
197
|
+
_, recruitment_profile = self._normalized_recruitment_profile(request)
|
|
198
|
+
self._require_agent_client_allowed(
|
|
199
|
+
recruitment_profile,
|
|
200
|
+
request.source_application,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
coordinator = await self.ensure_system_project_coordinator(
|
|
204
|
+
actor,
|
|
205
|
+
request.personal_space_id,
|
|
206
|
+
)
|
|
207
|
+
coordinator_agent_id = request.coordinator_agent_id or coordinator.agent_id
|
|
208
|
+
|
|
161
209
|
model_strategy, model_source = self._effective_model_strategy(
|
|
162
210
|
request,
|
|
163
211
|
lead,
|
|
164
212
|
coordinator.profile.default_model_strategy,
|
|
165
213
|
)
|
|
166
214
|
recruitment = None
|
|
215
|
+
recruitments = []
|
|
167
216
|
routing_outcome = 'assigned_existing' if lead else 'unassigned'
|
|
168
217
|
task_status = 'queued' if lead else 'blocked'
|
|
169
218
|
if request.staffing_intent == 'unassigned':
|
|
170
219
|
reason = 'explicit_unassigned'
|
|
171
220
|
routing_outcome = 'unassigned'
|
|
172
221
|
task_status = 'blocked'
|
|
222
|
+
elif reason == 'manual_agent_selection':
|
|
223
|
+
routing_outcome = 'unassigned'
|
|
224
|
+
task_status = 'blocked'
|
|
225
|
+
elif parallel_recruitment_slots:
|
|
226
|
+
recruited_count = 0
|
|
227
|
+
recruited_lead = False
|
|
228
|
+
for slot in parallel_recruitment_slots:
|
|
229
|
+
slot_recruitment, recruited = await self._open_recruitment(
|
|
230
|
+
actor,
|
|
231
|
+
space,
|
|
232
|
+
request,
|
|
233
|
+
task_id,
|
|
234
|
+
coordinator_agent_id,
|
|
235
|
+
reason,
|
|
236
|
+
participant_role=slot['participant_role'],
|
|
237
|
+
recruitment_slot=slot['recruitment_slot'],
|
|
238
|
+
)
|
|
239
|
+
if slot_recruitment:
|
|
240
|
+
recruitments.append(slot_recruitment)
|
|
241
|
+
if slot['participant_role'] == 'lead':
|
|
242
|
+
recruitment = slot_recruitment
|
|
243
|
+
if not recruited:
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
recruited_count += 1
|
|
247
|
+
participant = {
|
|
248
|
+
'agent_id': recruited['agent_id'],
|
|
249
|
+
'role': slot['participant_role'],
|
|
250
|
+
'assignment_summary': recruited.get('responsibility'),
|
|
251
|
+
}
|
|
252
|
+
if not any(
|
|
253
|
+
item['agent_id'] == recruited['agent_id']
|
|
254
|
+
for item in participants
|
|
255
|
+
):
|
|
256
|
+
if slot['participant_role'] == 'lead':
|
|
257
|
+
participants.insert(0, participant)
|
|
258
|
+
else:
|
|
259
|
+
participants.append(participant)
|
|
260
|
+
if slot['participant_role'] == 'lead':
|
|
261
|
+
lead = recruited
|
|
262
|
+
recruited_lead = True
|
|
263
|
+
|
|
264
|
+
if recruitment is None and recruitments:
|
|
265
|
+
recruitment = recruitments[0]
|
|
266
|
+
if recruited_count == len(parallel_recruitment_slots):
|
|
267
|
+
routing_outcome = 'recruited'
|
|
268
|
+
task_status = 'queued'
|
|
269
|
+
match_basis = [
|
|
270
|
+
*match_basis,
|
|
271
|
+
'parallel automatic recruitment: filled '
|
|
272
|
+
f'{recruited_count} missing participant slot(s)',
|
|
273
|
+
'HR recruitment recorded before Agent creation',
|
|
274
|
+
]
|
|
275
|
+
if recruited_lead:
|
|
276
|
+
model_strategy, model_source = self._effective_model_strategy(
|
|
277
|
+
request,
|
|
278
|
+
lead,
|
|
279
|
+
coordinator.profile.default_model_strategy,
|
|
280
|
+
)
|
|
281
|
+
elif any(
|
|
282
|
+
item.status == 'awaiting_confirmation'
|
|
283
|
+
for item in recruitments
|
|
284
|
+
):
|
|
285
|
+
routing_outcome = 'recruitment_required'
|
|
286
|
+
task_status = 'awaiting_recruitment'
|
|
287
|
+
else:
|
|
288
|
+
routing_outcome = 'blocked'
|
|
289
|
+
task_status = 'blocked'
|
|
290
|
+
reason = 'hr_unavailable'
|
|
173
291
|
elif not lead or request.staffing_intent in {'new_durable', 'temporary'}:
|
|
174
292
|
recruitment, recruited = await self._open_recruitment(
|
|
175
293
|
actor,
|
|
@@ -179,6 +297,8 @@ class StoreProjectAgentTasks(
|
|
|
179
297
|
coordinator_agent_id,
|
|
180
298
|
reason,
|
|
181
299
|
)
|
|
300
|
+
if recruitment:
|
|
301
|
+
recruitments.append(recruitment)
|
|
182
302
|
if recruited:
|
|
183
303
|
lead = recruited
|
|
184
304
|
if not any(
|
|
@@ -283,6 +403,7 @@ class StoreProjectAgentTasks(
|
|
|
283
403
|
participants=participants,
|
|
284
404
|
decision=decision,
|
|
285
405
|
recruitment=recruitment,
|
|
406
|
+
recruitments=recruitments,
|
|
286
407
|
executor_decision=executor_decision,
|
|
287
408
|
created_at=now,
|
|
288
409
|
)
|
|
@@ -300,6 +421,7 @@ class StoreProjectAgentTasks(
|
|
|
300
421
|
task,
|
|
301
422
|
recruitment,
|
|
302
423
|
assigned_agent=assigned_agent,
|
|
424
|
+
recruitments=recruitments,
|
|
303
425
|
)
|
|
304
426
|
|
|
305
427
|
async def get_project_agent_task(
|
|
@@ -307,19 +429,33 @@ class StoreProjectAgentTasks(
|
|
|
307
429
|
actor: dict,
|
|
308
430
|
personal_space_id: str,
|
|
309
431
|
task_id: str,
|
|
432
|
+
*,
|
|
433
|
+
personal_project_id: str | None = None,
|
|
310
434
|
) -> ProjectAgentTaskRecord:
|
|
311
435
|
self._require_personal()
|
|
312
436
|
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
313
|
-
|
|
437
|
+
if personal_project_id:
|
|
438
|
+
await authorize_personal_project(
|
|
439
|
+
self,
|
|
440
|
+
actor,
|
|
441
|
+
space,
|
|
442
|
+
personal_project_id,
|
|
443
|
+
)
|
|
444
|
+
row = await self._find_task_row(
|
|
445
|
+
personal_space_id,
|
|
446
|
+
task_id,
|
|
447
|
+
personal_project_id,
|
|
448
|
+
)
|
|
314
449
|
if not row:
|
|
315
450
|
raise HTTPException(status_code=404, detail='project Agent task not found')
|
|
316
451
|
task = dict(row['task'])
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
452
|
+
if not personal_project_id:
|
|
453
|
+
await authorize_personal_project(
|
|
454
|
+
self,
|
|
455
|
+
actor,
|
|
456
|
+
space,
|
|
457
|
+
task['personal_project_id'],
|
|
458
|
+
)
|
|
323
459
|
return self._task_from_row(row)
|
|
324
460
|
|
|
325
461
|
async def list_project_agent_tasks(
|
|
@@ -362,349 +498,6 @@ class StoreProjectAgentTasks(
|
|
|
362
498
|
)
|
|
363
499
|
return [self._task_from_row(row) for row in records]
|
|
364
500
|
|
|
365
|
-
async def record_project_agent_task_activity(
|
|
366
|
-
self,
|
|
367
|
-
actor: dict,
|
|
368
|
-
request: ProjectAgentTaskActivityCreate,
|
|
369
|
-
) -> ProjectAgentTaskRecord:
|
|
370
|
-
self._require_personal()
|
|
371
|
-
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
372
|
-
await authorize_personal_project(
|
|
373
|
-
self,
|
|
374
|
-
actor,
|
|
375
|
-
space,
|
|
376
|
-
request.personal_project_id,
|
|
377
|
-
)
|
|
378
|
-
row = await self._find_task_row(request.personal_space_id, request.task_id)
|
|
379
|
-
if not row:
|
|
380
|
-
raise HTTPException(status_code=404, detail='project Agent task not found')
|
|
381
|
-
raw_task = dict(row['task'])
|
|
382
|
-
if raw_task['personal_project_id'] != request.personal_project_id:
|
|
383
|
-
raise HTTPException(status_code=404, detail='project Agent task not found')
|
|
384
|
-
event_id = stable_uuid(
|
|
385
|
-
self.settings.provider_id,
|
|
386
|
-
request.personal_space_id,
|
|
387
|
-
'project-agent-task-event',
|
|
388
|
-
request.task_id,
|
|
389
|
-
request.idempotency_key,
|
|
390
|
-
)
|
|
391
|
-
payload_hash = self._payload_hash(request)
|
|
392
|
-
existing_event = next(
|
|
393
|
-
(
|
|
394
|
-
dict(value)
|
|
395
|
-
for value in (row.get('event_rows') or [])
|
|
396
|
-
if value
|
|
397
|
-
and (value.get('event_id') or value.get('id')) == event_id
|
|
398
|
-
),
|
|
399
|
-
None,
|
|
400
|
-
)
|
|
401
|
-
if existing_event:
|
|
402
|
-
if existing_event.get('payload_hash') != payload_hash:
|
|
403
|
-
raise HTTPException(
|
|
404
|
-
status_code=409,
|
|
405
|
-
detail=(
|
|
406
|
-
'task event idempotency key was used with different input'
|
|
407
|
-
),
|
|
408
|
-
)
|
|
409
|
-
return self._task_from_row(row)
|
|
410
|
-
self._validate_task_transition(
|
|
411
|
-
raw_task.get('status'),
|
|
412
|
-
request.status,
|
|
413
|
-
has_actual_executor=bool(request.actual_executor_id),
|
|
414
|
-
)
|
|
415
|
-
expected_revision = getattr(request, 'expected_revision', None)
|
|
416
|
-
if expected_revision is None:
|
|
417
|
-
expected_revision = raw_task.get('revision', 0)
|
|
418
|
-
if request.agent_id:
|
|
419
|
-
participant = next(
|
|
420
|
-
(
|
|
421
|
-
item for item in self._participant_maps(row)
|
|
422
|
-
if item['agent_id'] == request.agent_id
|
|
423
|
-
),
|
|
424
|
-
None,
|
|
425
|
-
)
|
|
426
|
-
if not participant:
|
|
427
|
-
raise HTTPException(
|
|
428
|
-
status_code=409,
|
|
429
|
-
detail='Agent is not assigned to this task',
|
|
430
|
-
)
|
|
431
|
-
if participant.get('profile_json'):
|
|
432
|
-
self._require_agent_client_allowed(
|
|
433
|
-
{
|
|
434
|
-
'agent_id': request.agent_id,
|
|
435
|
-
'profile': ProjectAgentProfile.model_validate_json(
|
|
436
|
-
participant['profile_json']
|
|
437
|
-
),
|
|
438
|
-
},
|
|
439
|
-
request.source_application,
|
|
440
|
-
)
|
|
441
|
-
now = now_utc()
|
|
442
|
-
terminal_at = now if request.status in TERMINAL_TASK_STATUSES else None
|
|
443
|
-
actual_executor_id = getattr(request, 'actual_executor_id', None)
|
|
444
|
-
matched_executor_rule_id = getattr(request, 'matched_executor_rule_id', None)
|
|
445
|
-
executor_selection_reason = getattr(request, 'executor_selection_reason', None)
|
|
446
|
-
executor_fallback_reason = getattr(request, 'executor_fallback_reason', None)
|
|
447
|
-
executor_blocked_reason = getattr(request, 'executor_blocked_reason', None)
|
|
448
|
-
worker_id = getattr(request, 'worker_id', None)
|
|
449
|
-
worker_label = getattr(request, 'worker_label', None)
|
|
450
|
-
worker_occupation_emoji = getattr(
|
|
451
|
-
request,
|
|
452
|
-
'worker_occupation_emoji',
|
|
453
|
-
None,
|
|
454
|
-
)
|
|
455
|
-
worker_status = getattr(request, 'worker_status', None)
|
|
456
|
-
actual_recorder = None
|
|
457
|
-
actual_report = None
|
|
458
|
-
if actual_executor_id:
|
|
459
|
-
actual_recorder = getattr(
|
|
460
|
-
self,
|
|
461
|
-
'record_project_agent_executor_actual',
|
|
462
|
-
None,
|
|
463
|
-
)
|
|
464
|
-
if not callable(actual_recorder):
|
|
465
|
-
raise HTTPException(
|
|
466
|
-
status_code=503,
|
|
467
|
-
detail='executor evidence control plane is unavailable',
|
|
468
|
-
)
|
|
469
|
-
strategy = (
|
|
470
|
-
ProjectAgentModelStrategy.model_validate_json(
|
|
471
|
-
raw_task['effective_model_strategy_json']
|
|
472
|
-
)
|
|
473
|
-
if raw_task.get('effective_model_strategy_json')
|
|
474
|
-
else ProjectAgentModelStrategy()
|
|
475
|
-
)
|
|
476
|
-
actual_report = ProjectAgentExecutorActualReport(
|
|
477
|
-
personal_space_id=request.personal_space_id,
|
|
478
|
-
personal_project_id=request.personal_project_id,
|
|
479
|
-
task_id=request.task_id,
|
|
480
|
-
run_id=event_id,
|
|
481
|
-
agent_id=request.agent_id,
|
|
482
|
-
executor_id=actual_executor_id,
|
|
483
|
-
provider=request.actual_model_provider,
|
|
484
|
-
model=request.actual_model,
|
|
485
|
-
model_strategy=strategy,
|
|
486
|
-
model_strategy_source=(
|
|
487
|
-
raw_task.get('model_strategy_source') or 'coordinator'
|
|
488
|
-
),
|
|
489
|
-
matched_rule_id=matched_executor_rule_id,
|
|
490
|
-
fallback_reason=executor_fallback_reason,
|
|
491
|
-
idempotency_key=f'actual:{event_id}',
|
|
492
|
-
occurred_at=now,
|
|
493
|
-
source_application=request.source_application,
|
|
494
|
-
source_session_id=request.source_session_id,
|
|
495
|
-
)
|
|
496
|
-
records, _, _ = await self.runtime.driver.execute_query(
|
|
497
|
-
'''
|
|
498
|
-
MATCH (space:FuliSpace {
|
|
499
|
-
id: $personal_space_id, kind: 'personal'
|
|
500
|
-
})-[:HAS_PROJECT_AGENT_TASK]->
|
|
501
|
-
(task:FuliProjectAgentTask {
|
|
502
|
-
personal_space_id: $personal_space_id,
|
|
503
|
-
task_id: $task_id
|
|
504
|
-
})
|
|
505
|
-
WHERE task.revision = $expected_revision
|
|
506
|
-
OR EXISTS {
|
|
507
|
-
MATCH (task)-[:HAS_TASK_EVENT]->
|
|
508
|
-
(:FuliProjectAgentTaskEvent {id: $event_id})
|
|
509
|
-
}
|
|
510
|
-
OPTIONAL MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
511
|
-
(agent:FuliProjectAgent {agent_id: $agent_id})
|
|
512
|
-
WHERE $agent_id IS NULL OR EXISTS {
|
|
513
|
-
MATCH (task)-[:HAS_PARTICIPANT]->(agent)
|
|
514
|
-
}
|
|
515
|
-
MERGE (event:FuliProjectAgentTaskEvent {id: $event_id})
|
|
516
|
-
ON CREATE SET event.event_id = $event_id,
|
|
517
|
-
event.task_id = $task_id,
|
|
518
|
-
event.agent_id = $agent_id,
|
|
519
|
-
event.status = $status,
|
|
520
|
-
event.actor_kind = $actor_kind,
|
|
521
|
-
event.summary = $summary,
|
|
522
|
-
event.payload_hash = $payload_hash,
|
|
523
|
-
event.source_application = $source_application,
|
|
524
|
-
event.source_session_id = $source_session_id,
|
|
525
|
-
event.actual_model_provider = $actual_model_provider,
|
|
526
|
-
event.actual_model = $actual_model,
|
|
527
|
-
event.actual_executor_id = $actual_executor_id,
|
|
528
|
-
event.matched_executor_rule_id =
|
|
529
|
-
$matched_executor_rule_id,
|
|
530
|
-
event.executor_selection_reason =
|
|
531
|
-
$executor_selection_reason,
|
|
532
|
-
event.executor_fallback_reason =
|
|
533
|
-
$executor_fallback_reason,
|
|
534
|
-
event.executor_blocked_reason =
|
|
535
|
-
$executor_blocked_reason,
|
|
536
|
-
event.worker_id = $worker_id,
|
|
537
|
-
event.worker_label = $worker_label,
|
|
538
|
-
event.worker_occupation_emoji = $worker_occupation_emoji,
|
|
539
|
-
event.worker_status = $worker_status,
|
|
540
|
-
event.activity_date = CASE
|
|
541
|
-
WHEN $terminal_at IS NULL THEN null
|
|
542
|
-
ELSE date($terminal_at) END,
|
|
543
|
-
event.created_at = $updated_at
|
|
544
|
-
WITH task, event, agent,
|
|
545
|
-
event.payload_hash = $payload_hash AS same_payload,
|
|
546
|
-
task.revision = $expected_revision AS applied_transition
|
|
547
|
-
FOREACH (_ IN CASE
|
|
548
|
-
WHEN same_payload AND applied_transition
|
|
549
|
-
THEN [1] ELSE [] END |
|
|
550
|
-
MERGE (task)-[:HAS_TASK_EVENT]->(event)
|
|
551
|
-
)
|
|
552
|
-
FOREACH (_ IN CASE
|
|
553
|
-
WHEN same_payload AND applied_transition AND agent IS NOT NULL
|
|
554
|
-
THEN [1] ELSE [] END |
|
|
555
|
-
MERGE (event)-[:EVENT_AGENT]->(agent)
|
|
556
|
-
)
|
|
557
|
-
FOREACH (_ IN CASE
|
|
558
|
-
WHEN same_payload AND applied_transition
|
|
559
|
-
THEN [1] ELSE [] END |
|
|
560
|
-
SET task.status = $status,
|
|
561
|
-
task.result_summary = CASE
|
|
562
|
-
WHEN $status = 'completed' THEN $summary
|
|
563
|
-
ELSE task.result_summary END,
|
|
564
|
-
task.failure_reason = CASE
|
|
565
|
-
WHEN $status = 'failed' THEN $summary
|
|
566
|
-
ELSE task.failure_reason END,
|
|
567
|
-
task.actual_executor_id = coalesce(
|
|
568
|
-
$actual_executor_id, task.actual_executor_id),
|
|
569
|
-
task.actual_model_provider = coalesce(
|
|
570
|
-
$actual_model_provider, task.actual_model_provider),
|
|
571
|
-
task.actual_model = coalesce($actual_model, task.actual_model),
|
|
572
|
-
task.matched_executor_rule_id = coalesce(
|
|
573
|
-
$matched_executor_rule_id,
|
|
574
|
-
task.matched_executor_rule_id),
|
|
575
|
-
task.executor_selection_reason = coalesce(
|
|
576
|
-
$executor_selection_reason,
|
|
577
|
-
task.executor_selection_reason),
|
|
578
|
-
task.executor_fallback_reason = coalesce(
|
|
579
|
-
$executor_fallback_reason,
|
|
580
|
-
task.executor_fallback_reason),
|
|
581
|
-
task.executor_blocked_reason = coalesce(
|
|
582
|
-
$executor_blocked_reason,
|
|
583
|
-
task.executor_blocked_reason),
|
|
584
|
-
task.completed_at = $terminal_at,
|
|
585
|
-
task.updated_at = $updated_at,
|
|
586
|
-
task.revision = task.revision + 1
|
|
587
|
-
)
|
|
588
|
-
FOREACH (_ IN CASE
|
|
589
|
-
WHEN same_payload AND applied_transition AND agent IS NOT NULL
|
|
590
|
-
THEN [1] ELSE [] END |
|
|
591
|
-
SET task.lead_agent_id = CASE
|
|
592
|
-
WHEN task.lead_agent_id IS NULL AND $status = 'running'
|
|
593
|
-
THEN agent.agent_id ELSE task.lead_agent_id END
|
|
594
|
-
)
|
|
595
|
-
WITH task, event, same_payload, applied_transition
|
|
596
|
-
OPTIONAL MATCH (task)-[participant:HAS_PARTICIPANT]->
|
|
597
|
-
(participant_agent:FuliProjectAgent)
|
|
598
|
-
FOREACH (_ IN CASE
|
|
599
|
-
WHEN same_payload
|
|
600
|
-
AND applied_transition
|
|
601
|
-
AND participant IS NOT NULL
|
|
602
|
-
AND NOT (coalesce(participant.status, '') IN
|
|
603
|
-
['completed', 'failed', 'cancelled'])
|
|
604
|
-
AND (
|
|
605
|
-
$terminal_at IS NOT NULL
|
|
606
|
-
OR $agent_id IS NULL
|
|
607
|
-
OR participant_agent.agent_id = $agent_id
|
|
608
|
-
)
|
|
609
|
-
THEN [1] ELSE [] END |
|
|
610
|
-
SET participant.status = $status,
|
|
611
|
-
participant.updated_at = $updated_at,
|
|
612
|
-
participant.ended_at = $terminal_at
|
|
613
|
-
)
|
|
614
|
-
RETURN DISTINCT task, event, same_payload, applied_transition
|
|
615
|
-
''',
|
|
616
|
-
personal_space_id=request.personal_space_id,
|
|
617
|
-
task_id=request.task_id,
|
|
618
|
-
expected_revision=expected_revision,
|
|
619
|
-
event_id=event_id,
|
|
620
|
-
agent_id=request.agent_id,
|
|
621
|
-
status=request.status,
|
|
622
|
-
actor_kind=request.actor_kind,
|
|
623
|
-
summary=request.summary,
|
|
624
|
-
payload_hash=payload_hash,
|
|
625
|
-
source_application=request.source_application,
|
|
626
|
-
source_session_id=request.source_session_id,
|
|
627
|
-
actual_model_provider=request.actual_model_provider,
|
|
628
|
-
actual_model=request.actual_model,
|
|
629
|
-
actual_executor_id=actual_executor_id,
|
|
630
|
-
matched_executor_rule_id=matched_executor_rule_id,
|
|
631
|
-
executor_selection_reason=executor_selection_reason,
|
|
632
|
-
executor_fallback_reason=executor_fallback_reason,
|
|
633
|
-
executor_blocked_reason=executor_blocked_reason,
|
|
634
|
-
worker_id=worker_id,
|
|
635
|
-
worker_label=worker_label,
|
|
636
|
-
worker_occupation_emoji=worker_occupation_emoji,
|
|
637
|
-
worker_status=worker_status,
|
|
638
|
-
terminal_at=terminal_at,
|
|
639
|
-
updated_at=now,
|
|
640
|
-
)
|
|
641
|
-
if not records:
|
|
642
|
-
raise HTTPException(
|
|
643
|
-
status_code=409,
|
|
644
|
-
detail='task revision is stale or participant is invalid',
|
|
645
|
-
)
|
|
646
|
-
if not records[0]['same_payload']:
|
|
647
|
-
raise HTTPException(
|
|
648
|
-
status_code=409,
|
|
649
|
-
detail='task event idempotency key was used with different input',
|
|
650
|
-
)
|
|
651
|
-
if not records[0].get('applied_transition'):
|
|
652
|
-
return await self.get_project_agent_task(
|
|
653
|
-
actor,
|
|
654
|
-
request.personal_space_id,
|
|
655
|
-
request.task_id,
|
|
656
|
-
)
|
|
657
|
-
if actual_report:
|
|
658
|
-
await actual_recorder(actor, actual_report)
|
|
659
|
-
evidence_status = (
|
|
660
|
-
request.status
|
|
661
|
-
if request.status in TERMINAL_TASK_STATUSES
|
|
662
|
-
else worker_status
|
|
663
|
-
)
|
|
664
|
-
if (
|
|
665
|
-
evidence_status in TERMINAL_TASK_STATUSES
|
|
666
|
-
and actual_executor_id
|
|
667
|
-
and request.agent_id
|
|
668
|
-
):
|
|
669
|
-
evidence_recorder = getattr(
|
|
670
|
-
self,
|
|
671
|
-
'record_project_agent_executor_outcome_evidence',
|
|
672
|
-
None,
|
|
673
|
-
)
|
|
674
|
-
if callable(evidence_recorder):
|
|
675
|
-
strategy = (
|
|
676
|
-
ProjectAgentModelStrategy.model_validate_json(
|
|
677
|
-
raw_task['effective_model_strategy_json']
|
|
678
|
-
)
|
|
679
|
-
if raw_task.get('effective_model_strategy_json')
|
|
680
|
-
else ProjectAgentModelStrategy()
|
|
681
|
-
)
|
|
682
|
-
await evidence_recorder(
|
|
683
|
-
actor,
|
|
684
|
-
ProjectAgentExecutorOutcomeEvidenceCreate(
|
|
685
|
-
personal_space_id=request.personal_space_id,
|
|
686
|
-
personal_project_id=request.personal_project_id,
|
|
687
|
-
work_kind=raw_task['work_kind'],
|
|
688
|
-
agent_id=request.agent_id,
|
|
689
|
-
executor_id=actual_executor_id,
|
|
690
|
-
task_id=request.task_id,
|
|
691
|
-
run_id=event_id,
|
|
692
|
-
model_strategy=strategy,
|
|
693
|
-
evidence_kind='terminal_outcome',
|
|
694
|
-
source='system_terminal',
|
|
695
|
-
terminal_outcome=evidence_status,
|
|
696
|
-
reference_ids=[event_id],
|
|
697
|
-
idempotency_key=f'terminal:{event_id}',
|
|
698
|
-
occurred_at=now,
|
|
699
|
-
),
|
|
700
|
-
)
|
|
701
|
-
await self._archive_finished_temporary_agent(request, raw_task, now)
|
|
702
|
-
return await self.get_project_agent_task(
|
|
703
|
-
actor,
|
|
704
|
-
request.personal_space_id,
|
|
705
|
-
request.task_id,
|
|
706
|
-
)
|
|
707
|
-
|
|
708
501
|
async def get_project_agent_activity(
|
|
709
502
|
self,
|
|
710
503
|
actor: dict,
|
|
@@ -712,9 +505,18 @@ class StoreProjectAgentTasks(
|
|
|
712
505
|
agent_id: str,
|
|
713
506
|
from_date: date,
|
|
714
507
|
to_date: date,
|
|
508
|
+
*,
|
|
509
|
+
personal_project_id: str | None = None,
|
|
715
510
|
) -> ProjectAgentActivityResult:
|
|
716
511
|
self._require_personal()
|
|
717
|
-
await self.authorize(actor, personal_space_id, 'reader')
|
|
512
|
+
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
513
|
+
if personal_project_id:
|
|
514
|
+
await authorize_personal_project(
|
|
515
|
+
self,
|
|
516
|
+
actor,
|
|
517
|
+
space,
|
|
518
|
+
personal_project_id,
|
|
519
|
+
)
|
|
718
520
|
if from_date > to_date:
|
|
719
521
|
raise HTTPException(status_code=422, detail='from date must not exceed to date')
|
|
720
522
|
records, _, _ = await self.runtime.driver.execute_query(
|
|
@@ -726,12 +528,18 @@ class StoreProjectAgentTasks(
|
|
|
726
528
|
(task:FuliProjectAgentTask)-[:HAS_TASK_EVENT]->
|
|
727
529
|
(event:FuliProjectAgentTaskEvent)-[:EVENT_AGENT]->(agent)
|
|
728
530
|
WHERE event.status IN ['completed', 'failed', 'cancelled']
|
|
531
|
+
AND ($personal_project_id IS NULL
|
|
532
|
+
OR task.personal_project_id = $personal_project_id)
|
|
729
533
|
AND event.activity_date >= date($from_date)
|
|
730
534
|
AND event.activity_date <= date($to_date)
|
|
731
|
-
|
|
535
|
+
WITH task, event, agent
|
|
536
|
+
OPTIONAL MATCH (task)-[:HAS_TASK_EVENT]->(evidence_event:FuliProjectAgentTaskEvent)-[:EVENT_AGENT]->(agent)
|
|
537
|
+
WHERE evidence_event.created_at <= event.created_at
|
|
538
|
+
RETURN event, task.title AS title, collect(evidence_event) AS evidence_events
|
|
732
539
|
ORDER BY event.created_at DESC, event.event_id
|
|
733
540
|
''',
|
|
734
541
|
personal_space_id=personal_space_id,
|
|
542
|
+
personal_project_id=personal_project_id,
|
|
735
543
|
agent_id=agent_id,
|
|
736
544
|
from_date=from_date.isoformat(),
|
|
737
545
|
to_date=to_date.isoformat(),
|
|
@@ -739,7 +547,9 @@ class StoreProjectAgentTasks(
|
|
|
739
547
|
)
|
|
740
548
|
grouped = defaultdict(list)
|
|
741
549
|
for row in records:
|
|
742
|
-
event =
|
|
550
|
+
event = self._event_with_latest_execution_evidence(
|
|
551
|
+
row['event'], row.get('evidence_events') or []
|
|
552
|
+
)
|
|
743
553
|
occurred_at = native_datetime(event['created_at'])
|
|
744
554
|
grouped[occurred_at.date()].append(ProjectAgentActivityTask(
|
|
745
555
|
task_id=event['task_id'],
|
|
@@ -777,7 +587,6 @@ class StoreProjectAgentTasks(
|
|
|
777
587
|
|
|
778
588
|
async def _route_assigned_agent(self, actor, request, task):
|
|
779
589
|
"""Project the selected durable identity into the route response."""
|
|
780
|
-
|
|
781
590
|
agent_id = getattr(task, 'lead_agent_id', None)
|
|
782
591
|
getter = getattr(self, 'get_project_agent', None)
|
|
783
592
|
if not agent_id or not callable(getter):
|
|
@@ -891,7 +700,7 @@ class StoreProjectAgentTasks(
|
|
|
891
700
|
task_id=task_id,
|
|
892
701
|
agent_id=selected['agent_id'],
|
|
893
702
|
work_kind=request.work_kind,
|
|
894
|
-
required_capabilities=request.
|
|
703
|
+
required_capabilities=request.executor_capability_hints,
|
|
895
704
|
model_strategy=model_strategy,
|
|
896
705
|
task_override=(
|
|
897
706
|
getattr(request, 'executor_policy_override', None)
|
|
@@ -912,9 +721,6 @@ class StoreProjectAgentTasks(
|
|
|
912
721
|
return {}
|
|
913
722
|
raw = decision.model_dump(mode='json') if hasattr(decision, 'model_dump') else dict(decision)
|
|
914
723
|
aliases = {
|
|
915
|
-
# The routing decision audit field is JSON data. Keep the
|
|
916
|
-
# provider-neutral snapshot instead of leaking a Pydantic model
|
|
917
|
-
# into the parent record (which fails validation in real routing).
|
|
918
724
|
'executor_decision': raw,
|
|
919
725
|
'selected_executor_id': raw.get('selected_executor_id') or raw.get('executor_id'),
|
|
920
726
|
'matched_executor_rule_id': raw.get('matched_rule_id') or raw.get('rule_id'),
|
|
@@ -928,26 +734,15 @@ class StoreProjectAgentTasks(
|
|
|
928
734
|
if key in ProjectAgentRoutingDecisionRecord.model_fields
|
|
929
735
|
}
|
|
930
736
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
for key, value in {
|
|
936
|
-
'actual_executor_id': event.get('actual_executor_id'),
|
|
937
|
-
'matched_executor_rule_id': event.get('matched_executor_rule_id'),
|
|
938
|
-
'worker_id': event.get('worker_id'),
|
|
939
|
-
'worker_label': event.get('worker_label'),
|
|
940
|
-
'worker_occupation_emoji': event.get(
|
|
941
|
-
'worker_occupation_emoji'
|
|
942
|
-
),
|
|
943
|
-
'worker_status': event.get('worker_status'),
|
|
944
|
-
}.items()
|
|
945
|
-
if key in ProjectAgentActivityTask.model_fields
|
|
946
|
-
}
|
|
737
|
+
async def _persist_task(self, request, **values):
|
|
738
|
+
# The task must not become visible before all of its planned edges exist.
|
|
739
|
+
async with self.runtime.driver.transaction() as transaction:
|
|
740
|
+
await self._persist_task_transaction(request, transaction, **values)
|
|
947
741
|
|
|
948
|
-
async def
|
|
742
|
+
async def _persist_task_transaction(
|
|
949
743
|
self,
|
|
950
744
|
request,
|
|
745
|
+
transaction,
|
|
951
746
|
*,
|
|
952
747
|
task_id,
|
|
953
748
|
payload_hash,
|
|
@@ -967,7 +762,24 @@ class StoreProjectAgentTasks(
|
|
|
967
762
|
recruitment,
|
|
968
763
|
executor_decision,
|
|
969
764
|
created_at,
|
|
765
|
+
recruitments=None,
|
|
970
766
|
):
|
|
767
|
+
linked_recruitments = list(recruitments or [])
|
|
768
|
+
if recruitment and not any(
|
|
769
|
+
item.recruitment_id == recruitment.recruitment_id
|
|
770
|
+
for item in linked_recruitments
|
|
771
|
+
):
|
|
772
|
+
linked_recruitments.insert(0, recruitment)
|
|
773
|
+
recruitment_links = [
|
|
774
|
+
{
|
|
775
|
+
'recruitment_id': item.recruitment_id,
|
|
776
|
+
'is_primary': bool(
|
|
777
|
+
recruitment
|
|
778
|
+
and item.recruitment_id == recruitment.recruitment_id
|
|
779
|
+
),
|
|
780
|
+
}
|
|
781
|
+
for item in linked_recruitments
|
|
782
|
+
]
|
|
971
783
|
decision_raw = decision.model_dump(mode='json')
|
|
972
784
|
executor_raw = (
|
|
973
785
|
executor_decision.model_dump(mode='json')
|
|
@@ -981,15 +793,15 @@ class StoreProjectAgentTasks(
|
|
|
981
793
|
task_id,
|
|
982
794
|
'created',
|
|
983
795
|
)
|
|
984
|
-
await
|
|
796
|
+
result = await transaction.run(
|
|
985
797
|
'''
|
|
986
798
|
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})
|
|
987
799
|
MATCH (space)-[:CONTAINS_PROJECT]->
|
|
988
800
|
(project:FuliPersonalProject {project_id: $personal_project_id})
|
|
989
801
|
MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
990
802
|
(coordinator:FuliProjectAgent {agent_id: $coordinator_agent_id})
|
|
991
|
-
|
|
992
|
-
|
|
803
|
+
MERGE (task:FuliProjectAgentTask {id: $task_id})
|
|
804
|
+
ON CREATE SET task += {
|
|
993
805
|
task_id: $task_id,
|
|
994
806
|
personal_space_id: $personal_space_id,
|
|
995
807
|
personal_project_id: $personal_project_id,
|
|
@@ -998,6 +810,7 @@ class StoreProjectAgentTasks(
|
|
|
998
810
|
objective: $objective,
|
|
999
811
|
work_kind: $work_kind,
|
|
1000
812
|
required_capabilities: $required_capabilities,
|
|
813
|
+
executor_capability_hints: $executor_capability_hints,
|
|
1001
814
|
duration: $duration,
|
|
1002
815
|
staffing_intent: $staffing_intent,
|
|
1003
816
|
status: $status,
|
|
@@ -1021,32 +834,39 @@ class StoreProjectAgentTasks(
|
|
|
1021
834
|
executor_decision_json: $executor_decision_json,
|
|
1022
835
|
executor_policy_json: $executor_policy_json,
|
|
1023
836
|
selected_executor_id: $selected_executor_id,
|
|
837
|
+
participant_plan_json: $participant_plan_json,
|
|
838
|
+
recruitment_plan_json: $recruitment_plan_json,
|
|
1024
839
|
created_at: $created_at,
|
|
1025
840
|
updated_at: $created_at
|
|
841
|
+
}
|
|
842
|
+
WITH space, project, coordinator, task
|
|
843
|
+
WHERE task.payload_hash = $payload_hash
|
|
844
|
+
MERGE (decision_node:FuliProjectAgentRoutingDecision {
|
|
845
|
+
id: $decision_id
|
|
1026
846
|
})
|
|
1027
|
-
CREATE
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
summary: $routing_explanation,
|
|
1041
|
-
source_application: $source_application,
|
|
1042
|
-
source_session_id: $source_session_id,
|
|
1043
|
-
created_at: $created_at
|
|
1044
|
-
})
|
|
847
|
+
ON CREATE SET decision_node.decision_id = $decision_id,
|
|
848
|
+
decision_node.task_id = $task_id,
|
|
849
|
+
decision_node.decision_json = $decision_json,
|
|
850
|
+
decision_node.created_at = $created_at
|
|
851
|
+
MERGE (event:FuliProjectAgentTaskEvent {id: $initial_event_id})
|
|
852
|
+
ON CREATE SET event.event_id = $initial_event_id,
|
|
853
|
+
event.task_id = $task_id,
|
|
854
|
+
event.status = $status,
|
|
855
|
+
event.actor_kind = 'system',
|
|
856
|
+
event.summary = $routing_explanation,
|
|
857
|
+
event.source_application = $source_application,
|
|
858
|
+
event.source_session_id = $source_session_id,
|
|
859
|
+
event.created_at = $created_at
|
|
1045
860
|
MERGE (space)-[:HAS_PROJECT_AGENT_TASK]->(task)
|
|
1046
861
|
MERGE (project)-[:HAS_PROJECT_AGENT_TASK]->(task)
|
|
1047
862
|
MERGE (task)-[:COORDINATED_BY]->(coordinator)
|
|
1048
863
|
MERGE (task)-[:ROUTED_BY]->(decision_node)
|
|
1049
864
|
MERGE (task)-[:HAS_TASK_EVENT]->(event)
|
|
865
|
+
RETURN true AS payload_matches,
|
|
866
|
+
task.participant_plan_json AS participant_plan_json,
|
|
867
|
+
task.recruitment_plan_json AS recruitment_plan_json,
|
|
868
|
+
task.status AS persisted_status,
|
|
869
|
+
task.created_at AS persisted_created_at
|
|
1050
870
|
''',
|
|
1051
871
|
task_id=task_id,
|
|
1052
872
|
personal_space_id=request.personal_space_id,
|
|
@@ -1056,6 +876,7 @@ class StoreProjectAgentTasks(
|
|
|
1056
876
|
objective=request.objective,
|
|
1057
877
|
work_kind=request.work_kind,
|
|
1058
878
|
required_capabilities=request.required_capabilities,
|
|
879
|
+
executor_capability_hints=request.executor_capability_hints,
|
|
1059
880
|
duration=request.duration,
|
|
1060
881
|
staffing_intent=request.staffing_intent,
|
|
1061
882
|
status=status,
|
|
@@ -1094,51 +915,30 @@ class StoreProjectAgentTasks(
|
|
|
1094
915
|
(executor_raw or {}).get('selected_executor_id')
|
|
1095
916
|
or (executor_raw or {}).get('executor_id')
|
|
1096
917
|
),
|
|
918
|
+
participant_plan_json=json.dumps(participants, sort_keys=True),
|
|
919
|
+
recruitment_plan_json=json.dumps(recruitment_links, sort_keys=True),
|
|
1097
920
|
created_at=created_at,
|
|
1098
921
|
decision_id=decision.decision_id,
|
|
1099
922
|
decision_json=json.dumps(decision_raw, sort_keys=True),
|
|
1100
923
|
initial_event_id=initial_event_id,
|
|
1101
924
|
)
|
|
1102
|
-
for
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
})-[:HAS_PROJECT_AGENT_TASK]->
|
|
1108
|
-
(task:FuliProjectAgentTask {task_id: $task_id})
|
|
1109
|
-
MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
1110
|
-
(agent:FuliProjectAgent {agent_id: $agent_id})
|
|
1111
|
-
MERGE (task)-[participant:HAS_PARTICIPANT]->(agent)
|
|
1112
|
-
SET participant.role = $role,
|
|
1113
|
-
participant.status = $status,
|
|
1114
|
-
participant.assignment_summary = $assignment_summary,
|
|
1115
|
-
participant.joined_at = $created_at,
|
|
1116
|
-
participant.updated_at = $created_at
|
|
1117
|
-
''',
|
|
1118
|
-
personal_space_id=request.personal_space_id,
|
|
1119
|
-
task_id=task_id,
|
|
1120
|
-
agent_id=participant['agent_id'],
|
|
1121
|
-
role=participant['role'],
|
|
1122
|
-
status=status,
|
|
1123
|
-
assignment_summary=participant.get('assignment_summary'),
|
|
1124
|
-
created_at=created_at,
|
|
1125
|
-
)
|
|
1126
|
-
if recruitment:
|
|
1127
|
-
await self.runtime.driver.execute_query(
|
|
1128
|
-
'''
|
|
1129
|
-
MATCH (space:FuliSpace {
|
|
1130
|
-
id: $personal_space_id, kind: 'personal'
|
|
1131
|
-
})-[:HAS_PROJECT_AGENT_TASK]->
|
|
1132
|
-
(task:FuliProjectAgentTask {task_id: $task_id})
|
|
1133
|
-
MATCH (space)-[:HAS_PROJECT_AGENT_RECRUITMENT]->
|
|
1134
|
-
(recruitment:FuliProjectAgentRecruitment {
|
|
1135
|
-
recruitment_id: $recruitment_id
|
|
1136
|
-
})
|
|
1137
|
-
SET task.recruitment_id = $recruitment_id,
|
|
1138
|
-
task.hr_agent_id = recruitment.hr_agent_id
|
|
1139
|
-
MERGE (task)-[:TRIGGERED_RECRUITMENT]->(recruitment)
|
|
1140
|
-
''',
|
|
1141
|
-
personal_space_id=request.personal_space_id,
|
|
1142
|
-
task_id=task_id,
|
|
1143
|
-
recruitment_id=recruitment.recruitment_id,
|
|
925
|
+
records = [record async for record in result]
|
|
926
|
+
if not records or not dict(records[0]).get('payload_matches'):
|
|
927
|
+
raise HTTPException(
|
|
928
|
+
status_code=409,
|
|
929
|
+
detail='task idempotency key was used with different input',
|
|
1144
930
|
)
|
|
931
|
+
persisted = dict(records[0])
|
|
932
|
+
await self._persist_task_links(
|
|
933
|
+
request,
|
|
934
|
+
transaction=transaction,
|
|
935
|
+
task_id=task_id,
|
|
936
|
+
participants=self._task_link_plan(
|
|
937
|
+
persisted.get('participant_plan_json')
|
|
938
|
+
),
|
|
939
|
+
recruitment_links=self._task_link_plan(
|
|
940
|
+
persisted.get('recruitment_plan_json')
|
|
941
|
+
),
|
|
942
|
+
status=persisted.get('persisted_status') or status,
|
|
943
|
+
created_at=persisted.get('persisted_created_at') or created_at,
|
|
944
|
+
)
|