fuli-context 0.7.6 → 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-sLV9WChi.js → AboutPage-BcZeFcHe.js} +1 -1
- package/dist/web/assets/{ConnectionsPage-BWn4ywGJ.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-CRQWOaos.js → KnowledgeInspector-BqPJH5Rr.js} +1 -1
- package/dist/web/assets/{KnowledgeOrganizerPage-Dfj3vIpU.js → KnowledgeOrganizerPage-CyvRflXM.js} +1 -1
- package/dist/web/assets/KnowledgePage-59eczTqe.js +1 -0
- package/dist/web/assets/{KnowledgeWorkspace-ChEB5Bwk.js → KnowledgeWorkspace-CfIjhOog.js} +1 -1
- package/dist/web/assets/{OverviewPage-Cd66M0RT.js → OverviewPage-Xyfm5xkZ.js} +1 -1
- package/dist/web/assets/{PersonalProfilePage-RCFHL0Wz.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-mstPEjHe.js → PublicProjectsPage-BEZ1YPs3.js} +1 -1
- package/dist/web/assets/{ReviewPage-6Y1SmfZy.js → ReviewPage-CQYGwFBH.js} +1 -1
- package/dist/web/assets/SearchableSelect-D27H0MoP.js +1 -0
- package/dist/web/assets/{SettingsPage--LdTMUm7.js → SettingsPage-BlEZ03Yi.js} +1 -1
- package/dist/web/assets/{WritingTastePage-CIpKswvs.js → WritingTastePage-DvRt_nmP.js} +1 -1
- package/dist/web/assets/catalog-DFvIaHVS.js +1 -0
- package/dist/web/assets/{console-BT1P2Db4.js → console-tOxOl8Vq.js} +1 -1
- package/dist/web/assets/conversation-launcher-settings-DuFSpPAo.js +1 -0
- package/dist/web/assets/{i18n-js6gfc7j.js → i18n-CgBHZthI.js} +3 -3
- package/dist/web/assets/{identity-CfDfanpa.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-DJkLAJsg.js → model-BtnVr7fX.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-BdSti7Y2.js → useApi-CROJJdhE-B6MHm_Av.js} +1 -1
- package/dist/web/assets/{vue-router-Kk67EaCP.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_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.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 +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 +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/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-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
|
@@ -32,10 +32,13 @@ from .project_agent_executor_models import (
|
|
|
32
32
|
ProjectAgentExecutorScope,
|
|
33
33
|
project_agent_model_strategy_key,
|
|
34
34
|
)
|
|
35
|
-
from .project_agent_models import
|
|
35
|
+
from .project_agent_models import (
|
|
36
|
+
ProjectAgentExecutorPolicy, ProjectAgentModelStrategy, ProjectAgentProfile,
|
|
37
|
+
)
|
|
36
38
|
from .provider_values import native_datetime, now_utc, stable_uuid
|
|
37
39
|
from .store_project_agent_executor_learning import StoreProjectAgentExecutorLearning
|
|
38
40
|
from .store_project_agent_executor_routing import StoreProjectAgentExecutorRouting
|
|
41
|
+
from .store_transactions import query_store_transaction
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
class StoreProjectAgentExecutors(
|
|
@@ -131,6 +134,8 @@ class StoreProjectAgentExecutors(
|
|
|
131
134
|
(executor:FuliProjectAgentExecutor {
|
|
132
135
|
executor_id: $executor_id
|
|
133
136
|
})
|
|
137
|
+
SET executor._authorization_write_lock = true
|
|
138
|
+
REMOVE executor._authorization_write_lock
|
|
134
139
|
WITH executor, permission,
|
|
135
140
|
coalesce(
|
|
136
141
|
permission.authorization_idempotency_key = $idempotency_key,
|
|
@@ -251,14 +256,27 @@ class StoreProjectAgentExecutors(
|
|
|
251
256
|
(executor:FuliProjectAgentExecutor {
|
|
252
257
|
executor_id: $executor_id
|
|
253
258
|
})
|
|
259
|
+
SET executor._preflight_write_lock = true
|
|
260
|
+
REMOVE executor._preflight_write_lock
|
|
254
261
|
WITH executor, permission,
|
|
255
262
|
coalesce(
|
|
256
263
|
executor.preflight_idempotency_key = $idempotency_key,
|
|
257
264
|
false
|
|
258
265
|
) AS same_key
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
266
|
+
WITH executor, permission, same_key,
|
|
267
|
+
(
|
|
268
|
+
(same_key
|
|
269
|
+
AND executor.preflight_payload_hash = $payload_hash)
|
|
270
|
+
OR (
|
|
271
|
+
NOT same_key
|
|
272
|
+
AND (
|
|
273
|
+
executor.preflight_at IS NULL
|
|
274
|
+
OR executor.preflight_at < $checked_at
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
) AS accepted
|
|
278
|
+
FOREACH (_ IN CASE
|
|
279
|
+
WHEN accepted AND NOT same_key THEN [1] ELSE [] END |
|
|
262
280
|
SET executor.preflight_status = $preflight_status,
|
|
263
281
|
executor.preflight_passed = $preflight_passed,
|
|
264
282
|
executor.preflight_reason = $reason,
|
|
@@ -271,7 +289,7 @@ class StoreProjectAgentExecutors(
|
|
|
271
289
|
executor.updated_at = $timestamp,
|
|
272
290
|
executor.revision = coalesce(executor.revision, 0) + 1
|
|
273
291
|
)
|
|
274
|
-
RETURN executor, permission
|
|
292
|
+
RETURN executor, permission, accepted
|
|
275
293
|
''',
|
|
276
294
|
personal_space_id=request.personal_space_id,
|
|
277
295
|
executor_id=request.executor_id,
|
|
@@ -291,6 +309,11 @@ class StoreProjectAgentExecutors(
|
|
|
291
309
|
)
|
|
292
310
|
if not records:
|
|
293
311
|
raise HTTPException(status_code=404, detail='registered executor not found')
|
|
312
|
+
if not records[0].get('accepted'):
|
|
313
|
+
raise HTTPException(
|
|
314
|
+
status_code=409,
|
|
315
|
+
detail='executor preflight report is stale or its idempotency key was reused',
|
|
316
|
+
)
|
|
294
317
|
return self._executor_from_row(records[0], request.personal_space_id)
|
|
295
318
|
|
|
296
319
|
async def record_project_agent_executor_health(
|
|
@@ -309,14 +332,26 @@ class StoreProjectAgentExecutors(
|
|
|
309
332
|
(executor:FuliProjectAgentExecutor {
|
|
310
333
|
executor_id: $executor_id
|
|
311
334
|
})
|
|
335
|
+
SET executor._health_write_lock = true
|
|
336
|
+
REMOVE executor._health_write_lock
|
|
312
337
|
WITH executor, permission,
|
|
313
338
|
coalesce(
|
|
314
339
|
executor.health_idempotency_key = $idempotency_key,
|
|
315
340
|
false
|
|
316
341
|
) AS same_key
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
342
|
+
WITH executor, permission, same_key,
|
|
343
|
+
(
|
|
344
|
+
(same_key AND executor.health_payload_hash = $payload_hash)
|
|
345
|
+
OR (
|
|
346
|
+
NOT same_key
|
|
347
|
+
AND (
|
|
348
|
+
executor.health_checked_at IS NULL
|
|
349
|
+
OR executor.health_checked_at < $checked_at
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
) AS accepted
|
|
353
|
+
FOREACH (_ IN CASE
|
|
354
|
+
WHEN accepted AND NOT same_key THEN [1] ELSE [] END |
|
|
320
355
|
SET executor.health_status = $health_status,
|
|
321
356
|
executor.health_reason = $reason,
|
|
322
357
|
executor.health_checked_at = $checked_at,
|
|
@@ -325,7 +360,7 @@ class StoreProjectAgentExecutors(
|
|
|
325
360
|
executor.updated_at = $timestamp,
|
|
326
361
|
executor.revision = coalesce(executor.revision, 0) + 1
|
|
327
362
|
)
|
|
328
|
-
RETURN executor, permission
|
|
363
|
+
RETURN executor, permission, accepted
|
|
329
364
|
''',
|
|
330
365
|
personal_space_id=request.personal_space_id,
|
|
331
366
|
executor_id=request.executor_id,
|
|
@@ -338,6 +373,11 @@ class StoreProjectAgentExecutors(
|
|
|
338
373
|
)
|
|
339
374
|
if not records:
|
|
340
375
|
raise HTTPException(status_code=404, detail='registered executor not found')
|
|
376
|
+
if not records[0].get('accepted'):
|
|
377
|
+
raise HTTPException(
|
|
378
|
+
status_code=409,
|
|
379
|
+
detail='executor health report is stale or its idempotency key was reused',
|
|
380
|
+
)
|
|
341
381
|
return self._executor_from_row(records[0], request.personal_space_id)
|
|
342
382
|
|
|
343
383
|
async def list_project_agent_executors(
|
|
@@ -363,6 +403,10 @@ class StoreProjectAgentExecutors(
|
|
|
363
403
|
AND executor.preflight_status = 'passed'
|
|
364
404
|
AND coalesce(executor.workspace_permission, false) = true
|
|
365
405
|
AND executor.health_status <> 'unhealthy'
|
|
406
|
+
AND (
|
|
407
|
+
coalesce(executor.health_required, false) = false
|
|
408
|
+
OR executor.health_status = 'healthy'
|
|
409
|
+
)
|
|
366
410
|
))
|
|
367
411
|
RETURN executor, permission
|
|
368
412
|
ORDER BY coalesce(executor.global_priority, 100), executor.executor_id
|
|
@@ -676,6 +720,10 @@ class StoreProjectAgentExecutors(
|
|
|
676
720
|
) -> ProjectAgentExecutorActualReport:
|
|
677
721
|
"""Persist the executor/model actually observed by a Task Run."""
|
|
678
722
|
|
|
723
|
+
async with query_store_transaction(self) as scoped:
|
|
724
|
+
return await scoped._record_project_agent_executor_actual(actor, request)
|
|
725
|
+
|
|
726
|
+
async def _record_project_agent_executor_actual(self, actor, request):
|
|
679
727
|
self._require_personal()
|
|
680
728
|
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
681
729
|
await authorize_personal_project(
|
|
@@ -684,6 +732,33 @@ class StoreProjectAgentExecutors(
|
|
|
684
732
|
space,
|
|
685
733
|
request.personal_project_id,
|
|
686
734
|
)
|
|
735
|
+
# Explicit ordering also applies when activity already owns the task lock.
|
|
736
|
+
# Profile/preflight/revocation writers cannot change validated state until
|
|
737
|
+
# this observation and its projection commit together.
|
|
738
|
+
for target, match in (
|
|
739
|
+
('task', '''MATCH (task:FuliProjectAgentTask {
|
|
740
|
+
personal_space_id: $personal_space_id,
|
|
741
|
+
personal_project_id: $personal_project_id, task_id: $task_id
|
|
742
|
+
})'''),
|
|
743
|
+
('agent', '''MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})
|
|
744
|
+
-[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
745
|
+
(agent:FuliProjectAgent {agent_id: $agent_id})'''),
|
|
746
|
+
('executor', '''MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})
|
|
747
|
+
-[:HAS_EXECUTOR_PERMISSION]->
|
|
748
|
+
(executor:FuliProjectAgentExecutor {executor_id: $executor_id})'''),
|
|
749
|
+
('permission', '''MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})
|
|
750
|
+
-[permission:HAS_EXECUTOR_PERMISSION]->
|
|
751
|
+
(:FuliProjectAgentExecutor {executor_id: $executor_id})'''),
|
|
752
|
+
):
|
|
753
|
+
await self.runtime.driver.execute_query(
|
|
754
|
+
f'{match} SET {target}._actual_write_lock = true '
|
|
755
|
+
f'REMOVE {target}._actual_write_lock RETURN count(*) AS locked',
|
|
756
|
+
personal_space_id=request.personal_space_id,
|
|
757
|
+
personal_project_id=request.personal_project_id,
|
|
758
|
+
task_id=request.task_id,
|
|
759
|
+
agent_id=request.agent_id,
|
|
760
|
+
executor_id=request.executor_id,
|
|
761
|
+
)
|
|
687
762
|
validation_records, _, _ = await self.runtime.driver.execute_query(
|
|
688
763
|
'''
|
|
689
764
|
MATCH (task:FuliProjectAgentTask {
|
|
@@ -718,7 +793,58 @@ class StoreProjectAgentExecutors(
|
|
|
718
793
|
detail='actual executor is not registered, authorized, and preflighted',
|
|
719
794
|
)
|
|
720
795
|
validation_row = validation_records[0]
|
|
796
|
+
profile_json = dict(validation_row.get('agent') or {}).get('profile_json')
|
|
797
|
+
if profile_json:
|
|
798
|
+
profile = ProjectAgentProfile.model_validate_json(profile_json)
|
|
799
|
+
if (request.source_application or 'other') not in profile.allowed_clients:
|
|
800
|
+
raise HTTPException(
|
|
801
|
+
status_code=403,
|
|
802
|
+
detail='project Agent is not available to this source client',
|
|
803
|
+
)
|
|
721
804
|
task_raw = dict(validation_row.get('task') or {})
|
|
805
|
+
current_projection_exists = any(task_raw.get(key) for key in (
|
|
806
|
+
'actual_executor_id', 'actual_model_provider', 'actual_model'))
|
|
807
|
+
current_actual_at = native_datetime(task_raw.get('actual_occurred_at'))
|
|
808
|
+
if current_projection_exists and current_actual_at is None and task_raw.get('actual_run_id'):
|
|
809
|
+
previous, _, _ = await self.runtime.driver.execute_query('''
|
|
810
|
+
MATCH (task:FuliProjectAgentTask {
|
|
811
|
+
personal_space_id: $personal_space_id,
|
|
812
|
+
personal_project_id: $personal_project_id, task_id: $task_id
|
|
813
|
+
})-[:HAS_EXECUTOR_OBSERVATION]->(observation {
|
|
814
|
+
run_id: $actual_run_id
|
|
815
|
+
})
|
|
816
|
+
WHERE observation.executor_id = task.actual_executor_id
|
|
817
|
+
AND observation.provider = task.actual_model_provider
|
|
818
|
+
AND observation.model = task.actual_model
|
|
819
|
+
AND observation.agent_id = $agent_id
|
|
820
|
+
AND (
|
|
821
|
+
observation.projection_applied = true
|
|
822
|
+
OR (
|
|
823
|
+
observation.projection_applied IS NULL
|
|
824
|
+
AND observation.projection_considered IS NULL
|
|
825
|
+
)
|
|
826
|
+
)
|
|
827
|
+
RETURN observation.occurred_at AS occurred_at
|
|
828
|
+
''', personal_space_id=request.personal_space_id,
|
|
829
|
+
personal_project_id=request.personal_project_id,
|
|
830
|
+
task_id=request.task_id, actual_run_id=task_raw['actual_run_id'],
|
|
831
|
+
agent_id=request.agent_id, routing_='r')
|
|
832
|
+
previous_times = [
|
|
833
|
+
native_datetime(row.get('occurred_at')) for row in previous
|
|
834
|
+
]
|
|
835
|
+
if previous_times and all(
|
|
836
|
+
value is not None
|
|
837
|
+
and callable(getattr(value, 'utcoffset', None))
|
|
838
|
+
and value.utcoffset() is not None
|
|
839
|
+
for value in previous_times
|
|
840
|
+
):
|
|
841
|
+
current_actual_at = max(previous_times)
|
|
842
|
+
projection_allowed = not current_projection_exists
|
|
843
|
+
if current_projection_exists and current_actual_at is not None:
|
|
844
|
+
projection_allowed = (
|
|
845
|
+
current_actual_at.utcoffset() is not None
|
|
846
|
+
and request.occurred_at >= current_actual_at
|
|
847
|
+
)
|
|
722
848
|
task_strategy = ProjectAgentModelStrategy()
|
|
723
849
|
task_strategy_json = task_raw.get('effective_model_strategy_json')
|
|
724
850
|
if task_strategy_json:
|
|
@@ -757,6 +883,11 @@ class StoreProjectAgentExecutors(
|
|
|
757
883
|
status_code=409,
|
|
758
884
|
detail='actual model was not reported by the latest passed preflight',
|
|
759
885
|
)
|
|
886
|
+
if self._select_model([reported_model], task_strategy) is None:
|
|
887
|
+
raise HTTPException(
|
|
888
|
+
status_code=409,
|
|
889
|
+
detail='actual model does not satisfy the task model strategy',
|
|
890
|
+
)
|
|
760
891
|
if (
|
|
761
892
|
executor_record.health_status == 'unhealthy'
|
|
762
893
|
or (
|
|
@@ -841,11 +972,18 @@ class StoreProjectAgentExecutors(
|
|
|
841
972
|
observation.source_session_id = $source_session_id
|
|
842
973
|
WITH task, observation,
|
|
843
974
|
observation.payload_hash = $payload_hash AS payload_matches,
|
|
844
|
-
coalesce(observation.
|
|
975
|
+
coalesce(observation.projection_considered,
|
|
976
|
+
observation.projection_applied, false) = false
|
|
845
977
|
AS projection_pending
|
|
846
978
|
FOREACH (_ IN CASE
|
|
847
979
|
WHEN payload_matches AND projection_pending THEN [1] ELSE [] END |
|
|
848
980
|
MERGE (task)-[:HAS_EXECUTOR_OBSERVATION]->(observation)
|
|
981
|
+
SET observation.projection_considered = true
|
|
982
|
+
)
|
|
983
|
+
FOREACH (_ IN CASE
|
|
984
|
+
WHEN payload_matches AND projection_pending
|
|
985
|
+
AND $projection_allowed
|
|
986
|
+
THEN [1] ELSE [] END |
|
|
849
987
|
SET task.actual_executor_id = $executor_id,
|
|
850
988
|
task.actual_run_id = $run_id,
|
|
851
989
|
task.actual_model_provider = $provider,
|
|
@@ -854,7 +992,10 @@ class StoreProjectAgentExecutors(
|
|
|
854
992
|
task.actual_model_strategy_source = $model_strategy_source,
|
|
855
993
|
task.matched_executor_rule_id = $matched_rule_id,
|
|
856
994
|
task.executor_fallback_reason = $fallback_reason,
|
|
857
|
-
task.
|
|
995
|
+
task.actual_occurred_at = $occurred_at,
|
|
996
|
+
task.updated_at = CASE
|
|
997
|
+
WHEN task.updated_at IS NULL OR task.updated_at < $occurred_at
|
|
998
|
+
THEN $occurred_at ELSE task.updated_at END,
|
|
858
999
|
observation.projection_applied = true
|
|
859
1000
|
)
|
|
860
1001
|
RETURN task, observation, payload_matches
|
|
@@ -876,6 +1017,7 @@ class StoreProjectAgentExecutors(
|
|
|
876
1017
|
occurred_at=request.occurred_at,
|
|
877
1018
|
source_application=request.source_application,
|
|
878
1019
|
source_session_id=request.source_session_id,
|
|
1020
|
+
projection_allowed=projection_allowed,
|
|
879
1021
|
)
|
|
880
1022
|
if not records:
|
|
881
1023
|
raise HTTPException(status_code=404, detail='project Agent task not found')
|
|
@@ -912,10 +1054,34 @@ class StoreProjectAgentExecutors(
|
|
|
912
1054
|
(agent:FuliProjectAgent)
|
|
913
1055
|
WHERE agent.test_source = $test_source
|
|
914
1056
|
AND coalesce(agent.cleanup_eligible, false) = true
|
|
1057
|
+
WITH space, agent
|
|
1058
|
+
ORDER BY agent.agent_id
|
|
1059
|
+
SET agent._task_lifecycle_lock = true
|
|
1060
|
+
REMOVE agent._task_lifecycle_lock
|
|
1061
|
+
WITH space, agent
|
|
1062
|
+
OPTIONAL MATCH (space)-[:HAS_PROJECT_AGENT_TASK]->
|
|
1063
|
+
(task:FuliProjectAgentTask)-
|
|
1064
|
+
[participant:HAS_PARTICIPANT]->(agent)
|
|
1065
|
+
WITH space, agent, collect(DISTINCT {
|
|
1066
|
+
task_id: task.task_id,
|
|
1067
|
+
status: participant.status
|
|
1068
|
+
}) AS tasks
|
|
1069
|
+
WITH space, agent, [item IN tasks
|
|
1070
|
+
WHERE item.task_id IS NOT NULL
|
|
1071
|
+
AND item.status IN [
|
|
1072
|
+
'awaiting_recruitment', 'queued', 'running',
|
|
1073
|
+
'paused', 'blocked', 'awaiting_review'
|
|
1074
|
+
]] AS open_tasks
|
|
1075
|
+
WHERE agent.status = 'archived' OR size(open_tasks) = 0
|
|
1076
|
+
WITH space, agent,
|
|
1077
|
+
coalesce(agent.status, 'active') <> 'archived'
|
|
1078
|
+
AS newly_archived
|
|
915
1079
|
SET agent.status = 'archived',
|
|
916
1080
|
agent.updated_at = $updated_at,
|
|
917
|
-
agent.
|
|
918
|
-
|
|
1081
|
+
agent.archive_reason = coalesce(agent.archive_reason,
|
|
1082
|
+
'test cleanup'),
|
|
1083
|
+
agent.archived_at = coalesce(agent.archived_at, $updated_at)
|
|
1084
|
+
WITH space, agent, newly_archived
|
|
919
1085
|
OPTIONAL MATCH (space)-[:CONTAINS_PROJECT]->
|
|
920
1086
|
(:FuliPersonalProject)-
|
|
921
1087
|
[:HAS_PROJECT_AGENT_ASSIGNMENT]->
|
|
@@ -927,7 +1093,8 @@ class StoreProjectAgentExecutors(
|
|
|
927
1093
|
assignment.ended_at = $updated_at,
|
|
928
1094
|
assignment.updated_at = $updated_at,
|
|
929
1095
|
assignment.revision = coalesce(assignment.revision, 0) + 1
|
|
930
|
-
RETURN count(DISTINCT agent)
|
|
1096
|
+
RETURN count(DISTINCT CASE WHEN newly_archived THEN agent END)
|
|
1097
|
+
AS archived_count
|
|
931
1098
|
''',
|
|
932
1099
|
personal_space_id=personal_space_id,
|
|
933
1100
|
test_source=test_source,
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Versioned working notes; one graph-backed head per space/project/Agent.
|
|
2
|
+
|
|
3
|
+
The property-dependent SET acquires Neo4j's write lock before checking the
|
|
4
|
+
revision. This protects against concurrent hosts, not just one Python process.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
from fastapi import HTTPException
|
|
11
|
+
|
|
12
|
+
from .project_agent_access import authorize_project_agent
|
|
13
|
+
from .project_agent_memory_models import (
|
|
14
|
+
ProjectAgentMemoryRecord,
|
|
15
|
+
ProjectAgentMemoryView,
|
|
16
|
+
ProjectAgentMemoryWrite,
|
|
17
|
+
)
|
|
18
|
+
from .provider_values import now_utc, stable_uuid
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class StoreProjectAgentMemory:
|
|
22
|
+
async def get_project_agent_memory(
|
|
23
|
+
self, actor, personal_space_id, personal_project_id, agent_id, *, limit=1,
|
|
24
|
+
) -> ProjectAgentMemoryView:
|
|
25
|
+
self._require_personal()
|
|
26
|
+
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
27
|
+
await authorize_project_agent(
|
|
28
|
+
self, actor, space, personal_project_id, agent_id, require_memory=True,
|
|
29
|
+
)
|
|
30
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
31
|
+
'''
|
|
32
|
+
MATCH (head:FuliProjectAgentMemory {id: $memory_id})-
|
|
33
|
+
[:HAS_MEMORY_CHECKPOINT]->(checkpoint:FuliProjectAgentMemoryCheckpoint)
|
|
34
|
+
RETURN checkpoint.record_json AS record_json
|
|
35
|
+
ORDER BY checkpoint.revision DESC LIMIT $limit
|
|
36
|
+
''',
|
|
37
|
+
memory_id=self._agent_memory_id(
|
|
38
|
+
personal_space_id, personal_project_id, agent_id,
|
|
39
|
+
),
|
|
40
|
+
limit=max(1, min(limit, 10)),
|
|
41
|
+
routing_='r',
|
|
42
|
+
)
|
|
43
|
+
history = [
|
|
44
|
+
ProjectAgentMemoryRecord.model_validate_json(row['record_json'])
|
|
45
|
+
for row in records
|
|
46
|
+
]
|
|
47
|
+
current = history[0] if history else None
|
|
48
|
+
return ProjectAgentMemoryView(
|
|
49
|
+
personal_space_id=personal_space_id,
|
|
50
|
+
personal_project_id=personal_project_id,
|
|
51
|
+
agent_id=agent_id,
|
|
52
|
+
revision=current.revision if current else 0,
|
|
53
|
+
current=current,
|
|
54
|
+
history=history,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
async def write_project_agent_memory(
|
|
58
|
+
self, actor, request: ProjectAgentMemoryWrite,
|
|
59
|
+
) -> ProjectAgentMemoryRecord:
|
|
60
|
+
self._require_personal()
|
|
61
|
+
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
62
|
+
agent = await authorize_project_agent(
|
|
63
|
+
self, actor, space, request.personal_project_id, request.agent_id,
|
|
64
|
+
require_active=True, require_memory=True,
|
|
65
|
+
)
|
|
66
|
+
allowed_clients = json.loads(agent['profile_json']).get('allowed_clients', [])
|
|
67
|
+
if request.source_application not in allowed_clients:
|
|
68
|
+
raise HTTPException(status_code=403, detail='Agent is not allowed in this client')
|
|
69
|
+
if request.task_id:
|
|
70
|
+
task_rows, _, _ = await self.runtime.driver.execute_query(
|
|
71
|
+
'''
|
|
72
|
+
MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
73
|
+
[:HAS_PROJECT_AGENT_TASK]->(task:FuliProjectAgentTask {
|
|
74
|
+
task_id: $task_id, personal_project_id: $project_id
|
|
75
|
+
})-[:HAS_PARTICIPANT]->(agent:FuliProjectAgent {agent_id: $agent_id})
|
|
76
|
+
MATCH (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(agent)
|
|
77
|
+
RETURN task.task_id AS task_id
|
|
78
|
+
''',
|
|
79
|
+
space_id=request.personal_space_id,
|
|
80
|
+
project_id=request.personal_project_id,
|
|
81
|
+
agent_id=request.agent_id,
|
|
82
|
+
task_id=request.task_id,
|
|
83
|
+
routing_='r',
|
|
84
|
+
)
|
|
85
|
+
if not task_rows:
|
|
86
|
+
raise HTTPException(status_code=404, detail='Agent memory task not found')
|
|
87
|
+
memory_id = self._agent_memory_id(
|
|
88
|
+
request.personal_space_id, request.personal_project_id, request.agent_id,
|
|
89
|
+
)
|
|
90
|
+
checkpoint_id = stable_uuid(memory_id, request.idempotency_key)
|
|
91
|
+
payload = request.model_dump(mode='json', exclude={
|
|
92
|
+
'source_application', 'source_session_id',
|
|
93
|
+
})
|
|
94
|
+
payload_hash = hashlib.sha256(json.dumps(
|
|
95
|
+
payload, sort_keys=True, ensure_ascii=False, separators=(',', ':'),
|
|
96
|
+
).encode()).hexdigest()
|
|
97
|
+
checkpoint = ProjectAgentMemoryRecord(
|
|
98
|
+
checkpoint_id=checkpoint_id,
|
|
99
|
+
personal_space_id=request.personal_space_id,
|
|
100
|
+
personal_project_id=request.personal_project_id,
|
|
101
|
+
agent_id=request.agent_id,
|
|
102
|
+
revision=request.expected_revision + 1,
|
|
103
|
+
memory=request.memory,
|
|
104
|
+
source_application=request.source_application,
|
|
105
|
+
source_session_id=request.source_session_id,
|
|
106
|
+
task_id=request.task_id,
|
|
107
|
+
created_at=now_utc(),
|
|
108
|
+
)
|
|
109
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
110
|
+
'''
|
|
111
|
+
MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
112
|
+
[:HAS_PROJECT_AGENT_IDENTITY]->(agent:FuliProjectAgent {
|
|
113
|
+
agent_id: $agent_id, status: 'active'
|
|
114
|
+
})
|
|
115
|
+
MATCH (space)-[:CONTAINS_PROJECT]->
|
|
116
|
+
(project:FuliPersonalProject {project_id: $project_id})-
|
|
117
|
+
[:HAS_PROJECT_AGENT_ASSIGNMENT]->
|
|
118
|
+
(:FuliProjectAgentAssignment {status: 'active'})-[:ASSIGNED_AGENT]->(agent)
|
|
119
|
+
WHERE coalesce(agent.memory_scope, 'reviewed_agent') <> 'task_only'
|
|
120
|
+
WITH DISTINCT space, project, agent
|
|
121
|
+
MERGE (head:FuliProjectAgentMemory {id: $memory_id})
|
|
122
|
+
ON CREATE SET head.revision = 0,
|
|
123
|
+
head.personal_space_id = $space_id,
|
|
124
|
+
head.personal_project_id = $project_id,
|
|
125
|
+
head.agent_id = $agent_id
|
|
126
|
+
SET head.write_serial = coalesce(head.write_serial, 0) + 1
|
|
127
|
+
MERGE (agent)-[:HAS_WORKING_MEMORY]->(head)
|
|
128
|
+
MERGE (project)-[:HAS_AGENT_WORKING_MEMORY]->(head)
|
|
129
|
+
WITH head
|
|
130
|
+
OPTIONAL MATCH (head)-[:HAS_MEMORY_CHECKPOINT]->
|
|
131
|
+
(existing:FuliProjectAgentMemoryCheckpoint {id: $checkpoint_id})
|
|
132
|
+
WITH head, existing,
|
|
133
|
+
existing IS NULL AND head.revision = $expected_revision AS can_write
|
|
134
|
+
FOREACH (ignored IN CASE WHEN can_write THEN [1] ELSE [] END |
|
|
135
|
+
CREATE (checkpoint:FuliProjectAgentMemoryCheckpoint {
|
|
136
|
+
id: $checkpoint_id, revision: $revision,
|
|
137
|
+
payload_hash: $payload_hash, record_json: $record_json
|
|
138
|
+
})
|
|
139
|
+
CREATE (head)-[:HAS_MEMORY_CHECKPOINT]->(checkpoint)
|
|
140
|
+
SET head.revision = $revision, head.updated_at = $created_at
|
|
141
|
+
)
|
|
142
|
+
WITH head
|
|
143
|
+
OPTIONAL MATCH (head)-[:HAS_MEMORY_CHECKPOINT]->
|
|
144
|
+
(saved:FuliProjectAgentMemoryCheckpoint {id: $checkpoint_id})
|
|
145
|
+
RETURN saved.record_json AS record_json, saved.payload_hash AS payload_hash,
|
|
146
|
+
head.revision AS current_revision
|
|
147
|
+
''',
|
|
148
|
+
space_id=request.personal_space_id,
|
|
149
|
+
project_id=request.personal_project_id,
|
|
150
|
+
agent_id=request.agent_id,
|
|
151
|
+
memory_id=memory_id,
|
|
152
|
+
checkpoint_id=checkpoint_id,
|
|
153
|
+
expected_revision=request.expected_revision,
|
|
154
|
+
revision=checkpoint.revision,
|
|
155
|
+
payload_hash=payload_hash,
|
|
156
|
+
record_json=checkpoint.model_dump_json(),
|
|
157
|
+
created_at=checkpoint.created_at,
|
|
158
|
+
)
|
|
159
|
+
if not records:
|
|
160
|
+
raise HTTPException(status_code=409, detail='Agent assignment is no longer active')
|
|
161
|
+
row = records[0]
|
|
162
|
+
if not row.get('record_json'):
|
|
163
|
+
raise HTTPException(
|
|
164
|
+
status_code=409,
|
|
165
|
+
detail='Agent memory changed; reload its latest revision before merging notes',
|
|
166
|
+
)
|
|
167
|
+
if row['payload_hash'] != payload_hash:
|
|
168
|
+
raise HTTPException(status_code=409, detail='memory idempotency key has different input')
|
|
169
|
+
return ProjectAgentMemoryRecord.model_validate_json(row['record_json'])
|
|
170
|
+
|
|
171
|
+
def _agent_memory_id(self, space_id, project_id, agent_id):
|
|
172
|
+
return stable_uuid(
|
|
173
|
+
self.settings.provider_id, space_id, project_id, agent_id, 'working-memory',
|
|
174
|
+
)
|