fuli-context 0.7.4-beta.3 → 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 (128) hide show
  1. package/README.md +77 -6
  2. package/README.zh-CN.md +69 -6
  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-D_2zC4lm.js → console-BlAhujsH.js} +1 -1
  23. package/dist/web/assets/{conversation-launcher-settings-DQmMH2j7.js → conversation-launcher-settings-DTYxiLJt.js} +1 -1
  24. package/dist/web/assets/i18n-Ci3heuiL.js +9 -0
  25. package/dist/web/assets/{identity-DmGcY3Y6.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-CVoOdRsL.js → model-yG0YqUyZ.js} +1 -1
  29. package/dist/web/assets/{useApi-CROJJdhE-CBNg4CdH.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/graph-provider/fuli_graph/app.py +8 -0
  33. package/graph-provider/fuli_graph/collaboration_context.py +69 -6
  34. package/graph-provider/fuli_graph/knowledge_search.py +84 -5
  35. package/graph-provider/fuli_graph/model_validation.py +130 -0
  36. package/graph-provider/fuli_graph/models.py +24 -5
  37. package/graph-provider/fuli_graph/project_agent_access.py +54 -0
  38. package/graph-provider/fuli_graph/project_agent_executor_models.py +635 -0
  39. package/graph-provider/fuli_graph/project_agent_models.py +278 -0
  40. package/graph-provider/fuli_graph/project_agent_routes.py +597 -0
  41. package/graph-provider/fuli_graph/project_agent_task_models.py +542 -0
  42. package/graph-provider/fuli_graph/runtime.py +259 -0
  43. package/graph-provider/fuli_graph/search_projection.py +3 -0
  44. package/graph-provider/fuli_graph/store.py +6 -0
  45. package/graph-provider/fuli_graph/store_knowledge.py +62 -9
  46. package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +756 -0
  47. package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +601 -0
  48. package/graph-provider/fuli_graph/store_project_agent_executors.py +1087 -0
  49. package/graph-provider/fuli_graph/store_project_agent_task_reads.py +458 -0
  50. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +699 -0
  51. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +409 -0
  52. package/graph-provider/fuli_graph/store_project_agent_tasks.py +1144 -0
  53. package/graph-provider/fuli_graph/store_project_agents.py +965 -0
  54. package/graph-provider/pyproject.toml +2 -1
  55. package/npm-shrinkwrap.json +2 -2
  56. package/package.json +1 -1
  57. package/skills/capturing-session-knowledge/SKILL.md +39 -0
  58. package/src/adaptive-runtime/executor-pool.js +127 -0
  59. package/src/adaptive-runtime/graph-services.js +166 -0
  60. package/src/adaptive-runtime/lease-client.js +335 -0
  61. package/src/adaptive-runtime/runtime-broker.js +318 -0
  62. package/src/adaptive-runtime/settings.js +111 -0
  63. package/src/adaptive-runtime/state.js +74 -0
  64. package/src/agent-tools/graph-definitions.js +396 -0
  65. package/src/agent-tools/graph-handlers.js +61 -0
  66. package/src/agent-tools/project-agent-definitions.js +307 -0
  67. package/src/backup/graph-backup.js +289 -0
  68. package/src/backup/identity-reconciliation.js +208 -0
  69. package/src/backup/runtime-adapters.js +269 -0
  70. package/src/cli/command-registry.js +6 -2
  71. package/src/cli/graph-data-command.js +112 -0
  72. package/src/cli/local-runtime-command.js +16 -2
  73. package/src/cli/setup-command.js +19 -3
  74. package/src/cli/update-command.js +4 -0
  75. package/src/cli.js +6 -0
  76. package/src/graphiti/collaboration-preference-projection.js +29 -1
  77. package/src/graphiti/collaboration-preference-workflow.js +121 -0
  78. package/src/graphiti/federated-application.js +53 -90
  79. package/src/graphiti/project-agent-control-plane.js +263 -0
  80. package/src/graphiti/project-agent-mapping.js +1003 -0
  81. package/src/graphiti/project-agent-workflows.js +557 -0
  82. package/src/graphiti/provider-client.js +270 -1
  83. package/src/http/api-router.js +63 -9
  84. package/src/http/graph-api-router.js +278 -0
  85. package/src/local-runtime/lifecycle.js +46 -10
  86. package/src/mcp/context-resources.js +15 -6
  87. package/src/mcp/create-mcp-server.js +51 -16
  88. package/src/mcp/instructions.js +7 -5
  89. package/src/mcp/runtime.js +17 -5
  90. package/src/mcp/session-id.js +38 -1
  91. package/src/mcp/tool-annotations.js +69 -0
  92. package/src/mcp-server.js +23 -6
  93. package/src/native-runtime/runtime.js +717 -0
  94. package/src/server.js +6 -2
  95. package/src/setup/agent-installation-status.js +7 -1
  96. package/src/setup/agents.js +3 -1
  97. package/src/setup/claude-code-config.js +3 -1
  98. package/src/setup/codex-bootstrap.js +11 -10
  99. package/src/setup/codex-config.js +7 -1
  100. package/src/setup/cursor-config.js +3 -1
  101. package/src/setup/graph-runtime.js +158 -13
  102. package/src/setup/neo4j-memory-profile.js +43 -0
  103. package/src/setup/options.js +38 -2
  104. package/src/setup/paths.js +14 -0
  105. package/src/setup/setup.js +37 -1
  106. package/src/system/resource-monitor.js +181 -16
  107. package/src/system/runtime-settings.js +10 -1
  108. package/src/system/system-service.js +27 -2
  109. package/dist/web/assets/AboutPage-Dpf5fRWv.js +0 -1
  110. package/dist/web/assets/ConnectionsPage-D21aUDCP.js +0 -1
  111. package/dist/web/assets/KnowledgeEditDialog-u7lpfN3j.js +0 -1
  112. package/dist/web/assets/KnowledgeInspector-k4xnHtXd.js +0 -1
  113. package/dist/web/assets/KnowledgeOrganizerPage-Bdj7Rn84.js +0 -1
  114. package/dist/web/assets/KnowledgePage-UHVAbC1W.js +0 -1
  115. package/dist/web/assets/KnowledgeWorkspace-Lq3d-MeC.js +0 -4
  116. package/dist/web/assets/OverviewPage-CycAgA1V.js +0 -1
  117. package/dist/web/assets/PersonalProfilePage-U9oIV5FT.js +0 -1
  118. package/dist/web/assets/PersonalProjectsPage-C5RpXKcA.js +0 -1
  119. package/dist/web/assets/PublicProjectsPage-DiWxm35E.js +0 -1
  120. package/dist/web/assets/ReviewPage-jxRfGSJK.js +0 -1
  121. package/dist/web/assets/SearchableSelect-B3-5IPup.js +0 -1
  122. package/dist/web/assets/SettingsPage-BqfGDOho.js +0 -1
  123. package/dist/web/assets/SettingsPage-CAfz4qiV.css +0 -1
  124. package/dist/web/assets/WritingTastePage-BJxhMqJR.js +0 -1
  125. package/dist/web/assets/i18n-CwqXDI1C.js +0 -5
  126. package/dist/web/assets/index-IUGLt4lG.js +0 -2
  127. package/dist/web/assets/index-X2j29j8i.css +0 -1
  128. package/dist/web/assets/vue-router-CWSsUUio.js +0 -1
@@ -11,6 +11,7 @@ from .models import (
11
11
  _validate_confirmation_state,
12
12
  )
13
13
  from .personal_project_access import authorize_personal_project
14
+ from .project_agent_access import authorize_project_agent
14
15
  from .provider_values import json_object, native_datetime as _native_datetime
15
16
 
16
17
 
@@ -20,6 +21,7 @@ async def read_collaboration_context(
20
21
  space_id: str,
21
22
  personal_project_id: str | None = None,
22
23
  limit: int = 100,
24
+ project_agent_id: str | None = None,
23
25
  ) -> CollaborationContextResult:
24
26
  space = await store.authorize(actor, space_id, 'reader')
25
27
  if space['kind'] != 'personal':
@@ -34,6 +36,20 @@ async def read_collaboration_context(
34
36
  space,
35
37
  personal_project_id,
36
38
  )
39
+ if project_agent_id:
40
+ if not personal_project_id:
41
+ raise HTTPException(
42
+ status_code=422,
43
+ detail='project Agent preferences require a personal project',
44
+ )
45
+ await authorize_project_agent(
46
+ store,
47
+ actor,
48
+ space,
49
+ personal_project_id,
50
+ project_agent_id,
51
+ require_memory=True,
52
+ )
37
53
  project_scopes = await _project_scopes(
38
54
  store,
39
55
  space,
@@ -50,6 +66,7 @@ async def read_collaboration_context(
50
66
  _node_query(),
51
67
  group_id=space['group_id'],
52
68
  project_id=personal_project_id,
69
+ project_agent_id=project_agent_id,
53
70
  inherited_project_ids=inherited_project_ids,
54
71
  limit=candidate_limit + 1,
55
72
  routing_='r',
@@ -58,6 +75,7 @@ async def read_collaboration_context(
58
75
  _edge_query(),
59
76
  group_id=space['group_id'],
60
77
  project_id=personal_project_id,
78
+ project_agent_id=project_agent_id,
61
79
  inherited_project_ids=inherited_project_ids,
62
80
  limit=candidate_limit + 1,
63
81
  routing_='r',
@@ -97,17 +115,26 @@ async def read_collaboration_context(
97
115
  project_preferences = [
98
116
  item for item in items if item.preference_scope == 'project'
99
117
  ]
118
+ agent_preferences = [
119
+ item for item in items if item.preference_scope == 'agent'
120
+ ]
100
121
  (
101
122
  authoritative_global,
102
123
  authoritative_project,
124
+ authoritative_agent,
103
125
  cross_scope_lower_authority_ids,
104
- ) = _highest_authority_by_key(global_preferences, project_preferences)
126
+ ) = _highest_authority_by_key(
127
+ global_preferences,
128
+ project_preferences,
129
+ agent_preferences,
130
+ )
105
131
  (
106
132
  project_effective,
107
133
  project_conflicts,
108
134
  overridden_inherited_ids,
109
135
  overridden_lower_authority_ids,
110
136
  ) = _scope_conflict_free(authoritative_project)
137
+ agent_effective, agent_conflicts = _conflict_free(authoritative_agent)
111
138
  overridden_lower_authority_ids = sorted({
112
139
  *cross_scope_lower_authority_ids,
113
140
  *overridden_lower_authority_ids,
@@ -115,27 +142,45 @@ async def read_collaboration_context(
115
142
  # Any authoritative project-layer claim blocks global fallback for that
116
143
  # semantic key. A same-layer project conflict must remain deferred instead
117
144
  # of silently reviving a global value as effective.
145
+ agent_keys = {item.preference_key for item in authoritative_agent}
118
146
  project_keys = {item.preference_key for item in authoritative_project}
147
+ overridden_project_ids = sorted([
148
+ item.id
149
+ for item in authoritative_project
150
+ if item.preference_key in agent_keys
151
+ ])
119
152
  overridden_global_ids = sorted([
120
153
  item.id
121
154
  for item in authoritative_global
122
- if item.preference_key in project_keys
155
+ if item.preference_key in project_keys or item.preference_key in agent_keys
123
156
  ])
124
157
  global_effective, global_conflicts = _conflict_free([
125
158
  item for item in authoritative_global
126
159
  if item.preference_key not in project_keys
160
+ and item.preference_key not in agent_keys
127
161
  ])
128
- effective_preferences = [*global_effective, *project_effective]
162
+ project_effective = [
163
+ item for item in project_effective
164
+ if item.preference_key not in agent_keys
165
+ ]
166
+ effective_preferences = [
167
+ *global_effective,
168
+ *project_effective,
169
+ *agent_effective,
170
+ ]
129
171
 
130
172
  return CollaborationContextResult(
131
173
  personal_space_id=space_id,
132
174
  personal_project_id=personal_project_id,
175
+ project_agent_id=project_agent_id,
133
176
  global_preferences=global_preferences,
134
177
  project_preferences=project_preferences,
178
+ agent_preferences=agent_preferences,
135
179
  effective_preferences=effective_preferences,
136
- conflicts=[*global_conflicts, *project_conflicts],
180
+ conflicts=[*global_conflicts, *project_conflicts, *agent_conflicts],
137
181
  overridden_global_ids=overridden_global_ids,
138
182
  overridden_inherited_ids=overridden_inherited_ids,
183
+ overridden_project_ids=overridden_project_ids,
139
184
  overridden_lower_authority_ids=overridden_lower_authority_ids,
140
185
  truncated=truncated,
141
186
  )
@@ -220,6 +265,11 @@ def _node_query() -> str:
220
265
  )
221
266
  )
222
267
  )
268
+ OR (
269
+ node.fuli_preference_scope = 'agent'
270
+ AND node.fuli_preference_project_id = $project_id
271
+ AND node.fuli_preference_agent_id = $project_agent_id
272
+ )
223
273
  )
224
274
  RETURN node.uuid AS id,
225
275
  coalesce(node.fuli_key, node.uuid) AS key,
@@ -228,6 +278,7 @@ def _node_query() -> str:
228
278
  node.fuli_profile_aspect AS profile_aspect,
229
279
  coalesce(node.fuli_preference_scope, 'global') AS preference_scope,
230
280
  node.fuli_preference_project_id AS preference_project_id,
281
+ node.fuli_preference_agent_id AS preference_agent_id,
231
282
  node.fuli_attributes_json AS attributes_json,
232
283
  node.fuli_confirmation_basis_json AS confirmation_basis_json,
233
284
  node.fuli_reasoning_summary AS reasoning_summary,
@@ -265,6 +316,11 @@ def _edge_query() -> str:
265
316
  )
266
317
  )
267
318
  )
319
+ OR (
320
+ edge.fuli_preference_scope = 'agent'
321
+ AND edge.fuli_preference_project_id = $project_id
322
+ AND edge.fuli_preference_agent_id = $project_agent_id
323
+ )
268
324
  )
269
325
  RETURN edge.uuid AS id,
270
326
  coalesce(edge.fuli_key, edge.uuid) AS key,
@@ -276,6 +332,7 @@ def _edge_query() -> str:
276
332
  edge.fuli_profile_aspect AS profile_aspect,
277
333
  coalesce(edge.fuli_preference_scope, 'global') AS preference_scope,
278
334
  edge.fuli_preference_project_id AS preference_project_id,
335
+ edge.fuli_preference_agent_id AS preference_agent_id,
279
336
  edge.fuli_attributes_json AS attributes_json,
280
337
  edge.fuli_confirmation_basis_json AS confirmation_basis_json,
281
338
  edge.fuli_reasoning_summary AS reasoning_summary,
@@ -337,6 +394,7 @@ def _preference_item(
337
394
  or key
338
395
  )
339
396
  project_id = record.get('preference_project_id')
397
+ agent_id = record.get('preference_agent_id')
340
398
  scope = project_scopes.get(project_id, {
341
399
  'scope_distance': 0,
342
400
  'scope_path': [],
@@ -375,6 +433,7 @@ def _preference_item(
375
433
  profile_aspect=record['profile_aspect'],
376
434
  preference_scope=record['preference_scope'],
377
435
  preference_project_id=project_id,
436
+ preference_agent_id=agent_id,
378
437
  attributes=attributes,
379
438
  weight=weight,
380
439
  reason=reason,
@@ -425,6 +484,7 @@ def _conflict_free(
425
484
  preference_key=preference_key,
426
485
  preference_scope=first.preference_scope,
427
486
  preference_project_id=first.preference_project_id,
487
+ preference_agent_id=first.preference_agent_id,
428
488
  item_ids=[item.id for item in candidates],
429
489
  ))
430
490
  effective.sort(key=_sort_key)
@@ -481,13 +541,15 @@ def _scope_conflict_free(
481
541
  def _highest_authority_by_key(
482
542
  global_items: list[CollaborationPreferenceItem],
483
543
  project_items: list[CollaborationPreferenceItem],
544
+ agent_items: list[CollaborationPreferenceItem],
484
545
  ) -> tuple[
546
+ list[CollaborationPreferenceItem],
485
547
  list[CollaborationPreferenceItem],
486
548
  list[CollaborationPreferenceItem],
487
549
  list[str],
488
550
  ]:
489
551
  grouped: dict[str, list[CollaborationPreferenceItem]] = defaultdict(list)
490
- for item in [*global_items, *project_items]:
552
+ for item in [*global_items, *project_items, *agent_items]:
491
553
  grouped[item.preference_key].append(item)
492
554
  selected_ids = set()
493
555
  lower_authority_ids = []
@@ -505,6 +567,7 @@ def _highest_authority_by_key(
505
567
  return (
506
568
  [item for item in global_items if item.id in selected_ids],
507
569
  [item for item in project_items if item.id in selected_ids],
570
+ [item for item in agent_items if item.id in selected_ids],
508
571
  sorted(lower_authority_ids),
509
572
  )
510
573
 
@@ -512,7 +575,7 @@ def _highest_authority_by_key(
512
575
  def _sort_key(item: CollaborationPreferenceItem) -> tuple:
513
576
  created = -item.created_at.timestamp() if item.created_at else 0
514
577
  return (
515
- 0 if item.preference_scope == 'global' else 1,
578
+ {'global': 0, 'project': 1, 'agent': 2}[item.preference_scope],
516
579
  0 if item.confirmation_status == 'confirmed' else 1,
517
580
  item.profile_aspect,
518
581
  item.preference_key,
@@ -6,6 +6,7 @@ from graphiti_core.edges import EntityEdge
6
6
 
7
7
  from .models import ConfirmationBasis, EntitySearchResult, SearchRequest, SearchResult
8
8
  from .personal_project_access import authorize_personal_project
9
+ from .project_agent_access import authorize_project_agent
9
10
  from .provider_values import (
10
11
  json_object,
11
12
  native_datetime as _native_datetime,
@@ -103,6 +104,7 @@ async def search_knowledge(store, actor: dict, request: SearchRequest) -> Search
103
104
  ranked_edges,
104
105
  epistemic,
105
106
  request.active_personal_project_id,
107
+ request.project_agent_id,
106
108
  )
107
109
  ranked_edges = ranked_edges[: request.limit]
108
110
  entities = (
@@ -193,10 +195,14 @@ async def personal_edge_scopes(
193
195
  coalesce(edge.fuli_preference_scope, 'global')
194
196
  AS preference_scope,
195
197
  edge.fuli_preference_project_id AS preference_project_id,
198
+ edge.fuli_preference_agent_id AS preference_agent_id,
196
199
  assignment.project_id AS assignment_project_id,
197
200
  [episode IN episodes
198
201
  WHERE episode.fuli_personal_project_id IS NOT NULL |
199
202
  episode.fuli_personal_project_id] AS episode_project_ids,
203
+ [episode IN episodes
204
+ WHERE episode.fuli_project_agent_id IS NOT NULL |
205
+ episode.fuli_project_agent_id] AS episode_project_agent_ids,
200
206
  any(episode IN episodes
201
207
  WHERE episode.fuli_personal_project_id IS NULL)
202
208
  AS has_global_episode,
@@ -310,6 +316,16 @@ async def _personal_search_space(store, actor, spaces, request):
310
316
  space = personal_spaces[0]
311
317
  for project_id in request.personal_project_ids:
312
318
  await authorize_personal_project(store, actor, space, project_id)
319
+ if request.project_agent_id:
320
+ await authorize_project_agent(
321
+ store,
322
+ actor,
323
+ space,
324
+ request.active_personal_project_id,
325
+ request.project_agent_id,
326
+ require_active=True,
327
+ require_memory=True,
328
+ )
313
329
  return space
314
330
 
315
331
 
@@ -367,11 +383,15 @@ async def _personal_entities(
367
383
  ELSE coalesce(node.fuli_preference_scope, 'global') END
368
384
  AS preference_scope,
369
385
  node.fuli_preference_project_id AS preference_project_id,
386
+ node.fuli_preference_agent_id AS preference_agent_id,
370
387
  coalesce(node.fuli_key, node.uuid) AS key,
371
388
  assignment.project_id AS assignment_project_id,
372
389
  [episode IN episodes
373
390
  WHERE episode.fuli_personal_project_id IS NOT NULL |
374
391
  episode.fuli_personal_project_id] AS episode_project_ids,
392
+ [episode IN episodes
393
+ WHERE episode.fuli_project_agent_id IS NOT NULL |
394
+ episode.fuli_project_agent_id] AS episode_project_agent_ids,
375
395
  any(episode IN episodes
376
396
  WHERE episode.fuli_personal_project_id IS NULL)
377
397
  AS has_global_episode,
@@ -461,6 +481,7 @@ async def _personal_entities(
461
481
  ranked = _dedupe_ranked_entities(
462
482
  ranked,
463
483
  request.active_personal_project_id,
484
+ request.project_agent_id,
464
485
  )[: request.limit]
465
486
  return [
466
487
  _entity_search_result(record, score, space['id'])
@@ -478,8 +499,10 @@ def _item_scope_metadata(
478
499
  field in record
479
500
  for field in (
480
501
  'profile_aspect',
502
+ 'preference_agent_id',
481
503
  'assignment_project_id',
482
504
  'episode_project_ids',
505
+ 'episode_project_agent_ids',
483
506
  'reference_project_ids',
484
507
  'has_global_episode',
485
508
  )
@@ -497,10 +520,38 @@ def _item_scope_metadata(
497
520
  project_id = record.get('preference_project_id')
498
521
  if project_id not in request.personal_project_ids:
499
522
  return None
523
+ if preference_scope == 'agent':
524
+ agent_id = record.get('preference_agent_id')
525
+ if (
526
+ not request.project_agent_id
527
+ or agent_id != request.project_agent_id
528
+ or project_id != request.active_personal_project_id
529
+ ):
530
+ return None
531
+ return _agent_scope_metadata(
532
+ project_id,
533
+ project_scopes[project_id],
534
+ agent_id,
535
+ )
500
536
  return _project_scope_metadata(project_id, project_scopes[project_id])
501
537
 
502
538
  assignment_project_id = record.get('assignment_project_id')
503
539
  episode_project_ids = list(record.get('episode_project_ids') or [])
540
+ episode_project_agent_ids = list(
541
+ record.get('episode_project_agent_ids') or []
542
+ )
543
+ if episode_project_agent_ids:
544
+ if (
545
+ not request.project_agent_id
546
+ or request.project_agent_id not in episode_project_agent_ids
547
+ or request.active_personal_project_id not in episode_project_ids
548
+ ):
549
+ return None
550
+ return _agent_scope_metadata(
551
+ request.active_personal_project_id,
552
+ project_scopes[request.active_personal_project_id],
553
+ request.project_agent_id,
554
+ )
504
555
  reference_project_ids = list(record.get('reference_project_ids') or [])
505
556
  project_ids = (
506
557
  [assignment_project_id]
@@ -566,6 +617,17 @@ def _project_scope_metadata(project_id: str, scope: dict) -> dict:
566
617
  }
567
618
 
568
619
 
620
+ def _agent_scope_metadata(project_id: str, scope: dict, agent_id: str) -> dict:
621
+ return {
622
+ **_project_scope_metadata(project_id, scope),
623
+ 'project_agent_id': agent_id,
624
+ 'inherited_from_project_id': None,
625
+ 'scope_distance': 0,
626
+ 'scope_path': [project_id],
627
+ 'inherited': False,
628
+ }
629
+
630
+
569
631
  def _ranked_relevance(base_score: float, metadata) -> float:
570
632
  if base_score <= 0:
571
633
  return 0.0
@@ -595,21 +657,34 @@ def _ranked_relevance(base_score: float, metadata) -> float:
595
657
  )
596
658
 
597
659
 
598
- def _scope_precedence(metadata, active_project_id: str | None) -> tuple:
660
+ def _scope_precedence(
661
+ metadata,
662
+ active_project_id: str | None,
663
+ project_agent_id: str | None = None,
664
+ ) -> tuple:
599
665
  project_id = metadata.get('defined_project_id')
600
666
  return (
667
+ 0 if project_agent_id and metadata.get('project_agent_id') == project_agent_id else 1,
601
668
  0 if active_project_id and project_id == active_project_id else 1,
602
669
  1 if metadata.get('inherited_from_project_id') else 0,
603
670
  int(metadata.get('scope_distance') or 0),
604
671
  )
605
672
 
606
673
 
607
- def _dedupe_ranked_edges(ranked, metadata_by_id, active_project_id):
674
+ def _dedupe_ranked_edges(
675
+ ranked,
676
+ metadata_by_id,
677
+ active_project_id,
678
+ project_agent_id=None,
679
+ ):
608
680
  selected = {}
609
681
  for score, edge in ranked:
610
682
  metadata = metadata_by_id.get(edge.uuid, {})
611
683
  key = metadata.get('key') or edge.uuid
612
- quality = (_scope_precedence(metadata, active_project_id), -score)
684
+ quality = (
685
+ _scope_precedence(metadata, active_project_id, project_agent_id),
686
+ -score,
687
+ )
613
688
  current = selected.get(key)
614
689
  if current is None or quality < current[0]:
615
690
  selected[key] = (quality, score, edge)
@@ -620,11 +695,14 @@ def _dedupe_ranked_edges(ranked, metadata_by_id, active_project_id):
620
695
  )
621
696
 
622
697
 
623
- def _dedupe_ranked_entities(ranked, active_project_id):
698
+ def _dedupe_ranked_entities(ranked, active_project_id, project_agent_id=None):
624
699
  selected = {}
625
700
  for record, score in ranked:
626
701
  key = record.get('key') or record.get('id')
627
- quality = (_scope_precedence(record, active_project_id), -score)
702
+ quality = (
703
+ _scope_precedence(record, active_project_id, project_agent_id),
704
+ -score,
705
+ )
628
706
  current = selected.get(key)
629
707
  if current is None or quality < current[0]:
630
708
  selected[key] = (quality, record, score)
@@ -665,6 +743,7 @@ def _entity_search_result(record, score: float, space_id: str) -> EntitySearchRe
665
743
  'attributes_json',
666
744
  'assignment_project_id',
667
745
  'episode_project_ids',
746
+ 'episode_project_agent_ids',
668
747
  'has_global_episode',
669
748
  'reference_project_ids',
670
749
  'inherited',
@@ -19,6 +19,136 @@ _CREDENTIAL_PATTERNS = (
19
19
  ),
20
20
  )
21
21
 
22
+ # Keep this deliberately small and provider-local instead of depending on a
23
+ # third-party emoji database. The supplementary-plane range covers current
24
+ # pictographs and the explicit BMP ranges cover the common text-presentation
25
+ # symbols that become emoji with a variation selector (for example, ``❤️``).
26
+ _EMOJI_BASE_RANGES = (
27
+ (0x1F000, 0x1FAFF),
28
+ (0x2600, 0x27BF),
29
+ )
30
+ _EMOJI_BASE_CODEPOINTS = frozenset({
31
+ 0x00A9, 0x00AE,
32
+ 0x203C, 0x2049,
33
+ 0x2122, 0x2139,
34
+ 0x2194, 0x2195, 0x2196, 0x2197, 0x2198, 0x2199,
35
+ 0x21A9, 0x21AA,
36
+ 0x231A, 0x231B, 0x2328, 0x23CF,
37
+ 0x23E9, 0x23EA, 0x23EB, 0x23EC, 0x23ED, 0x23EE,
38
+ 0x23EF, 0x23F0, 0x23F1, 0x23F2, 0x23F3,
39
+ 0x23F8, 0x23F9, 0x23FA,
40
+ 0x24C2,
41
+ 0x25AA, 0x25AB, 0x25B6, 0x25C0,
42
+ 0x25FB, 0x25FC, 0x25FD, 0x25FE,
43
+ 0x2934, 0x2935,
44
+ 0x2B05, 0x2B06, 0x2B07, 0x2B1B, 0x2B1C, 0x2B50, 0x2B55,
45
+ 0x3030, 0x303D, 0x3297, 0x3299,
46
+ })
47
+ _EMOJI_VARIATION_SELECTORS = frozenset({0xFE0E, 0xFE0F})
48
+ _EMOJI_SKIN_TONES = range(0x1F3FB, 0x1F400)
49
+ _EMOJI_TAG_RANGE = range(0xE0020, 0xE0080)
50
+ _EMOJI_KEYCAP_MARK = 0x20E3
51
+ _EMOJI_ZWJ = 0x200D
52
+
53
+
54
+ def _is_emoji_base(character: str) -> bool:
55
+ codepoint = ord(character)
56
+ return (
57
+ codepoint not in _EMOJI_SKIN_TONES
58
+ and any(
59
+ start <= codepoint <= end
60
+ for start, end in _EMOJI_BASE_RANGES
61
+ )
62
+ ) or codepoint in _EMOJI_BASE_CODEPOINTS
63
+
64
+
65
+ def _is_regional_indicator(character: str) -> bool:
66
+ return 0x1F1E6 <= ord(character) <= 0x1F1FF
67
+
68
+
69
+ def _is_keycap_base(character: str) -> bool:
70
+ return character in '#*' or '0' <= character <= '9'
71
+
72
+
73
+ def _valid_emoji_sequence(value: str) -> bool:
74
+ """Accept one emoji grapheme, including common composed sequences."""
75
+
76
+ index = 0
77
+ saw_base = False
78
+ expect_base = True
79
+ while index < len(value):
80
+ character = value[index]
81
+ codepoint = ord(character)
82
+ if expect_base:
83
+ if _is_keycap_base(character):
84
+ index += 1
85
+ if (
86
+ index < len(value)
87
+ and ord(value[index]) in _EMOJI_VARIATION_SELECTORS
88
+ ):
89
+ index += 1
90
+ if index >= len(value) or ord(value[index]) != _EMOJI_KEYCAP_MARK:
91
+ return False
92
+ index += 1
93
+ saw_base = True
94
+ expect_base = False
95
+ continue
96
+ if not _is_emoji_base(character):
97
+ return False
98
+ index += 1
99
+ saw_base = True
100
+ # A pair of regional indicators is one flag grapheme.
101
+ if (
102
+ _is_regional_indicator(character)
103
+ and index < len(value)
104
+ and _is_regional_indicator(value[index])
105
+ ):
106
+ index += 1
107
+ expect_base = False
108
+ continue
109
+
110
+ if codepoint in _EMOJI_SKIN_TONES:
111
+ index += 1
112
+ continue
113
+ if codepoint in _EMOJI_VARIATION_SELECTORS:
114
+ index += 1
115
+ continue
116
+ if codepoint in _EMOJI_TAG_RANGE:
117
+ index += 1
118
+ continue
119
+ if codepoint == _EMOJI_ZWJ:
120
+ index += 1
121
+ if index >= len(value):
122
+ return False
123
+ expect_base = True
124
+ continue
125
+ # A second standalone base would be two graphemes. Occupation
126
+ # markers intentionally accept one grapheme only.
127
+ return False
128
+ return saw_base and not expect_base
129
+
130
+
131
+ def validate_emoji_sequence(
132
+ value: str | None,
133
+ label: str = 'emoji',
134
+ ) -> str | None:
135
+ """Normalize and validate a conservative emoji-only field value."""
136
+
137
+ if value is None:
138
+ return None
139
+ if not isinstance(value, str):
140
+ raise ValueError(f'{label} must be an emoji')
141
+ normalized = value.strip()
142
+ if not normalized:
143
+ raise ValueError(f'{label} must be an emoji')
144
+ if len(normalized) > 32:
145
+ raise ValueError(f'{label} must contain at most 32 characters')
146
+ if any(character.isspace() for character in normalized):
147
+ raise ValueError(f'{label} must not contain whitespace')
148
+ if not _valid_emoji_sequence(normalized):
149
+ raise ValueError(f'{label} must contain one valid emoji sequence')
150
+ return normalized
151
+
22
152
 
23
153
  def reject_credentials(model: Any, label: str) -> None:
24
154
  searchable = json.dumps(model.model_dump(mode='json'), ensure_ascii=False)
@@ -104,6 +104,7 @@ ConfirmationStatus = Literal['confirmed', 'agent_confirmed', 'pending']
104
104
  ConfirmationActorKind = Literal['user', 'agent', 'authoritative_source', 'import']
105
105
  PersonalProfileAspect = Literal['taste', 'personality', 'judgment_preference']
106
106
  PreferenceScope = Literal['global', 'project']
107
+ ScopedPreferenceScope = Literal['global', 'project', 'agent']
107
108
  PreferenceConflictQueueStatus = Literal['ai_pending', 'resolved']
108
109
  PreferenceConflictResolutionAction = Literal[
109
110
  'merge',
@@ -671,12 +672,18 @@ class PublicationDraftDecision(StrictModel):
671
672
  if self.decision != 'submit_public' and self.shared_proposal_id:
672
673
  raise ValueError('shared_proposal_id is only valid for public submission')
673
674
  return self
674
-
675
675
  class KnowledgeCommit(StrictModel):
676
676
  space_id: str = Field(min_length=1, max_length=128)
677
677
  personal_project_id: str | None = Field(default=None, min_length=1, max_length=128)
678
+ project_agent_id: str | None = Field(default=None, min_length=1, max_length=128)
678
679
  episode: StructuredEpisode
679
680
 
681
+ @model_validator(mode='after')
682
+ def validate_project_agent_scope(self):
683
+ if self.project_agent_id and not self.personal_project_id:
684
+ raise ValueError('project Agent knowledge requires personal_project_id')
685
+ return self
686
+
680
687
 
681
688
  class CommitResult(StrictModel):
682
689
  status: Literal['committed', 'duplicate']
@@ -727,6 +734,7 @@ class SearchRequest(StrictModel):
727
734
  min_length=1,
728
735
  max_length=128,
729
736
  )
737
+ project_agent_id: str | None = Field(default=None, min_length=1, max_length=128)
730
738
  inherit_project_knowledge: bool = True
731
739
  include_personal_global: bool = False
732
740
 
@@ -749,6 +757,8 @@ class SearchRequest(StrictModel):
749
757
  raise ValueError(
750
758
  'active personal project id must be present in personal project ids'
751
759
  )
760
+ if self.project_agent_id and not self.active_personal_project_id:
761
+ raise ValueError('project Agent search requires active_personal_project_id')
752
762
  return self
753
763
 
754
764
 
@@ -767,12 +777,14 @@ class EntitySearchResult(StrictModel):
767
777
  confirmation_basis: ConfirmationBasis | None = None
768
778
  reasoning_summary: str | None = None
769
779
  profile_aspect: PersonalProfileAspect | None = None
770
- preference_scope: PreferenceScope | None = None
780
+ preference_scope: ScopedPreferenceScope | None = None
771
781
  preference_project_id: str | None = None
782
+ preference_agent_id: str | None = None
772
783
  key: str | None = None
773
784
  preference_key: str | None = None
774
785
  preference_qualifiers: dict[str, Any] = Field(default_factory=dict)
775
786
  defined_project_id: str | None = None
787
+ project_agent_id: str | None = None
776
788
  inheritance_mode: KnowledgeInheritanceMode = 'local_only'
777
789
  inherited_project_ids: list[str] = Field(default_factory=list, max_length=32)
778
790
  human_edited: bool = False
@@ -816,12 +828,14 @@ class FactResult(StrictModel):
816
828
  confirmation_basis: ConfirmationBasis | None = None
817
829
  reasoning_summary: str | None = None
818
830
  profile_aspect: PersonalProfileAspect | None = None
819
- preference_scope: PreferenceScope | None = None
831
+ preference_scope: ScopedPreferenceScope | None = None
820
832
  preference_project_id: str | None = None
833
+ preference_agent_id: str | None = None
821
834
  key: str | None = None
822
835
  preference_key: str | None = None
823
836
  preference_qualifiers: dict[str, Any] = Field(default_factory=dict)
824
837
  defined_project_id: str | None = None
838
+ project_agent_id: str | None = None
825
839
  inheritance_mode: KnowledgeInheritanceMode = 'local_only'
826
840
  inherited_project_ids: list[str] = Field(default_factory=list, max_length=32)
827
841
  human_edited: bool = False
@@ -859,8 +873,9 @@ class CollaborationPreferenceItem(StrictModel):
859
873
  title: str
860
874
  instruction: str
861
875
  profile_aspect: PersonalProfileAspect
862
- preference_scope: PreferenceScope
876
+ preference_scope: ScopedPreferenceScope
863
877
  preference_project_id: str | None = None
878
+ preference_agent_id: str | None = None
864
879
  attributes: dict[str, Any] = Field(default_factory=dict)
865
880
  weight: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
866
881
  reason: str | None = None
@@ -878,20 +893,24 @@ class CollaborationPreferenceItem(StrictModel):
878
893
 
879
894
  class CollaborationPreferenceConflict(StrictModel):
880
895
  preference_key: str
881
- preference_scope: PreferenceScope
896
+ preference_scope: ScopedPreferenceScope
882
897
  preference_project_id: str | None = None
898
+ preference_agent_id: str | None = None
883
899
  item_ids: list[str] = Field(min_length=2)
884
900
 
885
901
 
886
902
  class CollaborationContextResult(StrictModel):
887
903
  personal_space_id: str
888
904
  personal_project_id: str | None = None
905
+ project_agent_id: str | None = None
889
906
  global_preferences: list[CollaborationPreferenceItem] = Field(default_factory=list)
890
907
  project_preferences: list[CollaborationPreferenceItem] = Field(default_factory=list)
908
+ agent_preferences: list[CollaborationPreferenceItem] = Field(default_factory=list)
891
909
  effective_preferences: list[CollaborationPreferenceItem] = Field(default_factory=list)
892
910
  conflicts: list[CollaborationPreferenceConflict] = Field(default_factory=list)
893
911
  overridden_global_ids: list[str] = Field(default_factory=list)
894
912
  overridden_inherited_ids: list[str] = Field(default_factory=list)
913
+ overridden_project_ids: list[str] = Field(default_factory=list)
895
914
  overridden_lower_authority_ids: list[str] = Field(default_factory=list)
896
915
  truncated: bool = False
897
916