fable-engine 1.3.1__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.
- fable_compressor.py +356 -0
- fable_engine/__init__.py +1 -0
- fable_engine/actions/__init__.py +291 -0
- fable_engine/actions/cas.py +182 -0
- fable_engine/actions/deliberation.py +523 -0
- fable_engine/actions/fleet.py +807 -0
- fable_engine/actions/lifecycle.py +298 -0
- fable_engine/actions/scrapers.py +116 -0
- fable_engine/actions/system3.py +815 -0
- fable_engine/browser.py +824 -0
- fable_engine/cas.py +974 -0
- fable_engine/fable_session.json +510 -0
- fable_engine/guards.py +283 -0
- fable_engine/schema.py +714 -0
- fable_engine/scrapers/__init__.py +32 -0
- fable_engine/scrapers/arxiv.py +115 -0
- fable_engine/scrapers/base.py +386 -0
- fable_engine/scrapers/github.py +129 -0
- fable_engine/scrapers/reddit.py +154 -0
- fable_engine/scrapers/web.py +120 -0
- fable_engine/scrapers/x.py +125 -0
- fable_engine/scrapers/youtube.py +132 -0
- fable_engine/server.py +414 -0
- fable_engine/session.py +1819 -0
- fable_engine/test_server.py +1362 -0
- fable_engine/updater.py +541 -0
- fable_engine-1.3.1.dist-info/LICENSE +22 -0
- fable_engine-1.3.1.dist-info/METADATA +173 -0
- fable_engine-1.3.1.dist-info/RECORD +104 -0
- fable_engine-1.3.1.dist-info/WHEEL +5 -0
- fable_engine-1.3.1.dist-info/entry_points.txt +5 -0
- fable_engine-1.3.1.dist-info/top_level.txt +6 -0
- fable_mode/__init__.py +3 -0
- fable_mode/__main__.py +4 -0
- fable_mode/adapters.py +1014 -0
- fable_mode/installer.py +553 -0
- fable_mode/launcher.py +437 -0
- fable_mode/manifest.py +142 -0
- fable_mode/resources.json +114 -0
- fable_mode/safety.py +103 -0
- fable_mode_entry.py +10 -0
- fable_v2/__init__.py +146 -0
- fable_v2/adapters.py +151 -0
- fable_v2/coder_fleet/__init__.py +100 -0
- fable_v2/coder_fleet/ast_tools.py +158 -0
- fable_v2/coder_fleet/compute.py +199 -0
- fable_v2/coder_fleet/design_engine.py +1316 -0
- fable_v2/coder_fleet/diagnostics.py +293 -0
- fable_v2/coder_fleet/fleet_dispatcher.py +214 -0
- fable_v2/coder_fleet/mock_auditor.py +306 -0
- fable_v2/coder_fleet/mutation.py +216 -0
- fable_v2/coder_fleet/property_oracle.py +260 -0
- fable_v2/coder_fleet/receipt_attestor.py +122 -0
- fable_v2/coder_fleet/red_team_swarm.py +908 -0
- fable_v2/coder_fleet/test_harness.py +198 -0
- fable_v2/coder_fleet/vector_engine.py +1287 -0
- fable_v2/coder_fleet/visual.py +357 -0
- fable_v2/coder_fleet/workspace.py +153 -0
- fable_v2/cortical/__init__.py +20 -0
- fable_v2/cortical/plasticity_engine.py +992 -0
- fable_v2/execution_broker.py +811 -0
- fable_v2/proof_engine.py +1141 -0
- fable_v2/protocol.py +485 -0
- fable_v2/runtime.py +1010 -0
- fable_v2/system3/__init__.py +204 -0
- fable_v2/system3/causal.py +558 -0
- fable_v2/system3/dialectical.py +577 -0
- fable_v2/system3/evolution.py +503 -0
- fable_v2/system3/executive.py +338 -0
- fable_v2/system3/free_energy.py +479 -0
- fable_v2/system3/hyperbolic.py +555 -0
- fable_v2/system3/induction.py +336 -0
- fable_v2/system3/kripke.py +548 -0
- fable_v2/system3/oracle.py +745 -0
- fable_v2/verifiers.py +72 -0
- tests/__init__.py +1 -0
- tests/test_anti_loop_circuit_breaker.py +64 -0
- tests/test_auto_updater.py +407 -0
- tests/test_coder_fleet.py +535 -0
- tests/test_delegation_compiler.py +54 -0
- tests/test_descriptor_boundaries.py +126 -0
- tests/test_design_engine.py +603 -0
- tests/test_epistemic_evidence_validator.py +66 -0
- tests/test_execution_broker.py +233 -0
- tests/test_fable_v2.py +406 -0
- tests/test_fleet_transitions.py +116 -0
- tests/test_fsm_redteam_evolution.py +406 -0
- tests/test_goal_rubric_and_pipeline.py +367 -0
- tests/test_hebbian_plasticity.py +585 -0
- tests/test_packaging_runtime.py +194 -0
- tests/test_proof_engine.py +259 -0
- tests/test_red_team_swarm.py +645 -0
- tests/test_redteam_remediation.py +169 -0
- tests/test_registration_transaction.py +375 -0
- tests/test_requested_regressions.py +467 -0
- tests/test_scrapers.py +370 -0
- tests/test_server_actions.py +93 -0
- tests/test_server_frontier_actions.py +269 -0
- tests/test_server_protocol.py +88 -0
- tests/test_stealth_browser.py +970 -0
- tests/test_system3.py +381 -0
- tests/test_system3_deep_integration.py +385 -0
- tests/test_system3_frontier.py +436 -0
- tests/test_vector_engine.py +608 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Session lifecycle, timer pacing, and state transition action handlers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import collections
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import math
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("fable-engine.actions.lifecycle")
|
|
15
|
+
|
|
16
|
+
from fable_engine.session import (
|
|
17
|
+
ACTIVE_SESSIONS,
|
|
18
|
+
PHASES,
|
|
19
|
+
SESSIONS_DIR,
|
|
20
|
+
SILENT_DELIBERATION_REMINDER,
|
|
21
|
+
FableSession,
|
|
22
|
+
SessionState,
|
|
23
|
+
_validate_session_name,
|
|
24
|
+
_validate_time_budget,
|
|
25
|
+
_safe_session_file,
|
|
26
|
+
get_or_load_session,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def _handle_create_session(arguments: Dict[str, Any]) -> str:
|
|
30
|
+
action = arguments.get("action", "").strip().lower()
|
|
31
|
+
session_name = arguments.get("session_name", "").strip()
|
|
32
|
+
session: Optional[FableSession] = None
|
|
33
|
+
if not session_name:
|
|
34
|
+
return "Error: 'session_name' is required for action 'create_session'."
|
|
35
|
+
session_name = _validate_session_name(session_name)
|
|
36
|
+
objective = arguments.get("objective", "").strip()
|
|
37
|
+
if not objective:
|
|
38
|
+
return "Error: 'objective' is required for action 'create_session'."
|
|
39
|
+
|
|
40
|
+
time_budget = arguments.get("time_budget_minutes", 60.0)
|
|
41
|
+
try:
|
|
42
|
+
time_budget_min = float(time_budget)
|
|
43
|
+
except (ValueError, TypeError):
|
|
44
|
+
return f"Error: Invalid 'time_budget_minutes': {time_budget}."
|
|
45
|
+
|
|
46
|
+
session = FableSession(session_name, objective, time_budget_min)
|
|
47
|
+
ACTIVE_SESSIONS[session_name] = session
|
|
48
|
+
session.save()
|
|
49
|
+
|
|
50
|
+
tel = session.get_telemetry()
|
|
51
|
+
return (
|
|
52
|
+
f"### 🛡️ Fable Cognitive Session Initialized\n\n"
|
|
53
|
+
f"- **Session Name**: `{session.session_name}`\n"
|
|
54
|
+
f"- **Session ID**: `{session.session_id}`\n"
|
|
55
|
+
f"- **Objective**: {session.objective}\n"
|
|
56
|
+
f"- **Time Budget**: `{session.time_budget_minutes}` minutes ({tel['remaining_formatted']})\n"
|
|
57
|
+
f"- **Active Phase**: `{session.active_phase}`\n"
|
|
58
|
+
f"- **Execution Lock**: `LOCKED (can_execute_code: False)` 🛑\n"
|
|
59
|
+
f"- **Cognitive Gates**: 0/2 [PROVEN] items, 0/1 Invariant recorded\n\n"
|
|
60
|
+
f"> [!IMPORTANT]\n"
|
|
61
|
+
f"> Anti-Rush Lockout is ACTIVE. Proceed with epistemic grounding, research, and invariant modeling before requesting execution unlock."
|
|
62
|
+
f"{SILENT_DELIBERATION_REMINDER if session.execution_locked else ''}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _handle_set_timer(arguments: Dict[str, Any]) -> str:
|
|
67
|
+
action = arguments.get("action", "").strip().lower()
|
|
68
|
+
session_name = arguments.get("session_name", "").strip()
|
|
69
|
+
session: Optional[FableSession] = None
|
|
70
|
+
if not session_name:
|
|
71
|
+
return "Error: 'session_name' is required for action 'set_timer'."
|
|
72
|
+
time_budget = arguments.get("time_budget_minutes")
|
|
73
|
+
if time_budget is None:
|
|
74
|
+
return "Error: 'time_budget_minutes' is required for action 'set_timer'."
|
|
75
|
+
try:
|
|
76
|
+
time_budget_min = float(time_budget)
|
|
77
|
+
except (ValueError, TypeError):
|
|
78
|
+
return f"Error: Invalid 'time_budget_minutes': {time_budget}."
|
|
79
|
+
|
|
80
|
+
session = get_or_load_session(session_name)
|
|
81
|
+
tel = session.set_timer(time_budget_min)
|
|
82
|
+
session.save()
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
f"### ⏱️ Fable Session Timer Updated\n\n"
|
|
86
|
+
f"- **Session Name**: `{session.session_name}`\n"
|
|
87
|
+
f"- **Pacing Timer**: `{session.pacing_budget_minutes}` minutes\n"
|
|
88
|
+
f"- **Authority Budget**: `{session.time_budget_minutes}` minutes (immutable)\n"
|
|
89
|
+
f"- **Elapsed Time**: `{tel['elapsed_formatted']}`\n"
|
|
90
|
+
f"- **Pacing Remaining**: `{tel['pacing_remaining_formatted']}`\n"
|
|
91
|
+
f"- **Authority Remaining**: `{tel['authority_remaining_formatted']}`\n"
|
|
92
|
+
f"- **Pacing Ratio**: `{tel['pacing_percentage']}`\n"
|
|
93
|
+
f"- **Authority Deadline**: `{time.ctime(session.deadline_time)}`"
|
|
94
|
+
f"{SILENT_DELIBERATION_REMINDER if session.execution_locked else ''}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _handle_get_status(arguments: Dict[str, Any]) -> str:
|
|
99
|
+
action = arguments.get("action", "").strip().lower()
|
|
100
|
+
session_name = arguments.get("session_name", "").strip()
|
|
101
|
+
session: Optional[FableSession] = None
|
|
102
|
+
if not session_name:
|
|
103
|
+
return "Error: 'session_name' is required for action 'get_status'."
|
|
104
|
+
|
|
105
|
+
session = get_or_load_session(session_name)
|
|
106
|
+
tel = session.get_telemetry()
|
|
107
|
+
|
|
108
|
+
lock_badge = "🔴 LOCKED (`can_execute_code: False`)" if session.execution_locked else "🟢 UNLOCKED (`can_execute_code: True`)"
|
|
109
|
+
|
|
110
|
+
# Format ledger breakdown
|
|
111
|
+
counts = tel["epistemic_counts"]
|
|
112
|
+
ledger_lines = []
|
|
113
|
+
for item in session.epistemic_ledger[-5:]: # show recent 5
|
|
114
|
+
ev_str = f" (Evidence: {item['evidence']})" if item.get('evidence') else ""
|
|
115
|
+
ledger_lines.append(f"- `[{item['tag']}]` **{item['id']}**: {item['claim']}{ev_str}")
|
|
116
|
+
ledger_preview = "\n".join(ledger_lines) if ledger_lines else "- No items logged yet."
|
|
117
|
+
|
|
118
|
+
# Format invariants preview
|
|
119
|
+
inv_lines = []
|
|
120
|
+
for inv in session.invariants[-5:]:
|
|
121
|
+
inv_lines.append(f"- **{inv['name']}** `[{inv['domain']}]`: {inv['formal_statement']}")
|
|
122
|
+
inv_preview = "\n".join(inv_lines) if inv_lines else "- No invariants recorded yet."
|
|
123
|
+
|
|
124
|
+
# Format refinement preview
|
|
125
|
+
ref_lines = []
|
|
126
|
+
for ref in session.refinement_cycles[-3:]:
|
|
127
|
+
ref_lines.append(f"- **Cycle #{ref['cycle_number']}** `[{ref['refinement_type'].upper()}]` ({ref['focus_area']}): {ref['architectural_refinement']}")
|
|
128
|
+
ref_preview = "\n".join(ref_lines) if ref_lines else "- No refinement cycles recorded yet."
|
|
129
|
+
|
|
130
|
+
cog_state = tel.get("system3_cognitive_state", {})
|
|
131
|
+
return (
|
|
132
|
+
f"### 📊 Fable Session Status & Telemetry (`{session.session_name}`)\n\n"
|
|
133
|
+
f"- **Objective**: {session.objective}\n"
|
|
134
|
+
f"- **Active Phase**: `{session.active_phase}` (Phase {tel['phase_index']}/{tel['total_phases']})\n"
|
|
135
|
+
f"- **Execution Lock**: {lock_badge}\n"
|
|
136
|
+
f"- **Pacing**: `{tel['elapsed_formatted']}` elapsed / `{tel['pacing_remaining_formatted']}` remaining (`{tel['pacing_percentage']}` budget used)\n"
|
|
137
|
+
f"- **Authority**: `{tel['authority_remaining_formatted']}` remaining (immutable outer deadline)\n"
|
|
138
|
+
f"- **Epistemic Breakdown**: `{counts['proven']} PROVEN`, `{counts['hypothesis']} HYPOTHESIS`, `{counts['unknown']} UNKNOWN` (Total: `{counts['total']}`)\n"
|
|
139
|
+
f"- **Invariants Recorded**: `{tel['invariants_count']}`\n"
|
|
140
|
+
f"- **Refinement Cycles**: `{tel['refinement_count']}`\n\n"
|
|
141
|
+
f"#### 🧠 System 3 Meta-Cognitive State:\n"
|
|
142
|
+
f"- **Variational Free Energy $F$**: `{cog_state.get('free_energy_f', 'N/A')}` "
|
|
143
|
+
f"(Complexity $D_{{KL}}$: `{cog_state.get('complexity_kl', 'N/A')}`, Accuracy: `{cog_state.get('accuracy_log_likelihood', 'N/A')}`)\n"
|
|
144
|
+
f"- **Kripke Safety Invariant**: `{cog_state.get('kripke_safety_invariant', 'AG(safe) -> True')}`\n"
|
|
145
|
+
f"- **Active Biases Tracked**: `{cog_state.get('active_biases_count', 0)}`\n"
|
|
146
|
+
f"- **Contradiction Density**: `{cog_state.get('contradiction_density', 0.0)}`\n"
|
|
147
|
+
f"- **Hyperbolic Metric**: `{cog_state.get('hyperbolic_metric', {}).get('status', 'INITIALIZED')}`\n\n"
|
|
148
|
+
f"#### 🔍 Recent Epistemic Ledger Items:\n{ledger_preview}\n\n"
|
|
149
|
+
f"#### 📐 Invariants Specification:\n{inv_preview}\n\n"
|
|
150
|
+
f"#### 🔄 Recent Refinement Cycles:\n{ref_preview}"
|
|
151
|
+
f"{SILENT_DELIBERATION_REMINDER if session.execution_locked else ''}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _handle_advance_phase(arguments: Dict[str, Any]) -> str:
|
|
156
|
+
action = arguments.get("action", "").strip().lower()
|
|
157
|
+
session_name = arguments.get("session_name", "").strip()
|
|
158
|
+
session: Optional[FableSession] = None
|
|
159
|
+
if not session_name:
|
|
160
|
+
return "Error: 'session_name' is required for action 'advance_phase'."
|
|
161
|
+
next_phase = arguments.get("next_phase", "").strip()
|
|
162
|
+
if not next_phase:
|
|
163
|
+
return "Error: 'next_phase' is required for action 'advance_phase'."
|
|
164
|
+
phase_summary = arguments.get("phase_summary", "").strip() or "Advanced phase transition."
|
|
165
|
+
|
|
166
|
+
session = get_or_load_session(session_name)
|
|
167
|
+
tel = session.advance_phase(next_phase, phase_summary)
|
|
168
|
+
session.save()
|
|
169
|
+
|
|
170
|
+
cog_state = tel.get("system3_cognitive_state", {})
|
|
171
|
+
bias_lines = ""
|
|
172
|
+
if session.active_biases:
|
|
173
|
+
bias_items = "\n".join([f" * ⚠️ **{b['bias_type']}** ({b['severity']}): {b['description']} -> *{b['mitigation_recommendation']}*" for b in session.active_biases])
|
|
174
|
+
bias_lines = f"\n- **Active Biases Intercepted** ({len(session.active_biases)}):\n{bias_items}"
|
|
175
|
+
|
|
176
|
+
sys3_advisory = (
|
|
177
|
+
f"\n\n### 🧠 System 3 Meta-Cognitive Advisory & Active Inference\n"
|
|
178
|
+
f"- **Live Free Energy $F$**: `{cog_state.get('free_energy_f', 'N/A')}` "
|
|
179
|
+
f"(Complexity $D_{{KL}}$: `{cog_state.get('complexity_kl', 'N/A')}`, Accuracy: `{cog_state.get('accuracy_log_likelihood', 'N/A')}`)\n"
|
|
180
|
+
f"- **Kripke Safety Invariant**: `{cog_state.get('kripke_safety_invariant', 'AG(safe) -> True')}`\n"
|
|
181
|
+
f"- **Contradiction Density**: `{cog_state.get('contradiction_density', 0.0)}`"
|
|
182
|
+
f"{bias_lines}"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
f"### 🚀 Fable Phase Advanced Successfully\n\n"
|
|
187
|
+
f"- **Session**: `{session.session_name}`\n"
|
|
188
|
+
f"- **New Active Phase**: `{session.active_phase}` (Phase {tel['phase_index']}/{tel['total_phases']})\n"
|
|
189
|
+
f"- **Phase Summary**: {phase_summary}\n"
|
|
190
|
+
f"- **Execution Status**: `{'LOCKED 🛑' if session.execution_locked else 'UNLOCKED 🟢'}`\n"
|
|
191
|
+
f"- **Pacing Remaining**: `{tel['pacing_remaining_formatted']}` (`{tel['pacing_percentage']}` used)\n"
|
|
192
|
+
f"- **Authority Remaining**: `{tel['authority_remaining_formatted']}`"
|
|
193
|
+
f"{sys3_advisory}"
|
|
194
|
+
f"{SILENT_DELIBERATION_REMINDER if session.execution_locked else ''}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _handle_unlock_execution(arguments: Dict[str, Any]) -> str:
|
|
199
|
+
action = arguments.get("action", "").strip().lower()
|
|
200
|
+
session_name = arguments.get("session_name", "").strip()
|
|
201
|
+
session: Optional[FableSession] = None
|
|
202
|
+
if not session_name:
|
|
203
|
+
return "Error: 'session_name' is required for action 'unlock_execution'."
|
|
204
|
+
rationale = arguments.get("rationale", "").strip()
|
|
205
|
+
if not rationale:
|
|
206
|
+
return "Error: 'rationale' is required for action 'unlock_execution'."
|
|
207
|
+
# No model-provided override is accepted. Administrative callers
|
|
208
|
+
# must use the direct host API with an out-of-band secret.
|
|
209
|
+
session = get_or_load_session(session_name)
|
|
210
|
+
try:
|
|
211
|
+
res = session.unlock_execution(rationale)
|
|
212
|
+
session.save()
|
|
213
|
+
override_msg = " ⚠️ *(Out-of-band emergency override)*" if session.unlock_details.get("force_override_used") else ""
|
|
214
|
+
return (
|
|
215
|
+
f"### 🔓 Execution Lock Lifted Successfully{override_msg}\n\n"
|
|
216
|
+
f"- **Session**: `{session.session_name}`\n"
|
|
217
|
+
f"- **Status**: `🟢 UNLOCKED`\n"
|
|
218
|
+
f"- **`can_execute_code`**: `True`\n"
|
|
219
|
+
f"- **Phase at Unlock**: `{session.active_phase}`\n"
|
|
220
|
+
f"- **Rationale**: {rationale}\n"
|
|
221
|
+
f"- **Validated Gates**: `{len(session.epistemic_ledger)}` epistemic items ({session.unlock_details['proven_count']} PROVEN), `{len(session.invariants)}` Invariants, `{len(session.refinement_cycles)}` Refinement Cycles\n\n"
|
|
222
|
+
f"> [!TIP]\n"
|
|
223
|
+
f"> Implementer subagents may now execute code and run automated tests."
|
|
224
|
+
)
|
|
225
|
+
except PermissionError as pe:
|
|
226
|
+
return str(pe)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _handle_checkpoint_session(arguments: Dict[str, Any]) -> str:
|
|
230
|
+
action = arguments.get("action", "").strip().lower()
|
|
231
|
+
session_name = arguments.get("session_name", "").strip()
|
|
232
|
+
session: Optional[FableSession] = None
|
|
233
|
+
if not session_name:
|
|
234
|
+
return "Error: 'session_name' is required for action 'checkpoint_session'."
|
|
235
|
+
session = get_or_load_session(session_name)
|
|
236
|
+
saved_path = session.save()
|
|
237
|
+
return (
|
|
238
|
+
f"### 💾 Fable Session Checkpointed\n\n"
|
|
239
|
+
f"- **Session**: `{session.session_name}`\n"
|
|
240
|
+
f"- **Path**: `{saved_path}`\n"
|
|
241
|
+
f"- **Phase**: `{session.active_phase}`\n"
|
|
242
|
+
f"- **Epistemic Items**: `{len(session.epistemic_ledger)}`\n"
|
|
243
|
+
f"- **Invariants**: `{len(session.invariants)}`\n"
|
|
244
|
+
f"- **Refinement Cycles**: `{len(session.refinement_cycles)}`\n"
|
|
245
|
+
f"- **Execution Lock**: `{'LOCKED' if session.execution_locked else 'UNLOCKED'}`"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _handle_restore_session(arguments: Dict[str, Any]) -> str:
|
|
250
|
+
action = arguments.get("action", "").strip().lower()
|
|
251
|
+
session_name = arguments.get("session_name", "").strip()
|
|
252
|
+
session: Optional[FableSession] = None
|
|
253
|
+
if not session_name:
|
|
254
|
+
return "Error: 'session_name' is required for action 'restore_session'."
|
|
255
|
+
session = get_or_load_session(session_name)
|
|
256
|
+
tel = session.get_telemetry()
|
|
257
|
+
return (
|
|
258
|
+
f"### 📂 Fable Session Restored\n\n"
|
|
259
|
+
f"- **Session**: `{session.session_name}` (`{session.session_id}`)\n"
|
|
260
|
+
f"- **Objective**: {session.objective}\n"
|
|
261
|
+
f"- **Active Phase**: `{session.active_phase}`\n"
|
|
262
|
+
f"- **Pacing Remaining**: `{tel['remaining_formatted']}`\n"
|
|
263
|
+
f"- **Execution Lock**: `{'LOCKED' if session.execution_locked else 'UNLOCKED'}`\n"
|
|
264
|
+
f"- **Ledger**: `{tel['epistemic_counts']['proven']} PROVEN`, `{tel['epistemic_counts']['hypothesis']} HYPOTHESIS`\n"
|
|
265
|
+
f"- **Invariants**: `{tel['invariants_count']}`\n"
|
|
266
|
+
f"- **Refinement Cycles**: `{tel['refinement_count']}`"
|
|
267
|
+
f"{SILENT_DELIBERATION_REMINDER if session.execution_locked else ''}"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _handle_list_sessions(arguments: Dict[str, Any]) -> str:
|
|
272
|
+
action = arguments.get("action", "").strip().lower()
|
|
273
|
+
session_name = arguments.get("session_name", "").strip()
|
|
274
|
+
session: Optional[FableSession] = None
|
|
275
|
+
files = list(SESSIONS_DIR.glob("*.json"))
|
|
276
|
+
session_entries = []
|
|
277
|
+
for f in files:
|
|
278
|
+
try:
|
|
279
|
+
_safe_session_file(f)
|
|
280
|
+
with open(f, "r", encoding="utf-8") as s_file:
|
|
281
|
+
s_data = json.load(s_file)
|
|
282
|
+
status_icon = "🟢" if not s_data.get("execution_locked", True) else "🛑"
|
|
283
|
+
session_entries.append(
|
|
284
|
+
f"- `{s_data.get('session_name', f.stem)}` {status_icon} | "
|
|
285
|
+
f"**Phase**: {s_data.get('active_phase', 'Unknown')} | "
|
|
286
|
+
f"**Budget**: {s_data.get('time_budget_minutes', 0)}m | "
|
|
287
|
+
f"**File**: `{f.name}`"
|
|
288
|
+
)
|
|
289
|
+
except Exception:
|
|
290
|
+
session_entries.append(f"- `{f.stem}` (Unreadable session file)")
|
|
291
|
+
|
|
292
|
+
listing = "\n".join(session_entries) if session_entries else "- No saved sessions found."
|
|
293
|
+
return (
|
|
294
|
+
f"### 🗂️ Available Fable Sessions in `{SESSIONS_DIR}`\n\n"
|
|
295
|
+
f"{listing}"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fable Engine Research Scraping Action Handlers.
|
|
3
|
+
Exposes zero-cost scrapers for Web, YouTube, Reddit, X, GitHub, and arXiv as individual MCP actions.
|
|
4
|
+
Integrates retrieved evidence into Fable Session epistemic ledger as [HYPOTHESIS] candidate items.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, Dict
|
|
11
|
+
|
|
12
|
+
from fable_engine.scrapers import (
|
|
13
|
+
ResearchResult,
|
|
14
|
+
scrape_arxiv,
|
|
15
|
+
scrape_github,
|
|
16
|
+
scrape_reddit,
|
|
17
|
+
scrape_web,
|
|
18
|
+
scrape_x,
|
|
19
|
+
scrape_youtube,
|
|
20
|
+
)
|
|
21
|
+
from fable_engine.session import ACTIVE_SESSIONS, get_or_load_session
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("fable-engine.actions.scrapers")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_target_from_args(args: Dict[str, Any]) -> str:
|
|
27
|
+
"""Extracts target string from args using common parameter keys."""
|
|
28
|
+
target = args.get("target") or args.get("query") or args.get("url") or args.get("target_resource") or args.get("claim") or ""
|
|
29
|
+
return str(target).strip()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _auto_log_epistemic_if_requested(args: Dict[str, Any], action_name: str, target: str, res: ResearchResult):
|
|
33
|
+
"""
|
|
34
|
+
Optionally logs successful scraped result as a [HYPOTHESIS] epistemic item if requested.
|
|
35
|
+
Failed retrievals (res.ok == False) are strictly excluded from epistemic logging.
|
|
36
|
+
Scraped raw web data is tagged as [HYPOTHESIS] so agents are required to
|
|
37
|
+
cross-check and verify evidence before promoting claims to [PROVEN].
|
|
38
|
+
"""
|
|
39
|
+
auto_log = args.get("auto_log_epistemic") or args.get("auto_log")
|
|
40
|
+
if not auto_log:
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
# Do not auto-log failed retrievals
|
|
44
|
+
if not res.ok:
|
|
45
|
+
logger.debug(f"Skipping auto_log_epistemic for {action_name}: retrieval failed.")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
session_name = args.get("session_name", "").strip()
|
|
49
|
+
if not session_name:
|
|
50
|
+
if ACTIVE_SESSIONS:
|
|
51
|
+
session_name = next(iter(ACTIVE_SESSIONS.keys()))
|
|
52
|
+
else:
|
|
53
|
+
return
|
|
54
|
+
try:
|
|
55
|
+
session = get_or_load_session(session_name)
|
|
56
|
+
claim = f"Retrieved {action_name} research candidate: '{target}' ({res.title or 'Untitled'})"
|
|
57
|
+
evidence_content = res.to_markdown()
|
|
58
|
+
evidence_snippet = evidence_content[:1500] if len(evidence_content) > 1500 else evidence_content
|
|
59
|
+
# Log as HYPOTHESIS candidate evidence
|
|
60
|
+
session.log_epistemic_item("HYPOTHESIS", claim, evidence_snippet)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.warning(f"Could not auto-log epistemic hypothesis for {action_name}: {e}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _handle_scrape_web(args: Dict[str, Any]) -> str:
|
|
66
|
+
target = _get_target_from_args(args)
|
|
67
|
+
if not target:
|
|
68
|
+
return "Error: Missing required parameter 'target' or 'url' or 'query' for scrape_web."
|
|
69
|
+
res = scrape_web(target)
|
|
70
|
+
_auto_log_epistemic_if_requested(args, "scrape_web", target, res)
|
|
71
|
+
return res.to_markdown()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _handle_scrape_youtube(args: Dict[str, Any]) -> str:
|
|
75
|
+
target = _get_target_from_args(args)
|
|
76
|
+
if not target:
|
|
77
|
+
return "Error: Missing required parameter 'target' or 'url' or 'video_id' for scrape_youtube."
|
|
78
|
+
res = scrape_youtube(target)
|
|
79
|
+
_auto_log_epistemic_if_requested(args, "scrape_youtube", target, res)
|
|
80
|
+
return res.to_markdown()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _handle_scrape_reddit(args: Dict[str, Any]) -> str:
|
|
84
|
+
target = _get_target_from_args(args)
|
|
85
|
+
if not target:
|
|
86
|
+
return "Error: Missing required parameter 'target' or 'url' or 'subreddit' for scrape_reddit."
|
|
87
|
+
res = scrape_reddit(target)
|
|
88
|
+
_auto_log_epistemic_if_requested(args, "scrape_reddit", target, res)
|
|
89
|
+
return res.to_markdown()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _handle_scrape_x(args: Dict[str, Any]) -> str:
|
|
93
|
+
target = _get_target_from_args(args)
|
|
94
|
+
if not target:
|
|
95
|
+
return "Error: Missing required parameter 'target' or 'url' or 'handle' for scrape_x."
|
|
96
|
+
res = scrape_x(target)
|
|
97
|
+
_auto_log_epistemic_if_requested(args, "scrape_x", target, res)
|
|
98
|
+
return res.to_markdown()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _handle_scrape_github(args: Dict[str, Any]) -> str:
|
|
102
|
+
target = _get_target_from_args(args)
|
|
103
|
+
if not target:
|
|
104
|
+
return "Error: Missing required parameter 'target' or 'repo' or 'query' for scrape_github."
|
|
105
|
+
res = scrape_github(target)
|
|
106
|
+
_auto_log_epistemic_if_requested(args, "scrape_github", target, res)
|
|
107
|
+
return res.to_markdown()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _handle_scrape_arxiv(args: Dict[str, Any]) -> str:
|
|
111
|
+
target = _get_target_from_args(args)
|
|
112
|
+
if not target:
|
|
113
|
+
return "Error: Missing required parameter 'target' or 'paper_id' or 'query' for scrape_arxiv."
|
|
114
|
+
res = scrape_arxiv(target)
|
|
115
|
+
_auto_log_epistemic_if_requested(args, "scrape_arxiv", target, res)
|
|
116
|
+
return res.to_markdown()
|