shellbrain 0.1.0__py3-none-any.whl
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.
- app/__init__.py +1 -0
- app/__main__.py +7 -0
- app/boot/__init__.py +1 -0
- app/boot/admin_db.py +88 -0
- app/boot/config.py +14 -0
- app/boot/create_policy.py +52 -0
- app/boot/db.py +70 -0
- app/boot/embeddings.py +55 -0
- app/boot/home.py +45 -0
- app/boot/migrations.py +61 -0
- app/boot/read_policy.py +179 -0
- app/boot/repos.py +15 -0
- app/boot/retrieval.py +3 -0
- app/boot/thresholds.py +19 -0
- app/boot/update_policy.py +34 -0
- app/boot/use_cases.py +22 -0
- app/config/__init__.py +1 -0
- app/config/defaults/create_policy.yaml +7 -0
- app/config/defaults/read_policy.yaml +25 -0
- app/config/defaults/runtime.yaml +10 -0
- app/config/defaults/thresholds.yaml +3 -0
- app/config/defaults/update_policy.yaml +5 -0
- app/config/loader.py +58 -0
- app/core/__init__.py +1 -0
- app/core/contracts/__init__.py +1 -0
- app/core/contracts/errors.py +29 -0
- app/core/contracts/requests.py +211 -0
- app/core/contracts/responses.py +15 -0
- app/core/entities/__init__.py +1 -0
- app/core/entities/associations.py +58 -0
- app/core/entities/episodes.py +66 -0
- app/core/entities/evidence.py +29 -0
- app/core/entities/facts.py +30 -0
- app/core/entities/guidance.py +47 -0
- app/core/entities/identity.py +48 -0
- app/core/entities/memory.py +34 -0
- app/core/entities/runtime_context.py +19 -0
- app/core/entities/session_state.py +31 -0
- app/core/entities/telemetry.py +152 -0
- app/core/entities/utility.py +14 -0
- app/core/interfaces/__init__.py +1 -0
- app/core/interfaces/clock.py +12 -0
- app/core/interfaces/config.py +28 -0
- app/core/interfaces/embeddings.py +12 -0
- app/core/interfaces/idgen.py +11 -0
- app/core/interfaces/repos.py +279 -0
- app/core/interfaces/retrieval.py +20 -0
- app/core/interfaces/session_state_store.py +33 -0
- app/core/interfaces/unit_of_work.py +50 -0
- app/core/policies/__init__.py +1 -0
- app/core/policies/_shared/__init__.py +1 -0
- app/core/policies/_shared/executor.py +132 -0
- app/core/policies/_shared/side_effects.py +9 -0
- app/core/policies/create_policy/__init__.py +1 -0
- app/core/policies/create_policy/pipeline.py +96 -0
- app/core/policies/read_policy/__init__.py +1 -0
- app/core/policies/read_policy/bm25.py +114 -0
- app/core/policies/read_policy/context_pack_builder.py +140 -0
- app/core/policies/read_policy/expansion.py +132 -0
- app/core/policies/read_policy/fusion_rrf.py +34 -0
- app/core/policies/read_policy/lexical_query.py +101 -0
- app/core/policies/read_policy/pipeline.py +93 -0
- app/core/policies/read_policy/scenario_lift.py +11 -0
- app/core/policies/read_policy/scoring.py +61 -0
- app/core/policies/read_policy/seed_retrieval.py +54 -0
- app/core/policies/read_policy/utility_prior.py +11 -0
- app/core/policies/update_policy/__init__.py +1 -0
- app/core/policies/update_policy/pipeline.py +80 -0
- app/core/use_cases/__init__.py +1 -0
- app/core/use_cases/build_guidance.py +85 -0
- app/core/use_cases/create_memory.py +26 -0
- app/core/use_cases/manage_session_state.py +159 -0
- app/core/use_cases/read_memory.py +21 -0
- app/core/use_cases/record_episode_sync_telemetry.py +19 -0
- app/core/use_cases/record_operation_telemetry.py +32 -0
- app/core/use_cases/sync_episode.py +162 -0
- app/core/use_cases/update_memory.py +40 -0
- app/migrations/__init__.py +1 -0
- app/migrations/env.py +65 -0
- app/migrations/versions/20260226_0001_initial_schema.py +232 -0
- app/migrations/versions/20260312_0002_add_hard_invariants.py +60 -0
- app/migrations/versions/20260312_0003_drop_create_confidence.py +40 -0
- app/migrations/versions/20260313_0004_episode_sync_hardening.py +71 -0
- app/migrations/versions/20260313_0005_evidence_episode_event_refs.py +45 -0
- app/migrations/versions/20260318_0006_usage_telemetry_schema.py +175 -0
- app/migrations/versions/20260319_0007_identity_session_guidance.py +49 -0
- app/migrations/versions/20260320_0008_instance_metadata_and_backup_safety.py +31 -0
- app/migrations/versions/__init__.py +1 -0
- app/periphery/__init__.py +1 -0
- app/periphery/admin/__init__.py +1 -0
- app/periphery/admin/backup.py +360 -0
- app/periphery/admin/destructive_guard.py +32 -0
- app/periphery/admin/doctor.py +192 -0
- app/periphery/admin/init.py +996 -0
- app/periphery/admin/instance_guard.py +211 -0
- app/periphery/admin/machine_state.py +354 -0
- app/periphery/admin/privileges.py +42 -0
- app/periphery/admin/repo_state.py +266 -0
- app/periphery/admin/restore.py +30 -0
- app/periphery/cli/__init__.py +1 -0
- app/periphery/cli/handlers.py +830 -0
- app/periphery/cli/hydration.py +119 -0
- app/periphery/cli/main.py +710 -0
- app/periphery/cli/presenter_json.py +10 -0
- app/periphery/cli/schema_validation.py +201 -0
- app/periphery/db/__init__.py +1 -0
- app/periphery/db/engine.py +10 -0
- app/periphery/db/models/__init__.py +1 -0
- app/periphery/db/models/associations.py +55 -0
- app/periphery/db/models/episodes.py +55 -0
- app/periphery/db/models/evidence.py +19 -0
- app/periphery/db/models/experiences.py +33 -0
- app/periphery/db/models/instance_metadata.py +17 -0
- app/periphery/db/models/memories.py +39 -0
- app/periphery/db/models/metadata.py +6 -0
- app/periphery/db/models/registry.py +18 -0
- app/periphery/db/models/telemetry.py +174 -0
- app/periphery/db/models/utility.py +19 -0
- app/periphery/db/models/views.py +154 -0
- app/periphery/db/repos/__init__.py +1 -0
- app/periphery/db/repos/relational/__init__.py +1 -0
- app/periphery/db/repos/relational/associations_repo.py +117 -0
- app/periphery/db/repos/relational/episodes_repo.py +188 -0
- app/periphery/db/repos/relational/evidence_repo.py +82 -0
- app/periphery/db/repos/relational/experiences_repo.py +41 -0
- app/periphery/db/repos/relational/memories_repo.py +99 -0
- app/periphery/db/repos/relational/read_policy_repo.py +202 -0
- app/periphery/db/repos/relational/telemetry_repo.py +161 -0
- app/periphery/db/repos/relational/utility_repo.py +30 -0
- app/periphery/db/repos/semantic/__init__.py +1 -0
- app/periphery/db/repos/semantic/keyword_retrieval_repo.py +63 -0
- app/periphery/db/repos/semantic/semantic_retrieval_repo.py +111 -0
- app/periphery/db/session.py +10 -0
- app/periphery/db/uow.py +75 -0
- app/periphery/embeddings/__init__.py +1 -0
- app/periphery/embeddings/local_provider.py +35 -0
- app/periphery/embeddings/query_vector_search.py +18 -0
- app/periphery/episodes/__init__.py +1 -0
- app/periphery/episodes/claude_code.py +387 -0
- app/periphery/episodes/codex.py +423 -0
- app/periphery/episodes/launcher.py +66 -0
- app/periphery/episodes/normalization.py +31 -0
- app/periphery/episodes/poller.py +299 -0
- app/periphery/episodes/source_discovery.py +66 -0
- app/periphery/episodes/tool_filter.py +165 -0
- app/periphery/identity/__init__.py +1 -0
- app/periphery/identity/claude_hook_install.py +67 -0
- app/periphery/identity/claude_runtime.py +83 -0
- app/periphery/identity/codex_runtime.py +32 -0
- app/periphery/identity/compatibility.py +38 -0
- app/periphery/identity/resolver.py +163 -0
- app/periphery/session_state/__init__.py +1 -0
- app/periphery/session_state/file_store.py +100 -0
- app/periphery/telemetry/__init__.py +33 -0
- app/periphery/telemetry/operation_summary.py +299 -0
- app/periphery/telemetry/session_selection.py +156 -0
- app/periphery/telemetry/sync_summary.py +65 -0
- app/periphery/validation/__init__.py +1 -0
- app/periphery/validation/integrity_validation.py +253 -0
- app/periphery/validation/semantic_validation.py +94 -0
- shellbrain-0.1.0.dist-info/METADATA +130 -0
- shellbrain-0.1.0.dist-info/RECORD +165 -0
- shellbrain-0.1.0.dist-info/WHEEL +5 -0
- shellbrain-0.1.0.dist-info/entry_points.txt +2 -0
- shellbrain-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
"""This module defines CLI command handlers that dispatch to core use-case functions."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import replace
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from time import perf_counter
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
from app.boot.create_policy import get_create_hydration_defaults, get_create_policy_settings, validate_create_policy_settings
|
|
11
|
+
from app.boot.read_policy import get_read_hydration_defaults
|
|
12
|
+
from app.boot.update_policy import get_update_policy_settings, validate_update_policy_settings
|
|
13
|
+
from app.core.contracts.errors import ErrorCode, ErrorDetail
|
|
14
|
+
from app.core.contracts.requests import (
|
|
15
|
+
EpisodeEventsRequest,
|
|
16
|
+
MemoryBatchUpdateRequest,
|
|
17
|
+
MemoryCreateRequest,
|
|
18
|
+
MemoryUpdateRequest,
|
|
19
|
+
)
|
|
20
|
+
from app.core.contracts.responses import OperationResult
|
|
21
|
+
from app.core.entities.guidance import GuidanceDecision
|
|
22
|
+
from app.core.entities.identity import CallerIdentity, IdentityTrustLevel
|
|
23
|
+
from app.core.entities.telemetry import OperationDispatchTelemetryContext, SessionSelectionSummary
|
|
24
|
+
from app.core.use_cases.build_guidance import build_pending_utility_guidance
|
|
25
|
+
from app.core.use_cases.manage_session_state import SessionStateManager
|
|
26
|
+
from app.core.use_cases.create_memory import execute_create_memory
|
|
27
|
+
from app.core.use_cases.read_memory import execute_read_memory
|
|
28
|
+
from app.core.use_cases.record_episode_sync_telemetry import record_episode_sync_telemetry
|
|
29
|
+
from app.core.use_cases.record_operation_telemetry import record_operation_telemetry
|
|
30
|
+
from app.core.use_cases.sync_episode import sync_episode
|
|
31
|
+
from app.core.use_cases.update_memory import execute_update_memory
|
|
32
|
+
from app.periphery.cli.hydration import (
|
|
33
|
+
hydrate_create_payload,
|
|
34
|
+
hydrate_events_payload,
|
|
35
|
+
hydrate_read_payload,
|
|
36
|
+
hydrate_update_payload,
|
|
37
|
+
)
|
|
38
|
+
from app.periphery.cli.schema_validation import (
|
|
39
|
+
validate_create_schema,
|
|
40
|
+
validate_events_schema,
|
|
41
|
+
validate_internal_create_contract,
|
|
42
|
+
validate_internal_events_contract,
|
|
43
|
+
validate_internal_read_contract,
|
|
44
|
+
validate_internal_update_contract,
|
|
45
|
+
validate_read_schema,
|
|
46
|
+
validate_update_schema,
|
|
47
|
+
)
|
|
48
|
+
from app.periphery.episodes.normalization import normalize_host_transcript
|
|
49
|
+
from app.periphery.identity.resolver import (
|
|
50
|
+
discover_untrusted_events_candidate,
|
|
51
|
+
resolve_caller_identity,
|
|
52
|
+
resolve_trusted_events_source,
|
|
53
|
+
)
|
|
54
|
+
from app.periphery.session_state.file_store import FileSessionStateStore
|
|
55
|
+
from app.periphery.telemetry import get_operation_telemetry_context
|
|
56
|
+
from app.periphery.telemetry.operation_summary import (
|
|
57
|
+
build_operation_invocation_record,
|
|
58
|
+
build_read_summary_records,
|
|
59
|
+
build_write_summary_records,
|
|
60
|
+
infer_error_stage_from_errors,
|
|
61
|
+
)
|
|
62
|
+
from app.periphery.telemetry.session_selection import (
|
|
63
|
+
EventsDiscoveryCandidate,
|
|
64
|
+
summarize_runtime_selection,
|
|
65
|
+
)
|
|
66
|
+
from app.periphery.telemetry.sync_summary import build_episode_sync_records
|
|
67
|
+
from app.periphery.validation.integrity_validation import validate_create_integrity, validate_update_integrity
|
|
68
|
+
from app.periphery.validation.semantic_validation import validate_create_semantics, validate_update_semantics
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _error_response(errors: list[ErrorDetail]) -> dict:
|
|
72
|
+
"""This function builds a standardized error response envelope for CLI handlers."""
|
|
73
|
+
|
|
74
|
+
return OperationResult(status="error", errors=errors).model_dump(mode="python")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _validate_create_request(request: MemoryCreateRequest, *, uow, gates: list[str]) -> list[ErrorDetail]:
|
|
78
|
+
"""Run non-schema create validations before invoking core execution."""
|
|
79
|
+
|
|
80
|
+
if "semantic" in gates:
|
|
81
|
+
semantic_errors = validate_create_semantics(request)
|
|
82
|
+
if semantic_errors:
|
|
83
|
+
return semantic_errors
|
|
84
|
+
if "integrity" in gates:
|
|
85
|
+
return validate_create_integrity(request, uow)
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _validate_update_request(request: MemoryUpdateRequest | MemoryBatchUpdateRequest, *, uow, gates: list[str]) -> list[ErrorDetail]:
|
|
90
|
+
"""Run non-schema update validations before invoking core execution."""
|
|
91
|
+
|
|
92
|
+
if "semantic" in gates:
|
|
93
|
+
semantic_errors = validate_update_semantics(request)
|
|
94
|
+
if semantic_errors:
|
|
95
|
+
return semantic_errors
|
|
96
|
+
if "integrity" in gates:
|
|
97
|
+
return validate_update_integrity(request, uow)
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def handle_create(
|
|
102
|
+
payload: dict,
|
|
103
|
+
*,
|
|
104
|
+
uow_factory,
|
|
105
|
+
embedding_provider_factory,
|
|
106
|
+
embedding_model: str,
|
|
107
|
+
inferred_repo_id: str,
|
|
108
|
+
defaults: dict | None = None,
|
|
109
|
+
telemetry_context: OperationDispatchTelemetryContext | None = None,
|
|
110
|
+
repo_root: Path | None = None,
|
|
111
|
+
):
|
|
112
|
+
"""This function validates and dispatches a create payload to the create use-case."""
|
|
113
|
+
|
|
114
|
+
started_at = perf_counter()
|
|
115
|
+
resolved_repo_root = (repo_root or Path.cwd()).resolve()
|
|
116
|
+
resolved_telemetry_context = _ensure_telemetry_context(telemetry_context=telemetry_context, repo_root=resolved_repo_root)
|
|
117
|
+
session_manager = SessionStateManager(store=FileSessionStateStore())
|
|
118
|
+
request: MemoryCreateRequest | None = None
|
|
119
|
+
result: dict | None = None
|
|
120
|
+
error_stage: str | None = None
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
policy_errors = validate_create_policy_settings()
|
|
124
|
+
if policy_errors:
|
|
125
|
+
error_stage = infer_error_stage_from_errors(_dump_errors(policy_errors), default_stage="contract_validation")
|
|
126
|
+
result = _error_response(policy_errors)
|
|
127
|
+
else:
|
|
128
|
+
policy = get_create_policy_settings()
|
|
129
|
+
agent_request, errors = validate_create_schema(payload)
|
|
130
|
+
if errors:
|
|
131
|
+
error_stage = infer_error_stage_from_errors(_dump_errors(errors), default_stage="schema_validation")
|
|
132
|
+
result = _error_response(errors)
|
|
133
|
+
else:
|
|
134
|
+
assert agent_request is not None
|
|
135
|
+
resolved_defaults = defaults if defaults is not None else get_create_hydration_defaults()
|
|
136
|
+
hydrated_payload = hydrate_create_payload(
|
|
137
|
+
agent_request.model_dump(mode="python", exclude_none=True),
|
|
138
|
+
inferred_repo_id=inferred_repo_id,
|
|
139
|
+
defaults=resolved_defaults,
|
|
140
|
+
)
|
|
141
|
+
request, contract_errors = validate_internal_create_contract(hydrated_payload)
|
|
142
|
+
if contract_errors:
|
|
143
|
+
error_stage = infer_error_stage_from_errors(
|
|
144
|
+
_dump_errors(contract_errors),
|
|
145
|
+
default_stage="contract_validation",
|
|
146
|
+
)
|
|
147
|
+
result = _error_response(contract_errors)
|
|
148
|
+
else:
|
|
149
|
+
assert request is not None
|
|
150
|
+
with uow_factory() as uow:
|
|
151
|
+
validation_errors = _validate_create_request(request, uow=uow, gates=policy["gates"])
|
|
152
|
+
if validation_errors:
|
|
153
|
+
error_stage = infer_error_stage_from_errors(
|
|
154
|
+
_dump_errors(validation_errors),
|
|
155
|
+
default_stage="semantic_validation",
|
|
156
|
+
)
|
|
157
|
+
result = _error_response(validation_errors)
|
|
158
|
+
else:
|
|
159
|
+
embedding_provider = embedding_provider_factory()
|
|
160
|
+
result = execute_create_memory(
|
|
161
|
+
request,
|
|
162
|
+
uow,
|
|
163
|
+
embedding_provider=embedding_provider,
|
|
164
|
+
embedding_model=embedding_model,
|
|
165
|
+
).model_dump(mode="python")
|
|
166
|
+
if result.get("status") == "ok":
|
|
167
|
+
session_state = session_manager.load_active_state(
|
|
168
|
+
repo_root=resolved_repo_root,
|
|
169
|
+
caller_identity=resolved_telemetry_context.caller_identity,
|
|
170
|
+
)
|
|
171
|
+
request_links = request.memory.links
|
|
172
|
+
if request.memory.kind == "problem":
|
|
173
|
+
session_state = session_manager.record_problem(
|
|
174
|
+
repo_root=resolved_repo_root,
|
|
175
|
+
caller_identity=resolved_telemetry_context.caller_identity,
|
|
176
|
+
problem_id=str(result["data"]["memory_id"]),
|
|
177
|
+
)
|
|
178
|
+
elif request.memory.kind in {"solution", "failed_tactic"} and request_links.problem_id:
|
|
179
|
+
session_state = session_manager.record_problem(
|
|
180
|
+
repo_root=resolved_repo_root,
|
|
181
|
+
caller_identity=resolved_telemetry_context.caller_identity,
|
|
182
|
+
problem_id=request_links.problem_id,
|
|
183
|
+
)
|
|
184
|
+
strong_guidance = request.memory.kind == "solution"
|
|
185
|
+
guidance = _build_guidance_payloads(
|
|
186
|
+
uow_factory=uow_factory,
|
|
187
|
+
repo_id=inferred_repo_id,
|
|
188
|
+
caller_identity=resolved_telemetry_context.caller_identity,
|
|
189
|
+
session_state=session_state,
|
|
190
|
+
strong=strong_guidance,
|
|
191
|
+
)
|
|
192
|
+
if guidance:
|
|
193
|
+
_attach_guidance(result, guidance)
|
|
194
|
+
if session_state is not None and session_state.current_problem_id is not None:
|
|
195
|
+
session_manager.record_guidance(
|
|
196
|
+
repo_root=resolved_repo_root,
|
|
197
|
+
caller_identity=resolved_telemetry_context.caller_identity,
|
|
198
|
+
problem_id=session_state.current_problem_id,
|
|
199
|
+
)
|
|
200
|
+
except Exception as exc: # pragma: no cover - defensive fallback envelope
|
|
201
|
+
error_stage = "internal_error"
|
|
202
|
+
result = _error_response([ErrorDetail(code=ErrorCode.INTERNAL_ERROR, message=str(exc))])
|
|
203
|
+
|
|
204
|
+
assert result is not None
|
|
205
|
+
_persist_operation_telemetry_best_effort(
|
|
206
|
+
command="create",
|
|
207
|
+
uow_factory=uow_factory,
|
|
208
|
+
repo_id=inferred_repo_id,
|
|
209
|
+
telemetry_context=resolved_telemetry_context,
|
|
210
|
+
result=result,
|
|
211
|
+
error_stage=error_stage,
|
|
212
|
+
request=request,
|
|
213
|
+
agent_payload=payload,
|
|
214
|
+
total_latency_ms=int((perf_counter() - started_at) * 1000),
|
|
215
|
+
)
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def handle_read(
|
|
220
|
+
payload: dict,
|
|
221
|
+
*,
|
|
222
|
+
uow_factory,
|
|
223
|
+
inferred_repo_id: str,
|
|
224
|
+
defaults: dict | None = None,
|
|
225
|
+
telemetry_context: OperationDispatchTelemetryContext | None = None,
|
|
226
|
+
repo_root: Path | None = None,
|
|
227
|
+
):
|
|
228
|
+
"""This function validates and dispatches a read payload to the read use-case."""
|
|
229
|
+
|
|
230
|
+
started_at = perf_counter()
|
|
231
|
+
resolved_repo_root = (repo_root or Path.cwd()).resolve()
|
|
232
|
+
resolved_telemetry_context = _ensure_telemetry_context(telemetry_context=telemetry_context, repo_root=resolved_repo_root)
|
|
233
|
+
session_manager = SessionStateManager(store=FileSessionStateStore())
|
|
234
|
+
session_manager.load_active_state(
|
|
235
|
+
repo_root=resolved_repo_root,
|
|
236
|
+
caller_identity=resolved_telemetry_context.caller_identity,
|
|
237
|
+
)
|
|
238
|
+
request = None
|
|
239
|
+
result: dict | None = None
|
|
240
|
+
error_stage: str | None = None
|
|
241
|
+
try:
|
|
242
|
+
agent_request, errors = validate_read_schema(payload)
|
|
243
|
+
if errors:
|
|
244
|
+
error_stage = infer_error_stage_from_errors(_dump_errors(errors), default_stage="schema_validation")
|
|
245
|
+
result = _error_response(errors)
|
|
246
|
+
else:
|
|
247
|
+
assert agent_request is not None
|
|
248
|
+
resolved_defaults = defaults if defaults is not None else get_read_hydration_defaults()
|
|
249
|
+
hydrated_payload = hydrate_read_payload(
|
|
250
|
+
agent_request.model_dump(mode="python", exclude_none=True),
|
|
251
|
+
inferred_repo_id=inferred_repo_id,
|
|
252
|
+
defaults=resolved_defaults,
|
|
253
|
+
)
|
|
254
|
+
request, contract_errors = validate_internal_read_contract(hydrated_payload)
|
|
255
|
+
if contract_errors:
|
|
256
|
+
error_stage = infer_error_stage_from_errors(
|
|
257
|
+
_dump_errors(contract_errors),
|
|
258
|
+
default_stage="contract_validation",
|
|
259
|
+
)
|
|
260
|
+
result = _error_response(contract_errors)
|
|
261
|
+
else:
|
|
262
|
+
assert request is not None
|
|
263
|
+
with uow_factory() as uow:
|
|
264
|
+
result = execute_read_memory(request, uow).model_dump(mode="python")
|
|
265
|
+
except Exception as exc: # pragma: no cover - defensive fallback envelope
|
|
266
|
+
error_stage = "internal_error"
|
|
267
|
+
result = _error_response([ErrorDetail(code=ErrorCode.INTERNAL_ERROR, message=str(exc))])
|
|
268
|
+
|
|
269
|
+
assert result is not None
|
|
270
|
+
_persist_operation_telemetry_best_effort(
|
|
271
|
+
command="read",
|
|
272
|
+
uow_factory=uow_factory,
|
|
273
|
+
repo_id=inferred_repo_id,
|
|
274
|
+
telemetry_context=resolved_telemetry_context,
|
|
275
|
+
result=result,
|
|
276
|
+
error_stage=error_stage,
|
|
277
|
+
request=request,
|
|
278
|
+
agent_payload=payload,
|
|
279
|
+
total_latency_ms=int((perf_counter() - started_at) * 1000),
|
|
280
|
+
)
|
|
281
|
+
return result
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def handle_events(
|
|
285
|
+
payload: dict,
|
|
286
|
+
*,
|
|
287
|
+
uow_factory,
|
|
288
|
+
inferred_repo_id: str,
|
|
289
|
+
repo_root: Path | None = None,
|
|
290
|
+
search_roots_by_host: dict[str, list[Path]] | None = None,
|
|
291
|
+
telemetry_context: OperationDispatchTelemetryContext | None = None,
|
|
292
|
+
):
|
|
293
|
+
"""Validate and dispatch an events payload to the active-episode browsing flow."""
|
|
294
|
+
|
|
295
|
+
started_at = perf_counter()
|
|
296
|
+
resolved_repo_root = (repo_root or Path.cwd()).resolve()
|
|
297
|
+
resolved_telemetry_context = _ensure_telemetry_context(
|
|
298
|
+
telemetry_context=telemetry_context,
|
|
299
|
+
repo_root=resolved_repo_root,
|
|
300
|
+
)
|
|
301
|
+
session_manager = SessionStateManager(store=FileSessionStateStore())
|
|
302
|
+
request = None
|
|
303
|
+
result: dict | None = None
|
|
304
|
+
error_stage: str | None = None
|
|
305
|
+
selection_summary = SessionSelectionSummary()
|
|
306
|
+
sync_run = None
|
|
307
|
+
sync_tool_types = ()
|
|
308
|
+
try:
|
|
309
|
+
agent_request, errors = validate_events_schema(payload)
|
|
310
|
+
if errors:
|
|
311
|
+
error_stage = infer_error_stage_from_errors(_dump_errors(errors), default_stage="schema_validation")
|
|
312
|
+
result = _error_response(errors)
|
|
313
|
+
else:
|
|
314
|
+
assert agent_request is not None
|
|
315
|
+
hydrated_payload = hydrate_events_payload(
|
|
316
|
+
agent_request.model_dump(mode="python", exclude_none=True),
|
|
317
|
+
inferred_repo_id=inferred_repo_id,
|
|
318
|
+
)
|
|
319
|
+
request, contract_errors = validate_internal_events_contract(hydrated_payload)
|
|
320
|
+
if contract_errors:
|
|
321
|
+
error_stage = infer_error_stage_from_errors(
|
|
322
|
+
_dump_errors(contract_errors),
|
|
323
|
+
default_stage="contract_validation",
|
|
324
|
+
)
|
|
325
|
+
result = _error_response(contract_errors)
|
|
326
|
+
else:
|
|
327
|
+
assert request is not None
|
|
328
|
+
source = _resolve_events_source(
|
|
329
|
+
repo_root=resolved_repo_root,
|
|
330
|
+
search_roots_by_host=search_roots_by_host,
|
|
331
|
+
runtime_context=resolved_telemetry_context,
|
|
332
|
+
)
|
|
333
|
+
selection_summary = _selection_summary_from_events_source(source)
|
|
334
|
+
sync_started_at = perf_counter()
|
|
335
|
+
try:
|
|
336
|
+
normalized_events = normalize_host_transcript(
|
|
337
|
+
host_app=str(source.host_app),
|
|
338
|
+
host_session_key=str(source.host_session_key),
|
|
339
|
+
transcript_path=Path(str(source.transcript_path)),
|
|
340
|
+
)
|
|
341
|
+
with uow_factory() as uow:
|
|
342
|
+
sync_result = sync_episode(
|
|
343
|
+
repo_id=request.repo_id,
|
|
344
|
+
host_app=str(source.host_app),
|
|
345
|
+
host_session_key=str(source.host_session_key),
|
|
346
|
+
thread_id=str(source.canonical_thread_id),
|
|
347
|
+
transcript_path=str(source.transcript_path),
|
|
348
|
+
normalized_events=normalized_events,
|
|
349
|
+
uow=uow,
|
|
350
|
+
)
|
|
351
|
+
events = uow.episodes.list_recent_events(
|
|
352
|
+
repo_id=request.repo_id,
|
|
353
|
+
episode_id=str(sync_result["episode_id"]),
|
|
354
|
+
limit=request.limit,
|
|
355
|
+
)
|
|
356
|
+
result = OperationResult(
|
|
357
|
+
status="ok",
|
|
358
|
+
data={
|
|
359
|
+
"episode_id": sync_result["episode_id"],
|
|
360
|
+
"host_app": source.host_app,
|
|
361
|
+
"thread_id": sync_result["thread_id"],
|
|
362
|
+
"events": [_serialize_episode_event(event) for event in events],
|
|
363
|
+
},
|
|
364
|
+
).model_dump(mode="python")
|
|
365
|
+
selection_summary = replace(selection_summary, selected_episode_id=str(sync_result["episode_id"]))
|
|
366
|
+
if source.trusted:
|
|
367
|
+
session_manager.record_events(
|
|
368
|
+
repo_root=resolved_repo_root,
|
|
369
|
+
caller_identity=resolved_telemetry_context.caller_identity,
|
|
370
|
+
episode_id=str(sync_result["episode_id"]),
|
|
371
|
+
event_ids=[str(event["id"]) for event in result["data"]["events"]],
|
|
372
|
+
)
|
|
373
|
+
sync_run, sync_tool_types = build_episode_sync_records(
|
|
374
|
+
sync_run_id=str(uuid4()),
|
|
375
|
+
source="events_inline",
|
|
376
|
+
invocation_id=resolved_telemetry_context.invocation_id,
|
|
377
|
+
repo_id=request.repo_id,
|
|
378
|
+
host_app=str(source.host_app),
|
|
379
|
+
host_session_key=str(source.host_session_key),
|
|
380
|
+
thread_id=str(sync_result["thread_id"]),
|
|
381
|
+
episode_id=str(sync_result["episode_id"]),
|
|
382
|
+
transcript_path=str(sync_result["transcript_path"]),
|
|
383
|
+
outcome="ok",
|
|
384
|
+
error_stage=None,
|
|
385
|
+
error_message=None,
|
|
386
|
+
duration_ms=int((perf_counter() - sync_started_at) * 1000),
|
|
387
|
+
imported_event_count=int(sync_result["imported_event_count"]),
|
|
388
|
+
total_event_count=int(sync_result["total_event_count"]),
|
|
389
|
+
user_event_count=int(sync_result["user_event_count"]),
|
|
390
|
+
assistant_event_count=int(sync_result["assistant_event_count"]),
|
|
391
|
+
tool_event_count=int(sync_result["tool_event_count"]),
|
|
392
|
+
system_event_count=int(sync_result["system_event_count"]),
|
|
393
|
+
tool_type_counts=dict(sync_result["tool_type_counts"]),
|
|
394
|
+
)
|
|
395
|
+
except Exception as exc:
|
|
396
|
+
error_stage = "sync"
|
|
397
|
+
result = _error_response([ErrorDetail(code=ErrorCode.INTERNAL_ERROR, message=str(exc))])
|
|
398
|
+
sync_run, sync_tool_types = build_episode_sync_records(
|
|
399
|
+
sync_run_id=str(uuid4()),
|
|
400
|
+
source="events_inline",
|
|
401
|
+
invocation_id=resolved_telemetry_context.invocation_id,
|
|
402
|
+
repo_id=request.repo_id,
|
|
403
|
+
host_app=str(source.host_app),
|
|
404
|
+
host_session_key=str(source.host_session_key),
|
|
405
|
+
thread_id=selection_summary.selected_thread_id or str(source.canonical_thread_id),
|
|
406
|
+
episode_id=selection_summary.selected_episode_id,
|
|
407
|
+
transcript_path=str(source.transcript_path),
|
|
408
|
+
outcome="error",
|
|
409
|
+
error_stage="sync",
|
|
410
|
+
error_message=str(exc),
|
|
411
|
+
duration_ms=int((perf_counter() - sync_started_at) * 1000),
|
|
412
|
+
imported_event_count=0,
|
|
413
|
+
total_event_count=0,
|
|
414
|
+
user_event_count=0,
|
|
415
|
+
assistant_event_count=0,
|
|
416
|
+
tool_event_count=0,
|
|
417
|
+
system_event_count=0,
|
|
418
|
+
tool_type_counts={},
|
|
419
|
+
)
|
|
420
|
+
except _EventsSelectionError as exc:
|
|
421
|
+
error_stage = "session_selection"
|
|
422
|
+
result = _error_response([exc.error])
|
|
423
|
+
except Exception as exc: # pragma: no cover - defensive fallback envelope
|
|
424
|
+
error_stage = "internal_error"
|
|
425
|
+
result = _error_response([ErrorDetail(code=ErrorCode.INTERNAL_ERROR, message=str(exc))])
|
|
426
|
+
|
|
427
|
+
assert result is not None
|
|
428
|
+
_persist_operation_telemetry_best_effort(
|
|
429
|
+
command="events",
|
|
430
|
+
uow_factory=uow_factory,
|
|
431
|
+
repo_id=inferred_repo_id,
|
|
432
|
+
telemetry_context=resolved_telemetry_context,
|
|
433
|
+
result=result,
|
|
434
|
+
error_stage=error_stage,
|
|
435
|
+
request=request,
|
|
436
|
+
selection_summary=selection_summary,
|
|
437
|
+
sync_run=sync_run,
|
|
438
|
+
sync_tool_types=sync_tool_types,
|
|
439
|
+
total_latency_ms=int((perf_counter() - started_at) * 1000),
|
|
440
|
+
)
|
|
441
|
+
return result
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def handle_update(
|
|
445
|
+
payload: dict,
|
|
446
|
+
*,
|
|
447
|
+
uow_factory,
|
|
448
|
+
inferred_repo_id: str,
|
|
449
|
+
telemetry_context: OperationDispatchTelemetryContext | None = None,
|
|
450
|
+
repo_root: Path | None = None,
|
|
451
|
+
):
|
|
452
|
+
"""This function validates and dispatches an update payload to the update use-case."""
|
|
453
|
+
|
|
454
|
+
started_at = perf_counter()
|
|
455
|
+
resolved_repo_root = (repo_root or Path.cwd()).resolve()
|
|
456
|
+
resolved_telemetry_context = _ensure_telemetry_context(telemetry_context=telemetry_context, repo_root=resolved_repo_root)
|
|
457
|
+
session_manager = SessionStateManager(store=FileSessionStateStore())
|
|
458
|
+
session_state = session_manager.load_active_state(
|
|
459
|
+
repo_root=resolved_repo_root,
|
|
460
|
+
caller_identity=resolved_telemetry_context.caller_identity,
|
|
461
|
+
)
|
|
462
|
+
request = None
|
|
463
|
+
result: dict | None = None
|
|
464
|
+
error_stage: str | None = None
|
|
465
|
+
try:
|
|
466
|
+
policy_errors = validate_update_policy_settings()
|
|
467
|
+
if policy_errors:
|
|
468
|
+
error_stage = infer_error_stage_from_errors(_dump_errors(policy_errors), default_stage="contract_validation")
|
|
469
|
+
result = _error_response(policy_errors)
|
|
470
|
+
else:
|
|
471
|
+
policy = get_update_policy_settings()
|
|
472
|
+
agent_request, errors = validate_update_schema(payload)
|
|
473
|
+
if errors:
|
|
474
|
+
error_stage = infer_error_stage_from_errors(_dump_errors(errors), default_stage="schema_validation")
|
|
475
|
+
result = _error_response(errors)
|
|
476
|
+
else:
|
|
477
|
+
assert agent_request is not None
|
|
478
|
+
hydrated_payload = hydrate_update_payload(
|
|
479
|
+
agent_request.model_dump(mode="python", exclude_none=True),
|
|
480
|
+
inferred_repo_id=inferred_repo_id,
|
|
481
|
+
)
|
|
482
|
+
request, contract_errors = validate_internal_update_contract(hydrated_payload)
|
|
483
|
+
if contract_errors:
|
|
484
|
+
error_stage = infer_error_stage_from_errors(
|
|
485
|
+
_dump_errors(contract_errors),
|
|
486
|
+
default_stage="contract_validation",
|
|
487
|
+
)
|
|
488
|
+
result = _error_response(contract_errors)
|
|
489
|
+
else:
|
|
490
|
+
assert request is not None
|
|
491
|
+
request, hydration_errors = _hydrate_update_request_evidence_from_session_state(
|
|
492
|
+
request=request,
|
|
493
|
+
session_state=session_state,
|
|
494
|
+
)
|
|
495
|
+
if hydration_errors:
|
|
496
|
+
error_stage = infer_error_stage_from_errors(
|
|
497
|
+
_dump_errors(hydration_errors),
|
|
498
|
+
default_stage="semantic_validation",
|
|
499
|
+
)
|
|
500
|
+
result = _error_response(hydration_errors)
|
|
501
|
+
request = None
|
|
502
|
+
raise _ReturnHandledError()
|
|
503
|
+
with uow_factory() as uow:
|
|
504
|
+
validation_errors = _validate_update_request(request, uow=uow, gates=policy["gates"])
|
|
505
|
+
if validation_errors:
|
|
506
|
+
error_stage = infer_error_stage_from_errors(
|
|
507
|
+
_dump_errors(validation_errors),
|
|
508
|
+
default_stage="semantic_validation",
|
|
509
|
+
)
|
|
510
|
+
result = _error_response(validation_errors)
|
|
511
|
+
else:
|
|
512
|
+
result = execute_update_memory(request, uow).model_dump(mode="python")
|
|
513
|
+
guidance = _build_guidance_payloads(
|
|
514
|
+
uow_factory=uow_factory,
|
|
515
|
+
repo_id=inferred_repo_id,
|
|
516
|
+
caller_identity=resolved_telemetry_context.caller_identity,
|
|
517
|
+
session_state=session_state,
|
|
518
|
+
strong=False,
|
|
519
|
+
)
|
|
520
|
+
if guidance:
|
|
521
|
+
_attach_guidance(result, guidance)
|
|
522
|
+
if session_state is not None and session_state.current_problem_id is not None:
|
|
523
|
+
session_manager.record_guidance(
|
|
524
|
+
repo_root=resolved_repo_root,
|
|
525
|
+
caller_identity=resolved_telemetry_context.caller_identity,
|
|
526
|
+
problem_id=session_state.current_problem_id,
|
|
527
|
+
)
|
|
528
|
+
except _ReturnHandledError:
|
|
529
|
+
pass
|
|
530
|
+
except Exception as exc: # pragma: no cover - defensive fallback envelope
|
|
531
|
+
error_stage = "internal_error"
|
|
532
|
+
result = _error_response([ErrorDetail(code=ErrorCode.INTERNAL_ERROR, message=str(exc))])
|
|
533
|
+
|
|
534
|
+
assert result is not None
|
|
535
|
+
_persist_operation_telemetry_best_effort(
|
|
536
|
+
command="update",
|
|
537
|
+
uow_factory=uow_factory,
|
|
538
|
+
repo_id=inferred_repo_id,
|
|
539
|
+
telemetry_context=resolved_telemetry_context,
|
|
540
|
+
result=result,
|
|
541
|
+
error_stage=error_stage,
|
|
542
|
+
request=request,
|
|
543
|
+
agent_payload=payload,
|
|
544
|
+
total_latency_ms=int((perf_counter() - started_at) * 1000),
|
|
545
|
+
)
|
|
546
|
+
return result
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
class _EventsSelectionError(Exception):
|
|
550
|
+
"""Internal control-flow exception for expected events selection failures."""
|
|
551
|
+
|
|
552
|
+
def __init__(self, error: ErrorDetail) -> None:
|
|
553
|
+
super().__init__(error.message)
|
|
554
|
+
self.error = error
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class _ReturnHandledError(Exception):
|
|
558
|
+
"""Internal control-flow exception for already-materialized responses."""
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _resolve_events_source(
|
|
562
|
+
*,
|
|
563
|
+
repo_root: Path,
|
|
564
|
+
search_roots_by_host: dict[str, list[Path]] | None,
|
|
565
|
+
runtime_context: OperationDispatchTelemetryContext,
|
|
566
|
+
):
|
|
567
|
+
"""Resolve the exact trusted events source or an untrusted fallback candidate."""
|
|
568
|
+
|
|
569
|
+
caller_identity = runtime_context.caller_identity
|
|
570
|
+
if runtime_context.caller_identity_error is not None:
|
|
571
|
+
raise _EventsSelectionError(runtime_context.caller_identity_error)
|
|
572
|
+
|
|
573
|
+
if caller_identity is not None and caller_identity.trust_level == IdentityTrustLevel.TRUSTED:
|
|
574
|
+
source = resolve_trusted_events_source(
|
|
575
|
+
caller_identity=caller_identity,
|
|
576
|
+
repo_root=repo_root,
|
|
577
|
+
search_roots_by_host=search_roots_by_host,
|
|
578
|
+
)
|
|
579
|
+
if source.error is not None:
|
|
580
|
+
raise _EventsSelectionError(source.error)
|
|
581
|
+
return source
|
|
582
|
+
|
|
583
|
+
fallback = discover_untrusted_events_candidate(
|
|
584
|
+
repo_root=repo_root,
|
|
585
|
+
search_roots_by_host=search_roots_by_host,
|
|
586
|
+
)
|
|
587
|
+
if fallback is None:
|
|
588
|
+
raise _EventsSelectionError(
|
|
589
|
+
ErrorDetail(
|
|
590
|
+
code=ErrorCode.NOT_FOUND,
|
|
591
|
+
message="No active host session found for this repo",
|
|
592
|
+
)
|
|
593
|
+
)
|
|
594
|
+
return fallback
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _serialize_episode_event(event) -> dict:
|
|
598
|
+
"""Render one stored episode event into deterministic JSON-safe output."""
|
|
599
|
+
|
|
600
|
+
content = str(event.content)
|
|
601
|
+
try:
|
|
602
|
+
parsed_content = json.loads(content)
|
|
603
|
+
except json.JSONDecodeError:
|
|
604
|
+
parsed_content = content
|
|
605
|
+
created_at = event.created_at.isoformat() if event.created_at is not None else None
|
|
606
|
+
return {
|
|
607
|
+
"id": event.id,
|
|
608
|
+
"seq": event.seq,
|
|
609
|
+
"source": event.source.value if hasattr(event.source, "value") else str(event.source),
|
|
610
|
+
"content": parsed_content,
|
|
611
|
+
"created_at": created_at,
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _ensure_telemetry_context(
|
|
616
|
+
*,
|
|
617
|
+
telemetry_context: OperationDispatchTelemetryContext | None,
|
|
618
|
+
repo_root: Path | None,
|
|
619
|
+
) -> OperationDispatchTelemetryContext:
|
|
620
|
+
"""Return the active handler telemetry context or synthesize one for direct calls."""
|
|
621
|
+
|
|
622
|
+
if telemetry_context is not None:
|
|
623
|
+
return telemetry_context
|
|
624
|
+
inherited = get_operation_telemetry_context()
|
|
625
|
+
if inherited is not None:
|
|
626
|
+
return inherited
|
|
627
|
+
caller_identity_resolution = resolve_caller_identity()
|
|
628
|
+
return OperationDispatchTelemetryContext(
|
|
629
|
+
invocation_id=str(uuid4()),
|
|
630
|
+
repo_root=str((repo_root or Path.cwd()).resolve()),
|
|
631
|
+
no_sync=False,
|
|
632
|
+
caller_identity=caller_identity_resolution.caller_identity,
|
|
633
|
+
caller_identity_error=caller_identity_resolution.error,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _dump_errors(errors: list[ErrorDetail]) -> list[dict]:
|
|
638
|
+
"""Render structured errors into plain dicts for telemetry stage mapping."""
|
|
639
|
+
|
|
640
|
+
return [error.model_dump(mode="python") for error in errors]
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _persist_operation_telemetry_best_effort(
|
|
644
|
+
*,
|
|
645
|
+
command: str,
|
|
646
|
+
uow_factory,
|
|
647
|
+
repo_id: str,
|
|
648
|
+
telemetry_context: OperationDispatchTelemetryContext,
|
|
649
|
+
result: dict,
|
|
650
|
+
error_stage: str | None,
|
|
651
|
+
request=None,
|
|
652
|
+
agent_payload: dict | None = None,
|
|
653
|
+
selection_summary: SessionSelectionSummary | None = None,
|
|
654
|
+
sync_run=None,
|
|
655
|
+
sync_tool_types=(),
|
|
656
|
+
total_latency_ms: int | None = None,
|
|
657
|
+
) -> None:
|
|
658
|
+
"""Persist invocation telemetry in a second best-effort transaction."""
|
|
659
|
+
|
|
660
|
+
try:
|
|
661
|
+
with uow_factory() as telemetry_uow:
|
|
662
|
+
resolved_selection = selection_summary
|
|
663
|
+
if resolved_selection is None:
|
|
664
|
+
resolved_selection = _selection_summary_from_runtime_context(
|
|
665
|
+
caller_identity=telemetry_context.caller_identity,
|
|
666
|
+
repo_id=repo_id,
|
|
667
|
+
repo_root=Path(telemetry_context.repo_root),
|
|
668
|
+
uow=telemetry_uow,
|
|
669
|
+
)
|
|
670
|
+
invocation = build_operation_invocation_record(
|
|
671
|
+
command=command,
|
|
672
|
+
repo_id=repo_id,
|
|
673
|
+
runtime_context=telemetry_context,
|
|
674
|
+
selection_summary=resolved_selection,
|
|
675
|
+
result=result,
|
|
676
|
+
error_stage=error_stage,
|
|
677
|
+
total_latency_ms=total_latency_ms if total_latency_ms is not None else 0,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
read_summary = None
|
|
681
|
+
read_items = ()
|
|
682
|
+
write_summary = None
|
|
683
|
+
write_items = ()
|
|
684
|
+
|
|
685
|
+
if result.get("status") == "ok" and command == "read" and request is not None:
|
|
686
|
+
pack = result.get("data", {}).get("pack", {})
|
|
687
|
+
if isinstance(pack, dict):
|
|
688
|
+
read_summary, read_items = build_read_summary_records(
|
|
689
|
+
invocation_id=telemetry_context.invocation_id,
|
|
690
|
+
agent_payload=agent_payload or {},
|
|
691
|
+
request=request,
|
|
692
|
+
pack=pack,
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
if result.get("status") == "ok" and command in {"create", "update"} and request is not None:
|
|
696
|
+
planned_side_effects = result.get("data", {}).get("planned_side_effects", [])
|
|
697
|
+
if isinstance(planned_side_effects, list):
|
|
698
|
+
write_summary, write_items = build_write_summary_records(
|
|
699
|
+
invocation_id=telemetry_context.invocation_id,
|
|
700
|
+
command=command,
|
|
701
|
+
request=request,
|
|
702
|
+
planned_side_effects=planned_side_effects,
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
record_operation_telemetry(
|
|
706
|
+
uow=telemetry_uow,
|
|
707
|
+
invocation=invocation,
|
|
708
|
+
read_summary=read_summary,
|
|
709
|
+
read_items=read_items,
|
|
710
|
+
write_summary=write_summary,
|
|
711
|
+
write_items=write_items,
|
|
712
|
+
)
|
|
713
|
+
if sync_run is not None:
|
|
714
|
+
record_episode_sync_telemetry(
|
|
715
|
+
uow=telemetry_uow,
|
|
716
|
+
run=sync_run,
|
|
717
|
+
tool_types=sync_tool_types,
|
|
718
|
+
)
|
|
719
|
+
except Exception:
|
|
720
|
+
return
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _selection_summary_from_events_source(source) -> SessionSelectionSummary:
|
|
724
|
+
"""Build telemetry selection summary from one resolved events source."""
|
|
725
|
+
|
|
726
|
+
return SessionSelectionSummary(
|
|
727
|
+
selected_host_app=source.host_app,
|
|
728
|
+
selected_host_session_key=source.host_session_key,
|
|
729
|
+
selected_thread_id=source.canonical_thread_id,
|
|
730
|
+
matching_candidate_count=source.matching_candidate_count,
|
|
731
|
+
selection_ambiguous=source.selection_ambiguous,
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def _selection_summary_from_runtime_context(*, caller_identity: CallerIdentity | None, repo_id: str, repo_root: Path, uow) -> SessionSelectionSummary:
|
|
736
|
+
"""Build lightweight non-events selection summary from trusted caller identity when present."""
|
|
737
|
+
|
|
738
|
+
if caller_identity is None or caller_identity.trust_level != IdentityTrustLevel.TRUSTED:
|
|
739
|
+
return summarize_runtime_selection(repo_root=repo_root, repo_id=repo_id, uow=uow)
|
|
740
|
+
selected_episode_id = None
|
|
741
|
+
episode = uow.episodes.get_episode_by_thread(repo_id=repo_id, thread_id=caller_identity.canonical_id or "")
|
|
742
|
+
if episode is not None:
|
|
743
|
+
selected_episode_id = episode.id
|
|
744
|
+
return SessionSelectionSummary(
|
|
745
|
+
selected_host_app=caller_identity.host_app,
|
|
746
|
+
selected_host_session_key=caller_identity.host_session_key,
|
|
747
|
+
selected_thread_id=caller_identity.canonical_id,
|
|
748
|
+
selected_episode_id=selected_episode_id,
|
|
749
|
+
matching_candidate_count=1,
|
|
750
|
+
selection_ambiguous=False,
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _build_guidance_payloads(*, uow_factory, repo_id: str, caller_identity: CallerIdentity | None, session_state, strong: bool) -> list[dict]:
|
|
755
|
+
"""Build public guidance payloads from telemetry and session state."""
|
|
756
|
+
|
|
757
|
+
if session_state is None:
|
|
758
|
+
return []
|
|
759
|
+
with uow_factory() as guidance_uow:
|
|
760
|
+
decisions = build_pending_utility_guidance(
|
|
761
|
+
repo_id=repo_id,
|
|
762
|
+
caller_identity=caller_identity,
|
|
763
|
+
session_state=session_state,
|
|
764
|
+
telemetry=guidance_uow.telemetry,
|
|
765
|
+
now_iso=datetime.now(timezone.utc).isoformat(),
|
|
766
|
+
strong=strong,
|
|
767
|
+
)
|
|
768
|
+
return [decision.to_payload() for decision in decisions]
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _attach_guidance(result: dict, guidance_payloads: list[dict]) -> None:
|
|
772
|
+
"""Attach one or more guidance payloads to a successful result."""
|
|
773
|
+
|
|
774
|
+
data = result.setdefault("data", {})
|
|
775
|
+
if not isinstance(data, dict):
|
|
776
|
+
return
|
|
777
|
+
data["guidance"] = guidance_payloads
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _hydrate_update_request_evidence_from_session_state(*, request, session_state):
|
|
781
|
+
"""Auto-fill missing utility evidence refs from session state when possible."""
|
|
782
|
+
|
|
783
|
+
if session_state is None:
|
|
784
|
+
if isinstance(request, MemoryBatchUpdateRequest):
|
|
785
|
+
if any(item.update.evidence_refs for item in request.updates):
|
|
786
|
+
return request, []
|
|
787
|
+
return request, _missing_events_evidence_errors(request)
|
|
788
|
+
if request.update.type != "utility_vote" or request.update.evidence_refs:
|
|
789
|
+
return request, []
|
|
790
|
+
return request, _missing_events_evidence_errors(request)
|
|
791
|
+
|
|
792
|
+
if isinstance(request, MemoryBatchUpdateRequest):
|
|
793
|
+
if any(item.update.evidence_refs for item in request.updates):
|
|
794
|
+
return request, []
|
|
795
|
+
if not session_state.last_events_event_ids:
|
|
796
|
+
return request, _missing_events_evidence_errors(request)
|
|
797
|
+
payload = request.model_dump(mode="python")
|
|
798
|
+
for item in payload["updates"]:
|
|
799
|
+
item["update"]["evidence_refs"] = list(session_state.last_events_event_ids)
|
|
800
|
+
return MemoryBatchUpdateRequest.model_validate(payload), []
|
|
801
|
+
|
|
802
|
+
if request.update.type != "utility_vote" or request.update.evidence_refs:
|
|
803
|
+
return request, []
|
|
804
|
+
if not session_state.last_events_event_ids:
|
|
805
|
+
return request, _missing_events_evidence_errors(request)
|
|
806
|
+
payload = request.model_dump(mode="python")
|
|
807
|
+
payload["update"]["evidence_refs"] = list(session_state.last_events_event_ids)
|
|
808
|
+
return MemoryUpdateRequest.model_validate(payload), []
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _missing_events_evidence_errors(request) -> list[ErrorDetail]:
|
|
812
|
+
"""Return the canonical semantic error when utility evidence cannot be auto-filled."""
|
|
813
|
+
|
|
814
|
+
if isinstance(request, MemoryBatchUpdateRequest):
|
|
815
|
+
return [
|
|
816
|
+
ErrorDetail(
|
|
817
|
+
code=ErrorCode.SEMANTIC_ERROR,
|
|
818
|
+
message="Batch utility votes require recent episode evidence; run `events` first.",
|
|
819
|
+
field="updates",
|
|
820
|
+
)
|
|
821
|
+
]
|
|
822
|
+
if getattr(request.update, "type", None) == "utility_vote":
|
|
823
|
+
return [
|
|
824
|
+
ErrorDetail(
|
|
825
|
+
code=ErrorCode.SEMANTIC_ERROR,
|
|
826
|
+
message="utility_vote requires recent episode evidence; run `events` first.",
|
|
827
|
+
field="update.evidence_refs",
|
|
828
|
+
)
|
|
829
|
+
]
|
|
830
|
+
return []
|