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.
Files changed (104) hide show
  1. fable_compressor.py +356 -0
  2. fable_engine/__init__.py +1 -0
  3. fable_engine/actions/__init__.py +291 -0
  4. fable_engine/actions/cas.py +182 -0
  5. fable_engine/actions/deliberation.py +523 -0
  6. fable_engine/actions/fleet.py +807 -0
  7. fable_engine/actions/lifecycle.py +298 -0
  8. fable_engine/actions/scrapers.py +116 -0
  9. fable_engine/actions/system3.py +815 -0
  10. fable_engine/browser.py +824 -0
  11. fable_engine/cas.py +974 -0
  12. fable_engine/fable_session.json +510 -0
  13. fable_engine/guards.py +283 -0
  14. fable_engine/schema.py +714 -0
  15. fable_engine/scrapers/__init__.py +32 -0
  16. fable_engine/scrapers/arxiv.py +115 -0
  17. fable_engine/scrapers/base.py +386 -0
  18. fable_engine/scrapers/github.py +129 -0
  19. fable_engine/scrapers/reddit.py +154 -0
  20. fable_engine/scrapers/web.py +120 -0
  21. fable_engine/scrapers/x.py +125 -0
  22. fable_engine/scrapers/youtube.py +132 -0
  23. fable_engine/server.py +414 -0
  24. fable_engine/session.py +1819 -0
  25. fable_engine/test_server.py +1362 -0
  26. fable_engine/updater.py +541 -0
  27. fable_engine-1.3.1.dist-info/LICENSE +22 -0
  28. fable_engine-1.3.1.dist-info/METADATA +173 -0
  29. fable_engine-1.3.1.dist-info/RECORD +104 -0
  30. fable_engine-1.3.1.dist-info/WHEEL +5 -0
  31. fable_engine-1.3.1.dist-info/entry_points.txt +5 -0
  32. fable_engine-1.3.1.dist-info/top_level.txt +6 -0
  33. fable_mode/__init__.py +3 -0
  34. fable_mode/__main__.py +4 -0
  35. fable_mode/adapters.py +1014 -0
  36. fable_mode/installer.py +553 -0
  37. fable_mode/launcher.py +437 -0
  38. fable_mode/manifest.py +142 -0
  39. fable_mode/resources.json +114 -0
  40. fable_mode/safety.py +103 -0
  41. fable_mode_entry.py +10 -0
  42. fable_v2/__init__.py +146 -0
  43. fable_v2/adapters.py +151 -0
  44. fable_v2/coder_fleet/__init__.py +100 -0
  45. fable_v2/coder_fleet/ast_tools.py +158 -0
  46. fable_v2/coder_fleet/compute.py +199 -0
  47. fable_v2/coder_fleet/design_engine.py +1316 -0
  48. fable_v2/coder_fleet/diagnostics.py +293 -0
  49. fable_v2/coder_fleet/fleet_dispatcher.py +214 -0
  50. fable_v2/coder_fleet/mock_auditor.py +306 -0
  51. fable_v2/coder_fleet/mutation.py +216 -0
  52. fable_v2/coder_fleet/property_oracle.py +260 -0
  53. fable_v2/coder_fleet/receipt_attestor.py +122 -0
  54. fable_v2/coder_fleet/red_team_swarm.py +908 -0
  55. fable_v2/coder_fleet/test_harness.py +198 -0
  56. fable_v2/coder_fleet/vector_engine.py +1287 -0
  57. fable_v2/coder_fleet/visual.py +357 -0
  58. fable_v2/coder_fleet/workspace.py +153 -0
  59. fable_v2/cortical/__init__.py +20 -0
  60. fable_v2/cortical/plasticity_engine.py +992 -0
  61. fable_v2/execution_broker.py +811 -0
  62. fable_v2/proof_engine.py +1141 -0
  63. fable_v2/protocol.py +485 -0
  64. fable_v2/runtime.py +1010 -0
  65. fable_v2/system3/__init__.py +204 -0
  66. fable_v2/system3/causal.py +558 -0
  67. fable_v2/system3/dialectical.py +577 -0
  68. fable_v2/system3/evolution.py +503 -0
  69. fable_v2/system3/executive.py +338 -0
  70. fable_v2/system3/free_energy.py +479 -0
  71. fable_v2/system3/hyperbolic.py +555 -0
  72. fable_v2/system3/induction.py +336 -0
  73. fable_v2/system3/kripke.py +548 -0
  74. fable_v2/system3/oracle.py +745 -0
  75. fable_v2/verifiers.py +72 -0
  76. tests/__init__.py +1 -0
  77. tests/test_anti_loop_circuit_breaker.py +64 -0
  78. tests/test_auto_updater.py +407 -0
  79. tests/test_coder_fleet.py +535 -0
  80. tests/test_delegation_compiler.py +54 -0
  81. tests/test_descriptor_boundaries.py +126 -0
  82. tests/test_design_engine.py +603 -0
  83. tests/test_epistemic_evidence_validator.py +66 -0
  84. tests/test_execution_broker.py +233 -0
  85. tests/test_fable_v2.py +406 -0
  86. tests/test_fleet_transitions.py +116 -0
  87. tests/test_fsm_redteam_evolution.py +406 -0
  88. tests/test_goal_rubric_and_pipeline.py +367 -0
  89. tests/test_hebbian_plasticity.py +585 -0
  90. tests/test_packaging_runtime.py +194 -0
  91. tests/test_proof_engine.py +259 -0
  92. tests/test_red_team_swarm.py +645 -0
  93. tests/test_redteam_remediation.py +169 -0
  94. tests/test_registration_transaction.py +375 -0
  95. tests/test_requested_regressions.py +467 -0
  96. tests/test_scrapers.py +370 -0
  97. tests/test_server_actions.py +93 -0
  98. tests/test_server_frontier_actions.py +269 -0
  99. tests/test_server_protocol.py +88 -0
  100. tests/test_stealth_browser.py +970 -0
  101. tests/test_system3.py +381 -0
  102. tests/test_system3_deep_integration.py +385 -0
  103. tests/test_system3_frontier.py +436 -0
  104. 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()