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.
- package/README.md +77 -6
- package/README.zh-CN.md +69 -6
- package/compose.graphiti.yml +6 -6
- package/dist/web/assets/AboutPage-ByCUor-j.js +1 -0
- package/dist/web/assets/ConnectionsPage-DkdkZ40Q.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-DSe3Gwep.js +1 -0
- package/dist/web/assets/KnowledgeInspector-CYAjAb9J.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CV4F54R_.js +1 -0
- package/dist/web/assets/KnowledgePage-vYYXipOi.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-z4u5mXOG.js +4 -0
- package/dist/web/assets/OverviewPage-BWOfeS6k.js +1 -0
- package/dist/web/assets/PersonalProfilePage-c3w4-h7a.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-B7AlLEwf.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-CgftpIPB.css +1 -0
- package/dist/web/assets/ProjectAgentsPage-DTHcL_ox.js +11 -0
- package/dist/web/assets/PublicProjectsPage-DdH0oGB3.js +1 -0
- package/dist/web/assets/ReviewPage-BYIpcIHG.js +1 -0
- package/dist/web/assets/SearchableSelect-Dz13ClQb.js +1 -0
- package/dist/web/assets/SettingsPage-2VkbXgZi.js +1 -0
- package/dist/web/assets/SettingsPage-CwZp2ka4.css +1 -0
- package/dist/web/assets/WritingTastePage-BOPQZuva.js +1 -0
- package/dist/web/assets/{console-D_2zC4lm.js → console-BlAhujsH.js} +1 -1
- package/dist/web/assets/{conversation-launcher-settings-DQmMH2j7.js → conversation-launcher-settings-DTYxiLJt.js} +1 -1
- package/dist/web/assets/i18n-Ci3heuiL.js +9 -0
- package/dist/web/assets/{identity-DmGcY3Y6.js → identity-4_-v6cB7.js} +1 -1
- package/dist/web/assets/index-BnH5mcZx.css +1 -0
- package/dist/web/assets/index-C67Q0JY2.js +2 -0
- package/dist/web/assets/{model-CVoOdRsL.js → model-yG0YqUyZ.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-CBNg4CdH.js → useApi-CROJJdhE-twhdZArA.js} +1 -1
- package/dist/web/assets/vue-router-C-wot_Di.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_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 +597 -0
- package/graph-provider/fuli_graph/project_agent_task_models.py +542 -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_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 +699 -0
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +409 -0
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +1144 -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 +396 -0
- package/src/agent-tools/graph-handlers.js +61 -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 +263 -0
- package/src/graphiti/project-agent-mapping.js +1003 -0
- package/src/graphiti/project-agent-workflows.js +557 -0
- package/src/graphiti/provider-client.js +270 -1
- package/src/http/api-router.js +63 -9
- package/src/http/graph-api-router.js +278 -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 +69 -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,756 @@
|
|
|
1
|
+
"""Auditable, decayed executor outcome evidence and learning aggregates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from fastapi import HTTPException
|
|
10
|
+
|
|
11
|
+
from .personal_project_access import authorize_personal_project
|
|
12
|
+
from .project_agent_executor_models import (
|
|
13
|
+
ProjectAgentExecutorEvidenceContribution,
|
|
14
|
+
ProjectAgentExecutorEvidenceIgnore,
|
|
15
|
+
ProjectAgentExecutorOutcomeAggregate,
|
|
16
|
+
ProjectAgentExecutorOutcomeEvidenceCreate,
|
|
17
|
+
ProjectAgentExecutorOutcomeEvidenceRecord,
|
|
18
|
+
ProjectAgentExecutorOutcomeReset,
|
|
19
|
+
project_agent_model_strategy_key,
|
|
20
|
+
)
|
|
21
|
+
from .project_agent_models import ProjectAgentModelStrategy
|
|
22
|
+
from .provider_values import native_datetime, now_utc, stable_uuid
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_TERMINAL_NEUTRAL = {'cancelled'}
|
|
26
|
+
_DEFAULT_HALF_LIFE_DAYS = 30.0
|
|
27
|
+
_DEFAULT_RECENT_DAYS = 7
|
|
28
|
+
_MINIMUM_EVIDENCE_SAMPLES = 3
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def project_agent_executor_outcome_bucket_id(
|
|
32
|
+
provider_id: str,
|
|
33
|
+
personal_space_id: str,
|
|
34
|
+
personal_project_id: str,
|
|
35
|
+
work_kind: str,
|
|
36
|
+
agent_id: str,
|
|
37
|
+
executor_id: str,
|
|
38
|
+
model_strategy_key: str,
|
|
39
|
+
*,
|
|
40
|
+
bucket_kind: str,
|
|
41
|
+
) -> str:
|
|
42
|
+
"""Return the provider- and space-scoped identity for a learning cache."""
|
|
43
|
+
|
|
44
|
+
if bucket_kind not in {'aggregate', 'reset'}:
|
|
45
|
+
raise ValueError('unsupported Project Agent executor outcome bucket kind')
|
|
46
|
+
return stable_uuid(
|
|
47
|
+
provider_id,
|
|
48
|
+
personal_space_id,
|
|
49
|
+
f'project-agent-executor-outcome-{bucket_kind}',
|
|
50
|
+
personal_project_id,
|
|
51
|
+
work_kind,
|
|
52
|
+
agent_id,
|
|
53
|
+
executor_id,
|
|
54
|
+
model_strategy_key,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class StoreProjectAgentExecutorLearning:
|
|
59
|
+
"""Persist explainable evidence without overriding explicit user policy."""
|
|
60
|
+
|
|
61
|
+
async def record_project_agent_executor_outcome_evidence(
|
|
62
|
+
self,
|
|
63
|
+
actor: dict,
|
|
64
|
+
request: ProjectAgentExecutorOutcomeEvidenceCreate,
|
|
65
|
+
) -> ProjectAgentExecutorOutcomeEvidenceRecord:
|
|
66
|
+
self._require_personal()
|
|
67
|
+
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
68
|
+
await authorize_personal_project(
|
|
69
|
+
self,
|
|
70
|
+
actor,
|
|
71
|
+
space,
|
|
72
|
+
request.personal_project_id,
|
|
73
|
+
)
|
|
74
|
+
evidence_id = stable_uuid(
|
|
75
|
+
self.settings.provider_id,
|
|
76
|
+
request.personal_space_id,
|
|
77
|
+
'project-agent-executor-evidence',
|
|
78
|
+
request.task_id,
|
|
79
|
+
request.run_id or '',
|
|
80
|
+
request.executor_id,
|
|
81
|
+
project_agent_model_strategy_key(request.model_strategy),
|
|
82
|
+
request.idempotency_key,
|
|
83
|
+
)
|
|
84
|
+
strategy_key = project_agent_model_strategy_key(request.model_strategy)
|
|
85
|
+
payload_hash = self._payload_hash(request)
|
|
86
|
+
timestamp = now_utc()
|
|
87
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
88
|
+
'''
|
|
89
|
+
MATCH (task:FuliProjectAgentTask {
|
|
90
|
+
personal_space_id: $personal_space_id,
|
|
91
|
+
personal_project_id: $personal_project_id,
|
|
92
|
+
task_id: $task_id
|
|
93
|
+
})
|
|
94
|
+
MERGE (evidence:FuliProjectAgentExecutorOutcomeEvidence {
|
|
95
|
+
id: $evidence_id
|
|
96
|
+
})
|
|
97
|
+
ON CREATE SET evidence.evidence_id = $evidence_id,
|
|
98
|
+
evidence.payload_hash = $payload_hash,
|
|
99
|
+
evidence.personal_space_id = $personal_space_id,
|
|
100
|
+
evidence.personal_project_id = $personal_project_id,
|
|
101
|
+
evidence.work_kind = $work_kind,
|
|
102
|
+
evidence.agent_id = $agent_id,
|
|
103
|
+
evidence.executor_id = $executor_id,
|
|
104
|
+
evidence.task_id = $task_id,
|
|
105
|
+
evidence.run_id = $run_id,
|
|
106
|
+
evidence.model_strategy_json = $model_strategy_json,
|
|
107
|
+
evidence.model_strategy_key = $model_strategy_key,
|
|
108
|
+
evidence.evidence_kind = $evidence_kind,
|
|
109
|
+
evidence.source = $source,
|
|
110
|
+
evidence.terminal_outcome = $terminal_outcome,
|
|
111
|
+
evidence.rating = $rating,
|
|
112
|
+
evidence.reference_ids = $reference_ids,
|
|
113
|
+
evidence.note = $note,
|
|
114
|
+
evidence.occurred_at = $occurred_at,
|
|
115
|
+
evidence.ignored = false,
|
|
116
|
+
evidence.created_at = $created_at
|
|
117
|
+
MERGE (task)-[:HAS_EXECUTOR_OUTCOME_EVIDENCE]->(evidence)
|
|
118
|
+
RETURN evidence
|
|
119
|
+
''',
|
|
120
|
+
personal_space_id=request.personal_space_id,
|
|
121
|
+
personal_project_id=request.personal_project_id,
|
|
122
|
+
work_kind=request.work_kind,
|
|
123
|
+
agent_id=request.agent_id,
|
|
124
|
+
executor_id=request.executor_id,
|
|
125
|
+
task_id=request.task_id,
|
|
126
|
+
run_id=request.run_id,
|
|
127
|
+
evidence_id=evidence_id,
|
|
128
|
+
payload_hash=payload_hash,
|
|
129
|
+
model_strategy_json=request.model_strategy.model_dump_json(),
|
|
130
|
+
model_strategy_key=strategy_key,
|
|
131
|
+
evidence_kind=request.evidence_kind,
|
|
132
|
+
source=request.source,
|
|
133
|
+
terminal_outcome=request.terminal_outcome,
|
|
134
|
+
rating=request.rating,
|
|
135
|
+
reference_ids=request.reference_ids,
|
|
136
|
+
note=request.note,
|
|
137
|
+
occurred_at=request.occurred_at,
|
|
138
|
+
created_at=timestamp,
|
|
139
|
+
)
|
|
140
|
+
if not records:
|
|
141
|
+
raise HTTPException(status_code=404, detail='project Agent task not found')
|
|
142
|
+
raw = dict(records[0].get('evidence') or {})
|
|
143
|
+
if raw.get('payload_hash') not in {None, payload_hash}:
|
|
144
|
+
raise HTTPException(
|
|
145
|
+
status_code=409,
|
|
146
|
+
detail='outcome evidence idempotency key was reused',
|
|
147
|
+
)
|
|
148
|
+
evidence = self._outcome_evidence_from_raw(raw, request)
|
|
149
|
+
await self.aggregate_project_agent_executor_outcomes(
|
|
150
|
+
actor,
|
|
151
|
+
personal_space_id=request.personal_space_id,
|
|
152
|
+
personal_project_id=request.personal_project_id,
|
|
153
|
+
work_kind=request.work_kind,
|
|
154
|
+
agent_id=request.agent_id,
|
|
155
|
+
executor_id=request.executor_id,
|
|
156
|
+
model_strategy=request.model_strategy,
|
|
157
|
+
)
|
|
158
|
+
return evidence
|
|
159
|
+
|
|
160
|
+
async def list_project_agent_executor_outcome_evidence(
|
|
161
|
+
self,
|
|
162
|
+
actor: dict,
|
|
163
|
+
personal_space_id: str,
|
|
164
|
+
personal_project_id: str,
|
|
165
|
+
*,
|
|
166
|
+
work_kind: str | None = None,
|
|
167
|
+
executor_id: str | None = None,
|
|
168
|
+
agent_id: str | None = None,
|
|
169
|
+
model_strategy_key: str | None = None,
|
|
170
|
+
include_ignored: bool = False,
|
|
171
|
+
) -> list[ProjectAgentExecutorOutcomeEvidenceRecord]:
|
|
172
|
+
self._require_personal()
|
|
173
|
+
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
174
|
+
await authorize_personal_project(
|
|
175
|
+
self,
|
|
176
|
+
actor,
|
|
177
|
+
space,
|
|
178
|
+
personal_project_id,
|
|
179
|
+
)
|
|
180
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
181
|
+
'''
|
|
182
|
+
MATCH (evidence:FuliProjectAgentExecutorOutcomeEvidence {
|
|
183
|
+
personal_space_id: $personal_space_id,
|
|
184
|
+
personal_project_id: $personal_project_id
|
|
185
|
+
})
|
|
186
|
+
WHERE ($work_kind IS NULL OR evidence.work_kind = $work_kind)
|
|
187
|
+
AND ($executor_id IS NULL OR evidence.executor_id = $executor_id)
|
|
188
|
+
AND ($agent_id IS NULL OR evidence.agent_id = $agent_id)
|
|
189
|
+
AND ($model_strategy_key IS NULL
|
|
190
|
+
OR evidence.model_strategy_key = $model_strategy_key)
|
|
191
|
+
AND ($include_ignored = true OR coalesce(evidence.ignored, false) = false)
|
|
192
|
+
RETURN evidence
|
|
193
|
+
ORDER BY evidence.occurred_at DESC, evidence.evidence_id
|
|
194
|
+
''',
|
|
195
|
+
personal_space_id=personal_space_id,
|
|
196
|
+
personal_project_id=personal_project_id,
|
|
197
|
+
work_kind=work_kind,
|
|
198
|
+
executor_id=executor_id,
|
|
199
|
+
agent_id=agent_id,
|
|
200
|
+
model_strategy_key=model_strategy_key,
|
|
201
|
+
include_ignored=include_ignored,
|
|
202
|
+
routing_='r',
|
|
203
|
+
)
|
|
204
|
+
evidence = [
|
|
205
|
+
self._outcome_evidence_from_raw(dict(row['evidence']))
|
|
206
|
+
for row in records
|
|
207
|
+
if row.get('evidence')
|
|
208
|
+
]
|
|
209
|
+
if model_strategy_key is not None:
|
|
210
|
+
evidence = [
|
|
211
|
+
item
|
|
212
|
+
for item in evidence
|
|
213
|
+
if item.model_strategy_key == model_strategy_key
|
|
214
|
+
]
|
|
215
|
+
return evidence
|
|
216
|
+
|
|
217
|
+
async def list_project_agent_executor_outcome_aggregates(
|
|
218
|
+
self,
|
|
219
|
+
actor: dict,
|
|
220
|
+
personal_space_id: str,
|
|
221
|
+
*,
|
|
222
|
+
personal_project_id: str | None = None,
|
|
223
|
+
work_kind: str | None = None,
|
|
224
|
+
agent_id: str | None = None,
|
|
225
|
+
executor_id: str | None = None,
|
|
226
|
+
model_strategy_key: str | None = None,
|
|
227
|
+
) -> list[ProjectAgentExecutorOutcomeAggregate]:
|
|
228
|
+
"""List durable learning buckets without combining strategies."""
|
|
229
|
+
|
|
230
|
+
self._require_personal()
|
|
231
|
+
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
232
|
+
if personal_project_id is not None:
|
|
233
|
+
await authorize_personal_project(
|
|
234
|
+
self,
|
|
235
|
+
actor,
|
|
236
|
+
space,
|
|
237
|
+
personal_project_id,
|
|
238
|
+
)
|
|
239
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
240
|
+
'''
|
|
241
|
+
MATCH (aggregate:FuliProjectAgentExecutorOutcomeAggregate {
|
|
242
|
+
personal_space_id: $personal_space_id
|
|
243
|
+
})
|
|
244
|
+
WHERE ($personal_project_id IS NULL
|
|
245
|
+
OR aggregate.personal_project_id = $personal_project_id)
|
|
246
|
+
AND ($work_kind IS NULL OR aggregate.work_kind = $work_kind)
|
|
247
|
+
AND ($agent_id IS NULL OR aggregate.agent_id = $agent_id)
|
|
248
|
+
AND ($executor_id IS NULL OR aggregate.executor_id = $executor_id)
|
|
249
|
+
AND ($model_strategy_key IS NULL
|
|
250
|
+
OR aggregate.model_strategy_key = $model_strategy_key)
|
|
251
|
+
RETURN aggregate
|
|
252
|
+
ORDER BY aggregate.as_of DESC, aggregate.executor_id
|
|
253
|
+
''',
|
|
254
|
+
personal_space_id=personal_space_id,
|
|
255
|
+
personal_project_id=personal_project_id,
|
|
256
|
+
work_kind=work_kind,
|
|
257
|
+
agent_id=agent_id,
|
|
258
|
+
executor_id=executor_id,
|
|
259
|
+
model_strategy_key=model_strategy_key,
|
|
260
|
+
routing_='r',
|
|
261
|
+
)
|
|
262
|
+
aggregates: list[ProjectAgentExecutorOutcomeAggregate] = []
|
|
263
|
+
for row in records:
|
|
264
|
+
raw = dict(row.get('aggregate') or {})
|
|
265
|
+
aggregate_json = raw.get('aggregate_json')
|
|
266
|
+
if aggregate_json:
|
|
267
|
+
try:
|
|
268
|
+
raw.update(json.loads(aggregate_json))
|
|
269
|
+
except (TypeError, ValueError):
|
|
270
|
+
pass
|
|
271
|
+
# Storage identities are intentionally internal: the public model
|
|
272
|
+
# remains the provider-neutral bucket contract.
|
|
273
|
+
raw.pop('id', None)
|
|
274
|
+
raw.pop('aggregate_id', None)
|
|
275
|
+
raw.pop('aggregate_json', None)
|
|
276
|
+
if not raw.get('model_strategy_key'):
|
|
277
|
+
strategy_value = raw.get('model_strategy')
|
|
278
|
+
if isinstance(strategy_value, str):
|
|
279
|
+
try:
|
|
280
|
+
strategy_value = json.loads(strategy_value)
|
|
281
|
+
except (TypeError, ValueError):
|
|
282
|
+
strategy_value = None
|
|
283
|
+
try:
|
|
284
|
+
strategy = ProjectAgentModelStrategy.model_validate(
|
|
285
|
+
strategy_value or {}
|
|
286
|
+
)
|
|
287
|
+
except Exception:
|
|
288
|
+
strategy = ProjectAgentModelStrategy()
|
|
289
|
+
raw['model_strategy'] = strategy
|
|
290
|
+
raw['model_strategy_key'] = project_agent_model_strategy_key(strategy)
|
|
291
|
+
try:
|
|
292
|
+
aggregate = ProjectAgentExecutorOutcomeAggregate.model_validate(raw)
|
|
293
|
+
except Exception:
|
|
294
|
+
# An incomplete legacy node cannot be presented as an audited
|
|
295
|
+
# learning bucket; it remains available for migration/repair.
|
|
296
|
+
continue
|
|
297
|
+
if (
|
|
298
|
+
model_strategy_key is None
|
|
299
|
+
or aggregate.model_strategy_key == model_strategy_key
|
|
300
|
+
):
|
|
301
|
+
aggregates.append(aggregate)
|
|
302
|
+
return aggregates
|
|
303
|
+
|
|
304
|
+
async def ignore_project_agent_executor_outcome_evidence(
|
|
305
|
+
self,
|
|
306
|
+
actor: dict,
|
|
307
|
+
request: ProjectAgentExecutorEvidenceIgnore,
|
|
308
|
+
) -> ProjectAgentExecutorOutcomeEvidenceRecord:
|
|
309
|
+
self._require_personal()
|
|
310
|
+
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
311
|
+
await authorize_personal_project(
|
|
312
|
+
self,
|
|
313
|
+
actor,
|
|
314
|
+
space,
|
|
315
|
+
request.personal_project_id,
|
|
316
|
+
)
|
|
317
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
318
|
+
'''
|
|
319
|
+
MATCH (evidence:FuliProjectAgentExecutorOutcomeEvidence {
|
|
320
|
+
personal_space_id: $personal_space_id,
|
|
321
|
+
personal_project_id: $personal_project_id,
|
|
322
|
+
agent_id: $agent_id,
|
|
323
|
+
evidence_id: $evidence_id
|
|
324
|
+
})
|
|
325
|
+
WHERE evidence.ignore_idempotency_key IS NULL
|
|
326
|
+
OR evidence.ignore_idempotency_key = $idempotency_key
|
|
327
|
+
SET evidence.ignored = true,
|
|
328
|
+
evidence.ignored_reason = $reason,
|
|
329
|
+
evidence.ignore_idempotency_key = $idempotency_key,
|
|
330
|
+
evidence.ignored_at = $updated_at
|
|
331
|
+
RETURN evidence
|
|
332
|
+
''',
|
|
333
|
+
personal_space_id=request.personal_space_id,
|
|
334
|
+
personal_project_id=request.personal_project_id,
|
|
335
|
+
agent_id=request.agent_id,
|
|
336
|
+
evidence_id=request.evidence_id,
|
|
337
|
+
reason=request.reason,
|
|
338
|
+
idempotency_key=request.idempotency_key,
|
|
339
|
+
updated_at=now_utc(),
|
|
340
|
+
)
|
|
341
|
+
if not records:
|
|
342
|
+
raise HTTPException(
|
|
343
|
+
status_code=409,
|
|
344
|
+
detail='outcome evidence is missing or ignore idempotency key was reused',
|
|
345
|
+
)
|
|
346
|
+
evidence = self._outcome_evidence_from_raw(dict(records[0]['evidence']))
|
|
347
|
+
# Ignoring evidence changes the effective sample immediately. Rebuild
|
|
348
|
+
# only this strategy bucket so a later route can use the new signal
|
|
349
|
+
# without requiring a separate UI aggregation action.
|
|
350
|
+
await self.aggregate_project_agent_executor_outcomes(
|
|
351
|
+
actor,
|
|
352
|
+
personal_space_id=evidence.personal_space_id,
|
|
353
|
+
personal_project_id=evidence.personal_project_id,
|
|
354
|
+
work_kind=evidence.work_kind,
|
|
355
|
+
agent_id=evidence.agent_id,
|
|
356
|
+
executor_id=evidence.executor_id,
|
|
357
|
+
model_strategy=evidence.model_strategy,
|
|
358
|
+
)
|
|
359
|
+
return evidence
|
|
360
|
+
|
|
361
|
+
async def reset_project_agent_executor_outcomes(
|
|
362
|
+
self,
|
|
363
|
+
actor: dict,
|
|
364
|
+
request: ProjectAgentExecutorOutcomeReset,
|
|
365
|
+
) -> ProjectAgentExecutorOutcomeAggregate | None:
|
|
366
|
+
self._require_personal()
|
|
367
|
+
space = await self.authorize(actor, request.personal_space_id, 'maintainer')
|
|
368
|
+
await authorize_personal_project(
|
|
369
|
+
self,
|
|
370
|
+
actor,
|
|
371
|
+
space,
|
|
372
|
+
request.personal_project_id,
|
|
373
|
+
)
|
|
374
|
+
strategy_key = project_agent_model_strategy_key(request.model_strategy)
|
|
375
|
+
reset_id = project_agent_executor_outcome_bucket_id(
|
|
376
|
+
self.settings.provider_id,
|
|
377
|
+
request.personal_space_id,
|
|
378
|
+
request.personal_project_id,
|
|
379
|
+
request.work_kind,
|
|
380
|
+
request.agent_id,
|
|
381
|
+
request.executor_id,
|
|
382
|
+
strategy_key,
|
|
383
|
+
bucket_kind='reset',
|
|
384
|
+
)
|
|
385
|
+
payload_hash = self._payload_hash(request)
|
|
386
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
387
|
+
'''
|
|
388
|
+
MERGE (reset:FuliProjectAgentExecutorOutcomeReset {
|
|
389
|
+
id: $reset_id
|
|
390
|
+
})
|
|
391
|
+
ON CREATE SET reset.reset_id = $reset_id,
|
|
392
|
+
reset.personal_space_id = $personal_space_id,
|
|
393
|
+
reset.personal_project_id = $personal_project_id,
|
|
394
|
+
reset.work_kind = $work_kind,
|
|
395
|
+
reset.agent_id = $agent_id,
|
|
396
|
+
reset.executor_id = $executor_id,
|
|
397
|
+
reset.model_strategy_key = $model_strategy_key,
|
|
398
|
+
reset.payload_hash = $payload_hash,
|
|
399
|
+
reset.idempotency_key = $idempotency_key
|
|
400
|
+
WITH reset,
|
|
401
|
+
reset.payload_hash IS NULL
|
|
402
|
+
OR reset.payload_hash = $payload_hash
|
|
403
|
+
OR (
|
|
404
|
+
reset.idempotency_key <> $idempotency_key
|
|
405
|
+
AND (reset.reset_at IS NULL OR reset.reset_at < $reset_at)
|
|
406
|
+
) AS accepted
|
|
407
|
+
WHERE accepted
|
|
408
|
+
SET reset.reset_at = $reset_at,
|
|
409
|
+
reset.reason = $reason,
|
|
410
|
+
reset.model_strategy_json = $model_strategy_json,
|
|
411
|
+
reset.model_strategy_key = $model_strategy_key,
|
|
412
|
+
reset.payload_hash = $payload_hash,
|
|
413
|
+
reset.idempotency_key = $idempotency_key,
|
|
414
|
+
reset.updated_at = $reset_at
|
|
415
|
+
RETURN reset
|
|
416
|
+
''',
|
|
417
|
+
reset_id=reset_id,
|
|
418
|
+
personal_space_id=request.personal_space_id,
|
|
419
|
+
personal_project_id=request.personal_project_id,
|
|
420
|
+
work_kind=request.work_kind,
|
|
421
|
+
agent_id=request.agent_id,
|
|
422
|
+
executor_id=request.executor_id,
|
|
423
|
+
model_strategy_json=request.model_strategy.model_dump_json(),
|
|
424
|
+
model_strategy_key=strategy_key,
|
|
425
|
+
payload_hash=payload_hash,
|
|
426
|
+
reset_at=request.reset_at,
|
|
427
|
+
reason=request.reason,
|
|
428
|
+
idempotency_key=request.idempotency_key,
|
|
429
|
+
)
|
|
430
|
+
if not records:
|
|
431
|
+
raise HTTPException(
|
|
432
|
+
status_code=409,
|
|
433
|
+
detail='outcome reset idempotency key was reused with different input',
|
|
434
|
+
)
|
|
435
|
+
return await self.aggregate_project_agent_executor_outcomes(
|
|
436
|
+
actor,
|
|
437
|
+
personal_space_id=request.personal_space_id,
|
|
438
|
+
personal_project_id=request.personal_project_id,
|
|
439
|
+
work_kind=request.work_kind,
|
|
440
|
+
agent_id=request.agent_id,
|
|
441
|
+
executor_id=request.executor_id,
|
|
442
|
+
model_strategy=request.model_strategy,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
async def aggregate_project_agent_executor_outcomes(
|
|
446
|
+
self,
|
|
447
|
+
actor: dict,
|
|
448
|
+
*,
|
|
449
|
+
personal_space_id: str,
|
|
450
|
+
personal_project_id: str,
|
|
451
|
+
work_kind: str,
|
|
452
|
+
executor_id: str,
|
|
453
|
+
agent_id: str,
|
|
454
|
+
model_strategy: ProjectAgentModelStrategy | None = None,
|
|
455
|
+
as_of: datetime | None = None,
|
|
456
|
+
decay_half_life_days: float = _DEFAULT_HALF_LIFE_DAYS,
|
|
457
|
+
) -> ProjectAgentExecutorOutcomeAggregate:
|
|
458
|
+
self._require_personal()
|
|
459
|
+
space = await self.authorize(actor, personal_space_id, 'reader')
|
|
460
|
+
await authorize_personal_project(
|
|
461
|
+
self,
|
|
462
|
+
actor,
|
|
463
|
+
space,
|
|
464
|
+
personal_project_id,
|
|
465
|
+
)
|
|
466
|
+
if decay_half_life_days <= 0:
|
|
467
|
+
raise HTTPException(status_code=422, detail='decay half-life must be positive')
|
|
468
|
+
strategy = model_strategy or ProjectAgentModelStrategy()
|
|
469
|
+
strategy_key = project_agent_model_strategy_key(strategy)
|
|
470
|
+
reset_id = project_agent_executor_outcome_bucket_id(
|
|
471
|
+
self.settings.provider_id,
|
|
472
|
+
personal_space_id,
|
|
473
|
+
personal_project_id,
|
|
474
|
+
work_kind,
|
|
475
|
+
agent_id,
|
|
476
|
+
executor_id,
|
|
477
|
+
strategy_key,
|
|
478
|
+
bucket_kind='reset',
|
|
479
|
+
)
|
|
480
|
+
aggregate_id = project_agent_executor_outcome_bucket_id(
|
|
481
|
+
self.settings.provider_id,
|
|
482
|
+
personal_space_id,
|
|
483
|
+
personal_project_id,
|
|
484
|
+
work_kind,
|
|
485
|
+
agent_id,
|
|
486
|
+
executor_id,
|
|
487
|
+
strategy_key,
|
|
488
|
+
bucket_kind='aggregate',
|
|
489
|
+
)
|
|
490
|
+
reset_records, _, _ = await self.runtime.driver.execute_query(
|
|
491
|
+
'''
|
|
492
|
+
OPTIONAL MATCH (reset:FuliProjectAgentExecutorOutcomeReset {
|
|
493
|
+
id: $reset_id
|
|
494
|
+
})
|
|
495
|
+
RETURN reset
|
|
496
|
+
''',
|
|
497
|
+
reset_id=reset_id,
|
|
498
|
+
routing_='r',
|
|
499
|
+
)
|
|
500
|
+
records, _, _ = await self.runtime.driver.execute_query(
|
|
501
|
+
'''
|
|
502
|
+
MATCH (evidence:FuliProjectAgentExecutorOutcomeEvidence {
|
|
503
|
+
personal_space_id: $personal_space_id,
|
|
504
|
+
personal_project_id: $personal_project_id,
|
|
505
|
+
work_kind: $work_kind,
|
|
506
|
+
agent_id: $agent_id,
|
|
507
|
+
executor_id: $executor_id,
|
|
508
|
+
model_strategy_key: $model_strategy_key
|
|
509
|
+
})
|
|
510
|
+
RETURN evidence
|
|
511
|
+
''',
|
|
512
|
+
personal_space_id=personal_space_id,
|
|
513
|
+
personal_project_id=personal_project_id,
|
|
514
|
+
work_kind=work_kind,
|
|
515
|
+
agent_id=agent_id,
|
|
516
|
+
executor_id=executor_id,
|
|
517
|
+
model_strategy_key=strategy_key,
|
|
518
|
+
routing_='r',
|
|
519
|
+
)
|
|
520
|
+
current = as_of or now_utc()
|
|
521
|
+
reset_at = None
|
|
522
|
+
for row in reset_records:
|
|
523
|
+
reset_raw = row.get('reset')
|
|
524
|
+
if not reset_raw:
|
|
525
|
+
continue
|
|
526
|
+
candidate = native_datetime(dict(reset_raw).get('reset_at'))
|
|
527
|
+
if candidate and (reset_at is None or candidate > reset_at):
|
|
528
|
+
reset_at = candidate
|
|
529
|
+
evidence_rows = []
|
|
530
|
+
for row in records:
|
|
531
|
+
raw = row.get('evidence')
|
|
532
|
+
if raw:
|
|
533
|
+
raw = dict(raw)
|
|
534
|
+
raw_key = raw.get('model_strategy_key')
|
|
535
|
+
if not raw_key:
|
|
536
|
+
evidence_strategy_json = raw.get('model_strategy_json')
|
|
537
|
+
if evidence_strategy_json:
|
|
538
|
+
try:
|
|
539
|
+
evidence_strategy = ProjectAgentModelStrategy.model_validate_json(
|
|
540
|
+
evidence_strategy_json
|
|
541
|
+
)
|
|
542
|
+
except Exception:
|
|
543
|
+
evidence_strategy = ProjectAgentModelStrategy()
|
|
544
|
+
else:
|
|
545
|
+
evidence_strategy = ProjectAgentModelStrategy()
|
|
546
|
+
raw_key = project_agent_model_strategy_key(evidence_strategy)
|
|
547
|
+
if raw_key == strategy_key:
|
|
548
|
+
evidence_rows.append(raw)
|
|
549
|
+
evidence = [
|
|
550
|
+
self._outcome_evidence_from_raw(raw)
|
|
551
|
+
for raw in evidence_rows
|
|
552
|
+
if not raw.get('ignored')
|
|
553
|
+
and (
|
|
554
|
+
reset_at is None
|
|
555
|
+
or native_datetime(raw.get('occurred_at')) > reset_at
|
|
556
|
+
)
|
|
557
|
+
]
|
|
558
|
+
contributions: list[ProjectAgentExecutorEvidenceContribution] = []
|
|
559
|
+
success_count = 0
|
|
560
|
+
failure_count = 0
|
|
561
|
+
rework_count = 0
|
|
562
|
+
rating_values: list[int] = []
|
|
563
|
+
recent_count = 0
|
|
564
|
+
weighted_success = 0.0
|
|
565
|
+
weighted_failure = 0.0
|
|
566
|
+
evidence_refs: set[str] = set()
|
|
567
|
+
for item in evidence:
|
|
568
|
+
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:
|
|
572
|
+
recent_count += 1
|
|
573
|
+
evidence_refs.update(item.reference_ids)
|
|
574
|
+
if item.task_id:
|
|
575
|
+
evidence_refs.add(f'task:{item.task_id}')
|
|
576
|
+
if item.run_id:
|
|
577
|
+
evidence_refs.add(f'run:{item.run_id}')
|
|
578
|
+
signal, value = self._evidence_signal(item)
|
|
579
|
+
if signal == 'success':
|
|
580
|
+
success_count += 1
|
|
581
|
+
weighted_success += weight * value
|
|
582
|
+
elif signal == 'failure':
|
|
583
|
+
failure_count += 1
|
|
584
|
+
weighted_failure += weight * value
|
|
585
|
+
elif signal == 'rework':
|
|
586
|
+
rework_count += 1
|
|
587
|
+
weighted_failure += weight * value
|
|
588
|
+
elif signal == 'rating' and item.rating is not None:
|
|
589
|
+
rating_values.append(item.rating)
|
|
590
|
+
weighted_success += weight * max(0.0, (item.rating - 3) / 2)
|
|
591
|
+
weighted_failure += weight * max(0.0, (3 - item.rating) / 2)
|
|
592
|
+
contributions.append(
|
|
593
|
+
ProjectAgentExecutorEvidenceContribution(
|
|
594
|
+
evidence_id=item.evidence_id,
|
|
595
|
+
evidence_kind=item.evidence_kind,
|
|
596
|
+
signal=signal,
|
|
597
|
+
value=value,
|
|
598
|
+
decay_weight=weight,
|
|
599
|
+
occurred_at=occurred,
|
|
600
|
+
reference_ids=item.reference_ids,
|
|
601
|
+
)
|
|
602
|
+
)
|
|
603
|
+
sample_count = len(evidence)
|
|
604
|
+
aggregate = ProjectAgentExecutorOutcomeAggregate(
|
|
605
|
+
personal_space_id=personal_space_id,
|
|
606
|
+
personal_project_id=personal_project_id,
|
|
607
|
+
work_kind=work_kind,
|
|
608
|
+
agent_id=agent_id,
|
|
609
|
+
executor_id=executor_id,
|
|
610
|
+
model_strategy=strategy,
|
|
611
|
+
model_strategy_key=strategy_key,
|
|
612
|
+
sample_count=sample_count,
|
|
613
|
+
recent_count=recent_count,
|
|
614
|
+
success_count=success_count,
|
|
615
|
+
rework_count=rework_count,
|
|
616
|
+
failure_count=failure_count,
|
|
617
|
+
rating_count=len(rating_values),
|
|
618
|
+
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=False,
|
|
621
|
+
reset_at=reset_at,
|
|
622
|
+
as_of=current,
|
|
623
|
+
decay_half_life_days=decay_half_life_days,
|
|
624
|
+
weighted_success=weighted_success,
|
|
625
|
+
weighted_failure=weighted_failure,
|
|
626
|
+
evidence_refs=sorted(evidence_refs),
|
|
627
|
+
evidence_contributions=contributions,
|
|
628
|
+
)
|
|
629
|
+
await self.runtime.driver.execute_query(
|
|
630
|
+
'''
|
|
631
|
+
MERGE (aggregate:FuliProjectAgentExecutorOutcomeAggregate {
|
|
632
|
+
id: $aggregate_id
|
|
633
|
+
})
|
|
634
|
+
ON CREATE SET aggregate.aggregate_id = $aggregate_id,
|
|
635
|
+
aggregate.personal_space_id = $personal_space_id,
|
|
636
|
+
aggregate.personal_project_id = $personal_project_id,
|
|
637
|
+
aggregate.work_kind = $work_kind,
|
|
638
|
+
aggregate.agent_id = $agent_id,
|
|
639
|
+
aggregate.executor_id = $executor_id,
|
|
640
|
+
aggregate.model_strategy_key = $model_strategy_key
|
|
641
|
+
WITH aggregate
|
|
642
|
+
WHERE aggregate.as_of IS NULL OR aggregate.as_of <= $as_of
|
|
643
|
+
SET aggregate.aggregate_json = $aggregate_json,
|
|
644
|
+
aggregate.sample_count = $sample_count,
|
|
645
|
+
aggregate.recent_count = $recent_count,
|
|
646
|
+
aggregate.success_count = $success_count,
|
|
647
|
+
aggregate.rework_count = $rework_count,
|
|
648
|
+
aggregate.failure_count = $failure_count,
|
|
649
|
+
aggregate.rating_count = $rating_count,
|
|
650
|
+
aggregate.average_rating = $average_rating,
|
|
651
|
+
aggregate.neutral_due_to_insufficient_evidence =
|
|
652
|
+
$neutral_due_to_insufficient_evidence,
|
|
653
|
+
aggregate.reset_at = $reset_at,
|
|
654
|
+
aggregate.as_of = $as_of,
|
|
655
|
+
aggregate.decay_half_life_days = $decay_half_life_days,
|
|
656
|
+
aggregate.weighted_success = $weighted_success,
|
|
657
|
+
aggregate.weighted_failure = $weighted_failure,
|
|
658
|
+
aggregate.evidence_refs = $evidence_refs
|
|
659
|
+
''',
|
|
660
|
+
aggregate_id=aggregate_id,
|
|
661
|
+
personal_space_id=personal_space_id,
|
|
662
|
+
personal_project_id=personal_project_id,
|
|
663
|
+
work_kind=work_kind,
|
|
664
|
+
agent_id=agent_id,
|
|
665
|
+
executor_id=executor_id,
|
|
666
|
+
model_strategy_key=strategy_key,
|
|
667
|
+
aggregate_json=aggregate.model_dump_json(),
|
|
668
|
+
sample_count=aggregate.sample_count,
|
|
669
|
+
recent_count=aggregate.recent_count,
|
|
670
|
+
success_count=aggregate.success_count,
|
|
671
|
+
rework_count=aggregate.rework_count,
|
|
672
|
+
failure_count=aggregate.failure_count,
|
|
673
|
+
rating_count=aggregate.rating_count,
|
|
674
|
+
average_rating=aggregate.average_rating,
|
|
675
|
+
neutral_due_to_insufficient_evidence=(
|
|
676
|
+
aggregate.neutral_due_to_insufficient_evidence
|
|
677
|
+
),
|
|
678
|
+
reset_at=aggregate.reset_at,
|
|
679
|
+
as_of=aggregate.as_of,
|
|
680
|
+
decay_half_life_days=aggregate.decay_half_life_days,
|
|
681
|
+
weighted_success=aggregate.weighted_success,
|
|
682
|
+
weighted_failure=aggregate.weighted_failure,
|
|
683
|
+
evidence_refs=aggregate.evidence_refs,
|
|
684
|
+
)
|
|
685
|
+
return aggregate
|
|
686
|
+
|
|
687
|
+
@staticmethod
|
|
688
|
+
def _outcome_evidence_from_raw(
|
|
689
|
+
raw: dict,
|
|
690
|
+
fallback: ProjectAgentExecutorOutcomeEvidenceCreate | None = None,
|
|
691
|
+
) -> ProjectAgentExecutorOutcomeEvidenceRecord:
|
|
692
|
+
model_strategy_json = raw.get('model_strategy_json')
|
|
693
|
+
if model_strategy_json:
|
|
694
|
+
try:
|
|
695
|
+
strategy = ProjectAgentModelStrategy.model_validate_json(model_strategy_json)
|
|
696
|
+
except Exception:
|
|
697
|
+
strategy = ProjectAgentModelStrategy()
|
|
698
|
+
elif fallback:
|
|
699
|
+
strategy = fallback.model_strategy
|
|
700
|
+
else:
|
|
701
|
+
strategy = ProjectAgentModelStrategy()
|
|
702
|
+
strategy_key = raw.get('model_strategy_key')
|
|
703
|
+
if not strategy_key:
|
|
704
|
+
strategy_key = project_agent_model_strategy_key(strategy)
|
|
705
|
+
return ProjectAgentExecutorOutcomeEvidenceRecord(
|
|
706
|
+
personal_space_id=raw.get('personal_space_id') or (fallback.personal_space_id if fallback else ''),
|
|
707
|
+
personal_project_id=raw.get('personal_project_id') or (fallback.personal_project_id if fallback else ''),
|
|
708
|
+
work_kind=raw.get('work_kind') or (fallback.work_kind if fallback else ''),
|
|
709
|
+
agent_id=raw.get('agent_id') or (fallback.agent_id if fallback else ''),
|
|
710
|
+
executor_id=raw.get('executor_id') or (fallback.executor_id if fallback else ''),
|
|
711
|
+
task_id=raw.get('task_id') or (fallback.task_id if fallback else ''),
|
|
712
|
+
run_id=raw.get('run_id') or (fallback.run_id if fallback else None),
|
|
713
|
+
model_strategy=strategy,
|
|
714
|
+
model_strategy_key=strategy_key,
|
|
715
|
+
evidence_kind=raw.get('evidence_kind') or (fallback.evidence_kind if fallback else 'terminal_outcome'),
|
|
716
|
+
source=raw.get('source') or (fallback.source if fallback else 'system_terminal'),
|
|
717
|
+
terminal_outcome=raw.get('terminal_outcome') or (fallback.terminal_outcome if fallback else None),
|
|
718
|
+
rating=raw.get('rating') if raw.get('rating') is not None else (fallback.rating if fallback else None),
|
|
719
|
+
reference_ids=list(raw.get('reference_ids') or (fallback.reference_ids if fallback else [])),
|
|
720
|
+
note=raw.get('note') or (fallback.note if fallback else None),
|
|
721
|
+
idempotency_key=raw.get('idempotency_key') or (fallback.idempotency_key if fallback else 'stored-evidence-key'),
|
|
722
|
+
occurred_at=native_datetime(raw.get('occurred_at')) or (fallback.occurred_at if fallback else now_utc()),
|
|
723
|
+
evidence_id=raw.get('evidence_id') or raw.get('id') or 'stored-evidence',
|
|
724
|
+
ignored=bool(raw.get('ignored', False)),
|
|
725
|
+
ignored_reason=raw.get('ignored_reason'),
|
|
726
|
+
created_at=native_datetime(raw.get('created_at')) or now_utc(),
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
@staticmethod
|
|
730
|
+
def _evidence_signal(
|
|
731
|
+
item: ProjectAgentExecutorOutcomeEvidenceRecord,
|
|
732
|
+
) -> tuple[str, float]:
|
|
733
|
+
if item.evidence_kind in {'explicit_rating'}:
|
|
734
|
+
return 'rating', 1.0
|
|
735
|
+
if item.evidence_kind in {
|
|
736
|
+
'terminal_outcome',
|
|
737
|
+
'explicit_praise',
|
|
738
|
+
'test_passed',
|
|
739
|
+
'acceptance_passed',
|
|
740
|
+
}:
|
|
741
|
+
if item.evidence_kind == 'terminal_outcome':
|
|
742
|
+
if item.terminal_outcome in _TERMINAL_NEUTRAL:
|
|
743
|
+
return 'neutral', 0.0
|
|
744
|
+
if item.terminal_outcome == 'failed':
|
|
745
|
+
return 'failure', 1.0
|
|
746
|
+
return 'success', 1.0
|
|
747
|
+
if item.evidence_kind in {'rework_requested'}:
|
|
748
|
+
return 'rework', 1.0
|
|
749
|
+
return 'failure', 1.0
|
|
750
|
+
|
|
751
|
+
record_executor_outcome_evidence = record_project_agent_executor_outcome_evidence
|
|
752
|
+
list_executor_outcome_evidence = list_project_agent_executor_outcome_evidence
|
|
753
|
+
list_executor_outcome_aggregates = list_project_agent_executor_outcome_aggregates
|
|
754
|
+
ignore_executor_outcome_evidence = ignore_project_agent_executor_outcome_evidence
|
|
755
|
+
reset_executor_outcomes = reset_project_agent_executor_outcomes
|
|
756
|
+
aggregate_executor_outcomes = aggregate_project_agent_executor_outcomes
|