crprotocol 2.0.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.
Files changed (153) hide show
  1. crp/__init__.py +126 -0
  2. crp/__main__.py +8 -0
  3. crp/_typing.py +27 -0
  4. crp/_version.py +5 -0
  5. crp/adapters.py +31 -0
  6. crp/advanced/__init__.py +40 -0
  7. crp/advanced/auto_ingest.py +400 -0
  8. crp/advanced/cqs.py +235 -0
  9. crp/advanced/cross_window.py +477 -0
  10. crp/advanced/curator.py +265 -0
  11. crp/advanced/feedback.py +146 -0
  12. crp/advanced/hierarchical.py +211 -0
  13. crp/advanced/meta_learning.py +401 -0
  14. crp/advanced/parallel.py +98 -0
  15. crp/advanced/review_cycle.py +329 -0
  16. crp/advanced/scale_mode.py +129 -0
  17. crp/advanced/source_grounding.py +207 -0
  18. crp/ckf/__init__.py +35 -0
  19. crp/ckf/community.py +377 -0
  20. crp/ckf/fabric.py +445 -0
  21. crp/ckf/gc.py +175 -0
  22. crp/ckf/graph_walk.py +87 -0
  23. crp/ckf/merge.py +133 -0
  24. crp/ckf/pattern_query.py +122 -0
  25. crp/ckf/pubsub.py +128 -0
  26. crp/ckf/semantic.py +207 -0
  27. crp/cli/__init__.py +7 -0
  28. crp/cli/main.py +329 -0
  29. crp/cli/sidecar.py +929 -0
  30. crp/cli/startup.py +272 -0
  31. crp/continuation/__init__.py +103 -0
  32. crp/continuation/completion.py +348 -0
  33. crp/continuation/degradation.py +157 -0
  34. crp/continuation/document_map.py +160 -0
  35. crp/continuation/flow.py +109 -0
  36. crp/continuation/gap.py +419 -0
  37. crp/continuation/manager.py +484 -0
  38. crp/continuation/quality_monitor.py +179 -0
  39. crp/continuation/stitch.py +419 -0
  40. crp/continuation/trigger.py +142 -0
  41. crp/continuation/voice.py +157 -0
  42. crp/core/__init__.py +69 -0
  43. crp/core/batch.py +77 -0
  44. crp/core/circuit_breaker.py +116 -0
  45. crp/core/config.py +377 -0
  46. crp/core/context_tools.py +540 -0
  47. crp/core/dispatch_router.py +3977 -0
  48. crp/core/errors.py +128 -0
  49. crp/core/extraction_facade.py +384 -0
  50. crp/core/facilitator.py +713 -0
  51. crp/core/idempotency.py +215 -0
  52. crp/core/orchestrator.py +1435 -0
  53. crp/core/relay_strategies.py +613 -0
  54. crp/core/security_manager.py +140 -0
  55. crp/core/session.py +134 -0
  56. crp/core/task_intent.py +36 -0
  57. crp/core/window.py +363 -0
  58. crp/envelope/__init__.py +30 -0
  59. crp/envelope/builder.py +288 -0
  60. crp/envelope/decomposer.py +236 -0
  61. crp/envelope/formatter.py +168 -0
  62. crp/envelope/packer.py +211 -0
  63. crp/envelope/reranker.py +209 -0
  64. crp/envelope/scoring.py +310 -0
  65. crp/extraction/__init__.py +45 -0
  66. crp/extraction/complexity.py +96 -0
  67. crp/extraction/contradiction.py +132 -0
  68. crp/extraction/pipeline.py +360 -0
  69. crp/extraction/quality_gate.py +237 -0
  70. crp/extraction/stage1_regex.py +173 -0
  71. crp/extraction/stage2_statistical.py +244 -0
  72. crp/extraction/stage3_gliner.py +210 -0
  73. crp/extraction/stage4_uie.py +183 -0
  74. crp/extraction/stage5_discourse.py +175 -0
  75. crp/extraction/stage6_llm.py +178 -0
  76. crp/extraction/structured_output.py +219 -0
  77. crp/extraction/types.py +299 -0
  78. crp/license_guard.py +722 -0
  79. crp/observability/__init__.py +30 -0
  80. crp/observability/audit.py +118 -0
  81. crp/observability/events.py +233 -0
  82. crp/observability/metrics.py +264 -0
  83. crp/observability/quality.py +135 -0
  84. crp/observability/structured_logging.py +81 -0
  85. crp/observability/telemetry.py +117 -0
  86. crp/provenance/__init__.py +314 -0
  87. crp/provenance/_embeddings.py +97 -0
  88. crp/provenance/_types.py +378 -0
  89. crp/provenance/attribution_scorer.py +252 -0
  90. crp/provenance/claim_detector.py +229 -0
  91. crp/provenance/contradiction_detector.py +243 -0
  92. crp/provenance/distortion_detector.py +397 -0
  93. crp/provenance/entailment_verifier.py +358 -0
  94. crp/provenance/fabrication_detector.py +203 -0
  95. crp/provenance/hallucination_scorer.py +320 -0
  96. crp/provenance/omission_analyzer.py +106 -0
  97. crp/provenance/provenance_chain.py +205 -0
  98. crp/provenance/report_generator.py +440 -0
  99. crp/providers/__init__.py +43 -0
  100. crp/providers/anthropic.py +270 -0
  101. crp/providers/base.py +135 -0
  102. crp/providers/custom.py +63 -0
  103. crp/providers/diagnostic.py +251 -0
  104. crp/providers/llamacpp.py +224 -0
  105. crp/providers/manager.py +139 -0
  106. crp/providers/ollama.py +243 -0
  107. crp/providers/openai.py +628 -0
  108. crp/providers/tokenizers.py +48 -0
  109. crp/py.typed +0 -0
  110. crp/resources/__init__.py +53 -0
  111. crp/resources/adaptive_allocator.py +525 -0
  112. crp/resources/cost_model.py +388 -0
  113. crp/resources/overhead_manager.py +217 -0
  114. crp/resources/resource_manager.py +262 -0
  115. crp/schemas/__init__.py +20 -0
  116. crp/schemas/cost-estimate.json +33 -0
  117. crp/schemas/crp-error.json +43 -0
  118. crp/schemas/envelope-preview.json +40 -0
  119. crp/schemas/persisted-state-header.json +27 -0
  120. crp/schemas/quality-report.json +94 -0
  121. crp/schemas/session-handle.json +33 -0
  122. crp/schemas/session-status.json +57 -0
  123. crp/schemas/stream-event.json +18 -0
  124. crp/schemas/task-intent.json +42 -0
  125. crp/security/__init__.py +93 -0
  126. crp/security/audit_trail.py +392 -0
  127. crp/security/binding.py +192 -0
  128. crp/security/compliance.py +813 -0
  129. crp/security/consent.py +593 -0
  130. crp/security/embedding_defense.py +161 -0
  131. crp/security/encryption.py +202 -0
  132. crp/security/injection.py +335 -0
  133. crp/security/integrity.py +267 -0
  134. crp/security/privacy.py +662 -0
  135. crp/security/quarantine.py +249 -0
  136. crp/security/rbac.py +221 -0
  137. crp/security/validation.py +164 -0
  138. crp/state/__init__.py +31 -0
  139. crp/state/cold_storage.py +258 -0
  140. crp/state/compaction.py +263 -0
  141. crp/state/critical_state.py +104 -0
  142. crp/state/event_log.py +313 -0
  143. crp/state/fact.py +189 -0
  144. crp/state/serialization.py +189 -0
  145. crp/state/session_cleanup.py +77 -0
  146. crp/state/snapshot.py +290 -0
  147. crp/state/warm_store.py +346 -0
  148. crprotocol-2.0.0.dist-info/METADATA +1295 -0
  149. crprotocol-2.0.0.dist-info/RECORD +153 -0
  150. crprotocol-2.0.0.dist-info/WHEEL +4 -0
  151. crprotocol-2.0.0.dist-info/entry_points.txt +2 -0
  152. crprotocol-2.0.0.dist-info/licenses/LICENSE.md +170 -0
  153. crprotocol-2.0.0.dist-info/licenses/NOTICE +18 -0
@@ -0,0 +1,713 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """CRP Facilitator — LLM-in-the-Loop cognitive engine (§22).
4
+
5
+ ╔═══════════════════════════════════════════════════════════════════╗
6
+ ║ PARADIGM INVERSION ║
7
+ ║ ║
8
+ ║ Traditional: User → LLM → Text ║
9
+ ║ RAG: User → Context + LLM → Text ║
10
+ ║ Agentic: LLM (controller) → Tools → LLM → Output ║
11
+ ║ ║
12
+ ║ CRP §22: CRP (orchestrator) → LLM (cognitive engine) ║
13
+ ║ → CRP (acts on reasoning) → LLM (generates) ║
14
+ ║ → CRP (evaluates via LLM) → CRP (curates) ║
15
+ ║ ║
16
+ ║ The LLM is INSIDE CRP. CRP is the system. ║
17
+ ║ The LLM provides reasoning. CRP provides structure. ║
18
+ ╚═══════════════════════════════════════════════════════════════════╝
19
+
20
+ The CRPFacilitator uses the LLM for six cognitive functions:
21
+
22
+ §22.1 TASK ANALYSIS — Parse task complexity, domain, knowledge needs
23
+ §22.2 STRATEGY ROUTING — Choose optimal dispatch strategy
24
+ §22.3 FACT SYNTHESIS — Merge/compress/derive new knowledge from facts
25
+ §22.4 OUTPUT EVALUATION — Assess output quality against task intent
26
+ §22.5 MEMORY CURATION — Decide what knowledge to keep/merge/discard
27
+ §22.6 EXECUTION PLANNING — Decompose complex tasks into sub-tasks
28
+
29
+ The facilitator wraps each cognitive call in a structured prompt
30
+ with constrained JSON output. CRP interprets the JSON and acts
31
+ on it — the LLM never touches CRP's internal state directly.
32
+
33
+ Inspired by:
34
+ - Xi et al., "The Rise and Potential of LLM-Based Agents" (2023):
35
+ Agent = Brain(LLM) + Perception + Action
36
+ - Park et al., "Generative Agents" (2023):
37
+ Observation → Planning → Reflection cognitive loop
38
+ - Packer et al., "MemGPT" (2023):
39
+ LLM manages its own memory via structured interrupts
40
+
41
+ CRP differs from all three: the LLM is NOT the controller.
42
+ CRP IS the controller. The LLM is the reasoning module.
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import json
48
+ import logging
49
+ import re
50
+ import time
51
+ from dataclasses import dataclass, field
52
+ from typing import Any, Callable, TYPE_CHECKING
53
+
54
+ if TYPE_CHECKING:
55
+ from crp.extraction.types import Fact
56
+ from crp.providers.base import LLMProvider
57
+ from crp.state.warm_store import WarmStateStore
58
+
59
+ logger = logging.getLogger("crp.facilitator")
60
+
61
+
62
+ # ═══════════════════════════════════════════════════════════════════════
63
+ # Cognitive output data structures
64
+ # ═══════════════════════════════════════════════════════════════════════
65
+
66
+ @dataclass
67
+ class TaskAnalysis:
68
+ """LLM's analysis of a task — §22.1."""
69
+ complexity: str = "medium" # "simple", "medium", "complex", "multi_part"
70
+ domain: str = "general" # Detected domain/topic
71
+ knowledge_needs: list[str] = field(default_factory=list) # What knowledge the task requires
72
+ expected_output_length: str = "medium" # "short", "medium", "long", "variable"
73
+ requires_factual_grounding: bool = True
74
+ requires_creativity: bool = False
75
+ requires_reasoning: bool = False
76
+ subtasks: list[str] = field(default_factory=list) # If multi_part
77
+ confidence: float = 0.8
78
+
79
+
80
+ @dataclass
81
+ class StrategyDecision:
82
+ """LLM's routing decision — §22.2."""
83
+ strategy: str = "push" # "push", "pull", "reflexive", "progressive", "stream_augmented", "agentic"
84
+ reasoning: str = "" # Why this strategy was chosen
85
+ envelope_priority: str = "balanced" # "minimal", "balanced", "maximal"
86
+ continuation_likelihood: str = "low" # "none", "low", "medium", "high"
87
+ confidence: float = 0.7
88
+
89
+
90
+ @dataclass
91
+ class SynthesizedKnowledge:
92
+ """LLM-synthesized knowledge from existing facts — §22.3."""
93
+ summary: str = "" # Coherent synthesis of input facts
94
+ key_insights: list[str] = field(default_factory=list) # Distilled insights
95
+ contradictions: list[str] = field(default_factory=list) # Detected contradictions
96
+ knowledge_gaps: list[str] = field(default_factory=list) # Gaps identified
97
+ redundant_fact_ids: list[str] = field(default_factory=list) # Facts that are redundant
98
+ merged_facts: list[dict[str, str]] = field(default_factory=list) # New merged facts
99
+
100
+
101
+ @dataclass
102
+ class OutputEvaluation:
103
+ """LLM's evaluation of CRP's output — §22.4."""
104
+ task_completion: float = 0.0 # 0-1: how well the output addresses the task
105
+ factual_accuracy: float = 0.0 # 0-1: estimated accuracy
106
+ coherence: float = 0.0 # 0-1: logical flow and structure
107
+ missing_elements: list[str] = field(default_factory=list) # What's missing
108
+ revision_needed: bool = False
109
+ revision_focus: str = "" # What to focus on in revision
110
+ overall_grade: str = "B" # S/A/B/C/D
111
+
112
+
113
+ @dataclass
114
+ class CurationDecision:
115
+ """LLM's memory curation recommendations — §22.5."""
116
+ promote_ids: list[str] = field(default_factory=list) # Fact IDs to boost confidence
117
+ demote_ids: list[str] = field(default_factory=list) # Fact IDs to reduce confidence
118
+ merge_groups: list[list[str]] = field(default_factory=list) # Groups of IDs to merge
119
+ discard_ids: list[str] = field(default_factory=list) # Fact IDs to remove
120
+ reasoning: str = ""
121
+
122
+
123
+ @dataclass
124
+ class ExecutionPlan:
125
+ """LLM's execution plan for complex tasks — §22.6."""
126
+ steps: list[PlanStep] = field(default_factory=list)
127
+ estimated_windows: int = 1
128
+ parallel_possible: bool = False
129
+
130
+
131
+ @dataclass
132
+ class PlanStep:
133
+ """One step in an execution plan."""
134
+ description: str = ""
135
+ strategy: str = "push" # Which dispatch strategy
136
+ context_needs: list[str] = field(default_factory=list)
137
+ depends_on: list[int] = field(default_factory=list) # Step indices this depends on
138
+ priority: int = 1 # 1=highest
139
+
140
+
141
+ @dataclass
142
+ class FacilitatorMetrics:
143
+ """Telemetry for facilitator cognitive calls."""
144
+ task_analysis_ms: float = 0.0
145
+ strategy_routing_ms: float = 0.0
146
+ fact_synthesis_ms: float = 0.0
147
+ output_evaluation_ms: float = 0.0
148
+ memory_curation_ms: float = 0.0
149
+ execution_planning_ms: float = 0.0
150
+ total_cognitive_tokens: int = 0
151
+ cognitive_calls: int = 0
152
+
153
+ @property
154
+ def total_cognitive_ms(self) -> float:
155
+ return (self.task_analysis_ms + self.strategy_routing_ms +
156
+ self.fact_synthesis_ms + self.output_evaluation_ms +
157
+ self.memory_curation_ms + self.execution_planning_ms)
158
+
159
+
160
+ # ═══════════════════════════════════════════════════════════════════════
161
+ # Core Facilitator Engine
162
+ # ═══════════════════════════════════════════════════════════════════════
163
+
164
+ class CRPFacilitator:
165
+ """LLM-in-the-Loop cognitive engine for CRP.
166
+
167
+ The facilitator uses the LLM for CRP's internal reasoning:
168
+ - Task analysis (what kind of task is this?)
169
+ - Strategy routing (which dispatch method?)
170
+ - Fact synthesis (merge/compress knowledge)
171
+ - Output evaluation (did we actually help?)
172
+ - Memory curation (what to keep/discard?)
173
+ - Execution planning (how to handle complex tasks?)
174
+
175
+ The LLM never touches CRP state directly. It returns structured
176
+ JSON that CRP interprets and acts upon. This maintains CRP's
177
+ sovereignty over its own subsystems while leveraging the LLM's
178
+ reasoning capability at every decision point.
179
+ """
180
+
181
+ # Maximum tokens budget for each cognitive call
182
+ MAX_ANALYSIS_TOKENS = 500
183
+ MAX_ROUTING_TOKENS = 300
184
+ MAX_SYNTHESIS_TOKENS = 800
185
+ MAX_EVALUATION_TOKENS = 500
186
+ MAX_CURATION_TOKENS = 500
187
+ MAX_PLANNING_TOKENS = 600
188
+
189
+ def __init__(
190
+ self,
191
+ provider: LLMProvider,
192
+ count_tokens: Callable[[str], int],
193
+ ):
194
+ self._provider = provider
195
+ self._count_tokens = count_tokens
196
+ self._metrics = FacilitatorMetrics()
197
+
198
+ @property
199
+ def metrics(self) -> FacilitatorMetrics:
200
+ return self._metrics
201
+
202
+ def reset_metrics(self) -> None:
203
+ self._metrics = FacilitatorMetrics()
204
+
205
+ # ───────────────────────────────────────────────────────────────
206
+ # §22.1 TASK ANALYSIS
207
+ # ───────────────────────────────────────────────────────────────
208
+
209
+ def analyze_task(
210
+ self,
211
+ task_input: str,
212
+ system_prompt: str,
213
+ fact_count: int = 0,
214
+ ) -> TaskAnalysis:
215
+ """Use the LLM to analyze what kind of task this is.
216
+
217
+ Instead of CRP guessing task complexity via regex, the LLM
218
+ actually *understands* the task semantically. This feeds
219
+ into every downstream decision: strategy routing, envelope
220
+ budget, continuation thresholds, extraction depth.
221
+ """
222
+ prompt = _TASK_ANALYSIS_PROMPT.format(
223
+ system_prompt=system_prompt[:300],
224
+ task_input=task_input[:500],
225
+ fact_count=fact_count,
226
+ )
227
+
228
+ start = time.monotonic_ns()
229
+ raw = self._cognitive_call(prompt, self.MAX_ANALYSIS_TOKENS)
230
+ self._metrics.task_analysis_ms = (time.monotonic_ns() - start) / 1_000_000
231
+
232
+ return _parse_task_analysis(raw)
233
+
234
+ # ───────────────────────────────────────────────────────────────
235
+ # §22.2 STRATEGY ROUTING
236
+ # ───────────────────────────────────────────────────────────────
237
+
238
+ def route_strategy(
239
+ self,
240
+ analysis: TaskAnalysis,
241
+ fact_count: int,
242
+ available_strategies: list[str] | None = None,
243
+ ) -> StrategyDecision:
244
+ """Use the LLM to choose the optimal dispatch strategy.
245
+
246
+ Given the task analysis and CRP's current state (fact count,
247
+ available strategies), the LLM reasons about which approach
248
+ will produce the best output.
249
+
250
+ This replaces the hardcoded "always use push" default.
251
+ """
252
+ strategies = available_strategies or [
253
+ "push", "pull", "reflexive", "progressive", "stream_augmented",
254
+ ]
255
+
256
+ prompt = _STRATEGY_ROUTING_PROMPT.format(
257
+ complexity=analysis.complexity,
258
+ domain=analysis.domain,
259
+ knowledge_needs=", ".join(analysis.knowledge_needs[:5]),
260
+ output_length=analysis.expected_output_length,
261
+ requires_grounding=analysis.requires_factual_grounding,
262
+ requires_creativity=analysis.requires_creativity,
263
+ fact_count=fact_count,
264
+ available_strategies=", ".join(strategies),
265
+ )
266
+
267
+ start = time.monotonic_ns()
268
+ raw = self._cognitive_call(prompt, self.MAX_ROUTING_TOKENS)
269
+ self._metrics.strategy_routing_ms = (time.monotonic_ns() - start) / 1_000_000
270
+
271
+ return _parse_strategy_decision(raw, strategies)
272
+
273
+ # ───────────────────────────────────────────────────────────────
274
+ # §22.3 FACT SYNTHESIS
275
+ # ───────────────────────────────────────────────────────────────
276
+
277
+ def synthesize_facts(
278
+ self,
279
+ facts: list[tuple[str, str, float]], # [(id, text, confidence), ...]
280
+ task_context: str = "",
281
+ ) -> SynthesizedKnowledge:
282
+ """Use the LLM to synthesize coherent knowledge from raw facts.
283
+
284
+ Instead of CRP dumping raw facts into an envelope, the LLM:
285
+ - Merges overlapping facts into coherent statements
286
+ - Identifies contradictions
287
+ - Spots knowledge gaps
288
+ - Ranks by actual relevance to the task
289
+
290
+ This produces BETTER context than raw fact packing.
291
+ """
292
+ if not facts:
293
+ return SynthesizedKnowledge()
294
+
295
+ facts_block = "\n".join(
296
+ f"[{fid}] (conf={conf:.0%}) {text}"
297
+ for fid, text, conf in facts[:30] # Cap at 30 for token budget
298
+ )
299
+
300
+ prompt = _FACT_SYNTHESIS_PROMPT.format(
301
+ task_context=task_context[:200],
302
+ facts_block=facts_block,
303
+ fact_count=len(facts),
304
+ )
305
+
306
+ start = time.monotonic_ns()
307
+ raw = self._cognitive_call(prompt, self.MAX_SYNTHESIS_TOKENS)
308
+ self._metrics.fact_synthesis_ms = (time.monotonic_ns() - start) / 1_000_000
309
+
310
+ return _parse_synthesis(raw)
311
+
312
+ # ───────────────────────────────────────────────────────────────
313
+ # §22.4 OUTPUT EVALUATION
314
+ # ───────────────────────────────────────────────────────────────
315
+
316
+ def evaluate_output(
317
+ self,
318
+ task_input: str,
319
+ output: str,
320
+ facts_used: int = 0,
321
+ ) -> OutputEvaluation:
322
+ """Use the LLM to evaluate CRP's own output.
323
+
324
+ After generation, the LLM assesses whether the output
325
+ actually addresses the task. This replaces the heuristic
326
+ quality scoring (fact count × saturation × token count).
327
+
328
+ The evaluation feeds back into:
329
+ - Whether to trigger continuation
330
+ - Whether to try a different strategy
331
+ - Quality tier classification
332
+ - Memory curation decisions
333
+ """
334
+ prompt = _OUTPUT_EVALUATION_PROMPT.format(
335
+ task_input=task_input[:300],
336
+ output=output[:1500],
337
+ facts_used=facts_used,
338
+ )
339
+
340
+ start = time.monotonic_ns()
341
+ raw = self._cognitive_call(prompt, self.MAX_EVALUATION_TOKENS)
342
+ self._metrics.output_evaluation_ms = (time.monotonic_ns() - start) / 1_000_000
343
+
344
+ return _parse_evaluation(raw)
345
+
346
+ # ───────────────────────────────────────────────────────────────
347
+ # §22.5 MEMORY CURATION
348
+ # ───────────────────────────────────────────────────────────────
349
+
350
+ def curate_memory(
351
+ self,
352
+ facts: list[tuple[str, str, float, int]], # [(id, text, confidence, age), ...]
353
+ recent_task: str = "",
354
+ ) -> CurationDecision:
355
+ """Use the LLM to curate CRP's knowledge base.
356
+
357
+ Instead of fixed-interval curation with hardcoded rules,
358
+ the LLM reviews the knowledge base and makes intelligent
359
+ decisions about what to keep, merge, or discard.
360
+
361
+ This is the LLM managing CRP's own memory — the deepest
362
+ integration of LLM-as-cognitive-engine.
363
+ """
364
+ if not facts:
365
+ return CurationDecision()
366
+
367
+ facts_block = "\n".join(
368
+ f"[{fid}] age={age}w conf={conf:.0%} | {text}"
369
+ for fid, text, conf, age in facts[:25]
370
+ )
371
+
372
+ prompt = _MEMORY_CURATION_PROMPT.format(
373
+ facts_block=facts_block,
374
+ fact_count=len(facts),
375
+ recent_task=recent_task[:200],
376
+ )
377
+
378
+ start = time.monotonic_ns()
379
+ raw = self._cognitive_call(prompt, self.MAX_CURATION_TOKENS)
380
+ self._metrics.memory_curation_ms = (time.monotonic_ns() - start) / 1_000_000
381
+
382
+ return _parse_curation(raw)
383
+
384
+ # ───────────────────────────────────────────────────────────────
385
+ # §22.6 EXECUTION PLANNING
386
+ # ───────────────────────────────────────────────────────────────
387
+
388
+ def plan_execution(
389
+ self,
390
+ analysis: TaskAnalysis,
391
+ fact_count: int = 0,
392
+ ) -> ExecutionPlan:
393
+ """Use the LLM to create an execution plan for complex tasks.
394
+
395
+ For multi-part or complex tasks, the LLM decomposes the task
396
+ into steps, each with its own dispatch strategy and context
397
+ needs. CRP then orchestrates the plan, running each step
398
+ with the optimal configuration.
399
+
400
+ This replaces the flat "one dispatch" approach with intelligent
401
+ multi-step orchestration.
402
+ """
403
+ if analysis.complexity in ("simple", "medium"):
404
+ # Simple tasks don't need planning
405
+ return ExecutionPlan(
406
+ steps=[PlanStep(
407
+ description="Direct dispatch",
408
+ strategy="push",
409
+ priority=1,
410
+ )],
411
+ estimated_windows=1,
412
+ )
413
+
414
+ subtasks_text = "\n".join(
415
+ f"- {st}" for st in analysis.subtasks
416
+ ) if analysis.subtasks else "None identified yet"
417
+
418
+ prompt = _EXECUTION_PLANNING_PROMPT.format(
419
+ complexity=analysis.complexity,
420
+ domain=analysis.domain,
421
+ knowledge_needs=", ".join(analysis.knowledge_needs[:5]),
422
+ subtasks=subtasks_text,
423
+ fact_count=fact_count,
424
+ )
425
+
426
+ start = time.monotonic_ns()
427
+ raw = self._cognitive_call(prompt, self.MAX_PLANNING_TOKENS)
428
+ self._metrics.execution_planning_ms = (time.monotonic_ns() - start) / 1_000_000
429
+
430
+ return _parse_execution_plan(raw)
431
+
432
+ # ───────────────────────────────────────────────────────────────
433
+ # Internal: structured LLM call
434
+ # ───────────────────────────────────────────────────────────────
435
+
436
+ def _cognitive_call(self, prompt: str, max_tokens: int) -> str:
437
+ """Execute a single cognitive LLM call.
438
+
439
+ All cognitive calls go through this bottleneck so we can:
440
+ - Track token usage
441
+ - Enforce budget limits
442
+ - Log cognitive reasoning
443
+ - Handle failures gracefully
444
+ """
445
+ messages = [
446
+ {
447
+ "role": "system",
448
+ "content": (
449
+ "You are the cognitive engine of CRP (Context Relay Protocol). "
450
+ "You make internal reasoning decisions for the system. "
451
+ "ALWAYS respond with valid JSON matching the requested schema. "
452
+ "Be concise and precise."
453
+ ),
454
+ },
455
+ {"role": "user", "content": prompt},
456
+ ]
457
+
458
+ try:
459
+ response, _ = self._provider.generate_chat(
460
+ messages, max_tokens=max_tokens,
461
+ )
462
+ self._metrics.cognitive_calls += 1
463
+ self._metrics.total_cognitive_tokens += self._count_tokens(response)
464
+ return response
465
+ except Exception as exc:
466
+ logger.warning("Facilitator cognitive call failed: %s", exc)
467
+ return "{}" # Return empty JSON — parsers handle gracefully
468
+
469
+
470
+ # ═══════════════════════════════════════════════════════════════════════
471
+ # Structured prompts for cognitive calls
472
+ # ═══════════════════════════════════════════════════════════════════════
473
+
474
+ _TASK_ANALYSIS_PROMPT = """\
475
+ Analyze this task for CRP's internal routing. You are NOT generating the response — \
476
+ you are analyzing what KIND of task this is so CRP can configure itself optimally.
477
+
478
+ SYSTEM PROMPT (context): {system_prompt}
479
+ TASK INPUT: {task_input}
480
+ AVAILABLE FACTS: {fact_count} facts in knowledge base
481
+
482
+ Respond with JSON:
483
+ {{
484
+ "complexity": "simple|medium|complex|multi_part",
485
+ "domain": "<detected domain, e.g. 'software engineering', 'history', 'data analysis'>",
486
+ "knowledge_needs": ["<what knowledge this task requires>", ...],
487
+ "expected_output_length": "short|medium|long|variable",
488
+ "requires_factual_grounding": true/false,
489
+ "requires_creativity": true/false,
490
+ "requires_reasoning": true/false,
491
+ "subtasks": ["<if multi_part, list the sub-tasks>"],
492
+ "confidence": 0.0-1.0
493
+ }}"""
494
+
495
+ _STRATEGY_ROUTING_PROMPT = """\
496
+ Choose the optimal CRP dispatch strategy. You are CRP's routing brain.
497
+
498
+ TASK PROFILE:
499
+ Complexity: {complexity}
500
+ Domain: {domain}
501
+ Knowledge needs: {knowledge_needs}
502
+ Expected output: {output_length}
503
+ Needs grounding: {requires_grounding}
504
+ Needs creativity: {requires_creativity}
505
+ Facts available: {fact_count}
506
+
507
+ AVAILABLE STRATEGIES: {available_strategies}
508
+ - push: Pre-load ALL relevant context (envelope). Best for fact-heavy tasks with enough KB coverage.
509
+ - pull: LLM requests context via tools. Best when LLM knows what it needs.
510
+ - reflexive: Generate blind, then fact-check & refine. Best for accuracy-critical tasks.
511
+ - progressive: Send compact index, expand on demand. Best for broad tasks with many available facts.
512
+ - stream_augmented: Inject context mid-generation. Best for long-form coherent output.
513
+
514
+ Respond with JSON:
515
+ {{
516
+ "strategy": "<chosen strategy>",
517
+ "reasoning": "<1-2 sentences explaining why>",
518
+ "envelope_priority": "minimal|balanced|maximal",
519
+ "continuation_likelihood": "none|low|medium|high",
520
+ "confidence": 0.0-1.0
521
+ }}"""
522
+
523
+ _FACT_SYNTHESIS_PROMPT = """\
524
+ Synthesize these CRP knowledge base facts into coherent knowledge. \
525
+ You are CRP's knowledge engine — merge, deduplicate, and organize.
526
+
527
+ TASK CONTEXT: {task_context}
528
+ FACTS ({fact_count} total):
529
+ {facts_block}
530
+
531
+ Respond with JSON:
532
+ {{
533
+ "summary": "<coherent 2-3 sentence synthesis of the key knowledge>",
534
+ "key_insights": ["<distilled insight 1>", "<insight 2>", ...],
535
+ "contradictions": ["<any contradicting facts>"],
536
+ "knowledge_gaps": ["<what important knowledge is missing>"],
537
+ "redundant_fact_ids": ["<IDs of facts that are duplicates or subsumed>"],
538
+ "merged_facts": [{{"original_ids": ["id1", "id2"], "merged_text": "<new merged fact>"}}]
539
+ }}"""
540
+
541
+ _OUTPUT_EVALUATION_PROMPT = """\
542
+ Evaluate this CRP output. You are CRP's quality brain — assess whether the output \
543
+ actually addresses the task. This is NOT about style — it's about substance.
544
+
545
+ ORIGINAL TASK: {task_input}
546
+ CRP OUTPUT (may be truncated): {output}
547
+ FACTS USED: {facts_used}
548
+
549
+ Respond with JSON:
550
+ {{
551
+ "task_completion": 0.0-1.0,
552
+ "factual_accuracy": 0.0-1.0,
553
+ "coherence": 0.0-1.0,
554
+ "missing_elements": ["<what's missing>"],
555
+ "revision_needed": true/false,
556
+ "revision_focus": "<what to focus on if revision needed>",
557
+ "overall_grade": "S|A|B|C|D"
558
+ }}"""
559
+
560
+ _MEMORY_CURATION_PROMPT = """\
561
+ Review CRP's knowledge base and make curation decisions. You are CRP's memory manager — \
562
+ decide what to keep, merge, or discard.
563
+
564
+ RECENT TASK CONTEXT: {recent_task}
565
+ KNOWLEDGE BASE ({fact_count} facts):
566
+ {facts_block}
567
+
568
+ Respond with JSON:
569
+ {{
570
+ "promote_ids": ["<fact IDs to boost — high value, should rank higher>"],
571
+ "demote_ids": ["<fact IDs to reduce — low value or stale>"],
572
+ "merge_groups": [["<id1>", "<id2>"], ...],
573
+ "discard_ids": ["<fact IDs that are useless or harmful>"],
574
+ "reasoning": "<brief explanation of curation logic>"
575
+ }}"""
576
+
577
+ _EXECUTION_PLANNING_PROMPT = """\
578
+ Create an execution plan for this complex task. You are CRP's planner — \
579
+ decompose the task and assign strategies.
580
+
581
+ TASK PROFILE:
582
+ Complexity: {complexity}
583
+ Domain: {domain}
584
+ Knowledge needs: {knowledge_needs}
585
+ Identified subtasks: {subtasks}
586
+ Facts available: {fact_count}
587
+
588
+ Respond with JSON:
589
+ {{
590
+ "steps": [
591
+ {{
592
+ "description": "<what this step does>",
593
+ "strategy": "push|pull|reflexive|progressive|stream_augmented",
594
+ "context_needs": ["<what knowledge this step needs>"],
595
+ "depends_on": [<step indices this depends on, 0-indexed>],
596
+ "priority": 1-3
597
+ }}
598
+ ],
599
+ "estimated_windows": <total generation windows expected>,
600
+ "parallel_possible": true/false
601
+ }}"""
602
+
603
+
604
+ # ═══════════════════════════════════════════════════════════════════════
605
+ # JSON parsers — extract structured data from LLM output
606
+ # ═══════════════════════════════════════════════════════════════════════
607
+
608
+ def _extract_json(raw: str) -> dict[str, Any]:
609
+ """Extract JSON from LLM response, handling markdown fences etc."""
610
+ # Try direct parse first
611
+ text = raw.strip()
612
+ if text.startswith("```"):
613
+ # Strip markdown code fence
614
+ lines = text.split("\n")
615
+ lines = [l for l in lines if not l.strip().startswith("```")]
616
+ text = "\n".join(lines).strip()
617
+
618
+ try:
619
+ return json.loads(text)
620
+ except json.JSONDecodeError:
621
+ # Try to find JSON object in the text
622
+ match = re.search(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', text, re.DOTALL)
623
+ if match:
624
+ try:
625
+ return json.loads(match.group())
626
+ except json.JSONDecodeError:
627
+ pass
628
+ return {}
629
+
630
+
631
+ def _parse_task_analysis(raw: str) -> TaskAnalysis:
632
+ d = _extract_json(raw)
633
+ return TaskAnalysis(
634
+ complexity=d.get("complexity", "medium"),
635
+ domain=d.get("domain", "general"),
636
+ knowledge_needs=d.get("knowledge_needs", []),
637
+ expected_output_length=d.get("expected_output_length", "medium"),
638
+ requires_factual_grounding=d.get("requires_factual_grounding", True),
639
+ requires_creativity=d.get("requires_creativity", False),
640
+ requires_reasoning=d.get("requires_reasoning", False),
641
+ subtasks=d.get("subtasks", []),
642
+ confidence=float(d.get("confidence", 0.5)),
643
+ )
644
+
645
+
646
+ def _parse_strategy_decision(raw: str, valid_strategies: list[str]) -> StrategyDecision:
647
+ d = _extract_json(raw)
648
+ strategy = d.get("strategy", "push")
649
+ if strategy not in valid_strategies:
650
+ strategy = "push" # Safe fallback
651
+ return StrategyDecision(
652
+ strategy=strategy,
653
+ reasoning=d.get("reasoning", ""),
654
+ envelope_priority=d.get("envelope_priority", "balanced"),
655
+ continuation_likelihood=d.get("continuation_likelihood", "low"),
656
+ confidence=float(d.get("confidence", 0.5)),
657
+ )
658
+
659
+
660
+ def _parse_synthesis(raw: str) -> SynthesizedKnowledge:
661
+ d = _extract_json(raw)
662
+ return SynthesizedKnowledge(
663
+ summary=d.get("summary", ""),
664
+ key_insights=d.get("key_insights", []),
665
+ contradictions=d.get("contradictions", []),
666
+ knowledge_gaps=d.get("knowledge_gaps", []),
667
+ redundant_fact_ids=d.get("redundant_fact_ids", []),
668
+ merged_facts=d.get("merged_facts", []),
669
+ )
670
+
671
+
672
+ def _parse_evaluation(raw: str) -> OutputEvaluation:
673
+ d = _extract_json(raw)
674
+ return OutputEvaluation(
675
+ task_completion=float(d.get("task_completion", 0.5)),
676
+ factual_accuracy=float(d.get("factual_accuracy", 0.5)),
677
+ coherence=float(d.get("coherence", 0.5)),
678
+ missing_elements=d.get("missing_elements", []),
679
+ revision_needed=d.get("revision_needed", False),
680
+ revision_focus=d.get("revision_focus", ""),
681
+ overall_grade=d.get("overall_grade", "B"),
682
+ )
683
+
684
+
685
+ def _parse_curation(raw: str) -> CurationDecision:
686
+ d = _extract_json(raw)
687
+ return CurationDecision(
688
+ promote_ids=d.get("promote_ids", []),
689
+ demote_ids=d.get("demote_ids", []),
690
+ merge_groups=d.get("merge_groups", []),
691
+ discard_ids=d.get("discard_ids", []),
692
+ reasoning=d.get("reasoning", ""),
693
+ )
694
+
695
+
696
+ def _parse_execution_plan(raw: str) -> ExecutionPlan:
697
+ d = _extract_json(raw)
698
+ steps = []
699
+ for s in d.get("steps", []):
700
+ steps.append(PlanStep(
701
+ description=s.get("description", ""),
702
+ strategy=s.get("strategy", "push"),
703
+ context_needs=s.get("context_needs", []),
704
+ depends_on=s.get("depends_on", []),
705
+ priority=int(s.get("priority", 1)),
706
+ ))
707
+ if not steps:
708
+ steps = [PlanStep(description="Direct dispatch", strategy="push", priority=1)]
709
+ return ExecutionPlan(
710
+ steps=steps,
711
+ estimated_windows=int(d.get("estimated_windows", 1)),
712
+ parallel_possible=d.get("parallel_possible", False),
713
+ )