fuli-context 0.7.4-beta.3 → 0.7.6
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.
- package/README.md +77 -6
- package/README.zh-CN.md +69 -6
- package/compose.graphiti.yml +6 -6
- package/dist/web/assets/AboutPage-sLV9WChi.js +1 -0
- package/dist/web/assets/ConnectionsPage-BWn4ywGJ.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-CZ5ziPrD.js +1 -0
- package/dist/web/assets/KnowledgeInspector-CRQWOaos.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-Dfj3vIpU.js +1 -0
- package/dist/web/assets/KnowledgePage-BRYC9S5Y.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-ChEB5Bwk.js +4 -0
- package/dist/web/assets/OverviewPage-Cd66M0RT.js +1 -0
- package/dist/web/assets/PersonalProfilePage-RCFHL0Wz.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-DEh8XWby.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-Crh7PrXa.css +1 -0
- package/dist/web/assets/ProjectAgentsPage-DPnaH-TR.js +11 -0
- package/dist/web/assets/PublicProjectsPage-mstPEjHe.js +1 -0
- package/dist/web/assets/ReviewPage-6Y1SmfZy.js +1 -0
- package/dist/web/assets/SearchableSelect-D3HM8YF8.js +1 -0
- package/dist/web/assets/SettingsPage--LdTMUm7.js +1 -0
- package/dist/web/assets/SettingsPage-CwZp2ka4.css +1 -0
- package/dist/web/assets/WritingTastePage-CIpKswvs.js +1 -0
- package/dist/web/assets/{console-D_2zC4lm.js → console-BT1P2Db4.js} +1 -1
- package/dist/web/assets/{conversation-launcher-settings-DQmMH2j7.js → conversation-launcher-settings-DrJEotdf.js} +1 -1
- package/dist/web/assets/i18n-js6gfc7j.js +9 -0
- package/dist/web/assets/{identity-DmGcY3Y6.js → identity-CfDfanpa.js} +1 -1
- package/dist/web/assets/index-BnH5mcZx.css +1 -0
- package/dist/web/assets/index-GqJf1xqg.js +2 -0
- package/dist/web/assets/{model-CVoOdRsL.js → model-DJkLAJsg.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-CBNg4CdH.js → useApi-CROJJdhE-BdSti7Y2.js} +1 -1
- package/dist/web/assets/vue-router-Kk67EaCP.js +1 -0
- package/dist/web/index.html +6 -6
- package/graph-provider/fuli_graph/app.py +8 -0
- package/graph-provider/fuli_graph/collaboration_context.py +69 -6
- package/graph-provider/fuli_graph/knowledge_search.py +84 -5
- package/graph-provider/fuli_graph/model_validation.py +130 -0
- package/graph-provider/fuli_graph/models.py +24 -5
- package/graph-provider/fuli_graph/project_agent_access.py +54 -0
- package/graph-provider/fuli_graph/project_agent_coordination_models.py +22 -0
- package/graph-provider/fuli_graph/project_agent_executor_models.py +635 -0
- package/graph-provider/fuli_graph/project_agent_models.py +278 -0
- package/graph-provider/fuli_graph/project_agent_routes.py +626 -0
- package/graph-provider/fuli_graph/project_agent_task_models.py +545 -0
- package/graph-provider/fuli_graph/runtime.py +259 -0
- package/graph-provider/fuli_graph/search_projection.py +3 -0
- package/graph-provider/fuli_graph/store.py +6 -0
- package/graph-provider/fuli_graph/store_knowledge.py +62 -9
- package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +122 -0
- package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +756 -0
- package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +601 -0
- package/graph-provider/fuli_graph/store_project_agent_executors.py +1087 -0
- package/graph-provider/fuli_graph/store_project_agent_task_reads.py +458 -0
- package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +705 -0
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +586 -0
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +1151 -0
- package/graph-provider/fuli_graph/store_project_agents.py +965 -0
- package/graph-provider/pyproject.toml +2 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/capturing-session-knowledge/SKILL.md +39 -0
- package/src/adaptive-runtime/executor-pool.js +127 -0
- package/src/adaptive-runtime/graph-services.js +166 -0
- package/src/adaptive-runtime/lease-client.js +335 -0
- package/src/adaptive-runtime/runtime-broker.js +318 -0
- package/src/adaptive-runtime/settings.js +111 -0
- package/src/adaptive-runtime/state.js +74 -0
- package/src/agent-tools/graph-definitions.js +419 -0
- package/src/agent-tools/graph-handlers.js +65 -0
- package/src/agent-tools/project-agent-definitions.js +307 -0
- package/src/backup/graph-backup.js +289 -0
- package/src/backup/identity-reconciliation.js +208 -0
- package/src/backup/runtime-adapters.js +269 -0
- package/src/cli/command-registry.js +6 -2
- package/src/cli/graph-data-command.js +112 -0
- package/src/cli/local-runtime-command.js +16 -2
- package/src/cli/setup-command.js +19 -3
- package/src/cli/update-command.js +4 -0
- package/src/cli.js +6 -0
- package/src/graphiti/collaboration-preference-projection.js +29 -1
- package/src/graphiti/collaboration-preference-workflow.js +121 -0
- package/src/graphiti/federated-application.js +53 -90
- package/src/graphiti/project-agent-control-plane.js +275 -0
- package/src/graphiti/project-agent-mapping.js +1024 -0
- package/src/graphiti/project-agent-workflows.js +576 -0
- package/src/graphiti/provider-client.js +282 -1
- package/src/http/api-router.js +63 -9
- package/src/http/graph-api-router.js +290 -0
- package/src/local-runtime/lifecycle.js +46 -10
- package/src/mcp/context-resources.js +15 -6
- package/src/mcp/create-mcp-server.js +51 -16
- package/src/mcp/instructions.js +7 -5
- package/src/mcp/runtime.js +17 -5
- package/src/mcp/session-id.js +38 -1
- package/src/mcp/tool-annotations.js +71 -0
- package/src/mcp-server.js +23 -6
- package/src/native-runtime/runtime.js +717 -0
- package/src/server.js +6 -2
- package/src/setup/agent-installation-status.js +7 -1
- package/src/setup/agents.js +3 -1
- package/src/setup/claude-code-config.js +3 -1
- package/src/setup/codex-bootstrap.js +11 -10
- package/src/setup/codex-config.js +7 -1
- package/src/setup/cursor-config.js +3 -1
- package/src/setup/graph-runtime.js +158 -13
- package/src/setup/neo4j-memory-profile.js +43 -0
- package/src/setup/options.js +38 -2
- package/src/setup/paths.js +14 -0
- package/src/setup/setup.js +37 -1
- package/src/system/resource-monitor.js +181 -16
- package/src/system/runtime-settings.js +10 -1
- package/src/system/system-service.js +27 -2
- package/dist/web/assets/AboutPage-Dpf5fRWv.js +0 -1
- package/dist/web/assets/ConnectionsPage-D21aUDCP.js +0 -1
- package/dist/web/assets/KnowledgeEditDialog-u7lpfN3j.js +0 -1
- package/dist/web/assets/KnowledgeInspector-k4xnHtXd.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-Bdj7Rn84.js +0 -1
- package/dist/web/assets/KnowledgePage-UHVAbC1W.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-Lq3d-MeC.js +0 -4
- package/dist/web/assets/OverviewPage-CycAgA1V.js +0 -1
- package/dist/web/assets/PersonalProfilePage-U9oIV5FT.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-C5RpXKcA.js +0 -1
- package/dist/web/assets/PublicProjectsPage-DiWxm35E.js +0 -1
- package/dist/web/assets/ReviewPage-jxRfGSJK.js +0 -1
- package/dist/web/assets/SearchableSelect-B3-5IPup.js +0 -1
- package/dist/web/assets/SettingsPage-BqfGDOho.js +0 -1
- package/dist/web/assets/SettingsPage-CAfz4qiV.css +0 -1
- package/dist/web/assets/WritingTastePage-BJxhMqJR.js +0 -1
- package/dist/web/assets/i18n-CwqXDI1C.js +0 -5
- package/dist/web/assets/index-IUGLt4lG.js +0 -2
- package/dist/web/assets/index-X2j29j8i.css +0 -1
- 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
|
+
)
|