fuli-context 0.7.6 → 0.7.8
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-sLV9WChi.js → AboutPage-9CVX91tr.js} +1 -1
- package/dist/web/assets/{ConnectionsPage-BWn4ywGJ.js → ConnectionsPage-CWbCVFdv.js} +1 -1
- package/dist/web/assets/EmployeeRecruitDialog-CQgNpGJ_.js +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-CfFlyKSh.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-CWg4WJhr.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-CwU4qBgD.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-B6TGAcFH.js +1 -0
- package/dist/web/assets/{KnowledgeEditDialog-DWUV_NPp.css → KnowledgeEditDialog-iv-dgUeD.css} +1 -1
- package/dist/web/assets/{KnowledgeInspector-CRQWOaos.js → KnowledgeInspector-M2pLY0bR.js} +1 -1
- package/dist/web/assets/{KnowledgeOrganizerPage-Dfj3vIpU.js → KnowledgeOrganizerPage-DzeAlk9b.js} +1 -1
- package/dist/web/assets/KnowledgePage-BJu9w0fF.js +1 -0
- package/dist/web/assets/{KnowledgeWorkspace-ChEB5Bwk.js → KnowledgeWorkspace-CCt8rnBb.js} +1 -1
- package/dist/web/assets/{OverviewPage-Cd66M0RT.js → OverviewPage-CZswVqui.js} +1 -1
- package/dist/web/assets/{PersonalProfilePage-RCFHL0Wz.js → PersonalProfilePage-f1nSF6kx.js} +1 -1
- package/dist/web/assets/PersonalProjectsPage-D__eJf0t.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +1 -0
- package/dist/web/assets/ProjectAgentsPage-LrwagMGj.js +11 -0
- package/dist/web/assets/{PublicProjectsPage-mstPEjHe.js → PublicProjectsPage-BdC2Qk4n.js} +1 -1
- package/dist/web/assets/{ReviewPage-6Y1SmfZy.js → ReviewPage-Cbgl8_Si.js} +1 -1
- package/dist/web/assets/SearchableSelect-ERm-k4KI.js +1 -0
- package/dist/web/assets/{SettingsPage--LdTMUm7.js → SettingsPage-DWso4x29.js} +1 -1
- package/dist/web/assets/{WritingTastePage-CIpKswvs.js → WritingTastePage-CFpCjmIY.js} +1 -1
- package/dist/web/assets/catalog-WGLn7ERJ.js +1 -0
- package/dist/web/assets/{console-BT1P2Db4.js → console-DQgu0JIA.js} +1 -1
- package/dist/web/assets/conversation-launcher-settings-0KtlG_LV.js +1 -0
- package/dist/web/assets/{i18n-js6gfc7j.js → i18n-ByRiA3pJ.js} +3 -3
- package/dist/web/assets/{identity-CfDfanpa.js → identity-CxdPRMwH.js} +1 -1
- package/dist/web/assets/index-JAZe6lfX.js +2 -0
- package/dist/web/assets/index-wcXzzggp.css +1 -0
- package/dist/web/assets/{model-DJkLAJsg.js → model-Db4-KefQ.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-BdSti7Y2.js → useApi-CROJJdhE-CePMjc08.js} +1 -1
- package/dist/web/assets/{vue-router-Kk67EaCP.js → vue-router-CzX-hn3P.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_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 +21 -1
- package/graph-provider/fuli_graph/project_agent_task_models.py +217 -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_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 +748 -162
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +230 -18
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +261 -468
- 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 +25 -6
- package/src/agent-tools/graph-handlers.js +11 -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/update-command.js +6 -43
- 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 +20 -3
- package/src/graphiti/project-agent-mapping.js +155 -5
- 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 +30 -14
- package/src/graphiti/provider-client.js +66 -4
- package/src/http/api-router.js +30 -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 +2 -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 +10 -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/semantic-version.js +42 -0
- 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/src/system/package-version-checker.js +72 -0
- package/src/system/system-service.js +3 -0
- package/dist/web/assets/KnowledgeEditDialog-CZ5ziPrD.js +0 -1
- package/dist/web/assets/KnowledgePage-BRYC9S5Y.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-DEh8XWby.js +0 -1
- package/dist/web/assets/ProjectAgentsPage-Crh7PrXa.css +0 -1
- package/dist/web/assets/ProjectAgentsPage-DPnaH-TR.js +0 -11
- package/dist/web/assets/SearchableSelect-D3HM8YF8.js +0 -1
- package/dist/web/assets/conversation-launcher-settings-DrJEotdf.js +0 -1
- package/dist/web/assets/index-BnH5mcZx.css +0 -1
- package/dist/web/assets/index-GqJf1xqg.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,35 +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
|
-
from .store_project_agent_coordination_policy import
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
TERMINAL_TASK_STATUSES = {'completed', 'failed', 'cancelled'}
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
40
26
|
class StoreProjectAgentTasks(
|
|
27
|
+
StoreProjectAgentTaskActivity,
|
|
41
28
|
StoreProjectAgentCoordinationPolicy,
|
|
42
29
|
StoreProjectAgentTaskRecruitment,
|
|
43
30
|
StoreProjectAgentTaskReads,
|
|
44
|
-
|
|
31
|
+
StoreProjectAgentTaskPersistence,
|
|
45
32
|
StoreProjectAgentTaskStaffing,
|
|
46
33
|
):
|
|
47
34
|
"""Durable task, routing, recruitment, and verified activity control plane.
|
|
48
35
|
|
|
49
|
-
|
|
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.
|
|
36
|
+
Clients append real state events; this control plane does not run workers.
|
|
52
37
|
"""
|
|
53
|
-
|
|
54
38
|
async def submit_project_agent_task(
|
|
55
39
|
self,
|
|
56
40
|
actor: dict,
|
|
57
41
|
request: ProjectAgentTaskSubmit,
|
|
58
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)
|
|
59
46
|
self._require_personal()
|
|
60
47
|
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
61
48
|
await authorize_personal_project(
|
|
@@ -79,11 +66,32 @@ class StoreProjectAgentTasks(
|
|
|
79
66
|
status_code=409,
|
|
80
67
|
detail='task idempotency key was used with different input',
|
|
81
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
|
+
)
|
|
82
74
|
task = self._task_from_row(existing)
|
|
83
|
-
|
|
75
|
+
recruitments = await self._task_recruitments(
|
|
84
76
|
request.personal_space_id,
|
|
85
77
|
task.task_id,
|
|
86
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
|
|
87
95
|
assigned_agent = await self._route_assigned_agent(
|
|
88
96
|
actor,
|
|
89
97
|
request,
|
|
@@ -93,13 +101,9 @@ class StoreProjectAgentTasks(
|
|
|
93
101
|
task,
|
|
94
102
|
recruitment,
|
|
95
103
|
assigned_agent=assigned_agent,
|
|
104
|
+
recruitments=recruitments,
|
|
96
105
|
)
|
|
97
106
|
|
|
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
107
|
if request.coordinator_agent_id:
|
|
104
108
|
await authorize_project_agent(
|
|
105
109
|
self,
|
|
@@ -162,12 +166,53 @@ class StoreProjectAgentTasks(
|
|
|
162
166
|
'qualified durable Agent candidates'
|
|
163
167
|
)
|
|
164
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
|
+
|
|
165
209
|
model_strategy, model_source = self._effective_model_strategy(
|
|
166
210
|
request,
|
|
167
211
|
lead,
|
|
168
212
|
coordinator.profile.default_model_strategy,
|
|
169
213
|
)
|
|
170
214
|
recruitment = None
|
|
215
|
+
recruitments = []
|
|
171
216
|
routing_outcome = 'assigned_existing' if lead else 'unassigned'
|
|
172
217
|
task_status = 'queued' if lead else 'blocked'
|
|
173
218
|
if request.staffing_intent == 'unassigned':
|
|
@@ -177,6 +222,72 @@ class StoreProjectAgentTasks(
|
|
|
177
222
|
elif reason == 'manual_agent_selection':
|
|
178
223
|
routing_outcome = 'unassigned'
|
|
179
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'
|
|
180
291
|
elif not lead or request.staffing_intent in {'new_durable', 'temporary'}:
|
|
181
292
|
recruitment, recruited = await self._open_recruitment(
|
|
182
293
|
actor,
|
|
@@ -186,6 +297,8 @@ class StoreProjectAgentTasks(
|
|
|
186
297
|
coordinator_agent_id,
|
|
187
298
|
reason,
|
|
188
299
|
)
|
|
300
|
+
if recruitment:
|
|
301
|
+
recruitments.append(recruitment)
|
|
189
302
|
if recruited:
|
|
190
303
|
lead = recruited
|
|
191
304
|
if not any(
|
|
@@ -290,6 +403,7 @@ class StoreProjectAgentTasks(
|
|
|
290
403
|
participants=participants,
|
|
291
404
|
decision=decision,
|
|
292
405
|
recruitment=recruitment,
|
|
406
|
+
recruitments=recruitments,
|
|
293
407
|
executor_decision=executor_decision,
|
|
294
408
|
created_at=now,
|
|
295
409
|
)
|
|
@@ -307,6 +421,7 @@ class StoreProjectAgentTasks(
|
|
|
307
421
|
task,
|
|
308
422
|
recruitment,
|
|
309
423
|
assigned_agent=assigned_agent,
|
|
424
|
+
recruitments=recruitments,
|
|
310
425
|
)
|
|
311
426
|
|
|
312
427
|
async def get_project_agent_task(
|
|
@@ -314,19 +429,33 @@ class StoreProjectAgentTasks(
|
|
|
314
429
|
actor: dict,
|
|
315
430
|
personal_space_id: str,
|
|
316
431
|
task_id: str,
|
|
432
|
+
*,
|
|
433
|
+
personal_project_id: str | None = None,
|
|
317
434
|
) -> ProjectAgentTaskRecord:
|
|
318
435
|
self._require_personal()
|
|
319
436
|
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
320
|
-
|
|
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
|
+
)
|
|
321
449
|
if not row:
|
|
322
450
|
raise HTTPException(status_code=404, detail='project Agent task not found')
|
|
323
451
|
task = dict(row['task'])
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
452
|
+
if not personal_project_id:
|
|
453
|
+
await authorize_personal_project(
|
|
454
|
+
self,
|
|
455
|
+
actor,
|
|
456
|
+
space,
|
|
457
|
+
task['personal_project_id'],
|
|
458
|
+
)
|
|
330
459
|
return self._task_from_row(row)
|
|
331
460
|
|
|
332
461
|
async def list_project_agent_tasks(
|
|
@@ -369,349 +498,6 @@ class StoreProjectAgentTasks(
|
|
|
369
498
|
)
|
|
370
499
|
return [self._task_from_row(row) for row in records]
|
|
371
500
|
|
|
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
501
|
async def get_project_agent_activity(
|
|
716
502
|
self,
|
|
717
503
|
actor: dict,
|
|
@@ -719,9 +505,18 @@ class StoreProjectAgentTasks(
|
|
|
719
505
|
agent_id: str,
|
|
720
506
|
from_date: date,
|
|
721
507
|
to_date: date,
|
|
508
|
+
*,
|
|
509
|
+
personal_project_id: str | None = None,
|
|
722
510
|
) -> ProjectAgentActivityResult:
|
|
723
511
|
self._require_personal()
|
|
724
|
-
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
|
+
)
|
|
725
520
|
if from_date > to_date:
|
|
726
521
|
raise HTTPException(status_code=422, detail='from date must not exceed to date')
|
|
727
522
|
records, _, _ = await self.runtime.driver.execute_query(
|
|
@@ -733,12 +528,18 @@ class StoreProjectAgentTasks(
|
|
|
733
528
|
(task:FuliProjectAgentTask)-[:HAS_TASK_EVENT]->
|
|
734
529
|
(event:FuliProjectAgentTaskEvent)-[:EVENT_AGENT]->(agent)
|
|
735
530
|
WHERE event.status IN ['completed', 'failed', 'cancelled']
|
|
531
|
+
AND ($personal_project_id IS NULL
|
|
532
|
+
OR task.personal_project_id = $personal_project_id)
|
|
736
533
|
AND event.activity_date >= date($from_date)
|
|
737
534
|
AND event.activity_date <= date($to_date)
|
|
738
|
-
|
|
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
|
|
739
539
|
ORDER BY event.created_at DESC, event.event_id
|
|
740
540
|
''',
|
|
741
541
|
personal_space_id=personal_space_id,
|
|
542
|
+
personal_project_id=personal_project_id,
|
|
742
543
|
agent_id=agent_id,
|
|
743
544
|
from_date=from_date.isoformat(),
|
|
744
545
|
to_date=to_date.isoformat(),
|
|
@@ -746,7 +547,9 @@ class StoreProjectAgentTasks(
|
|
|
746
547
|
)
|
|
747
548
|
grouped = defaultdict(list)
|
|
748
549
|
for row in records:
|
|
749
|
-
event =
|
|
550
|
+
event = self._event_with_latest_execution_evidence(
|
|
551
|
+
row['event'], row.get('evidence_events') or []
|
|
552
|
+
)
|
|
750
553
|
occurred_at = native_datetime(event['created_at'])
|
|
751
554
|
grouped[occurred_at.date()].append(ProjectAgentActivityTask(
|
|
752
555
|
task_id=event['task_id'],
|
|
@@ -784,7 +587,6 @@ class StoreProjectAgentTasks(
|
|
|
784
587
|
|
|
785
588
|
async def _route_assigned_agent(self, actor, request, task):
|
|
786
589
|
"""Project the selected durable identity into the route response."""
|
|
787
|
-
|
|
788
590
|
agent_id = getattr(task, 'lead_agent_id', None)
|
|
789
591
|
getter = getattr(self, 'get_project_agent', None)
|
|
790
592
|
if not agent_id or not callable(getter):
|
|
@@ -898,7 +700,7 @@ class StoreProjectAgentTasks(
|
|
|
898
700
|
task_id=task_id,
|
|
899
701
|
agent_id=selected['agent_id'],
|
|
900
702
|
work_kind=request.work_kind,
|
|
901
|
-
required_capabilities=request.
|
|
703
|
+
required_capabilities=request.executor_capability_hints,
|
|
902
704
|
model_strategy=model_strategy,
|
|
903
705
|
task_override=(
|
|
904
706
|
getattr(request, 'executor_policy_override', None)
|
|
@@ -919,9 +721,6 @@ class StoreProjectAgentTasks(
|
|
|
919
721
|
return {}
|
|
920
722
|
raw = decision.model_dump(mode='json') if hasattr(decision, 'model_dump') else dict(decision)
|
|
921
723
|
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
724
|
'executor_decision': raw,
|
|
926
725
|
'selected_executor_id': raw.get('selected_executor_id') or raw.get('executor_id'),
|
|
927
726
|
'matched_executor_rule_id': raw.get('matched_rule_id') or raw.get('rule_id'),
|
|
@@ -935,26 +734,15 @@ class StoreProjectAgentTasks(
|
|
|
935
734
|
if key in ProjectAgentRoutingDecisionRecord.model_fields
|
|
936
735
|
}
|
|
937
736
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
}
|
|
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)
|
|
954
741
|
|
|
955
|
-
async def
|
|
742
|
+
async def _persist_task_transaction(
|
|
956
743
|
self,
|
|
957
744
|
request,
|
|
745
|
+
transaction,
|
|
958
746
|
*,
|
|
959
747
|
task_id,
|
|
960
748
|
payload_hash,
|
|
@@ -974,7 +762,24 @@ class StoreProjectAgentTasks(
|
|
|
974
762
|
recruitment,
|
|
975
763
|
executor_decision,
|
|
976
764
|
created_at,
|
|
765
|
+
recruitments=None,
|
|
977
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
|
+
]
|
|
978
783
|
decision_raw = decision.model_dump(mode='json')
|
|
979
784
|
executor_raw = (
|
|
980
785
|
executor_decision.model_dump(mode='json')
|
|
@@ -988,15 +793,15 @@ class StoreProjectAgentTasks(
|
|
|
988
793
|
task_id,
|
|
989
794
|
'created',
|
|
990
795
|
)
|
|
991
|
-
await
|
|
796
|
+
result = await transaction.run(
|
|
992
797
|
'''
|
|
993
798
|
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})
|
|
994
799
|
MATCH (space)-[:CONTAINS_PROJECT]->
|
|
995
800
|
(project:FuliPersonalProject {project_id: $personal_project_id})
|
|
996
801
|
MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
997
802
|
(coordinator:FuliProjectAgent {agent_id: $coordinator_agent_id})
|
|
998
|
-
|
|
999
|
-
|
|
803
|
+
MERGE (task:FuliProjectAgentTask {id: $task_id})
|
|
804
|
+
ON CREATE SET task += {
|
|
1000
805
|
task_id: $task_id,
|
|
1001
806
|
personal_space_id: $personal_space_id,
|
|
1002
807
|
personal_project_id: $personal_project_id,
|
|
@@ -1005,6 +810,7 @@ class StoreProjectAgentTasks(
|
|
|
1005
810
|
objective: $objective,
|
|
1006
811
|
work_kind: $work_kind,
|
|
1007
812
|
required_capabilities: $required_capabilities,
|
|
813
|
+
executor_capability_hints: $executor_capability_hints,
|
|
1008
814
|
duration: $duration,
|
|
1009
815
|
staffing_intent: $staffing_intent,
|
|
1010
816
|
status: $status,
|
|
@@ -1028,32 +834,39 @@ class StoreProjectAgentTasks(
|
|
|
1028
834
|
executor_decision_json: $executor_decision_json,
|
|
1029
835
|
executor_policy_json: $executor_policy_json,
|
|
1030
836
|
selected_executor_id: $selected_executor_id,
|
|
837
|
+
participant_plan_json: $participant_plan_json,
|
|
838
|
+
recruitment_plan_json: $recruitment_plan_json,
|
|
1031
839
|
created_at: $created_at,
|
|
1032
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
|
|
1033
846
|
})
|
|
1034
|
-
CREATE
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
summary: $routing_explanation,
|
|
1048
|
-
source_application: $source_application,
|
|
1049
|
-
source_session_id: $source_session_id,
|
|
1050
|
-
created_at: $created_at
|
|
1051
|
-
})
|
|
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
|
|
1052
860
|
MERGE (space)-[:HAS_PROJECT_AGENT_TASK]->(task)
|
|
1053
861
|
MERGE (project)-[:HAS_PROJECT_AGENT_TASK]->(task)
|
|
1054
862
|
MERGE (task)-[:COORDINATED_BY]->(coordinator)
|
|
1055
863
|
MERGE (task)-[:ROUTED_BY]->(decision_node)
|
|
1056
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
|
|
1057
870
|
''',
|
|
1058
871
|
task_id=task_id,
|
|
1059
872
|
personal_space_id=request.personal_space_id,
|
|
@@ -1063,6 +876,7 @@ class StoreProjectAgentTasks(
|
|
|
1063
876
|
objective=request.objective,
|
|
1064
877
|
work_kind=request.work_kind,
|
|
1065
878
|
required_capabilities=request.required_capabilities,
|
|
879
|
+
executor_capability_hints=request.executor_capability_hints,
|
|
1066
880
|
duration=request.duration,
|
|
1067
881
|
staffing_intent=request.staffing_intent,
|
|
1068
882
|
status=status,
|
|
@@ -1101,51 +915,30 @@ class StoreProjectAgentTasks(
|
|
|
1101
915
|
(executor_raw or {}).get('selected_executor_id')
|
|
1102
916
|
or (executor_raw or {}).get('executor_id')
|
|
1103
917
|
),
|
|
918
|
+
participant_plan_json=json.dumps(participants, sort_keys=True),
|
|
919
|
+
recruitment_plan_json=json.dumps(recruitment_links, sort_keys=True),
|
|
1104
920
|
created_at=created_at,
|
|
1105
921
|
decision_id=decision.decision_id,
|
|
1106
922
|
decision_json=json.dumps(decision_raw, sort_keys=True),
|
|
1107
923
|
initial_event_id=initial_event_id,
|
|
1108
924
|
)
|
|
1109
|
-
for
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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,
|
|
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',
|
|
1151
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
|
+
)
|