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
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""Atomic task activity, execution audit and terminal evidence writes."""
|
|
2
|
+
|
|
3
|
+
from fastapi import HTTPException
|
|
4
|
+
|
|
5
|
+
from .personal_project_access import authorize_personal_project
|
|
6
|
+
from .project_agent_executor_models import (
|
|
7
|
+
ProjectAgentExecutorActualReport,
|
|
8
|
+
ProjectAgentExecutorOutcomeEvidenceCreate,
|
|
9
|
+
)
|
|
10
|
+
from .project_agent_models import ProjectAgentModelStrategy, ProjectAgentProfile
|
|
11
|
+
from .project_agent_task_models import ProjectAgentTaskActivityCreate, ProjectAgentTaskRecord
|
|
12
|
+
from .provider_values import now_utc, stable_uuid
|
|
13
|
+
from .store_project_agent_task_recruitment import RECRUITMENT_CLAIM_TTL
|
|
14
|
+
from .store_transactions import query_store_transaction
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
TERMINAL_TASK_STATUSES = {"completed", "failed", "cancelled"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StoreProjectAgentTaskActivity:
|
|
21
|
+
"""Persist activity and its derived audit within one request-owned transaction."""
|
|
22
|
+
|
|
23
|
+
async def record_project_agent_task_activity(
|
|
24
|
+
self,
|
|
25
|
+
actor: dict,
|
|
26
|
+
request: ProjectAgentTaskActivityCreate,
|
|
27
|
+
) -> ProjectAgentTaskRecord:
|
|
28
|
+
async with query_store_transaction(self) as scoped:
|
|
29
|
+
return await scoped._record_project_agent_task_activity(actor, request)
|
|
30
|
+
|
|
31
|
+
async def _record_project_agent_task_activity(
|
|
32
|
+
self,
|
|
33
|
+
actor: dict,
|
|
34
|
+
request: ProjectAgentTaskActivityCreate,
|
|
35
|
+
) -> ProjectAgentTaskRecord:
|
|
36
|
+
self._require_personal()
|
|
37
|
+
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
38
|
+
await authorize_personal_project(
|
|
39
|
+
self,
|
|
40
|
+
actor,
|
|
41
|
+
space,
|
|
42
|
+
request.personal_project_id,
|
|
43
|
+
)
|
|
44
|
+
# Acquire the task's write lock before reading its revision, transition
|
|
45
|
+
# state or idempotency events. A transaction alone permits two concurrent
|
|
46
|
+
# readers to validate the same revision before either obtains a lock.
|
|
47
|
+
await self.runtime.driver.execute_query(
|
|
48
|
+
'''
|
|
49
|
+
MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})
|
|
50
|
+
-[:HAS_PROJECT_AGENT_TASK]->(task:FuliProjectAgentTask {
|
|
51
|
+
personal_space_id: $personal_space_id,
|
|
52
|
+
personal_project_id: $personal_project_id,
|
|
53
|
+
task_id: $task_id
|
|
54
|
+
})
|
|
55
|
+
SET task._activity_write_lock = true
|
|
56
|
+
REMOVE task._activity_write_lock
|
|
57
|
+
RETURN task.task_id AS task_id
|
|
58
|
+
''',
|
|
59
|
+
personal_space_id=request.personal_space_id,
|
|
60
|
+
personal_project_id=request.personal_project_id,
|
|
61
|
+
task_id=request.task_id,
|
|
62
|
+
)
|
|
63
|
+
row = await self._find_task_row(request.personal_space_id, request.task_id)
|
|
64
|
+
if not row:
|
|
65
|
+
raise HTTPException(status_code=404, detail='project Agent task not found')
|
|
66
|
+
raw_task = dict(row['task'])
|
|
67
|
+
if raw_task['personal_project_id'] != request.personal_project_id:
|
|
68
|
+
raise HTTPException(status_code=404, detail='project Agent task not found')
|
|
69
|
+
event_id = stable_uuid(
|
|
70
|
+
self.settings.provider_id,
|
|
71
|
+
request.personal_space_id,
|
|
72
|
+
'project-agent-task-event',
|
|
73
|
+
request.task_id,
|
|
74
|
+
request.idempotency_key,
|
|
75
|
+
)
|
|
76
|
+
payload_hash = self._payload_hash(request)
|
|
77
|
+
existing_event = next(
|
|
78
|
+
(
|
|
79
|
+
dict(value)
|
|
80
|
+
for value in (row.get('event_rows') or [])
|
|
81
|
+
if value
|
|
82
|
+
and (value.get('event_id') or value.get('id')) == event_id
|
|
83
|
+
),
|
|
84
|
+
None,
|
|
85
|
+
)
|
|
86
|
+
if existing_event:
|
|
87
|
+
if existing_event.get('payload_hash') != payload_hash:
|
|
88
|
+
raise HTTPException(
|
|
89
|
+
status_code=409,
|
|
90
|
+
detail=(
|
|
91
|
+
'task event idempotency key was used with different input'
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
return self._task_from_row(row)
|
|
95
|
+
self._validate_task_transition(
|
|
96
|
+
raw_task.get('status'),
|
|
97
|
+
request.status,
|
|
98
|
+
has_actual_executor=bool(request.actual_executor_id),
|
|
99
|
+
)
|
|
100
|
+
expected_revision = getattr(request, 'expected_revision', None)
|
|
101
|
+
if expected_revision is None:
|
|
102
|
+
expected_revision = raw_task.get('revision') or 0
|
|
103
|
+
if request.agent_id:
|
|
104
|
+
participant = next(
|
|
105
|
+
(
|
|
106
|
+
item for item in self._participant_maps(row)
|
|
107
|
+
if item['agent_id'] == request.agent_id
|
|
108
|
+
),
|
|
109
|
+
None,
|
|
110
|
+
)
|
|
111
|
+
if not participant:
|
|
112
|
+
raise HTTPException(
|
|
113
|
+
status_code=409,
|
|
114
|
+
detail='Agent is not assigned to this task',
|
|
115
|
+
)
|
|
116
|
+
if participant.get('profile_json'):
|
|
117
|
+
profile = ProjectAgentProfile.model_validate_json(
|
|
118
|
+
participant['profile_json']
|
|
119
|
+
)
|
|
120
|
+
for application in request.reported_client_applications:
|
|
121
|
+
self._require_agent_client_allowed(
|
|
122
|
+
{'agent_id': request.agent_id, 'profile': profile},
|
|
123
|
+
application,
|
|
124
|
+
)
|
|
125
|
+
now = now_utc()
|
|
126
|
+
terminal_at = now if request.status in TERMINAL_TASK_STATUSES else None
|
|
127
|
+
actual_executor_id = getattr(request, 'actual_executor_id', None)
|
|
128
|
+
matched_executor_rule_id = getattr(request, 'matched_executor_rule_id', None)
|
|
129
|
+
executor_selection_reason = getattr(request, 'executor_selection_reason', None)
|
|
130
|
+
executor_fallback_reason = getattr(request, 'executor_fallback_reason', None)
|
|
131
|
+
executor_blocked_reason = getattr(request, 'executor_blocked_reason', None)
|
|
132
|
+
worker_id = getattr(request, 'worker_id', None)
|
|
133
|
+
worker_label = getattr(request, 'worker_label', None)
|
|
134
|
+
worker_occupation_emoji = request.worker_occupation_emoji
|
|
135
|
+
worker_status = getattr(request, 'worker_status', None)
|
|
136
|
+
evidence_json = {
|
|
137
|
+
key: value.model_dump_json() if value is not None else None
|
|
138
|
+
for key, value in (
|
|
139
|
+
('token_usage', request.token_usage),
|
|
140
|
+
('worker_runtime', request.worker_runtime),
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
actual_recorder = None
|
|
144
|
+
actual_report = None
|
|
145
|
+
if actual_executor_id:
|
|
146
|
+
actual_recorder = getattr(
|
|
147
|
+
self,
|
|
148
|
+
'record_project_agent_executor_actual',
|
|
149
|
+
None,
|
|
150
|
+
)
|
|
151
|
+
if not callable(actual_recorder):
|
|
152
|
+
raise HTTPException(
|
|
153
|
+
status_code=503,
|
|
154
|
+
detail='executor evidence control plane is unavailable',
|
|
155
|
+
)
|
|
156
|
+
strategy = (
|
|
157
|
+
ProjectAgentModelStrategy.model_validate_json(
|
|
158
|
+
raw_task['effective_model_strategy_json']
|
|
159
|
+
)
|
|
160
|
+
if raw_task.get('effective_model_strategy_json')
|
|
161
|
+
else ProjectAgentModelStrategy()
|
|
162
|
+
)
|
|
163
|
+
actual_report = ProjectAgentExecutorActualReport(
|
|
164
|
+
personal_space_id=request.personal_space_id,
|
|
165
|
+
personal_project_id=request.personal_project_id,
|
|
166
|
+
task_id=request.task_id,
|
|
167
|
+
run_id=event_id,
|
|
168
|
+
agent_id=request.agent_id,
|
|
169
|
+
executor_id=actual_executor_id,
|
|
170
|
+
provider=request.actual_model_provider,
|
|
171
|
+
model=request.actual_model,
|
|
172
|
+
model_strategy=strategy,
|
|
173
|
+
model_strategy_source=(
|
|
174
|
+
raw_task.get('model_strategy_source') or 'coordinator'
|
|
175
|
+
),
|
|
176
|
+
matched_rule_id=matched_executor_rule_id,
|
|
177
|
+
fallback_reason=executor_fallback_reason,
|
|
178
|
+
idempotency_key=f'actual:{event_id}',
|
|
179
|
+
occurred_at=now,
|
|
180
|
+
source_application=request.source_application,
|
|
181
|
+
source_session_id=request.source_session_id,
|
|
182
|
+
)
|
|
183
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
184
|
+
'''
|
|
185
|
+
MATCH (space:FuliSpace {
|
|
186
|
+
id: $personal_space_id, kind: 'personal'
|
|
187
|
+
})-[:HAS_PROJECT_AGENT_TASK]->
|
|
188
|
+
(task:FuliProjectAgentTask {
|
|
189
|
+
personal_space_id: $personal_space_id,
|
|
190
|
+
task_id: $task_id
|
|
191
|
+
})
|
|
192
|
+
WHERE coalesce(task.revision, 0) = $expected_revision
|
|
193
|
+
OR EXISTS {
|
|
194
|
+
MATCH (task)-[:HAS_TASK_EVENT]->
|
|
195
|
+
(:FuliProjectAgentTaskEvent {id: $event_id})
|
|
196
|
+
}
|
|
197
|
+
WITH space, task
|
|
198
|
+
WHERE task.status <> 'awaiting_recruitment'
|
|
199
|
+
OR $status = 'awaiting_recruitment'
|
|
200
|
+
OR task.recruitment_provisioning_claimed_at IS NULL
|
|
201
|
+
OR task.recruitment_provisioning_claimed_at <=
|
|
202
|
+
$recruitment_claim_expired_before
|
|
203
|
+
OPTIONAL MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
204
|
+
(agent:FuliProjectAgent {agent_id: $agent_id})
|
|
205
|
+
WHERE $agent_id IS NULL OR EXISTS {
|
|
206
|
+
MATCH (task)-[:HAS_PARTICIPANT]->(agent)
|
|
207
|
+
}
|
|
208
|
+
WITH task, agent,
|
|
209
|
+
task.lead_agent_id IS NULL AND $status = 'running'
|
|
210
|
+
AS promote_lead
|
|
211
|
+
WHERE NOT promote_lead OR (
|
|
212
|
+
agent IS NOT NULL AND NOT EXISTS {
|
|
213
|
+
MATCH (task)-[existing_lead:HAS_PARTICIPANT]->()
|
|
214
|
+
WHERE existing_lead.role = 'lead'
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
MERGE (event:FuliProjectAgentTaskEvent {id: $event_id})
|
|
218
|
+
ON CREATE SET event.event_id = $event_id,
|
|
219
|
+
event.task_id = $task_id,
|
|
220
|
+
event.agent_id = $agent_id,
|
|
221
|
+
event.status = $status,
|
|
222
|
+
event.actor_kind = $actor_kind,
|
|
223
|
+
event.summary = $summary,
|
|
224
|
+
event.payload_hash = $payload_hash,
|
|
225
|
+
event.source_application = $source_application,
|
|
226
|
+
event.source_session_id = $source_session_id,
|
|
227
|
+
event.source_session_url = $source_session_url,
|
|
228
|
+
event.tools_used = $tools_used,
|
|
229
|
+
event.actual_model_provider = $actual_model_provider,
|
|
230
|
+
event.actual_model = $actual_model,
|
|
231
|
+
event.actual_executor_id = $actual_executor_id,
|
|
232
|
+
event.matched_executor_rule_id =
|
|
233
|
+
$matched_executor_rule_id,
|
|
234
|
+
event.executor_selection_reason =
|
|
235
|
+
$executor_selection_reason,
|
|
236
|
+
event.executor_fallback_reason =
|
|
237
|
+
$executor_fallback_reason,
|
|
238
|
+
event.executor_blocked_reason =
|
|
239
|
+
$executor_blocked_reason,
|
|
240
|
+
event.worker_id = $worker_id,
|
|
241
|
+
event.worker_label = $worker_label,
|
|
242
|
+
event.worker_occupation_emoji = $worker_occupation_emoji,
|
|
243
|
+
event.worker_status = $worker_status,
|
|
244
|
+
event.token_usage_json = $token_usage_json,
|
|
245
|
+
event.worker_runtime_json = $worker_runtime_json,
|
|
246
|
+
event.activity_date = CASE
|
|
247
|
+
WHEN $terminal_at IS NULL THEN null
|
|
248
|
+
ELSE date($terminal_at) END,
|
|
249
|
+
event.created_at = $updated_at
|
|
250
|
+
WITH task, event, agent, promote_lead,
|
|
251
|
+
event.payload_hash = $payload_hash AS same_payload,
|
|
252
|
+
coalesce(task.revision, 0) = $expected_revision
|
|
253
|
+
AS applied_transition
|
|
254
|
+
FOREACH (_ IN CASE
|
|
255
|
+
WHEN same_payload AND applied_transition
|
|
256
|
+
THEN [1] ELSE [] END |
|
|
257
|
+
MERGE (task)-[:HAS_TASK_EVENT]->(event)
|
|
258
|
+
)
|
|
259
|
+
FOREACH (_ IN CASE
|
|
260
|
+
WHEN same_payload AND applied_transition AND agent IS NOT NULL
|
|
261
|
+
THEN [1] ELSE [] END |
|
|
262
|
+
MERGE (event)-[:EVENT_AGENT]->(agent)
|
|
263
|
+
)
|
|
264
|
+
FOREACH (_ IN CASE
|
|
265
|
+
WHEN same_payload AND applied_transition
|
|
266
|
+
THEN [1] ELSE [] END |
|
|
267
|
+
SET task.status = $status,
|
|
268
|
+
task.result_summary = CASE
|
|
269
|
+
WHEN $status = 'completed' THEN $summary
|
|
270
|
+
ELSE task.result_summary END,
|
|
271
|
+
task.failure_reason = CASE
|
|
272
|
+
WHEN $status = 'failed' THEN $summary
|
|
273
|
+
ELSE task.failure_reason END,
|
|
274
|
+
task.matched_executor_rule_id = CASE
|
|
275
|
+
WHEN $actual_executor_id IS NULL AND task.actual_run_id IS NULL
|
|
276
|
+
THEN coalesce($matched_executor_rule_id, task.matched_executor_rule_id)
|
|
277
|
+
ELSE task.matched_executor_rule_id END,
|
|
278
|
+
task.executor_selection_reason = coalesce(
|
|
279
|
+
$executor_selection_reason,
|
|
280
|
+
task.executor_selection_reason),
|
|
281
|
+
task.executor_fallback_reason = CASE
|
|
282
|
+
WHEN $actual_executor_id IS NULL AND task.actual_run_id IS NULL
|
|
283
|
+
THEN coalesce($executor_fallback_reason, task.executor_fallback_reason)
|
|
284
|
+
ELSE task.executor_fallback_reason END,
|
|
285
|
+
task.executor_blocked_reason = coalesce(
|
|
286
|
+
$executor_blocked_reason,
|
|
287
|
+
task.executor_blocked_reason),
|
|
288
|
+
task.completed_at = $terminal_at,
|
|
289
|
+
task.updated_at = CASE
|
|
290
|
+
WHEN task.updated_at IS NULL OR task.updated_at < $updated_at
|
|
291
|
+
THEN $updated_at ELSE task.updated_at END,
|
|
292
|
+
task.revision = coalesce(task.revision, 0) + 1
|
|
293
|
+
)
|
|
294
|
+
FOREACH (_ IN CASE
|
|
295
|
+
WHEN same_payload AND applied_transition AND agent IS NOT NULL
|
|
296
|
+
THEN [1] ELSE [] END |
|
|
297
|
+
SET task.lead_agent_id = CASE
|
|
298
|
+
WHEN promote_lead
|
|
299
|
+
THEN agent.agent_id ELSE task.lead_agent_id END
|
|
300
|
+
)
|
|
301
|
+
WITH task, event, same_payload, applied_transition, promote_lead
|
|
302
|
+
OPTIONAL MATCH (task)-[participant:HAS_PARTICIPANT]->
|
|
303
|
+
(participant_agent:FuliProjectAgent)
|
|
304
|
+
FOREACH (_ IN CASE
|
|
305
|
+
WHEN same_payload
|
|
306
|
+
AND applied_transition
|
|
307
|
+
AND participant IS NOT NULL
|
|
308
|
+
AND NOT (coalesce(participant.status, '') IN
|
|
309
|
+
['completed', 'failed', 'cancelled'])
|
|
310
|
+
AND (
|
|
311
|
+
$terminal_at IS NOT NULL
|
|
312
|
+
OR $agent_id IS NULL
|
|
313
|
+
OR participant_agent.agent_id = $agent_id
|
|
314
|
+
)
|
|
315
|
+
THEN [1] ELSE [] END |
|
|
316
|
+
SET participant.status = $status,
|
|
317
|
+
participant.role = CASE
|
|
318
|
+
WHEN promote_lead
|
|
319
|
+
AND participant_agent.agent_id = $agent_id
|
|
320
|
+
THEN 'lead' ELSE participant.role END,
|
|
321
|
+
participant.updated_at = $updated_at,
|
|
322
|
+
participant.ended_at = $terminal_at
|
|
323
|
+
)
|
|
324
|
+
RETURN DISTINCT task, event, same_payload, applied_transition
|
|
325
|
+
''',
|
|
326
|
+
personal_space_id=request.personal_space_id,
|
|
327
|
+
task_id=request.task_id,
|
|
328
|
+
expected_revision=expected_revision,
|
|
329
|
+
event_id=event_id,
|
|
330
|
+
agent_id=request.agent_id,
|
|
331
|
+
status=request.status,
|
|
332
|
+
recruitment_claim_expired_before=now - RECRUITMENT_CLAIM_TTL,
|
|
333
|
+
actor_kind=request.actor_kind,
|
|
334
|
+
summary=request.summary,
|
|
335
|
+
payload_hash=payload_hash,
|
|
336
|
+
source_application=request.source_application,
|
|
337
|
+
source_session_id=request.source_session_id,
|
|
338
|
+
source_session_url=request.source_session_url,
|
|
339
|
+
tools_used=request.tools_used,
|
|
340
|
+
actual_model_provider=request.actual_model_provider,
|
|
341
|
+
actual_model=request.actual_model,
|
|
342
|
+
actual_executor_id=actual_executor_id,
|
|
343
|
+
matched_executor_rule_id=matched_executor_rule_id,
|
|
344
|
+
executor_selection_reason=executor_selection_reason,
|
|
345
|
+
executor_fallback_reason=executor_fallback_reason,
|
|
346
|
+
executor_blocked_reason=executor_blocked_reason,
|
|
347
|
+
worker_id=worker_id,
|
|
348
|
+
worker_label=worker_label,
|
|
349
|
+
worker_occupation_emoji=worker_occupation_emoji,
|
|
350
|
+
worker_status=worker_status,
|
|
351
|
+
token_usage_json=evidence_json.get('token_usage'),
|
|
352
|
+
worker_runtime_json=evidence_json.get('worker_runtime'),
|
|
353
|
+
terminal_at=terminal_at,
|
|
354
|
+
updated_at=now,
|
|
355
|
+
)
|
|
356
|
+
if not records:
|
|
357
|
+
raise HTTPException(
|
|
358
|
+
status_code=409,
|
|
359
|
+
detail='task revision is stale or participant is invalid',
|
|
360
|
+
)
|
|
361
|
+
if not records[0]['same_payload']:
|
|
362
|
+
raise HTTPException(
|
|
363
|
+
status_code=409,
|
|
364
|
+
detail='task event idempotency key was used with different input',
|
|
365
|
+
)
|
|
366
|
+
if not records[0].get('applied_transition'):
|
|
367
|
+
return await self.get_project_agent_task(
|
|
368
|
+
actor,
|
|
369
|
+
request.personal_space_id,
|
|
370
|
+
request.task_id,
|
|
371
|
+
)
|
|
372
|
+
if actual_report:
|
|
373
|
+
# This is the sole owner of actual projection fields: an older
|
|
374
|
+
# activity observation must not partially overwrite a newer run.
|
|
375
|
+
await actual_recorder(actor, actual_report)
|
|
376
|
+
evidence_status = (
|
|
377
|
+
request.status
|
|
378
|
+
if request.status in TERMINAL_TASK_STATUSES
|
|
379
|
+
else worker_status
|
|
380
|
+
)
|
|
381
|
+
if (
|
|
382
|
+
evidence_status in TERMINAL_TASK_STATUSES
|
|
383
|
+
and actual_executor_id
|
|
384
|
+
and request.agent_id
|
|
385
|
+
):
|
|
386
|
+
evidence_recorder = getattr(
|
|
387
|
+
self,
|
|
388
|
+
'record_project_agent_executor_outcome_evidence',
|
|
389
|
+
None,
|
|
390
|
+
)
|
|
391
|
+
if callable(evidence_recorder):
|
|
392
|
+
strategy = (
|
|
393
|
+
ProjectAgentModelStrategy.model_validate_json(
|
|
394
|
+
raw_task['effective_model_strategy_json']
|
|
395
|
+
)
|
|
396
|
+
if raw_task.get('effective_model_strategy_json')
|
|
397
|
+
else ProjectAgentModelStrategy()
|
|
398
|
+
)
|
|
399
|
+
await evidence_recorder(
|
|
400
|
+
actor,
|
|
401
|
+
ProjectAgentExecutorOutcomeEvidenceCreate(
|
|
402
|
+
personal_space_id=request.personal_space_id,
|
|
403
|
+
personal_project_id=request.personal_project_id,
|
|
404
|
+
work_kind=raw_task['work_kind'],
|
|
405
|
+
agent_id=request.agent_id,
|
|
406
|
+
executor_id=actual_executor_id,
|
|
407
|
+
task_id=request.task_id,
|
|
408
|
+
run_id=event_id,
|
|
409
|
+
model_strategy=strategy,
|
|
410
|
+
evidence_kind='terminal_outcome',
|
|
411
|
+
source='system_terminal',
|
|
412
|
+
terminal_outcome=evidence_status,
|
|
413
|
+
reference_ids=[event_id],
|
|
414
|
+
idempotency_key=f'terminal:{event_id}',
|
|
415
|
+
occurred_at=now,
|
|
416
|
+
),
|
|
417
|
+
)
|
|
418
|
+
await self._archive_finished_temporary_agent(request, raw_task, now)
|
|
419
|
+
return await self.get_project_agent_task(
|
|
420
|
+
actor,
|
|
421
|
+
request.personal_space_id,
|
|
422
|
+
request.task_id,
|
|
423
|
+
)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Recoverable participant and recruitment links for durable Agent tasks."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from fastapi import HTTPException
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StoreProjectAgentTaskPersistence:
|
|
8
|
+
"""Persist idempotent task edges and repair them on task replay."""
|
|
9
|
+
|
|
10
|
+
async def _persist_task_links(
|
|
11
|
+
self,
|
|
12
|
+
request,
|
|
13
|
+
*,
|
|
14
|
+
transaction,
|
|
15
|
+
task_id,
|
|
16
|
+
participants,
|
|
17
|
+
recruitment_links,
|
|
18
|
+
status,
|
|
19
|
+
created_at,
|
|
20
|
+
updated_at=None,
|
|
21
|
+
ended_at=None,
|
|
22
|
+
):
|
|
23
|
+
# Canonical lock order avoids mirrored multi-Agent submissions taking
|
|
24
|
+
# Agent node locks in opposite orders.
|
|
25
|
+
for participant in sorted(
|
|
26
|
+
participants,
|
|
27
|
+
key=lambda item: item['agent_id'],
|
|
28
|
+
):
|
|
29
|
+
result = await transaction.run(
|
|
30
|
+
'''
|
|
31
|
+
MATCH (space:FuliSpace {
|
|
32
|
+
id: $personal_space_id, kind: 'personal'
|
|
33
|
+
})-[:HAS_PROJECT_AGENT_TASK]->
|
|
34
|
+
(task:FuliProjectAgentTask {task_id: $task_id})
|
|
35
|
+
MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
36
|
+
(agent:FuliProjectAgent {agent_id: $agent_id})
|
|
37
|
+
// Share the Agent node lock with archive operations. If an
|
|
38
|
+
// archive wins, the active-status guard makes this link fail;
|
|
39
|
+
// if this write wins, archive observes the participant.
|
|
40
|
+
SET agent._task_lifecycle_lock = true
|
|
41
|
+
REMOVE agent._task_lifecycle_lock
|
|
42
|
+
WITH task, agent
|
|
43
|
+
WHERE agent.status = 'active'
|
|
44
|
+
OR $status IN ['completed', 'failed', 'cancelled']
|
|
45
|
+
MERGE (task)-[participant:HAS_PARTICIPANT]->(agent)
|
|
46
|
+
ON CREATE SET participant.role = $role,
|
|
47
|
+
participant.status = $status,
|
|
48
|
+
participant.assignment_summary =
|
|
49
|
+
$assignment_summary,
|
|
50
|
+
participant.joined_at = $created_at,
|
|
51
|
+
participant.updated_at = $updated_at,
|
|
52
|
+
participant.ended_at = $ended_at
|
|
53
|
+
RETURN count(participant) AS linked_count
|
|
54
|
+
''',
|
|
55
|
+
personal_space_id=request.personal_space_id,
|
|
56
|
+
task_id=task_id,
|
|
57
|
+
agent_id=participant['agent_id'],
|
|
58
|
+
role=participant['role'],
|
|
59
|
+
status=status,
|
|
60
|
+
assignment_summary=participant.get('assignment_summary'),
|
|
61
|
+
created_at=created_at,
|
|
62
|
+
updated_at=updated_at or created_at,
|
|
63
|
+
ended_at=ended_at,
|
|
64
|
+
)
|
|
65
|
+
await self._require_task_link(result)
|
|
66
|
+
for linked in recruitment_links:
|
|
67
|
+
result = await transaction.run(
|
|
68
|
+
'''
|
|
69
|
+
MATCH (space:FuliSpace {
|
|
70
|
+
id: $personal_space_id, kind: 'personal'
|
|
71
|
+
})-[:HAS_PROJECT_AGENT_TASK]->
|
|
72
|
+
(task:FuliProjectAgentTask {task_id: $task_id})
|
|
73
|
+
MATCH (space)-[:HAS_PROJECT_AGENT_RECRUITMENT]->
|
|
74
|
+
(recruitment:FuliProjectAgentRecruitment {
|
|
75
|
+
recruitment_id: $recruitment_id
|
|
76
|
+
})
|
|
77
|
+
SET task.recruitment_id = CASE
|
|
78
|
+
WHEN $is_primary THEN $recruitment_id
|
|
79
|
+
ELSE task.recruitment_id END,
|
|
80
|
+
task.hr_agent_id = CASE
|
|
81
|
+
WHEN $is_primary THEN recruitment.hr_agent_id
|
|
82
|
+
ELSE task.hr_agent_id END
|
|
83
|
+
MERGE (task)-[:TRIGGERED_RECRUITMENT]->(recruitment)
|
|
84
|
+
RETURN count(recruitment) AS linked_count
|
|
85
|
+
''',
|
|
86
|
+
personal_space_id=request.personal_space_id,
|
|
87
|
+
task_id=task_id,
|
|
88
|
+
recruitment_id=linked['recruitment_id'],
|
|
89
|
+
is_primary=bool(linked.get('is_primary')),
|
|
90
|
+
)
|
|
91
|
+
await self._require_task_link(result)
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
async def _require_task_link(result):
|
|
95
|
+
records = [record async for record in result]
|
|
96
|
+
if len(records) != 1 or records[0]['linked_count'] != 1:
|
|
97
|
+
raise HTTPException(
|
|
98
|
+
status_code=409,
|
|
99
|
+
detail='task participant or recruitment target is unavailable',
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
async def _repair_task_persistence(self, request, raw_task):
|
|
103
|
+
participant_plan = self._task_link_plan(
|
|
104
|
+
raw_task.get('participant_plan_json')
|
|
105
|
+
)
|
|
106
|
+
recruitment_plan = self._task_link_plan(
|
|
107
|
+
raw_task.get('recruitment_plan_json')
|
|
108
|
+
)
|
|
109
|
+
if not participant_plan and not recruitment_plan:
|
|
110
|
+
return
|
|
111
|
+
async with self.runtime.driver.transaction() as transaction:
|
|
112
|
+
# Take the same task-node write lock used by activity transitions before
|
|
113
|
+
# reading lifecycle state. The initial replay lookup may already be stale.
|
|
114
|
+
result = await transaction.run(
|
|
115
|
+
'''
|
|
116
|
+
MATCH (space:FuliSpace {
|
|
117
|
+
id: $personal_space_id, kind: 'personal'
|
|
118
|
+
})-[:HAS_PROJECT_AGENT_TASK]->
|
|
119
|
+
(task:FuliProjectAgentTask {task_id: $task_id})
|
|
120
|
+
WHERE task.personal_project_id = $personal_project_id
|
|
121
|
+
SET task.revision = coalesce(task.revision, 0)
|
|
122
|
+
RETURN task
|
|
123
|
+
''',
|
|
124
|
+
personal_space_id=request.personal_space_id,
|
|
125
|
+
personal_project_id=request.personal_project_id,
|
|
126
|
+
task_id=raw_task['task_id'],
|
|
127
|
+
)
|
|
128
|
+
records = [record async for record in result]
|
|
129
|
+
if len(records) != 1:
|
|
130
|
+
raise HTTPException(status_code=409, detail='task is unavailable for repair')
|
|
131
|
+
current = dict(records[0]['task'])
|
|
132
|
+
await self._persist_task_links(
|
|
133
|
+
request,
|
|
134
|
+
transaction=transaction,
|
|
135
|
+
task_id=raw_task['task_id'],
|
|
136
|
+
participants=participant_plan,
|
|
137
|
+
recruitment_links=recruitment_plan,
|
|
138
|
+
status=current['status'],
|
|
139
|
+
created_at=current['created_at'],
|
|
140
|
+
updated_at=current.get('updated_at'),
|
|
141
|
+
ended_at=current.get('completed_at'),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _task_link_plan(value):
|
|
146
|
+
if not value:
|
|
147
|
+
return []
|
|
148
|
+
try:
|
|
149
|
+
parsed = json.loads(value)
|
|
150
|
+
except (TypeError, ValueError):
|
|
151
|
+
return []
|
|
152
|
+
return parsed if isinstance(parsed, list) else []
|