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.
- package/README.md +69 -13
- package/README.zh-CN.md +60 -14
- package/dist/web/assets/{AboutPage-sLV9WChi.js → AboutPage-9CVX91tr.js} +1 -1
- package/dist/web/assets/{ConnectionsPage-BWn4ywGJ.js → ConnectionsPage-CWbCVFdv.js} +1 -1
- package/dist/web/assets/EmployeeRecruitDialog-CQgNpGJ_.js +1 -0
- package/dist/web/assets/EmployeeRecruitDialog-CfFlyKSh.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-CWg4WJhr.css +1 -0
- package/dist/web/assets/EmployeeWorkbenchPage-CwU4qBgD.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-B6TGAcFH.js +1 -0
- package/dist/web/assets/{KnowledgeEditDialog-DWUV_NPp.css → KnowledgeEditDialog-iv-dgUeD.css} +1 -1
- package/dist/web/assets/{KnowledgeInspector-CRQWOaos.js → KnowledgeInspector-M2pLY0bR.js} +1 -1
- package/dist/web/assets/{KnowledgeOrganizerPage-Dfj3vIpU.js → KnowledgeOrganizerPage-DzeAlk9b.js} +1 -1
- package/dist/web/assets/KnowledgePage-BJu9w0fF.js +1 -0
- package/dist/web/assets/{KnowledgeWorkspace-ChEB5Bwk.js → KnowledgeWorkspace-CCt8rnBb.js} +1 -1
- package/dist/web/assets/{OverviewPage-Cd66M0RT.js → OverviewPage-CZswVqui.js} +1 -1
- package/dist/web/assets/{PersonalProfilePage-RCFHL0Wz.js → PersonalProfilePage-f1nSF6kx.js} +1 -1
- package/dist/web/assets/PersonalProjectsPage-D__eJf0t.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-C3vW6wmk.css +1 -0
- package/dist/web/assets/ProjectAgentsPage-LrwagMGj.js +11 -0
- package/dist/web/assets/{PublicProjectsPage-mstPEjHe.js → PublicProjectsPage-BdC2Qk4n.js} +1 -1
- package/dist/web/assets/{ReviewPage-6Y1SmfZy.js → ReviewPage-Cbgl8_Si.js} +1 -1
- package/dist/web/assets/SearchableSelect-ERm-k4KI.js +1 -0
- package/dist/web/assets/{SettingsPage--LdTMUm7.js → SettingsPage-DWso4x29.js} +1 -1
- package/dist/web/assets/{WritingTastePage-CIpKswvs.js → WritingTastePage-CFpCjmIY.js} +1 -1
- package/dist/web/assets/catalog-WGLn7ERJ.js +1 -0
- package/dist/web/assets/{console-BT1P2Db4.js → console-DQgu0JIA.js} +1 -1
- package/dist/web/assets/conversation-launcher-settings-0KtlG_LV.js +1 -0
- package/dist/web/assets/{i18n-js6gfc7j.js → i18n-ByRiA3pJ.js} +3 -3
- package/dist/web/assets/{identity-CfDfanpa.js → identity-CxdPRMwH.js} +1 -1
- package/dist/web/assets/index-JAZe6lfX.js +2 -0
- package/dist/web/assets/index-wcXzzggp.css +1 -0
- package/dist/web/assets/{model-DJkLAJsg.js → model-Db4-KefQ.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-BdSti7Y2.js → useApi-CROJJdhE-CePMjc08.js} +1 -1
- package/dist/web/assets/{vue-router-Kk67EaCP.js → vue-router-CzX-hn3P.js} +1 -1
- package/dist/web/index.html +7 -6
- package/docs/employee-agents.md +99 -0
- package/docs/project-agent-memory.md +479 -0
- package/graph-provider/fuli_graph/knowledge_audit_models.py +171 -0
- package/graph-provider/fuli_graph/model_base.py +5 -0
- package/graph-provider/fuli_graph/model_validation.py +32 -0
- package/graph-provider/fuli_graph/models.py +26 -170
- package/graph-provider/fuli_graph/project_agent_context_models.py +29 -0
- package/graph-provider/fuli_graph/project_agent_executor_models.py +35 -7
- package/graph-provider/fuli_graph/project_agent_memory_models.py +79 -0
- package/graph-provider/fuli_graph/project_agent_memory_routes.py +46 -0
- package/graph-provider/fuli_graph/project_agent_models.py +9 -0
- package/graph-provider/fuli_graph/project_agent_recruitment_values.py +48 -0
- package/graph-provider/fuli_graph/project_agent_routes.py +21 -1
- package/graph-provider/fuli_graph/project_agent_task_models.py +217 -29
- package/graph-provider/fuli_graph/runtime.py +37 -1
- package/graph-provider/fuli_graph/store.py +6 -0
- package/graph-provider/fuli_graph/store_project_agent_context.py +65 -0
- package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +175 -11
- package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +33 -10
- package/graph-provider/fuli_graph/store_project_agent_executors.py +181 -14
- package/graph-provider/fuli_graph/store_project_agent_memory.py +174 -0
- package/graph-provider/fuli_graph/store_project_agent_task_activity.py +423 -0
- package/graph-provider/fuli_graph/store_project_agent_task_persistence.py +152 -0
- package/graph-provider/fuli_graph/store_project_agent_task_reads.py +187 -21
- package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +748 -162
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +230 -18
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +261 -468
- package/graph-provider/fuli_graph/store_project_agents.py +214 -46
- package/graph-provider/fuli_graph/store_task_contexts.py +217 -0
- package/graph-provider/fuli_graph/store_transactions.py +40 -0
- package/graph-provider/fuli_graph/task_context_models.py +50 -0
- package/graph-provider/fuli_graph/task_context_routes.py +31 -0
- package/graph-provider/fuli_graph/workflow_observation_models.py +1 -1
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +8 -8
- package/package.json +5 -2
- package/skills/capturing-session-knowledge/SKILL.md +18 -6
- package/src/adaptive-runtime/executor-pool.js +46 -5
- package/src/adaptive-runtime/graph-services.js +10 -3
- package/src/adaptive-runtime/lease-client.js +66 -14
- package/src/adaptive-runtime/runtime-broker.js +24 -9
- package/src/agent-tools/employee-definitions.js +43 -0
- package/src/agent-tools/graph-definitions.js +25 -6
- package/src/agent-tools/graph-handlers.js +11 -0
- package/src/agent-tools/project-agent-definitions.js +35 -4
- package/src/agent-tools/project-agent-memory-definitions.js +36 -0
- package/src/agent-tools.js +6 -2
- package/src/agents/codex/lifecycle-hook.js +107 -0
- package/src/agents/codex/lifecycle-hooks.js +231 -0
- package/src/agents/cursor/lifecycle-hook.js +133 -0
- package/src/agents/cursor/lifecycle-hooks.js +50 -0
- package/src/agents/lifecycle-message.js +19 -0
- package/src/app/agent-request-context.js +23 -0
- package/src/cli/command-registry.js +2 -0
- package/src/cli/employee-command.js +17 -0
- package/src/cli/remote-mcp-command.js +282 -0
- package/src/cli/setup-command.js +5 -0
- package/src/cli/update-command.js +6 -43
- package/src/cli.js +10 -0
- package/src/employees/catalog/jefa.json +21 -0
- package/src/employees/install-package.js +77 -0
- package/src/employees/management-policy.js +88 -0
- package/src/employees/manifest.js +53 -0
- package/src/employees/package-registry.js +107 -0
- package/src/employees/project-selection.js +50 -0
- package/src/employees/recruitment.js +199 -0
- package/src/employees/service.js +83 -0
- package/src/employees/task-entry.js +39 -0
- package/src/graphiti/agent-knowledge-workflows.js +135 -25
- package/src/graphiti/federated-application.js +78 -35
- package/src/graphiti/project-agent-control-plane.js +20 -3
- package/src/graphiti/project-agent-mapping.js +155 -5
- package/src/graphiti/project-agent-memory.js +115 -0
- package/src/graphiti/project-agent-task-entry.js +126 -0
- package/src/graphiti/project-agent-workflows.js +30 -14
- package/src/graphiti/provider-client.js +66 -4
- package/src/http/api-router.js +30 -5
- package/src/http/employee-api-router.js +64 -0
- package/src/http/error-mapping.js +4 -0
- package/src/http/graph-api-router.js +2 -0
- package/src/http/request-policy.js +6 -3
- package/src/http/static-handler.js +4 -3
- package/src/mcp/create-mcp-server.js +98 -37
- package/src/mcp/direct-call.js +38 -0
- package/src/mcp/instructions.js +8 -7
- package/src/mcp/provider-task-context-registry.js +74 -0
- package/src/mcp/remote-http-server.js +761 -0
- package/src/mcp/session-id.js +29 -3
- package/src/mcp/task-context-registry.js +88 -29
- package/src/mcp/tool-annotations.js +10 -2
- package/src/mcp/tool-result.js +8 -3
- package/src/mcp-server.js +20 -8
- package/src/native-runtime/runtime.js +58 -8
- package/src/semantic-version.js +42 -0
- package/src/server.js +76 -28
- package/src/setup/agent-installation-status.js +38 -6
- package/src/setup/agents.js +20 -8
- package/src/setup/claude-code-config.js +55 -13
- package/src/setup/codex-bootstrap.js +11 -10
- package/src/setup/codex-config.js +74 -7
- package/src/setup/config-backup.js +18 -2
- package/src/setup/cursor-config.js +22 -5
- package/src/setup/setup.js +30 -22
- package/src/storage/json-file.js +11 -3
- package/src/system/package-version-checker.js +72 -0
- package/src/system/system-service.js +3 -0
- package/dist/web/assets/KnowledgeEditDialog-CZ5ziPrD.js +0 -1
- package/dist/web/assets/KnowledgePage-BRYC9S5Y.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-DEh8XWby.js +0 -1
- package/dist/web/assets/ProjectAgentsPage-Crh7PrXa.css +0 -1
- package/dist/web/assets/ProjectAgentsPage-DPnaH-TR.js +0 -11
- package/dist/web/assets/SearchableSelect-D3HM8YF8.js +0 -1
- package/dist/web/assets/conversation-launcher-settings-DrJEotdf.js +0 -1
- package/dist/web/assets/index-BnH5mcZx.css +0 -1
- package/dist/web/assets/index-GqJf1xqg.js +0 -2
|
@@ -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
|
+
)
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import math
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
7
8
|
from datetime import datetime
|
|
8
9
|
|
|
9
10
|
from fastapi import HTTPException
|
|
@@ -20,6 +21,7 @@ from .project_agent_executor_models import (
|
|
|
20
21
|
)
|
|
21
22
|
from .project_agent_models import ProjectAgentModelStrategy
|
|
22
23
|
from .provider_values import native_datetime, now_utc, stable_uuid
|
|
24
|
+
from .store_transactions import query_store_transaction
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
_TERMINAL_NEUTRAL = {'cancelled'}
|
|
@@ -58,6 +60,36 @@ def project_agent_executor_outcome_bucket_id(
|
|
|
58
60
|
class StoreProjectAgentExecutorLearning:
|
|
59
61
|
"""Persist explainable evidence without overriding explicit user policy."""
|
|
60
62
|
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _learning_scope(request):
|
|
65
|
+
return {key: getattr(request, key) for key in (
|
|
66
|
+
'personal_space_id', 'personal_project_id', 'work_kind',
|
|
67
|
+
'agent_id', 'executor_id', 'model_strategy')}
|
|
68
|
+
|
|
69
|
+
@asynccontextmanager
|
|
70
|
+
async def _learning_bucket_transaction(self, **scope):
|
|
71
|
+
"""Serialize a bucket before reading its evidence and commit together."""
|
|
72
|
+
strategy_key = project_agent_model_strategy_key(scope['model_strategy'])
|
|
73
|
+
bucket_id = project_agent_executor_outcome_bucket_id(
|
|
74
|
+
self.settings.provider_id, scope['personal_space_id'],
|
|
75
|
+
scope['personal_project_id'], scope['work_kind'], scope['agent_id'],
|
|
76
|
+
scope['executor_id'], strategy_key, bucket_kind='aggregate')
|
|
77
|
+
async with query_store_transaction(self) as scoped:
|
|
78
|
+
await scoped.runtime.driver.execute_query('''
|
|
79
|
+
MERGE (bucket:FuliProjectAgentExecutorOutcomeAggregate {id: $bucket_id})
|
|
80
|
+
ON CREATE SET bucket.aggregate_id = $bucket_id,
|
|
81
|
+
bucket.personal_space_id = $personal_space_id,
|
|
82
|
+
bucket.personal_project_id = $personal_project_id,
|
|
83
|
+
bucket.work_kind = $work_kind,
|
|
84
|
+
bucket.agent_id = $agent_id,
|
|
85
|
+
bucket.executor_id = $executor_id,
|
|
86
|
+
bucket.model_strategy_key = $model_strategy_key
|
|
87
|
+
SET bucket._write_lock = true
|
|
88
|
+
REMOVE bucket._write_lock
|
|
89
|
+
''', bucket_id=bucket_id, model_strategy_key=strategy_key,
|
|
90
|
+
**{key: value for key, value in scope.items() if key != 'model_strategy'})
|
|
91
|
+
yield scoped
|
|
92
|
+
|
|
61
93
|
async def record_project_agent_executor_outcome_evidence(
|
|
62
94
|
self,
|
|
63
95
|
actor: dict,
|
|
@@ -71,6 +103,77 @@ class StoreProjectAgentExecutorLearning:
|
|
|
71
103
|
space,
|
|
72
104
|
request.personal_project_id,
|
|
73
105
|
)
|
|
106
|
+
async with query_store_transaction(self) as scoped:
|
|
107
|
+
# Relationship writes below also lock the task. Match the activity
|
|
108
|
+
# path's task -> bucket order instead of introducing its inverse.
|
|
109
|
+
await scoped.runtime.driver.execute_query('''
|
|
110
|
+
MATCH (task:FuliProjectAgentTask {
|
|
111
|
+
personal_space_id: $personal_space_id,
|
|
112
|
+
personal_project_id: $personal_project_id, task_id: $task_id
|
|
113
|
+
})
|
|
114
|
+
SET task._outcome_write_lock = true
|
|
115
|
+
REMOVE task._outcome_write_lock
|
|
116
|
+
''', **{key: getattr(request, key) for key in (
|
|
117
|
+
'personal_space_id', 'personal_project_id', 'task_id')})
|
|
118
|
+
await scoped._validate_outcome_execution(request)
|
|
119
|
+
async with scoped._learning_bucket_transaction(**self._learning_scope(request)) as bucket:
|
|
120
|
+
return await bucket._record_outcome_evidence(actor, request)
|
|
121
|
+
|
|
122
|
+
async def _validate_outcome_execution(self, request):
|
|
123
|
+
"""Bind credit to recorded work, not a caller-chosen learning bucket.
|
|
124
|
+
|
|
125
|
+
External test/user references remain caller-supplied assertions. Only
|
|
126
|
+
system terminal evidence is derived from a persisted task event.
|
|
127
|
+
Historical execution remains usable after an executor is revoked.
|
|
128
|
+
"""
|
|
129
|
+
rows, _, _ = await self.runtime.driver.execute_query('''
|
|
130
|
+
MATCH (task:FuliProjectAgentTask {
|
|
131
|
+
personal_space_id: $personal_space_id,
|
|
132
|
+
personal_project_id: $personal_project_id, task_id: $task_id
|
|
133
|
+
})-[:HAS_PARTICIPANT]->(agent:FuliProjectAgent {agent_id: $agent_id})
|
|
134
|
+
MATCH (:FuliSpace {id: $personal_space_id, kind: 'personal'})
|
|
135
|
+
-[:HAS_PROJECT_AGENT_IDENTITY]->(agent)
|
|
136
|
+
OPTIONAL MATCH (task)-[:HAS_EXECUTOR_OBSERVATION]->
|
|
137
|
+
(observation:FuliProjectAgentExecutorObservation {
|
|
138
|
+
agent_id: $agent_id, executor_id: $executor_id
|
|
139
|
+
})
|
|
140
|
+
WHERE $run_id IS NULL OR observation.run_id = $run_id
|
|
141
|
+
WITH task, collect(DISTINCT observation) AS observations
|
|
142
|
+
OPTIONAL MATCH (task)-[:HAS_TASK_EVENT]->(event:FuliProjectAgentTaskEvent {
|
|
143
|
+
event_id: $run_id, agent_id: $agent_id, actual_executor_id: $executor_id
|
|
144
|
+
})
|
|
145
|
+
RETURN task, observations, event
|
|
146
|
+
''', **{key: getattr(request, key) for key in (
|
|
147
|
+
'personal_space_id', 'personal_project_id', 'task_id',
|
|
148
|
+
'agent_id', 'executor_id', 'run_id')}, routing_='r')
|
|
149
|
+
if not rows:
|
|
150
|
+
raise HTTPException(status_code=409, detail='outcome Agent is not a task participant')
|
|
151
|
+
row = rows[0]
|
|
152
|
+
task = dict(row['task'])
|
|
153
|
+
strategy_key = project_agent_model_strategy_key(request.model_strategy)
|
|
154
|
+
|
|
155
|
+
def matches_strategy(raw):
|
|
156
|
+
strategy = ProjectAgentModelStrategy.model_validate_json(raw or '{}')
|
|
157
|
+
return project_agent_model_strategy_key(strategy) == strategy_key
|
|
158
|
+
|
|
159
|
+
if (task.get('work_kind') != request.work_kind
|
|
160
|
+
or not matches_strategy(task.get('effective_model_strategy_json'))):
|
|
161
|
+
raise HTTPException(status_code=409, detail='outcome work kind or strategy does not match the task')
|
|
162
|
+
observations = [dict(value) for value in row.get('observations') or []]
|
|
163
|
+
if not any(matches_strategy(value.get('model_strategy_json')) for value in observations):
|
|
164
|
+
raise HTTPException(status_code=409, detail='outcome requires a matching recorded execution')
|
|
165
|
+
if request.evidence_kind == 'terminal_outcome':
|
|
166
|
+
event = dict(row.get('event') or {})
|
|
167
|
+
terminal_status = event.get('status')
|
|
168
|
+
if terminal_status not in {'completed', 'failed', 'cancelled'}:
|
|
169
|
+
terminal_status = event.get('worker_status')
|
|
170
|
+
if (not event or terminal_status != request.terminal_outcome
|
|
171
|
+
or request.reference_ids != [request.run_id]
|
|
172
|
+
or request.idempotency_key != f'terminal:{request.run_id}'
|
|
173
|
+
or request.occurred_at != native_datetime(event.get('created_at'))):
|
|
174
|
+
raise HTTPException(status_code=409, detail='terminal outcome must match its canonical task event')
|
|
175
|
+
|
|
176
|
+
async def _record_outcome_evidence(self, actor, request):
|
|
74
177
|
evidence_id = stable_uuid(
|
|
75
178
|
self.settings.provider_id,
|
|
76
179
|
request.personal_space_id,
|
|
@@ -96,6 +199,7 @@ class StoreProjectAgentExecutorLearning:
|
|
|
96
199
|
})
|
|
97
200
|
ON CREATE SET evidence.evidence_id = $evidence_id,
|
|
98
201
|
evidence.payload_hash = $payload_hash,
|
|
202
|
+
evidence.idempotency_key = $idempotency_key,
|
|
99
203
|
evidence.personal_space_id = $personal_space_id,
|
|
100
204
|
evidence.personal_project_id = $personal_project_id,
|
|
101
205
|
evidence.work_kind = $work_kind,
|
|
@@ -126,6 +230,7 @@ class StoreProjectAgentExecutorLearning:
|
|
|
126
230
|
run_id=request.run_id,
|
|
127
231
|
evidence_id=evidence_id,
|
|
128
232
|
payload_hash=payload_hash,
|
|
233
|
+
idempotency_key=request.idempotency_key,
|
|
129
234
|
model_strategy_json=request.model_strategy.model_dump_json(),
|
|
130
235
|
model_strategy_key=strategy_key,
|
|
131
236
|
evidence_kind=request.evidence_kind,
|
|
@@ -314,6 +419,21 @@ class StoreProjectAgentExecutorLearning:
|
|
|
314
419
|
space,
|
|
315
420
|
request.personal_project_id,
|
|
316
421
|
)
|
|
422
|
+
records, _, _ = await self.runtime.driver.execute_query('''
|
|
423
|
+
MATCH (evidence:FuliProjectAgentExecutorOutcomeEvidence {
|
|
424
|
+
personal_space_id: $personal_space_id, personal_project_id: $personal_project_id,
|
|
425
|
+
agent_id: $agent_id, evidence_id: $evidence_id
|
|
426
|
+
})
|
|
427
|
+
RETURN evidence
|
|
428
|
+
''', **{key: getattr(request, key) for key in (
|
|
429
|
+
'personal_space_id', 'personal_project_id', 'agent_id', 'evidence_id')})
|
|
430
|
+
if not records:
|
|
431
|
+
raise HTTPException(status_code=409, detail='outcome evidence is missing')
|
|
432
|
+
evidence = self._outcome_evidence_from_raw(dict(records[0]['evidence']))
|
|
433
|
+
async with self._learning_bucket_transaction(**self._learning_scope(evidence)) as scoped:
|
|
434
|
+
return await scoped._ignore_outcome_evidence(actor, request)
|
|
435
|
+
|
|
436
|
+
async def _ignore_outcome_evidence(self, actor, request):
|
|
317
437
|
records, _, _ = await self.runtime.driver.execute_query(
|
|
318
438
|
'''
|
|
319
439
|
MATCH (evidence:FuliProjectAgentExecutorOutcomeEvidence {
|
|
@@ -323,11 +443,15 @@ class StoreProjectAgentExecutorLearning:
|
|
|
323
443
|
evidence_id: $evidence_id
|
|
324
444
|
})
|
|
325
445
|
WHERE evidence.ignore_idempotency_key IS NULL
|
|
326
|
-
OR evidence.ignore_idempotency_key = $idempotency_key
|
|
446
|
+
OR (evidence.ignore_idempotency_key = $idempotency_key
|
|
447
|
+
AND (evidence.ignore_payload_hash = $payload_hash
|
|
448
|
+
OR (evidence.ignore_payload_hash IS NULL
|
|
449
|
+
AND evidence.ignored_reason = $reason)))
|
|
327
450
|
SET evidence.ignored = true,
|
|
328
451
|
evidence.ignored_reason = $reason,
|
|
329
452
|
evidence.ignore_idempotency_key = $idempotency_key,
|
|
330
|
-
evidence.
|
|
453
|
+
evidence.ignore_payload_hash = $payload_hash,
|
|
454
|
+
evidence.ignored_at = coalesce(evidence.ignored_at, $updated_at)
|
|
331
455
|
RETURN evidence
|
|
332
456
|
''',
|
|
333
457
|
personal_space_id=request.personal_space_id,
|
|
@@ -336,6 +460,7 @@ class StoreProjectAgentExecutorLearning:
|
|
|
336
460
|
evidence_id=request.evidence_id,
|
|
337
461
|
reason=request.reason,
|
|
338
462
|
idempotency_key=request.idempotency_key,
|
|
463
|
+
payload_hash=self._payload_hash(request),
|
|
339
464
|
updated_at=now_utc(),
|
|
340
465
|
)
|
|
341
466
|
if not records:
|
|
@@ -371,6 +496,10 @@ class StoreProjectAgentExecutorLearning:
|
|
|
371
496
|
space,
|
|
372
497
|
request.personal_project_id,
|
|
373
498
|
)
|
|
499
|
+
async with self._learning_bucket_transaction(**self._learning_scope(request)) as scoped:
|
|
500
|
+
return await scoped._reset_outcomes(actor, request)
|
|
501
|
+
|
|
502
|
+
async def _reset_outcomes(self, actor, request):
|
|
374
503
|
strategy_key = project_agent_model_strategy_key(request.model_strategy)
|
|
375
504
|
reset_id = project_agent_executor_outcome_bucket_id(
|
|
376
505
|
self.settings.provider_id,
|
|
@@ -383,6 +512,12 @@ class StoreProjectAgentExecutorLearning:
|
|
|
383
512
|
bucket_kind='reset',
|
|
384
513
|
)
|
|
385
514
|
payload_hash = self._payload_hash(request)
|
|
515
|
+
previous, _, _ = await self.runtime.driver.execute_query('''
|
|
516
|
+
MATCH (reset:FuliProjectAgentExecutorOutcomeReset {id: $reset_id})
|
|
517
|
+
RETURN reset.reset_at AS previous_reset_at
|
|
518
|
+
''', reset_id=reset_id, routing_='r')
|
|
519
|
+
previous_time = native_datetime(previous[0]['previous_reset_at']) if previous else None
|
|
520
|
+
replace_unknown_timezone = previous_time is not None and previous_time.utcoffset() is None
|
|
386
521
|
records, _, _ = await self.runtime.driver.execute_query(
|
|
387
522
|
'''
|
|
388
523
|
MERGE (reset:FuliProjectAgentExecutorOutcomeReset {
|
|
@@ -402,7 +537,8 @@ class StoreProjectAgentExecutorLearning:
|
|
|
402
537
|
OR reset.payload_hash = $payload_hash
|
|
403
538
|
OR (
|
|
404
539
|
reset.idempotency_key <> $idempotency_key
|
|
405
|
-
AND (reset.reset_at IS NULL OR
|
|
540
|
+
AND (reset.reset_at IS NULL OR $replace_unknown_timezone
|
|
541
|
+
OR reset.reset_at < $reset_at)
|
|
406
542
|
) AS accepted
|
|
407
543
|
WHERE accepted
|
|
408
544
|
SET reset.reset_at = $reset_at,
|
|
@@ -424,6 +560,7 @@ class StoreProjectAgentExecutorLearning:
|
|
|
424
560
|
model_strategy_key=strategy_key,
|
|
425
561
|
payload_hash=payload_hash,
|
|
426
562
|
reset_at=request.reset_at,
|
|
563
|
+
replace_unknown_timezone=replace_unknown_timezone,
|
|
427
564
|
reason=request.reason,
|
|
428
565
|
idempotency_key=request.idempotency_key,
|
|
429
566
|
)
|
|
@@ -466,6 +603,19 @@ class StoreProjectAgentExecutorLearning:
|
|
|
466
603
|
if decay_half_life_days <= 0:
|
|
467
604
|
raise HTTPException(status_code=422, detail='decay half-life must be positive')
|
|
468
605
|
strategy = model_strategy or ProjectAgentModelStrategy()
|
|
606
|
+
async with self._learning_bucket_transaction(
|
|
607
|
+
personal_space_id=personal_space_id, personal_project_id=personal_project_id,
|
|
608
|
+
work_kind=work_kind, executor_id=executor_id, agent_id=agent_id,
|
|
609
|
+
model_strategy=strategy,
|
|
610
|
+
) as scoped:
|
|
611
|
+
return await scoped._aggregate_outcomes(
|
|
612
|
+
personal_space_id=personal_space_id, personal_project_id=personal_project_id,
|
|
613
|
+
work_kind=work_kind, executor_id=executor_id, agent_id=agent_id,
|
|
614
|
+
strategy=strategy, as_of=as_of, decay_half_life_days=decay_half_life_days)
|
|
615
|
+
|
|
616
|
+
async def _aggregate_outcomes(self, *, personal_space_id, personal_project_id,
|
|
617
|
+
work_kind, executor_id, agent_id, strategy,
|
|
618
|
+
as_of, decay_half_life_days):
|
|
469
619
|
strategy_key = project_agent_model_strategy_key(strategy)
|
|
470
620
|
reset_id = project_agent_executor_outcome_bucket_id(
|
|
471
621
|
self.settings.provider_id,
|
|
@@ -518,13 +668,18 @@ class StoreProjectAgentExecutorLearning:
|
|
|
518
668
|
routing_='r',
|
|
519
669
|
)
|
|
520
670
|
current = as_of or now_utc()
|
|
671
|
+
if current.utcoffset() is None:
|
|
672
|
+
raise HTTPException(status_code=422, detail='aggregation time requires a timezone')
|
|
673
|
+
validation_warnings = []
|
|
521
674
|
reset_at = None
|
|
522
675
|
for row in reset_records:
|
|
523
676
|
reset_raw = row.get('reset')
|
|
524
677
|
if not reset_raw:
|
|
525
678
|
continue
|
|
526
679
|
candidate = native_datetime(dict(reset_raw).get('reset_at'))
|
|
527
|
-
if candidate and (
|
|
680
|
+
if candidate and candidate.utcoffset() is None:
|
|
681
|
+
validation_warnings.append('reset_timestamp_timezone_missing')
|
|
682
|
+
if candidate and (reset_at is None or (not validation_warnings and candidate > reset_at)):
|
|
528
683
|
reset_at = candidate
|
|
529
684
|
evidence_rows = []
|
|
530
685
|
for row in records:
|
|
@@ -546,12 +701,20 @@ class StoreProjectAgentExecutorLearning:
|
|
|
546
701
|
raw_key = project_agent_model_strategy_key(evidence_strategy)
|
|
547
702
|
if raw_key == strategy_key:
|
|
548
703
|
evidence_rows.append(raw)
|
|
704
|
+
if (not raw.get('ignored')
|
|
705
|
+
and native_datetime(raw.get('occurred_at')).utcoffset() is None):
|
|
706
|
+
validation_warnings.append(
|
|
707
|
+
f'evidence_timestamp_timezone_missing:{raw.get("evidence_id") or raw.get("id")}')
|
|
708
|
+
# Older requests accepted local datetimes. Never guess their timezone or
|
|
709
|
+
# let one invalid value prevent subsequent ignore/reset repairs. Expose
|
|
710
|
+
# the history, but keep the entire bucket neutral until it is usable.
|
|
549
711
|
evidence = [
|
|
550
712
|
self._outcome_evidence_from_raw(raw)
|
|
551
713
|
for raw in evidence_rows
|
|
552
714
|
if not raw.get('ignored')
|
|
553
715
|
and (
|
|
554
|
-
|
|
716
|
+
validation_warnings
|
|
717
|
+
or reset_at is None
|
|
555
718
|
or native_datetime(raw.get('occurred_at')) > reset_at
|
|
556
719
|
)
|
|
557
720
|
]
|
|
@@ -566,16 +729,16 @@ class StoreProjectAgentExecutorLearning:
|
|
|
566
729
|
evidence_refs: set[str] = set()
|
|
567
730
|
for item in evidence:
|
|
568
731
|
occurred = item.occurred_at
|
|
569
|
-
age = max(0.0, (current - occurred).total_seconds() / 86400)
|
|
570
|
-
weight = math.pow(0.5, age / decay_half_life_days)
|
|
571
|
-
if age <= _DEFAULT_RECENT_DAYS:
|
|
732
|
+
age = None if validation_warnings else max(0.0, (current - occurred).total_seconds() / 86400)
|
|
733
|
+
weight = 0.0 if age is None else math.pow(0.5, age / decay_half_life_days)
|
|
734
|
+
if age is not None and age <= _DEFAULT_RECENT_DAYS:
|
|
572
735
|
recent_count += 1
|
|
573
736
|
evidence_refs.update(item.reference_ids)
|
|
574
737
|
if item.task_id:
|
|
575
738
|
evidence_refs.add(f'task:{item.task_id}')
|
|
576
739
|
if item.run_id:
|
|
577
740
|
evidence_refs.add(f'run:{item.run_id}')
|
|
578
|
-
signal, value = self._evidence_signal(item)
|
|
741
|
+
signal, value = ('neutral', 0.0) if validation_warnings else self._evidence_signal(item)
|
|
579
742
|
if signal == 'success':
|
|
580
743
|
success_count += 1
|
|
581
744
|
weighted_success += weight * value
|
|
@@ -616,8 +779,8 @@ class StoreProjectAgentExecutorLearning:
|
|
|
616
779
|
failure_count=failure_count,
|
|
617
780
|
rating_count=len(rating_values),
|
|
618
781
|
average_rating=(sum(rating_values) / len(rating_values)) if rating_values else None,
|
|
619
|
-
neutral_due_to_insufficient_evidence=sample_count < _MINIMUM_EVIDENCE_SAMPLES,
|
|
620
|
-
ignored=
|
|
782
|
+
neutral_due_to_insufficient_evidence=bool(validation_warnings) or sample_count < _MINIMUM_EVIDENCE_SAMPLES,
|
|
783
|
+
ignored=bool(validation_warnings),
|
|
621
784
|
reset_at=reset_at,
|
|
622
785
|
as_of=current,
|
|
623
786
|
decay_half_life_days=decay_half_life_days,
|
|
@@ -625,6 +788,7 @@ class StoreProjectAgentExecutorLearning:
|
|
|
625
788
|
weighted_failure=weighted_failure,
|
|
626
789
|
evidence_refs=sorted(evidence_refs),
|
|
627
790
|
evidence_contributions=contributions,
|
|
791
|
+
validation_warnings=validation_warnings,
|
|
628
792
|
)
|
|
629
793
|
await self.runtime.driver.execute_query(
|
|
630
794
|
'''
|
|
@@ -13,6 +13,7 @@ from .project_agent_executor_models import (
|
|
|
13
13
|
ProjectAgentExecutorRoutingRuleRecord,
|
|
14
14
|
ProjectAgentExecutorScope,
|
|
15
15
|
ProjectAgentExecutorSelection,
|
|
16
|
+
project_agent_capability_key,
|
|
16
17
|
project_agent_model_strategy_key,
|
|
17
18
|
)
|
|
18
19
|
from .project_agent_models import ProjectAgentExecutorPolicy, ProjectAgentModelStrategy
|
|
@@ -20,8 +21,6 @@ from .provider_values import now_utc, stable_uuid
|
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
_RULE_SCOPE_RANK = {'global': 0, 'space': 1, 'project': 2, 'task': 3}
|
|
23
|
-
|
|
24
|
-
|
|
25
24
|
class StoreProjectAgentExecutorRouting:
|
|
26
25
|
"""Resolve registered executors without owning directory mutations."""
|
|
27
26
|
|
|
@@ -66,7 +65,7 @@ class StoreProjectAgentExecutorRouting:
|
|
|
66
65
|
MATCH (space:FuliSpace {id: $personal_space_id, kind: 'personal'})-
|
|
67
66
|
[:HAS_PROJECT_AGENT_IDENTITY]->
|
|
68
67
|
(agent:FuliProjectAgent {agent_id: $agent_id})
|
|
69
|
-
|
|
68
|
+
MATCH (space)-[:CONTAINS_PROJECT]->
|
|
70
69
|
(:FuliPersonalProject {project_id: $personal_project_id})-
|
|
71
70
|
[:HAS_PROJECT_AGENT_ASSIGNMENT]->
|
|
72
71
|
(assignment:FuliProjectAgentAssignment)-[:ASSIGNED_AGENT]->(agent)
|
|
@@ -84,9 +83,28 @@ class StoreProjectAgentExecutorRouting:
|
|
|
84
83
|
routing_='r',
|
|
85
84
|
)
|
|
86
85
|
if not agent_rows:
|
|
87
|
-
raise HTTPException(
|
|
86
|
+
raise HTTPException(
|
|
87
|
+
status_code=404,
|
|
88
|
+
detail='active project Agent assignment not found',
|
|
89
|
+
)
|
|
88
90
|
agent_raw = dict(agent_rows[0].get('agent') or {})
|
|
89
91
|
assignment_raw = dict(agent_rows[0].get('assignment') or {})
|
|
92
|
+
if (
|
|
93
|
+
not assignment_raw
|
|
94
|
+
or assignment_raw.get('status') != 'active'
|
|
95
|
+
or (
|
|
96
|
+
assignment_id is None
|
|
97
|
+
and work_kind not in (assignment_raw.get('work_kinds') or [])
|
|
98
|
+
)
|
|
99
|
+
or (
|
|
100
|
+
assignment_id is not None
|
|
101
|
+
and assignment_raw.get('assignment_id') != assignment_id
|
|
102
|
+
)
|
|
103
|
+
):
|
|
104
|
+
raise HTTPException(
|
|
105
|
+
status_code=404,
|
|
106
|
+
detail='active project Agent assignment not found',
|
|
107
|
+
)
|
|
90
108
|
agent_policy = self._policy_from_agent(agent_raw)
|
|
91
109
|
assignment_policy = self._policy_from_assignment(assignment_raw)
|
|
92
110
|
override = task_override if task_override is not None else executor_policy
|
|
@@ -139,7 +157,9 @@ class StoreProjectAgentExecutorRouting:
|
|
|
139
157
|
work_kind=work_kind,
|
|
140
158
|
routing_='r',
|
|
141
159
|
)
|
|
142
|
-
requested_capabilities = {
|
|
160
|
+
requested_capabilities = {
|
|
161
|
+
project_agent_capability_key(item) for item in required
|
|
162
|
+
}
|
|
143
163
|
rules = [
|
|
144
164
|
rule
|
|
145
165
|
for rule in (
|
|
@@ -148,7 +168,8 @@ class StoreProjectAgentExecutorRouting:
|
|
|
148
168
|
if row.get('rule')
|
|
149
169
|
)
|
|
150
170
|
if {
|
|
151
|
-
item
|
|
171
|
+
project_agent_capability_key(item)
|
|
172
|
+
for item in rule.required_capabilities
|
|
152
173
|
}.issubset(requested_capabilities)
|
|
153
174
|
]
|
|
154
175
|
top_scope = max((_RULE_SCOPE_RANK[item.scope] for item in rules), default=None)
|
|
@@ -551,11 +572,11 @@ class StoreProjectAgentExecutorRouting:
|
|
|
551
572
|
if executor.preflight_status != 'passed':
|
|
552
573
|
return f'preflight status is {executor.preflight_status}'
|
|
553
574
|
available_capabilities = {
|
|
554
|
-
item
|
|
575
|
+
project_agent_capability_key(item) for item in executor.capabilities
|
|
555
576
|
}
|
|
556
577
|
missing = [
|
|
557
578
|
item for item in required_capabilities
|
|
558
|
-
if item
|
|
579
|
+
if project_agent_capability_key(item) not in available_capabilities
|
|
559
580
|
]
|
|
560
581
|
if missing:
|
|
561
582
|
return f'missing capabilities: {", ".join(missing)}'
|
|
@@ -589,9 +610,11 @@ class StoreProjectAgentExecutorRouting:
|
|
|
589
610
|
or strategy.reasoning_effort in model.reasoning_efforts
|
|
590
611
|
or strategy.reasoning_effort == 'default'
|
|
591
612
|
)
|
|
592
|
-
capabilities = {
|
|
613
|
+
capabilities = {
|
|
614
|
+
project_agent_capability_key(item) for item in model.capabilities
|
|
615
|
+
}
|
|
593
616
|
hints_ok = all(
|
|
594
|
-
hint
|
|
617
|
+
project_agent_capability_key(hint) in capabilities
|
|
595
618
|
for hint in strategy.capability_hints
|
|
596
619
|
)
|
|
597
620
|
if mode_ok and effort_ok and hints_ok:
|