fuli-context 0.7.6 → 0.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +69 -13
  2. package/README.zh-CN.md +60 -14
  3. package/dist/web/assets/{AboutPage-sLV9WChi.js → AboutPage-9CVX91tr.js} +1 -1
  4. package/dist/web/assets/{ConnectionsPage-BWn4ywGJ.js → ConnectionsPage-CWbCVFdv.js} +1 -1
  5. package/dist/web/assets/EmployeeRecruitDialog-CQgNpGJ_.js +1 -0
  6. package/dist/web/assets/EmployeeRecruitDialog-CfFlyKSh.css +1 -0
  7. package/dist/web/assets/EmployeeWorkbenchPage-CWg4WJhr.css +1 -0
  8. package/dist/web/assets/EmployeeWorkbenchPage-CwU4qBgD.js +1 -0
  9. package/dist/web/assets/KnowledgeEditDialog-B6TGAcFH.js +1 -0
  10. package/dist/web/assets/{KnowledgeEditDialog-DWUV_NPp.css → KnowledgeEditDialog-iv-dgUeD.css} +1 -1
  11. package/dist/web/assets/{KnowledgeInspector-CRQWOaos.js → KnowledgeInspector-M2pLY0bR.js} +1 -1
  12. package/dist/web/assets/{KnowledgeOrganizerPage-Dfj3vIpU.js → KnowledgeOrganizerPage-DzeAlk9b.js} +1 -1
  13. package/dist/web/assets/KnowledgePage-BJu9w0fF.js +1 -0
  14. package/dist/web/assets/{KnowledgeWorkspace-ChEB5Bwk.js → KnowledgeWorkspace-CCt8rnBb.js} +1 -1
  15. package/dist/web/assets/{OverviewPage-Cd66M0RT.js → OverviewPage-CZswVqui.js} +1 -1
  16. package/dist/web/assets/{PersonalProfilePage-RCFHL0Wz.js → PersonalProfilePage-f1nSF6kx.js} +1 -1
  17. package/dist/web/assets/PersonalProjectsPage-D__eJf0t.js +1 -0
  18. package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +1 -0
  19. package/dist/web/assets/ProjectAgentsPage-LrwagMGj.js +11 -0
  20. package/dist/web/assets/{PublicProjectsPage-mstPEjHe.js → PublicProjectsPage-BdC2Qk4n.js} +1 -1
  21. package/dist/web/assets/{ReviewPage-6Y1SmfZy.js → ReviewPage-Cbgl8_Si.js} +1 -1
  22. package/dist/web/assets/SearchableSelect-ERm-k4KI.js +1 -0
  23. package/dist/web/assets/{SettingsPage--LdTMUm7.js → SettingsPage-DWso4x29.js} +1 -1
  24. package/dist/web/assets/{WritingTastePage-CIpKswvs.js → WritingTastePage-CFpCjmIY.js} +1 -1
  25. package/dist/web/assets/catalog-WGLn7ERJ.js +1 -0
  26. package/dist/web/assets/{console-BT1P2Db4.js → console-DQgu0JIA.js} +1 -1
  27. package/dist/web/assets/conversation-launcher-settings-0KtlG_LV.js +1 -0
  28. package/dist/web/assets/{i18n-js6gfc7j.js → i18n-ByRiA3pJ.js} +3 -3
  29. package/dist/web/assets/{identity-CfDfanpa.js → identity-CxdPRMwH.js} +1 -1
  30. package/dist/web/assets/index-JAZe6lfX.js +2 -0
  31. package/dist/web/assets/index-wcXzzggp.css +1 -0
  32. package/dist/web/assets/{model-DJkLAJsg.js → model-Db4-KefQ.js} +1 -1
  33. package/dist/web/assets/{useApi-CROJJdhE-BdSti7Y2.js → useApi-CROJJdhE-CePMjc08.js} +1 -1
  34. package/dist/web/assets/{vue-router-Kk67EaCP.js → vue-router-CzX-hn3P.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_executor_models.py +35 -7
  44. package/graph-provider/fuli_graph/project_agent_memory_models.py +79 -0
  45. package/graph-provider/fuli_graph/project_agent_memory_routes.py +46 -0
  46. package/graph-provider/fuli_graph/project_agent_models.py +9 -0
  47. package/graph-provider/fuli_graph/project_agent_recruitment_values.py +48 -0
  48. package/graph-provider/fuli_graph/project_agent_routes.py +21 -1
  49. package/graph-provider/fuli_graph/project_agent_task_models.py +217 -29
  50. package/graph-provider/fuli_graph/runtime.py +37 -1
  51. package/graph-provider/fuli_graph/store.py +6 -0
  52. package/graph-provider/fuli_graph/store_project_agent_context.py +65 -0
  53. package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +175 -11
  54. package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +33 -10
  55. package/graph-provider/fuli_graph/store_project_agent_executors.py +181 -14
  56. package/graph-provider/fuli_graph/store_project_agent_memory.py +174 -0
  57. package/graph-provider/fuli_graph/store_project_agent_task_activity.py +423 -0
  58. package/graph-provider/fuli_graph/store_project_agent_task_persistence.py +152 -0
  59. package/graph-provider/fuli_graph/store_project_agent_task_reads.py +187 -21
  60. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +748 -162
  61. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +230 -18
  62. package/graph-provider/fuli_graph/store_project_agent_tasks.py +261 -468
  63. package/graph-provider/fuli_graph/store_project_agents.py +214 -46
  64. package/graph-provider/fuli_graph/store_task_contexts.py +217 -0
  65. package/graph-provider/fuli_graph/store_transactions.py +40 -0
  66. package/graph-provider/fuli_graph/task_context_models.py +50 -0
  67. package/graph-provider/fuli_graph/task_context_routes.py +31 -0
  68. package/graph-provider/fuli_graph/workflow_observation_models.py +1 -1
  69. package/graph-provider/pyproject.toml +1 -1
  70. package/npm-shrinkwrap.json +8 -8
  71. package/package.json +5 -2
  72. package/skills/capturing-session-knowledge/SKILL.md +18 -6
  73. package/src/adaptive-runtime/executor-pool.js +46 -5
  74. package/src/adaptive-runtime/graph-services.js +10 -3
  75. package/src/adaptive-runtime/lease-client.js +66 -14
  76. package/src/adaptive-runtime/runtime-broker.js +24 -9
  77. package/src/agent-tools/employee-definitions.js +43 -0
  78. package/src/agent-tools/graph-definitions.js +25 -6
  79. package/src/agent-tools/graph-handlers.js +11 -0
  80. package/src/agent-tools/project-agent-definitions.js +35 -4
  81. package/src/agent-tools/project-agent-memory-definitions.js +36 -0
  82. package/src/agent-tools.js +6 -2
  83. package/src/agents/codex/lifecycle-hook.js +107 -0
  84. package/src/agents/codex/lifecycle-hooks.js +231 -0
  85. package/src/agents/cursor/lifecycle-hook.js +133 -0
  86. package/src/agents/cursor/lifecycle-hooks.js +50 -0
  87. package/src/agents/lifecycle-message.js +19 -0
  88. package/src/app/agent-request-context.js +23 -0
  89. package/src/cli/command-registry.js +2 -0
  90. package/src/cli/employee-command.js +17 -0
  91. package/src/cli/remote-mcp-command.js +282 -0
  92. package/src/cli/setup-command.js +5 -0
  93. package/src/cli/update-command.js +6 -43
  94. package/src/cli.js +10 -0
  95. package/src/employees/catalog/jefa.json +21 -0
  96. package/src/employees/install-package.js +77 -0
  97. package/src/employees/management-policy.js +88 -0
  98. package/src/employees/manifest.js +53 -0
  99. package/src/employees/package-registry.js +107 -0
  100. package/src/employees/project-selection.js +50 -0
  101. package/src/employees/recruitment.js +199 -0
  102. package/src/employees/service.js +83 -0
  103. package/src/employees/task-entry.js +39 -0
  104. package/src/graphiti/agent-knowledge-workflows.js +135 -25
  105. package/src/graphiti/federated-application.js +78 -35
  106. package/src/graphiti/project-agent-control-plane.js +20 -3
  107. package/src/graphiti/project-agent-mapping.js +155 -5
  108. package/src/graphiti/project-agent-memory.js +115 -0
  109. package/src/graphiti/project-agent-task-entry.js +126 -0
  110. package/src/graphiti/project-agent-workflows.js +30 -14
  111. package/src/graphiti/provider-client.js +66 -4
  112. package/src/http/api-router.js +30 -5
  113. package/src/http/employee-api-router.js +64 -0
  114. package/src/http/error-mapping.js +4 -0
  115. package/src/http/graph-api-router.js +2 -0
  116. package/src/http/request-policy.js +6 -3
  117. package/src/http/static-handler.js +4 -3
  118. package/src/mcp/create-mcp-server.js +98 -37
  119. package/src/mcp/direct-call.js +38 -0
  120. package/src/mcp/instructions.js +8 -7
  121. package/src/mcp/provider-task-context-registry.js +74 -0
  122. package/src/mcp/remote-http-server.js +761 -0
  123. package/src/mcp/session-id.js +29 -3
  124. package/src/mcp/task-context-registry.js +88 -29
  125. package/src/mcp/tool-annotations.js +10 -2
  126. package/src/mcp/tool-result.js +8 -3
  127. package/src/mcp-server.js +20 -8
  128. package/src/native-runtime/runtime.js +58 -8
  129. package/src/semantic-version.js +42 -0
  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/src/system/package-version-checker.js +72 -0
  141. package/src/system/system-service.js +3 -0
  142. package/dist/web/assets/KnowledgeEditDialog-CZ5ziPrD.js +0 -1
  143. package/dist/web/assets/KnowledgePage-BRYC9S5Y.js +0 -1
  144. package/dist/web/assets/PersonalProjectsPage-DEh8XWby.js +0 -1
  145. package/dist/web/assets/ProjectAgentsPage-Crh7PrXa.css +0 -1
  146. package/dist/web/assets/ProjectAgentsPage-DPnaH-TR.js +0 -11
  147. package/dist/web/assets/SearchableSelect-D3HM8YF8.js +0 -1
  148. package/dist/web/assets/conversation-launcher-settings-DrJEotdf.js +0 -1
  149. package/dist/web/assets/index-BnH5mcZx.css +0 -1
  150. package/dist/web/assets/index-GqJf1xqg.js +0 -2
@@ -0,0 +1,46 @@
1
+ """Transport boundary for host-independent Agent working memory."""
2
+
3
+ from typing import Annotated
4
+
5
+ from fastapi import HTTPException, Query
6
+
7
+ from .project_agent_memory_models import (
8
+ ProjectAgentMemoryRecord,
9
+ ProjectAgentMemoryView,
10
+ ProjectAgentMemoryWrite,
11
+ )
12
+ from .project_agent_context_models import (
13
+ ProjectAgentContextRequest, ProjectAgentContextResolution,
14
+ )
15
+ from .task_context_routes import register_task_context_routes
16
+
17
+
18
+ def register_project_agent_memory_routes(application, store, Actor):
19
+ register_task_context_routes(application, store, Actor)
20
+ @application.post(
21
+ '/v1/project-agent-context/resolve', response_model=ProjectAgentContextResolution,
22
+ )
23
+ async def resolve_context(request: ProjectAgentContextRequest, actor: Actor):
24
+ return await store.resolve_project_agent_context(actor, request)
25
+
26
+ @application.get(
27
+ '/v1/project-agents/{agent_id}/memory', response_model=ProjectAgentMemoryView,
28
+ )
29
+ async def get_memory(
30
+ agent_id: str,
31
+ actor: Actor,
32
+ personal_space_id: Annotated[str, Query(min_length=1, max_length=128)],
33
+ personal_project_id: Annotated[str, Query(min_length=1, max_length=128)],
34
+ limit: Annotated[int, Query(ge=1, le=10)] = 1,
35
+ ):
36
+ return await store.get_project_agent_memory(
37
+ actor, personal_space_id, personal_project_id, agent_id, limit=limit,
38
+ )
39
+
40
+ @application.put(
41
+ '/v1/project-agents/{agent_id}/memory', response_model=ProjectAgentMemoryRecord,
42
+ )
43
+ async def write_memory(agent_id: str, request: ProjectAgentMemoryWrite, actor: Actor):
44
+ if request.agent_id != agent_id:
45
+ raise HTTPException(status_code=422, detail='Agent memory target does not match path')
46
+ return await store.write_project_agent_memory(actor, request)
@@ -27,6 +27,7 @@ ProjectAgentReasoningEffort = Literal['default', 'low', 'medium', 'high']
27
27
  ProjectAgentExecutorPolicyMode = Literal['flexible', 'locked']
28
28
  PROJECT_AGENT_CLIENTS: tuple[SourceApplication, ...] = (
29
29
  'codex',
30
+ 'claude',
30
31
  'claude_code',
31
32
  'cursor',
32
33
  'kiro',
@@ -162,6 +163,14 @@ class ProjectAgentUpsert(StrictModel):
162
163
  agent_id: str = Field(min_length=1, max_length=128)
163
164
  profile: ProjectAgentProfile
164
165
 
166
+ @model_validator(mode='after')
167
+ def require_dedicated_archive_operation(self):
168
+ if self.profile.status == 'archived':
169
+ raise ValueError(
170
+ 'archive Project Agents through the dedicated archive operation'
171
+ )
172
+ return self
173
+
165
174
 
166
175
  class ProjectAgentAssignmentCreate(StrictModel):
167
176
  personal_space_id: str = Field(min_length=1, max_length=128)
@@ -0,0 +1,48 @@
1
+ """Value conversion helpers for durable project Agent recruitment records."""
2
+
3
+ from .project_agent_models import ProjectAgentProfile
4
+ from .project_agent_task_models import ProjectAgentRecruitmentRecord
5
+ from .provider_values import native_datetime
6
+
7
+
8
+ def project_agent_recruitment_record(raw):
9
+ proposed_profile = ProjectAgentProfile.model_validate_json(
10
+ raw['proposed_profile_json']
11
+ )
12
+ occupation_emoji = raw.get('occupation_emoji') or raw.get(
13
+ 'occupationEmoji'
14
+ )
15
+ if not proposed_profile.occupation_emoji and occupation_emoji:
16
+ proposed_profile = proposed_profile.model_copy(
17
+ update={'occupation_emoji': occupation_emoji}
18
+ )
19
+ fields = {
20
+ 'recruitment_id': raw.get('recruitment_id') or raw['id'],
21
+ 'personal_space_id': raw['personal_space_id'],
22
+ 'personal_project_id': raw['personal_project_id'],
23
+ 'task_id': raw['task_id'],
24
+ 'coordinator_agent_id': raw['coordinator_agent_id'],
25
+ 'hr_agent_id': raw.get('hr_agent_id'),
26
+ 'position_kind': raw['position_kind'],
27
+ 'work_kind': raw['work_kind'],
28
+ 'required_capabilities': list(raw.get('required_capabilities') or []),
29
+ 'reason_code': raw['reason_code'],
30
+ 'reason': raw['reason'],
31
+ 'status': raw['status'],
32
+ 'confirmation_mode': raw['confirmation_mode'],
33
+ 'proposed_agent_id': raw['proposed_agent_id'],
34
+ 'proposed_profile': proposed_profile,
35
+ 'participant_role': raw.get('participant_role') or 'lead',
36
+ 'recruitment_slot': raw.get('recruitment_slot') or 'lead',
37
+ 'trigger_source_application': raw.get('trigger_source_application'),
38
+ 'trigger_source_session_id': raw.get('trigger_source_session_id'),
39
+ 'revision': int(raw.get('revision') or 0),
40
+ 'recruited_agent_id': raw.get('recruited_agent_id'),
41
+ 'created_at': native_datetime(raw['created_at']),
42
+ 'updated_at': native_datetime(raw['updated_at']),
43
+ 'fulfilled_at': native_datetime(raw.get('fulfilled_at')),
44
+ }
45
+ for name in ('test_source', 'cleanup_eligible'):
46
+ if name in ProjectAgentRecruitmentRecord.model_fields:
47
+ fields[name] = raw.get(name)
48
+ return ProjectAgentRecruitmentRecord(**fields)
@@ -5,6 +5,8 @@ from typing import Annotated, Any
5
5
 
6
6
  from fastapi import FastAPI, HTTPException, Query
7
7
 
8
+ from .project_agent_memory_routes import register_project_agent_memory_routes
9
+
8
10
  from .project_agent_models import (
9
11
  ProjectAgentAssignmentCreate,
10
12
  ProjectAgentAssignmentEnd,
@@ -57,6 +59,8 @@ def register_project_agent_routes(
57
59
  ) -> None:
58
60
  """Attach Project Agent routes while keeping ``create_app`` orchestration small."""
59
61
 
62
+ register_project_agent_memory_routes(application, store, Actor)
63
+
60
64
  @application.put('/v1/project-agents', response_model=ProjectAgentRecord)
61
65
  async def upsert_project_agent(
62
66
  request: ProjectAgentUpsert,
@@ -233,8 +237,17 @@ def register_project_agent_routes(
233
237
  task_id: str,
234
238
  actor: Actor,
235
239
  personal_space_id: Annotated[str, Query(min_length=1, max_length=128)],
240
+ personal_project_id: Annotated[
241
+ str | None,
242
+ Query(min_length=1, max_length=128),
243
+ ] = None,
236
244
  ) -> ProjectAgentTaskRecord:
237
- return await store.get_project_agent_task(actor, personal_space_id, task_id)
245
+ return await store.get_project_agent_task(
246
+ actor,
247
+ personal_space_id,
248
+ task_id,
249
+ personal_project_id=personal_project_id,
250
+ )
238
251
 
239
252
  @application.post(
240
253
  '/v1/project-agent-tasks/{task_id}/events',
@@ -259,6 +272,10 @@ def register_project_agent_routes(
259
272
  personal_space_id: Annotated[str, Query(min_length=1, max_length=128)],
260
273
  from_date: Annotated[date, Query(alias='from')],
261
274
  to_date: Annotated[date, Query(alias='to')],
275
+ personal_project_id: Annotated[
276
+ str | None,
277
+ Query(min_length=1, max_length=128),
278
+ ] = None,
262
279
  ) -> ProjectAgentActivityResult:
263
280
  return await store.get_project_agent_activity(
264
281
  actor,
@@ -266,6 +283,7 @@ def register_project_agent_routes(
266
283
  agent_id,
267
284
  from_date,
268
285
  to_date,
286
+ personal_project_id=personal_project_id,
269
287
  )
270
288
 
271
289
  @application.get(
@@ -296,6 +314,7 @@ def register_project_agent_routes(
296
314
  @application.get(
297
315
  '/v1/project-agent-recruitment-policy',
298
316
  response_model=ProjectAgentRecruitmentPolicyRecord,
317
+ deprecated=True,
299
318
  )
300
319
  async def get_project_agent_recruitment_policy(
301
320
  actor: Actor,
@@ -309,6 +328,7 @@ def register_project_agent_routes(
309
328
  @application.put(
310
329
  '/v1/project-agent-recruitment-policy',
311
330
  response_model=ProjectAgentRecruitmentPolicyRecord,
331
+ deprecated=True,
312
332
  )
313
333
  async def update_project_agent_recruitment_policy(
314
334
  request: ProjectAgentRecruitmentPolicyUpdate,
@@ -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,
@@ -42,6 +46,7 @@ ProjectAgentRoutingOutcome = Literal[
42
46
  ProjectAgentRoutingReason = Literal[
43
47
  'explicit_agent',
44
48
  'project_continuity',
49
+ 'project_default',
45
50
  'sole_active_assignment',
46
51
  'manual_agent_selection',
47
52
  'exact_work_kind',
@@ -72,6 +77,16 @@ ProjectAgentOptimizationPriority = Literal[
72
77
  'token_and_cost',
73
78
  'time',
74
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
+ }
75
90
 
76
91
 
77
92
  def _unique_text(value: list[str], label: str) -> list[str]:
@@ -83,6 +98,19 @@ def _unique_text(value: list[str], label: str) -> list[str]:
83
98
  return normalized
84
99
 
85
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
+
86
114
  class ProjectAgentParallelPlan(StrictModel):
87
115
  enabled: bool = False
88
116
  independent_verification: bool = False
@@ -120,6 +148,10 @@ class ProjectAgentTaskSubmit(StrictModel):
120
148
  objective: str = Field(min_length=1, max_length=4096)
121
149
  work_kind: str = Field(min_length=1, max_length=128)
122
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
+ )
123
155
  duration: ProjectAgentTaskDuration = 'ongoing'
124
156
  staffing_intent: ProjectAgentStaffingIntent = 'reuse_preferred'
125
157
  lead_agent_id: str | None = Field(default=None, min_length=1, max_length=128)
@@ -143,6 +175,14 @@ class ProjectAgentTaskSubmit(StrictModel):
143
175
  def validate_capabilities(cls, value: list[str]) -> list[str]:
144
176
  return _unique_text(value, 'required capabilities')
145
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
+
146
186
  @field_validator('collaborator_agent_ids')
147
187
  @classmethod
148
188
  def validate_collaborators(cls, value: list[str]) -> list[str]:
@@ -185,6 +225,80 @@ class ProjectAgentTaskParticipantRecord(StrictModel):
185
225
  ended_at: datetime | None = None
186
226
 
187
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
+
188
302
  class ProjectAgentTaskExecutionSummary(StrictModel):
189
303
  """Auditable execution evidence projected onto a real task participant.
190
304
 
@@ -203,17 +317,31 @@ class ProjectAgentTaskExecutionSummary(StrictModel):
203
317
  source_application: SourceApplication | None = None
204
318
  actual_model_provider: str | None = None
205
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
206
324
  work_summary: str | None = None
207
325
  status: ProjectAgentTaskStatus
208
326
  worker_id: str | None = None
209
327
  worker_label: str | None = None
210
328
  worker_occupation_emoji: str | None = None
329
+ worker_runtime: ProjectAgentWorkerRuntime | None = None
211
330
 
212
331
  @field_validator('occupation_emoji', 'worker_occupation_emoji', mode='before')
213
332
  @classmethod
214
333
  def validate_emojis(cls, value):
215
334
  return validate_emoji_sequence(value, 'occupation emoji')
216
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
+
217
345
 
218
346
  class ProjectAgentTaskEventRecord(StrictModel):
219
347
  event_id: str
@@ -224,6 +352,8 @@ class ProjectAgentTaskEventRecord(StrictModel):
224
352
  summary: str
225
353
  source_application: SourceApplication | None = None
226
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)
227
357
  actual_model_provider: str | None = None
228
358
  actual_model: str | None = None
229
359
  actual_executor_id: str | None = None
@@ -232,32 +362,32 @@ class ProjectAgentTaskEventRecord(StrictModel):
232
362
  executor_selection_reason: str | None = None
233
363
  executor_fallback_reason: str | None = None
234
364
  executor_blocked_reason: str | None = None
235
- worker_id: str | None = Field(
236
- default=None,
237
- validation_alias=AliasChoices('worker_id', 'workerId'),
238
- )
239
- worker_label: str | None = Field(
240
- default=None,
241
- validation_alias=AliasChoices('worker_label', 'workerLabel'),
242
- )
243
- worker_occupation_emoji: str | None = Field(
244
- default=None,
245
- validation_alias=AliasChoices(
246
- 'worker_occupation_emoji',
247
- 'workerOccupationEmoji',
248
- ),
249
- )
250
- worker_status: ProjectAgentTaskStatus | None = Field(
251
- default=None,
252
- validation_alias=AliasChoices('worker_status', 'workerStatus'),
253
- )
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
254
371
  created_at: datetime
255
372
 
373
+ _normalize_worker_aliases = model_validator(mode='before')(
374
+ _normalize_worker_evidence_aliases
375
+ )
376
+
256
377
  @field_validator('worker_occupation_emoji', mode='before')
257
378
  @classmethod
258
379
  def validate_worker_occupation_emoji(cls, value):
259
380
  return validate_emoji_sequence(value, 'worker occupation emoji')
260
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
+
261
391
 
262
392
  class ProjectAgentRoutingDecisionRecord(StrictModel):
263
393
  decision_id: str
@@ -302,6 +432,7 @@ class ProjectAgentTaskRecord(StrictModel):
302
432
  objective: str
303
433
  work_kind: str
304
434
  required_capabilities: list[str]
435
+ executor_capability_hints: list[str] = Field(default_factory=list)
305
436
  duration: ProjectAgentTaskDuration
306
437
  staffing_intent: ProjectAgentStaffingIntent
307
438
  status: ProjectAgentTaskStatus
@@ -366,6 +497,8 @@ class ProjectAgentTaskActivityCreate(StrictModel):
366
497
  actor_kind: Literal['agent', 'human'] = 'agent'
367
498
  source_application: SourceApplication | None = None
368
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)
369
502
  actual_model_provider: str | None = Field(default=None, min_length=1, max_length=128)
370
503
  actual_model: str | None = Field(default=None, min_length=1, max_length=256)
371
504
  actual_executor_id: str | None = Field(default=None, min_length=1, max_length=128)
@@ -393,33 +526,46 @@ class ProjectAgentTaskActivityCreate(StrictModel):
393
526
  default=None,
394
527
  min_length=1,
395
528
  max_length=128,
396
- validation_alias=AliasChoices('worker_id', 'workerId'),
397
529
  )
398
530
  worker_label: str | None = Field(
399
531
  default=None,
400
532
  min_length=1,
401
533
  max_length=160,
402
- validation_alias=AliasChoices('worker_label', 'workerLabel'),
403
534
  )
404
535
  worker_occupation_emoji: str | None = Field(
405
536
  default=None,
406
537
  min_length=1,
407
538
  max_length=32,
408
- validation_alias=AliasChoices(
409
- 'worker_occupation_emoji',
410
- 'workerOccupationEmoji',
411
- ),
412
539
  )
413
- worker_status: ProjectAgentTaskStatus | None = Field(
414
- default=None,
415
- 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
416
546
  )
417
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
+
418
555
  @field_validator('worker_occupation_emoji', mode='before')
419
556
  @classmethod
420
557
  def validate_worker_occupation_emoji(cls, value):
421
558
  return validate_emoji_sequence(value, 'worker occupation emoji')
422
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
+
423
569
  @model_validator(mode='after')
424
570
  def validate_actual_model(self):
425
571
  if self.status in {'completed', 'failed'} and not self.agent_id:
@@ -436,6 +582,24 @@ class ProjectAgentTaskActivityCreate(StrictModel):
436
582
  raise ValueError('actual execution report requires the participating Agent ID')
437
583
  if self.worker_id and not self.agent_id:
438
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
+ )
439
603
  if (self.worker_label or self.worker_occupation_emoji) and not self.worker_id:
440
604
  raise ValueError(
441
605
  'worker label or occupation emoji requires a worker ID'
@@ -460,6 +624,8 @@ class ProjectAgentRecruitmentRecord(StrictModel):
460
624
  confirmation_mode: ProjectAgentRecruitmentConfirmationMode
461
625
  proposed_agent_id: str
462
626
  proposed_profile: ProjectAgentProfile
627
+ participant_role: ProjectAgentParticipantRole = 'lead'
628
+ recruitment_slot: str = Field(default='lead', min_length=1, max_length=128)
463
629
  trigger_source_application: SourceApplication | None = None
464
630
  trigger_source_session_id: str | None = None
465
631
  revision: int = Field(ge=0)
@@ -475,6 +641,7 @@ class ProjectAgentTaskRouteResult(StrictModel):
475
641
  task: ProjectAgentTaskRecord
476
642
  assigned_agent: ProjectAgentRecord | None = None
477
643
  recruitment: ProjectAgentRecruitmentRecord | None = None
644
+ recruitments: list[ProjectAgentRecruitmentRecord] = Field(default_factory=list)
478
645
  decision: Literal[
479
646
  'reused',
480
647
  'recruited',
@@ -490,6 +657,13 @@ class ProjectAgentRecruitmentPolicyRecord(StrictModel):
490
657
  personal_space_id: str
491
658
  confirmation_mode: ProjectAgentRecruitmentConfirmationMode = 'automatic'
492
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
+ )
493
667
 
494
668
 
495
669
  class ProjectAgentRecruitmentPolicyUpdate(StrictModel):
@@ -521,12 +695,26 @@ class ProjectAgentActivityTask(StrictModel):
521
695
  actual_model_provider: str | None = None
522
696
  actual_model: str | None = None
523
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
524
703
  matched_executor_rule_id: str | None = None
525
704
  worker_id: str | None = None
526
705
  worker_label: str | None = None
527
706
  worker_occupation_emoji: str | None = None
528
707
  worker_status: ProjectAgentTaskStatus | None = None
529
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
+
530
718
 
531
719
  class ProjectAgentActivityDay(StrictModel):
532
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,