fuli-context 0.7.4-beta.2 → 0.7.5

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.
Files changed (133) hide show
  1. package/README.md +93 -7
  2. package/README.zh-CN.md +84 -7
  3. package/compose.graphiti.yml +6 -6
  4. package/dist/web/assets/AboutPage-ByCUor-j.js +1 -0
  5. package/dist/web/assets/ConnectionsPage-DkdkZ40Q.js +1 -0
  6. package/dist/web/assets/KnowledgeEditDialog-DSe3Gwep.js +1 -0
  7. package/dist/web/assets/KnowledgeInspector-CYAjAb9J.js +1 -0
  8. package/dist/web/assets/KnowledgeOrganizerPage-CV4F54R_.js +1 -0
  9. package/dist/web/assets/KnowledgePage-vYYXipOi.js +1 -0
  10. package/dist/web/assets/KnowledgeWorkspace-z4u5mXOG.js +4 -0
  11. package/dist/web/assets/OverviewPage-BWOfeS6k.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-c3w4-h7a.js +1 -0
  13. package/dist/web/assets/PersonalProjectsPage-B7AlLEwf.js +1 -0
  14. package/dist/web/assets/ProjectAgentsPage-CgftpIPB.css +1 -0
  15. package/dist/web/assets/ProjectAgentsPage-DTHcL_ox.js +11 -0
  16. package/dist/web/assets/PublicProjectsPage-DdH0oGB3.js +1 -0
  17. package/dist/web/assets/ReviewPage-BYIpcIHG.js +1 -0
  18. package/dist/web/assets/SearchableSelect-Dz13ClQb.js +1 -0
  19. package/dist/web/assets/SettingsPage-2VkbXgZi.js +1 -0
  20. package/dist/web/assets/SettingsPage-CwZp2ka4.css +1 -0
  21. package/dist/web/assets/WritingTastePage-BOPQZuva.js +1 -0
  22. package/dist/web/assets/{console-DMvHxKf9.js → console-BlAhujsH.js} +1 -1
  23. package/dist/web/assets/{conversation-launcher-settings-Dnlcw7Rj.js → conversation-launcher-settings-DTYxiLJt.js} +1 -1
  24. package/dist/web/assets/i18n-Ci3heuiL.js +9 -0
  25. package/dist/web/assets/{identity-C9mLdR8L.js → identity-4_-v6cB7.js} +1 -1
  26. package/dist/web/assets/index-BnH5mcZx.css +1 -0
  27. package/dist/web/assets/index-C67Q0JY2.js +2 -0
  28. package/dist/web/assets/{model-CsYAR8O3.js → model-yG0YqUyZ.js} +1 -1
  29. package/dist/web/assets/{useApi-CROJJdhE-BLQiPVw0.js → useApi-CROJJdhE-twhdZArA.js} +1 -1
  30. package/dist/web/assets/vue-router-C-wot_Di.js +1 -0
  31. package/dist/web/index.html +6 -6
  32. package/docs/public-personal-architecture.md +73 -0
  33. package/graph-provider/fuli_graph/app.py +8 -0
  34. package/graph-provider/fuli_graph/collaboration_context.py +69 -6
  35. package/graph-provider/fuli_graph/knowledge_search.py +84 -5
  36. package/graph-provider/fuli_graph/model_validation.py +134 -0
  37. package/graph-provider/fuli_graph/models.py +24 -5
  38. package/graph-provider/fuli_graph/project_agent_access.py +54 -0
  39. package/graph-provider/fuli_graph/project_agent_executor_models.py +635 -0
  40. package/graph-provider/fuli_graph/project_agent_models.py +278 -0
  41. package/graph-provider/fuli_graph/project_agent_routes.py +597 -0
  42. package/graph-provider/fuli_graph/project_agent_task_models.py +542 -0
  43. package/graph-provider/fuli_graph/runtime.py +259 -0
  44. package/graph-provider/fuli_graph/search_projection.py +3 -0
  45. package/graph-provider/fuli_graph/store.py +6 -0
  46. package/graph-provider/fuli_graph/store_knowledge.py +62 -9
  47. package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +756 -0
  48. package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +601 -0
  49. package/graph-provider/fuli_graph/store_project_agent_executors.py +1087 -0
  50. package/graph-provider/fuli_graph/store_project_agent_task_reads.py +458 -0
  51. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +699 -0
  52. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +409 -0
  53. package/graph-provider/fuli_graph/store_project_agent_tasks.py +1144 -0
  54. package/graph-provider/fuli_graph/store_project_agents.py +965 -0
  55. package/graph-provider/pyproject.toml +2 -1
  56. package/npm-shrinkwrap.json +2 -2
  57. package/package.json +2 -1
  58. package/skills/capturing-session-knowledge/SKILL.md +43 -2
  59. package/src/adaptive-runtime/executor-pool.js +127 -0
  60. package/src/adaptive-runtime/graph-services.js +166 -0
  61. package/src/adaptive-runtime/lease-client.js +335 -0
  62. package/src/adaptive-runtime/runtime-broker.js +318 -0
  63. package/src/adaptive-runtime/settings.js +111 -0
  64. package/src/adaptive-runtime/state.js +74 -0
  65. package/src/agent-tools/graph-definitions.js +396 -0
  66. package/src/agent-tools/graph-handlers.js +61 -0
  67. package/src/agent-tools/project-agent-definitions.js +307 -0
  68. package/src/backup/graph-backup.js +289 -0
  69. package/src/backup/identity-reconciliation.js +208 -0
  70. package/src/backup/runtime-adapters.js +269 -0
  71. package/src/cli/command-registry.js +7 -2
  72. package/src/cli/graph-data-command.js +112 -0
  73. package/src/cli/local-runtime-command.js +16 -2
  74. package/src/cli/setup-command.js +19 -3
  75. package/src/cli/update-command.js +4 -0
  76. package/src/cli/workspace-connection-command.js +333 -0
  77. package/src/cli.js +11 -0
  78. package/src/graphiti/collaboration-preference-projection.js +29 -1
  79. package/src/graphiti/collaboration-preference-workflow.js +121 -0
  80. package/src/graphiti/federated-application.js +74 -112
  81. package/src/graphiti/knowledge-provider-mapping.js +7 -1
  82. package/src/graphiti/project-agent-control-plane.js +263 -0
  83. package/src/graphiti/project-agent-mapping.js +1003 -0
  84. package/src/graphiti/project-agent-workflows.js +557 -0
  85. package/src/graphiti/provider-client.js +270 -1
  86. package/src/graphiti/runtime-config.js +6 -1
  87. package/src/graphiti/workspace-provider-client.js +403 -0
  88. package/src/http/api-router.js +63 -9
  89. package/src/http/graph-api-router.js +278 -0
  90. package/src/local-runtime/lifecycle.js +46 -10
  91. package/src/mcp/context-resources.js +15 -6
  92. package/src/mcp/create-mcp-server.js +51 -16
  93. package/src/mcp/instructions.js +7 -5
  94. package/src/mcp/runtime.js +17 -5
  95. package/src/mcp/session-id.js +38 -1
  96. package/src/mcp/tool-annotations.js +69 -0
  97. package/src/mcp-server.js +23 -6
  98. package/src/native-runtime/runtime.js +717 -0
  99. package/src/server.js +6 -2
  100. package/src/setup/agent-installation-status.js +7 -1
  101. package/src/setup/agents.js +3 -1
  102. package/src/setup/claude-code-config.js +3 -1
  103. package/src/setup/codex-bootstrap.js +11 -10
  104. package/src/setup/codex-config.js +7 -1
  105. package/src/setup/cursor-config.js +3 -1
  106. package/src/setup/graph-runtime.js +158 -13
  107. package/src/setup/neo4j-memory-profile.js +43 -0
  108. package/src/setup/options.js +38 -2
  109. package/src/setup/paths.js +14 -0
  110. package/src/setup/setup.js +37 -1
  111. package/src/system/resource-monitor.js +181 -16
  112. package/src/system/runtime-settings.js +10 -1
  113. package/src/system/system-service.js +27 -2
  114. package/dist/web/assets/AboutPage-CftRexSY.js +0 -1
  115. package/dist/web/assets/ConnectionsPage-D5J91x6d.js +0 -1
  116. package/dist/web/assets/KnowledgeEditDialog-wotVahvr.js +0 -1
  117. package/dist/web/assets/KnowledgeInspector-BzkW0mVs.js +0 -1
  118. package/dist/web/assets/KnowledgeOrganizerPage-BebUDiHt.js +0 -1
  119. package/dist/web/assets/KnowledgePage-OUT07aKH.js +0 -1
  120. package/dist/web/assets/KnowledgeWorkspace-1pHJ0MNZ.js +0 -4
  121. package/dist/web/assets/OverviewPage-CHE6XhdS.js +0 -1
  122. package/dist/web/assets/PersonalProfilePage-CXXiabUy.js +0 -1
  123. package/dist/web/assets/PersonalProjectsPage-d2wYGW8n.js +0 -1
  124. package/dist/web/assets/PublicProjectsPage-hxLShtnZ.js +0 -1
  125. package/dist/web/assets/ReviewPage-DUeyu7E8.js +0 -1
  126. package/dist/web/assets/SearchableSelect-C5RiB5F8.js +0 -1
  127. package/dist/web/assets/SettingsPage-BMuASSzQ.js +0 -1
  128. package/dist/web/assets/SettingsPage-CAfz4qiV.css +0 -1
  129. package/dist/web/assets/WritingTastePage-B9BCYIwQ.js +0 -1
  130. package/dist/web/assets/i18n-CWfd8Dtt.js +0 -5
  131. package/dist/web/assets/index-X2j29j8i.css +0 -1
  132. package/dist/web/assets/index-nP5p5OaY.js +0 -2
  133. package/dist/web/assets/vue-router-DN8x6iY1.js +0 -1
@@ -18,6 +18,9 @@ from graphiti_core.prompts.models import Message
18
18
  from pydantic import BaseModel
19
19
 
20
20
  from .config import Settings
21
+ from .store_project_agent_executor_learning import (
22
+ project_agent_executor_outcome_bucket_id,
23
+ )
21
24
 
22
25
 
23
26
  class AgentStructuredLLM(LLMClient):
@@ -235,6 +238,42 @@ class GraphitiRuntime:
235
238
  'FOR (n:FuliPersonalProjectRelationReview) '
236
239
  'REQUIRE n.id IS UNIQUE'
237
240
  )
241
+ # Legacy learning caches were keyed only by a composite property map.
242
+ # Collapse those buckets before adding ID uniqueness so startup remains
243
+ # safe for upgraded stores as well as fresh databases.
244
+ await self._migrate_project_agent_executor_learning_buckets()
245
+ for query in (
246
+ 'CREATE CONSTRAINT fuli_project_agent_id IF NOT EXISTS '
247
+ 'FOR (n:FuliProjectAgent) REQUIRE n.id IS UNIQUE',
248
+ 'CREATE CONSTRAINT fuli_project_agent_assignment_id IF NOT EXISTS '
249
+ 'FOR (n:FuliProjectAgentAssignment) REQUIRE n.id IS UNIQUE',
250
+ 'CREATE CONSTRAINT fuli_project_agent_task_id IF NOT EXISTS '
251
+ 'FOR (n:FuliProjectAgentTask) REQUIRE n.id IS UNIQUE',
252
+ 'CREATE CONSTRAINT fuli_project_agent_task_event_id IF NOT EXISTS '
253
+ 'FOR (n:FuliProjectAgentTaskEvent) REQUIRE n.id IS UNIQUE',
254
+ 'CREATE CONSTRAINT fuli_project_agent_routing_decision_id IF NOT EXISTS '
255
+ 'FOR (n:FuliProjectAgentRoutingDecision) REQUIRE n.id IS UNIQUE',
256
+ 'CREATE CONSTRAINT fuli_project_agent_recruitment_id IF NOT EXISTS '
257
+ 'FOR (n:FuliProjectAgentRecruitment) REQUIRE n.id IS UNIQUE',
258
+ 'CREATE CONSTRAINT fuli_project_agent_recruitment_policy_space '
259
+ 'IF NOT EXISTS FOR (n:FuliProjectAgentRecruitmentPolicy) '
260
+ 'REQUIRE n.personal_space_id IS UNIQUE',
261
+ 'CREATE CONSTRAINT fuli_project_agent_executor_id IF NOT EXISTS '
262
+ 'FOR (n:FuliProjectAgentExecutor) REQUIRE n.id IS UNIQUE',
263
+ 'CREATE CONSTRAINT fuli_project_agent_executor_routing_rule_id IF NOT EXISTS '
264
+ 'FOR (n:FuliProjectAgentExecutorRoutingRule) REQUIRE n.id IS UNIQUE',
265
+ 'CREATE CONSTRAINT fuli_project_agent_executor_decision_id IF NOT EXISTS '
266
+ 'FOR (n:FuliProjectAgentExecutorDecision) REQUIRE n.id IS UNIQUE',
267
+ 'CREATE CONSTRAINT fuli_project_agent_executor_outcome_evidence_id IF NOT EXISTS '
268
+ 'FOR (n:FuliProjectAgentExecutorOutcomeEvidence) REQUIRE n.id IS UNIQUE',
269
+ 'CREATE CONSTRAINT fuli_project_agent_executor_outcome_aggregate_id IF NOT EXISTS '
270
+ 'FOR (n:FuliProjectAgentExecutorOutcomeAggregate) REQUIRE n.id IS UNIQUE',
271
+ 'CREATE CONSTRAINT fuli_project_agent_executor_outcome_reset_id IF NOT EXISTS '
272
+ 'FOR (n:FuliProjectAgentExecutorOutcomeReset) REQUIRE n.id IS UNIQUE',
273
+ 'CREATE CONSTRAINT fuli_project_agent_executor_observation_id IF NOT EXISTS '
274
+ 'FOR (n:FuliProjectAgentExecutorObservation) REQUIRE n.id IS UNIQUE',
275
+ ):
276
+ await self.driver.execute_query(query)
238
277
  for query in (
239
278
  'CREATE INDEX fuli_entity_confirmation IF NOT EXISTS '
240
279
  'FOR (n:Entity) ON (n.group_id, n.fuli_confirmation_status)',
@@ -259,14 +298,234 @@ class GraphitiRuntime:
259
298
  'ON (n.personal_space_id, n.scope_key, n.status)',
260
299
  'CREATE INDEX fuli_personal_project_relation_type IF NOT EXISTS '
261
300
  'FOR ()-[r:PERSONAL_PROJECT_RELATION]-() ON (r.relation_type)',
301
+ 'CREATE INDEX fuli_project_agent_assignment_lookup IF NOT EXISTS '
302
+ 'FOR (n:FuliProjectAgentAssignment) '
303
+ 'ON (n.status, n.assigned_at)',
304
+ 'CREATE INDEX fuli_project_agent_task_lookup IF NOT EXISTS '
305
+ 'FOR (n:FuliProjectAgentTask) '
306
+ 'ON (n.personal_space_id, n.personal_project_id, n.status)',
307
+ 'CREATE INDEX fuli_project_agent_event_activity IF NOT EXISTS '
308
+ 'FOR (n:FuliProjectAgentTaskEvent) '
309
+ 'ON (n.agent_id, n.activity_date, n.status)',
310
+ 'CREATE INDEX fuli_project_agent_executor_eligibility IF NOT EXISTS '
311
+ 'FOR (n:FuliProjectAgentExecutor) '
312
+ 'ON (n.personal_space_id, n.registration_status, n.preflight_status, n.health_status)',
313
+ 'CREATE INDEX fuli_project_agent_executor_rule_scope IF NOT EXISTS '
314
+ 'FOR (n:FuliProjectAgentExecutorRoutingRule) '
315
+ 'ON (n.personal_space_id, n.scope, n.personal_project_id, n.task_id)',
316
+ 'CREATE INDEX fuli_project_agent_executor_evidence_lookup IF NOT EXISTS '
317
+ 'FOR (n:FuliProjectAgentExecutorOutcomeEvidence) '
318
+ 'ON (n.personal_space_id, n.agent_id, n.work_kind, n.executor_id, n.occurred_at)',
319
+ 'CREATE INDEX fuli_project_agent_aggregate_lookup IF NOT EXISTS '
320
+ 'FOR (n:FuliProjectAgentExecutorOutcomeAggregate) '
321
+ 'ON (n.personal_space_id, n.personal_project_id, n.agent_id, '
322
+ 'n.work_kind, n.executor_id, n.model_strategy_key)',
262
323
  ):
263
324
  await self.driver.execute_query(query)
325
+ await self._migrate_project_agent_control_plane()
264
326
  await self._migrate_knowledge_lifecycle_defaults()
265
327
  await self._migrate_personal_project_relation_review_defaults()
266
328
  await self._migrate_space_visibility_and_owners()
267
329
  await self._migrate_project_releases()
268
330
  await self._migrate_legacy_group_ids()
269
331
 
332
+ async def _migrate_project_agent_executor_learning_buckets(self) -> None:
333
+ """Give legacy learning caches stable IDs and collapse duplicates.
334
+
335
+ Aggregates and resets are replaceable projections. For each complete
336
+ provider/space bucket we keep the newest projection, remove only older
337
+ projection nodes, and leave immutable outcome evidence untouched.
338
+ """
339
+
340
+ bucket_properties = (
341
+ 'personal_space_id',
342
+ 'personal_project_id',
343
+ 'work_kind',
344
+ 'agent_id',
345
+ 'executor_id',
346
+ 'model_strategy_key',
347
+ )
348
+ migrations = (
349
+ (
350
+ 'FuliProjectAgentExecutorOutcomeAggregate',
351
+ 'aggregate',
352
+ 'bucket.as_of',
353
+ 'aggregate_id',
354
+ ),
355
+ (
356
+ 'FuliProjectAgentExecutorOutcomeReset',
357
+ 'reset',
358
+ 'bucket.reset_at',
359
+ 'reset_id',
360
+ ),
361
+ )
362
+ for label, bucket_kind, latest_property, public_id_property in migrations:
363
+ projections = ', '.join(
364
+ f'bucket.{property_name} AS {property_name}'
365
+ for property_name in bucket_properties
366
+ )
367
+ completeness = ' AND '.join(
368
+ f'bucket.{property_name} IS NOT NULL'
369
+ for property_name in bucket_properties
370
+ )
371
+ records, _, _ = await self.driver.execute_query(
372
+ f'''
373
+ MATCH (bucket:{label})
374
+ WHERE {completeness}
375
+ RETURN DISTINCT {projections}
376
+ ''',
377
+ routing_='r',
378
+ )
379
+ for record in records:
380
+ bucket = {
381
+ property_name: str(record[property_name])
382
+ for property_name in bucket_properties
383
+ }
384
+ bucket_id = project_agent_executor_outcome_bucket_id(
385
+ self.settings.provider_id,
386
+ bucket['personal_space_id'],
387
+ bucket['personal_project_id'],
388
+ bucket['work_kind'],
389
+ bucket['agent_id'],
390
+ bucket['executor_id'],
391
+ bucket['model_strategy_key'],
392
+ bucket_kind=bucket_kind,
393
+ )
394
+ property_match = ', '.join(
395
+ f'{property_name}: ${property_name}'
396
+ for property_name in bucket_properties
397
+ )
398
+ await self.driver.execute_query(
399
+ f'''
400
+ MATCH (bucket:{label} {{{property_match}}})
401
+ WITH bucket
402
+ ORDER BY coalesce(
403
+ {latest_property},
404
+ bucket.updated_at,
405
+ bucket.created_at,
406
+ datetime({{epochMillis: 0}})
407
+ ) DESC, elementId(bucket) DESC
408
+ WITH collect(bucket) AS buckets
409
+ WITH head(buckets) AS keeper, tail(buckets) AS duplicates
410
+ CALL {{
411
+ WITH duplicates
412
+ UNWIND duplicates AS duplicate
413
+ DETACH DELETE duplicate
414
+ RETURN count(*) AS removed_count
415
+ }}
416
+ SET keeper.id = $bucket_id,
417
+ keeper.{public_id_property} = $bucket_id
418
+ RETURN removed_count
419
+ ''',
420
+ **bucket,
421
+ bucket_id=bucket_id,
422
+ )
423
+
424
+ async def _migrate_project_agent_control_plane(self) -> None:
425
+ """Preserve legacy project-bound records while adding space identities.
426
+
427
+ The migration does not merge Agents by name or rewrite any existing
428
+ knowledge namespace. It only creates explicit assignment history and
429
+ the one system coordinator required by the lightweight control plane.
430
+ """
431
+ await self.driver.execute_query(
432
+ '''
433
+ MATCH (space:FuliSpace {kind: 'personal'})-[:CONTAINS_PROJECT]->
434
+ (project:FuliPersonalProject)-[:HAS_PROJECT_AGENT]->
435
+ (agent:FuliProjectAgent)
436
+ MERGE (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(agent)
437
+ SET agent.agent_id = coalesce(agent.agent_id, agent.id),
438
+ agent.agent_type = coalesce(agent.agent_type, 'durable'),
439
+ agent.memory_scope = coalesce(agent.memory_scope, 'reviewed_agent'),
440
+ agent.status = coalesce(agent.status, 'active')
441
+ WITH project, agent,
442
+ agent.id + ':legacy-assignment:' + project.id AS assignment_id
443
+ MERGE (assignment:FuliProjectAgentAssignment {id: assignment_id})
444
+ ON CREATE SET assignment.assignment_id = assignment_id,
445
+ assignment.responsibility =
446
+ coalesce(agent.responsibility, agent.name),
447
+ assignment.work_kinds =
448
+ coalesce(agent.work_kinds, []),
449
+ assignment.capabilities =
450
+ coalesce(agent.capabilities, []),
451
+ assignment.reason = 'legacy Project Agent migration',
452
+ assignment.status = CASE agent.status
453
+ WHEN 'active' THEN 'active' ELSE 'ended' END,
454
+ assignment.revision = 0,
455
+ assignment.assigned_at = agent.created_at,
456
+ assignment.updated_at =
457
+ coalesce(agent.updated_at, agent.created_at),
458
+ assignment.legacy_assignment = true
459
+ MERGE (project)-[:HAS_PROJECT_AGENT_ASSIGNMENT]->(assignment)
460
+ MERGE (assignment)-[:ASSIGNED_AGENT]->(agent)
461
+ '''
462
+ )
463
+ await self.driver.execute_query(
464
+ '''
465
+ MATCH (space:FuliSpace {kind: 'personal'})
466
+ MERGE (policy:FuliProjectAgentRecruitmentPolicy {
467
+ personal_space_id: space.id
468
+ })
469
+ ON CREATE SET policy.confirmation_mode = 'automatic',
470
+ policy.updated_at = datetime()
471
+ MERGE (space)-[:HAS_PROJECT_AGENT_RECRUITMENT_POLICY]->(policy)
472
+ '''
473
+ )
474
+ records, _, _ = await self.driver.execute_query(
475
+ "MATCH (space:FuliSpace {kind: 'personal'}) RETURN space.id AS space_id",
476
+ routing_='r',
477
+ )
478
+ if not records:
479
+ return
480
+ from .project_agent_models import ProjectAgentProfile
481
+ from .provider_values import now_utc, stable_uuid
482
+
483
+ profile = ProjectAgentProfile(
484
+ name='项目协调人',
485
+ responsibility='评估任务、应用用户配置并路由已有 Agent。',
486
+ agent_type='coordinator',
487
+ work_kinds=['task-coordination'],
488
+ capabilities=['任务评估', '策略应用', 'Agent 路由'],
489
+ initial_preferences=['质量与可验收完成优先于成本和时间'],
490
+ status='active',
491
+ )
492
+ updated_at = now_utc()
493
+ for record in records:
494
+ space_id = record['space_id']
495
+ node_id = stable_uuid(
496
+ self.settings.provider_id,
497
+ space_id,
498
+ 'project-agent',
499
+ 'fuli-project-coordinator',
500
+ )
501
+ await self.driver.execute_query(
502
+ '''
503
+ MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
504
+ MERGE (agent:FuliProjectAgent {id: $id})
505
+ ON CREATE SET agent.agent_id = 'fuli-project-coordinator',
506
+ agent.profile_json = $profile_json,
507
+ agent.name = $name,
508
+ agent.responsibility = $responsibility,
509
+ agent.capabilities = $capabilities,
510
+ agent.work_kinds = $work_kinds,
511
+ agent.agent_type = 'coordinator',
512
+ agent.memory_scope = 'reviewed_agent',
513
+ agent.status = 'active',
514
+ agent.system_managed = true,
515
+ agent.created_at = $updated_at,
516
+ agent.updated_at = $updated_at
517
+ MERGE (space)-[:HAS_PROJECT_AGENT_IDENTITY]->(agent)
518
+ ''',
519
+ space_id=space_id,
520
+ id=node_id,
521
+ profile_json=profile.model_dump_json(),
522
+ name=profile.name,
523
+ responsibility=profile.responsibility,
524
+ capabilities=profile.capabilities,
525
+ work_kinds=profile.work_kinds,
526
+ updated_at=updated_at,
527
+ )
528
+
270
529
  async def _migrate_personal_project_relation_review_defaults(self) -> None:
271
530
  await self.driver.execute_query(
272
531
  '''
@@ -57,6 +57,7 @@ async def read_edge_epistemic_metadata(store, edge_ids: list[str]) -> dict[str,
57
57
  ELSE coalesce(edge.fuli_preference_scope, 'global') END
58
58
  AS preference_scope,
59
59
  edge.fuli_preference_project_id AS preference_project_id,
60
+ edge.fuli_preference_agent_id AS preference_agent_id,
60
61
  edge.fuli_key AS key,
61
62
  edge.fuli_attributes_json AS attributes_json,
62
63
  coalesce(edge.fuli_inheritance_mode, 'local_only')
@@ -140,6 +141,7 @@ def fact_result(
140
141
  profile_aspect=metadata.get('profile_aspect'),
141
142
  preference_scope=metadata.get('preference_scope'),
142
143
  preference_project_id=metadata.get('preference_project_id'),
144
+ preference_agent_id=metadata.get('preference_agent_id'),
143
145
  key=metadata.get('key'),
144
146
  preference_key=(
145
147
  attributes.get('preferenceKey')
@@ -149,6 +151,7 @@ def fact_result(
149
151
  ),
150
152
  preference_qualifiers=preference_qualifiers(attributes),
151
153
  defined_project_id=metadata.get('defined_project_id'),
154
+ project_agent_id=metadata.get('project_agent_id'),
152
155
  inheritance_mode=metadata.get('inheritance_mode') or 'local_only',
153
156
  inherited_project_ids=metadata.get('inherited_project_ids') or [],
154
157
  human_edited=metadata.get('human_edited') is True,
@@ -3,6 +3,9 @@ from .provider_values import graphiti_group_id, native_datetime
3
3
  from .runtime import GraphitiRuntime
4
4
  from .store_identity import StoreIdentity
5
5
  from .store_knowledge import StoreKnowledge
6
+ from .store_project_agent_executors import StoreProjectAgentExecutors
7
+ from .store_project_agents import StoreProjectAgents
8
+ from .store_project_agent_tasks import StoreProjectAgentTasks
6
9
  from .store_projects import StoreProjects
7
10
  from .store_publication import StorePublication
8
11
  from .store_records import StoreRecords
@@ -11,6 +14,9 @@ from .store_records import StoreRecords
11
14
  class GraphStore(
12
15
  StoreIdentity,
13
16
  StoreProjects,
17
+ StoreProjectAgents,
18
+ StoreProjectAgentExecutors,
19
+ StoreProjectAgentTasks,
14
20
  StorePublication,
15
21
  StoreKnowledge,
16
22
  StoreRecords,
@@ -16,6 +16,7 @@ from .models import (
16
16
  StructuredEpisode,
17
17
  )
18
18
  from .personal_project_access import authorize_personal_project
19
+ from .project_agent_access import authorize_project_agent
19
20
  from .provider_values import now_utc, stable_uuid
20
21
  from .workflow_candidates import materialize_workflow_candidates
21
22
 
@@ -30,10 +31,21 @@ class StoreKnowledge:
30
31
  await authorize_personal_project(
31
32
  self, actor, space, request.personal_project_id
32
33
  )
34
+ if request.project_agent_id:
35
+ await authorize_project_agent(
36
+ self,
37
+ actor,
38
+ space,
39
+ request.personal_project_id,
40
+ request.project_agent_id,
41
+ require_active=True,
42
+ require_memory=True,
43
+ )
33
44
  return await self._commit_episode(
34
45
  space,
35
46
  request.episode,
36
47
  personal_project_id=request.personal_project_id,
48
+ project_agent_id=request.project_agent_id,
37
49
  )
38
50
 
39
51
  async def commit_workflow_observation(
@@ -52,10 +64,21 @@ class StoreKnowledge:
52
64
  await authorize_personal_project(
53
65
  self, actor, space, request.personal_project_id
54
66
  )
67
+ if request.project_agent_id:
68
+ await authorize_project_agent(
69
+ self,
70
+ actor,
71
+ space,
72
+ request.personal_project_id,
73
+ request.project_agent_id,
74
+ require_active=True,
75
+ require_memory=True,
76
+ )
55
77
  return await self._commit_episode(
56
78
  space,
57
79
  request.episode,
58
80
  personal_project_id=request.personal_project_id,
81
+ project_agent_id=request.project_agent_id,
59
82
  workflow_session_authority='mcp_host',
60
83
  )
61
84
 
@@ -80,6 +103,7 @@ class StoreKnowledge:
80
103
  space_id: str,
81
104
  personal_project_id: str | None = None,
82
105
  limit: int = 100,
106
+ project_agent_id: str | None = None,
83
107
  ) -> CollaborationContextResult:
84
108
  return await read_collaboration_context(
85
109
  self,
@@ -87,6 +111,7 @@ class StoreKnowledge:
87
111
  space_id,
88
112
  personal_project_id,
89
113
  limit,
114
+ project_agent_id,
90
115
  )
91
116
 
92
117
  async def _commit_episode(
@@ -95,6 +120,7 @@ class StoreKnowledge:
95
120
  episode: StructuredEpisode,
96
121
  *,
97
122
  personal_project_id: str | None = None,
123
+ project_agent_id: str | None = None,
98
124
  workflow_session_authority: str | None = None,
99
125
  ) -> CommitResult:
100
126
  group_id = space['group_id']
@@ -112,6 +138,7 @@ class StoreKnowledge:
112
138
  space['kind'],
113
139
  personal_project_id,
114
140
  entity.key,
141
+ project_agent_id=project_agent_id,
115
142
  )
116
143
  for entity in episode.entities
117
144
  ]
@@ -129,6 +156,7 @@ class StoreKnowledge:
129
156
  relationship.key,
130
157
  relationship.fact,
131
158
  (relationship.valid_at or episode.reference_time).isoformat(),
159
+ project_agent_id=project_agent_id,
132
160
  workflow_session_authority=workflow_session_authority,
133
161
  )
134
162
  for relationship in episode.relationships
@@ -208,12 +236,16 @@ class StoreKnowledge:
208
236
  'inheritance_mode': entity.inheritance_mode,
209
237
  'inherited_project_ids': entity.inherited_project_ids,
210
238
  'preference_scope': (
211
- 'project' if entity.profile_aspect and personal_project_id
239
+ 'agent' if entity.profile_aspect and project_agent_id
240
+ else 'project' if entity.profile_aspect and personal_project_id
212
241
  else 'global' if entity.profile_aspect else None
213
242
  ),
214
243
  'preference_project_id': (
215
244
  personal_project_id if entity.profile_aspect else None
216
245
  ),
246
+ 'preference_agent_id': (
247
+ project_agent_id if entity.profile_aspect else None
248
+ ),
217
249
  'attributes_json': json.dumps(
218
250
  entity.attributes, ensure_ascii=False, sort_keys=True
219
251
  ),
@@ -262,12 +294,16 @@ class StoreKnowledge:
262
294
  'inheritance_mode': relationship.inheritance_mode,
263
295
  'inherited_project_ids': relationship.inherited_project_ids,
264
296
  'preference_scope': (
265
- 'project' if relationship.profile_aspect and personal_project_id
297
+ 'agent' if relationship.profile_aspect and project_agent_id
298
+ else 'project' if relationship.profile_aspect and personal_project_id
266
299
  else 'global' if relationship.profile_aspect else None
267
300
  ),
268
301
  'preference_project_id': (
269
302
  personal_project_id if relationship.profile_aspect else None
270
303
  ),
304
+ 'preference_agent_id': (
305
+ project_agent_id if relationship.profile_aspect else None
306
+ ),
271
307
  'attributes_json': json.dumps(
272
308
  relationship.attributes, ensure_ascii=False, sort_keys=True
273
309
  ),
@@ -325,6 +361,7 @@ class StoreKnowledge:
325
361
  entity.fuli_inherited_project_ids = row.inherited_project_ids,
326
362
  entity.fuli_preference_scope = row.preference_scope,
327
363
  entity.fuli_preference_project_id = row.preference_project_id,
364
+ entity.fuli_preference_agent_id = row.preference_agent_id,
328
365
  entity.fuli_attributes_json = row.attributes_json
329
366
  WITH count(entity) AS entity_count
330
367
  CREATE (episode:Episodic {
@@ -346,6 +383,7 @@ class StoreKnowledge:
346
383
  fuli_summary: $summary,
347
384
  fuli_sensitivity: $sensitivity,
348
385
  fuli_personal_project_id: $personal_project_id,
386
+ fuli_project_agent_id: $project_agent_id,
349
387
  fuli_idempotency_key: $idempotency_key,
350
388
  fuli_workflow_session_authority:
351
389
  $workflow_session_authority
@@ -388,6 +426,7 @@ class StoreKnowledge:
388
426
  edge.fuli_inherited_project_ids = row.inherited_project_ids,
389
427
  edge.fuli_preference_scope = row.preference_scope,
390
428
  edge.fuli_preference_project_id = row.preference_project_id,
429
+ edge.fuli_preference_agent_id = row.preference_agent_id,
391
430
  edge.fuli_attributes_json = row.attributes_json,
392
431
  edge.fuli_workflow_condition_json =
393
432
  row.workflow_condition_json,
@@ -436,6 +475,17 @@ class StoreKnowledge:
436
475
  }
437
476
  )
438
477
  )
478
+ AND EXISTS {
479
+ MATCH (agent_episode:Episodic {group_id: $group_id})
480
+ WHERE agent_episode.uuid IN coalesce(old.episodes, [])
481
+ AND (
482
+ agent_episode.fuli_project_agent_id = $project_agent_id
483
+ OR (
484
+ agent_episode.fuli_project_agent_id IS NULL
485
+ AND $project_agent_id IS NULL
486
+ )
487
+ )
488
+ }
439
489
  AND any(
440
490
  replacement IN $superseded_relationships
441
491
  WHERE replacement.key = old.fuli_key
@@ -478,6 +528,7 @@ class StoreKnowledge:
478
528
  summary=episode.summary,
479
529
  sensitivity=episode.sensitivity,
480
530
  personal_project_id=personal_project_id,
531
+ project_agent_id=project_agent_id,
481
532
  workflow_session_authority=workflow_session_authority,
482
533
  idempotency_key=episode.idempotency_key,
483
534
  )
@@ -531,15 +582,14 @@ def _entity_id(
531
582
  space_kind: str,
532
583
  personal_project_id: str | None,
533
584
  key: str,
585
+ *,
586
+ project_agent_id: str | None = None,
534
587
  ) -> str:
535
588
  if space_kind == 'personal' and personal_project_id:
536
- return stable_uuid(
537
- group_id,
538
- 'entity',
539
- 'personal-project',
540
- personal_project_id,
541
- key,
542
- )
589
+ parts = [group_id, 'entity', 'personal-project', personal_project_id]
590
+ if project_agent_id:
591
+ parts.extend(['project-agent', project_agent_id])
592
+ return stable_uuid(*parts, key)
543
593
  return stable_uuid(group_id, 'entity', key)
544
594
 
545
595
 
@@ -551,11 +601,14 @@ def _relationship_id(
551
601
  fact: str,
552
602
  reference_time: str,
553
603
  *,
604
+ project_agent_id: str | None = None,
554
605
  workflow_session_authority: str | None = None,
555
606
  ) -> str:
556
607
  parts = [group_id, 'relationship']
557
608
  if space_kind == 'personal' and personal_project_id:
558
609
  parts.extend(['personal-project', personal_project_id])
610
+ if project_agent_id:
611
+ parts.extend(['project-agent', project_agent_id])
559
612
  if workflow_session_authority:
560
613
  parts.extend([
561
614
  'workflow-session-authority',