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
@@ -0,0 +1,458 @@
1
+ """Task row reconstruction, terminal cleanup, and route-result presentation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+
8
+ from .project_agent_models import ProjectAgentModelStrategy, ProjectAgentProfile
9
+ from .project_agent_task_models import (
10
+ ProjectAgentRoutingDecisionRecord,
11
+ ProjectAgentTaskExecutionSummary,
12
+ ProjectAgentTaskEventRecord,
13
+ ProjectAgentTaskParticipantRecord,
14
+ ProjectAgentTaskRecord,
15
+ ProjectAgentTaskRouteResult,
16
+ )
17
+ from .provider_values import native_datetime
18
+
19
+
20
+ TERMINAL_TASK_STATUSES = {'completed', 'failed', 'cancelled'}
21
+
22
+
23
+ class StoreProjectAgentTaskReads:
24
+ """Read durable task history and derive client-facing route results."""
25
+
26
+ async def _find_task_row(self, personal_space_id, task_id):
27
+ records, _, _ = await self.runtime.driver.execute_query(
28
+ self._task_read_query('WHERE task.task_id = $task_id'),
29
+ personal_space_id=personal_space_id,
30
+ task_id=task_id,
31
+ routing_='r',
32
+ )
33
+ return records[0] if records else None
34
+
35
+ @staticmethod
36
+ def _task_read_query(where_clause):
37
+ return f'''
38
+ MATCH (space:FuliSpace {{id: $personal_space_id, kind: 'personal'}})-
39
+ [:HAS_PROJECT_AGENT_TASK]->
40
+ (task:FuliProjectAgentTask {{
41
+ personal_space_id: $personal_space_id
42
+ }})
43
+ {where_clause}
44
+ OPTIONAL MATCH (task)-[participant:HAS_PARTICIPANT]->
45
+ (participant_agent:FuliProjectAgent)
46
+ WITH task, collect(DISTINCT {{
47
+ agent_id: participant_agent.agent_id,
48
+ agent_name: participant_agent.name,
49
+ occupation_emoji: participant_agent.occupation_emoji,
50
+ role: participant.role,
51
+ status: participant.status,
52
+ assignment_summary: participant.assignment_summary,
53
+ profile_json: participant_agent.profile_json,
54
+ joined_at: participant.joined_at,
55
+ updated_at: participant.updated_at,
56
+ ended_at: participant.ended_at
57
+ }}) AS participant_rows
58
+ OPTIONAL MATCH (task)-[:HAS_TASK_EVENT]->
59
+ (event:FuliProjectAgentTaskEvent)
60
+ WITH task, participant_rows, collect(DISTINCT event) AS event_rows
61
+ OPTIONAL MATCH (task)-[:ROUTED_BY]->
62
+ (decision:FuliProjectAgentRoutingDecision)
63
+ RETURN task, participant_rows, event_rows, decision
64
+ '''
65
+
66
+ def _task_from_row(self, row):
67
+ raw = dict(row['task'])
68
+ decision_raw = dict(row['decision']) if row.get('decision') else {}
69
+ if decision_raw.get('decision_json'):
70
+ decision = ProjectAgentRoutingDecisionRecord.model_validate_json(
71
+ decision_raw['decision_json']
72
+ )
73
+ else:
74
+ decision = ProjectAgentRoutingDecisionRecord(
75
+ decision_id=decision_raw.get('decision_id') or 'legacy',
76
+ task_id=raw['task_id'],
77
+ coordinator_agent_id=raw['coordinator_agent_id'],
78
+ complexity=raw.get('complexity') or 'standard',
79
+ complexity_basis=list(raw.get('complexity_basis') or []),
80
+ selected_model_strategy=(
81
+ ProjectAgentModelStrategy.model_validate_json(
82
+ raw['effective_model_strategy_json']
83
+ )
84
+ if raw.get('effective_model_strategy_json') else None
85
+ ),
86
+ model_strategy_source=raw.get('model_strategy_source') or 'coordinator',
87
+ outcome=raw.get('routing_outcome') or 'unassigned',
88
+ reason=raw.get('routing_reason') or 'no_match',
89
+ match_basis=list(raw.get('match_basis') or []),
90
+ created_at=native_datetime(raw['created_at']),
91
+ )
92
+ events = []
93
+ for value in row.get('event_rows') or []:
94
+ if value is None:
95
+ continue
96
+ event = dict(value)
97
+ if not event.get('event_id'):
98
+ continue
99
+ fields = {
100
+ 'event_id': event['event_id'],
101
+ 'task_id': event['task_id'],
102
+ 'agent_id': event.get('agent_id'),
103
+ 'status': event['status'],
104
+ 'actor_kind': event.get('actor_kind') or 'system',
105
+ 'summary': event['summary'],
106
+ 'source_application': event.get('source_application'),
107
+ 'source_session_id': event.get('source_session_id'),
108
+ 'actual_model_provider': event.get('actual_model_provider'),
109
+ 'actual_model': event.get('actual_model'),
110
+ 'created_at': native_datetime(event['created_at']),
111
+ }
112
+ for name in (
113
+ 'actual_executor_id',
114
+ 'matched_executor_rule_id',
115
+ 'executor_selection_reason',
116
+ 'executor_fallback_reason',
117
+ 'executor_blocked_reason',
118
+ 'worker_id',
119
+ 'worker_label',
120
+ 'worker_occupation_emoji',
121
+ 'worker_status',
122
+ ):
123
+ if name in ProjectAgentTaskEventRecord.model_fields:
124
+ fields[name] = event.get(name)
125
+ events.append(ProjectAgentTaskEventRecord(**fields))
126
+ events.sort(key=lambda item: item.created_at)
127
+ fields = {
128
+ 'task_id': raw['task_id'],
129
+ 'personal_space_id': raw['personal_space_id'],
130
+ 'personal_project_id': raw['personal_project_id'],
131
+ 'title': raw['title'],
132
+ 'objective': raw['objective'],
133
+ 'work_kind': raw['work_kind'],
134
+ 'required_capabilities': list(raw.get('required_capabilities') or []),
135
+ 'duration': raw.get('duration') or 'ongoing',
136
+ 'staffing_intent': raw.get('staffing_intent') or 'reuse_preferred',
137
+ 'status': raw['status'],
138
+ 'revision': int(raw.get('revision') or 0),
139
+ 'routing_outcome': raw.get('routing_outcome') or 'unassigned',
140
+ 'routing_reason': raw.get('routing_reason') or 'no_match',
141
+ 'routing_explanation': raw.get('routing_explanation') or '',
142
+ 'match_basis': list(raw.get('match_basis') or []),
143
+ 'coordinator_agent_id': raw['coordinator_agent_id'],
144
+ 'complexity': raw.get('complexity') or 'standard',
145
+ 'complexity_basis': list(raw.get('complexity_basis') or []),
146
+ 'routing_decision': decision,
147
+ 'lead_agent_id': raw.get('lead_agent_id'),
148
+ 'participants': [
149
+ ProjectAgentTaskParticipantRecord(
150
+ agent_id=value['agent_id'],
151
+ role=value['role'],
152
+ status=value['status'],
153
+ assignment_summary=value.get('assignment_summary'),
154
+ joined_at=native_datetime(value['joined_at']),
155
+ updated_at=native_datetime(value['updated_at']),
156
+ ended_at=native_datetime(value.get('ended_at')),
157
+ )
158
+ for value in self._participant_maps(row)
159
+ ],
160
+ 'effective_model_strategy': (
161
+ ProjectAgentModelStrategy.model_validate_json(
162
+ raw['effective_model_strategy_json']
163
+ )
164
+ if raw.get('effective_model_strategy_json') else None
165
+ ),
166
+ 'model_strategy_source': raw.get('model_strategy_source') or 'coordinator',
167
+ 'hr_agent_id': raw.get('hr_agent_id'),
168
+ 'recruitment_id': raw.get('recruitment_id'),
169
+ 'source_application': raw.get('source_application'),
170
+ 'source_session_id': raw.get('source_session_id'),
171
+ 'result_summary': raw.get('result_summary'),
172
+ 'failure_reason': raw.get('failure_reason'),
173
+ 'created_at': native_datetime(raw['created_at']),
174
+ 'updated_at': native_datetime(raw['updated_at']),
175
+ 'completed_at': native_datetime(raw.get('completed_at')),
176
+ 'events': events,
177
+ }
178
+ fields['execution_summary'] = self._execution_summary(row, events)
179
+ executor_raw = json.loads(raw['executor_decision_json']) \
180
+ if raw.get('executor_decision_json') else None
181
+ executor_policy_raw = raw.get('executor_policy_json')
182
+ if executor_policy_raw:
183
+ fields['executor_policy'] = json.loads(executor_policy_raw)
184
+ for name, value in {
185
+ 'selected_executor_id': raw.get('selected_executor_id'),
186
+ 'actual_executor_id': raw.get('actual_executor_id'),
187
+ 'actual_run_id': raw.get('actual_run_id'),
188
+ 'actual_model_provider': raw.get('actual_model_provider'),
189
+ 'actual_model': raw.get('actual_model'),
190
+ 'matched_executor_rule_id': raw.get('matched_executor_rule_id'),
191
+ 'executor_selection_reason': raw.get('executor_selection_reason'),
192
+ 'executor_fallback_outcome': raw.get('executor_fallback_outcome'),
193
+ 'executor_fallback_reason': raw.get('executor_fallback_reason'),
194
+ 'executor_blocked_reason': raw.get('executor_blocked_reason'),
195
+ 'executor_decision': executor_raw,
196
+ }.items():
197
+ if name in ProjectAgentTaskRecord.model_fields:
198
+ fields[name] = value
199
+ return ProjectAgentTaskRecord(**fields)
200
+
201
+ @staticmethod
202
+ def _participant_maps(row):
203
+ return [
204
+ dict(value)
205
+ for value in (row.get('participant_rows') or [])
206
+ if value and value.get('agent_id') and value.get('role')
207
+ ]
208
+
209
+ @staticmethod
210
+ def _participant_profile(participant):
211
+ """Return the durable display fields, tolerating legacy profiles."""
212
+
213
+ profile_json = participant.get('profile_json')
214
+ profile = None
215
+ if profile_json:
216
+ try:
217
+ profile = ProjectAgentProfile.model_validate_json(profile_json)
218
+ except (TypeError, ValueError):
219
+ # A malformed legacy profile must not make an otherwise
220
+ # readable task invent identity data or fail the whole read.
221
+ profile = None
222
+ agent_name = (
223
+ profile.name if profile else participant.get('agent_name')
224
+ )
225
+ occupation_emoji = (
226
+ profile.occupation_emoji if profile else None
227
+ ) or participant.get('occupation_emoji')
228
+ if profile is None and profile_json:
229
+ try:
230
+ legacy = json.loads(profile_json)
231
+ except (TypeError, ValueError):
232
+ legacy = {}
233
+ agent_name = agent_name or legacy.get('name')
234
+ occupation_emoji = occupation_emoji or legacy.get('occupationEmoji')
235
+ occupation_emoji = occupation_emoji or legacy.get('occupation_emoji')
236
+ return agent_name, occupation_emoji
237
+
238
+ @classmethod
239
+ def _execution_summary(cls, row, events):
240
+ """Project only observed events onto identities actually assigned."""
241
+
242
+ result = []
243
+ participants = cls._participant_maps(row)
244
+ task_raw = dict(row.get('task') or {})
245
+ task_fallback = task_raw if len(participants) == 1 else {}
246
+ for participant in participants:
247
+ agent_id = participant['agent_id']
248
+ participant_events = [
249
+ event for event in events
250
+ if event.agent_id == agent_id
251
+ and cls._is_execution_event(event)
252
+ ]
253
+ agent_name, occupation_emoji = cls._participant_profile(participant)
254
+ worker_groups = {}
255
+ for event in participant_events:
256
+ if event.worker_id:
257
+ worker_groups.setdefault(event.worker_id, []).append(event)
258
+
259
+ # A worker id is an explicit observation of a concrete execution
260
+ # worker. Preserve one row per observed worker, even when several
261
+ # workers share one durable Agent identity.
262
+ if worker_groups:
263
+ for worker_id, worker_events in worker_groups.items():
264
+ result.append(
265
+ cls._execution_summary_row(
266
+ participant,
267
+ agent_name,
268
+ occupation_emoji,
269
+ worker_events,
270
+ worker_id=worker_id,
271
+ task_fallback={},
272
+ )
273
+ )
274
+ continue
275
+
276
+ # A configured participant is not execution evidence. Legacy
277
+ # clients may omit worker_id, but they must still have recorded an
278
+ # Agent-linked running/terminal event or concrete executor/model
279
+ # evidence before a summary row exists.
280
+ if not participant_events:
281
+ continue
282
+ result.append(
283
+ cls._execution_summary_row(
284
+ participant,
285
+ agent_name,
286
+ occupation_emoji,
287
+ participant_events,
288
+ task_fallback=task_fallback,
289
+ )
290
+ )
291
+ return result
292
+
293
+ @staticmethod
294
+ def _is_execution_event(event):
295
+ return bool(
296
+ event.worker_id
297
+ or event.worker_status
298
+ or event.actual_executor_id
299
+ or event.actual_model_provider
300
+ or event.actual_model
301
+ or event.status in {
302
+ 'running', 'paused', 'awaiting_review', 'blocked',
303
+ 'completed', 'failed', 'cancelled',
304
+ }
305
+ )
306
+
307
+ @staticmethod
308
+ def _execution_summary_row(
309
+ participant,
310
+ agent_name,
311
+ occupation_emoji,
312
+ events,
313
+ *,
314
+ worker_id=None,
315
+ task_fallback=None,
316
+ ):
317
+ task_fallback = task_fallback or {}
318
+ latest = events[-1] if events else None
319
+
320
+ def latest_value(name):
321
+ return next(
322
+ (
323
+ getattr(event, name)
324
+ for event in reversed(events)
325
+ if getattr(event, name, None) is not None
326
+ ),
327
+ None,
328
+ )
329
+
330
+ actual_executor_id = (
331
+ latest_value('actual_executor_id')
332
+ or task_fallback.get('actual_executor_id')
333
+ )
334
+ worker_label = latest_value('worker_label')
335
+ worker_occupation_emoji = latest_value('worker_occupation_emoji')
336
+ worker_status = latest_value('worker_status')
337
+ status = worker_status or (
338
+ latest.status
339
+ if latest
340
+ else participant['status']
341
+ )
342
+ return ProjectAgentTaskExecutionSummary(
343
+ agent_id=participant['agent_id'],
344
+ agent_name=agent_name,
345
+ occupation_emoji=occupation_emoji,
346
+ participant_role=participant['role'],
347
+ executor=actual_executor_id,
348
+ executor_id=actual_executor_id,
349
+ source_application=latest_value('source_application'),
350
+ actual_model_provider=(
351
+ latest_value('actual_model_provider')
352
+ or task_fallback.get('actual_model_provider')
353
+ ),
354
+ actual_model=(
355
+ latest_value('actual_model')
356
+ or task_fallback.get('actual_model')
357
+ ),
358
+ work_summary=(
359
+ latest.summary
360
+ if latest
361
+ else task_fallback.get('result_summary')
362
+ or task_fallback.get('failure_reason')
363
+ ),
364
+ status=status,
365
+ worker_id=worker_id,
366
+ worker_label=worker_label,
367
+ worker_occupation_emoji=worker_occupation_emoji,
368
+ )
369
+
370
+ async def _archive_finished_temporary_agent(self, request, raw_task, updated_at):
371
+ if request.status not in TERMINAL_TASK_STATUSES:
372
+ return
373
+ await self.runtime.driver.execute_query(
374
+ '''
375
+ MATCH (space:FuliSpace {
376
+ id: $personal_space_id, kind: 'personal'
377
+ })-[:HAS_PROJECT_AGENT_TASK]->
378
+ (task:FuliProjectAgentTask {task_id: $task_id})-
379
+ [:HAS_PARTICIPANT]->
380
+ (agent:FuliProjectAgent {
381
+ agent_type: 'temporary',
382
+ temporary_task_id: $task_id
383
+ })
384
+ SET agent.status = 'archived',
385
+ agent.updated_at = $updated_at
386
+ WITH space, agent
387
+ MATCH (space)-[:CONTAINS_PROJECT]->(:FuliPersonalProject)-
388
+ [:HAS_PROJECT_AGENT_ASSIGNMENT]->
389
+ (assignment:FuliProjectAgentAssignment {status: 'active'})-
390
+ [:ASSIGNED_AGENT]->(agent)
391
+ SET assignment.status = 'ended',
392
+ assignment.end_reason = 'temporary task reached terminal state',
393
+ assignment.ended_at = $updated_at,
394
+ assignment.updated_at = $updated_at,
395
+ assignment.revision = assignment.revision + 1
396
+ ''',
397
+ personal_space_id=request.personal_space_id,
398
+ task_id=request.task_id,
399
+ updated_at=updated_at,
400
+ )
401
+
402
+ @staticmethod
403
+ def _routing_explanation(reason, match_basis, executor_decision):
404
+ basis = '; '.join(match_basis) if match_basis else 'no exact assignment match'
405
+ value = f'{reason}: {basis}'
406
+ if executor_decision:
407
+ raw = (
408
+ executor_decision.model_dump(mode='json')
409
+ if hasattr(executor_decision, 'model_dump')
410
+ else executor_decision
411
+ )
412
+ executor_reason = raw.get('selection_reason') or raw.get('reason')
413
+ if executor_reason:
414
+ value += f'; executor: {executor_reason}'
415
+ return value
416
+
417
+ @staticmethod
418
+ def _payload_hash(request):
419
+ payload = request.model_dump(mode='json', exclude={'idempotency_key'})
420
+ return hashlib.sha256(
421
+ json.dumps(payload, sort_keys=True, separators=(',', ':')).encode()
422
+ ).hexdigest()
423
+
424
+ def _route_result(self, task, recruitment, *, assigned_agent=None):
425
+ if recruitment and recruitment.status == 'fulfilled':
426
+ decision = 'recruited'
427
+ elif recruitment and recruitment.status == 'awaiting_confirmation':
428
+ decision = 'awaiting_confirmation'
429
+ elif task.routing_outcome == 'assigned_existing':
430
+ decision = 'reused'
431
+ elif task.routing_outcome == 'unassigned':
432
+ decision = 'unassigned'
433
+ else:
434
+ decision = 'blocked'
435
+ notice = None
436
+ must_disclose = bool(
437
+ recruitment
438
+ and recruitment.confirmation_mode == 'automatic'
439
+ and recruitment.status == 'fulfilled'
440
+ )
441
+ if must_disclose:
442
+ notice = (
443
+ f'HR {recruitment.hr_agent_id} recruited '
444
+ f'{recruitment.proposed_profile.name} as a '
445
+ f'{recruitment.position_kind} Agent for '
446
+ f'{recruitment.proposed_profile.responsibility}. '
447
+ f'Reason: {recruitment.reason}. '
448
+ f'Trigger: {recruitment.trigger_source_application or "unspecified"}; '
449
+ f'time: {recruitment.fulfilled_at.isoformat() if recruitment.fulfilled_at else "pending"}.'
450
+ )
451
+ return ProjectAgentTaskRouteResult(
452
+ task=task,
453
+ assigned_agent=assigned_agent,
454
+ recruitment=recruitment,
455
+ decision=decision,
456
+ must_disclose_recruitment=must_disclose,
457
+ client_notice=notice,
458
+ )