fuli-context 0.7.5 → 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.
Files changed (148) hide show
  1. package/README.md +69 -13
  2. package/README.zh-CN.md +60 -14
  3. package/dist/web/assets/{AboutPage-ByCUor-j.js → AboutPage-BcZeFcHe.js} +1 -1
  4. package/dist/web/assets/{ConnectionsPage-DkdkZ40Q.js → ConnectionsPage-xAQYl1SQ.js} +1 -1
  5. package/dist/web/assets/EmployeeRecruitDialog-CbPAmhy7.js +1 -0
  6. package/dist/web/assets/EmployeeRecruitDialog-CfFlyKSh.css +1 -0
  7. package/dist/web/assets/EmployeeWorkbenchPage-BQvTLoc8.css +1 -0
  8. package/dist/web/assets/EmployeeWorkbenchPage-DF3EL0Ui.js +1 -0
  9. package/dist/web/assets/KnowledgeEditDialog-UnnBDt0I.js +1 -0
  10. package/dist/web/assets/{KnowledgeEditDialog-DWUV_NPp.css → KnowledgeEditDialog-iv-dgUeD.css} +1 -1
  11. package/dist/web/assets/{KnowledgeInspector-CYAjAb9J.js → KnowledgeInspector-BqPJH5Rr.js} +1 -1
  12. package/dist/web/assets/{KnowledgeOrganizerPage-CV4F54R_.js → KnowledgeOrganizerPage-CyvRflXM.js} +1 -1
  13. package/dist/web/assets/KnowledgePage-59eczTqe.js +1 -0
  14. package/dist/web/assets/{KnowledgeWorkspace-z4u5mXOG.js → KnowledgeWorkspace-CfIjhOog.js} +1 -1
  15. package/dist/web/assets/{OverviewPage-BWOfeS6k.js → OverviewPage-Xyfm5xkZ.js} +1 -1
  16. package/dist/web/assets/{PersonalProfilePage-c3w4-h7a.js → PersonalProfilePage-DB9mByB3.js} +1 -1
  17. package/dist/web/assets/PersonalProjectsPage-_-YXVi5O.js +1 -0
  18. package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +1 -0
  19. package/dist/web/assets/ProjectAgentsPage-DeeQByPX.js +11 -0
  20. package/dist/web/assets/{PublicProjectsPage-DdH0oGB3.js → PublicProjectsPage-BEZ1YPs3.js} +1 -1
  21. package/dist/web/assets/{ReviewPage-BYIpcIHG.js → ReviewPage-CQYGwFBH.js} +1 -1
  22. package/dist/web/assets/SearchableSelect-D27H0MoP.js +1 -0
  23. package/dist/web/assets/{SettingsPage-2VkbXgZi.js → SettingsPage-BlEZ03Yi.js} +1 -1
  24. package/dist/web/assets/{WritingTastePage-BOPQZuva.js → WritingTastePage-DvRt_nmP.js} +1 -1
  25. package/dist/web/assets/catalog-DFvIaHVS.js +1 -0
  26. package/dist/web/assets/{console-BlAhujsH.js → console-tOxOl8Vq.js} +1 -1
  27. package/dist/web/assets/conversation-launcher-settings-DuFSpPAo.js +1 -0
  28. package/dist/web/assets/{i18n-Ci3heuiL.js → i18n-CgBHZthI.js} +3 -3
  29. package/dist/web/assets/{identity-4_-v6cB7.js → identity-CFo_Uw1H.js} +1 -1
  30. package/dist/web/assets/index-CJc5ppXH.css +1 -0
  31. package/dist/web/assets/index-yCi3cz2l.js +2 -0
  32. package/dist/web/assets/{model-yG0YqUyZ.js → model-BtnVr7fX.js} +1 -1
  33. package/dist/web/assets/{useApi-CROJJdhE-twhdZArA.js → useApi-CROJJdhE-B6MHm_Av.js} +1 -1
  34. package/dist/web/assets/{vue-router-C-wot_Di.js → vue-router-CzHm-y-x.js} +1 -1
  35. package/dist/web/index.html +7 -6
  36. package/docs/employee-agents.md +99 -0
  37. package/docs/project-agent-memory.md +479 -0
  38. package/graph-provider/fuli_graph/knowledge_audit_models.py +171 -0
  39. package/graph-provider/fuli_graph/model_base.py +5 -0
  40. package/graph-provider/fuli_graph/model_validation.py +32 -0
  41. package/graph-provider/fuli_graph/models.py +26 -170
  42. package/graph-provider/fuli_graph/project_agent_context_models.py +29 -0
  43. package/graph-provider/fuli_graph/project_agent_coordination_models.py +22 -0
  44. package/graph-provider/fuli_graph/project_agent_executor_models.py +35 -7
  45. package/graph-provider/fuli_graph/project_agent_memory_models.py +79 -0
  46. package/graph-provider/fuli_graph/project_agent_memory_routes.py +46 -0
  47. package/graph-provider/fuli_graph/project_agent_models.py +9 -0
  48. package/graph-provider/fuli_graph/project_agent_recruitment_values.py +48 -0
  49. package/graph-provider/fuli_graph/project_agent_routes.py +50 -1
  50. package/graph-provider/fuli_graph/project_agent_task_models.py +220 -29
  51. package/graph-provider/fuli_graph/runtime.py +37 -1
  52. package/graph-provider/fuli_graph/store.py +6 -0
  53. package/graph-provider/fuli_graph/store_project_agent_context.py +65 -0
  54. package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +122 -0
  55. package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +175 -11
  56. package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +33 -10
  57. package/graph-provider/fuli_graph/store_project_agent_executors.py +181 -14
  58. package/graph-provider/fuli_graph/store_project_agent_memory.py +174 -0
  59. package/graph-provider/fuli_graph/store_project_agent_task_activity.py +423 -0
  60. package/graph-provider/fuli_graph/store_project_agent_task_persistence.py +152 -0
  61. package/graph-provider/fuli_graph/store_project_agent_task_reads.py +187 -21
  62. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +756 -164
  63. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +396 -7
  64. package/graph-provider/fuli_graph/store_project_agent_tasks.py +265 -465
  65. package/graph-provider/fuli_graph/store_project_agents.py +214 -46
  66. package/graph-provider/fuli_graph/store_task_contexts.py +217 -0
  67. package/graph-provider/fuli_graph/store_transactions.py +40 -0
  68. package/graph-provider/fuli_graph/task_context_models.py +50 -0
  69. package/graph-provider/fuli_graph/task_context_routes.py +31 -0
  70. package/graph-provider/fuli_graph/workflow_observation_models.py +1 -1
  71. package/graph-provider/pyproject.toml +1 -1
  72. package/npm-shrinkwrap.json +8 -8
  73. package/package.json +5 -2
  74. package/skills/capturing-session-knowledge/SKILL.md +18 -6
  75. package/src/adaptive-runtime/executor-pool.js +46 -5
  76. package/src/adaptive-runtime/graph-services.js +10 -3
  77. package/src/adaptive-runtime/lease-client.js +66 -14
  78. package/src/adaptive-runtime/runtime-broker.js +24 -9
  79. package/src/agent-tools/employee-definitions.js +43 -0
  80. package/src/agent-tools/graph-definitions.js +48 -6
  81. package/src/agent-tools/graph-handlers.js +15 -0
  82. package/src/agent-tools/project-agent-definitions.js +35 -4
  83. package/src/agent-tools/project-agent-memory-definitions.js +36 -0
  84. package/src/agent-tools.js +6 -2
  85. package/src/agents/codex/lifecycle-hook.js +107 -0
  86. package/src/agents/codex/lifecycle-hooks.js +231 -0
  87. package/src/agents/cursor/lifecycle-hook.js +133 -0
  88. package/src/agents/cursor/lifecycle-hooks.js +50 -0
  89. package/src/agents/lifecycle-message.js +19 -0
  90. package/src/app/agent-request-context.js +23 -0
  91. package/src/cli/command-registry.js +2 -0
  92. package/src/cli/employee-command.js +17 -0
  93. package/src/cli/remote-mcp-command.js +282 -0
  94. package/src/cli/setup-command.js +5 -0
  95. package/src/cli.js +10 -0
  96. package/src/employees/catalog/jefa.json +21 -0
  97. package/src/employees/install-package.js +77 -0
  98. package/src/employees/management-policy.js +88 -0
  99. package/src/employees/manifest.js +53 -0
  100. package/src/employees/package-registry.js +107 -0
  101. package/src/employees/project-selection.js +50 -0
  102. package/src/employees/recruitment.js +199 -0
  103. package/src/employees/service.js +83 -0
  104. package/src/employees/task-entry.js +39 -0
  105. package/src/graphiti/agent-knowledge-workflows.js +135 -25
  106. package/src/graphiti/federated-application.js +78 -35
  107. package/src/graphiti/project-agent-control-plane.js +32 -3
  108. package/src/graphiti/project-agent-mapping.js +175 -4
  109. package/src/graphiti/project-agent-memory.js +115 -0
  110. package/src/graphiti/project-agent-task-entry.js +126 -0
  111. package/src/graphiti/project-agent-workflows.js +49 -14
  112. package/src/graphiti/provider-client.js +78 -4
  113. package/src/http/api-router.js +25 -5
  114. package/src/http/employee-api-router.js +64 -0
  115. package/src/http/error-mapping.js +4 -0
  116. package/src/http/graph-api-router.js +14 -0
  117. package/src/http/request-policy.js +6 -3
  118. package/src/http/static-handler.js +4 -3
  119. package/src/mcp/create-mcp-server.js +98 -37
  120. package/src/mcp/direct-call.js +38 -0
  121. package/src/mcp/instructions.js +8 -7
  122. package/src/mcp/provider-task-context-registry.js +74 -0
  123. package/src/mcp/remote-http-server.js +761 -0
  124. package/src/mcp/session-id.js +29 -3
  125. package/src/mcp/task-context-registry.js +88 -29
  126. package/src/mcp/tool-annotations.js +12 -2
  127. package/src/mcp/tool-result.js +8 -3
  128. package/src/mcp-server.js +20 -8
  129. package/src/native-runtime/runtime.js +58 -8
  130. package/src/server.js +76 -28
  131. package/src/setup/agent-installation-status.js +38 -6
  132. package/src/setup/agents.js +20 -8
  133. package/src/setup/claude-code-config.js +55 -13
  134. package/src/setup/codex-bootstrap.js +11 -10
  135. package/src/setup/codex-config.js +74 -7
  136. package/src/setup/config-backup.js +18 -2
  137. package/src/setup/cursor-config.js +22 -5
  138. package/src/setup/setup.js +30 -22
  139. package/src/storage/json-file.js +11 -3
  140. package/dist/web/assets/KnowledgeEditDialog-DSe3Gwep.js +0 -1
  141. package/dist/web/assets/KnowledgePage-vYYXipOi.js +0 -1
  142. package/dist/web/assets/PersonalProjectsPage-B7AlLEwf.js +0 -1
  143. package/dist/web/assets/ProjectAgentsPage-CgftpIPB.css +0 -1
  144. package/dist/web/assets/ProjectAgentsPage-DTHcL_ox.js +0 -11
  145. package/dist/web/assets/SearchableSelect-Dz13ClQb.js +0 -1
  146. package/dist/web/assets/conversation-launcher-settings-DTYxiLJt.js +0 -1
  147. package/dist/web/assets/index-BnH5mcZx.css +0 -1
  148. package/dist/web/assets/index-C67Q0JY2.js +0 -2
@@ -3,7 +3,11 @@ from typing import Literal
3
3
 
4
4
  from pydantic import AliasChoices, Field, field_validator, model_validator
5
5
 
6
- from .model_validation import reject_credentials, validate_emoji_sequence
6
+ from .model_validation import (
7
+ normalize_source_session_url,
8
+ reject_credentials,
9
+ validate_emoji_sequence,
10
+ )
7
11
  from .models import SourceApplication, StrictModel
8
12
  from .project_agent_models import (
9
13
  ProjectAgentExecutorPolicy,
@@ -41,6 +45,10 @@ ProjectAgentRoutingOutcome = Literal[
41
45
  ]
42
46
  ProjectAgentRoutingReason = Literal[
43
47
  'explicit_agent',
48
+ 'project_continuity',
49
+ 'project_default',
50
+ 'sole_active_assignment',
51
+ 'manual_agent_selection',
44
52
  'exact_work_kind',
45
53
  'exact_capability',
46
54
  'no_match',
@@ -69,6 +77,16 @@ ProjectAgentOptimizationPriority = Literal[
69
77
  'token_and_cost',
70
78
  'time',
71
79
  ]
80
+ ProjectAgentTokenUsageSource = Literal['executor', 'host', 'dingdong']
81
+
82
+ _WORKER_EVIDENCE_ALIASES = {
83
+ 'worker_id': 'workerId',
84
+ 'worker_label': 'workerLabel',
85
+ 'worker_occupation_emoji': 'workerOccupationEmoji',
86
+ 'worker_status': 'workerStatus',
87
+ 'token_usage': 'tokenUsage',
88
+ 'worker_runtime': 'workerRuntime',
89
+ }
72
90
 
73
91
 
74
92
  def _unique_text(value: list[str], label: str) -> list[str]:
@@ -80,6 +98,19 @@ def _unique_text(value: list[str], label: str) -> list[str]:
80
98
  return normalized
81
99
 
82
100
 
101
+ def _normalize_worker_evidence_aliases(value):
102
+ if not isinstance(value, dict):
103
+ return value
104
+ normalized = dict(value)
105
+ for field_name, alias in _WORKER_EVIDENCE_ALIASES.items():
106
+ if alias not in normalized:
107
+ continue
108
+ if field_name in normalized and normalized[field_name] != normalized[alias]:
109
+ raise ValueError(f'conflicting {field_name} and {alias} values')
110
+ normalized[field_name] = normalized.pop(alias)
111
+ return normalized
112
+
113
+
83
114
  class ProjectAgentParallelPlan(StrictModel):
84
115
  enabled: bool = False
85
116
  independent_verification: bool = False
@@ -117,6 +148,10 @@ class ProjectAgentTaskSubmit(StrictModel):
117
148
  objective: str = Field(min_length=1, max_length=4096)
118
149
  work_kind: str = Field(min_length=1, max_length=128)
119
150
  required_capabilities: list[str] = Field(default_factory=list, max_length=16)
151
+ executor_capability_hints: list[str] = Field(
152
+ default_factory=list,
153
+ max_length=16,
154
+ )
120
155
  duration: ProjectAgentTaskDuration = 'ongoing'
121
156
  staffing_intent: ProjectAgentStaffingIntent = 'reuse_preferred'
122
157
  lead_agent_id: str | None = Field(default=None, min_length=1, max_length=128)
@@ -140,6 +175,14 @@ class ProjectAgentTaskSubmit(StrictModel):
140
175
  def validate_capabilities(cls, value: list[str]) -> list[str]:
141
176
  return _unique_text(value, 'required capabilities')
142
177
 
178
+ @field_validator('executor_capability_hints')
179
+ @classmethod
180
+ def validate_executor_capability_hints(
181
+ cls,
182
+ value: list[str],
183
+ ) -> list[str]:
184
+ return _unique_text(value, 'executor capability hints')
185
+
143
186
  @field_validator('collaborator_agent_ids')
144
187
  @classmethod
145
188
  def validate_collaborators(cls, value: list[str]) -> list[str]:
@@ -182,6 +225,80 @@ class ProjectAgentTaskParticipantRecord(StrictModel):
182
225
  ended_at: datetime | None = None
183
226
 
184
227
 
228
+ class ProjectAgentTokenUsage(StrictModel):
229
+ """Cumulative token snapshot reported for one concrete worker run."""
230
+
231
+ source: ProjectAgentTokenUsageSource
232
+ total_tokens: int = Field(
233
+ ge=0,
234
+ validation_alias=AliasChoices('total_tokens', 'totalTokens'),
235
+ )
236
+ input_tokens: int | None = Field(
237
+ default=None,
238
+ ge=0,
239
+ validation_alias=AliasChoices('input_tokens', 'inputTokens'),
240
+ )
241
+ output_tokens: int | None = Field(
242
+ default=None,
243
+ ge=0,
244
+ validation_alias=AliasChoices('output_tokens', 'outputTokens'),
245
+ )
246
+ cached_input_tokens: int | None = Field(
247
+ default=None,
248
+ ge=0,
249
+ validation_alias=AliasChoices(
250
+ 'cached_input_tokens',
251
+ 'cachedInputTokens',
252
+ ),
253
+ )
254
+ cache_write_input_tokens: int | None = Field(
255
+ default=None,
256
+ ge=0,
257
+ validation_alias=AliasChoices(
258
+ 'cache_write_input_tokens',
259
+ 'cacheWriteInputTokens',
260
+ ),
261
+ )
262
+ reasoning_output_tokens: int | None = Field(
263
+ default=None,
264
+ ge=0,
265
+ validation_alias=AliasChoices(
266
+ 'reasoning_output_tokens',
267
+ 'reasoningOutputTokens',
268
+ ),
269
+ )
270
+
271
+
272
+ class ProjectAgentWorkerRuntime(StrictModel):
273
+ """Host-reported worker location, never the reporter's trusted identity."""
274
+
275
+ application: SourceApplication
276
+ session_id: str | None = Field(
277
+ default=None, min_length=1, max_length=256,
278
+ validation_alias=AliasChoices('session_id', 'sessionId'),
279
+ )
280
+ session_url: str | None = Field(
281
+ default=None, max_length=2048,
282
+ validation_alias=AliasChoices('session_url', 'sessionUrl'),
283
+ )
284
+
285
+ _validate_session_url = field_validator('session_url')(
286
+ normalize_source_session_url
287
+ )
288
+
289
+ @field_validator('session_id')
290
+ @classmethod
291
+ def validate_session_id(cls, value):
292
+ if value is not None and not value.strip():
293
+ raise ValueError('worker session ID must not be blank')
294
+ return value.strip() if value is not None else None
295
+
296
+ @model_validator(mode='after')
297
+ def validate_evidence(self):
298
+ reject_credentials(self, 'worker runtime')
299
+ return self
300
+
301
+
185
302
  class ProjectAgentTaskExecutionSummary(StrictModel):
186
303
  """Auditable execution evidence projected onto a real task participant.
187
304
 
@@ -200,17 +317,31 @@ class ProjectAgentTaskExecutionSummary(StrictModel):
200
317
  source_application: SourceApplication | None = None
201
318
  actual_model_provider: str | None = None
202
319
  actual_model: str | None = None
320
+ source_session_id: str | None = None
321
+ source_session_url: str | None = None
322
+ tools_used: list[str] | None = Field(default=None, max_length=32)
323
+ token_usage: ProjectAgentTokenUsage | None = None
203
324
  work_summary: str | None = None
204
325
  status: ProjectAgentTaskStatus
205
326
  worker_id: str | None = None
206
327
  worker_label: str | None = None
207
328
  worker_occupation_emoji: str | None = None
329
+ worker_runtime: ProjectAgentWorkerRuntime | None = None
208
330
 
209
331
  @field_validator('occupation_emoji', 'worker_occupation_emoji', mode='before')
210
332
  @classmethod
211
333
  def validate_emojis(cls, value):
212
334
  return validate_emoji_sequence(value, 'occupation emoji')
213
335
 
336
+ _validate_session_url = field_validator('source_session_url')(
337
+ normalize_source_session_url
338
+ )
339
+
340
+ @field_validator('tools_used')
341
+ @classmethod
342
+ def validate_tools_used(cls, value):
343
+ return _unique_text(value, 'tools used') if value is not None else None
344
+
214
345
 
215
346
  class ProjectAgentTaskEventRecord(StrictModel):
216
347
  event_id: str
@@ -221,6 +352,8 @@ class ProjectAgentTaskEventRecord(StrictModel):
221
352
  summary: str
222
353
  source_application: SourceApplication | None = None
223
354
  source_session_id: str | None = None
355
+ source_session_url: str | None = None
356
+ tools_used: list[str] | None = Field(default=None, max_length=32)
224
357
  actual_model_provider: str | None = None
225
358
  actual_model: str | None = None
226
359
  actual_executor_id: str | None = None
@@ -229,32 +362,32 @@ class ProjectAgentTaskEventRecord(StrictModel):
229
362
  executor_selection_reason: str | None = None
230
363
  executor_fallback_reason: str | None = None
231
364
  executor_blocked_reason: str | None = None
232
- worker_id: str | None = Field(
233
- default=None,
234
- validation_alias=AliasChoices('worker_id', 'workerId'),
235
- )
236
- worker_label: str | None = Field(
237
- default=None,
238
- validation_alias=AliasChoices('worker_label', 'workerLabel'),
239
- )
240
- worker_occupation_emoji: str | None = Field(
241
- default=None,
242
- validation_alias=AliasChoices(
243
- 'worker_occupation_emoji',
244
- 'workerOccupationEmoji',
245
- ),
246
- )
247
- worker_status: ProjectAgentTaskStatus | None = Field(
248
- default=None,
249
- validation_alias=AliasChoices('worker_status', 'workerStatus'),
250
- )
365
+ worker_id: str | None = None
366
+ worker_label: str | None = None
367
+ worker_occupation_emoji: str | None = None
368
+ worker_status: ProjectAgentTaskStatus | None = None
369
+ token_usage: ProjectAgentTokenUsage | None = None
370
+ worker_runtime: ProjectAgentWorkerRuntime | None = None
251
371
  created_at: datetime
252
372
 
373
+ _normalize_worker_aliases = model_validator(mode='before')(
374
+ _normalize_worker_evidence_aliases
375
+ )
376
+
253
377
  @field_validator('worker_occupation_emoji', mode='before')
254
378
  @classmethod
255
379
  def validate_worker_occupation_emoji(cls, value):
256
380
  return validate_emoji_sequence(value, 'worker occupation emoji')
257
381
 
382
+ _validate_session_url = field_validator('source_session_url')(
383
+ normalize_source_session_url
384
+ )
385
+
386
+ @field_validator('tools_used')
387
+ @classmethod
388
+ def validate_tools_used(cls, value):
389
+ return _unique_text(value, 'tools used') if value is not None else None
390
+
258
391
 
259
392
  class ProjectAgentRoutingDecisionRecord(StrictModel):
260
393
  decision_id: str
@@ -299,6 +432,7 @@ class ProjectAgentTaskRecord(StrictModel):
299
432
  objective: str
300
433
  work_kind: str
301
434
  required_capabilities: list[str]
435
+ executor_capability_hints: list[str] = Field(default_factory=list)
302
436
  duration: ProjectAgentTaskDuration
303
437
  staffing_intent: ProjectAgentStaffingIntent
304
438
  status: ProjectAgentTaskStatus
@@ -363,6 +497,8 @@ class ProjectAgentTaskActivityCreate(StrictModel):
363
497
  actor_kind: Literal['agent', 'human'] = 'agent'
364
498
  source_application: SourceApplication | None = None
365
499
  source_session_id: str | None = Field(default=None, min_length=1, max_length=256)
500
+ source_session_url: str | None = Field(default=None, max_length=2048)
501
+ tools_used: list[str] | None = Field(default=None, max_length=32)
366
502
  actual_model_provider: str | None = Field(default=None, min_length=1, max_length=128)
367
503
  actual_model: str | None = Field(default=None, min_length=1, max_length=256)
368
504
  actual_executor_id: str | None = Field(default=None, min_length=1, max_length=128)
@@ -390,33 +526,46 @@ class ProjectAgentTaskActivityCreate(StrictModel):
390
526
  default=None,
391
527
  min_length=1,
392
528
  max_length=128,
393
- validation_alias=AliasChoices('worker_id', 'workerId'),
394
529
  )
395
530
  worker_label: str | None = Field(
396
531
  default=None,
397
532
  min_length=1,
398
533
  max_length=160,
399
- validation_alias=AliasChoices('worker_label', 'workerLabel'),
400
534
  )
401
535
  worker_occupation_emoji: str | None = Field(
402
536
  default=None,
403
537
  min_length=1,
404
538
  max_length=32,
405
- validation_alias=AliasChoices(
406
- 'worker_occupation_emoji',
407
- 'workerOccupationEmoji',
408
- ),
409
539
  )
410
- worker_status: ProjectAgentTaskStatus | None = Field(
411
- default=None,
412
- validation_alias=AliasChoices('worker_status', 'workerStatus'),
540
+ worker_status: ProjectAgentTaskStatus | None = None
541
+ token_usage: ProjectAgentTokenUsage | None = None
542
+ worker_runtime: ProjectAgentWorkerRuntime | None = None
543
+
544
+ _normalize_worker_aliases = model_validator(mode='before')(
545
+ _normalize_worker_evidence_aliases
413
546
  )
414
547
 
548
+ @property
549
+ def reported_client_applications(self):
550
+ # Worker claims can impose another restriction, never replace the
551
+ # host identity used for authorization and audit attribution.
552
+ return (self.source_application, self.worker_runtime.application) \
553
+ if self.worker_runtime else (self.source_application,)
554
+
415
555
  @field_validator('worker_occupation_emoji', mode='before')
416
556
  @classmethod
417
557
  def validate_worker_occupation_emoji(cls, value):
418
558
  return validate_emoji_sequence(value, 'worker occupation emoji')
419
559
 
560
+ _validate_session_url = field_validator('source_session_url')(
561
+ normalize_source_session_url
562
+ )
563
+
564
+ @field_validator('tools_used')
565
+ @classmethod
566
+ def validate_tools_used(cls, value):
567
+ return _unique_text(value, 'tools used') if value is not None else None
568
+
420
569
  @model_validator(mode='after')
421
570
  def validate_actual_model(self):
422
571
  if self.status in {'completed', 'failed'} and not self.agent_id:
@@ -433,6 +582,24 @@ class ProjectAgentTaskActivityCreate(StrictModel):
433
582
  raise ValueError('actual execution report requires the participating Agent ID')
434
583
  if self.worker_id and not self.agent_id:
435
584
  raise ValueError('worker execution report requires the participating Agent ID')
585
+ if self.worker_status and not self.worker_id:
586
+ raise ValueError('worker status requires a worker ID')
587
+ if self.worker_runtime and not self.worker_id:
588
+ raise ValueError('worker runtime requires a worker ID')
589
+ if self.token_usage and not self.agent_id:
590
+ raise ValueError('token usage report requires the participating Agent ID')
591
+ if (self.source_session_url or self.tools_used) and not self.agent_id:
592
+ raise ValueError(
593
+ 'execution evidence requires the participating Agent ID'
594
+ )
595
+ if (
596
+ self.token_usage
597
+ and self.token_usage.source == 'executor'
598
+ and not self.actual_executor_id
599
+ ):
600
+ raise ValueError(
601
+ 'executor token usage requires the actual executor report'
602
+ )
436
603
  if (self.worker_label or self.worker_occupation_emoji) and not self.worker_id:
437
604
  raise ValueError(
438
605
  'worker label or occupation emoji requires a worker ID'
@@ -457,6 +624,8 @@ class ProjectAgentRecruitmentRecord(StrictModel):
457
624
  confirmation_mode: ProjectAgentRecruitmentConfirmationMode
458
625
  proposed_agent_id: str
459
626
  proposed_profile: ProjectAgentProfile
627
+ participant_role: ProjectAgentParticipantRole = 'lead'
628
+ recruitment_slot: str = Field(default='lead', min_length=1, max_length=128)
460
629
  trigger_source_application: SourceApplication | None = None
461
630
  trigger_source_session_id: str | None = None
462
631
  revision: int = Field(ge=0)
@@ -472,6 +641,7 @@ class ProjectAgentTaskRouteResult(StrictModel):
472
641
  task: ProjectAgentTaskRecord
473
642
  assigned_agent: ProjectAgentRecord | None = None
474
643
  recruitment: ProjectAgentRecruitmentRecord | None = None
644
+ recruitments: list[ProjectAgentRecruitmentRecord] = Field(default_factory=list)
475
645
  decision: Literal[
476
646
  'reused',
477
647
  'recruited',
@@ -487,6 +657,13 @@ class ProjectAgentRecruitmentPolicyRecord(StrictModel):
487
657
  personal_space_id: str
488
658
  confirmation_mode: ProjectAgentRecruitmentConfirmationMode = 'automatic'
489
659
  updated_at: datetime | None = None
660
+ policy_status: Literal['superseded'] = 'superseded'
661
+ applies_to_recruitment: Literal[False] = False
662
+ warning: str = (
663
+ 'Legacy storage only; this setting does not control recruitment. '
664
+ 'Use update_project_agent_coordination_policy with the exact project '
665
+ 'and askBeforeRecruitment to change effective authorization.'
666
+ )
490
667
 
491
668
 
492
669
  class ProjectAgentRecruitmentPolicyUpdate(StrictModel):
@@ -518,12 +695,26 @@ class ProjectAgentActivityTask(StrictModel):
518
695
  actual_model_provider: str | None = None
519
696
  actual_model: str | None = None
520
697
  actual_executor_id: str | None = None
698
+ source_session_id: str | None = None
699
+ source_session_url: str | None = Field(default=None, max_length=2048)
700
+ tools_used: list[str] | None = Field(default=None, max_length=32)
701
+ token_usage: ProjectAgentTokenUsage | None = None
702
+ worker_runtime: ProjectAgentWorkerRuntime | None = None
521
703
  matched_executor_rule_id: str | None = None
522
704
  worker_id: str | None = None
523
705
  worker_label: str | None = None
524
706
  worker_occupation_emoji: str | None = None
525
707
  worker_status: ProjectAgentTaskStatus | None = None
526
708
 
709
+ _validate_session_url = field_validator('source_session_url')(
710
+ normalize_source_session_url
711
+ )
712
+
713
+ @field_validator('tools_used')
714
+ @classmethod
715
+ def validate_tools_used(cls, value):
716
+ return _unique_text(value, 'tools used') if value is not None else None
717
+
527
718
 
528
719
  class ProjectAgentActivityDay(StrictModel):
529
720
  date: date
@@ -108,11 +108,37 @@ class LocalLexicalReranker(CrossEncoderClient):
108
108
  )
109
109
 
110
110
 
111
+ class ManagedNeo4jDriver(Neo4jDriver):
112
+ """Let GraphitiRuntime own index initialization exactly once.
113
+
114
+ graphiti-core schedules an eager background index build whenever its driver
115
+ is constructed inside a running event loop. Fuli has an explicit awaited
116
+ initialization sequence, so the dependency's untracked task would race it
117
+ and can leak unawaited query coroutines when one index request fails.
118
+ """
119
+
120
+ def __init__(self, *args, **kwargs):
121
+ self._fuli_constructing = True
122
+ try:
123
+ super().__init__(*args, **kwargs)
124
+ finally:
125
+ self._fuli_constructing = False
126
+
127
+ def build_indices_and_constraints(self, delete_existing: bool = False):
128
+ if self._fuli_constructing:
129
+ return _completed_index_build()
130
+ return super().build_indices_and_constraints(delete_existing)
131
+
132
+
133
+ async def _completed_index_build() -> None:
134
+ return None
135
+
136
+
111
137
  class GraphitiRuntime:
112
138
  def __init__(self, settings: Settings):
113
139
  self.settings = settings
114
140
  self.embedder = LocalHashEmbedder(settings.embedding_dim)
115
- driver = Neo4jDriver(
141
+ driver = ManagedNeo4jDriver(
116
142
  uri=settings.neo4j_uri,
117
143
  user=settings.neo4j_user,
118
144
  password=settings.neo4j_password,
@@ -247,6 +273,16 @@ class GraphitiRuntime:
247
273
  'FOR (n:FuliProjectAgent) REQUIRE n.id IS UNIQUE',
248
274
  'CREATE CONSTRAINT fuli_project_agent_assignment_id IF NOT EXISTS '
249
275
  'FOR (n:FuliProjectAgentAssignment) REQUIRE n.id IS UNIQUE',
276
+ 'CREATE CONSTRAINT fuli_project_agent_memory_id IF NOT EXISTS '
277
+ 'FOR (n:FuliProjectAgentMemory) REQUIRE n.id IS UNIQUE',
278
+ 'CREATE CONSTRAINT fuli_project_agent_memory_checkpoint_id IF NOT EXISTS '
279
+ 'FOR (n:FuliProjectAgentMemoryCheckpoint) REQUIRE n.id IS UNIQUE',
280
+ 'CREATE CONSTRAINT fuli_task_context_session_id IF NOT EXISTS '
281
+ 'FOR (n:FuliTaskContextSession) REQUIRE n.id IS UNIQUE',
282
+ 'CREATE CONSTRAINT fuli_task_context_id IF NOT EXISTS '
283
+ 'FOR (n:FuliTaskContext) REQUIRE n.id IS UNIQUE',
284
+ 'CREATE INDEX fuli_task_context_agent_scope IF NOT EXISTS '
285
+ 'FOR (n:FuliTaskContext) ON (n.personal_space_id, n.project_agent_id)',
250
286
  'CREATE CONSTRAINT fuli_project_agent_task_id IF NOT EXISTS '
251
287
  'FOR (n:FuliProjectAgentTask) REQUIRE n.id IS UNIQUE',
252
288
  'CREATE CONSTRAINT fuli_project_agent_task_event_id IF NOT EXISTS '
@@ -4,17 +4,23 @@ from .runtime import GraphitiRuntime
4
4
  from .store_identity import StoreIdentity
5
5
  from .store_knowledge import StoreKnowledge
6
6
  from .store_project_agent_executors import StoreProjectAgentExecutors
7
+ from .store_project_agent_memory import StoreProjectAgentMemory
8
+ from .store_project_agent_context import StoreProjectAgentContext
7
9
  from .store_project_agents import StoreProjectAgents
8
10
  from .store_project_agent_tasks import StoreProjectAgentTasks
9
11
  from .store_projects import StoreProjects
10
12
  from .store_publication import StorePublication
11
13
  from .store_records import StoreRecords
14
+ from .store_task_contexts import StoreTaskContexts
12
15
 
13
16
 
14
17
  class GraphStore(
15
18
  StoreIdentity,
16
19
  StoreProjects,
17
20
  StoreProjectAgents,
21
+ StoreProjectAgentMemory,
22
+ StoreProjectAgentContext,
23
+ StoreTaskContexts,
18
24
  StoreProjectAgentExecutors,
19
25
  StoreProjectAgentTasks,
20
26
  StorePublication,
@@ -0,0 +1,65 @@
1
+ """Resolve a durable role for the already-running caller, without execution."""
2
+
3
+ from .personal_project_access import authorize_personal_project
4
+ from .project_agent_context_models import ProjectAgentContextResolution
5
+ from .project_agent_task_models import ProjectAgentTaskSubmit
6
+
7
+
8
+ class StoreProjectAgentContext:
9
+ async def resolve_project_agent_context(self, actor, request):
10
+ self._require_personal()
11
+ space = await self.authorize(actor, request.personal_space_id, 'reader')
12
+ await authorize_personal_project(self, actor, space, request.personal_project_id)
13
+ owner = None
14
+ if request.session_id and not request.agent_id:
15
+ rows, _, _ = await self.runtime.driver.execute_query(
16
+ '''
17
+ MATCH (session:FuliTaskContextSession {id: $session_id})-[:HAS_CONTEXT]->
18
+ (context:FuliTaskContext {personal_space_id: $space_id,
19
+ personal_project_id: $project_id, completed: false})
20
+ WHERE session.current_token = context.token
21
+ AND ($turn_id IS NULL OR context.turn_id = $turn_id)
22
+ RETURN context.project_agent_id AS agent_id
23
+ ''', session_id=self._task_session_id(request.personal_space_id,
24
+ request.source_application, request.session_id),
25
+ space_id=request.personal_space_id, project_id=request.personal_project_id,
26
+ turn_id=request.turn_id,
27
+ routing_='r',
28
+ )
29
+ owner = rows[0]['agent_id'] if rows else None
30
+ # Reuse the task router's staffing policy. These fixed values and the
31
+ # request are not persisted; raw user prompts never enter this API.
32
+ selection = ProjectAgentTaskSubmit(
33
+ personal_space_id=request.personal_space_id,
34
+ personal_project_id=request.personal_project_id,
35
+ idempotency_key='read-only-task-context',
36
+ title='Resolve task context', objective='Select one existing durable role.',
37
+ work_kind=request.work_kind, required_capabilities=request.required_capabilities,
38
+ lead_agent_id=request.agent_id or owner, source_application=request.source_application,
39
+ routing_reason='Read-only task-entry context recovery',
40
+ )
41
+ selected, candidates, reason, basis = await self._select_agents(actor, space, selection)
42
+ if not selected and reason == 'no_match' and not request.required_capabilities:
43
+ selected, candidates, reason, basis = await self._select_agents(
44
+ actor, space, selection.model_copy(update={'work_kind': 'project_context'}),
45
+ )
46
+ if selected:
47
+ reason = 'project_context_fallback'
48
+ basis = [
49
+ f'no role matched requested work kind: {request.work_kind}; '
50
+ 'using project_context for continuity only',
51
+ *basis,
52
+ ]
53
+ if owner and selected:
54
+ reason, basis = 'active_task_owner', ['reuse the owner of this host session task']
55
+ agent = await self.get_project_agent(
56
+ actor, request.personal_space_id, request.personal_project_id,
57
+ selected[0]['agent_id'],
58
+ ) if selected else None
59
+ return ProjectAgentContextResolution(
60
+ status='ready' if agent else (
61
+ 'manual_selection' if reason == 'manual_agent_selection'
62
+ else 'agent_unavailable' if reason == 'agent_unavailable' else 'unassigned'
63
+ ),
64
+ agent=agent, reason=reason, match_basis=basis, candidate_count=len(candidates),
65
+ )
@@ -0,0 +1,122 @@
1
+ """Project-local Agent continuity and recruitment authorization."""
2
+
3
+ from .personal_project_access import authorize_personal_project
4
+ from .project_agent_coordination_models import (
5
+ ProjectAgentCoordinationPolicyRecord,
6
+ ProjectAgentCoordinationPolicyUpdate,
7
+ )
8
+ from .provider_values import native_datetime, now_utc, stable_uuid
9
+
10
+
11
+ class StoreProjectAgentCoordinationPolicy:
12
+ """Persist the two user-facing project Agent automation switches."""
13
+
14
+ async def get_project_agent_coordination_policy(
15
+ self,
16
+ actor: dict,
17
+ personal_space_id: str,
18
+ personal_project_id: str,
19
+ ) -> ProjectAgentCoordinationPolicyRecord:
20
+ self._require_personal()
21
+ space = await self.authorize(actor, personal_space_id, 'reader')
22
+ await authorize_personal_project(
23
+ self,
24
+ actor,
25
+ space,
26
+ personal_project_id,
27
+ )
28
+ records, _, _ = await self.runtime.driver.execute_query(
29
+ '''
30
+ MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})-
31
+ [:CONTAINS_PROJECT]->
32
+ (project:FuliPersonalProject {project_id: $personal_project_id})
33
+ OPTIONAL MATCH (project)-[:HAS_PROJECT_AGENT_COORDINATION_POLICY]->
34
+ (policy:FuliProjectAgentCoordinationPolicy {
35
+ policy_id: $policy_id
36
+ })
37
+ RETURN policy
38
+ ''',
39
+ personal_space_id=personal_space_id,
40
+ personal_project_id=personal_project_id,
41
+ policy_id=self._project_agent_coordination_policy_id(
42
+ personal_space_id,
43
+ personal_project_id,
44
+ ),
45
+ routing_='r',
46
+ )
47
+ raw = dict(records[0]['policy']) if records and records[0].get('policy') else {}
48
+ return ProjectAgentCoordinationPolicyRecord(
49
+ personal_space_id=personal_space_id,
50
+ personal_project_id=personal_project_id,
51
+ ask_before_recruitment=raw.get('ask_before_recruitment', True),
52
+ auto_reuse_previous_agent=raw.get(
53
+ 'auto_reuse_previous_agent',
54
+ True,
55
+ ),
56
+ updated_at=native_datetime(raw.get('updated_at')),
57
+ )
58
+
59
+ async def update_project_agent_coordination_policy(
60
+ self,
61
+ actor: dict,
62
+ request: ProjectAgentCoordinationPolicyUpdate,
63
+ ) -> ProjectAgentCoordinationPolicyRecord:
64
+ self._require_personal()
65
+ space = await self.authorize(
66
+ actor,
67
+ request.personal_space_id,
68
+ 'maintainer',
69
+ )
70
+ await authorize_personal_project(
71
+ self,
72
+ actor,
73
+ space,
74
+ request.personal_project_id,
75
+ )
76
+ policy_id = self._project_agent_coordination_policy_id(
77
+ request.personal_space_id,
78
+ request.personal_project_id,
79
+ )
80
+ updated_at = now_utc()
81
+ await self.runtime.driver.execute_query(
82
+ '''
83
+ MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})-
84
+ [:CONTAINS_PROJECT]->
85
+ (project:FuliPersonalProject {project_id: $personal_project_id})
86
+ MERGE (policy:FuliProjectAgentCoordinationPolicy {
87
+ policy_id: $policy_id
88
+ })
89
+ ON CREATE SET policy.personal_space_id = $personal_space_id,
90
+ policy.personal_project_id = $personal_project_id,
91
+ policy.created_at = $updated_at
92
+ SET policy.ask_before_recruitment = $ask_before_recruitment,
93
+ policy.auto_reuse_previous_agent = $auto_reuse_previous_agent,
94
+ policy.updated_at = $updated_at
95
+ MERGE (project)-[:HAS_PROJECT_AGENT_COORDINATION_POLICY]->(policy)
96
+ ''',
97
+ policy_id=policy_id,
98
+ personal_space_id=request.personal_space_id,
99
+ personal_project_id=request.personal_project_id,
100
+ ask_before_recruitment=request.ask_before_recruitment,
101
+ auto_reuse_previous_agent=request.auto_reuse_previous_agent,
102
+ updated_at=updated_at,
103
+ )
104
+ return ProjectAgentCoordinationPolicyRecord(
105
+ personal_space_id=request.personal_space_id,
106
+ personal_project_id=request.personal_project_id,
107
+ ask_before_recruitment=request.ask_before_recruitment,
108
+ auto_reuse_previous_agent=request.auto_reuse_previous_agent,
109
+ updated_at=updated_at,
110
+ )
111
+
112
+ def _project_agent_coordination_policy_id(
113
+ self,
114
+ personal_space_id: str,
115
+ personal_project_id: str,
116
+ ) -> str:
117
+ return stable_uuid(
118
+ self.settings.provider_id,
119
+ personal_space_id,
120
+ 'project-agent-coordination-policy',
121
+ personal_project_id,
122
+ )